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