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