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