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