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