001/**
002 *
003 * Copyright 2003-2007 Jive Software. 2020-2021 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 */
017
018package org.jivesoftware.smackx.muc;
019
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.concurrent.CopyOnWriteArrayList;
027import java.util.concurrent.CopyOnWriteArraySet;
028import java.util.logging.Level;
029import java.util.logging.Logger;
030
031import org.jivesoftware.smack.MessageListener;
032import org.jivesoftware.smack.PresenceListener;
033import org.jivesoftware.smack.SmackException;
034import org.jivesoftware.smack.SmackException.NoResponseException;
035import org.jivesoftware.smack.SmackException.NotConnectedException;
036import org.jivesoftware.smack.StanzaCollector;
037import org.jivesoftware.smack.StanzaListener;
038import org.jivesoftware.smack.XMPPConnection;
039import org.jivesoftware.smack.XMPPException;
040import org.jivesoftware.smack.XMPPException.XMPPErrorException;
041import org.jivesoftware.smack.chat.ChatMessageListener;
042import org.jivesoftware.smack.filter.AndFilter;
043import org.jivesoftware.smack.filter.FromMatchesFilter;
044import org.jivesoftware.smack.filter.MessageTypeFilter;
045import org.jivesoftware.smack.filter.MessageWithBodiesFilter;
046import org.jivesoftware.smack.filter.MessageWithSubjectFilter;
047import org.jivesoftware.smack.filter.MessageWithThreadFilter;
048import org.jivesoftware.smack.filter.NotFilter;
049import org.jivesoftware.smack.filter.OrFilter;
050import org.jivesoftware.smack.filter.PossibleFromTypeFilter;
051import org.jivesoftware.smack.filter.PresenceTypeFilter;
052import org.jivesoftware.smack.filter.StanzaExtensionFilter;
053import org.jivesoftware.smack.filter.StanzaFilter;
054import org.jivesoftware.smack.filter.StanzaIdFilter;
055import org.jivesoftware.smack.filter.StanzaTypeFilter;
056import org.jivesoftware.smack.filter.ToMatchesFilter;
057import org.jivesoftware.smack.packet.IQ;
058import org.jivesoftware.smack.packet.Message;
059import org.jivesoftware.smack.packet.MessageBuilder;
060import org.jivesoftware.smack.packet.MessageView;
061import org.jivesoftware.smack.packet.Presence;
062import org.jivesoftware.smack.packet.Stanza;
063import org.jivesoftware.smack.util.Objects;
064
065import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
066import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
067import org.jivesoftware.smackx.iqregister.packet.Registration;
068import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException;
069import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException;
070import org.jivesoftware.smackx.muc.MultiUserChatException.MucNotJoinedException;
071import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException;
072import org.jivesoftware.smackx.muc.filter.MUCUserStatusCodeFilter;
073import org.jivesoftware.smackx.muc.packet.Destroy;
074import org.jivesoftware.smackx.muc.packet.MUCAdmin;
075import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
076import org.jivesoftware.smackx.muc.packet.MUCItem;
077import org.jivesoftware.smackx.muc.packet.MUCOwner;
078import org.jivesoftware.smackx.muc.packet.MUCUser;
079import org.jivesoftware.smackx.muc.packet.MUCUser.Status;
080import org.jivesoftware.smackx.xdata.FormField;
081import org.jivesoftware.smackx.xdata.TextSingleFormField;
082import org.jivesoftware.smackx.xdata.form.FillableForm;
083import org.jivesoftware.smackx.xdata.form.Form;
084import org.jivesoftware.smackx.xdata.packet.DataForm;
085
086import org.jxmpp.jid.DomainBareJid;
087import org.jxmpp.jid.EntityBareJid;
088import org.jxmpp.jid.EntityFullJid;
089import org.jxmpp.jid.EntityJid;
090import org.jxmpp.jid.Jid;
091import org.jxmpp.jid.impl.JidCreate;
092import org.jxmpp.jid.parts.Resourcepart;
093
094/**
095 * A MultiUserChat room (XEP-45), created with {@link MultiUserChatManager#getMultiUserChat(EntityBareJid)}.
096 * <p>
097 * A MultiUserChat is a conversation that takes place among many users in a virtual
098 * room. A room could have many occupants with different affiliation and roles.
099 * Possible affiliations are "owner", "admin", "member", and "outcast". Possible roles
100 * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
101 * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
102 * Grant voice, Edit member list, etc.).
103 * </p>
104 * <p>
105 * <b>Note:</b> Make sure to leave the MUC ({@link #leave()}) when you don't need it anymore or
106 * otherwise you may leak the instance.
107 * </p>
108 *
109 * @author Gaston Dombiak
110 * @author Larry Kirschner
111 * @author Florian Schmaus
112 */
113public class MultiUserChat {
114    private static final Logger LOGGER = Logger.getLogger(MultiUserChat.class.getName());
115
116    private final XMPPConnection connection;
117    private final EntityBareJid room;
118    private final MultiUserChatManager multiUserChatManager;
119    private final Map<EntityFullJid, Presence> occupantsMap = new ConcurrentHashMap<>();
120
121    private final Set<InvitationRejectionListener> invitationRejectionListeners = new CopyOnWriteArraySet<InvitationRejectionListener>();
122    private final Set<SubjectUpdatedListener> subjectUpdatedListeners = new CopyOnWriteArraySet<SubjectUpdatedListener>();
123    private final Set<UserStatusListener> userStatusListeners = new CopyOnWriteArraySet<UserStatusListener>();
124    private final Set<ParticipantStatusListener> participantStatusListeners = new CopyOnWriteArraySet<ParticipantStatusListener>();
125    private final Set<MessageListener> messageListeners = new CopyOnWriteArraySet<MessageListener>();
126    private final Set<PresenceListener> presenceListeners = new CopyOnWriteArraySet<PresenceListener>();
127    private final Set<PresenceListener> presenceInterceptors = new CopyOnWriteArraySet<PresenceListener>();
128
129    /**
130     * This filter will match all stanzas send from the groupchat or from one if
131     * the groupchat participants, i.e. it filters only the bare JID of the from
132     * attribute against the JID of the MUC.
133     */
134    private final StanzaFilter fromRoomFilter;
135
136    /**
137     * Same as {@link #fromRoomFilter} together with {@link MessageTypeFilter#GROUPCHAT}.
138     */
139    private final StanzaFilter fromRoomGroupchatFilter;
140
141    private final StanzaListener presenceInterceptor;
142    private final StanzaListener messageListener;
143    private final StanzaListener presenceListener;
144    private final StanzaListener subjectListener;
145
146    private static final StanzaFilter DECLINE_FILTER = new AndFilter(MessageTypeFilter.NORMAL,
147                    new StanzaExtensionFilter(MUCUser.ELEMENT, MUCUser.NAMESPACE));
148    private final StanzaListener declinesListener;
149
150    private String subject;
151    private EntityFullJid myRoomJid;
152    private StanzaCollector messageCollector;
153
154    private DiscoverInfo mucServiceDiscoInfo;
155
156    /**
157     * Used to signal that the reflected self-presence was received <b>and</b> processed by us.
158     */
159    private volatile boolean processedReflectedSelfPresence;
160
161    private CopyOnWriteArrayList<MucMessageInterceptor> messageInterceptors;
162
163    MultiUserChat(XMPPConnection connection, EntityBareJid room, MultiUserChatManager multiUserChatManager) {
164        this.connection = connection;
165        this.room = room;
166        this.multiUserChatManager = multiUserChatManager;
167        this.messageInterceptors = MultiUserChatManager.getMessageInterceptors();
168
169        fromRoomFilter = FromMatchesFilter.create(room);
170        fromRoomGroupchatFilter = new AndFilter(fromRoomFilter, MessageTypeFilter.GROUPCHAT);
171
172        messageListener = new StanzaListener() {
173            @Override
174            public void processStanza(Stanza packet) throws NotConnectedException {
175                final Message message = (Message) packet;
176
177                for (MessageListener listener : messageListeners) {
178                            listener.processMessage(message);
179                }
180            }
181        };
182
183        // Create a listener for subject updates.
184        subjectListener = new StanzaListener() {
185            @Override
186            public void processStanza(Stanza packet) {
187                final Message msg = (Message) packet;
188                final EntityFullJid from = msg.getFrom().asEntityFullJidIfPossible();
189                // Update the room subject
190                subject = msg.getSubject();
191
192                // Fire event for subject updated listeners
193                for (SubjectUpdatedListener listener : subjectUpdatedListeners) {
194                    listener.subjectUpdated(msg.getSubject(), from);
195                }
196            }
197        };
198
199        // Create a listener for all presence updates.
200        presenceListener = new StanzaListener() {
201            @Override
202            public void processStanza(final Stanza packet) {
203                final Presence presence = (Presence) packet;
204                final EntityFullJid from = presence.getFrom().asEntityFullJidIfPossible();
205                if (from == null) {
206                    return;
207                }
208                final EntityFullJid myRoomJID = myRoomJid;
209                final boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
210                final MUCUser mucUser = MUCUser.from(packet);
211
212                switch (presence.getType()) {
213                case available:
214                    Presence oldPresence = occupantsMap.put(from, presence);
215                    if (oldPresence != null) {
216                        // Get the previous occupant's affiliation & role
217                        MUCUser mucExtension = MUCUser.from(oldPresence);
218                        MUCAffiliation oldAffiliation = mucExtension.getItem().getAffiliation();
219                        MUCRole oldRole = mucExtension.getItem().getRole();
220                        // Get the new occupant's affiliation & role
221                        MUCAffiliation newAffiliation = mucUser.getItem().getAffiliation();
222                        MUCRole newRole = mucUser.getItem().getRole();
223                        // Fire role modification events
224                        checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
225                        // Fire affiliation modification events
226                        checkAffiliationModifications(
227                            oldAffiliation,
228                            newAffiliation,
229                            isUserStatusModification,
230                            from);
231                    } else if (mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110)) {
232                        processedReflectedSelfPresence = true;
233                        synchronized (this) {
234                            notify();
235                        }
236                    } else {
237                        // A new occupant has joined the room
238                        for (ParticipantStatusListener listener : participantStatusListeners) {
239                            listener.joined(from);
240                        }
241                    }
242                    break;
243                case unavailable:
244                    occupantsMap.remove(from);
245                    if (mucUser != null && mucUser.hasStatus()) {
246                        if (isUserStatusModification) {
247                            userHasLeft();
248                        }
249                        // Fire events according to the received presence code
250                        checkPresenceCode(
251                            mucUser.getStatus(),
252                            isUserStatusModification,
253                            mucUser,
254                            from);
255                    } else {
256                        // An occupant has left the room
257                        if (!isUserStatusModification) {
258                            for (ParticipantStatusListener listener : participantStatusListeners) {
259                                listener.left(from);
260                            }
261                        }
262
263                        Destroy destroy = mucUser.getDestroy();
264                        // The room has been destroyed.
265                        if (destroy != null) {
266                            EntityBareJid alternateMucJid = destroy.getJid();
267                            final MultiUserChat alternateMuc;
268                            if (alternateMucJid == null) {
269                                alternateMuc = null;
270                            } else {
271                                alternateMuc = multiUserChatManager.getMultiUserChat(alternateMucJid);
272                            }
273
274                            for (UserStatusListener listener : userStatusListeners) {
275                                listener.roomDestroyed(alternateMuc, destroy.getReason());
276                            }
277                        }
278                    }
279                    if (isUserStatusModification) {
280                        for (UserStatusListener listener : userStatusListeners) {
281                            listener.removed(mucUser, presence);
282                        }
283                    } else {
284                        for (ParticipantStatusListener listener : participantStatusListeners) {
285                            listener.parted(from);
286                        }
287                    }
288                    break;
289                default:
290                    break;
291                }
292                for (PresenceListener listener : presenceListeners) {
293                    listener.processPresence(presence);
294                }
295            }
296        };
297
298        // Listens for all messages that include a MUCUser extension and fire the invitation
299        // rejection listeners if the message includes an invitation rejection.
300        declinesListener = new StanzaListener() {
301            @Override
302            public void processStanza(Stanza packet) {
303                Message message = (Message) packet;
304                // Get the MUC User extension
305                MUCUser mucUser = MUCUser.from(packet);
306                MUCUser.Decline rejection = mucUser.getDecline();
307                // Check if the MUCUser informs that the invitee has declined the invitation
308                if (rejection == null) {
309                    return;
310                }
311                // Fire event for invitation rejection listeners
312                fireInvitationRejectionListeners(message, rejection);
313            }
314        };
315
316        presenceInterceptor = new StanzaListener() {
317            @Override
318            public void processStanza(Stanza packet) {
319                Presence presence = (Presence) packet;
320                for (PresenceListener interceptor : presenceInterceptors) {
321                    interceptor.processPresence(presence);
322                }
323            }
324        };
325    }
326
327
328    /**
329     * Returns the name of the room this MultiUserChat object represents.
330     *
331     * @return the multi user chat room name.
332     */
333    public EntityBareJid getRoom() {
334        return room;
335    }
336
337    /**
338     * Enter a room, as described in XEP-45 7.2.
339     *
340     * @param conf the configuration used to enter the room.
341     * @return the returned presence by the service after the client send the initial presence in order to enter the room.
342     * @throws NotConnectedException if the XMPP connection is not connected.
343     * @throws NoResponseException if there was no response from the remote entity.
344     * @throws XMPPErrorException if there was an XMPP error returned.
345     * @throws InterruptedException if the calling thread was interrupted.
346     * @throws NotAMucServiceException if the entity is not a MUC serivce.
347     * @see <a href="http://xmpp.org/extensions/xep-0045.html#enter">XEP-45 7.2 Entering a Room</a>
348     */
349    private Presence enter(MucEnterConfiguration conf) throws NotConnectedException, NoResponseException,
350                    XMPPErrorException, InterruptedException, NotAMucServiceException {
351        final DomainBareJid mucService = room.asDomainBareJid();
352        mucServiceDiscoInfo = multiUserChatManager.getMucServiceDiscoInfo(mucService);
353        if (mucServiceDiscoInfo == null) {
354            throw new NotAMucServiceException(this);
355        }
356        // We enter a room by sending a presence packet where the "to"
357        // field is in the form "roomName@service/nickname"
358        Presence joinPresence = conf.getJoinPresence(this);
359
360        // Setup the messageListeners and presenceListeners *before* the join presence is send.
361        connection.addStanzaListener(messageListener, fromRoomGroupchatFilter);
362        StanzaFilter presenceFromRoomFilter = new AndFilter(fromRoomFilter,
363                        StanzaTypeFilter.PRESENCE,
364                        PossibleFromTypeFilter.ENTITY_FULL_JID);
365        connection.addStanzaListener(presenceListener, presenceFromRoomFilter);
366        // @formatter:off
367        connection.addStanzaListener(subjectListener,
368                        new AndFilter(fromRoomFilter,
369                                      MessageWithSubjectFilter.INSTANCE,
370                                      new NotFilter(MessageTypeFilter.ERROR),
371                                      // According to XEP-0045 § 8.1 "A message with a <subject/> and a <body/> or a <subject/> and a <thread/> is a
372                                      // legitimate message, but it SHALL NOT be interpreted as a subject change."
373                                      new NotFilter(MessageWithBodiesFilter.INSTANCE),
374                                      new NotFilter(MessageWithThreadFilter.INSTANCE))
375                        );
376        // @formatter:on
377        connection.addStanzaListener(declinesListener, new AndFilter(fromRoomFilter, DECLINE_FILTER));
378        connection.addStanzaSendingListener(presenceInterceptor, new AndFilter(ToMatchesFilter.create(room),
379                        StanzaTypeFilter.PRESENCE));
380        messageCollector = connection.createStanzaCollector(fromRoomGroupchatFilter);
381
382        // Wait for a presence packet back from the server.
383        // @formatter:off
384        StanzaFilter responseFilter = new AndFilter(StanzaTypeFilter.PRESENCE,
385                        new OrFilter(
386                            // We use a bare JID filter for positive responses, since the MUC service/room may rewrite the nickname.
387                            new AndFilter(FromMatchesFilter.createBare(getRoom()), MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF),
388                            // In case there is an error reply, we match on an error presence with the same stanza id and from the full
389                            // JID we send the join presence to.
390                            new AndFilter(FromMatchesFilter.createFull(joinPresence.getTo()), new StanzaIdFilter(joinPresence), PresenceTypeFilter.ERROR)
391                        )
392                    );
393        // @formatter:on
394        processedReflectedSelfPresence = false;
395        StanzaCollector presenceStanzaCollector = null;
396        final Presence reflectedSelfPresence;
397        try {
398            // This stanza collector will collect the final self presence from the MUC, which also signals that we have successful entered the MUC.
399            StanzaCollector selfPresenceCollector = connection.createStanzaCollectorAndSend(responseFilter, joinPresence);
400            StanzaCollector.Configuration presenceStanzaCollectorConfguration = StanzaCollector.newConfiguration().setCollectorToReset(
401                            selfPresenceCollector).setStanzaFilter(presenceFromRoomFilter);
402            // This stanza collector is used to reset the timeout of the selfPresenceCollector.
403            presenceStanzaCollector = connection.createStanzaCollector(presenceStanzaCollectorConfguration);
404            reflectedSelfPresence = selfPresenceCollector.nextResultOrThrow(conf.getTimeout());
405        }
406        catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) {
407            // Ensure that all callbacks are removed if there is an exception
408            removeConnectionCallbacks();
409            throw e;
410        }
411        finally {
412            if (presenceStanzaCollector != null) {
413                presenceStanzaCollector.cancel();
414            }
415        }
416
417        synchronized (presenceListener) {
418            // Only continue after we have received *and* processed the reflected self-presence. Since presences are
419            // handled in an extra listener, we may return from enter() without having processed all presences of the
420            // participants, resulting in a e.g. to low participant counter after enter(). Hence we wait here until the
421            // processing is done.
422            while (!processedReflectedSelfPresence) {
423                presenceListener.wait();
424            }
425        }
426
427        // This presence must be send from a full JID. We use the resourcepart of this JID as nick, since the room may
428        // performed roomnick rewriting
429        Resourcepart receivedNickname = reflectedSelfPresence.getFrom().getResourceOrThrow();
430        setNickname(receivedNickname);
431
432        // Update the list of joined rooms
433        multiUserChatManager.addJoinedRoom(room);
434        return reflectedSelfPresence;
435    }
436
437    private void setNickname(Resourcepart nickname) {
438        this.myRoomJid = JidCreate.entityFullFrom(room, nickname);
439    }
440
441    /**
442     * Get a new MUC enter configuration builder.
443     *
444     * @param nickname the nickname used when entering the MUC room.
445     * @return a new MUC enter configuration builder.
446     * @since 4.2
447     */
448    public MucEnterConfiguration.Builder getEnterConfigurationBuilder(Resourcepart nickname) {
449        return new MucEnterConfiguration.Builder(nickname, connection);
450    }
451
452    /**
453     * Creates the room according to some default configuration, assign the requesting user as the
454     * room owner, and add the owner to the room but not allow anyone else to enter the room
455     * (effectively "locking" the room). The requesting user will join the room under the specified
456     * nickname as soon as the room has been created.
457     * <p>
458     * To create an "Instant Room", that means a room with some default configuration that is
459     * available for immediate access, the room's owner should send an empty form after creating the
460     * room. Simply call {@link MucCreateConfigFormHandle#makeInstant()} on the returned {@link MucCreateConfigFormHandle}.
461     * </p>
462     * <p>
463     * To create a "Reserved Room", that means a room manually configured by the room creator before
464     * anyone is allowed to enter, the room's owner should complete and send a form after creating
465     * the room. Once the completed configuration form is sent to the server, the server will unlock
466     * the room. You can use the returned {@link MucCreateConfigFormHandle} to configure the room.
467     * </p>
468     *
469     * @param nickname the nickname to use.
470     * @return a handle to the MUC create configuration form API.
471     * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if
472     *         the user is not allowed to create the room)
473     * @throws NoResponseException if there was no response from the server.
474     * @throws InterruptedException if the calling thread was interrupted.
475     * @throws NotConnectedException if the XMPP connection is not connected.
476     * @throws MucAlreadyJoinedException if already joined the Multi-User Chat.7y
477     * @throws MissingMucCreationAcknowledgeException if there MUC creation was not acknowledged by the service.
478     * @throws NotAMucServiceException if the entity is not a MUC serivce.
479     */
480    public synchronized MucCreateConfigFormHandle create(Resourcepart nickname) throws NoResponseException,
481                    XMPPErrorException, InterruptedException, MucAlreadyJoinedException,
482                    NotConnectedException, MissingMucCreationAcknowledgeException, NotAMucServiceException {
483        if (isJoined()) {
484            throw new MucAlreadyJoinedException();
485        }
486
487        MucCreateConfigFormHandle mucCreateConfigFormHandle = createOrJoin(nickname);
488        if (mucCreateConfigFormHandle != null) {
489            // We successfully created a new room
490            return mucCreateConfigFormHandle;
491        }
492        // We need to leave the room since it seems that the room already existed
493        try {
494            leave();
495        }
496        catch (MucNotJoinedException e) {
497            LOGGER.log(Level.INFO, "Unexpected MucNotJoinedException", e);
498        }
499        throw new MissingMucCreationAcknowledgeException();
500    }
501
502    /**
503     * Create or join the MUC room with the given nickname.
504     *
505     * @param nickname the nickname to use in the MUC room.
506     * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
507     * @throws NoResponseException if there was no response from the remote entity.
508     * @throws XMPPErrorException if there was an XMPP error returned.
509     * @throws InterruptedException if the calling thread was interrupted.
510     * @throws NotConnectedException if the XMPP connection is not connected.
511     * @throws MucAlreadyJoinedException if already joined the Multi-User Chat.7y
512     * @throws NotAMucServiceException if the entity is not a MUC serivce.
513     */
514    public synchronized MucCreateConfigFormHandle createOrJoin(Resourcepart nickname) throws NoResponseException, XMPPErrorException,
515                    InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException {
516        MucEnterConfiguration mucEnterConfiguration = getEnterConfigurationBuilder(nickname).build();
517        return createOrJoin(mucEnterConfiguration);
518    }
519
520    /**
521     * Like {@link #create(Resourcepart)}, but will return a {@link MucCreateConfigFormHandle} if the room creation was acknowledged by
522     * the service (with an 201 status code). It's up to the caller to decide, based on the return
523     * value, if he needs to continue sending the room configuration. If {@code null} is returned, the room
524     * already existed and the user is able to join right away, without sending a form.
525     *
526     * @param mucEnterConfiguration the configuration used to enter the MUC.
527     * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
528     * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if
529     *         the user is not allowed to create the room)
530     * @throws NoResponseException if there was no response from the server.
531     * @throws InterruptedException if the calling thread was interrupted.
532     * @throws MucAlreadyJoinedException if the MUC is already joined
533     * @throws NotConnectedException if the XMPP connection is not connected.
534     * @throws NotAMucServiceException if the entity is not a MUC serivce.
535     */
536    public synchronized MucCreateConfigFormHandle createOrJoin(MucEnterConfiguration mucEnterConfiguration)
537                    throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException {
538        if (isJoined()) {
539            throw new MucAlreadyJoinedException();
540        }
541
542        Presence presence = enter(mucEnterConfiguration);
543
544        // Look for confirmation of room creation from the server
545        MUCUser mucUser = MUCUser.from(presence);
546        if (mucUser != null && mucUser.getStatus().contains(Status.ROOM_CREATED_201)) {
547            // Room was created and the user has joined the room
548            return new MucCreateConfigFormHandle();
549        }
550        return null;
551    }
552
553    /**
554     * A handle used to configure a newly created room. As long as the room is not configured it will be locked, which
555     * means that no one is able to join. The room will become unlocked as soon it got configured. In order to create an
556     * instant room, use {@link #makeInstant()}.
557     * <p>
558     * For advanced configuration options, use {@link MultiUserChat#getConfigurationForm()}, get the answer form with
559     * {@link Form#getFillableForm()}, fill it out and send it back to the room with
560     * {@link MultiUserChat#sendConfigurationForm(FillableForm)}.
561     * </p>
562     */
563    public class MucCreateConfigFormHandle {
564
565        /**
566         * Create an instant room. The default configuration will be accepted and the room will become unlocked, i.e.
567         * other users are able to join.
568         *
569         * @throws NoResponseException if there was no response from the remote entity.
570         * @throws XMPPErrorException if there was an XMPP error returned.
571         * @throws NotConnectedException if the XMPP connection is not connected.
572         * @throws InterruptedException if the calling thread was interrupted.
573         * @see <a href="http://www.xmpp.org/extensions/xep-0045.html#createroom-instant">XEP-45 § 10.1.2 Creating an
574         *      Instant Room</a>
575         */
576        public void makeInstant() throws NoResponseException, XMPPErrorException, NotConnectedException,
577                        InterruptedException {
578            sendConfigurationForm(null);
579        }
580
581        /**
582         * Alias for {@link MultiUserChat#getConfigFormManager()}.
583         *
584         * @return a MUC configuration form manager for this room.
585         * @throws NoResponseException if there was no response from the remote entity.
586         * @throws XMPPErrorException if there was an XMPP error returned.
587         * @throws NotConnectedException if the XMPP connection is not connected.
588         * @throws InterruptedException if the calling thread was interrupted.
589         * @see MultiUserChat#getConfigFormManager()
590         */
591        public MucConfigFormManager getConfigFormManager() throws NoResponseException,
592                        XMPPErrorException, NotConnectedException, InterruptedException {
593            return MultiUserChat.this.getConfigFormManager();
594        }
595    }
596
597    /**
598     * Create or join a MUC if it is necessary, i.e. if not the MUC is not already joined.
599     *
600     * @param nickname the required nickname to use.
601     * @param password the optional password required to join
602     * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
603     * @throws NoResponseException if there was no response from the remote entity.
604     * @throws XMPPErrorException if there was an XMPP error returned.
605     * @throws NotConnectedException if the XMPP connection is not connected.
606     * @throws InterruptedException if the calling thread was interrupted.
607     * @throws NotAMucServiceException if the entity is not a MUC serivce.
608     */
609    public MucCreateConfigFormHandle createOrJoinIfNecessary(Resourcepart nickname, String password) throws NoResponseException,
610                    XMPPErrorException, NotConnectedException, InterruptedException, NotAMucServiceException {
611        if (isJoined()) {
612            return null;
613        }
614        MucEnterConfiguration mucEnterConfiguration = getEnterConfigurationBuilder(nickname).withPassword(
615                        password).build();
616        try {
617            return createOrJoin(mucEnterConfiguration);
618        }
619        catch (MucAlreadyJoinedException e) {
620            return null;
621        }
622    }
623
624    /**
625     * Joins the chat room using the specified nickname. If already joined
626     * using another nickname, this method will first leave the room and then
627     * re-join using the new nickname. The default connection timeout for a reply
628     * from the group chat server that the join succeeded will be used. After
629     * joining the room, the room will decide the amount of history to send.
630     *
631     * @param nickname the nickname to use.
632     * @throws NoResponseException if there was no response from the remote entity.
633     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
634     *      401 error can occur if no password was provided and one is required; or a
635     *      403 error can occur if the user is banned; or a
636     *      404 error can occur if the room does not exist or is locked; or a
637     *      407 error can occur if user is not on the member list; or a
638     *      409 error can occur if someone is already in the group chat with the same nickname.
639     * @throws NoResponseException if there was no response from the server.
640     * @throws NotConnectedException if the XMPP connection is not connected.
641     * @throws InterruptedException if the calling thread was interrupted.
642     * @throws NotAMucServiceException if the entity is not a MUC serivce.
643     */
644    public void join(Resourcepart nickname) throws NoResponseException, XMPPErrorException,
645                    NotConnectedException, InterruptedException, NotAMucServiceException {
646        MucEnterConfiguration.Builder builder = getEnterConfigurationBuilder(nickname);
647        join(builder.build());
648    }
649
650    /**
651     * Joins the chat room using the specified nickname and password. If already joined
652     * using another nickname, this method will first leave the room and then
653     * re-join using the new nickname. The default connection timeout for a reply
654     * from the group chat server that the join succeeded will be used. After
655     * joining the room, the room will decide the amount of history to send.<p>
656     *
657     * A password is required when joining password protected rooms. If the room does
658     * not require a password there is no need to provide one.
659     *
660     * @param nickname the nickname to use.
661     * @param password the password to use.
662     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
663     *      401 error can occur if no password was provided and one is required; or a
664     *      403 error can occur if the user is banned; or a
665     *      404 error can occur if the room does not exist or is locked; or a
666     *      407 error can occur if user is not on the member list; or a
667     *      409 error can occur if someone is already in the group chat with the same nickname.
668     * @throws InterruptedException if the calling thread was interrupted.
669     * @throws NotConnectedException if the XMPP connection is not connected.
670     * @throws NoResponseException if there was no response from the server.
671     * @throws NotAMucServiceException if the entity is not a MUC serivce.
672     */
673    public void join(Resourcepart nickname, String password) throws XMPPErrorException, InterruptedException, NoResponseException, NotConnectedException, NotAMucServiceException {
674        MucEnterConfiguration.Builder builder = getEnterConfigurationBuilder(nickname).withPassword(
675                        password);
676        join(builder.build());
677    }
678
679    /**
680     * Joins the chat room using the specified nickname and password. If already joined
681     * using another nickname, this method will first leave the room and then
682     * re-join using the new nickname.<p>
683     *
684     * To control the amount of history to receive while joining a room you will need to provide
685     * a configured DiscussionHistory object.<p>
686     *
687     * A password is required when joining password protected rooms. If the room does
688     * not require a password there is no need to provide one.<p>
689     *
690     * If the room does not already exist when the user seeks to enter it, the server will
691     * decide to create a new room or not.
692     *
693     * @param mucEnterConfiguration the configuration used to enter the MUC.
694     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
695     *      401 error can occur if no password was provided and one is required; or a
696     *      403 error can occur if the user is banned; or a
697     *      404 error can occur if the room does not exist or is locked; or a
698     *      407 error can occur if user is not on the member list; or a
699     *      409 error can occur if someone is already in the group chat with the same nickname.
700     * @throws NoResponseException if there was no response from the server.
701     * @throws NotConnectedException if the XMPP connection is not connected.
702     * @throws InterruptedException if the calling thread was interrupted.
703     * @throws NotAMucServiceException if the entity is not a MUC serivce.
704     */
705    public synchronized void join(MucEnterConfiguration mucEnterConfiguration)
706        throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException, NotAMucServiceException {
707        // If we've already joined the room, leave it before joining under a new
708        // nickname.
709        if (isJoined()) {
710            try {
711                leaveSync();
712            }
713            catch (XMPPErrorException | NoResponseException | MucNotJoinedException e) {
714                LOGGER.log(Level.WARNING, "Could not leave MUC prior joining, assuming we are not joined", e);
715            }
716        }
717        enter(mucEnterConfiguration);
718    }
719
720    /**
721     * Returns true if currently in the multi user chat (after calling the {@link
722     * #join(Resourcepart)} method).
723     *
724     * @return true if currently in the multi user chat room.
725     */
726    public boolean isJoined() {
727        return myRoomJid != null;
728    }
729
730    /**
731     * Leave the chat room.
732     *
733     * @return the leave presence as reflected by the MUC.
734     * @throws NotConnectedException if the XMPP connection is not connected.
735     * @throws InterruptedException if the calling thread was interrupted.
736     * @throws XMPPErrorException if there was an XMPP error returned.
737     * @throws NoResponseException if there was no response from the remote entity.
738     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
739     * @deprecated use {@link #leave()} instead.
740     */
741    @Deprecated
742    // TODO: Remove in Smack 4.5.
743    public synchronized Presence leaveSync() throws NotConnectedException, InterruptedException, MucNotJoinedException, NoResponseException, XMPPErrorException {
744        return leave();
745    }
746
747    /**
748     * Leave the chat room.
749     *
750     * @return the leave presence as reflected by the MUC.
751     * @throws NotConnectedException if the XMPP connection is not connected.
752     * @throws InterruptedException if the calling thread was interrupted.
753     * @throws XMPPErrorException if there was an XMPP error returned.
754     * @throws NoResponseException if there was no response from the remote entity.
755     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
756     */
757    public synchronized Presence leave()
758                    throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, MucNotJoinedException {
759        //  Note that this method is intentionally not guarded by
760        // "if  (!joined) return" because it should be always be possible to leave the room in case the instance's
761        // state does not reflect the actual state.
762
763        final EntityFullJid myRoomJid = this.myRoomJid;
764        if (myRoomJid == null) {
765            throw new MucNotJoinedException(this);
766        }
767
768        // TODO: Consider adding a origin-id to the presence, once it is moved form smack-experimental into
769        // smack-extensions, in case the MUC service does not support stable IDs, and modify
770        // reflectedLeavePresenceFilters accordingly.
771
772        // We leave a room by sending a presence packet where the "to"
773        // field is in the form "roomName@service/nickname"
774        Presence leavePresence = connection.getStanzaFactory().buildPresenceStanza()
775                .ofType(Presence.Type.unavailable)
776                .to(myRoomJid)
777                .build();
778
779        List<StanzaFilter> reflectedLeavePresenceFilters = new ArrayList<>(3);
780        reflectedLeavePresenceFilters.add(StanzaTypeFilter.PRESENCE);
781        reflectedLeavePresenceFilters.add(new OrFilter(
782                        new AndFilter(FromMatchesFilter.createFull(myRoomJid), PresenceTypeFilter.UNAVAILABLE,
783                                        MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF),
784                        new AndFilter(fromRoomFilter, PresenceTypeFilter.ERROR)));
785
786        if (serviceSupportsStableIds()) {
787            reflectedLeavePresenceFilters.add(new StanzaIdFilter(leavePresence));
788        }
789
790        StanzaFilter reflectedLeavePresenceFilter = new AndFilter(reflectedLeavePresenceFilters);
791
792        // Reset occupant information first so that we are assume that we left the room even if sendStanza() would
793        // throw.
794        userHasLeft();
795
796        Presence reflectedLeavePresence = connection.createStanzaCollectorAndSend(reflectedLeavePresenceFilter, leavePresence).nextResultOrThrow();
797
798        return reflectedLeavePresence;
799    }
800
801    /**
802     * Get a {@link MucConfigFormManager} to configure this room.
803     * <p>
804     * Only room owners are able to configure a room.
805     * </p>
806     *
807     * @return a MUC configuration form manager for this room.
808     * @throws NoResponseException if there was no response from the remote entity.
809     * @throws XMPPErrorException if there was an XMPP error returned.
810     * @throws NotConnectedException if the XMPP connection is not connected.
811     * @throws InterruptedException if the calling thread was interrupted.
812     * @see <a href="http://xmpp.org/extensions/xep-0045.html#roomconfig">XEP-45 § 10.2 Subsequent Room Configuration</a>
813     * @since 4.2
814     */
815    public MucConfigFormManager getConfigFormManager() throws NoResponseException,
816                    XMPPErrorException, NotConnectedException, InterruptedException {
817        return new MucConfigFormManager(this);
818    }
819
820    /**
821     * Returns the room's configuration form that the room's owner can use or <code>null</code> if
822     * no configuration is possible. The configuration form allows to set the room's language,
823     * enable logging, specify room's type, etc..
824     *
825     * @return the Form that contains the fields to complete together with the instrucions or
826     * <code>null</code> if no configuration is possible.
827     * @throws XMPPErrorException if an error occurs asking the configuration form for the room.
828     * @throws NoResponseException if there was no response from the server.
829     * @throws NotConnectedException if the XMPP connection is not connected.
830     * @throws InterruptedException if the calling thread was interrupted.
831     */
832    public Form getConfigurationForm() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
833        MUCOwner iq = new MUCOwner();
834        iq.setTo(room);
835        iq.setType(IQ.Type.get);
836
837        IQ answer = connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
838        DataForm dataForm = DataForm.from(answer, MucConfigFormManager.FORM_TYPE);
839        return new Form(dataForm);
840    }
841
842    /**
843     * Sends the completed configuration form to the server. The room will be configured
844     * with the new settings defined in the form.
845     *
846     * @param form the form with the new settings.
847     * @throws XMPPErrorException if an error occurs setting the new rooms' configuration.
848     * @throws NoResponseException if there was no response from the server.
849     * @throws NotConnectedException if the XMPP connection is not connected.
850     * @throws InterruptedException if the calling thread was interrupted.
851     */
852    public void sendConfigurationForm(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
853        final DataForm dataForm;
854        if (form != null) {
855            dataForm = form.getDataFormToSubmit();
856        } else {
857            // Instant room, cf. XEP-0045 § 10.1.2
858            dataForm = DataForm.builder().build();
859        }
860
861        MUCOwner iq = new MUCOwner();
862        iq.setTo(room);
863        iq.setType(IQ.Type.set);
864        iq.addExtension(dataForm);
865
866        connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
867    }
868
869    /**
870     * Returns the room's registration form that an unaffiliated user, can use to become a member
871     * of the room or <code>null</code> if no registration is possible. Some rooms may restrict the
872     * privilege to register members and allow only room admins to add new members.<p>
873     *
874     * If the user requesting registration requirements is not allowed to register with the room
875     * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
876     * error to the user (error code 405).
877     *
878     * @return the registration Form that contains the fields to complete together with the
879     * instrucions or <code>null</code> if no registration is possible.
880     * @throws XMPPErrorException if an error occurs asking the registration form for the room or a
881     * 405 error if the user is not allowed to register with the room.
882     * @throws NoResponseException if there was no response from the server.
883     * @throws NotConnectedException if the XMPP connection is not connected.
884     * @throws InterruptedException if the calling thread was interrupted.
885     */
886    public Form getRegistrationForm() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
887        Registration reg = new Registration();
888        reg.setType(IQ.Type.get);
889        reg.setTo(room);
890
891        IQ result = connection.createStanzaCollectorAndSend(reg).nextResultOrThrow();
892        DataForm dataForm = DataForm.from(result);
893        return new Form(dataForm);
894    }
895
896    /**
897     * Sends the completed registration form to the server. After the user successfully submits
898     * the form, the room may queue the request for review by the room admins or may immediately
899     * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
900     *
901     * If the desired room nickname is already reserved for that room, the room will return a
902     * "Conflict" error to the user (error code 409). If the room does not support registration,
903     * it will return a "Service Unavailable" error to the user (error code 503).
904     *
905     * @param form the completed registration form.
906     * @throws XMPPErrorException if an error occurs submitting the registration form. In particular, a
907     *      409 error can occur if the desired room nickname is already reserved for that room;
908     *      or a 503 error can occur if the room does not support registration.
909     * @throws NoResponseException if there was no response from the server.
910     * @throws NotConnectedException if the XMPP connection is not connected.
911     * @throws InterruptedException if the calling thread was interrupted.
912     */
913    public void sendRegistrationForm(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
914        Registration reg = new Registration();
915        reg.setType(IQ.Type.set);
916        reg.setTo(room);
917        reg.addExtension(form.getDataFormToSubmit());
918
919        connection.createStanzaCollectorAndSend(reg).nextResultOrThrow();
920    }
921
922    /**
923     * Sends a request to the server to destroy the room. The sender of the request
924     * should be the room's owner. If the sender of the destroy request is not the room's owner
925     * then the server will answer a "Forbidden" error (403).
926     *
927     * @param reason the reason for the room destruction.
928     * @param alternateJID the JID of an alternate location.
929     * @throws XMPPErrorException if an error occurs while trying to destroy the room.
930     *      An error can occur which will be wrapped by an XMPPException --
931     *      XMPP error code 403. The error code can be used to present more
932     *      appropiate error messages to end-users.
933     * @throws NoResponseException if there was no response from the server.
934     * @throws NotConnectedException if the XMPP connection is not connected.
935     * @throws InterruptedException if the calling thread was interrupted.
936     */
937    public void destroy(String reason, EntityBareJid alternateJID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
938        MUCOwner iq = new MUCOwner();
939        iq.setTo(room);
940        iq.setType(IQ.Type.set);
941
942        // Create the reason for the room destruction
943        Destroy destroy = new Destroy(alternateJID, reason);
944        iq.setDestroy(destroy);
945
946        try {
947            connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
948        }
949        catch (XMPPErrorException e) {
950            // Note that we do not call userHasLeft() here because an XMPPErrorException would usually indicate that the
951            // room was not destroyed and we therefore we also did not leave the room.
952            throw e;
953        }
954        catch (NoResponseException | NotConnectedException | InterruptedException e) {
955            // Reset occupant information.
956            userHasLeft();
957            throw e;
958        }
959
960        // Reset occupant information.
961        userHasLeft();
962    }
963
964    /**
965     * Invites another user to the room in which one is an occupant. The invitation
966     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
967     *
968     * If the room is password-protected, the invitee will receive a password to use to join
969     * the room. If the room is members-only, the the invitee may be added to the member list.
970     *
971     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
972     * @param reason the reason why the user is being invited.
973     * @throws NotConnectedException if the XMPP connection is not connected.
974     * @throws InterruptedException if the calling thread was interrupted.
975     */
976    public void invite(EntityBareJid user, String reason) throws NotConnectedException, InterruptedException {
977        invite(connection.getStanzaFactory().buildMessageStanza(), user, reason);
978    }
979
980    /**
981     * Invites another user to the room in which one is an occupant using a given Message. The invitation
982     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
983     *
984     * If the room is password-protected, the invitee will receive a password to use to join
985     * the room. If the room is members-only, the the invitee may be added to the member list.
986     *
987     * @param message the message to use for sending the invitation.
988     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
989     * @param reason the reason why the user is being invited.
990     * @throws NotConnectedException if the XMPP connection is not connected.
991     * @throws InterruptedException if the calling thread was interrupted.
992     * @deprecated use {@link #invite(MessageBuilder, EntityBareJid, String)} instead.
993     */
994    @Deprecated
995    // TODO: Remove in Smack 4.5.
996    public void invite(Message message, EntityBareJid user, String reason) throws NotConnectedException, InterruptedException {
997        // TODO listen for 404 error code when inviter supplies a non-existent JID
998        message.setTo(room);
999
1000        // Create the MUCUser packet that will include the invitation
1001        MUCUser mucUser = new MUCUser();
1002        MUCUser.Invite invite = new MUCUser.Invite(reason, user);
1003        mucUser.setInvite(invite);
1004        // Add the MUCUser packet that includes the invitation to the message
1005        message.addExtension(mucUser);
1006
1007        connection.sendStanza(message);
1008    }
1009
1010    /**
1011     * Invites another user to the room in which one is an occupant using a given Message. The invitation
1012     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
1013     *
1014     * If the room is password-protected, the invitee will receive a password to use to join
1015     * the room. If the room is members-only, the the invitee may be added to the member list.
1016     *
1017     * @param messageBuilder the message to use for sending the invitation.
1018     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
1019     * @param reason the reason why the user is being invited.
1020     * @throws NotConnectedException if the XMPP connection is not connected.
1021     * @throws InterruptedException if the calling thread was interrupted.
1022     */
1023    public void invite(MessageBuilder messageBuilder, EntityBareJid user, String reason) throws NotConnectedException, InterruptedException {
1024        // TODO listen for 404 error code when inviter supplies a non-existent JID
1025        messageBuilder.to(room);
1026
1027        // Create the MUCUser packet that will include the invitation
1028        MUCUser mucUser = new MUCUser();
1029        MUCUser.Invite invite = new MUCUser.Invite(reason, user);
1030        mucUser.setInvite(invite);
1031        // Add the MUCUser packet that includes the invitation to the message
1032        messageBuilder.addExtension(mucUser);
1033
1034        Message message = messageBuilder.build();
1035        connection.sendStanza(message);
1036    }
1037
1038    /**
1039     * Adds a listener to invitation rejections notifications. The listener will be fired anytime
1040     * an invitation is declined.
1041     *
1042     * @param listener an invitation rejection listener.
1043     * @return true if the listener was not already added.
1044     */
1045    public boolean addInvitationRejectionListener(InvitationRejectionListener listener) {
1046         return invitationRejectionListeners.add(listener);
1047    }
1048
1049    /**
1050     * Removes a listener from invitation rejections notifications. The listener will be fired
1051     * anytime an invitation is declined.
1052     *
1053     * @param listener an invitation rejection listener.
1054     * @return true if the listener was registered and is now removed.
1055     */
1056    public boolean removeInvitationRejectionListener(InvitationRejectionListener listener) {
1057        return invitationRejectionListeners.remove(listener);
1058    }
1059
1060    /**
1061     * Fires invitation rejection listeners.
1062     *
1063     * @param invitee the user being invited.
1064     * @param reason the reason for the rejection
1065     */
1066    private void fireInvitationRejectionListeners(Message message, MUCUser.Decline rejection) {
1067        EntityBareJid invitee = rejection.getFrom();
1068        String reason = rejection.getReason();
1069        InvitationRejectionListener[] listeners;
1070        synchronized (invitationRejectionListeners) {
1071            listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
1072            invitationRejectionListeners.toArray(listeners);
1073        }
1074        for (InvitationRejectionListener listener : listeners) {
1075            listener.invitationDeclined(invitee, reason, message, rejection);
1076        }
1077    }
1078
1079    /**
1080     * Adds a listener to subject change notifications. The listener will be fired anytime
1081     * the room's subject changes.
1082     *
1083     * @param listener a subject updated listener.
1084     * @return true if the listener was not already added.
1085     */
1086    public boolean addSubjectUpdatedListener(SubjectUpdatedListener listener) {
1087        return subjectUpdatedListeners.add(listener);
1088    }
1089
1090    /**
1091     * Removes a listener from subject change notifications. The listener will be fired
1092     * anytime the room's subject changes.
1093     *
1094     * @param listener a subject updated listener.
1095     * @return true if the listener was registered and is now removed.
1096     */
1097    public boolean removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
1098        return subjectUpdatedListeners.remove(listener);
1099    }
1100
1101    /**
1102     * Adds a new {@link StanzaListener} that will be invoked every time a new presence
1103     * is going to be sent by this MultiUserChat to the server. Stanza interceptors may
1104     * add new extensions to the presence that is going to be sent to the MUC service.
1105     *
1106     * @param presenceInterceptor the new stanza interceptor that will intercept presence packets.
1107     */
1108    public void addPresenceInterceptor(PresenceListener presenceInterceptor) {
1109        presenceInterceptors.add(presenceInterceptor);
1110    }
1111
1112    /**
1113     * Removes a {@link StanzaListener} that was being invoked every time a new presence
1114     * was being sent by this MultiUserChat to the server. Stanza interceptors may
1115     * add new extensions to the presence that is going to be sent to the MUC service.
1116     *
1117     * @param presenceInterceptor the stanza interceptor to remove.
1118     */
1119    public void removePresenceInterceptor(PresenceListener presenceInterceptor) {
1120        presenceInterceptors.remove(presenceInterceptor);
1121    }
1122
1123    /**
1124     * Returns the last known room's subject or <code>null</code> if the user hasn't joined the room
1125     * or the room does not have a subject yet. In case the room has a subject, as soon as the
1126     * user joins the room a message with the current room's subject will be received.<p>
1127     *
1128     * To be notified every time the room's subject change you should add a listener
1129     * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
1130     *
1131     * To change the room's subject use {@link #changeSubject(String)}.
1132     *
1133     * @return the room's subject or <code>null</code> if the user hasn't joined the room or the
1134     * room does not have a subject yet.
1135     */
1136    public String getSubject() {
1137        return subject;
1138    }
1139
1140    /**
1141     * Returns the reserved room nickname for the user in the room. A user may have a reserved
1142     * nickname, for example through explicit room registration or database integration. In such
1143     * cases it may be desirable for the user to discover the reserved nickname before attempting
1144     * to enter the room.
1145     *
1146     * @return the reserved room nickname or <code>null</code> if none.
1147     * @throws SmackException if there was no response from the server.
1148     * @throws InterruptedException if the calling thread was interrupted.
1149     */
1150    public String getReservedNickname() throws SmackException, InterruptedException {
1151        try {
1152            DiscoverInfo result =
1153                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
1154                    room,
1155                    "x-roomuser-item");
1156            // Look for an Identity that holds the reserved nickname and return its name
1157            for (DiscoverInfo.Identity identity : result.getIdentities()) {
1158                return identity.getName();
1159            }
1160        }
1161        catch (XMPPException e) {
1162            LOGGER.log(Level.SEVERE, "Error retrieving room nickname", e);
1163        }
1164        // If no Identity was found then the user does not have a reserved room nickname
1165        return null;
1166    }
1167
1168    /**
1169     * Returns the nickname that was used to join the room, or <code>null</code> if not
1170     * currently joined.
1171     *
1172     * @return the nickname currently being used.
1173     */
1174    public Resourcepart getNickname() {
1175        final EntityFullJid myRoomJid = this.myRoomJid;
1176        if (myRoomJid == null) {
1177            return null;
1178        }
1179        return myRoomJid.getResourcepart();
1180    }
1181
1182    /**
1183     * Changes the occupant's nickname to a new nickname within the room. Each room occupant
1184     * will receive two presence packets. One of type "unavailable" for the old nickname and one
1185     * indicating availability for the new nickname. The unavailable presence will contain the new
1186     * nickname and an appropriate status code (namely 303) as extended presence information. The
1187     * status code 303 indicates that the occupant is changing his/her nickname.
1188     *
1189     * @param nickname the new nickname within the room.
1190     * @throws XMPPErrorException if the new nickname is already in use by another occupant.
1191     * @throws NoResponseException if there was no response from the server.
1192     * @throws NotConnectedException if the XMPP connection is not connected.
1193     * @throws InterruptedException if the calling thread was interrupted.
1194     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
1195     */
1196    public synchronized void changeNickname(Resourcepart nickname) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, MucNotJoinedException  {
1197        Objects.requireNonNull(nickname, "Nickname must not be null or blank.");
1198        // Check that we already have joined the room before attempting to change the
1199        // nickname.
1200        if (!isJoined()) {
1201            throw new MucNotJoinedException(this);
1202        }
1203        final EntityFullJid jid = JidCreate.entityFullFrom(room, nickname);
1204        // We change the nickname by sending a presence packet where the "to"
1205        // field is in the form "roomName@service/nickname"
1206        // We don't have to signal the MUC support again
1207        Presence joinPresence = connection.getStanzaFactory().buildPresenceStanza()
1208                .to(jid)
1209                .ofType(Presence.Type.available)
1210                .build();
1211
1212        // Wait for a presence packet back from the server.
1213        StanzaFilter responseFilter =
1214            new AndFilter(
1215                FromMatchesFilter.createFull(jid),
1216                new StanzaTypeFilter(Presence.class));
1217        StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, joinPresence);
1218        // Wait up to a certain number of seconds for a reply. If there is a negative reply, an
1219        // exception will be thrown
1220        response.nextResultOrThrow();
1221
1222        // TODO: Shouldn't this handle nickname rewriting by the MUC service?
1223        setNickname(nickname);
1224    }
1225
1226    /**
1227     * Changes the occupant's availability status within the room. The presence type
1228     * will remain available but with a new status that describes the presence update and
1229     * a new presence mode (e.g. Extended away).
1230     *
1231     * @param status a text message describing the presence update.
1232     * @param mode the mode type for the presence update.
1233     * @throws NotConnectedException if the XMPP connection is not connected.
1234     * @throws InterruptedException if the calling thread was interrupted.
1235     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
1236     */
1237    public void changeAvailabilityStatus(String status, Presence.Mode mode) throws NotConnectedException, InterruptedException, MucNotJoinedException {
1238        final EntityFullJid myRoomJid = this.myRoomJid;
1239        if (myRoomJid == null) {
1240            throw new MucNotJoinedException(this);
1241        }
1242
1243        // We change the availability status by sending a presence packet to the room with the
1244        // new presence status and mode
1245        Presence joinPresence = connection.getStanzaFactory().buildPresenceStanza()
1246                .to(myRoomJid)
1247                .ofType(Presence.Type.available)
1248                .setStatus(status)
1249                .setMode(mode)
1250                .build();
1251
1252        // Send join packet.
1253        connection.sendStanza(joinPresence);
1254    }
1255
1256    /**
1257     * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
1258     * of type "unavailable" including a status code 307 and optionally along with the reason
1259     * (if provided) and the bare JID of the user who initiated the kick. After the occupant
1260     * was kicked from the room, the rest of the occupants will receive a presence of type
1261     * "unavailable". The presence will include a status code 307 which means that the occupant
1262     * was kicked from the room.
1263     *
1264     * @param nickname the nickname of the participant or visitor to kick from the room
1265     * (e.g. "john").
1266     * @param reason the reason why the participant or visitor is being kicked from the room.
1267     * @throws XMPPErrorException if an error occurs kicking the occupant. In particular, a
1268     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1269     *      was intended to be kicked (i.e. Not Allowed error); or a
1270     *      403 error can occur if the occupant that intended to kick another occupant does
1271     *      not have kicking privileges (i.e. Forbidden error); or a
1272     *      400 error can occur if the provided nickname is not present in the room.
1273     * @throws NoResponseException if there was no response from the server.
1274     * @throws NotConnectedException if the XMPP connection is not connected.
1275     * @throws InterruptedException if the calling thread was interrupted.
1276     */
1277    public void kickParticipant(Resourcepart nickname, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1278        changeRole(nickname, MUCRole.none, reason);
1279    }
1280
1281    /**
1282     * Sends a voice request to the MUC. The room moderators usually need to approve this request.
1283     *
1284     * @throws NotConnectedException if the XMPP connection is not connected.
1285     * @throws InterruptedException if the calling thread was interrupted.
1286     * @see <a href="http://xmpp.org/extensions/xep-0045.html#requestvoice">XEP-45 § 7.13 Requesting
1287     *      Voice</a>
1288     * @since 4.1
1289     */
1290    public void requestVoice() throws NotConnectedException, InterruptedException {
1291        DataForm.Builder form = DataForm.builder()
1292                        .setFormType(MUCInitialPresence.NAMESPACE + "#request");
1293
1294        TextSingleFormField.Builder requestVoiceField = FormField.textSingleBuilder("muc#role");
1295        requestVoiceField.setLabel("Requested role");
1296        requestVoiceField.setValue("participant");
1297        form.addField(requestVoiceField.build());
1298
1299        Message message = connection.getStanzaFactory().buildMessageStanza()
1300                .to(room)
1301                .addExtension(form.build())
1302                .build();
1303        connection.sendStanza(message);
1304    }
1305
1306    /**
1307     * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
1308     * who does and does not have "voice" in the room. To have voice means that a room occupant
1309     * is able to send messages to the room occupants.
1310     *
1311     * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
1312     * @throws XMPPErrorException if an error occurs granting voice to a visitor. In particular, a
1313     *      403 error can occur if the occupant that intended to grant voice is not
1314     *      a moderator in this room (i.e. Forbidden error); or a
1315     *      400 error can occur if the provided nickname is not present in the room.
1316     * @throws NoResponseException if there was no response from the server.
1317     * @throws NotConnectedException if the XMPP connection is not connected.
1318     * @throws InterruptedException if the calling thread was interrupted.
1319     */
1320    public void grantVoice(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1321        changeRole(nicknames, MUCRole.participant);
1322    }
1323
1324    /**
1325     * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
1326     * who does and does not have "voice" in the room. To have voice means that a room occupant
1327     * is able to send messages to the room occupants.
1328     *
1329     * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
1330     * @throws XMPPErrorException if an error occurs granting voice to a visitor. In particular, a
1331     *      403 error can occur if the occupant that intended to grant voice is not
1332     *      a moderator in this room (i.e. Forbidden error); or a
1333     *      400 error can occur if the provided nickname is not present in the room.
1334     * @throws NoResponseException if there was no response from the server.
1335     * @throws NotConnectedException if the XMPP connection is not connected.
1336     * @throws InterruptedException if the calling thread was interrupted.
1337     */
1338    public void grantVoice(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1339        changeRole(nickname, MUCRole.participant, null);
1340    }
1341
1342    /**
1343     * Revokes voice from participants in the room. In a moderated room, a moderator may want to
1344     * revoke an occupant's privileges to speak. To have voice means that a room occupant
1345     * is able to send messages to the room occupants.
1346     *
1347     * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
1348     * @throws XMPPErrorException if an error occurs revoking voice from a participant. In particular, a
1349     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1350     *      was tried to revoke his voice (i.e. Not Allowed error); or a
1351     *      400 error can occur if the provided nickname is not present in the room.
1352     * @throws NoResponseException if there was no response from the server.
1353     * @throws NotConnectedException if the XMPP connection is not connected.
1354     * @throws InterruptedException if the calling thread was interrupted.
1355     */
1356    public void revokeVoice(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1357        changeRole(nicknames, MUCRole.visitor);
1358    }
1359
1360    /**
1361     * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
1362     * revoke an occupant's privileges to speak. To have voice means that a room occupant
1363     * is able to send messages to the room occupants.
1364     *
1365     * @param nickname the nickname of the participant to revoke voice (e.g. "john").
1366     * @throws XMPPErrorException if an error occurs revoking voice from a participant. In particular, a
1367     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1368     *      was tried to revoke his voice (i.e. Not Allowed error); or a
1369     *      400 error can occur if the provided nickname is not present in the room.
1370     * @throws NoResponseException if there was no response from the server.
1371     * @throws NotConnectedException if the XMPP connection is not connected.
1372     * @throws InterruptedException if the calling thread was interrupted.
1373     */
1374    public void revokeVoice(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1375        changeRole(nickname, MUCRole.visitor, null);
1376    }
1377
1378    /**
1379     * Bans users from the room. An admin or owner of the room can ban users from a room. This
1380     * means that the banned user will no longer be able to join the room unless the ban has been
1381     * removed. If the banned user was present in the room then he/she will be removed from the
1382     * room and notified that he/she was banned along with the reason (if provided) and the bare
1383     * XMPP user ID of the user who initiated the ban.
1384     *
1385     * @param jids the bare XMPP user IDs of the users to ban.
1386     * @throws XMPPErrorException if an error occurs banning a user. In particular, a
1387     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1388     *      was tried to be banned (i.e. Not Allowed error).
1389     * @throws NoResponseException if there was no response from the server.
1390     * @throws NotConnectedException if the XMPP connection is not connected.
1391     * @throws InterruptedException if the calling thread was interrupted.
1392     */
1393    public void banUsers(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1394        changeAffiliationByAdmin(jids, MUCAffiliation.outcast);
1395    }
1396
1397    /**
1398     * Bans a user from the room. An admin or owner of the room can ban users from a room. This
1399     * means that the banned user will no longer be able to join the room unless the ban has been
1400     * removed. If the banned user was present in the room then he/she will be removed from the
1401     * room and notified that he/she was banned along with the reason (if provided) and the bare
1402     * XMPP user ID of the user who initiated the ban.
1403     *
1404     * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
1405     * @param reason the optional reason why the user was banned.
1406     * @throws XMPPErrorException if an error occurs banning a user. In particular, a
1407     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1408     *      was tried to be banned (i.e. Not Allowed error).
1409     * @throws NoResponseException if there was no response from the server.
1410     * @throws NotConnectedException if the XMPP connection is not connected.
1411     * @throws InterruptedException if the calling thread was interrupted.
1412     */
1413    public void banUser(Jid jid, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1414        changeAffiliationByAdmin(jid, MUCAffiliation.outcast, reason);
1415    }
1416
1417    /**
1418     * Grants membership to other users. Only administrators are able to grant membership. A user
1419     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1420     * that a user cannot enter without being on the member list).
1421     *
1422     * @param jids the XMPP user IDs of the users to grant membership.
1423     * @throws XMPPErrorException if an error occurs granting membership to a user.
1424     * @throws NoResponseException if there was no response from the server.
1425     * @throws NotConnectedException if the XMPP connection is not connected.
1426     * @throws InterruptedException if the calling thread was interrupted.
1427     */
1428    public void grantMembership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1429        changeAffiliationByAdmin(jids, MUCAffiliation.member);
1430    }
1431
1432    /**
1433     * Grants membership to a user. Only administrators are able to grant membership. A user
1434     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1435     * that a user cannot enter without being on the member list).
1436     *
1437     * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
1438     * @throws XMPPErrorException if an error occurs granting membership to a user.
1439     * @throws NoResponseException if there was no response from the server.
1440     * @throws NotConnectedException if the XMPP connection is not connected.
1441     * @throws InterruptedException if the calling thread was interrupted.
1442     */
1443    public void grantMembership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1444        changeAffiliationByAdmin(jid, MUCAffiliation.member, null);
1445    }
1446
1447    /**
1448     * Revokes users' membership. Only administrators are able to revoke membership. A user
1449     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1450     * that a user cannot enter without being on the member list). If the user is in the room and
1451     * the room is of type members-only then the user will be removed from the room.
1452     *
1453     * @param jids the bare XMPP user IDs of the users to revoke membership.
1454     * @throws XMPPErrorException if an error occurs revoking membership to a user.
1455     * @throws NoResponseException if there was no response from the server.
1456     * @throws NotConnectedException if the XMPP connection is not connected.
1457     * @throws InterruptedException if the calling thread was interrupted.
1458     */
1459    public void revokeMembership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1460        changeAffiliationByAdmin(jids, MUCAffiliation.none);
1461    }
1462
1463    /**
1464     * Revokes a user's membership. Only administrators are able to revoke membership. A user
1465     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1466     * that a user cannot enter without being on the member list). If the user is in the room and
1467     * the room is of type members-only then the user will be removed from the room.
1468     *
1469     * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
1470     * @throws XMPPErrorException if an error occurs revoking membership to a user.
1471     * @throws NoResponseException if there was no response from the server.
1472     * @throws NotConnectedException if the XMPP connection is not connected.
1473     * @throws InterruptedException if the calling thread was interrupted.
1474     */
1475    public void revokeMembership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1476        changeAffiliationByAdmin(jid, MUCAffiliation.none, null);
1477    }
1478
1479    /**
1480     * Grants moderator privileges to participants or visitors. Room administrators may grant
1481     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1482     * other users, modify room's subject plus all the partcipants privileges.
1483     *
1484     * @param nicknames the nicknames of the occupants to grant moderator privileges.
1485     * @throws XMPPErrorException if an error occurs granting moderator privileges to a user.
1486     * @throws NoResponseException if there was no response from the server.
1487     * @throws NotConnectedException if the XMPP connection is not connected.
1488     * @throws InterruptedException if the calling thread was interrupted.
1489     */
1490    public void grantModerator(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1491        changeRole(nicknames, MUCRole.moderator);
1492    }
1493
1494    /**
1495     * Grants moderator privileges to a participant or visitor. Room administrators may grant
1496     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1497     * other users, modify room's subject plus all the partcipants privileges.
1498     *
1499     * @param nickname the nickname of the occupant to grant moderator privileges.
1500     * @throws XMPPErrorException if an error occurs granting moderator privileges to a user.
1501     * @throws NoResponseException if there was no response from the server.
1502     * @throws NotConnectedException if the XMPP connection is not connected.
1503     * @throws InterruptedException if the calling thread was interrupted.
1504     */
1505    public void grantModerator(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1506        changeRole(nickname, MUCRole.moderator, null);
1507    }
1508
1509    /**
1510     * Revokes moderator privileges from other users. The occupant that loses moderator
1511     * privileges will become a participant. Room administrators may revoke moderator privileges
1512     * only to occupants whose affiliation is member or none. This means that an administrator is
1513     * not allowed to revoke moderator privileges from other room administrators or owners.
1514     *
1515     * @param nicknames the nicknames of the occupants to revoke moderator privileges.
1516     * @throws XMPPErrorException if an error occurs revoking moderator privileges from a user.
1517     * @throws NoResponseException if there was no response from the server.
1518     * @throws NotConnectedException if the XMPP connection is not connected.
1519     * @throws InterruptedException if the calling thread was interrupted.
1520     */
1521    public void revokeModerator(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1522        changeRole(nicknames, MUCRole.participant);
1523    }
1524
1525    /**
1526     * Revokes moderator privileges from another user. The occupant that loses moderator
1527     * privileges will become a participant. Room administrators may revoke moderator privileges
1528     * only to occupants whose affiliation is member or none. This means that an administrator is
1529     * not allowed to revoke moderator privileges from other room administrators or owners.
1530     *
1531     * @param nickname the nickname of the occupant to revoke moderator privileges.
1532     * @throws XMPPErrorException if an error occurs revoking moderator privileges from a user.
1533     * @throws NoResponseException if there was no response from the server.
1534     * @throws NotConnectedException if the XMPP connection is not connected.
1535     * @throws InterruptedException if the calling thread was interrupted.
1536     */
1537    public void revokeModerator(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1538        changeRole(nickname, MUCRole.participant, null);
1539    }
1540
1541    /**
1542     * Grants ownership privileges to other users. Room owners may grant ownership privileges.
1543     * Some room implementations will not allow to grant ownership privileges to other users.
1544     * An owner is allowed to change defining room features as well as perform all administrative
1545     * functions.
1546     *
1547     * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
1548     * @throws XMPPErrorException if an error occurs granting ownership privileges to a user.
1549     * @throws NoResponseException if there was no response from the server.
1550     * @throws NotConnectedException if the XMPP connection is not connected.
1551     * @throws InterruptedException if the calling thread was interrupted.
1552     */
1553    public void grantOwnership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1554        changeAffiliationByAdmin(jids, MUCAffiliation.owner);
1555    }
1556
1557    /**
1558     * Grants ownership privileges to another user. Room owners may grant ownership privileges.
1559     * Some room implementations will not allow to grant ownership privileges to other users.
1560     * An owner is allowed to change defining room features as well as perform all administrative
1561     * functions.
1562     *
1563     * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
1564     * @throws XMPPErrorException if an error occurs granting ownership privileges to a user.
1565     * @throws NoResponseException if there was no response from the server.
1566     * @throws NotConnectedException if the XMPP connection is not connected.
1567     * @throws InterruptedException if the calling thread was interrupted.
1568     */
1569    public void grantOwnership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1570        changeAffiliationByAdmin(jid, MUCAffiliation.owner, null);
1571    }
1572
1573    /**
1574     * Revokes ownership privileges from other users. The occupant that loses ownership
1575     * privileges will become an administrator. Room owners may revoke ownership privileges.
1576     * Some room implementations will not allow to grant ownership privileges to other users.
1577     *
1578     * @param jids the bare XMPP user IDs of the users to revoke ownership.
1579     * @throws XMPPErrorException if an error occurs revoking ownership privileges from a user.
1580     * @throws NoResponseException if there was no response from the server.
1581     * @throws NotConnectedException if the XMPP connection is not connected.
1582     * @throws InterruptedException if the calling thread was interrupted.
1583     */
1584    public void revokeOwnership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1585        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1586    }
1587
1588    /**
1589     * Revokes ownership privileges from another user. The occupant that loses ownership
1590     * privileges will become an administrator. Room owners may revoke ownership privileges.
1591     * Some room implementations will not allow to grant ownership privileges to other users.
1592     *
1593     * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
1594     * @throws XMPPErrorException if an error occurs revoking ownership privileges from a user.
1595     * @throws NoResponseException if there was no response from the server.
1596     * @throws NotConnectedException if the XMPP connection is not connected.
1597     * @throws InterruptedException if the calling thread was interrupted.
1598     */
1599    public void revokeOwnership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1600        changeAffiliationByAdmin(jid, MUCAffiliation.admin, null);
1601    }
1602
1603    /**
1604     * Grants administrator privileges to other users. Room owners may grant administrator
1605     * privileges to a member or unaffiliated user. An administrator is allowed to perform
1606     * administrative functions such as banning users and edit moderator list.
1607     *
1608     * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
1609     * @throws XMPPErrorException if an error occurs granting administrator privileges to a user.
1610     * @throws NoResponseException if there was no response from the server.
1611     * @throws NotConnectedException if the XMPP connection is not connected.
1612     * @throws InterruptedException if the calling thread was interrupted.
1613     */
1614    public void grantAdmin(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1615        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1616    }
1617
1618    /**
1619     * Grants administrator privileges to another user. Room owners may grant administrator
1620     * privileges to a member or unaffiliated user. An administrator is allowed to perform
1621     * administrative functions such as banning users and edit moderator list.
1622     *
1623     * @param jid the bare XMPP user ID of the user to grant administrator privileges
1624     * (e.g. "user@host.org").
1625     * @throws XMPPErrorException if an error occurs granting administrator privileges to a user.
1626     * @throws NoResponseException if there was no response from the server.
1627     * @throws NotConnectedException if the XMPP connection is not connected.
1628     * @throws InterruptedException if the calling thread was interrupted.
1629     */
1630    public void grantAdmin(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1631        changeAffiliationByAdmin(jid, MUCAffiliation.admin);
1632    }
1633
1634    /**
1635     * Revokes administrator privileges from users. The occupant that loses administrator
1636     * privileges will become a member. Room owners may revoke administrator privileges from
1637     * a member or unaffiliated user.
1638     *
1639     * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
1640     * @throws XMPPErrorException if an error occurs revoking administrator privileges from a user.
1641     * @throws NoResponseException if there was no response from the server.
1642     * @throws NotConnectedException if the XMPP connection is not connected.
1643     * @throws InterruptedException if the calling thread was interrupted.
1644     */
1645    public void revokeAdmin(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1646        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1647    }
1648
1649    /**
1650     * Revokes administrator privileges from a user. The occupant that loses administrator
1651     * privileges will become a member. Room owners may revoke administrator privileges from
1652     * a member or unaffiliated user.
1653     *
1654     * @param jid the bare XMPP user ID of the user to revoke administrator privileges
1655     * (e.g. "user@host.org").
1656     * @throws XMPPErrorException if an error occurs revoking administrator privileges from a user.
1657     * @throws NoResponseException if there was no response from the server.
1658     * @throws NotConnectedException if the XMPP connection is not connected.
1659     * @throws InterruptedException if the calling thread was interrupted.
1660     */
1661    public void revokeAdmin(EntityJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1662        changeAffiliationByAdmin(jid, MUCAffiliation.member);
1663    }
1664
1665    /**
1666     * Tries to change the affiliation with an 'muc#admin' namespace
1667     *
1668     * @param jid TODO javadoc me please
1669     * @param affiliation TODO javadoc me please
1670     * @throws XMPPErrorException if there was an XMPP error returned.
1671     * @throws NoResponseException if there was no response from the remote entity.
1672     * @throws NotConnectedException if the XMPP connection is not connected.
1673     * @throws InterruptedException if the calling thread was interrupted.
1674     */
1675    private void changeAffiliationByAdmin(Jid jid, MUCAffiliation affiliation)
1676                    throws NoResponseException, XMPPErrorException,
1677                    NotConnectedException, InterruptedException {
1678        changeAffiliationByAdmin(jid, affiliation, null);
1679    }
1680
1681    /**
1682     * Tries to change the affiliation with an 'muc#admin' namespace
1683     *
1684     * @param jid TODO javadoc me please
1685     * @param affiliation TODO javadoc me please
1686     * @param reason the reason for the affiliation change (optional)
1687     * @throws XMPPErrorException if there was an XMPP error returned.
1688     * @throws NoResponseException if there was no response from the remote entity.
1689     * @throws NotConnectedException if the XMPP connection is not connected.
1690     * @throws InterruptedException if the calling thread was interrupted.
1691     */
1692    private void changeAffiliationByAdmin(Jid jid, MUCAffiliation affiliation, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1693        MUCAdmin iq = new MUCAdmin();
1694        iq.setTo(room);
1695        iq.setType(IQ.Type.set);
1696        // Set the new affiliation.
1697        MUCItem item = new MUCItem(affiliation, jid, reason);
1698        iq.addItem(item);
1699
1700        connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
1701    }
1702
1703    private void changeAffiliationByAdmin(Collection<? extends Jid> jids, MUCAffiliation affiliation)
1704                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1705        MUCAdmin iq = new MUCAdmin();
1706        iq.setTo(room);
1707        iq.setType(IQ.Type.set);
1708        for (Jid jid : jids) {
1709            // Set the new affiliation.
1710            MUCItem item = new MUCItem(affiliation, jid);
1711            iq.addItem(item);
1712        }
1713
1714        connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
1715    }
1716
1717    private void changeRole(Resourcepart nickname, MUCRole role, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1718        MUCAdmin iq = new MUCAdmin();
1719        iq.setTo(room);
1720        iq.setType(IQ.Type.set);
1721        // Set the new role.
1722        MUCItem item = new MUCItem(role, nickname, reason);
1723        iq.addItem(item);
1724
1725        connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
1726    }
1727
1728    private void changeRole(Collection<Resourcepart> nicknames, MUCRole role) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
1729        MUCAdmin iq = new MUCAdmin();
1730        iq.setTo(room);
1731        iq.setType(IQ.Type.set);
1732        for (Resourcepart nickname : nicknames) {
1733            // Set the new role.
1734            MUCItem item = new MUCItem(role, nickname);
1735            iq.addItem(item);
1736        }
1737
1738        connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
1739    }
1740
1741    /**
1742     * Returns the number of occupants in the group chat.<p>
1743     *
1744     * Note: this value will only be accurate after joining the group chat, and
1745     * may fluctuate over time. If you query this value directly after joining the
1746     * group chat it may not be accurate, as it takes a certain amount of time for
1747     * the server to send all presence packets to this client.
1748     *
1749     * @return the number of occupants in the group chat.
1750     */
1751    public int getOccupantsCount() {
1752        return occupantsMap.size();
1753    }
1754
1755    /**
1756     * Returns an List  for the list of fully qualified occupants
1757     * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
1758     * Typically, a client would only display the nickname of the occupant. To
1759     * get the nickname from the fully qualified name, use the
1760     * {@link org.jxmpp.util.XmppStringUtils#parseResource(String)} method.
1761     * Note: this value will only be accurate after joining the group chat, and may
1762     * fluctuate over time.
1763     *
1764     * @return a List of the occupants in the group chat.
1765     */
1766    public List<EntityFullJid> getOccupants() {
1767        return new ArrayList<>(occupantsMap.keySet());
1768    }
1769
1770    /**
1771     * Returns the presence info for a particular user, or <code>null</code> if the user
1772     * is not in the room.<p>
1773     *
1774     * @param user the room occupant to search for his presence. The format of user must
1775     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1776     * @return the occupant's current presence, or <code>null</code> if the user is unavailable
1777     *      or if no presence information is available.
1778     */
1779    public Presence getOccupantPresence(EntityFullJid user) {
1780        return occupantsMap.get(user);
1781    }
1782
1783    /**
1784     * Returns the Occupant information for a particular occupant, or <code>null</code> if the
1785     * user is not in the room. The Occupant object may include information such as full
1786     * JID of the user as well as the role and affiliation of the user in the room.<p>
1787     *
1788     * @param user the room occupant to search for his presence. The format of user must
1789     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1790     * @return the Occupant or <code>null</code> if the user is unavailable (i.e. not in the room).
1791     */
1792    public Occupant getOccupant(EntityFullJid user) {
1793        Presence presence = getOccupantPresence(user);
1794        if (presence != null) {
1795            return new Occupant(presence);
1796        }
1797        return null;
1798    }
1799
1800    /**
1801     * Adds a stanza listener that will be notified of any new Presence packets
1802     * sent to the group chat. Using a listener is a suitable way to know when the list
1803     * of occupants should be re-loaded due to any changes.
1804     *
1805     * @param listener a stanza listener that will be notified of any presence packets
1806     *      sent to the group chat.
1807     * @return true if the listener was not already added.
1808     */
1809    public boolean addParticipantListener(PresenceListener listener) {
1810        return presenceListeners.add(listener);
1811    }
1812
1813    /**
1814     * Removes a stanza listener that was being notified of any new Presence packets
1815     * sent to the group chat.
1816     *
1817     * @param listener a stanza listener that was being notified of any presence packets
1818     *      sent to the group chat.
1819     * @return true if the listener was removed, otherwise the listener was not added previously.
1820     */
1821    public boolean removeParticipantListener(PresenceListener listener) {
1822        return presenceListeners.remove(listener);
1823    }
1824
1825    /**
1826     * Returns a list of <code>Affiliate</code> with the room owners.
1827     *
1828     * @return a list of <code>Affiliate</code> with the room owners.
1829     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1830     * @throws NoResponseException if there was no response from the server.
1831     * @throws NotConnectedException if the XMPP connection is not connected.
1832     * @throws InterruptedException if the calling thread was interrupted.
1833     */
1834    public List<Affiliate> getOwners() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1835        return getAffiliatesByAdmin(MUCAffiliation.owner);
1836    }
1837
1838    /**
1839     * Returns a list of <code>Affiliate</code> with the room administrators.
1840     *
1841     * @return a list of <code>Affiliate</code> with the room administrators.
1842     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1843     * @throws NoResponseException if there was no response from the server.
1844     * @throws NotConnectedException if the XMPP connection is not connected.
1845     * @throws InterruptedException if the calling thread was interrupted.
1846     */
1847    public List<Affiliate> getAdmins() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1848        return getAffiliatesByAdmin(MUCAffiliation.admin);
1849    }
1850
1851    /**
1852     * Returns a list of <code>Affiliate</code> with the room members.
1853     *
1854     * @return a list of <code>Affiliate</code> with the room members.
1855     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1856     * @throws NoResponseException if there was no response from the server.
1857     * @throws NotConnectedException if the XMPP connection is not connected.
1858     * @throws InterruptedException if the calling thread was interrupted.
1859     */
1860    public List<Affiliate> getMembers() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
1861        return getAffiliatesByAdmin(MUCAffiliation.member);
1862    }
1863
1864    /**
1865     * Returns a list of <code>Affiliate</code> with the room outcasts.
1866     *
1867     * @return a list of <code>Affiliate</code> with the room outcasts.
1868     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1869     * @throws NoResponseException if there was no response from the server.
1870     * @throws NotConnectedException if the XMPP connection is not connected.
1871     * @throws InterruptedException if the calling thread was interrupted.
1872     */
1873    public List<Affiliate> getOutcasts() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1874        return getAffiliatesByAdmin(MUCAffiliation.outcast);
1875    }
1876
1877    /**
1878     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
1879     * sending a request in the admin namespace.
1880     *
1881     * @param affiliation the affiliation of the users in the room.
1882     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
1883     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1884     * @throws NoResponseException if there was no response from the server.
1885     * @throws NotConnectedException if the XMPP connection is not connected.
1886     * @throws InterruptedException if the calling thread was interrupted.
1887     */
1888    private List<Affiliate> getAffiliatesByAdmin(MUCAffiliation affiliation) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1889        MUCAdmin iq = new MUCAdmin();
1890        iq.setTo(room);
1891        iq.setType(IQ.Type.get);
1892        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
1893        MUCItem item = new MUCItem(affiliation);
1894        iq.addItem(item);
1895
1896        MUCAdmin answer = (MUCAdmin) connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
1897
1898        // Get the list of affiliates from the server's answer
1899        List<Affiliate> affiliates = new ArrayList<Affiliate>();
1900        for (MUCItem mucadminItem : answer.getItems()) {
1901            affiliates.add(new Affiliate(mucadminItem));
1902        }
1903        return affiliates;
1904    }
1905
1906    /**
1907     * Returns a list of <code>Occupant</code> with the room moderators.
1908     *
1909     * @return a list of <code>Occupant</code> with the room moderators.
1910     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1911     * @throws NoResponseException if there was no response from the server.
1912     * @throws NotConnectedException if the XMPP connection is not connected.
1913     * @throws InterruptedException if the calling thread was interrupted.
1914     */
1915    public List<Occupant> getModerators() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1916        return getOccupants(MUCRole.moderator);
1917    }
1918
1919    /**
1920     * Returns a list of <code>Occupant</code> with the room participants.
1921     *
1922     * @return a list of <code>Occupant</code> with the room participants.
1923     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1924     * @throws NoResponseException if there was no response from the server.
1925     * @throws NotConnectedException if the XMPP connection is not connected.
1926     * @throws InterruptedException if the calling thread was interrupted.
1927     */
1928    public List<Occupant> getParticipants() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1929        return getOccupants(MUCRole.participant);
1930    }
1931
1932    /**
1933     * Returns a list of <code>Occupant</code> that have the specified room role.
1934     *
1935     * @param role the role of the occupant in the room.
1936     * @return a list of <code>Occupant</code> that have the specified room role.
1937     * @throws XMPPErrorException if an error occurred while performing the request to the server or you
1938     *         don't have enough privileges to get this information.
1939     * @throws NoResponseException if there was no response from the server.
1940     * @throws NotConnectedException if the XMPP connection is not connected.
1941     * @throws InterruptedException if the calling thread was interrupted.
1942     */
1943    private List<Occupant> getOccupants(MUCRole role) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1944        MUCAdmin iq = new MUCAdmin();
1945        iq.setTo(room);
1946        iq.setType(IQ.Type.get);
1947        // Set the specified role. This may request the list of moderators/participants.
1948        MUCItem item = new MUCItem(role);
1949        iq.addItem(item);
1950
1951        MUCAdmin answer = (MUCAdmin) connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
1952        // Get the list of participants from the server's answer
1953        List<Occupant> participants = new ArrayList<Occupant>();
1954        for (MUCItem mucadminItem : answer.getItems()) {
1955            participants.add(new Occupant(mucadminItem));
1956        }
1957        return participants;
1958    }
1959
1960    /**
1961     * Sends a message to the chat room.
1962     *
1963     * @param text the text of the message to send.
1964     * @throws NotConnectedException if the XMPP connection is not connected.
1965     * @throws InterruptedException if the calling thread was interrupted.
1966     */
1967    public void sendMessage(String text) throws NotConnectedException, InterruptedException {
1968        Message message = buildMessage()
1969                .setBody(text)
1970                .build();
1971        connection.sendStanza(message);
1972    }
1973
1974    /**
1975     * Returns a new Chat for sending private messages to a given room occupant.
1976     * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
1977     * service will change the 'from' address to the sender's room JID and delivering the message
1978     * to the intended recipient's full JID.
1979     *
1980     * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
1981     * @param listener the listener is a message listener that will handle messages for the newly
1982     * created chat.
1983     * @return new Chat for sending private messages to a given room occupant.
1984     */
1985    // TODO This should be made new not using chat.Chat. Private MUC chats are different from XMPP-IM 1:1 chats in to many ways.
1986    // API sketch: PrivateMucChat createPrivateChat(Resourcepart nick)
1987    @SuppressWarnings("deprecation")
1988    public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityFullJid occupant, ChatMessageListener listener) {
1989        return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener);
1990    }
1991
1992    /**
1993     * Creates a new Message to send to the chat room.
1994     *
1995     * @return a new Message addressed to the chat room.
1996     * @deprecated use {@link #buildMessage()} instead.
1997     */
1998    @Deprecated
1999    // TODO: Remove when stanza builder is ready.
2000    public Message createMessage() {
2001        return connection.getStanzaFactory().buildMessageStanza()
2002                .ofType(Message.Type.groupchat)
2003                .to(room)
2004                .build();
2005    }
2006
2007    /**
2008     * Constructs a new message builder for messages send to this MUC room.
2009     *
2010     * @return a new message builder.
2011     */
2012    public MessageBuilder buildMessage() {
2013        return connection.getStanzaFactory()
2014                .buildMessageStanza()
2015                .ofType(Message.Type.groupchat)
2016                .to(room)
2017                ;
2018    }
2019
2020    /**
2021     * Sends a Message to the chat room.
2022     *
2023     * @param message the message.
2024     * @throws NotConnectedException if the XMPP connection is not connected.
2025     * @throws InterruptedException if the calling thread was interrupted.
2026     * @deprecated use {@link #sendMessage(MessageBuilder)} instead.
2027     */
2028    @Deprecated
2029    // TODO: Remove in Smack 4.5.
2030    public void sendMessage(Message message) throws NotConnectedException, InterruptedException {
2031        sendMessage(message.asBuilder());
2032    }
2033
2034    /**
2035     * Sends a Message to the chat room.
2036     *
2037     * @param messageBuilder the message.
2038     * @return a read-only view of the send message.
2039     * @throws NotConnectedException if the XMPP connection is not connected.
2040     * @throws InterruptedException if the calling thread was interrupted.
2041     */
2042    public MessageView sendMessage(MessageBuilder messageBuilder) throws NotConnectedException, InterruptedException {
2043        for (MucMessageInterceptor interceptor : messageInterceptors) {
2044            interceptor.intercept(messageBuilder, this);
2045        }
2046
2047        Message message = messageBuilder.to(room).ofType(Message.Type.groupchat).build();
2048        connection.sendStanza(message);
2049        return message;
2050    }
2051
2052    /**
2053    * Polls for and returns the next message, or <code>null</code> if there isn't
2054    * a message immediately available. This method provides significantly different
2055    * functionalty than the {@link #nextMessage()} method since it's non-blocking.
2056    * In other words, the method call will always return immediately, whereas the
2057    * nextMessage method will return only when a message is available (or after
2058    * a specific timeout).
2059    *
2060    * @return the next message if one is immediately available and
2061    *      <code>null</code> otherwise.
2062     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
2063    */
2064    public Message pollMessage() throws MucNotJoinedException {
2065        if (messageCollector == null) {
2066            throw new MucNotJoinedException(this);
2067        }
2068        return messageCollector.pollResult();
2069    }
2070
2071    /**
2072     * Returns the next available message in the chat. The method call will block
2073     * (not return) until a message is available.
2074     *
2075     * @return the next message.
2076     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
2077     * @throws InterruptedException if the calling thread was interrupted.
2078     */
2079    public Message nextMessage() throws MucNotJoinedException, InterruptedException {
2080        if (messageCollector == null) {
2081            throw new MucNotJoinedException(this);
2082        }
2083        return  messageCollector.nextResult();
2084    }
2085
2086    /**
2087     * Returns the next available message in the chat. The method call will block
2088     * (not return) until a stanza is available or the <code>timeout</code> has elapased.
2089     * If the timeout elapses without a result, <code>null</code> will be returned.
2090     *
2091     * @param timeout the maximum amount of time to wait for the next message.
2092     * @return the next message, or <code>null</code> if the timeout elapses without a
2093     *      message becoming available.
2094     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
2095     * @throws InterruptedException if the calling thread was interrupted.
2096     */
2097    public Message nextMessage(long timeout) throws MucNotJoinedException, InterruptedException {
2098        if (messageCollector == null) {
2099            throw new MucNotJoinedException(this);
2100        }
2101        return messageCollector.nextResult(timeout);
2102    }
2103
2104    /**
2105     * Adds a stanza listener that will be notified of any new messages in the
2106     * group chat. Only "group chat" messages addressed to this group chat will
2107     * be delivered to the listener. If you wish to listen for other packets
2108     * that may be associated with this group chat, you should register a
2109     * PacketListener directly with the XMPPConnection with the appropriate
2110     * PacketListener.
2111     *
2112     * @param listener a stanza listener.
2113     * @return true if the listener was not already added.
2114     */
2115    public boolean addMessageListener(MessageListener listener) {
2116        return messageListeners.add(listener);
2117    }
2118
2119    /**
2120     * Removes a stanza listener that was being notified of any new messages in the
2121     * multi user chat. Only "group chat" messages addressed to this multi user chat were
2122     * being delivered to the listener.
2123     *
2124     * @param listener a stanza listener.
2125     * @return true if the listener was removed, otherwise the listener was not added previously.
2126     */
2127    public boolean removeMessageListener(MessageListener listener) {
2128        return messageListeners.remove(listener);
2129    }
2130
2131    public boolean addMessageInterceptor(MucMessageInterceptor interceptor) {
2132        return messageInterceptors.add(interceptor);
2133    }
2134
2135    public boolean removeMessageInterceptor(MucMessageInterceptor interceptor) {
2136        return messageInterceptors.remove(interceptor);
2137    }
2138
2139    /**
2140     * Changes the subject within the room. As a default, only users with a role of "moderator"
2141     * are allowed to change the subject in a room. Although some rooms may be configured to
2142     * allow a mere participant or even a visitor to change the subject.
2143     *
2144     * @param subject the new room's subject to set.
2145     * @throws XMPPErrorException if someone without appropriate privileges attempts to change the
2146     *          room subject will throw an error with code 403 (i.e. Forbidden)
2147     * @throws NoResponseException if there was no response from the server.
2148     * @throws NotConnectedException if the XMPP connection is not connected.
2149     * @throws InterruptedException if the calling thread was interrupted.
2150     */
2151    public void changeSubject(final String subject) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
2152        MessageBuilder message = buildMessage();
2153        message.setSubject(subject);
2154        // Wait for an error or confirmation message back from the server.
2155        StanzaFilter responseFilter = new AndFilter(fromRoomGroupchatFilter, new StanzaFilter() {
2156            @Override
2157            public boolean accept(Stanza packet) {
2158                Message msg = (Message) packet;
2159                return subject.equals(msg.getSubject());
2160            }
2161        });
2162        StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, message.build());
2163        // Wait up to a certain number of seconds for a reply.
2164        response.nextResultOrThrow();
2165    }
2166
2167    /**
2168     * Remove the connection callbacks (PacketListener, PacketInterceptor, StanzaCollector) used by this MUC from the
2169     * connection.
2170     */
2171    private void removeConnectionCallbacks() {
2172        connection.removeStanzaListener(messageListener);
2173        connection.removeStanzaListener(presenceListener);
2174        connection.removeStanzaListener(subjectListener);
2175        connection.removeStanzaListener(declinesListener);
2176        connection.removeStanzaSendingListener(presenceInterceptor);
2177        if (messageCollector != null) {
2178            messageCollector.cancel();
2179            messageCollector = null;
2180        }
2181    }
2182
2183    /**
2184     * Remove all callbacks and resources necessary when the user has left the room for some reason.
2185     */
2186    private synchronized void userHasLeft() {
2187        // We do not reset nickname here, in case this method has been called erroneously, it should still be possible
2188        // to call leave() in order to resync the state. And leave() requires the nickname to send the unsubscribe
2189        // presence.
2190        occupantsMap.clear();
2191        myRoomJid = null;
2192        // Update the list of joined rooms
2193        multiUserChatManager.removeJoinedRoom(room);
2194        removeConnectionCallbacks();
2195    }
2196
2197    /**
2198     * Adds a listener that will be notified of changes in your status in the room
2199     * such as the user being kicked, banned, or granted admin permissions.
2200     *
2201     * @param listener a user status listener.
2202     * @return true if the user status listener was not already added.
2203     */
2204    public boolean addUserStatusListener(UserStatusListener listener) {
2205        return userStatusListeners.add(listener);
2206    }
2207
2208    /**
2209     * Removes a listener that was being notified of changes in your status in the room
2210     * such as the user being kicked, banned, or granted admin permissions.
2211     *
2212     * @param listener a user status listener.
2213     * @return true if the listener was registered and is now removed.
2214     */
2215    public boolean removeUserStatusListener(UserStatusListener listener) {
2216        return userStatusListeners.remove(listener);
2217    }
2218
2219    /**
2220     * Adds a listener that will be notified of changes in occupants status in the room
2221     * such as the user being kicked, banned, or granted admin permissions.
2222     *
2223     * @param listener a participant status listener.
2224     * @return true if the listener was not already added.
2225     */
2226    public boolean addParticipantStatusListener(ParticipantStatusListener listener) {
2227        return participantStatusListeners.add(listener);
2228    }
2229
2230    /**
2231     * Removes a listener that was being notified of changes in occupants status in the room
2232     * such as the user being kicked, banned, or granted admin permissions.
2233     *
2234     * @param listener a participant status listener.
2235     * @return true if the listener was registered and is now removed.
2236     */
2237    public boolean removeParticipantStatusListener(ParticipantStatusListener listener) {
2238        return participantStatusListeners.remove(listener);
2239    }
2240
2241    /**
2242     * Fires notification events if the role of a room occupant has changed. If the occupant that
2243     * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
2244     * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
2245     * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
2246     * <code>MultiUserChat</code> will be fired. The following table shows the events that will
2247     * be fired depending on the previous and new role of the occupant.
2248     *
2249     * <pre>
2250     * <table border="1">
2251     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
2252     *
2253     * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
2254     * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
2255     * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
2256     *
2257     * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
2258     * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
2259     * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
2260     *
2261     * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
2262     * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
2263     * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
2264     *
2265     * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
2266     * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
2267     * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
2268     * </table>
2269     * </pre>
2270     *
2271     * @param oldRole the previous role of the user in the room before receiving the new presence
2272     * @param newRole the new role of the user in the room after receiving the new presence
2273     * @param isUserModification whether the received presence is about your user in the room or not
2274     * @param from the occupant whose role in the room has changed
2275     * (e.g. room@conference.jabber.org/nick).
2276     */
2277    private void checkRoleModifications(
2278        MUCRole oldRole,
2279        MUCRole newRole,
2280        boolean isUserModification,
2281        EntityFullJid from) {
2282        // Voice was granted to a visitor
2283        if ((MUCRole.visitor.equals(oldRole) || MUCRole.none.equals(oldRole))
2284            && MUCRole.participant.equals(newRole)) {
2285            if (isUserModification) {
2286                for (UserStatusListener listener : userStatusListeners) {
2287                    listener.voiceGranted();
2288                }
2289            }
2290            else {
2291                for (ParticipantStatusListener listener : participantStatusListeners) {
2292                    listener.voiceGranted(from);
2293                }
2294            }
2295        }
2296        // The participant's voice was revoked from the room
2297        else if (
2298            MUCRole.participant.equals(oldRole)
2299                && (MUCRole.visitor.equals(newRole) || MUCRole.none.equals(newRole))) {
2300            if (isUserModification) {
2301                for (UserStatusListener listener : userStatusListeners) {
2302                    listener.voiceRevoked();
2303                }
2304            }
2305            else {
2306                for (ParticipantStatusListener listener : participantStatusListeners) {
2307                    listener.voiceRevoked(from);
2308                }
2309            }
2310        }
2311        // Moderator privileges were granted to a participant
2312        if (!MUCRole.moderator.equals(oldRole) && MUCRole.moderator.equals(newRole)) {
2313            if (MUCRole.visitor.equals(oldRole) || MUCRole.none.equals(oldRole)) {
2314                if (isUserModification) {
2315                    for (UserStatusListener listener : userStatusListeners) {
2316                        listener.voiceGranted();
2317                    }
2318                }
2319                else {
2320                    for (ParticipantStatusListener listener : participantStatusListeners) {
2321                        listener.voiceGranted(from);
2322                    }
2323                }
2324            }
2325            if (isUserModification) {
2326                for (UserStatusListener listener : userStatusListeners) {
2327                    listener.moderatorGranted();
2328                }
2329            }
2330            else {
2331                for (ParticipantStatusListener listener : participantStatusListeners) {
2332                    listener.moderatorGranted(from);
2333                }
2334            }
2335        }
2336        // Moderator privileges were revoked from a participant
2337        else if (MUCRole.moderator.equals(oldRole) && !MUCRole.moderator.equals(newRole)) {
2338            if (MUCRole.visitor.equals(newRole) || MUCRole.none.equals(newRole)) {
2339                if (isUserModification) {
2340                    for (UserStatusListener listener : userStatusListeners) {
2341                        listener.voiceRevoked();
2342                    }
2343                }
2344                else {
2345                    for (ParticipantStatusListener listener : participantStatusListeners) {
2346                        listener.voiceRevoked(from);
2347                    }
2348                }
2349            }
2350            if (isUserModification) {
2351                for (UserStatusListener listener : userStatusListeners) {
2352                    listener.moderatorRevoked();
2353                }
2354            }
2355            else {
2356                for (ParticipantStatusListener listener : participantStatusListeners) {
2357                    listener.moderatorRevoked(from);
2358                }
2359            }
2360        }
2361    }
2362
2363    /**
2364     * Fires notification events if the affiliation of a room occupant has changed. If the
2365     * occupant that changed his affiliation is your occupant then the
2366     * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
2367     * On the other hand, if the occupant that changed his affiliation is not yours then the
2368     * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
2369     * fired. The following table shows the events that will be fired depending on the previous
2370     * and new affiliation of the occupant.
2371     *
2372     * <pre>
2373     * <table border="1">
2374     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
2375     *
2376     * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
2377     * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
2378     * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
2379     *
2380     * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
2381     * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
2382     * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
2383     *
2384     * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
2385     * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
2386     * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
2387     *
2388     * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
2389     * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
2390     * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
2391     * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
2392     * </table>
2393     * </pre>
2394     *
2395     * @param oldAffiliation the previous affiliation of the user in the room before receiving the
2396     * new presence
2397     * @param newAffiliation the new affiliation of the user in the room after receiving the new
2398     * presence
2399     * @param isUserModification whether the received presence is about your user in the room or not
2400     * @param from the occupant whose role in the room has changed
2401     * (e.g. room@conference.jabber.org/nick).
2402     */
2403    private void checkAffiliationModifications(
2404        MUCAffiliation oldAffiliation,
2405        MUCAffiliation newAffiliation,
2406        boolean isUserModification,
2407        EntityFullJid from) {
2408        // First check for revoked affiliation and then for granted affiliations. The idea is to
2409        // first fire the "revoke" events and then fire the "grant" events.
2410
2411        // The user's ownership to the room was revoked
2412        if (MUCAffiliation.owner.equals(oldAffiliation) && !MUCAffiliation.owner.equals(newAffiliation)) {
2413            if (isUserModification) {
2414                for (UserStatusListener listener : userStatusListeners) {
2415                    listener.ownershipRevoked();
2416                }
2417            }
2418            else {
2419                for (ParticipantStatusListener listener : participantStatusListeners) {
2420                    listener.ownershipRevoked(from);
2421                }
2422            }
2423        }
2424        // The user's administrative privileges to the room were revoked
2425        else if (MUCAffiliation.admin.equals(oldAffiliation) && !MUCAffiliation.admin.equals(newAffiliation)) {
2426            if (isUserModification) {
2427                for (UserStatusListener listener : userStatusListeners) {
2428                    listener.adminRevoked();
2429                }
2430            }
2431            else {
2432                for (ParticipantStatusListener listener : participantStatusListeners) {
2433                    listener.adminRevoked(from);
2434                }
2435            }
2436        }
2437        // The user's membership to the room was revoked
2438        else if (MUCAffiliation.member.equals(oldAffiliation) && !MUCAffiliation.member.equals(newAffiliation)) {
2439            if (isUserModification) {
2440                for (UserStatusListener listener : userStatusListeners) {
2441                    listener.membershipRevoked();
2442                }
2443            }
2444            else {
2445                for (ParticipantStatusListener listener : participantStatusListeners) {
2446                    listener.membershipRevoked(from);
2447                }
2448            }
2449        }
2450
2451        // The user was granted ownership to the room
2452        if (!MUCAffiliation.owner.equals(oldAffiliation) && MUCAffiliation.owner.equals(newAffiliation)) {
2453            if (isUserModification) {
2454                for (UserStatusListener listener : userStatusListeners) {
2455                    listener.ownershipGranted();
2456                }
2457            }
2458            else {
2459                for (ParticipantStatusListener listener : participantStatusListeners) {
2460                    listener.ownershipGranted(from);
2461                }
2462            }
2463        }
2464        // The user was granted administrative privileges to the room
2465        else if (!MUCAffiliation.admin.equals(oldAffiliation) && MUCAffiliation.admin.equals(newAffiliation)) {
2466            if (isUserModification) {
2467                for (UserStatusListener listener : userStatusListeners) {
2468                    listener.adminGranted();
2469                }
2470            }
2471            else {
2472                for (ParticipantStatusListener listener : participantStatusListeners) {
2473                    listener.adminGranted(from);
2474                }
2475            }
2476        }
2477        // The user was granted membership to the room
2478        else if (!MUCAffiliation.member.equals(oldAffiliation) && MUCAffiliation.member.equals(newAffiliation)) {
2479            if (isUserModification) {
2480                for (UserStatusListener listener : userStatusListeners) {
2481                    listener.membershipGranted();
2482                }
2483            }
2484            else {
2485                for (ParticipantStatusListener listener : participantStatusListeners) {
2486                    listener.membershipGranted(from);
2487                }
2488            }
2489        }
2490    }
2491
2492    /**
2493     * Fires events according to the received presence code.
2494     *
2495     * @param statusCodes TODO javadoc me please
2496     * @param isUserModification TODO javadoc me please
2497     * @param mucUser TODO javadoc me please
2498     * @param from TODO javadoc me please
2499     */
2500    private void checkPresenceCode(
2501        Set<Status> statusCodes,
2502        boolean isUserModification,
2503        MUCUser mucUser,
2504        EntityFullJid from) {
2505        // Check if an occupant was kicked from the room
2506        if (statusCodes.contains(Status.KICKED_307)) {
2507            // Check if this occupant was kicked
2508            if (isUserModification) {
2509                for (UserStatusListener listener : userStatusListeners) {
2510                    listener.kicked(mucUser.getItem().getActor(), mucUser.getItem().getReason());
2511                }
2512            }
2513            else {
2514                for (ParticipantStatusListener listener : participantStatusListeners) {
2515                    listener.kicked(from, mucUser.getItem().getActor(), mucUser.getItem().getReason());
2516                }
2517            }
2518        }
2519        // A user was banned from the room
2520        if (statusCodes.contains(Status.BANNED_301)) {
2521            // Check if this occupant was banned
2522            if (isUserModification) {
2523                for (UserStatusListener listener : userStatusListeners) {
2524                    listener.banned(mucUser.getItem().getActor(), mucUser.getItem().getReason());
2525                }
2526            }
2527            else {
2528                for (ParticipantStatusListener listener : participantStatusListeners) {
2529                    listener.banned(from, mucUser.getItem().getActor(), mucUser.getItem().getReason());
2530                }
2531            }
2532        }
2533        // A user's membership was revoked from the room
2534        if (statusCodes.contains(Status.REMOVED_AFFIL_CHANGE_321)) {
2535            // Check if this occupant's membership was revoked
2536            if (isUserModification) {
2537                for (UserStatusListener listener : userStatusListeners) {
2538                    listener.membershipRevoked();
2539                }
2540            }
2541        }
2542        // A occupant has changed his nickname in the room
2543        if (statusCodes.contains(Status.NEW_NICKNAME_303)) {
2544            for (ParticipantStatusListener listener : participantStatusListeners) {
2545                listener.nicknameChanged(from, mucUser.getItem().getNick());
2546            }
2547        }
2548    }
2549
2550    /**
2551     * Get the XMPP connection associated with this chat instance.
2552     *
2553     * @return the associated XMPP connection.
2554     * @since 4.3.0
2555     */
2556    public XMPPConnection getXmppConnection() {
2557        return connection;
2558    }
2559
2560    public boolean serviceSupportsStableIds() {
2561        return mucServiceDiscoInfo.containsFeature(MultiUserChatConstants.STABLE_ID_FEATURE);
2562    }
2563
2564    @Override
2565    public String toString() {
2566        return "MUC: " + room + "(" + connection.getUser() + ")";
2567    }
2568}