MultiUserChatManager.java

  1. /**
  2.  *
  3.  * Copyright © 2014-2024 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.muc;

  18. import java.lang.ref.WeakReference;
  19. import java.util.ArrayList;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.HashSet;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import java.util.WeakHashMap;
  27. import java.util.concurrent.CopyOnWriteArrayList;
  28. import java.util.concurrent.CopyOnWriteArraySet;
  29. import java.util.logging.Level;
  30. import java.util.logging.Logger;

  31. import org.jivesoftware.smack.ConnectionCreationListener;
  32. import org.jivesoftware.smack.ConnectionListener;
  33. import org.jivesoftware.smack.Manager;
  34. import org.jivesoftware.smack.SmackException.NoResponseException;
  35. import org.jivesoftware.smack.SmackException.NotConnectedException;
  36. import org.jivesoftware.smack.StanzaListener;
  37. import org.jivesoftware.smack.XMPPConnection;
  38. import org.jivesoftware.smack.XMPPConnectionRegistry;
  39. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  40. import org.jivesoftware.smack.filter.AndFilter;
  41. import org.jivesoftware.smack.filter.ExtensionElementFilter;
  42. import org.jivesoftware.smack.filter.MessageTypeFilter;
  43. import org.jivesoftware.smack.filter.NotFilter;
  44. import org.jivesoftware.smack.filter.StanzaExtensionFilter;
  45. import org.jivesoftware.smack.filter.StanzaFilter;
  46. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  47. import org.jivesoftware.smack.packet.Message;
  48. import org.jivesoftware.smack.packet.MessageBuilder;
  49. import org.jivesoftware.smack.packet.Stanza;
  50. import org.jivesoftware.smack.util.Async;
  51. import org.jivesoftware.smack.util.CleaningWeakReferenceMap;

  52. import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
  53. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  54. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  55. import org.jivesoftware.smackx.disco.packet.DiscoverItems;
  56. import org.jivesoftware.smackx.muc.MultiUserChatException.MucNotJoinedException;
  57. import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException;
  58. import org.jivesoftware.smackx.muc.packet.GroupChatInvitation;
  59. import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
  60. import org.jivesoftware.smackx.muc.packet.MUCUser;

  61. import org.jxmpp.jid.DomainBareJid;
  62. import org.jxmpp.jid.EntityBareJid;
  63. import org.jxmpp.jid.EntityFullJid;
  64. import org.jxmpp.jid.EntityJid;
  65. import org.jxmpp.jid.Jid;
  66. import org.jxmpp.jid.parts.Resourcepart;
  67. import org.jxmpp.util.cache.ExpirationCache;

  68. /**
  69.  * A manager for Multi-User Chat rooms.
  70.  * <p>
  71.  * Use {@link #getMultiUserChat(EntityBareJid)} to retrieve an object representing a Multi-User Chat room.
  72.  * </p>
  73.  * <p>
  74.  * <b>Automatic rejoin:</b> The manager supports automatic rejoin of MultiUserChat rooms once the connection got
  75.  * re-established. This mechanism is disabled by default. To enable it, use {@link #setAutoJoinOnReconnect(boolean)}.
  76.  * You can set a {@link AutoJoinFailedCallback} via {@link #setAutoJoinFailedCallback(AutoJoinFailedCallback)} to get
  77.  * notified if this mechanism failed for some reason. Note that as soon as rejoining for a single room failed, no
  78.  * further attempts will be made for the other rooms.
  79.  * </p>
  80.  *
  81.  * Note:
  82.  * For inviting other users to a group chat or listening for such invitations, take a look at the
  83.  * {@link DirectMucInvitationManager} which provides an implementation of XEP-0249: Direct MUC Invitations.
  84.  *
  85.  * @see <a href="http://xmpp.org/extensions/xep-0045.html">XEP-0045: Multi-User Chat</a>
  86.  */
  87. public final class MultiUserChatManager extends Manager {
  88.     private static final String DISCO_NODE = MUCInitialPresence.NAMESPACE + "#rooms";

  89.     private static final Logger LOGGER = Logger.getLogger(MultiUserChatManager.class.getName());

  90.     static {
  91.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  92.             @Override
  93.             public void connectionCreated(final XMPPConnection connection) {
  94.                 // Set on every established connection that this client supports the Multi-User
  95.                 // Chat protocol. This information will be used when another client tries to
  96.                 // discover whether this client supports MUC or not.
  97.                 ServiceDiscoveryManager.getInstanceFor(connection).addFeature(MUCInitialPresence.NAMESPACE);

  98.                 // Set the NodeInformationProvider that will provide information about the
  99.                 // joined rooms whenever a disco request is received
  100.                 final WeakReference<XMPPConnection> weakRefConnection = new WeakReference<XMPPConnection>(connection);
  101.                 ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(DISCO_NODE,
  102.                                 new AbstractNodeInformationProvider() {
  103.                                     @Override
  104.                                     public List<DiscoverItems.Item> getNodeItems() {
  105.                                         XMPPConnection connection = weakRefConnection.get();
  106.                                         if (connection == null)
  107.                                             return Collections.emptyList();
  108.                                         Set<EntityBareJid> joinedRooms = MultiUserChatManager.getInstanceFor(connection).getJoinedRooms();
  109.                                         List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
  110.                                         for (EntityBareJid room : joinedRooms) {
  111.                                             answer.add(new DiscoverItems.Item(room));
  112.                                         }
  113.                                         return answer;
  114.                                     }
  115.                                 });
  116.             }
  117.         });
  118.     }

  119.     private static final Map<XMPPConnection, MultiUserChatManager> INSTANCES = new WeakHashMap<XMPPConnection, MultiUserChatManager>();

  120.     /**
  121.      * Get a instance of a multi user chat manager for the given connection.
  122.      *
  123.      * @param connection TODO javadoc me please
  124.      * @return a multi user chat manager.
  125.      */
  126.     public static synchronized MultiUserChatManager getInstanceFor(XMPPConnection connection) {
  127.         MultiUserChatManager multiUserChatManager = INSTANCES.get(connection);
  128.         if (multiUserChatManager == null) {
  129.             multiUserChatManager = new MultiUserChatManager(connection);
  130.             INSTANCES.put(connection, multiUserChatManager);
  131.         }
  132.         return multiUserChatManager;
  133.     }

  134.     private static final StanzaFilter INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(new MUCUser()),
  135.                     new NotFilter(MessageTypeFilter.ERROR));

  136.     private static final StanzaFilter DIRECT_INVITATION_FILTER =
  137.         new AndFilter(StanzaTypeFilter.MESSAGE,
  138.                       new ExtensionElementFilter<GroupChatInvitation>(GroupChatInvitation.class),
  139.                       NotFilter.of(MUCUser.class),
  140.                       new NotFilter(MessageTypeFilter.ERROR));

  141.     private static final ExpirationCache<DomainBareJid, DiscoverInfo> KNOWN_MUC_SERVICES = new ExpirationCache<>(
  142.         100, 1000 * 60 * 60 * 24);

  143.     private static final Set<MucMessageInterceptor> DEFAULT_MESSAGE_INTERCEPTORS = new HashSet<>();

  144.     private final Set<InvitationListener> invitationsListeners = new CopyOnWriteArraySet<InvitationListener>();

  145.     /**
  146.      * The XMPP addresses of currently joined rooms.
  147.      */
  148.     private final Set<EntityBareJid> joinedRooms = new CopyOnWriteArraySet<>();

  149.     /**
  150.      * A Map of MUC JIDs to {@link MultiUserChat} instances. We use weak references for the values in order to allow
  151.      * those instances to get garbage collected. Note that MultiUserChat instances can not get garbage collected while
  152.      * the user is joined, because then the MUC will have PacketListeners added to the XMPPConnection.
  153.      */
  154.     private final Map<EntityBareJid, WeakReference<MultiUserChat>> multiUserChats = new CleaningWeakReferenceMap<>();

  155.     private boolean autoJoinOnReconnect;

  156.     private AutoJoinFailedCallback autoJoinFailedCallback;

  157.     private AutoJoinSuccessCallback autoJoinSuccessCallback;

  158.     private final ServiceDiscoveryManager serviceDiscoveryManager;

  159.     private MultiUserChatManager(XMPPConnection connection) {
  160.         super(connection);
  161.         serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
  162.         // Listens for all messages that include a MUCUser extension and fire the invitation
  163.         // listeners if the message includes an invitation.
  164.         StanzaListener invitationPacketListener = new StanzaListener() {
  165.             @Override
  166.             public void processStanza(Stanza packet) {
  167.                 final Message message = (Message) packet;
  168.                 // Get the MUCUser extension
  169.                 final MUCUser mucUser = MUCUser.from(message);
  170.                 // Check if the MUCUser extension includes an invitation
  171.                 if (mucUser.getInvite() != null) {
  172.                     EntityBareJid mucJid = message.getFrom().asEntityBareJidIfPossible();
  173.                     if (mucJid == null) {
  174.                         LOGGER.warning("Invite to non bare JID: '" + message.toXML() + "'");
  175.                         return;
  176.                     }
  177.                     // Fire event for invitation listeners
  178.                     final MultiUserChat muc = getMultiUserChat(mucJid);
  179.                     final XMPPConnection connection = connection();
  180.                     final MUCUser.Invite invite = mucUser.getInvite();
  181.                     final EntityJid from = invite.getFrom();
  182.                     final String reason = invite.getReason();
  183.                     final String password = mucUser.getPassword();
  184.                     for (final InvitationListener listener : invitationsListeners) {
  185.                         listener.invitationReceived(connection, muc, from, reason, password, message, invite);
  186.                     }
  187.                 }
  188.             }
  189.         };
  190.         connection.addAsyncStanzaListener(invitationPacketListener, INVITATION_FILTER);

  191.         // Listens for all messages that include an XEP-0249 GroupChatInvitation extension and fire the invitation
  192.         // listeners
  193.         StanzaListener directInvitationStanzaListener = new StanzaListener() {
  194.             @Override
  195.             public void processStanza(Stanza stanza) {
  196.                 final Message message = (Message) stanza;
  197.                 GroupChatInvitation invite =
  198.                     stanza.getExtension(GroupChatInvitation.class);

  199.                 // Fire event for invitation listeners
  200.                 final MultiUserChat muc = getMultiUserChat(invite.getRoomAddress());
  201.                 final XMPPConnection connection = connection();
  202.                 final EntityJid from = message.getFrom().asEntityJidIfPossible();
  203.                 if (from == null) {
  204.                     LOGGER.warning("Group Chat Invitation from non entity JID in '" + message + "'");
  205.                     return;
  206.                 }
  207.                 final String reason = invite.getReason();
  208.                 final String password = invite.getPassword();
  209.                 final MUCUser.Invite mucInvite = new MUCUser.Invite(reason, from, connection.getUser().asEntityBareJid());
  210.                 for (final InvitationListener listener : invitationsListeners) {
  211.                     listener.invitationReceived(connection, muc, from, reason, password, message, mucInvite);
  212.                 }
  213.             }
  214.         };
  215.         connection.addAsyncStanzaListener(directInvitationStanzaListener, DIRECT_INVITATION_FILTER);

  216.         connection.addConnectionListener(new ConnectionListener() {
  217.             @Override
  218.             public void authenticated(XMPPConnection connection, boolean resumed) {
  219.                 if (resumed) return;
  220.                 if (!autoJoinOnReconnect) return;

  221.                 final Set<EntityBareJid> mucs = getJoinedRooms();
  222.                 if (mucs.isEmpty()) return;

  223.                 Async.go(new Runnable() {
  224.                     @Override
  225.                     public void run() {
  226.                         final AutoJoinFailedCallback failedCallback = autoJoinFailedCallback;
  227.                         final AutoJoinSuccessCallback successCallback = autoJoinSuccessCallback;
  228.                         for (EntityBareJid mucJid : mucs) {
  229.                             MultiUserChat muc = getMultiUserChat(mucJid);

  230.                             if (!muc.isJoined()) return;

  231.                             Resourcepart nickname = muc.getNickname();
  232.                             if (nickname == null) return;

  233.                             try {
  234.                                 muc.leave();
  235.                             } catch (NotConnectedException | InterruptedException | MucNotJoinedException
  236.                                             | NoResponseException | XMPPErrorException e) {
  237.                                 if (failedCallback != null) {
  238.                                     failedCallback.autoJoinFailed(muc, e);
  239.                                 } else {
  240.                                     LOGGER.log(Level.WARNING, "Could not leave room", e);
  241.                                 }
  242.                                 return;
  243.                             }
  244.                             try {
  245.                                 muc.join(nickname);
  246.                                 if (successCallback != null) {
  247.                                     successCallback.autoJoinSuccess(muc, nickname);
  248.                                 }
  249.                             } catch (NotAMucServiceException | NoResponseException | XMPPErrorException
  250.                                     | NotConnectedException | InterruptedException e) {
  251.                                 if (failedCallback != null) {
  252.                                     failedCallback.autoJoinFailed(muc, e);
  253.                                 } else {
  254.                                     LOGGER.log(Level.WARNING, "Could not leave room", e);
  255.                                 }
  256.                                 return;
  257.                             }
  258.                         }
  259.                     }

  260.                 });
  261.             }
  262.         });
  263.     }

  264.     /**
  265.      * Creates a multi user chat. Note: no information is sent to or received from the server until you attempt to
  266.      * {@link MultiUserChat#join(org.jxmpp.jid.parts.Resourcepart) join} the chat room. On some server implementations, the room will not be
  267.      * created until the first person joins it.
  268.      * <p>
  269.      * Most XMPP servers use a sub-domain for the chat service (e.g.chat.example.com for the XMPP server example.com).
  270.      * You must ensure that the room address you're trying to connect to includes the proper chat sub-domain.
  271.      * </p>
  272.      *
  273.      * @param jid the name of the room in the form "roomName@service", where "service" is the hostname at which the
  274.      *        multi-user chat service is running. Make sure to provide a valid JID.
  275.      * @return MultiUserChat instance of the room with the given jid.
  276.      */
  277.     public synchronized MultiUserChat getMultiUserChat(EntityBareJid jid) {
  278.         WeakReference<MultiUserChat> weakRefMultiUserChat = multiUserChats.get(jid);
  279.         if (weakRefMultiUserChat == null) {
  280.             return createNewMucAndAddToMap(jid);
  281.         }
  282.         MultiUserChat multiUserChat = weakRefMultiUserChat.get();
  283.         if (multiUserChat == null) {
  284.             return createNewMucAndAddToMap(jid);
  285.         }
  286.         return multiUserChat;
  287.     }

  288.     public static boolean addDefaultMessageInterceptor(MucMessageInterceptor messageInterceptor) {
  289.         synchronized (DEFAULT_MESSAGE_INTERCEPTORS) {
  290.             return DEFAULT_MESSAGE_INTERCEPTORS.add(messageInterceptor);
  291.         }
  292.     }

  293.     public static boolean removeDefaultMessageInterceptor(MucMessageInterceptor messageInterceptor) {
  294.         synchronized (DEFAULT_MESSAGE_INTERCEPTORS) {
  295.             return DEFAULT_MESSAGE_INTERCEPTORS.remove(messageInterceptor);
  296.         }
  297.     }

  298.     private MultiUserChat createNewMucAndAddToMap(EntityBareJid jid) {
  299.         MultiUserChat multiUserChat = new MultiUserChat(connection(), jid, this);
  300.         multiUserChats.put(jid, new WeakReference<MultiUserChat>(multiUserChat));
  301.         return multiUserChat;
  302.     }

  303.     /**
  304.      * Returns true if the specified user supports the Multi-User Chat protocol.
  305.      *
  306.      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
  307.      * @return a boolean indicating whether the specified user supports the MUC protocol.
  308.      * @throws XMPPErrorException if there was an XMPP error returned.
  309.      * @throws NoResponseException if there was no response from the remote entity.
  310.      * @throws NotConnectedException if the XMPP connection is not connected.
  311.      * @throws InterruptedException if the calling thread was interrupted.
  312.      */
  313.     public boolean isServiceEnabled(Jid user) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  314.         return serviceDiscoveryManager.supportsFeature(user, MUCInitialPresence.NAMESPACE);
  315.     }

  316.     /**
  317.      * Returns a Set of the rooms where the user has joined. The Iterator will contain Strings where each String
  318.      * represents a room (e.g. room@muc.jabber.org).
  319.      *
  320.      * Note: In order to get a list of bookmarked (but not necessarily joined) conferences, use
  321.      * {@link org.jivesoftware.smackx.bookmarks.BookmarkManager#getBookmarkedConferences()}.
  322.      *
  323.      * @return a List of the rooms where the user has joined using a given connection.
  324.      */
  325.     public Set<EntityBareJid> getJoinedRooms() {
  326.         return Collections.unmodifiableSet(joinedRooms);
  327.     }

  328.     /**
  329.      * Returns a List of the rooms where the requested user has joined. The Iterator will contain Strings where each
  330.      * String represents a room (e.g. room@muc.jabber.org).
  331.      *
  332.      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
  333.      * @return a List of the rooms where the requested user has joined.
  334.      * @throws XMPPErrorException if there was an XMPP error returned.
  335.      * @throws NoResponseException if there was no response from the remote entity.
  336.      * @throws NotConnectedException if the XMPP connection is not connected.
  337.      * @throws InterruptedException if the calling thread was interrupted.
  338.      */
  339.     public List<EntityBareJid> getJoinedRooms(EntityFullJid user) throws NoResponseException, XMPPErrorException,
  340.                     NotConnectedException, InterruptedException {
  341.         // Send the disco packet to the user
  342.         DiscoverItems result = serviceDiscoveryManager.discoverItems(user, DISCO_NODE);
  343.         List<DiscoverItems.Item> items = result.getItems();
  344.         List<EntityBareJid> answer = new ArrayList<>(items.size());
  345.         // Collect the entityID for each returned item
  346.         for (DiscoverItems.Item item : items) {
  347.             EntityBareJid muc = item.getEntityID().asEntityBareJidIfPossible();
  348.             if (muc == null) {
  349.                 LOGGER.warning("Not a bare JID: " + item.getEntityID());
  350.                 continue;
  351.             }
  352.             answer.add(muc);
  353.         }
  354.         return answer;
  355.     }

  356.     /**
  357.      * Returns the discovered information of a given room without actually having to join the room. The server will
  358.      * provide information only for rooms that are public.
  359.      *
  360.      * @param room the name of the room in the form "roomName@service" of which we want to discover its information.
  361.      * @return the discovered information of a given room without actually having to join the room.
  362.      * @throws XMPPErrorException if there was an XMPP error returned.
  363.      * @throws NoResponseException if there was no response from the remote entity.
  364.      * @throws NotConnectedException if the XMPP connection is not connected.
  365.      * @throws InterruptedException if the calling thread was interrupted.
  366.      */
  367.     public RoomInfo getRoomInfo(EntityBareJid room) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  368.         DiscoverInfo info = serviceDiscoveryManager.discoverInfo(room);
  369.         return new RoomInfo(info);
  370.     }

  371.     /**
  372.      * Returns a collection with the XMPP addresses of the Multi-User Chat services.
  373.      *
  374.      * @return a collection with the XMPP addresses of the Multi-User Chat services.
  375.      * @throws XMPPErrorException if there was an XMPP error returned.
  376.      * @throws NoResponseException if there was no response from the remote entity.
  377.      * @throws NotConnectedException if the XMPP connection is not connected.
  378.      * @throws InterruptedException if the calling thread was interrupted.
  379.      */
  380.     public List<DomainBareJid> getMucServiceDomains() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  381.         return serviceDiscoveryManager.findServices(MUCInitialPresence.NAMESPACE, false, false);
  382.     }

  383.     /**
  384.      * Check if the provided domain bare JID provides a MUC service.
  385.      *
  386.      * @param domainBareJid the domain bare JID to check.
  387.      * @return <code>true</code> if the provided JID provides a MUC service, <code>false</code> otherwise.
  388.      * @throws NoResponseException if there was no response from the remote entity.
  389.      * @throws XMPPErrorException if there was an XMPP error returned.
  390.      * @throws NotConnectedException if the XMPP connection is not connected.
  391.      * @throws InterruptedException if the calling thread was interrupted.
  392.      * @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>
  393.      * @since 4.2
  394.      */
  395.     public boolean providesMucService(DomainBareJid domainBareJid) throws NoResponseException,
  396.                     XMPPErrorException, NotConnectedException, InterruptedException {
  397.         return getMucServiceDiscoInfo(domainBareJid) != null;
  398.     }

  399.     DiscoverInfo getMucServiceDiscoInfo(DomainBareJid mucServiceAddress)
  400.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  401.         DiscoverInfo discoInfo = KNOWN_MUC_SERVICES.get(mucServiceAddress);
  402.         if (discoInfo != null) {
  403.             return discoInfo;
  404.         }

  405.         discoInfo = serviceDiscoveryManager.discoverInfo(mucServiceAddress);
  406.         if (!discoInfo.containsFeature(MUCInitialPresence.NAMESPACE)) {
  407.             return null;
  408.         }

  409.         KNOWN_MUC_SERVICES.put(mucServiceAddress, discoInfo);
  410.         return discoInfo;
  411.     }

  412.     /**
  413.      * Returns a Map of HostedRooms where each HostedRoom has the XMPP address of the room and the room's name.
  414.      * Once discovered the rooms hosted by a chat service it is possible to discover more detailed room information or
  415.      * join the room.
  416.      *
  417.      * @param serviceName the service that is hosting the rooms to discover.
  418.      * @return a map from the room's address to its HostedRoom information.
  419.      * @throws XMPPErrorException if there was an XMPP error returned.
  420.      * @throws NoResponseException if there was no response from the remote entity.
  421.      * @throws NotConnectedException if the XMPP connection is not connected.
  422.      * @throws InterruptedException if the calling thread was interrupted.
  423.      * @throws NotAMucServiceException if the entity is not a MUC service.
  424.      * @since 4.3.1
  425.      */
  426.     public Map<EntityBareJid, HostedRoom> getRoomsHostedBy(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException,
  427.                     NotConnectedException, InterruptedException, NotAMucServiceException {
  428.         if (!providesMucService(serviceName)) {
  429.             throw new NotAMucServiceException(serviceName);
  430.         }
  431.         DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(serviceName);
  432.         List<DiscoverItems.Item> items = discoverItems.getItems();

  433.         Map<EntityBareJid, HostedRoom> answer = new HashMap<>(items.size());
  434.         for (DiscoverItems.Item item : items) {
  435.             HostedRoom hostedRoom = new HostedRoom(item);
  436.             HostedRoom previousRoom = answer.put(hostedRoom.getJid(), hostedRoom);
  437.             assert previousRoom == null;
  438.         }

  439.         return answer;
  440.     }

  441.     /**
  442.      * Informs the sender of an invitation that the invitee declines the invitation. The rejection will be sent to the
  443.      * room which in turn will forward the rejection to the inviter.
  444.      *
  445.      * @param room the room that sent the original invitation.
  446.      * @param inviter the inviter of the declined invitation.
  447.      * @param reason the reason why the invitee is declining the invitation.
  448.      * @throws NotConnectedException if the XMPP connection is not connected.
  449.      * @throws InterruptedException if the calling thread was interrupted.
  450.      */
  451.     public void decline(EntityBareJid room, EntityBareJid inviter, String reason) throws NotConnectedException, InterruptedException {
  452.         XMPPConnection connection = connection();

  453.         MessageBuilder messageBuilder = connection.getStanzaFactory().buildMessageStanza().to(room);

  454.         // Create the MUCUser packet that will include the rejection
  455.         MUCUser mucUser = new MUCUser();
  456.         MUCUser.Decline decline = new MUCUser.Decline(reason, inviter);
  457.         mucUser.setDecline(decline);
  458.         // Add the MUCUser packet that includes the rejection
  459.         messageBuilder.addExtension(mucUser);

  460.         connection.sendStanza(messageBuilder.build());
  461.     }

  462.     /**
  463.      * Adds a listener to invitation notifications. The listener will be fired anytime an invitation is received.
  464.      *
  465.      * @param listener an invitation listener.
  466.      */
  467.     public void addInvitationListener(InvitationListener listener) {
  468.         invitationsListeners.add(listener);
  469.     }

  470.     /**
  471.      * Removes a listener to invitation notifications. The listener will be fired anytime an invitation is received.
  472.      *
  473.      * @param listener an invitation listener.
  474.      */
  475.     public void removeInvitationListener(InvitationListener listener) {
  476.         invitationsListeners.remove(listener);
  477.     }

  478.     /**
  479.      * If automatic join on reconnect is enabled, then the manager will try to auto join MUC rooms after the connection
  480.      * got re-established.
  481.      *
  482.      * @param autoJoin <code>true</code> to enable, <code>false</code> to disable.
  483.      */
  484.     public void setAutoJoinOnReconnect(boolean autoJoin) {
  485.         autoJoinOnReconnect = autoJoin;
  486.     }

  487.     /**
  488.      * Set a callback invoked by this manager when automatic join on reconnect failed. If failedCallback is not
  489.      * <code>null</code>, then automatic rejoin get also enabled.
  490.      *
  491.      * @param failedCallback the callback.
  492.      */
  493.     public void setAutoJoinFailedCallback(AutoJoinFailedCallback failedCallback) {
  494.         autoJoinFailedCallback = failedCallback;
  495.         if (failedCallback != null) {
  496.             setAutoJoinOnReconnect(true);
  497.         }
  498.     }

  499.     /**
  500.      * Set a callback invoked by this manager when automatic join on reconnect success.
  501.      * If successCallback is not <code>null</code>, automatic rejoin will also
  502.      * be enabled.
  503.      *
  504.      * @param successCallback the callback
  505.      */
  506.     public void setAutoJoinSuccessCallback(AutoJoinSuccessCallback successCallback) {
  507.         autoJoinSuccessCallback = successCallback;
  508.         if (successCallback != null) {
  509.             setAutoJoinOnReconnect(true);
  510.         }
  511.     }


  512.     void addJoinedRoom(EntityBareJid room) {
  513.         joinedRooms.add(room);
  514.     }

  515.     void removeJoinedRoom(EntityBareJid room) {
  516.         joinedRooms.remove(room);
  517.     }

  518.     static CopyOnWriteArrayList<MucMessageInterceptor> getMessageInterceptors() {
  519.         synchronized (DEFAULT_MESSAGE_INTERCEPTORS) {
  520.             return new CopyOnWriteArrayList<>(DEFAULT_MESSAGE_INTERCEPTORS);
  521.         }
  522.     }
  523. }