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