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