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