001/**
002 *
003 * Copyright 2016 Fernando Ramirez
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.muclight;
018
019import java.util.HashMap;
020import java.util.List;
021import java.util.Set;
022import java.util.concurrent.CopyOnWriteArraySet;
023
024import org.jivesoftware.smack.MessageListener;
025import org.jivesoftware.smack.SmackException.NoResponseException;
026import org.jivesoftware.smack.SmackException.NotConnectedException;
027import org.jivesoftware.smack.StanzaCollector;
028import org.jivesoftware.smack.StanzaListener;
029import org.jivesoftware.smack.XMPPConnection;
030import org.jivesoftware.smack.XMPPException.XMPPErrorException;
031import org.jivesoftware.smack.chat.ChatMessageListener;
032import org.jivesoftware.smack.filter.AndFilter;
033import org.jivesoftware.smack.filter.FromMatchesFilter;
034import org.jivesoftware.smack.filter.MessageTypeFilter;
035import org.jivesoftware.smack.filter.StanzaFilter;
036import org.jivesoftware.smack.packet.IQ;
037import org.jivesoftware.smack.packet.Message;
038import org.jivesoftware.smack.packet.MessageBuilder;
039import org.jivesoftware.smack.packet.Stanza;
040
041import org.jivesoftware.smackx.muclight.element.MUCLightAffiliationsIQ;
042import org.jivesoftware.smackx.muclight.element.MUCLightChangeAffiliationsIQ;
043import org.jivesoftware.smackx.muclight.element.MUCLightConfigurationIQ;
044import org.jivesoftware.smackx.muclight.element.MUCLightCreateIQ;
045import org.jivesoftware.smackx.muclight.element.MUCLightDestroyIQ;
046import org.jivesoftware.smackx.muclight.element.MUCLightGetAffiliationsIQ;
047import org.jivesoftware.smackx.muclight.element.MUCLightGetConfigsIQ;
048import org.jivesoftware.smackx.muclight.element.MUCLightGetInfoIQ;
049import org.jivesoftware.smackx.muclight.element.MUCLightInfoIQ;
050import org.jivesoftware.smackx.muclight.element.MUCLightSetConfigsIQ;
051
052import org.jxmpp.jid.EntityJid;
053import org.jxmpp.jid.Jid;
054
055/**
056 * MUCLight class.
057 *
058 * @author Fernando Ramirez
059 */
060public class MultiUserChatLight {
061
062    public static final String NAMESPACE = "urn:xmpp:muclight:0";
063
064    public static final String AFFILIATIONS = "#affiliations";
065    public static final String INFO = "#info";
066    public static final String CONFIGURATION = "#configuration";
067    public static final String CREATE = "#create";
068    public static final String DESTROY = "#destroy";
069    public static final String BLOCKING = "#blocking";
070
071    private final XMPPConnection connection;
072    private final EntityJid room;
073
074    private final Set<MessageListener> messageListeners = new CopyOnWriteArraySet<MessageListener>();
075
076    /**
077     * This filter will match all stanzas send from the groupchat or from one if
078     * the groupchat occupants.
079     */
080    private final StanzaFilter fromRoomFilter;
081
082    /**
083     * Same as {@link #fromRoomFilter} together with
084     * {@link MessageTypeFilter#GROUPCHAT}.
085     */
086    private final StanzaFilter fromRoomGroupChatFilter;
087
088    private final StanzaListener messageListener;
089
090    private StanzaCollector messageCollector;
091
092    MultiUserChatLight(XMPPConnection connection, EntityJid room) {
093        this.connection = connection;
094        this.room = room;
095
096        fromRoomFilter = FromMatchesFilter.create(room);
097        fromRoomGroupChatFilter = new AndFilter(fromRoomFilter, MessageTypeFilter.GROUPCHAT);
098
099        messageListener = new StanzaListener() {
100            @Override
101            public void processStanza(Stanza packet) throws NotConnectedException {
102                Message message = (Message) packet;
103                for (MessageListener listener : messageListeners) {
104                    listener.processMessage(message);
105                }
106            }
107        };
108
109        connection.addSyncStanzaListener(messageListener, fromRoomGroupChatFilter);
110    }
111
112    /**
113     * Returns the JID of the room.
114     *
115     * @return the MUCLight room JID.
116     */
117    public EntityJid getRoom() {
118        return room;
119    }
120
121    /**
122     * Sends a message to the chat room.
123     *
124     * @param text TODO javadoc me please
125     *            the text of the message to send.
126     * @throws NotConnectedException if the XMPP connection is not connected.
127     * @throws InterruptedException if the calling thread was interrupted.
128     */
129    public void sendMessage(String text) throws NotConnectedException, InterruptedException {
130        MessageBuilder message = buildMessage();
131        message.setBody(text);
132        connection.sendStanza(message.build());
133    }
134
135    /**
136     * Returns a new Chat for sending private messages to a given room occupant.
137     * The Chat's occupant address is the room's JID (i.e.
138     * roomName@service/nick). The server service will change the 'from' address
139     * to the sender's room JID and delivering the message to the intended
140     * recipient's full JID.
141     *
142     * @param occupant TODO javadoc me please
143     *            occupant unique room JID (e.g.
144     *            'darkcave@macbeth.shakespeare.lit/Paul').
145     * @param listener TODO javadoc me please
146     *            the listener is a message listener that will handle messages
147     *            for the newly created chat.
148     * @return new Chat for sending private messages to a given room occupant.
149     */
150    @Deprecated
151    // Do not re-use Chat API, which was designed for XMPP-IM 1:1 chats and not MUClight private chats.
152    public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityJid occupant, ChatMessageListener listener) {
153        return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener);
154    }
155
156    /**
157     * Creates a new Message to send to the chat room.
158     *
159     * @return a new Message addressed to the chat room.
160     * @deprecated use {@link #buildMessage()} instead.
161     */
162    @Deprecated
163    // TODO: Remove when stanza builder is ready.
164    public Message createMessage() {
165        return connection.getStanzaFactory().buildMessageStanza()
166                .ofType(Message.Type.groupchat)
167                .to(room)
168                .build();
169    }
170
171    /**
172     * Constructs a new message builder for messages send to this MUC room.
173     *
174     * @return a new message builder.
175     */
176    public MessageBuilder buildMessage() {
177        return connection.getStanzaFactory()
178                .buildMessageStanza()
179                .ofType(Message.Type.groupchat)
180                .to(room)
181                ;
182    }
183
184    /**
185     * Sends a Message to the chat room.
186     *
187     * @param message TODO javadoc me please
188     *            the message.
189     * @throws NotConnectedException if the XMPP connection is not connected.
190     * @throws InterruptedException if the calling thread was interrupted.
191     * @deprecated use {@link #sendMessage(MessageBuilder)} instead.
192     */
193    @Deprecated
194    // TODO: Remove in Smack 4.5.
195    public void sendMessage(Message message) throws NotConnectedException, InterruptedException {
196        sendMessage(message.asBuilder());
197    }
198
199    /**
200     * Sends a Message to the chat room.
201     *
202     * @param messageBuilder the message.
203     * @throws NotConnectedException if the XMPP connection is not connected.
204     * @throws InterruptedException if the calling thread was interrupted.
205     */
206    public void sendMessage(MessageBuilder messageBuilder) throws NotConnectedException, InterruptedException {
207        Message message = messageBuilder.to(room).ofType(Message.Type.groupchat).build();
208        connection.sendStanza(message);
209    }
210
211    /**
212     * Polls for and returns the next message.
213     *
214     * @return the next message if one is immediately available
215     */
216    public Message pollMessage() {
217        return messageCollector.pollResult();
218    }
219
220    /**
221     * Returns the next available message in the chat. The method call will
222     * block (not return) until a message is available.
223     *
224     * @return the next message.
225     * @throws InterruptedException if the calling thread was interrupted.
226     */
227    public Message nextMessage() throws InterruptedException {
228        return messageCollector.nextResult();
229    }
230
231    /**
232     * Returns the next available message in the chat.
233     *
234     * @param timeout TODO javadoc me please
235     *            the maximum amount of time to wait for the next message.
236     * @return the next message, or null if the timeout elapses without a
237     *         message becoming available.
238     * @throws InterruptedException if the calling thread was interrupted.
239     */
240    public Message nextMessage(long timeout) throws InterruptedException {
241        return messageCollector.nextResult(timeout);
242    }
243
244    /**
245     * Adds a stanza listener that will be notified of any new messages
246     * in the group chat. Only "group chat" messages addressed to this group
247     * chat will be delivered to the listener.
248     *
249     * @param listener TODO javadoc me please
250     *            a stanza listener.
251     * @return true if the listener was not already added.
252     */
253    public boolean addMessageListener(MessageListener listener) {
254        return messageListeners.add(listener);
255    }
256
257    /**
258     * Removes a stanza listener that was being notified of any new
259     * messages in the MUCLight. Only "group chat" messages addressed to this
260     * MUCLight were being delivered to the listener.
261     *
262     * @param listener TODO javadoc me please
263     *            a stanza listener.
264     * @return true if the listener was removed, otherwise the listener was not
265     *         added previously.
266     */
267    public boolean removeMessageListener(MessageListener listener) {
268        return messageListeners.remove(listener);
269    }
270
271    /**
272     * Remove the connection callbacks used by this MUC Light from the
273     * connection.
274     */
275    private void removeConnectionCallbacks() {
276        connection.removeSyncStanzaListener(messageListener);
277        if (messageCollector != null) {
278            messageCollector.cancel();
279            messageCollector = null;
280        }
281    }
282
283    @Override
284    public String toString() {
285        return "MUC Light: " + room + "(" + connection.getUser() + ")";
286    }
287
288    /**
289     * Create new MUCLight.
290     *
291     * @param roomName TODO javadoc me please
292     * @param subject TODO javadoc me please
293     * @param customConfigs TODO javadoc me please
294     * @param occupants TODO javadoc me please
295     * @throws Exception TODO javadoc me please
296     */
297    public void create(String roomName, String subject, HashMap<String, String> customConfigs, List<Jid> occupants)
298            throws Exception {
299        MUCLightCreateIQ createMUCLightIQ = new MUCLightCreateIQ(room, roomName, occupants);
300
301        messageCollector = connection.createStanzaCollector(fromRoomGroupChatFilter);
302
303        try {
304            connection.createStanzaCollectorAndSend(createMUCLightIQ).nextResultOrThrow();
305        } catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) {
306            removeConnectionCallbacks();
307            throw e;
308        }
309    }
310
311    /**
312     * Create new MUCLight.
313     *
314     * @param roomName TODO javadoc me please
315     * @param occupants TODO javadoc me please
316     * @throws Exception TODO javadoc me please
317     */
318    public void create(String roomName, List<Jid> occupants) throws Exception {
319        create(roomName, null, null, occupants);
320    }
321
322    /**
323     * Leave the MUCLight.
324     *
325     * @throws NotConnectedException if the XMPP connection is not connected.
326     * @throws InterruptedException if the calling thread was interrupted.
327     * @throws NoResponseException if there was no response from the remote entity.
328     * @throws XMPPErrorException if there was an XMPP error returned.
329     */
330    public void leave() throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException {
331        HashMap<Jid, MUCLightAffiliation> affiliations = new HashMap<>();
332        affiliations.put(connection.getUser(), MUCLightAffiliation.none);
333
334        MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations);
335        IQ responseIq = connection.createStanzaCollectorAndSend(changeAffiliationsIQ).nextResultOrThrow();
336        boolean roomLeft = responseIq.getType().equals(IQ.Type.result);
337
338        if (roomLeft) {
339            removeConnectionCallbacks();
340        }
341    }
342
343    /**
344     * Get the MUC Light info.
345     *
346     * @param version TODO javadoc me please
347     * @return the room info
348     * @throws NoResponseException if there was no response from the remote entity.
349     * @throws XMPPErrorException if there was an XMPP error returned.
350     * @throws NotConnectedException if the XMPP connection is not connected.
351     * @throws InterruptedException if the calling thread was interrupted.
352     */
353    public MUCLightRoomInfo getFullInfo(String version)
354            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
355        MUCLightGetInfoIQ mucLightGetInfoIQ = new MUCLightGetInfoIQ(room, version);
356
357        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetInfoIQ).nextResultOrThrow();
358        MUCLightInfoIQ mucLightInfoResponseIQ = (MUCLightInfoIQ) responseIq;
359
360        return new MUCLightRoomInfo(mucLightInfoResponseIQ.getVersion(), room,
361                mucLightInfoResponseIQ.getConfiguration(), mucLightInfoResponseIQ.getOccupants());
362    }
363
364    /**
365     * Get the MUC Light info.
366     *
367     * @return the room info
368     * @throws NoResponseException if there was no response from the remote entity.
369     * @throws XMPPErrorException if there was an XMPP error returned.
370     * @throws NotConnectedException if the XMPP connection is not connected.
371     * @throws InterruptedException if the calling thread was interrupted.
372     */
373    public MUCLightRoomInfo getFullInfo()
374            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
375        return getFullInfo(null);
376    }
377
378    /**
379     * Get the MUC Light configuration.
380     *
381     * @param version TODO javadoc me please
382     * @return the room configuration
383     * @throws NoResponseException if there was no response from the remote entity.
384     * @throws XMPPErrorException if there was an XMPP error returned.
385     * @throws NotConnectedException if the XMPP connection is not connected.
386     * @throws InterruptedException if the calling thread was interrupted.
387     */
388    public MUCLightRoomConfiguration getConfiguration(String version)
389            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
390        MUCLightGetConfigsIQ mucLightGetConfigsIQ = new MUCLightGetConfigsIQ(room, version);
391        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetConfigsIQ).nextResultOrThrow();
392        MUCLightConfigurationIQ mucLightConfigurationIQ = (MUCLightConfigurationIQ) responseIq;
393        return mucLightConfigurationIQ.getConfiguration();
394    }
395
396    /**
397     * Get the MUC Light configuration.
398     *
399     * @return the room configuration
400     * @throws NoResponseException if there was no response from the remote entity.
401     * @throws XMPPErrorException if there was an XMPP error returned.
402     * @throws NotConnectedException if the XMPP connection is not connected.
403     * @throws InterruptedException if the calling thread was interrupted.
404     */
405    public MUCLightRoomConfiguration getConfiguration()
406            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
407        return getConfiguration(null);
408    }
409
410    /**
411     * Get the MUC Light affiliations.
412     *
413     * @param version TODO javadoc me please
414     * @return the room affiliations
415     * @throws NoResponseException if there was no response from the remote entity.
416     * @throws XMPPErrorException if there was an XMPP error returned.
417     * @throws NotConnectedException if the XMPP connection is not connected.
418     * @throws InterruptedException if the calling thread was interrupted.
419     */
420    public HashMap<Jid, MUCLightAffiliation> getAffiliations(String version)
421            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
422        MUCLightGetAffiliationsIQ mucLightGetAffiliationsIQ = new MUCLightGetAffiliationsIQ(room, version);
423
424        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetAffiliationsIQ).nextResultOrThrow();
425        MUCLightAffiliationsIQ mucLightAffiliationsIQ = (MUCLightAffiliationsIQ) responseIq;
426
427        return mucLightAffiliationsIQ.getAffiliations();
428    }
429
430    /**
431     * Get the MUC Light affiliations.
432     *
433     * @return the room affiliations
434     * @throws NoResponseException if there was no response from the remote entity.
435     * @throws XMPPErrorException if there was an XMPP error returned.
436     * @throws NotConnectedException if the XMPP connection is not connected.
437     * @throws InterruptedException if the calling thread was interrupted.
438     */
439    public HashMap<Jid, MUCLightAffiliation> getAffiliations()
440            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
441        return getAffiliations(null);
442    }
443
444    /**
445     * Change the MUC Light affiliations.
446     *
447     * @param affiliations TODO javadoc me please
448     * @throws NoResponseException if there was no response from the remote entity.
449     * @throws XMPPErrorException if there was an XMPP error returned.
450     * @throws NotConnectedException if the XMPP connection is not connected.
451     * @throws InterruptedException if the calling thread was interrupted.
452     */
453    public void changeAffiliations(HashMap<Jid, MUCLightAffiliation> affiliations)
454            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
455        MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations);
456        connection.createStanzaCollectorAndSend(changeAffiliationsIQ).nextResultOrThrow();
457    }
458
459    /**
460     * Destroy the MUC Light. Only will work if it is requested by the owner.
461     *
462     * @throws NoResponseException if there was no response from the remote entity.
463     * @throws XMPPErrorException if there was an XMPP error returned.
464     * @throws NotConnectedException if the XMPP connection is not connected.
465     * @throws InterruptedException if the calling thread was interrupted.
466     */
467    public void destroy() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
468        MUCLightDestroyIQ mucLightDestroyIQ = new MUCLightDestroyIQ(room);
469        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightDestroyIQ).nextResultOrThrow();
470        boolean roomDestroyed = responseIq.getType().equals(IQ.Type.result);
471
472        if (roomDestroyed) {
473            removeConnectionCallbacks();
474        }
475    }
476
477    /**
478     * Change the subject of the MUC Light.
479     *
480     * @param subject TODO javadoc me please
481     * @throws NoResponseException if there was no response from the remote entity.
482     * @throws XMPPErrorException if there was an XMPP error returned.
483     * @throws NotConnectedException if the XMPP connection is not connected.
484     * @throws InterruptedException if the calling thread was interrupted.
485     */
486    public void changeSubject(String subject)
487            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
488        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, null, subject, null);
489        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
490    }
491
492    /**
493     * Change the name of the room.
494     *
495     * @param roomName TODO javadoc me please
496     * @throws NoResponseException if there was no response from the remote entity.
497     * @throws XMPPErrorException if there was an XMPP error returned.
498     * @throws NotConnectedException if the XMPP connection is not connected.
499     * @throws InterruptedException if the calling thread was interrupted.
500     */
501    public void changeRoomName(String roomName)
502            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
503        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, null);
504        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
505    }
506
507    /**
508     * Set the room configurations.
509     *
510     * @param customConfigs TODO javadoc me please
511     * @throws NoResponseException if there was no response from the remote entity.
512     * @throws XMPPErrorException if there was an XMPP error returned.
513     * @throws NotConnectedException if the XMPP connection is not connected.
514     * @throws InterruptedException if the calling thread was interrupted.
515     */
516    public void setRoomConfigs(HashMap<String, String> customConfigs)
517            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
518        setRoomConfigs(null, customConfigs);
519    }
520
521    /**
522     * Set the room configurations.
523     *
524     * @param roomName TODO javadoc me please
525     * @param customConfigs TODO javadoc me please
526     * @throws NoResponseException if there was no response from the remote entity.
527     * @throws XMPPErrorException if there was an XMPP error returned.
528     * @throws NotConnectedException if the XMPP connection is not connected.
529     * @throws InterruptedException if the calling thread was interrupted.
530     */
531    public void setRoomConfigs(String roomName, HashMap<String, String> customConfigs)
532            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
533        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, customConfigs);
534        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
535    }
536
537}