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