package com.kixeye.kixmpp.server.module.muc; /* * #%L * KIXMPP * %% * Copyright (C) 2014 KIXEYE, Inc * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import com.google.common.collect.Maps; import com.kixeye.kixmpp.server.cluster.message.GetMucRoomNicknamesRequest; import com.kixeye.kixmpp.server.cluster.message.RoomPresenceBroadcastTask; import io.netty.channel.Channel; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import java.util.*; import io.netty.util.concurrent.Promise; import org.jdom2.Element; import org.jdom2.Namespace; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.kixeye.kixmpp.KixmppJid; import com.kixeye.kixmpp.date.XmppDateUtils; import com.kixeye.kixmpp.server.cluster.message.RoomBroadcastTask; import com.kixeye.kixmpp.server.module.bind.BindKixmppServerModule; /** * A simple muc room. * * @author ebahtijaragic */ public class MucRoom { private final MucService service; private final KixmppJid roomJid; private final MucKixmppServerModule mucModule; private final String roomId; private final MucRoomSettings settings; private Map<KixmppJid, MucRole> jidRoles = new HashMap<>(); private Map<KixmppJid, MucAffiliation> jidAffiliations = new HashMap<>(); private Map<KixmppJid, String> nicknamesByBareJid = new HashMap<>(); private Map<String, User> usersByNickname = Maps.newConcurrentMap(); /** * @param service * @param roomJid * @param settings */ public MucRoom(MucService service, KixmppJid roomJid, MucRoomSettings settings) { this.service = service; this.roomJid = roomJid; this.roomId = roomJid.getNode(); this.mucModule = service.getServer().module(MucKixmppServerModule.class); this.settings = new MucRoomSettings(settings); } /** * Getter from roomJid * * @return */ public KixmppJid getRoomJid() { return roomJid; } /** * Adds a user. * * @param jid * @param nickname * @param role * @param affiliation */ public void addUser(KixmppJid jid, String nickname, MucRole role, MucAffiliation affiliation) { jid = jid.withoutResource(); checkForNicknameInUse(nickname, jid); nicknamesByBareJid.put(jid, nickname); jidRoles.put(jid, role); jidAffiliations.put(jid, affiliation); } /** * A user requets to join the room. * @param channel * @param nickname */ public void join(Channel channel, String nickname) { join(channel, nickname, null); } /** * A user requests to join the room. * * @param channel * @param nickname * @param mucStanza */ public void join(final Channel channel, String nickname, Element mucStanza) { KixmppJid jid = channel.attr(BindKixmppServerModule.JID).get(); if (settings.isOpen() && !jidRoles.containsKey(jid.withoutResource())) { addUser(jid, nickname, MucRole.Participant, MucAffiliation.Member); } verifyMembership(jid.withoutResource()); checkForNicknameInUse(nickname, jid); User user = usersByNickname.get(nickname); boolean existingUser = true; if (user == null) { user = new User(nickname, jid.withoutResource()); usersByNickname.put(nickname, user); MucRoomEventHandler handler = service.getServer().getMucRoomEventHandler(); if (handler != null) { handler.userAdded(this, user); } existingUser = false; } Client client = user.addClient(new Client(jid, nickname, channel)); // xep-0045 7.2.3 begin // self presence KixmppJid fromRoomJid = roomJid.withResource(nickname); channel.writeAndFlush(createPresence(fromRoomJid, jid, MucRole.Participant, null)); if (settings.isPresenceEnabled() && !existingUser) { // Send presence from existing occupants to new occupant sendExistingOccupantsPresenceToNewOccupant(user, channel); // Send new occupant's presence to all occupants broadcastPresence(fromRoomJid, MucRole.Participant, null); } // xep-0045 7.2.3 end if (settings.getSubject() != null) { Element message = new Element("message"); message.setAttribute("id", UUID.randomUUID().toString()); message.setAttribute("from", roomJid.withResource(nickname).toString()); message.setAttribute("to", channel.attr(BindKixmppServerModule.JID).get().toString()); message.setAttribute("type", "groupchat"); message.addContent(new Element("subject").setText(settings.getSubject())); channel.writeAndFlush(message); } if (mucStanza != null) { Element history = mucStanza.getChild("history", mucStanza.getNamespace()); if (history != null) { MucHistoryProvider historyProvider = mucModule.getHistoryProvider(); if (historyProvider != null) { Integer maxChars = null; Integer maxStanzas = null; Integer seconds = null; String parsableString = history.getAttributeValue("maxchars"); if (parsableString != null) { try { maxChars = Integer.parseInt(parsableString); } catch (Exception e) {} } parsableString = history.getAttributeValue("maxstanzas"); if (parsableString != null) { try { maxStanzas = Integer.parseInt(parsableString); } catch (Exception e) {} } parsableString = history.getAttributeValue("seconds"); if (parsableString != null) { try { seconds = Integer.parseInt(parsableString); } catch (Exception e) {} } String since = history.getAttributeValue("since"); historyProvider.getHistory(roomJid, user.getBareJid(), maxChars, maxStanzas, seconds, since).addListener(new GenericFutureListener<Future<List<MucHistory>>>() { @Override public void operationComplete(Future<List<MucHistory>> future) throws Exception { if (future.isSuccess()) { List<MucHistory> historyItems = future.get(); if (historyItems != null) { for (MucHistory historyItem : historyItems) { Element message = new Element("message") .setAttribute("id", UUID.randomUUID().toString()) .setAttribute("from", roomJid.withResource(historyItem.getNickname()).toString()) .setAttribute("to", channel.attr(BindKixmppServerModule.JID).get().toString()) .setAttribute("type", "groupchat"); message.addContent(new Element("body").setText(historyItem.getBody())); Element addresses = new Element("addresses", Namespace.getNamespace("http://jabber.org/protocol/address")); addresses.addContent(new Element("address", addresses.getNamespace()).setAttribute("type", "ofrom").setAttribute("jid", historyItem.getFrom().toString())); message.addContent(addresses); message.addContent(new Element("delay", Namespace.getNamespace("urn:xmpp:delay")) .setAttribute("from", roomJid.toString()) .setAttribute("stamp", XmppDateUtils.format(historyItem.getTimestamp()))); channel.write(message); } channel.flush(); } } } }); } } } channel.closeFuture().addListener(new CloseChannelListener(client)); } private void sendExistingOccupantsPresenceToNewOccupant(final User newUser, final Channel channel) { final KixmppJid jid = channel.attr(BindKixmppServerModule.JID).get(); final MucRole role = jidRoles.get(jid.withoutResource()); Promise<Set<String>> promise = service.getServer().createPromise(); promise.addListener(new GenericFutureListener<Future<Set<String>>>() { @Override public void operationComplete(Future<Set<String>> future) throws Exception { if (future.isSuccess()) { Set<String> nicknames = future.get(); for (String nickname : nicknames) { if (newUser.getNickname().equals(nickname)) { continue; } Element presence = createPresence(roomJid.withResource(nickname), jid, role, null); channel.write(presence); } if (!nicknames.isEmpty()) { channel.flush(); } } } }); service.getServer().sendMapReduceRequest(new GetMucRoomNicknamesRequest(service.getSubDomain(), roomId, jid, promise)); } private void broadcastPresence(KixmppJid fromRoomJid, MucRole role, String type) { receivePresence(fromRoomJid, role, type); service.getServer().getCluster().sendMessageToAll(new RoomPresenceBroadcastTask(this, service.getSubDomain(), roomId, fromRoomJid, role, type), false); } public void receivePresence(KixmppJid fromRoomJid, MucRole role, String type) { String nickname = fromRoomJid.getResource(); for (User user : usersByNickname.values()) { if (user.getNickname().equals(nickname)) { continue; } user.receivePresence(fromRoomJid, role, type); } } private void checkForNicknameInUse(String nickname, KixmppJid jid) { User user = usersByNickname.get(nickname); if (user != null && !user.getBareJid().equals(jid.withoutResource())) { throw new NicknameInUseException(this, nickname); } } private void verifyMembership(KixmppJid jid) { MucAffiliation affiliation = jidAffiliations.get(jid); if (affiliation == null) { throw new RoomJoinNotAllowedException(this, jid); } MucRole role = jidRoles.get(jid); if (role == null) { switch (affiliation) { case Owner: case Admin: role = MucRole.Moderator; break; case Member: role = MucRole.Participant; break; case None: role = MucRole.Visitor; break; case Outcast: throw new RoomJoinNotAllowedException(this, jid); default: role = MucRole.None; break; } } jidRoles.put(jid, role); jidAffiliations.put(jid, affiliation); } /** * Remove the {@link User} associated with the given {@link KixmppJid} from the * room. This will remove all {@link Client}s associated with the {@link User}. * * @param address * @return */ public boolean removeUser(KixmppJid address) { KixmppJid bareJid = address.withoutResource(); String nickname = nicknamesByBareJid.get(bareJid); if (nickname == null) { //user is not in the room return false; } User user = usersByNickname.get(nickname); if (user == null) { //user is no longer connected to the room return false; } user.removeClients(); removeDisconnectedUser(user); return true; } public boolean userLeft(Channel channel, String nickname) { User user = usersByNickname.get(nickname); if (user == null) { //user not found return false; } Client client = user.getClient(channel); if (client == null) { //client not found return false; } user.removeClient(client); removeDisconnectedUser(user); return true; } /** * A user leaves the room. * * @param client */ private void leave(Client client) { User user = usersByNickname.get(client.getNickname()); if (user != null) { user.removeClient(client); removeDisconnectedUser(user); } } private Element createPresence(KixmppJid from, KixmppJid to, MucRole role, String type) { Element presence = new Element("presence"); presence.setAttribute("id", UUID.randomUUID().toString()); presence.setAttribute("from", from.toString()); presence.setAttribute("to", to.toString()); Element x = new Element("x", Namespace.getNamespace("http://jabber.org/protocol/muc#user")); if (type != null) { presence.setAttribute("type", type); } Element item = new Element("item", Namespace.getNamespace("http://jabber.org/protocol/muc#user")); item.setAttribute("affiliation", "member"); switch (role) { case Participant: item.setAttribute("role", "participant"); break; case Moderator: item.setAttribute("role", "moderator"); break; case Visitor: item.setAttribute("role", "visitor"); break; } x.addContent(item); presence.addContent(x); return presence; } /** * Broadcasts a message using supplied nickname. * * @param fromAddress * @param messages */ public void receiveMessages(KixmppJid fromAddress, boolean sendToCluster, String... messages) { if (fromAddress == null) { return; } MucRole fromRole = jidRoles.get(fromAddress.withoutResource()); if (fromRole == null) { removeUser(fromAddress); return; } switch (fromRole) { case None: case Visitor: // TODO maybe send back and error? return; default: break; } String fromNickname = nicknamesByBareJid.get(fromAddress.withoutResource()); //TODO validate fromAddress is roomJid or is a member of the room KixmppJid fromRoomJid = roomJid.withResource(fromNickname); mucModule.publishMessage(fromRoomJid, fromAddress, fromNickname, messages); receive(fromAddress, fromRoomJid, messages); if (sendToCluster) { service.getServer().getCluster().sendMessageToAll(new RoomBroadcastTask(this, service.getSubDomain(), roomId, fromAddress, fromRoomJid, fromNickname, messages), false); } } public void receive(KixmppJid fromJid, KixmppJid fromRoomJid, String... messages) { MucRoomEventHandler handler = service.getServer().getMucRoomEventHandler(); if (handler != null) { handler.handleMessage(this, fromJid, fromRoomJid, messages); } } /** * Sends an direct invitation to a user. Note: The smack client does not recognize this as a valid room invite. */ public void sendDirectInvite(KixmppJid from, Channel userChannelToInvite, String reason) { Element message = new Element("message"); message.setAttribute("to", userChannelToInvite.attr(BindKixmppServerModule.JID).get().getFullJid()); if (from != null) { message.setAttribute("from", from.getFullJid()); } Element x = new Element("x", Namespace.getNamespace("jabber:x:conference")); x.setAttribute("jid", roomJid.getFullJid()); if (reason != null) { x.setAttribute("reason", reason); } message.addContent(x); userChannelToInvite.writeAndFlush(message); } /** * Sends a mediated invitation to a user. */ public void sendMediatedInvite(KixmppJid from, Channel userChannelToInvite, String reason) { Element invite = new Element("invite"); invite.setAttribute("from", from.getFullJid()); if (reason != null) { Element el = new Element("reason"); el.setText(reason); invite.addContent(el); } Element x = new Element("x", Namespace.getNamespace("http://jabber.org/protocol/muc#user")); x.addContent(invite); Element message = new Element("message"); message.setAttribute("to", userChannelToInvite.attr(BindKixmppServerModule.JID).get().getFullJid()); message.setAttribute("from", roomJid.getFullJid()); message.addContent(x); userChannelToInvite.writeAndFlush(message); } public List<User> getUsers() { return Lists.newArrayList(usersByNickname.values()); } public User getUser(String nickname) { return usersByNickname.get(nickname); } public User getUser(KixmppJid jid) { String nickname = nicknamesByBareJid.get(jid.withoutResource()); if (nickname != null) { return usersByNickname.get(nickname); } return null; } private class CloseChannelListener implements GenericFutureListener<Future<? super Void>> { private final Client client; /** * @param client */ public CloseChannelListener(Client client) { this.client = client; } public void operationComplete(Future<? super Void> future) throws Exception { leave(client); } } private void removeDisconnectedUser(User user) { if (user.getClientCount() == 0) { MucRole role = jidRoles.get(user.getBareJid()); if (settings.isPresenceEnabled()) { broadcastPresence(roomJid.withResource(user.getNickname()), role, "unavailable"); } this.usersByNickname.remove(user.getNickname()); this.jidAffiliations.remove(user.getBareJid()); this.jidRoles.remove(user.getBareJid()); MucRoomEventHandler handler = service.getServer().getMucRoomEventHandler(); if (handler != null) { handler.userRemoved(this, user); } } if (usersByNickname.isEmpty()) { this.service.removeRoom(roomId); } } /** * Represents a single user within a room. A User owns a unique nickname * within the room, but may have multiple Connections (1 per full JID) */ public class User { private String nickname; private KixmppJid bareJid; private Map<Channel, Client> clientsByChannel = new HashMap<>(); private Map<KixmppJid, Client> clientsByAddress = new HashMap<>(); public User(String nickname, KixmppJid bareJid) { this.nickname = nickname; this.bareJid = bareJid.withoutResource(); } public Client addClient(Client client) { Preconditions.checkNotNull(client.getAddress().getResource()); clientsByChannel.put(client.getChannel(), client); clientsByAddress.put(client.getAddress(), client); return client; } public Client getClient(Channel channel) { return clientsByChannel.get(channel); } public Client getClient(KixmppJid address) { return clientsByAddress.get(address); } public KixmppJid getBareJid() { return bareJid; } public void receivePresence(KixmppJid fromRoomJid, MucRole role, String type) { for (Client client : clientsByAddress.values()) { Element presence = createPresence(fromRoomJid, client.getAddress(), role, type); client.getChannel().writeAndFlush(presence); } } public Collection<Client> getConnections() { return clientsByAddress.values(); } public void removeClient(Client client) { clientsByAddress.remove(client.getAddress()); clientsByChannel.remove(client.getChannel()); } public String getNickname() { return nickname; } public int getClientCount() { return clientsByAddress.size(); } public void removeClients() { clientsByAddress.clear(); clientsByChannel.clear(); } } /** * Represents single connected occupant in the room. */ public class Client { private KixmppJid address; private String nickname; private Channel channel; public Client(KixmppJid address, String nickname, Channel channel) { Preconditions.checkNotNull(address.getResource()); this.address = address; this.nickname = nickname; this.channel = channel; } public String getNickname() { return nickname; } public KixmppJid getAddress() { return address; } public Channel getChannel() { return channel; } } }