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