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