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