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