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