| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| * |
| */ |
| package org.apache.vysper.xmpp.modules.extension.xep0045_muc.handler; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.vysper.compliance.SpecCompliant; |
| import org.apache.vysper.xml.fragment.XMLElement; |
| import org.apache.vysper.xml.fragment.XMLSemanticError; |
| import org.apache.vysper.xmpp.addressing.Entity; |
| import org.apache.vysper.xmpp.addressing.EntityImpl; |
| import org.apache.vysper.xmpp.delivery.failure.DeliveryException; |
| import org.apache.vysper.xmpp.delivery.failure.IgnoreFailureStrategy; |
| import org.apache.vysper.xmpp.modules.core.base.handler.DefaultPresenceHandler; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.MUCStanzaBuilder; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Affiliation; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Conference; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Occupant; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Role; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Room; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.RoomType; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.History; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.MucUserPresenceItem; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.Status; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.X; |
| import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.Status.StatusCode; |
| import org.apache.vysper.xmpp.protocol.NamespaceURIs; |
| import org.apache.vysper.xmpp.server.ServerRuntimeContext; |
| import org.apache.vysper.xmpp.server.SessionContext; |
| import org.apache.vysper.xmpp.stanza.PresenceStanza; |
| import org.apache.vysper.xmpp.stanza.PresenceStanzaType; |
| import org.apache.vysper.xmpp.stanza.Stanza; |
| import org.apache.vysper.xmpp.stanza.StanzaBuilder; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Implementation of <a href="http://xmpp.org/extensions/xep-0045.html">XEP-0045 Multi-user chat</a>. |
| * |
| * |
| * @author The Apache MINA Project (dev@mina.apache.org) |
| */ |
| @SpecCompliant(spec = "xep-0045", section = "7.1", status = SpecCompliant.ComplianceStatus.IN_PROGRESS, coverage = SpecCompliant.ComplianceCoverage.PARTIAL) |
| public class MUCPresenceHandler extends DefaultPresenceHandler { |
| |
| final Logger logger = LoggerFactory.getLogger(MUCPresenceHandler.class); |
| |
| private Conference conference; |
| |
| public MUCPresenceHandler(Conference conference) { |
| this.conference = conference; |
| } |
| |
| @Override |
| protected boolean verifyNamespace(Stanza stanza) { |
| // accept all messages sent to this module |
| return true; |
| } |
| |
| private Stanza createPresenceErrorStanza(Entity from, Entity to, String id, String type, String errorName) { |
| // "Note: If an error occurs in relation to joining a room, the service SHOULD include |
| // the MUC child element (i.e., <x xmlns='http://jabber.org/protocol/muc'/>) in the |
| // <presence/> stanza of type "error"." |
| |
| return MUCHandlerHelper.createErrorStanza("presence", NamespaceURIs.JABBER_CLIENT, from, to, id, type, |
| errorName, Arrays.asList((XMLElement) new X())); |
| } |
| |
| @Override |
| protected Stanza executePresenceLogic(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext, |
| SessionContext sessionContext) { |
| // TODO handle null |
| Entity roomAndNick = stanza.getTo(); |
| |
| Entity occupantJid = stanza.getFrom(); |
| |
| Entity roomJid = roomAndNick.getBareJID(); |
| String nick = roomAndNick.getResource(); |
| |
| // user did not send nick name |
| if (nick == null) { |
| return createPresenceErrorStanza(roomJid, occupantJid, stanza.getID(), "modify", "jid-malformed"); |
| } |
| |
| String type = stanza.getType(); |
| System.out.println("11111" + stanza); |
| if (type == null) { |
| return available(stanza, roomJid, occupantJid, nick, serverRuntimeContext); |
| } else if (type.equals("unavailable")) { |
| return unavailable(stanza, roomJid, occupantJid, nick, serverRuntimeContext); |
| } else { |
| throw new RuntimeException("Presence type not handled by MUC module: " + type); |
| } |
| |
| } |
| |
| private String getInnerElementText(XMLElement element, String childName) { |
| try { |
| XMLElement childElm = element.getSingleInnerElementsNamed(childName); |
| if (childElm != null && childElm.getInnerText() != null) { |
| return childElm.getInnerText().getText(); |
| } else { |
| return null; |
| } |
| } catch (XMLSemanticError e) { |
| return null; |
| } |
| } |
| |
| private Stanza available(PresenceStanza stanza, Entity roomJid, Entity newOccupantJid, String nick, |
| ServerRuntimeContext serverRuntimeContext) { |
| |
| // TODO what to use for the room name? |
| Room room = conference.findOrCreateRoom(roomJid, roomJid.getNode()); |
| System.out.println(room); |
| |
| if (room.isInRoom(newOccupantJid)) { |
| // user is already in room, change nick |
| logger.debug("{} has requested to change nick in room {}", newOccupantJid, roomJid); |
| |
| // occupant is already in room |
| Occupant occupant = room.findOccupantByJID(newOccupantJid); |
| if (nick.equals(occupant.getName())) { |
| // nick unchanged, change show and status |
| for (Occupant receiver : room.getOccupants()) { |
| sendChangeShowStatus(occupant, receiver, room, getInnerElementText(stanza, "show"), |
| getInnerElementText(stanza, "status"), serverRuntimeContext); |
| } |
| } else { |
| if (room.isInRoom(nick)) { |
| // user with this nick is already in room |
| return createPresenceErrorStanza(roomJid, newOccupantJid, stanza.getID(), "cancel", "conflict"); |
| } |
| |
| String oldNick = occupant.getName(); |
| // update the nick |
| occupant.setName(nick); |
| |
| // send out unavailable presences to all existing occupants |
| for (Occupant receiver : room.getOccupants()) { |
| sendChangeNickUnavailable(occupant, oldNick, receiver, room, serverRuntimeContext); |
| } |
| |
| // send out available presences to all existing occupants |
| for (Occupant receiver : room.getOccupants()) { |
| sendChangeNickAvailable(occupant, receiver, room, serverRuntimeContext); |
| } |
| |
| } |
| } else { |
| logger.debug("{} has requested to enter room {}", newOccupantJid, roomJid); |
| |
| if (room.isInRoom(nick)) { |
| // user with this nick is already in room |
| return createPresenceErrorStanza(roomJid, newOccupantJid, stanza.getID(), "cancel", "conflict"); |
| } |
| |
| // check password if password protected |
| if (room.isRoomType(RoomType.PasswordProtected)) { |
| X x = X.fromStanza(stanza); |
| String password = null; |
| if (x != null) { |
| password = x.getPasswordValue(); |
| } |
| |
| if (password == null || !password.equals(room.getPassword())) { |
| // password missing or not matching |
| return createPresenceErrorStanza(roomJid, newOccupantJid, stanza.getID(), "auth", "not-authorized"); |
| } |
| } |
| |
| Occupant newOccupant = room.addOccupant(newOccupantJid, nick); |
| if (newOccupant == null) { |
| // outcast |
| return createPresenceErrorStanza(roomJid, newOccupantJid, stanza.getID(), "auth", "forbidden"); |
| } |
| if (room.isRoomType(RoomType.MembersOnly) && newOccupant.getAffiliation() == Affiliation.None) { |
| // non-member can not enter members only room |
| return createPresenceErrorStanza(roomJid, newOccupantJid, stanza.getID(), "auth", |
| "registration-required"); |
| } |
| |
| // relay presence of all existing room occupants to the now joined occupant |
| for (Occupant occupant : room.getOccupants()) { |
| sendExistingOccupantToNewOccupant(newOccupant, occupant, room, serverRuntimeContext); |
| } |
| |
| // relay presence of the newly added occupant to all existing occupants |
| for (Occupant occupant : room.getOccupants()) { |
| sendNewOccupantPresenceToExisting(newOccupant, occupant, room, serverRuntimeContext); |
| } |
| |
| // send discussion history to user |
| boolean includeJid = room.isRoomType(RoomType.NonAnonymous); |
| List<Stanza> history = room.getHistory().createStanzas(newOccupant, includeJid, History.fromStanza(stanza)); |
| relayStanzas(newOccupantJid, history, serverRuntimeContext); |
| |
| logger.debug("{} successfully entered room {}", newOccupantJid, roomJid); |
| } |
| return null; |
| } |
| |
| private Stanza unavailable(PresenceStanza stanza, Entity roomJid, Entity occupantJid, String nick, |
| ServerRuntimeContext serverRuntimeContext) { |
| Room room = conference.findRoom(roomJid); |
| |
| // room must exist, or we do nothing |
| if (room != null) { |
| Occupant exitingOccupant = room.findOccupantByJID(occupantJid); |
| |
| // user must by in room, or we do nothing |
| if (exitingOccupant != null) { |
| Set<Occupant> allOccupants = room.getOccupants(); |
| |
| room.removeOccupant(occupantJid); |
| |
| // TODO replace with use of X |
| String statusMessage = null; |
| try { |
| XMLElement statusElement = stanza.getSingleInnerElementsNamed("status"); |
| if (statusElement != null && statusElement.getInnerText() != null) { |
| statusMessage = statusElement.getInnerText().getText(); |
| } |
| } catch (XMLSemanticError e) { |
| // ignore, status element did not exist |
| } |
| |
| // relay presence of the newly added occupant to all existing occupants |
| for (Occupant occupant : allOccupants) { |
| sendExitRoomPresenceToExisting(exitingOccupant, occupant, room, statusMessage, serverRuntimeContext); |
| } |
| |
| if (room.isRoomType(RoomType.Temporary) && room.isEmpty()) { |
| conference.deleteRoom(roomJid); |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| private void sendExistingOccupantToNewOccupant(Occupant newOccupant, Occupant existingOccupant, Room room, |
| ServerRuntimeContext serverRuntimeContext) { |
| // <presence |
| // from='darkcave@chat.shakespeare.lit/firstwitch' |
| // to='hag66@shakespeare.lit/pda'> |
| // <x xmlns='http://jabber.org/protocol/muc#user'> |
| // <item affiliation='owner' role='moderator'/> |
| // </x> |
| // </presence> |
| |
| // do not send own presence |
| if (existingOccupant.getJid().equals(newOccupant.getJid())) { |
| return; |
| } |
| |
| Entity roomAndOccupantNick = new EntityImpl(room.getJID(), existingOccupant.getName()); |
| Stanza presenceToNewOccupant = MUCStanzaBuilder.createPresenceStanza(roomAndOccupantNick, newOccupant.getJid(), |
| null, NamespaceURIs.XEP0045_MUC_USER, new MucUserPresenceItem(existingOccupant.getAffiliation(), |
| existingOccupant.getRole())); |
| |
| logger.debug("Room presence from {} sent to {}", newOccupant, roomAndOccupantNick); |
| relayStanza(newOccupant.getJid(), presenceToNewOccupant, serverRuntimeContext); |
| } |
| |
| private void sendNewOccupantPresenceToExisting(Occupant newOccupant, Occupant existingOccupant, Room room, |
| ServerRuntimeContext serverRuntimeContext) { |
| Entity roomAndNewUserNick = new EntityImpl(room.getJID(), newOccupant.getName()); |
| |
| List<XMLElement> inner = new ArrayList<XMLElement>(); |
| |
| // room is non-anonymous or semi-anonymous and the occupant a moderator, send full user JID |
| boolean includeJid = room.getRoomTypes().contains(RoomType.NonAnonymous) |
| || (room.getRoomTypes().contains(RoomType.SemiAnonymous) && existingOccupant.getRole() == Role.Moderator); |
| inner.add(new MucUserPresenceItem(newOccupant, includeJid, false)); |
| |
| if (existingOccupant.getJid().equals(newOccupant.getJid())) { |
| |
| if (room.getRoomTypes().contains(RoomType.NonAnonymous)) { |
| // notify the user that this is a non-anonymous room |
| inner.add(new Status(StatusCode.ROOM_NON_ANONYMOUS)); |
| } |
| |
| // send status to indicate that this is the users own presence |
| inner.add(new Status(StatusCode.OWN_PRESENCE)); |
| } |
| |
| Stanza presenceToExisting = MUCStanzaBuilder.createPresenceStanza(roomAndNewUserNick, |
| existingOccupant.getJid(), null, NamespaceURIs.XEP0045_MUC_USER, inner); |
| |
| logger.debug("Room presence from {} sent to {}", roomAndNewUserNick, existingOccupant); |
| relayStanza(existingOccupant.getJid(), presenceToExisting, serverRuntimeContext); |
| } |
| |
| private void sendChangeNickUnavailable(Occupant changer, String oldNick, Occupant receiver, Room room, |
| ServerRuntimeContext serverRuntimeContext) { |
| Entity roomAndOldNick = new EntityImpl(room.getJID(), oldNick); |
| |
| List<XMLElement> inner = new ArrayList<XMLElement>(); |
| |
| boolean includeJid = includeJidInItem(room, receiver); |
| inner.add(new MucUserPresenceItem(changer, includeJid, true)); |
| inner.add(new Status(StatusCode.NEW_NICK)); |
| |
| if (receiver.getJid().equals(changer.getJid())) { |
| // send status to indicate that this is the users own presence |
| inner.add(new Status(StatusCode.OWN_PRESENCE)); |
| } |
| Stanza presenceToReceiver = MUCStanzaBuilder.createPresenceStanza(roomAndOldNick, receiver.getJid(), |
| PresenceStanzaType.UNAVAILABLE, NamespaceURIs.XEP0045_MUC_USER, inner); |
| |
| logger.debug("Room presence from {} sent to {}", roomAndOldNick, receiver); |
| relayStanza(receiver.getJid(), presenceToReceiver, serverRuntimeContext); |
| } |
| |
| private void sendChangeShowStatus(Occupant changer, Occupant receiver, Room room, String show, String status, |
| ServerRuntimeContext serverRuntimeContext) { |
| Entity roomAndNick = new EntityImpl(room.getJID(), changer.getName()); |
| |
| StanzaBuilder builder = StanzaBuilder.createPresenceStanza(roomAndNick, receiver.getJid(), null, null, show, |
| status); |
| |
| boolean includeJid = includeJidInItem(room, receiver); |
| // if(receiver.getJid().equals(changer.getJid())) { |
| // // send status to indicate that this is the users own presence |
| // new Status(StatusCode.OWN_PRESENCE).insertElement(builder); |
| // } |
| |
| builder.addPreparedElement(new X(NamespaceURIs.XEP0045_MUC_USER, new MucUserPresenceItem(changer, includeJid, |
| true))); |
| |
| logger.debug("Room presence from {} sent to {}", roomAndNick, receiver); |
| relayStanza(receiver.getJid(), builder.build(), serverRuntimeContext); |
| } |
| |
| private boolean includeJidInItem(Room room, Occupant receiver) { |
| // room is non-anonymous or semi-anonmoys and the occupant a moderator, send full user JID |
| return room.getRoomTypes().contains(RoomType.NonAnonymous) |
| || (room.getRoomTypes().contains(RoomType.SemiAnonymous) && receiver.getRole() == Role.Moderator); |
| } |
| |
| private void sendChangeNickAvailable(Occupant changer, Occupant receiver, Room room, |
| ServerRuntimeContext serverRuntimeContext) { |
| Entity roomAndOldNick = new EntityImpl(room.getJID(), changer.getName()); |
| |
| List<XMLElement> inner = new ArrayList<XMLElement>(); |
| boolean includeJid = includeJidInItem(room, receiver); |
| inner.add(new MucUserPresenceItem(changer, includeJid, false)); |
| |
| if (receiver.getJid().equals(changer.getJid())) { |
| // send status to indicate that this is the users own presence |
| inner.add(new Status(StatusCode.OWN_PRESENCE)); |
| } |
| Stanza presenceToReceiver = MUCStanzaBuilder.createPresenceStanza(roomAndOldNick, receiver.getJid(), null, |
| NamespaceURIs.XEP0045_MUC_USER, inner); |
| |
| relayStanza(receiver.getJid(), presenceToReceiver, serverRuntimeContext); |
| } |
| |
| private void sendExitRoomPresenceToExisting(Occupant exitingOccupant, Occupant existingOccupant, Room room, |
| String statusMessage, ServerRuntimeContext serverRuntimeContext) { |
| Entity roomAndNewUserNick = new EntityImpl(room.getJID(), exitingOccupant.getName()); |
| |
| List<XMLElement> inner = new ArrayList<XMLElement>(); |
| inner.add(new MucUserPresenceItem(null, null, existingOccupant.getAffiliation(), Role.None)); |
| |
| // is this stanza to be sent to the exiting user himself? |
| boolean ownStanza = existingOccupant.getJid().equals(exitingOccupant.getJid()); |
| |
| if (ownStanza || statusMessage != null) { |
| |
| Status status; |
| if (ownStanza) { |
| // send status to indicate that this is the users own presence |
| status = new Status(StatusCode.OWN_PRESENCE, statusMessage); |
| } else { |
| status = new Status(statusMessage); |
| } |
| inner.add(status); |
| } |
| |
| Stanza presenceToExisting = MUCStanzaBuilder.createPresenceStanza(roomAndNewUserNick, |
| existingOccupant.getJid(), PresenceStanzaType.UNAVAILABLE, NamespaceURIs.XEP0045_MUC_USER, inner); |
| |
| relayStanza(existingOccupant.getJid(), presenceToExisting, serverRuntimeContext); |
| } |
| |
| protected void relayStanzas(Entity receiver, List<Stanza> stanzas, ServerRuntimeContext serverRuntimeContext) { |
| for (Stanza stanza : stanzas) { |
| relayStanza(receiver, stanza, serverRuntimeContext); |
| } |
| } |
| |
| protected void relayStanza(Entity receiver, Stanza stanza, ServerRuntimeContext serverRuntimeContext) { |
| try { |
| serverRuntimeContext.getStanzaRelay().relay(receiver, stanza, new IgnoreFailureStrategy()); |
| } catch (DeliveryException e) { |
| logger.warn("presence relaying failed ", e); |
| } |
| } |
| } |