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