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