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