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