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