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