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