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