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