001/**
002 *
003 * Copyright © 2014-2021 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.muc;
018
019import java.lang.ref.WeakReference;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.WeakHashMap;
028import java.util.concurrent.CopyOnWriteArrayList;
029import java.util.concurrent.CopyOnWriteArraySet;
030import java.util.logging.Level;
031import java.util.logging.Logger;
032
033import org.jivesoftware.smack.ConnectionCreationListener;
034import org.jivesoftware.smack.ConnectionListener;
035import org.jivesoftware.smack.Manager;
036import org.jivesoftware.smack.SmackException.NoResponseException;
037import org.jivesoftware.smack.SmackException.NotConnectedException;
038import org.jivesoftware.smack.StanzaListener;
039import org.jivesoftware.smack.XMPPConnection;
040import org.jivesoftware.smack.XMPPConnectionRegistry;
041import org.jivesoftware.smack.XMPPException.XMPPErrorException;
042import org.jivesoftware.smack.filter.AndFilter;
043import org.jivesoftware.smack.filter.ExtensionElementFilter;
044import org.jivesoftware.smack.filter.MessageTypeFilter;
045import org.jivesoftware.smack.filter.NotFilter;
046import org.jivesoftware.smack.filter.StanzaExtensionFilter;
047import org.jivesoftware.smack.filter.StanzaFilter;
048import org.jivesoftware.smack.filter.StanzaTypeFilter;
049import org.jivesoftware.smack.packet.Message;
050import org.jivesoftware.smack.packet.MessageBuilder;
051import org.jivesoftware.smack.packet.Stanza;
052import org.jivesoftware.smack.util.Async;
053import org.jivesoftware.smack.util.CleaningWeakReferenceMap;
054
055import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
056import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
057import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
058import org.jivesoftware.smackx.disco.packet.DiscoverItems;
059import org.jivesoftware.smackx.muc.MultiUserChatException.MucNotJoinedException;
060import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException;
061import org.jivesoftware.smackx.muc.packet.GroupChatInvitation;
062import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
063import org.jivesoftware.smackx.muc.packet.MUCUser;
064
065import org.jxmpp.jid.DomainBareJid;
066import org.jxmpp.jid.EntityBareJid;
067import org.jxmpp.jid.EntityFullJid;
068import org.jxmpp.jid.EntityJid;
069import org.jxmpp.jid.Jid;
070import org.jxmpp.jid.parts.Resourcepart;
071import org.jxmpp.util.cache.ExpirationCache;
072
073/**
074 * A manager for Multi-User Chat rooms.
075 * <p>
076 * Use {@link #getMultiUserChat(EntityBareJid)} to retrieve an object representing a Multi-User Chat room.
077 * </p>
078 * <p>
079 * <b>Automatic rejoin:</b> The manager supports automatic rejoin of MultiUserChat rooms once the connection got
080 * re-established. This mechanism is disabled by default. To enable it, use {@link #setAutoJoinOnReconnect(boolean)}.
081 * You can set a {@link AutoJoinFailedCallback} via {@link #setAutoJoinFailedCallback(AutoJoinFailedCallback)} to get
082 * notified if this mechanism failed for some reason. Note that as soon as rejoining for a single room failed, no
083 * further attempts will be made for the other rooms.
084 * </p>
085 *
086 * Note:
087 * For inviting other users to a group chat or listening for such invitations, take a look at the
088 * {@link DirectMucInvitationManager} which provides an implementation of XEP-0249: Direct MUC Invitations.
089 *
090 * @see <a href="http://xmpp.org/extensions/xep-0045.html">XEP-0045: Multi-User Chat</a>
091 */
092public final class MultiUserChatManager extends Manager {
093    private static final String DISCO_NODE = MUCInitialPresence.NAMESPACE + "#rooms";
094
095    private static final Logger LOGGER = Logger.getLogger(MultiUserChatManager.class.getName());
096
097    static {
098        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
099            @Override
100            public void connectionCreated(final XMPPConnection connection) {
101                // Set on every established connection that this client supports the Multi-User
102                // Chat protocol. This information will be used when another client tries to
103                // discover whether this client supports MUC or not.
104                ServiceDiscoveryManager.getInstanceFor(connection).addFeature(MUCInitialPresence.NAMESPACE);
105
106                // Set the NodeInformationProvider that will provide information about the
107                // joined rooms whenever a disco request is received
108                final WeakReference<XMPPConnection> weakRefConnection = new WeakReference<XMPPConnection>(connection);
109                ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(DISCO_NODE,
110                                new AbstractNodeInformationProvider() {
111                                    @Override
112                                    public List<DiscoverItems.Item> getNodeItems() {
113                                        XMPPConnection connection = weakRefConnection.get();
114                                        if (connection == null)
115                                            return Collections.emptyList();
116                                        Set<EntityBareJid> joinedRooms = MultiUserChatManager.getInstanceFor(connection).getJoinedRooms();
117                                        List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
118                                        for (EntityBareJid room : joinedRooms) {
119                                            answer.add(new DiscoverItems.Item(room));
120                                        }
121                                        return answer;
122                                    }
123                                });
124            }
125        });
126    }
127
128    private static final Map<XMPPConnection, MultiUserChatManager> INSTANCES = new WeakHashMap<XMPPConnection, MultiUserChatManager>();
129
130    /**
131     * Get a instance of a multi user chat manager for the given connection.
132     *
133     * @param connection TODO javadoc me please
134     * @return a multi user chat manager.
135     */
136    public static synchronized MultiUserChatManager getInstanceFor(XMPPConnection connection) {
137        MultiUserChatManager multiUserChatManager = INSTANCES.get(connection);
138        if (multiUserChatManager == null) {
139            multiUserChatManager = new MultiUserChatManager(connection);
140            INSTANCES.put(connection, multiUserChatManager);
141        }
142        return multiUserChatManager;
143    }
144
145    private static final StanzaFilter INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(new MUCUser()),
146                    new NotFilter(MessageTypeFilter.ERROR));
147
148    private static final StanzaFilter DIRECT_INVITATION_FILTER =
149        new AndFilter(StanzaTypeFilter.MESSAGE,
150                      new ExtensionElementFilter<GroupChatInvitation>(GroupChatInvitation.class),
151                      NotFilter.of(MUCUser.class),
152                      new NotFilter(MessageTypeFilter.ERROR));
153
154    private static final ExpirationCache<DomainBareJid, DiscoverInfo> KNOWN_MUC_SERVICES = new ExpirationCache<>(
155        100, 1000 * 60 * 60 * 24);
156
157    private static final Set<MucMessageInterceptor> DEFAULT_MESSAGE_INTERCEPTORS = new HashSet<>();
158
159    private final Set<InvitationListener> invitationsListeners = new CopyOnWriteArraySet<InvitationListener>();
160
161    /**
162     * The XMPP addresses of currently joined rooms.
163     */
164    private final Set<EntityBareJid> joinedRooms = new CopyOnWriteArraySet<>();
165
166    /**
167     * A Map of MUC JIDs to {@link MultiUserChat} instances. We use weak references for the values in order to allow
168     * those instances to get garbage collected. Note that MultiUserChat instances can not get garbage collected while
169     * the user is joined, because then the MUC will have PacketListeners added to the XMPPConnection.
170     */
171    private final Map<EntityBareJid, WeakReference<MultiUserChat>> multiUserChats = new CleaningWeakReferenceMap<>();
172
173    private boolean autoJoinOnReconnect;
174
175    private AutoJoinFailedCallback autoJoinFailedCallback;
176
177    private AutoJoinSuccessCallback autoJoinSuccessCallback;
178
179    private final ServiceDiscoveryManager serviceDiscoveryManager;
180
181    private MultiUserChatManager(XMPPConnection connection) {
182        super(connection);
183        serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
184        // Listens for all messages that include a MUCUser extension and fire the invitation
185        // listeners if the message includes an invitation.
186        StanzaListener invitationPacketListener = new StanzaListener() {
187            @Override
188            public void processStanza(Stanza packet) {
189                final Message message = (Message) packet;
190                // Get the MUCUser extension
191                final MUCUser mucUser = MUCUser.from(message);
192                // Check if the MUCUser extension includes an invitation
193                if (mucUser.getInvite() != null) {
194                    EntityBareJid mucJid = message.getFrom().asEntityBareJidIfPossible();
195                    if (mucJid == null) {
196                        LOGGER.warning("Invite to non bare JID: '" + message.toXML() + "'");
197                        return;
198                    }
199                    // Fire event for invitation listeners
200                    final MultiUserChat muc = getMultiUserChat(mucJid);
201                    final XMPPConnection connection = connection();
202                    final MUCUser.Invite invite = mucUser.getInvite();
203                    final EntityJid from = invite.getFrom();
204                    final String reason = invite.getReason();
205                    final String password = mucUser.getPassword();
206                    for (final InvitationListener listener : invitationsListeners) {
207                        listener.invitationReceived(connection, muc, from, reason, password, message, invite);
208                    }
209                }
210            }
211        };
212        connection.addAsyncStanzaListener(invitationPacketListener, INVITATION_FILTER);
213
214        // Listens for all messages that include an XEP-0249 GroupChatInvitation extension and fire the invitation
215        // listeners
216        StanzaListener directInvitationStanzaListener = new StanzaListener() {
217            @Override
218            public void processStanza(Stanza stanza) {
219                final Message message = (Message) stanza;
220                GroupChatInvitation invite =
221                    stanza.getExtension(GroupChatInvitation.class);
222
223                // Fire event for invitation listeners
224                final MultiUserChat muc = getMultiUserChat(invite.getRoomAddress());
225                final XMPPConnection connection = connection();
226                final EntityJid from = message.getFrom().asEntityJidIfPossible();
227                if (from == null) {
228                    LOGGER.warning("Group Chat Invitation from non entity JID in '" + message + "'");
229                    return;
230                }
231                final String reason = invite.getReason();
232                final String password = invite.getPassword();
233                final MUCUser.Invite mucInvite = new MUCUser.Invite(reason, from, connection.getUser().asEntityBareJid());
234                for (final InvitationListener listener : invitationsListeners) {
235                    listener.invitationReceived(connection, muc, from, reason, password, message, mucInvite);
236                }
237            }
238        };
239        connection.addAsyncStanzaListener(directInvitationStanzaListener, DIRECT_INVITATION_FILTER);
240
241        connection.addConnectionListener(new ConnectionListener() {
242            @Override
243            public void authenticated(XMPPConnection connection, boolean resumed) {
244                if (resumed) return;
245                if (!autoJoinOnReconnect) return;
246
247                final Set<EntityBareJid> mucs = getJoinedRooms();
248                if (mucs.isEmpty()) return;
249
250                Async.go(new Runnable() {
251                    @Override
252                    public void run() {
253                        final AutoJoinFailedCallback failedCallback = autoJoinFailedCallback;
254                        final AutoJoinSuccessCallback successCallback = autoJoinSuccessCallback;
255                        for (EntityBareJid mucJid : mucs) {
256                            MultiUserChat muc = getMultiUserChat(mucJid);
257
258                            if (!muc.isJoined()) return;
259
260                            Resourcepart nickname = muc.getNickname();
261                            if (nickname == null) return;
262
263                            try {
264                                muc.leave();
265                            } catch (NotConnectedException | InterruptedException | MucNotJoinedException
266                                            | NoResponseException | XMPPErrorException e) {
267                                if (failedCallback != null) {
268                                    failedCallback.autoJoinFailed(muc, e);
269                                } else {
270                                    LOGGER.log(Level.WARNING, "Could not leave room", e);
271                                }
272                                return;
273                            }
274                            try {
275                                muc.join(nickname);
276                                if (successCallback != null) {
277                                    successCallback.autoJoinSuccess(muc, nickname);
278                                }
279                            } catch (NotAMucServiceException | NoResponseException | XMPPErrorException
280                                    | NotConnectedException | InterruptedException e) {
281                                if (failedCallback != null) {
282                                    failedCallback.autoJoinFailed(muc, e);
283                                } else {
284                                    LOGGER.log(Level.WARNING, "Could not leave room", e);
285                                }
286                                return;
287                            }
288                        }
289                    }
290
291                });
292            }
293        });
294    }
295
296    /**
297     * Creates a multi user chat. Note: no information is sent to or received from the server until you attempt to
298     * {@link MultiUserChat#join(org.jxmpp.jid.parts.Resourcepart) join} the chat room. On some server implementations, the room will not be
299     * created until the first person joins it.
300     * <p>
301     * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com for the XMPP server example.com).
302     * You must ensure that the room address you're trying to connect to includes the proper chat sub-domain.
303     * </p>
304     *
305     * @param jid the name of the room in the form "roomName@service", where "service" is the hostname at which the
306     *        multi-user chat service is running. Make sure to provide a valid JID.
307     * @return MultiUserChat instance of the room with the given jid.
308     */
309    public synchronized MultiUserChat getMultiUserChat(EntityBareJid jid) {
310        WeakReference<MultiUserChat> weakRefMultiUserChat = multiUserChats.get(jid);
311        if (weakRefMultiUserChat == null) {
312            return createNewMucAndAddToMap(jid);
313        }
314        MultiUserChat multiUserChat = weakRefMultiUserChat.get();
315        if (multiUserChat == null) {
316            return createNewMucAndAddToMap(jid);
317        }
318        return multiUserChat;
319    }
320
321    public static boolean addDefaultMessageInterceptor(MucMessageInterceptor messageInterceptor) {
322        synchronized (DEFAULT_MESSAGE_INTERCEPTORS) {
323            return DEFAULT_MESSAGE_INTERCEPTORS.add(messageInterceptor);
324        }
325    }
326
327    public static boolean removeDefaultMessageInterceptor(MucMessageInterceptor messageInterceptor) {
328        synchronized (DEFAULT_MESSAGE_INTERCEPTORS) {
329            return DEFAULT_MESSAGE_INTERCEPTORS.remove(messageInterceptor);
330        }
331    }
332
333    private MultiUserChat createNewMucAndAddToMap(EntityBareJid jid) {
334        MultiUserChat multiUserChat = new MultiUserChat(connection(), jid, this);
335        multiUserChats.put(jid, new WeakReference<MultiUserChat>(multiUserChat));
336        return multiUserChat;
337    }
338
339    /**
340     * Returns true if the specified user supports the Multi-User Chat protocol.
341     *
342     * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
343     * @return a boolean indicating whether the specified user supports the MUC protocol.
344     * @throws XMPPErrorException if there was an XMPP error returned.
345     * @throws NoResponseException if there was no response from the remote entity.
346     * @throws NotConnectedException if the XMPP connection is not connected.
347     * @throws InterruptedException if the calling thread was interrupted.
348     */
349    public boolean isServiceEnabled(Jid user) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
350        return serviceDiscoveryManager.supportsFeature(user, MUCInitialPresence.NAMESPACE);
351    }
352
353    /**
354     * Returns a Set of the rooms where the user has joined. The Iterator will contain Strings where each String
355     * represents a room (e.g. room@muc.jabber.org).
356     *
357     * Note: In order to get a list of bookmarked (but not necessarily joined) conferences, use
358     * {@link org.jivesoftware.smackx.bookmarks.BookmarkManager#getBookmarkedConferences()}.
359     *
360     * @return a List of the rooms where the user has joined using a given connection.
361     */
362    public Set<EntityBareJid> getJoinedRooms() {
363        return Collections.unmodifiableSet(joinedRooms);
364    }
365
366    /**
367     * Returns a List of the rooms where the requested user has joined. The Iterator will contain Strings where each
368     * String represents a room (e.g. room@muc.jabber.org).
369     *
370     * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
371     * @return a List of the rooms where the requested user has joined.
372     * @throws XMPPErrorException if there was an XMPP error returned.
373     * @throws NoResponseException if there was no response from the remote entity.
374     * @throws NotConnectedException if the XMPP connection is not connected.
375     * @throws InterruptedException if the calling thread was interrupted.
376     */
377    public List<EntityBareJid> getJoinedRooms(EntityFullJid user) throws NoResponseException, XMPPErrorException,
378                    NotConnectedException, InterruptedException {
379        // Send the disco packet to the user
380        DiscoverItems result = serviceDiscoveryManager.discoverItems(user, DISCO_NODE);
381        List<DiscoverItems.Item> items = result.getItems();
382        List<EntityBareJid> answer = new ArrayList<>(items.size());
383        // Collect the entityID for each returned item
384        for (DiscoverItems.Item item : items) {
385            EntityBareJid muc = item.getEntityID().asEntityBareJidIfPossible();
386            if (muc == null) {
387                LOGGER.warning("Not a bare JID: " + item.getEntityID());
388                continue;
389            }
390            answer.add(muc);
391        }
392        return answer;
393    }
394
395    /**
396     * Returns the discovered information of a given room without actually having to join the room. The server will
397     * provide information only for rooms that are public.
398     *
399     * @param room the name of the room in the form "roomName@service" of which we want to discover its information.
400     * @return the discovered information of a given room without actually having to join the room.
401     * @throws XMPPErrorException if there was an XMPP error returned.
402     * @throws NoResponseException if there was no response from the remote entity.
403     * @throws NotConnectedException if the XMPP connection is not connected.
404     * @throws InterruptedException if the calling thread was interrupted.
405     */
406    public RoomInfo getRoomInfo(EntityBareJid room) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
407        DiscoverInfo info = serviceDiscoveryManager.discoverInfo(room);
408        return new RoomInfo(info);
409    }
410
411    /**
412     * Returns a collection with the XMPP addresses of the Multi-User Chat services.
413     *
414     * @return a collection with the XMPP addresses of the Multi-User Chat services.
415     * @throws XMPPErrorException if there was an XMPP error returned.
416     * @throws NoResponseException if there was no response from the remote entity.
417     * @throws NotConnectedException if the XMPP connection is not connected.
418     * @throws InterruptedException if the calling thread was interrupted.
419     */
420    public List<DomainBareJid> getMucServiceDomains() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
421        return serviceDiscoveryManager.findServices(MUCInitialPresence.NAMESPACE, false, false);
422    }
423
424    /**
425     * Returns a collection with the XMPP addresses of the Multi-User Chat services.
426     *
427     * @return a collection with the XMPP addresses of the Multi-User Chat services.
428     * @throws XMPPErrorException if there was an XMPP error returned.
429     * @throws NoResponseException if there was no response from the remote entity.
430     * @throws NotConnectedException if the XMPP connection is not connected.
431     * @throws InterruptedException if the calling thread was interrupted.
432     * @deprecated use {@link #getMucServiceDomains()} instead.
433     */
434    // TODO: Remove in Smack 4.5
435    @Deprecated
436    public List<DomainBareJid> getXMPPServiceDomains() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
437        return getMucServiceDomains();
438    }
439
440    /**
441     * Check if the provided domain bare JID provides a MUC service.
442     *
443     * @param domainBareJid the domain bare JID to check.
444     * @return <code>true</code> if the provided JID provides a MUC service, <code>false</code> otherwise.
445     * @throws NoResponseException if there was no response from the remote entity.
446     * @throws XMPPErrorException if there was an XMPP error returned.
447     * @throws NotConnectedException if the XMPP connection is not connected.
448     * @throws InterruptedException if the calling thread was interrupted.
449     * @see <a href="http://xmpp.org/extensions/xep-0045.html#disco-service-features">XEP-45 § 6.2 Discovering the Features Supported by a MUC Service</a>
450     * @since 4.2
451     */
452    public boolean providesMucService(DomainBareJid domainBareJid) throws NoResponseException,
453                    XMPPErrorException, NotConnectedException, InterruptedException {
454        return getMucServiceDiscoInfo(domainBareJid) != null;
455    }
456
457    DiscoverInfo getMucServiceDiscoInfo(DomainBareJid mucServiceAddress)
458                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
459        DiscoverInfo discoInfo = KNOWN_MUC_SERVICES.get(mucServiceAddress);
460        if (discoInfo != null) {
461            return discoInfo;
462        }
463
464        discoInfo = serviceDiscoveryManager.discoverInfo(mucServiceAddress);
465        if (!discoInfo.containsFeature(MUCInitialPresence.NAMESPACE)) {
466            return null;
467        }
468
469        KNOWN_MUC_SERVICES.put(mucServiceAddress, discoInfo);
470        return discoInfo;
471    }
472
473    /**
474     * Returns a Map of HostedRooms where each HostedRoom has the XMPP address of the room and the room's name.
475     * Once discovered the rooms hosted by a chat service it is possible to discover more detailed room information or
476     * join the room.
477     *
478     * @param serviceName the service that is hosting the rooms to discover.
479     * @return a map from the room's address to its HostedRoom information.
480     * @throws XMPPErrorException if there was an XMPP error returned.
481     * @throws NoResponseException if there was no response from the remote entity.
482     * @throws NotConnectedException if the XMPP connection is not connected.
483     * @throws InterruptedException if the calling thread was interrupted.
484     * @throws NotAMucServiceException if the entity is not a MUC serivce.
485     * @since 4.3.1
486     */
487    public Map<EntityBareJid, HostedRoom> getRoomsHostedBy(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException,
488                    NotConnectedException, InterruptedException, NotAMucServiceException {
489        if (!providesMucService(serviceName)) {
490            throw new NotAMucServiceException(serviceName);
491        }
492        DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(serviceName);
493        List<DiscoverItems.Item> items = discoverItems.getItems();
494
495        Map<EntityBareJid, HostedRoom> answer = new HashMap<>(items.size());
496        for (DiscoverItems.Item item : items) {
497            HostedRoom hostedRoom = new HostedRoom(item);
498            HostedRoom previousRoom = answer.put(hostedRoom.getJid(), hostedRoom);
499            assert previousRoom == null;
500        }
501
502        return answer;
503    }
504
505    /**
506     * Informs the sender of an invitation that the invitee declines the invitation. The rejection will be sent to the
507     * room which in turn will forward the rejection to the inviter.
508     *
509     * @param room the room that sent the original invitation.
510     * @param inviter the inviter of the declined invitation.
511     * @param reason the reason why the invitee is declining the invitation.
512     * @throws NotConnectedException if the XMPP connection is not connected.
513     * @throws InterruptedException if the calling thread was interrupted.
514     */
515    public void decline(EntityBareJid room, EntityBareJid inviter, String reason) throws NotConnectedException, InterruptedException {
516        XMPPConnection connection = connection();
517
518        MessageBuilder messageBuilder = connection.getStanzaFactory().buildMessageStanza().to(room);
519
520        // Create the MUCUser packet that will include the rejection
521        MUCUser mucUser = new MUCUser();
522        MUCUser.Decline decline = new MUCUser.Decline(reason, inviter);
523        mucUser.setDecline(decline);
524        // Add the MUCUser packet that includes the rejection
525        messageBuilder.addExtension(mucUser);
526
527        connection.sendStanza(messageBuilder.build());
528    }
529
530    /**
531     * Adds a listener to invitation notifications. The listener will be fired anytime an invitation is received.
532     *
533     * @param listener an invitation listener.
534     */
535    public void addInvitationListener(InvitationListener listener) {
536        invitationsListeners.add(listener);
537    }
538
539    /**
540     * Removes a listener to invitation notifications. The listener will be fired anytime an invitation is received.
541     *
542     * @param listener an invitation listener.
543     */
544    public void removeInvitationListener(InvitationListener listener) {
545        invitationsListeners.remove(listener);
546    }
547
548    /**
549     * If automatic join on reconnect is enabled, then the manager will try to auto join MUC rooms after the connection
550     * got re-established.
551     *
552     * @param autoJoin <code>true</code> to enable, <code>false</code> to disable.
553     */
554    public void setAutoJoinOnReconnect(boolean autoJoin) {
555        autoJoinOnReconnect = autoJoin;
556    }
557
558    /**
559     * Set a callback invoked by this manager when automatic join on reconnect failed. If failedCallback is not
560     * <code>null</code>, then automatic rejoin get also enabled.
561     *
562     * @param failedCallback the callback.
563     */
564    public void setAutoJoinFailedCallback(AutoJoinFailedCallback failedCallback) {
565        autoJoinFailedCallback = failedCallback;
566        if (failedCallback != null) {
567            setAutoJoinOnReconnect(true);
568        }
569    }
570
571    /**
572     * Set a callback invoked by this manager when automatic join on reconnect success.
573     * If successCallback is not <code>null</code>, automatic rejoin will also
574     * be enabled.
575     *
576     * @param successCallback the callback
577     */
578    public void setAutoJoinSuccessCallback(AutoJoinSuccessCallback successCallback) {
579        autoJoinSuccessCallback = successCallback;
580        if (successCallback != null) {
581            setAutoJoinOnReconnect(true);
582        }
583    }
584
585
586    void addJoinedRoom(EntityBareJid room) {
587        joinedRooms.add(room);
588    }
589
590    void removeJoinedRoom(EntityBareJid room) {
591        joinedRooms.remove(room);
592    }
593
594    static CopyOnWriteArrayList<MucMessageInterceptor> getMessageInterceptors() {
595        synchronized (DEFAULT_MESSAGE_INTERCEPTORS) {
596            return new CopyOnWriteArrayList<>(DEFAULT_MESSAGE_INTERCEPTORS);
597        }
598    }
599}