001/** 002 * 003 * Copyright © 2014 Florian Schmaus 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.muc; 018 019import java.lang.ref.WeakReference; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.WeakHashMap; 028import java.util.concurrent.CopyOnWriteArraySet; 029 030import org.jivesoftware.smack.ConnectionCreationListener; 031import org.jivesoftware.smack.Manager; 032import org.jivesoftware.smack.StanzaListener; 033import org.jivesoftware.smack.XMPPConnection; 034import org.jivesoftware.smack.XMPPConnectionRegistry; 035import org.jivesoftware.smack.SmackException.NoResponseException; 036import org.jivesoftware.smack.SmackException.NotConnectedException; 037import org.jivesoftware.smack.XMPPException.XMPPErrorException; 038import org.jivesoftware.smack.filter.AndFilter; 039import org.jivesoftware.smack.filter.MessageTypeFilter; 040import org.jivesoftware.smack.filter.StanzaExtensionFilter; 041import org.jivesoftware.smack.filter.StanzaFilter; 042import org.jivesoftware.smack.filter.NotFilter; 043import org.jivesoftware.smack.filter.StanzaTypeFilter; 044import org.jivesoftware.smack.packet.Message; 045import org.jivesoftware.smack.packet.Stanza; 046import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider; 047import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 048import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 049import org.jivesoftware.smackx.disco.packet.DiscoverItems; 050import org.jivesoftware.smackx.muc.packet.MUCInitialPresence; 051import org.jivesoftware.smackx.muc.packet.MUCUser; 052 053public class MultiUserChatManager extends Manager { 054 private final static String DISCO_NODE = MUCInitialPresence.NAMESPACE + "#rooms"; 055 056 static { 057 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 058 public void connectionCreated(final XMPPConnection connection) { 059 // Set on every established connection that this client supports the Multi-User 060 // Chat protocol. This information will be used when another client tries to 061 // discover whether this client supports MUC or not. 062 ServiceDiscoveryManager.getInstanceFor(connection).addFeature(MUCInitialPresence.NAMESPACE); 063 064 // Set the NodeInformationProvider that will provide information about the 065 // joined rooms whenever a disco request is received 066 final WeakReference<XMPPConnection> weakRefConnection = new WeakReference<XMPPConnection>(connection); 067 ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(DISCO_NODE, 068 new AbstractNodeInformationProvider() { 069 @Override 070 public List<DiscoverItems.Item> getNodeItems() { 071 XMPPConnection connection = weakRefConnection.get(); 072 if (connection == null) 073 return Collections.emptyList(); 074 Set<String> joinedRooms = MultiUserChatManager.getInstanceFor(connection).getJoinedRooms(); 075 List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>(); 076 for (String room : joinedRooms) { 077 answer.add(new DiscoverItems.Item(room)); 078 } 079 return answer; 080 } 081 }); 082 } 083 }); 084 } 085 086 private static final Map<XMPPConnection, MultiUserChatManager> INSTANCES = new WeakHashMap<XMPPConnection, MultiUserChatManager>(); 087 088 /** 089 * Get a instance of a multi user chat manager for the given connection. 090 * 091 * @param connection 092 * @return a multi user chat manager. 093 */ 094 public static synchronized MultiUserChatManager getInstanceFor(XMPPConnection connection) { 095 MultiUserChatManager multiUserChatManager = INSTANCES.get(connection); 096 if (multiUserChatManager == null) { 097 multiUserChatManager = new MultiUserChatManager(connection); 098 INSTANCES.put(connection, multiUserChatManager); 099 } 100 return multiUserChatManager; 101 } 102 103 private static final StanzaFilter INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(new MUCUser()), 104 new NotFilter(MessageTypeFilter.ERROR)); 105 106 private final Set<InvitationListener> invitationsListeners = new CopyOnWriteArraySet<InvitationListener>(); 107 private final Set<String> joinedRooms = new HashSet<String>(); 108 109 /** 110 * A Map of MUC JIDs to {@link MultiUserChat} instances. We use weak references for the values in order to allow 111 * those instances to get garbage collected. Note that MultiUserChat instances can not get garbage collected while 112 * the user is joined, because then the MUC will have PacketListeners added to the XMPPConnection. 113 */ 114 private final Map<String, WeakReference<MultiUserChat>> multiUserChats = new HashMap<String, WeakReference<MultiUserChat>>(); 115 116 private MultiUserChatManager(XMPPConnection connection) { 117 super(connection); 118 // Listens for all messages that include a MUCUser extension and fire the invitation 119 // listeners if the message includes an invitation. 120 StanzaListener invitationPacketListener = new StanzaListener() { 121 public void processPacket(Stanza packet) { 122 final Message message = (Message) packet; 123 // Get the MUCUser extension 124 final MUCUser mucUser = MUCUser.from(message); 125 // Check if the MUCUser extension includes an invitation 126 if (mucUser.getInvite() != null) { 127 // Fire event for invitation listeners 128 final MultiUserChat muc = getMultiUserChat(packet.getFrom()); 129 for (final InvitationListener listener : invitationsListeners) { 130 listener.invitationReceived(connection(), muc, mucUser.getInvite().getFrom(), 131 mucUser.getInvite().getReason(), mucUser.getPassword(), message); 132 } 133 } 134 } 135 }; 136 connection.addAsyncStanzaListener(invitationPacketListener, INVITATION_FILTER); 137 } 138 139 /** 140 * Creates a multi user chat. Note: no information is sent to or received from the server until you attempt to 141 * {@link MultiUserChat#join(String) join} the chat room. On some server implementations, the room will not be 142 * created until the first person joins it. 143 * <p> 144 * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com for the XMPP server example.com). 145 * You must ensure that the room address you're trying to connect to includes the proper chat sub-domain. 146 * </p> 147 * 148 * @param jid the name of the room in the form "roomName@service", where "service" is the hostname at which the 149 * multi-user chat service is running. Make sure to provide a valid JID. 150 */ 151 public synchronized MultiUserChat getMultiUserChat(String jid) { 152 WeakReference<MultiUserChat> weakRefMultiUserChat = multiUserChats.get(jid); 153 if (weakRefMultiUserChat == null) { 154 return createNewMucAndAddToMap(jid); 155 } 156 MultiUserChat multiUserChat = weakRefMultiUserChat.get(); 157 if (multiUserChat == null) { 158 return createNewMucAndAddToMap(jid); 159 } 160 return multiUserChat; 161 } 162 163 private MultiUserChat createNewMucAndAddToMap(String jid) { 164 MultiUserChat multiUserChat = new MultiUserChat(connection(), jid, this); 165 multiUserChats.put(jid, new WeakReference<MultiUserChat>(multiUserChat)); 166 return multiUserChat; 167 } 168 169 /** 170 * Returns true if the specified user supports the Multi-User Chat protocol. 171 * 172 * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com. 173 * @return a boolean indicating whether the specified user supports the MUC protocol. 174 * @throws XMPPErrorException 175 * @throws NoResponseException 176 * @throws NotConnectedException 177 */ 178 public boolean isServiceEnabled(String user) throws NoResponseException, XMPPErrorException, NotConnectedException { 179 return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(user, MUCInitialPresence.NAMESPACE); 180 } 181 182 /** 183 * Returns a Set of the rooms where the user has joined. The Iterator will contain Strings where each String 184 * represents a room (e.g. room@muc.jabber.org). 185 * 186 * @return a List of the rooms where the user has joined using a given connection. 187 */ 188 public Set<String> getJoinedRooms() { 189 return Collections.unmodifiableSet(joinedRooms); 190 } 191 192 /** 193 * Returns a List of the rooms where the requested user has joined. The Iterator will contain Strings where each 194 * String represents a room (e.g. room@muc.jabber.org). 195 * 196 * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com. 197 * @return a List of the rooms where the requested user has joined. 198 * @throws XMPPErrorException 199 * @throws NoResponseException 200 * @throws NotConnectedException 201 */ 202 public List<String> getJoinedRooms(String user) throws NoResponseException, XMPPErrorException, 203 NotConnectedException { 204 // Send the disco packet to the user 205 DiscoverItems result = ServiceDiscoveryManager.getInstanceFor(connection()).discoverItems(user, DISCO_NODE); 206 List<DiscoverItems.Item> items = result.getItems(); 207 List<String> answer = new ArrayList<String>(items.size()); 208 // Collect the entityID for each returned item 209 for (DiscoverItems.Item item : items) { 210 answer.add(item.getEntityID()); 211 } 212 return answer; 213 } 214 215 /** 216 * Returns the discovered information of a given room without actually having to join the room. The server will 217 * provide information only for rooms that are public. 218 * 219 * @param room the name of the room in the form "roomName@service" of which we want to discover its information. 220 * @return the discovered information of a given room without actually having to join the room. 221 * @throws XMPPErrorException 222 * @throws NoResponseException 223 * @throws NotConnectedException 224 */ 225 public RoomInfo getRoomInfo(String room) throws NoResponseException, XMPPErrorException, NotConnectedException { 226 DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection()).discoverInfo(room); 227 return new RoomInfo(info); 228 } 229 230 /** 231 * Returns a collection with the XMPP addresses of the Multi-User Chat services. 232 * 233 * @return a collection with the XMPP addresses of the Multi-User Chat services. 234 * @throws XMPPErrorException 235 * @throws NoResponseException 236 * @throws NotConnectedException 237 */ 238 public List<String> getServiceNames() throws NoResponseException, XMPPErrorException, NotConnectedException { 239 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection()); 240 return sdm.findServices(MUCInitialPresence.NAMESPACE, false, false); 241 } 242 243 /** 244 * Returns a List of HostedRooms where each HostedRoom has the XMPP address of the room and the room's name. 245 * Once discovered the rooms hosted by a chat service it is possible to discover more detailed room information or 246 * join the room. 247 * 248 * @param serviceName the service that is hosting the rooms to discover. 249 * @return a collection of HostedRooms. 250 * @throws XMPPErrorException 251 * @throws NoResponseException 252 * @throws NotConnectedException 253 */ 254 public List<HostedRoom> getHostedRooms(String serviceName) throws NoResponseException, XMPPErrorException, 255 NotConnectedException { 256 ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection()); 257 DiscoverItems discoverItems = discoManager.discoverItems(serviceName); 258 List<DiscoverItems.Item> items = discoverItems.getItems(); 259 List<HostedRoom> answer = new ArrayList<HostedRoom>(items.size()); 260 for (DiscoverItems.Item item : items) { 261 answer.add(new HostedRoom(item)); 262 } 263 return answer; 264 } 265 266 /** 267 * Informs the sender of an invitation that the invitee declines the invitation. The rejection will be sent to the 268 * room which in turn will forward the rejection to the inviter. 269 * 270 * @param room the room that sent the original invitation. 271 * @param inviter the inviter of the declined invitation. 272 * @param reason the reason why the invitee is declining the invitation. 273 * @throws NotConnectedException 274 */ 275 public void decline(String room, String inviter, String reason) throws NotConnectedException { 276 Message message = new Message(room); 277 278 // Create the MUCUser packet that will include the rejection 279 MUCUser mucUser = new MUCUser(); 280 MUCUser.Decline decline = new MUCUser.Decline(); 281 decline.setTo(inviter); 282 decline.setReason(reason); 283 mucUser.setDecline(decline); 284 // Add the MUCUser packet that includes the rejection 285 message.addExtension(mucUser); 286 287 connection().sendStanza(message); 288 } 289 290 /** 291 * Adds a listener to invitation notifications. The listener will be fired anytime an invitation is received. 292 * 293 * @param listener an invitation listener. 294 */ 295 public void addInvitationListener(InvitationListener listener) { 296 invitationsListeners.add(listener); 297 } 298 299 /** 300 * Removes a listener to invitation notifications. The listener will be fired anytime an invitation is received. 301 * 302 * @param listener an invitation listener. 303 */ 304 public void removeInvitationListener(InvitationListener listener) { 305 invitationsListeners.remove(listener); 306 } 307 308 void addJoinedRoom(String room) { 309 joinedRooms.add(room); 310 } 311 312 void removeJoinedRoom(String room) { 313 joinedRooms.remove(room); 314 } 315}