DeliveryReceiptManager.java

  1. /**
  2.  *
  3.  * Copyright 2013-2014 Georg Lukas, 2015-2020 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.receipts;

  18. import java.util.Map;
  19. import java.util.Set;
  20. import java.util.WeakHashMap;
  21. import java.util.concurrent.CopyOnWriteArraySet;
  22. import java.util.logging.Logger;

  23. import org.jivesoftware.smack.ConnectionCreationListener;
  24. import org.jivesoftware.smack.Manager;
  25. import org.jivesoftware.smack.SmackException;
  26. import org.jivesoftware.smack.SmackException.NotConnectedException;
  27. import org.jivesoftware.smack.StanzaListener;
  28. import org.jivesoftware.smack.XMPPConnection;
  29. import org.jivesoftware.smack.XMPPConnectionRegistry;
  30. import org.jivesoftware.smack.XMPPException;
  31. import org.jivesoftware.smack.filter.AndFilter;
  32. import org.jivesoftware.smack.filter.MessageTypeFilter;
  33. import org.jivesoftware.smack.filter.MessageWithBodiesFilter;
  34. import org.jivesoftware.smack.filter.NotFilter;
  35. import org.jivesoftware.smack.filter.StanzaExtensionFilter;
  36. import org.jivesoftware.smack.filter.StanzaFilter;
  37. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  38. import org.jivesoftware.smack.packet.Message;
  39. import org.jivesoftware.smack.packet.Stanza;
  40. import org.jivesoftware.smack.packet.StanzaBuilder;
  41. import org.jivesoftware.smack.roster.Roster;
  42. import org.jivesoftware.smack.util.StringUtils;

  43. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;

  44. import org.jxmpp.jid.Jid;

  45. /**
  46.  * Manager for XEP-0184: Message Delivery Receipts. This class implements
  47.  * the manager for {@link DeliveryReceipt} support, enabling and disabling of
  48.  * automatic DeliveryReceipt transmission.
  49.  *
  50.  * <p>
  51.  * You can send delivery receipt requests and listen for incoming delivery receipts as shown in this example:
  52.  * </p>
  53.  * <pre>
  54.  * deliveryReceiptManager.addReceiptReceivedListener(new ReceiptReceivedListener() {
  55.  *   void onReceiptReceived(String fromJid, String toJid, String receiptId, Stanza receipt) {
  56.  *     // If the receiving entity does not support delivery receipts,
  57.  *     // then the receipt received listener may not get invoked.
  58.  *   }
  59.  * });
  60.  * Message message = …
  61.  * DeliveryReceiptRequest.addTo(message);
  62.  * connection.sendStanza(message);
  63.  * </pre>
  64.  *
  65.  * DeliveryReceiptManager can be configured to automatically add delivery receipt requests to every
  66.  * message with {@link #autoAddDeliveryReceiptRequests()}.
  67.  *
  68.  * @author Georg Lukas
  69.  * @see <a href="http://xmpp.org/extensions/xep-0184.html">XEP-0184: Message Delivery Receipts</a>
  70.  */
  71. public final class DeliveryReceiptManager extends Manager {

  72.     /**
  73.      * Filters all non-error messages with receipt requests.
  74.      * See <a href="https://xmpp.org/extensions/xep-0184.html#when">XEP-0184 § 5.</a> "A sender could request receipts
  75.      * on any non-error content message (chat, groupchat, headline, or normal)…"
  76.      */
  77.     private static final StanzaFilter NON_ERROR_GROUPCHAT_MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST = new AndFilter(
  78.             StanzaTypeFilter.MESSAGE,
  79.             new StanzaExtensionFilter(new DeliveryReceiptRequest()),
  80.             new NotFilter(MessageTypeFilter.ERROR));

  81.     private static final StanzaFilter MESSAGES_WITH_DELIVERY_RECEIPT = new AndFilter(StanzaTypeFilter.MESSAGE,
  82.                     new StanzaExtensionFilter(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE));

  83.     private static final Logger LOGGER = Logger.getLogger(DeliveryReceiptManager.class.getName());

  84.     private static final Map<XMPPConnection, DeliveryReceiptManager> instances = new WeakHashMap<>();

  85.     static {
  86.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  87.             @Override
  88.             public void connectionCreated(XMPPConnection connection) {
  89.                 getInstanceFor(connection);
  90.             }
  91.         });
  92.     }

  93.     /**
  94.      * Specifies when incoming message delivery receipt requests should be automatically
  95.      * acknowledged with an receipt.
  96.      */
  97.     public enum AutoReceiptMode {

  98.         /**
  99.          * Never send deliver receipts.
  100.          */
  101.         disabled,

  102.         /**
  103.          * Only send delivery receipts if the requester is subscribed to our presence.
  104.          */
  105.         ifIsSubscribed,

  106.         /**
  107.          * Always send delivery receipts. <b>Warning:</b> this may causes presence leaks. See <a
  108.          * href="http://xmpp.org/extensions/xep-0184.html#security">XEP-0184: Message Delivery
  109.          * Receipts § 8. Security Considerations</a>
  110.          */
  111.         always,
  112.     }

  113.     private static AutoReceiptMode defaultAutoReceiptMode = AutoReceiptMode.ifIsSubscribed;

  114.     /**
  115.      * Set the default automatic receipt mode for new connections.
  116.      *
  117.      * @param autoReceiptMode the default automatic receipt mode.
  118.      */
  119.     public static void setDefaultAutoReceiptMode(AutoReceiptMode autoReceiptMode) {
  120.         defaultAutoReceiptMode = autoReceiptMode;
  121.     }

  122.     private AutoReceiptMode autoReceiptMode = defaultAutoReceiptMode;

  123.     private final Set<ReceiptReceivedListener> receiptReceivedListeners = new CopyOnWriteArraySet<ReceiptReceivedListener>();

  124.     private DeliveryReceiptManager(XMPPConnection connection) {
  125.         super(connection);
  126.         ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
  127.         sdm.addFeature(DeliveryReceipt.NAMESPACE);

  128.         // Add the packet listener to handling incoming delivery receipts
  129.         connection.addAsyncStanzaListener(new StanzaListener() {
  130.             @Override
  131.             public void processStanza(Stanza packet) throws NotConnectedException {
  132.                 DeliveryReceipt dr = DeliveryReceipt.from((Message) packet);
  133.                 // notify listeners of incoming receipt
  134.                 for (ReceiptReceivedListener l : receiptReceivedListeners) {
  135.                     l.onReceiptReceived(packet.getFrom(), packet.getTo(), dr.getId(), packet);
  136.                 }
  137.             }
  138.         }, MESSAGES_WITH_DELIVERY_RECEIPT);

  139.         // Add the packet listener to handle incoming delivery receipt requests
  140.         connection.addAsyncStanzaListener(new StanzaListener() {
  141.             @Override
  142.             public void processStanza(Stanza packet) throws NotConnectedException, InterruptedException {
  143.                 final Jid from = packet.getFrom();
  144.                 final XMPPConnection connection = connection();
  145.                 switch (autoReceiptMode) {
  146.                 case disabled:
  147.                     return;
  148.                 case ifIsSubscribed:
  149.                     if (!Roster.getInstanceFor(connection).isSubscribedToMyPresence(from)) {
  150.                         return;
  151.                     }
  152.                     break;
  153.                 case always:
  154.                     break;
  155.                 }

  156.                 final Message messageWithReceiptRequest = (Message) packet;
  157.                 Message ack = receiptMessageFor(messageWithReceiptRequest);
  158.                 if (ack == null) {
  159.                     LOGGER.warning("Received message stanza with receipt request from '" + from
  160.                                     + "' without a stanza ID set. Message: " + messageWithReceiptRequest);
  161.                     return;
  162.                 }
  163.                 connection.sendStanza(ack);
  164.             }
  165.         }, NON_ERROR_GROUPCHAT_MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST);
  166.     }

  167.     /**
  168.      * Obtain the DeliveryReceiptManager responsible for a connection.
  169.      *
  170.      * @param connection the connection object.
  171.      *
  172.      * @return the DeliveryReceiptManager instance for the given connection
  173.      */
  174.      public static synchronized DeliveryReceiptManager getInstanceFor(XMPPConnection connection) {
  175.         DeliveryReceiptManager receiptManager = instances.get(connection);

  176.         if (receiptManager == null) {
  177.             receiptManager = new DeliveryReceiptManager(connection);
  178.             instances.put(connection, receiptManager);
  179.         }

  180.         return receiptManager;
  181.     }

  182.     /**
  183.      * Returns true if Delivery Receipts are supported by a given JID.
  184.      *
  185.      * @param jid TODO javadoc me please
  186.      * @return true if supported
  187.      * @throws SmackException if there was no response from the server.
  188.      * @throws XMPPException if an XMPP protocol error was received.
  189.      * @throws InterruptedException if the calling thread was interrupted.
  190.      */
  191.     public boolean isSupported(Jid jid) throws SmackException, XMPPException, InterruptedException {
  192.         return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid,
  193.                         DeliveryReceipt.NAMESPACE);
  194.     }

  195.     /**
  196.      * Configure whether the {@link DeliveryReceiptManager} should automatically
  197.      * reply to incoming {@link DeliveryReceipt}s.
  198.      *
  199.      * @param autoReceiptMode the new auto receipt mode.
  200.      * @see AutoReceiptMode
  201.      */
  202.     public void setAutoReceiptMode(AutoReceiptMode autoReceiptMode) {
  203.         this.autoReceiptMode = autoReceiptMode;
  204.     }

  205.     /**
  206.      * Get the currently active auto receipt mode.
  207.      *
  208.      * @return the currently active auto receipt mode.
  209.      */
  210.     public AutoReceiptMode getAutoReceiptMode() {
  211.         return autoReceiptMode;
  212.     }

  213.     /**
  214.      * Get informed about incoming delivery receipts with a {@link ReceiptReceivedListener}.
  215.      *
  216.      * @param listener the listener to be informed about new receipts
  217.      */
  218.     public void addReceiptReceivedListener(ReceiptReceivedListener listener) {
  219.         receiptReceivedListeners.add(listener);
  220.     }

  221.     /**
  222.      * Stop getting informed about incoming delivery receipts.
  223.      *
  224.      * @param listener the listener to be removed
  225.      */
  226.     public void removeReceiptReceivedListener(ReceiptReceivedListener listener) {
  227.         receiptReceivedListeners.remove(listener);
  228.     }

  229.     /**
  230.      * A filter for stanzas to request delivery receipts for. Notably those are message stanzas of type normal, chat or
  231.      * headline, which <b>do not</b>contain a delivery receipt, i.e. are ack messages, and have a body extension.
  232.      *
  233.      * @see <a href="http://xmpp.org/extensions/xep-0184.html#when-ack">XEP-184 § 5.4 Ack Messages</a>
  234.      */
  235.     private static final StanzaFilter MESSAGES_TO_REQUEST_RECEIPTS_FOR = new AndFilter(
  236.                     // @formatter:off
  237.                     MessageTypeFilter.NORMAL_OR_CHAT_OR_HEADLINE,
  238.                     new NotFilter(new StanzaExtensionFilter(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE)),
  239.                     MessageWithBodiesFilter.INSTANCE
  240.                     );
  241.                    // @formatter:on

  242.     /**
  243.      * Enables automatic requests of delivery receipts for outgoing messages of
  244.      * {@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
  245.      * with a {@link org.jivesoftware.smack.packet.Message.Body} extension.
  246.      *
  247.      * @since 4.1
  248.      * @see #dontAutoAddDeliveryReceiptRequests()
  249.      */
  250.     public void autoAddDeliveryReceiptRequests() {
  251.         connection().addMessageInterceptor(DeliveryReceiptRequest::addTo, m -> {
  252.             return MESSAGES_TO_REQUEST_RECEIPTS_FOR.accept(m);
  253.         });
  254.     }

  255.     /**
  256.      * Disables automatically requests of delivery receipts for outgoing messages.
  257.      *
  258.      * @since 4.1
  259.      * @see #autoAddDeliveryReceiptRequests()
  260.      */
  261.     public void dontAutoAddDeliveryReceiptRequests() {
  262.         connection().removeMessageInterceptor(DeliveryReceiptRequest::addTo);
  263.     }

  264.     /**
  265.      * Test if a message requires a delivery receipt.
  266.      *
  267.      * @param message Stanza object to check for a DeliveryReceiptRequest
  268.      *
  269.      * @return true if a delivery receipt was requested
  270.      */
  271.     public static boolean hasDeliveryReceiptRequest(Message message) {
  272.         return DeliveryReceiptRequest.from(message) != null;
  273.     }

  274.     /**
  275.      * Add a delivery receipt request to an outgoing packet.
  276.      *
  277.      * Only message packets may contain receipt requests as of XEP-0184,
  278.      * therefore only allow Message as the parameter type.
  279.      *
  280.      * @param m Message object to add a request to
  281.      * @return the Message ID which will be used as receipt ID
  282.      * @deprecated use {@link DeliveryReceiptRequest#addTo(Message)}
  283.      */
  284.     @Deprecated
  285.     public static String addDeliveryReceiptRequest(Message m) {
  286.         return DeliveryReceiptRequest.addTo(m);
  287.     }

  288.     /**
  289.      * Create and return a new message including a delivery receipt extension for the given message.
  290.      * <p>
  291.      * If {@code messageWithReceiptRequest} does not have a Stanza ID set, then {@code null} will be returned.
  292.      * </p>
  293.      *
  294.      * @param messageWithReceiptRequest the given message with a receipt request extension.
  295.      * @return a new message with a receipt or <code>null</code>.
  296.      * @since 4.1
  297.      */
  298.     public static Message receiptMessageFor(Message messageWithReceiptRequest) {
  299.         String stanzaId = messageWithReceiptRequest.getStanzaId();
  300.         if (StringUtils.isNullOrEmpty(stanzaId)) {
  301.             return null;
  302.         }
  303.         Message message = StanzaBuilder.buildMessage()
  304.                 .ofType(messageWithReceiptRequest.getType())
  305.                 .to(messageWithReceiptRequest.getFrom())
  306.                 .addExtension(new DeliveryReceipt(stanzaId))
  307.                 .build();
  308.         return message;
  309.     }
  310. }