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