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