001/** 002 * 003 * Copyright 2016 Fernando Ramirez 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 */ 017package org.jivesoftware.smackx.muclight; 018 019import java.util.HashMap; 020import java.util.List; 021import java.util.Set; 022import java.util.concurrent.CopyOnWriteArraySet; 023 024import org.jivesoftware.smack.MessageListener; 025import org.jivesoftware.smack.SmackException.NoResponseException; 026import org.jivesoftware.smack.SmackException.NotConnectedException; 027import org.jivesoftware.smack.StanzaCollector; 028import org.jivesoftware.smack.StanzaListener; 029import org.jivesoftware.smack.XMPPConnection; 030import org.jivesoftware.smack.XMPPException.XMPPErrorException; 031import org.jivesoftware.smack.chat.ChatMessageListener; 032import org.jivesoftware.smack.filter.AndFilter; 033import org.jivesoftware.smack.filter.FromMatchesFilter; 034import org.jivesoftware.smack.filter.MessageTypeFilter; 035import org.jivesoftware.smack.filter.StanzaFilter; 036import org.jivesoftware.smack.packet.IQ; 037import org.jivesoftware.smack.packet.Message; 038import org.jivesoftware.smack.packet.MessageBuilder; 039import org.jivesoftware.smack.packet.Stanza; 040 041import org.jivesoftware.smackx.muclight.element.MUCLightAffiliationsIQ; 042import org.jivesoftware.smackx.muclight.element.MUCLightChangeAffiliationsIQ; 043import org.jivesoftware.smackx.muclight.element.MUCLightConfigurationIQ; 044import org.jivesoftware.smackx.muclight.element.MUCLightCreateIQ; 045import org.jivesoftware.smackx.muclight.element.MUCLightDestroyIQ; 046import org.jivesoftware.smackx.muclight.element.MUCLightGetAffiliationsIQ; 047import org.jivesoftware.smackx.muclight.element.MUCLightGetConfigsIQ; 048import org.jivesoftware.smackx.muclight.element.MUCLightGetInfoIQ; 049import org.jivesoftware.smackx.muclight.element.MUCLightInfoIQ; 050import org.jivesoftware.smackx.muclight.element.MUCLightSetConfigsIQ; 051 052import org.jxmpp.jid.EntityJid; 053import org.jxmpp.jid.Jid; 054 055/** 056 * MUCLight class. 057 * 058 * @author Fernando Ramirez 059 */ 060public class MultiUserChatLight { 061 062 public static final String NAMESPACE = "urn:xmpp:muclight:0"; 063 064 public static final String AFFILIATIONS = "#affiliations"; 065 public static final String INFO = "#info"; 066 public static final String CONFIGURATION = "#configuration"; 067 public static final String CREATE = "#create"; 068 public static final String DESTROY = "#destroy"; 069 public static final String BLOCKING = "#blocking"; 070 071 private final XMPPConnection connection; 072 private final EntityJid room; 073 074 private final Set<MessageListener> messageListeners = new CopyOnWriteArraySet<MessageListener>(); 075 076 /** 077 * This filter will match all stanzas send from the groupchat or from one if 078 * the groupchat occupants. 079 */ 080 private final StanzaFilter fromRoomFilter; 081 082 /** 083 * Same as {@link #fromRoomFilter} together with 084 * {@link MessageTypeFilter#GROUPCHAT}. 085 */ 086 private final StanzaFilter fromRoomGroupChatFilter; 087 088 private final StanzaListener messageListener; 089 090 private StanzaCollector messageCollector; 091 092 MultiUserChatLight(XMPPConnection connection, EntityJid room) { 093 this.connection = connection; 094 this.room = room; 095 096 fromRoomFilter = FromMatchesFilter.create(room); 097 fromRoomGroupChatFilter = new AndFilter(fromRoomFilter, MessageTypeFilter.GROUPCHAT); 098 099 messageListener = new StanzaListener() { 100 @Override 101 public void processStanza(Stanza packet) throws NotConnectedException { 102 Message message = (Message) packet; 103 for (MessageListener listener : messageListeners) { 104 listener.processMessage(message); 105 } 106 } 107 }; 108 109 connection.addSyncStanzaListener(messageListener, fromRoomGroupChatFilter); 110 } 111 112 /** 113 * Returns the JID of the room. 114 * 115 * @return the MUCLight room JID. 116 */ 117 public EntityJid getRoom() { 118 return room; 119 } 120 121 /** 122 * Sends a message to the chat room. 123 * 124 * @param text TODO javadoc me please 125 * the text of the message to send. 126 * @throws NotConnectedException if the XMPP connection is not connected. 127 * @throws InterruptedException if the calling thread was interrupted. 128 */ 129 public void sendMessage(String text) throws NotConnectedException, InterruptedException { 130 MessageBuilder message = buildMessage(); 131 message.setBody(text); 132 connection.sendStanza(message.build()); 133 } 134 135 /** 136 * Returns a new Chat for sending private messages to a given room occupant. 137 * The Chat's occupant address is the room's JID (i.e. 138 * roomName@service/nick). The server service will change the 'from' address 139 * to the sender's room JID and delivering the message to the intended 140 * recipient's full JID. 141 * 142 * @param occupant TODO javadoc me please 143 * occupant unique room JID (e.g. 144 * 'darkcave@macbeth.shakespeare.lit/Paul'). 145 * @param listener TODO javadoc me please 146 * the listener is a message listener that will handle messages 147 * for the newly created chat. 148 * @return new Chat for sending private messages to a given room occupant. 149 */ 150 @Deprecated 151 // Do not re-use Chat API, which was designed for XMPP-IM 1:1 chats and not MUClight private chats. 152 public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityJid occupant, ChatMessageListener listener) { 153 return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener); 154 } 155 156 /** 157 * Creates a new Message to send to the chat room. 158 * 159 * @return a new Message addressed to the chat room. 160 * @deprecated use {@link #buildMessage()} instead. 161 */ 162 @Deprecated 163 // TODO: Remove when stanza builder is ready. 164 public Message createMessage() { 165 return connection.getStanzaFactory().buildMessageStanza() 166 .ofType(Message.Type.groupchat) 167 .to(room) 168 .build(); 169 } 170 171 /** 172 * Constructs a new message builder for messages send to this MUC room. 173 * 174 * @return a new message builder. 175 */ 176 public MessageBuilder buildMessage() { 177 return connection.getStanzaFactory() 178 .buildMessageStanza() 179 .ofType(Message.Type.groupchat) 180 .to(room) 181 ; 182 } 183 184 /** 185 * Sends a Message to the chat room. 186 * 187 * @param message TODO javadoc me please 188 * the message. 189 * @throws NotConnectedException if the XMPP connection is not connected. 190 * @throws InterruptedException if the calling thread was interrupted. 191 * @deprecated use {@link #sendMessage(MessageBuilder)} instead. 192 */ 193 @Deprecated 194 // TODO: Remove in Smack 4.5. 195 public void sendMessage(Message message) throws NotConnectedException, InterruptedException { 196 sendMessage(message.asBuilder()); 197 } 198 199 /** 200 * Sends a Message to the chat room. 201 * 202 * @param messageBuilder the message. 203 * @throws NotConnectedException if the XMPP connection is not connected. 204 * @throws InterruptedException if the calling thread was interrupted. 205 */ 206 public void sendMessage(MessageBuilder messageBuilder) throws NotConnectedException, InterruptedException { 207 Message message = messageBuilder.to(room).ofType(Message.Type.groupchat).build(); 208 connection.sendStanza(message); 209 } 210 211 /** 212 * Polls for and returns the next message. 213 * 214 * @return the next message if one is immediately available 215 */ 216 public Message pollMessage() { 217 return messageCollector.pollResult(); 218 } 219 220 /** 221 * Returns the next available message in the chat. The method call will 222 * block (not return) until a message is available. 223 * 224 * @return the next message. 225 * @throws InterruptedException if the calling thread was interrupted. 226 */ 227 public Message nextMessage() throws InterruptedException { 228 return messageCollector.nextResult(); 229 } 230 231 /** 232 * Returns the next available message in the chat. 233 * 234 * @param timeout TODO javadoc me please 235 * the maximum amount of time to wait for the next message. 236 * @return the next message, or null if the timeout elapses without a 237 * message becoming available. 238 * @throws InterruptedException if the calling thread was interrupted. 239 */ 240 public Message nextMessage(long timeout) throws InterruptedException { 241 return messageCollector.nextResult(timeout); 242 } 243 244 /** 245 * Adds a stanza listener that will be notified of any new messages 246 * in the group chat. Only "group chat" messages addressed to this group 247 * chat will be delivered to the listener. 248 * 249 * @param listener TODO javadoc me please 250 * a stanza listener. 251 * @return true if the listener was not already added. 252 */ 253 public boolean addMessageListener(MessageListener listener) { 254 return messageListeners.add(listener); 255 } 256 257 /** 258 * Removes a stanza listener that was being notified of any new 259 * messages in the MUCLight. Only "group chat" messages addressed to this 260 * MUCLight were being delivered to the listener. 261 * 262 * @param listener TODO javadoc me please 263 * a stanza listener. 264 * @return true if the listener was removed, otherwise the listener was not 265 * added previously. 266 */ 267 public boolean removeMessageListener(MessageListener listener) { 268 return messageListeners.remove(listener); 269 } 270 271 /** 272 * Remove the connection callbacks used by this MUC Light from the 273 * connection. 274 */ 275 private void removeConnectionCallbacks() { 276 connection.removeSyncStanzaListener(messageListener); 277 if (messageCollector != null) { 278 messageCollector.cancel(); 279 messageCollector = null; 280 } 281 } 282 283 @Override 284 public String toString() { 285 return "MUC Light: " + room + "(" + connection.getUser() + ")"; 286 } 287 288 /** 289 * Create new MUCLight. 290 * 291 * @param roomName TODO javadoc me please 292 * @param subject TODO javadoc me please 293 * @param customConfigs TODO javadoc me please 294 * @param occupants TODO javadoc me please 295 * @throws Exception TODO javadoc me please 296 */ 297 public void create(String roomName, String subject, HashMap<String, String> customConfigs, List<Jid> occupants) 298 throws Exception { 299 MUCLightCreateIQ createMUCLightIQ = new MUCLightCreateIQ(room, roomName, occupants); 300 301 messageCollector = connection.createStanzaCollector(fromRoomGroupChatFilter); 302 303 try { 304 connection.createStanzaCollectorAndSend(createMUCLightIQ).nextResultOrThrow(); 305 } catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) { 306 removeConnectionCallbacks(); 307 throw e; 308 } 309 } 310 311 /** 312 * Create new MUCLight. 313 * 314 * @param roomName TODO javadoc me please 315 * @param occupants TODO javadoc me please 316 * @throws Exception TODO javadoc me please 317 */ 318 public void create(String roomName, List<Jid> occupants) throws Exception { 319 create(roomName, null, null, occupants); 320 } 321 322 /** 323 * Leave the MUCLight. 324 * 325 * @throws NotConnectedException if the XMPP connection is not connected. 326 * @throws InterruptedException if the calling thread was interrupted. 327 * @throws NoResponseException if there was no response from the remote entity. 328 * @throws XMPPErrorException if there was an XMPP error returned. 329 */ 330 public void leave() throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException { 331 HashMap<Jid, MUCLightAffiliation> affiliations = new HashMap<>(); 332 affiliations.put(connection.getUser(), MUCLightAffiliation.none); 333 334 MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations); 335 IQ responseIq = connection.createStanzaCollectorAndSend(changeAffiliationsIQ).nextResultOrThrow(); 336 boolean roomLeft = responseIq.getType().equals(IQ.Type.result); 337 338 if (roomLeft) { 339 removeConnectionCallbacks(); 340 } 341 } 342 343 /** 344 * Get the MUC Light info. 345 * 346 * @param version TODO javadoc me please 347 * @return the room info 348 * @throws NoResponseException if there was no response from the remote entity. 349 * @throws XMPPErrorException if there was an XMPP error returned. 350 * @throws NotConnectedException if the XMPP connection is not connected. 351 * @throws InterruptedException if the calling thread was interrupted. 352 */ 353 public MUCLightRoomInfo getFullInfo(String version) 354 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 355 MUCLightGetInfoIQ mucLightGetInfoIQ = new MUCLightGetInfoIQ(room, version); 356 357 IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetInfoIQ).nextResultOrThrow(); 358 MUCLightInfoIQ mucLightInfoResponseIQ = (MUCLightInfoIQ) responseIq; 359 360 return new MUCLightRoomInfo(mucLightInfoResponseIQ.getVersion(), room, 361 mucLightInfoResponseIQ.getConfiguration(), mucLightInfoResponseIQ.getOccupants()); 362 } 363 364 /** 365 * Get the MUC Light info. 366 * 367 * @return the room info 368 * @throws NoResponseException if there was no response from the remote entity. 369 * @throws XMPPErrorException if there was an XMPP error returned. 370 * @throws NotConnectedException if the XMPP connection is not connected. 371 * @throws InterruptedException if the calling thread was interrupted. 372 */ 373 public MUCLightRoomInfo getFullInfo() 374 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 375 return getFullInfo(null); 376 } 377 378 /** 379 * Get the MUC Light configuration. 380 * 381 * @param version TODO javadoc me please 382 * @return the room configuration 383 * @throws NoResponseException if there was no response from the remote entity. 384 * @throws XMPPErrorException if there was an XMPP error returned. 385 * @throws NotConnectedException if the XMPP connection is not connected. 386 * @throws InterruptedException if the calling thread was interrupted. 387 */ 388 public MUCLightRoomConfiguration getConfiguration(String version) 389 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 390 MUCLightGetConfigsIQ mucLightGetConfigsIQ = new MUCLightGetConfigsIQ(room, version); 391 IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetConfigsIQ).nextResultOrThrow(); 392 MUCLightConfigurationIQ mucLightConfigurationIQ = (MUCLightConfigurationIQ) responseIq; 393 return mucLightConfigurationIQ.getConfiguration(); 394 } 395 396 /** 397 * Get the MUC Light configuration. 398 * 399 * @return the room configuration 400 * @throws NoResponseException if there was no response from the remote entity. 401 * @throws XMPPErrorException if there was an XMPP error returned. 402 * @throws NotConnectedException if the XMPP connection is not connected. 403 * @throws InterruptedException if the calling thread was interrupted. 404 */ 405 public MUCLightRoomConfiguration getConfiguration() 406 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 407 return getConfiguration(null); 408 } 409 410 /** 411 * Get the MUC Light affiliations. 412 * 413 * @param version TODO javadoc me please 414 * @return the room affiliations 415 * @throws NoResponseException if there was no response from the remote entity. 416 * @throws XMPPErrorException if there was an XMPP error returned. 417 * @throws NotConnectedException if the XMPP connection is not connected. 418 * @throws InterruptedException if the calling thread was interrupted. 419 */ 420 public HashMap<Jid, MUCLightAffiliation> getAffiliations(String version) 421 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 422 MUCLightGetAffiliationsIQ mucLightGetAffiliationsIQ = new MUCLightGetAffiliationsIQ(room, version); 423 424 IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetAffiliationsIQ).nextResultOrThrow(); 425 MUCLightAffiliationsIQ mucLightAffiliationsIQ = (MUCLightAffiliationsIQ) responseIq; 426 427 return mucLightAffiliationsIQ.getAffiliations(); 428 } 429 430 /** 431 * Get the MUC Light affiliations. 432 * 433 * @return the room affiliations 434 * @throws NoResponseException if there was no response from the remote entity. 435 * @throws XMPPErrorException if there was an XMPP error returned. 436 * @throws NotConnectedException if the XMPP connection is not connected. 437 * @throws InterruptedException if the calling thread was interrupted. 438 */ 439 public HashMap<Jid, MUCLightAffiliation> getAffiliations() 440 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 441 return getAffiliations(null); 442 } 443 444 /** 445 * Change the MUC Light affiliations. 446 * 447 * @param affiliations TODO javadoc me please 448 * @throws NoResponseException if there was no response from the remote entity. 449 * @throws XMPPErrorException if there was an XMPP error returned. 450 * @throws NotConnectedException if the XMPP connection is not connected. 451 * @throws InterruptedException if the calling thread was interrupted. 452 */ 453 public void changeAffiliations(HashMap<Jid, MUCLightAffiliation> affiliations) 454 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 455 MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations); 456 connection.createStanzaCollectorAndSend(changeAffiliationsIQ).nextResultOrThrow(); 457 } 458 459 /** 460 * Destroy the MUC Light. Only will work if it is requested by the owner. 461 * 462 * @throws NoResponseException if there was no response from the remote entity. 463 * @throws XMPPErrorException if there was an XMPP error returned. 464 * @throws NotConnectedException if the XMPP connection is not connected. 465 * @throws InterruptedException if the calling thread was interrupted. 466 */ 467 public void destroy() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 468 MUCLightDestroyIQ mucLightDestroyIQ = new MUCLightDestroyIQ(room); 469 IQ responseIq = connection.createStanzaCollectorAndSend(mucLightDestroyIQ).nextResultOrThrow(); 470 boolean roomDestroyed = responseIq.getType().equals(IQ.Type.result); 471 472 if (roomDestroyed) { 473 removeConnectionCallbacks(); 474 } 475 } 476 477 /** 478 * Change the subject of the MUC Light. 479 * 480 * @param subject TODO javadoc me please 481 * @throws NoResponseException if there was no response from the remote entity. 482 * @throws XMPPErrorException if there was an XMPP error returned. 483 * @throws NotConnectedException if the XMPP connection is not connected. 484 * @throws InterruptedException if the calling thread was interrupted. 485 */ 486 public void changeSubject(String subject) 487 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 488 MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, null, subject, null); 489 connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow(); 490 } 491 492 /** 493 * Change the name of the room. 494 * 495 * @param roomName TODO javadoc me please 496 * @throws NoResponseException if there was no response from the remote entity. 497 * @throws XMPPErrorException if there was an XMPP error returned. 498 * @throws NotConnectedException if the XMPP connection is not connected. 499 * @throws InterruptedException if the calling thread was interrupted. 500 */ 501 public void changeRoomName(String roomName) 502 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 503 MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, null); 504 connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow(); 505 } 506 507 /** 508 * Set the room configurations. 509 * 510 * @param customConfigs TODO javadoc me please 511 * @throws NoResponseException if there was no response from the remote entity. 512 * @throws XMPPErrorException if there was an XMPP error returned. 513 * @throws NotConnectedException if the XMPP connection is not connected. 514 * @throws InterruptedException if the calling thread was interrupted. 515 */ 516 public void setRoomConfigs(HashMap<String, String> customConfigs) 517 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 518 setRoomConfigs(null, customConfigs); 519 } 520 521 /** 522 * Set the room configurations. 523 * 524 * @param roomName TODO javadoc me please 525 * @param customConfigs TODO javadoc me please 526 * @throws NoResponseException if there was no response from the remote entity. 527 * @throws XMPPErrorException if there was an XMPP error returned. 528 * @throws NotConnectedException if the XMPP connection is not connected. 529 * @throws InterruptedException if the calling thread was interrupted. 530 */ 531 public void setRoomConfigs(String roomName, HashMap<String, String> customConfigs) 532 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 533 MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, customConfigs); 534 connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow(); 535 } 536 537}