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