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