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