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