001/** 002 * 003 * Copyright 2013-2014 Georg Lukas, 2015 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.receipts; 018 019import java.util.Map; 020import java.util.Set; 021import java.util.WeakHashMap; 022import java.util.concurrent.CopyOnWriteArraySet; 023import java.util.logging.Logger; 024 025import org.jivesoftware.smack.SmackException; 026import org.jivesoftware.smack.SmackException.NotConnectedException; 027import org.jivesoftware.smack.XMPPConnection; 028import org.jivesoftware.smack.ConnectionCreationListener; 029import org.jivesoftware.smack.Manager; 030import org.jivesoftware.smack.StanzaListener; 031import org.jivesoftware.smack.XMPPConnectionRegistry; 032import org.jivesoftware.smack.XMPPException; 033import org.jivesoftware.smack.filter.AndFilter; 034import org.jivesoftware.smack.filter.MessageTypeFilter; 035import org.jivesoftware.smack.filter.MessageWithBodiesFilter; 036import org.jivesoftware.smack.filter.NotFilter; 037import org.jivesoftware.smack.filter.StanzaFilter; 038import org.jivesoftware.smack.filter.StanzaExtensionFilter; 039import org.jivesoftware.smack.filter.StanzaTypeFilter; 040import org.jivesoftware.smack.packet.Message; 041import org.jivesoftware.smack.packet.Stanza; 042import org.jivesoftware.smack.roster.Roster; 043import org.jivesoftware.smack.util.StringUtils; 044import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 045 046/** 047 * Manager for XEP-0184: Message Delivery Receipts. This class implements 048 * the manager for {@link DeliveryReceipt} support, enabling and disabling of 049 * automatic DeliveryReceipt transmission. 050 * 051 * <p> 052 * You can send delivery receipt requests and listen for incoming delivery receipts as shown in this example: 053 * </p> 054 * <pre> 055 * deliveryReceiptManager.addReceiptReceivedListener(new ReceiptReceivedListener() { 056 * void onReceiptReceived(String fromJid, String toJid, String receiptId, Stanza(/Packet) receipt) { 057 * // If the receiving entity does not support delivery receipts, 058 * // then the receipt received listener may not get invoked. 059 * } 060 * }); 061 * Message message = … 062 * DeliveryReceiptRequest.addTo(message); 063 * connection.sendStanza(message); 064 * </pre> 065 * 066 * DeliveryReceiptManager can be configured to automatically add delivery receipt requests to every 067 * message with {@link #autoAddDeliveryReceiptRequests()}. 068 * 069 * @author Georg Lukas 070 * @see <a href="http://xmpp.org/extensions/xep-0184.html">XEP-0184: Message Delivery Receipts</a> 071 */ 072public class DeliveryReceiptManager extends Manager { 073 074 private static final StanzaFilter MESSAGES_WITH_DEVLIERY_RECEIPT_REQUEST = new AndFilter(StanzaTypeFilter.MESSAGE, 075 new StanzaExtensionFilter(new DeliveryReceiptRequest())); 076 private static final StanzaFilter MESSAGES_WITH_DELIVERY_RECEIPT = new AndFilter(StanzaTypeFilter.MESSAGE, 077 new StanzaExtensionFilter(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE)); 078 079 private static final Logger LOGGER = Logger.getLogger(DeliveryReceiptManager.class.getName()); 080 081 private static Map<XMPPConnection, DeliveryReceiptManager> instances = new WeakHashMap<XMPPConnection, DeliveryReceiptManager>(); 082 083 static { 084 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 085 public void connectionCreated(XMPPConnection connection) { 086 getInstanceFor(connection); 087 } 088 }); 089 } 090 091 /** 092 * Specifies when incoming message delivery receipt requests should be automatically 093 * acknowledged with an receipt. 094 */ 095 public enum AutoReceiptMode { 096 097 /** 098 * Never send deliver receipts 099 */ 100 disabled, 101 102 /** 103 * Only send delivery receipts if the requester is subscribed to our presence. 104 */ 105 ifIsSubscribed, 106 107 /** 108 * Always send delivery receipts. <b>Warning:</b> this may causes presence leaks. See <a 109 * href="http://xmpp.org/extensions/xep-0184.html#security">XEP-0184: Message Delivery 110 * Receipts § 8. Security Considerations</a> 111 */ 112 always, 113 } 114 115 private static AutoReceiptMode defaultAutoReceiptMode = AutoReceiptMode.ifIsSubscribed; 116 117 /** 118 * Set the default automatic receipt mode for new connections. 119 * 120 * @param autoReceiptMode the default automatic receipt mode. 121 */ 122 public static void setDefaultAutoReceiptMode(AutoReceiptMode autoReceiptMode) { 123 defaultAutoReceiptMode = autoReceiptMode; 124 } 125 126 private AutoReceiptMode autoReceiptMode = defaultAutoReceiptMode; 127 128 private final Set<ReceiptReceivedListener> receiptReceivedListeners = new CopyOnWriteArraySet<ReceiptReceivedListener>(); 129 130 private DeliveryReceiptManager(XMPPConnection connection) { 131 super(connection); 132 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); 133 sdm.addFeature(DeliveryReceipt.NAMESPACE); 134 135 // Add the packet listener to handling incoming delivery receipts 136 connection.addAsyncStanzaListener(new StanzaListener() { 137 @Override 138 public void processPacket(Stanza packet) throws NotConnectedException { 139 DeliveryReceipt dr = DeliveryReceipt.from((Message) packet); 140 // notify listeners of incoming receipt 141 for (ReceiptReceivedListener l : receiptReceivedListeners) { 142 l.onReceiptReceived(packet.getFrom(), packet.getTo(), dr.getId(), packet); 143 } 144 } 145 }, MESSAGES_WITH_DELIVERY_RECEIPT); 146 147 // Add the packet listener to handle incoming delivery receipt requests 148 connection.addAsyncStanzaListener(new StanzaListener() { 149 @Override 150 public void processPacket(Stanza packet) throws NotConnectedException { 151 final String from = packet.getFrom(); 152 final XMPPConnection connection = connection(); 153 switch (autoReceiptMode) { 154 case disabled: 155 return; 156 case ifIsSubscribed: 157 if (!Roster.getInstanceFor(connection).isSubscribedToMyPresence(from)) { 158 return; 159 } 160 break; 161 case always: 162 break; 163 } 164 165 final Message messageWithReceiptRequest = (Message) packet; 166 Message ack = receiptMessageFor(messageWithReceiptRequest); 167 if (ack == null) { 168 LOGGER.warning("Received message stanza with receipt request from '" + from 169 + "' without a stanza ID set. Message: " + messageWithReceiptRequest); 170 return; 171 } 172 connection.sendStanza(ack); 173 } 174 }, MESSAGES_WITH_DEVLIERY_RECEIPT_REQUEST); 175 } 176 177 /** 178 * Obtain the DeliveryReceiptManager responsible for a connection. 179 * 180 * @param connection the connection object. 181 * 182 * @return the DeliveryReceiptManager instance for the given connection 183 */ 184 public static synchronized DeliveryReceiptManager getInstanceFor(XMPPConnection connection) { 185 DeliveryReceiptManager receiptManager = instances.get(connection); 186 187 if (receiptManager == null) { 188 receiptManager = new DeliveryReceiptManager(connection); 189 instances.put(connection, receiptManager); 190 } 191 192 return receiptManager; 193 } 194 195 /** 196 * Returns true if Delivery Receipts are supported by a given JID 197 * 198 * @param jid 199 * @return true if supported 200 * @throws SmackException if there was no response from the server. 201 * @throws XMPPException 202 */ 203 public boolean isSupported(String jid) throws SmackException, XMPPException { 204 return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, 205 DeliveryReceipt.NAMESPACE); 206 } 207 208 /** 209 * Configure whether the {@link DeliveryReceiptManager} should automatically 210 * reply to incoming {@link DeliveryReceipt}s. 211 * 212 * @param autoReceiptMode the new auto receipt mode. 213 * @see AutoReceiptMode 214 */ 215 public void setAutoReceiptMode(AutoReceiptMode autoReceiptMode) { 216 this.autoReceiptMode = autoReceiptMode; 217 } 218 219 /** 220 * Get the currently active auto receipt mode. 221 * 222 * @return the currently active auto receipt mode. 223 */ 224 public AutoReceiptMode getAutoReceiptMode() { 225 return autoReceiptMode; 226 } 227 228 /** 229 * Get informed about incoming delivery receipts with a {@link ReceiptReceivedListener}. 230 * 231 * @param listener the listener to be informed about new receipts 232 */ 233 public void addReceiptReceivedListener(ReceiptReceivedListener listener) { 234 receiptReceivedListeners.add(listener); 235 } 236 237 /** 238 * Stop getting informed about incoming delivery receipts. 239 * 240 * @param listener the listener to be removed 241 */ 242 public void removeReceiptReceivedListener(ReceiptReceivedListener listener) { 243 receiptReceivedListeners.remove(listener); 244 } 245 246 /** 247 * A filter for stanzas to request delivery receipts for. Notably those are message stanzas of type normal, chat or 248 * headline, which <b>do not</b>contain a delivery receipt, i.e. are ack messages, and have a body extension. 249 * 250 * @see <a href="http://xmpp.org/extensions/xep-0184.html#when-ack">XEP-184 § 5.4 Ack Messages</a> 251 */ 252 private static final StanzaFilter MESSAGES_TO_REQUEST_RECEIPTS_FOR = new AndFilter( 253 // @formatter:off 254 MessageTypeFilter.NORMAL_OR_CHAT_OR_HEADLINE, 255 new NotFilter(new StanzaExtensionFilter(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE)), 256 MessageWithBodiesFilter.INSTANCE 257 ); 258 // @formatter:on 259 260 private static final StanzaListener AUTO_ADD_DELIVERY_RECEIPT_REQUESTS_LISTENER = new StanzaListener() { 261 @Override 262 public void processPacket(Stanza packet) throws NotConnectedException { 263 Message message = (Message) packet; 264 DeliveryReceiptRequest.addTo(message); 265 } 266 }; 267 268 /** 269 * Enables automatic requests of delivery receipts for outgoing messages of 270 * {@link Message.Type#normal}, {@link Message.Type#chat} or {@link Message.Type#headline}, and 271 * with a {@link Message.Body} extension. 272 * 273 * @since 4.1 274 * @see #dontAutoAddDeliveryReceiptRequests() 275 */ 276 public void autoAddDeliveryReceiptRequests() { 277 connection().addPacketInterceptor(AUTO_ADD_DELIVERY_RECEIPT_REQUESTS_LISTENER, 278 MESSAGES_TO_REQUEST_RECEIPTS_FOR); 279 } 280 281 /** 282 * Disables automatically requests of delivery receipts for outgoing messages. 283 * 284 * @since 4.1 285 * @see #autoAddDeliveryReceiptRequests() 286 */ 287 public void dontAutoAddDeliveryReceiptRequests() { 288 connection().removePacketInterceptor(AUTO_ADD_DELIVERY_RECEIPT_REQUESTS_LISTENER); 289 } 290 291 /** 292 * Test if a message requires a delivery receipt. 293 * 294 * @param message Stanza(/Packet) object to check for a DeliveryReceiptRequest 295 * 296 * @return true if a delivery receipt was requested 297 */ 298 public static boolean hasDeliveryReceiptRequest(Message message) { 299 return (DeliveryReceiptRequest.from(message) != null); 300 } 301 302 /** 303 * Add a delivery receipt request to an outgoing packet. 304 * 305 * Only message packets may contain receipt requests as of XEP-0184, 306 * therefore only allow Message as the parameter type. 307 * 308 * @param m Message object to add a request to 309 * @return the Message ID which will be used as receipt ID 310 * @deprecated use {@link DeliveryReceiptRequest#addTo(Message)} 311 */ 312 @Deprecated 313 public static String addDeliveryReceiptRequest(Message m) { 314 return DeliveryReceiptRequest.addTo(m); 315 } 316 317 /** 318 * Create and return a new message including a delivery receipt extension for the given message. 319 * <p> 320 * If {@code messageWithReceiptRequest} does not have a Stanza ID set, then {@code null} will be returned. 321 * </p> 322 * 323 * @param messageWithReceiptRequest the given message with a receipt request extension. 324 * @return a new message with a receipt or <code>null</code>. 325 * @since 4.1 326 */ 327 public static Message receiptMessageFor(Message messageWithReceiptRequest) { 328 String stanzaId = messageWithReceiptRequest.getStanzaId(); 329 if (StringUtils.isNullOrEmpty(stanzaId)) { 330 return null; 331 } 332 Message message = new Message(messageWithReceiptRequest.getFrom(), messageWithReceiptRequest.getType()); 333 message.addExtension(new DeliveryReceipt(stanzaId)); 334 return message; 335 } 336}