/* * 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.List; import org.apache.vysper.compliance.SpecCompliance; import org.apache.vysper.compliance.SpecCompliant; import org.apache.vysper.xml.fragment.Attribute; 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.EntityFormatException; 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.DefaultMessageHandler; import org.apache.vysper.xmpp.modules.extension.xep0045_muc.MUCStanzaBuilder; import org.apache.vysper.xmpp.modules.extension.xep0045_muc.dataforms.VoiceRequestForm; 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.MucUserItem; import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.X; 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.MessageStanza; import org.apache.vysper.xmpp.stanza.MessageStanzaType; import org.apache.vysper.xmpp.stanza.Stanza; import org.apache.vysper.xmpp.stanza.StanzaBuilder; import org.apache.vysper.xmpp.stanza.StanzaErrorCondition; import org.apache.vysper.xmpp.stanza.StanzaErrorType; 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) */ @SpecCompliance(compliant = { @SpecCompliant(spec = "xep-0045", section = "7.9", status = SpecCompliant.ComplianceStatus.IN_PROGRESS, coverage = SpecCompliant.ComplianceCoverage.PARTIAL), @SpecCompliant(spec = "xep-0045", section = "7.9", status = SpecCompliant.ComplianceStatus.IN_PROGRESS, coverage = SpecCompliant.ComplianceCoverage.PARTIAL) }) public class MUCMessageHandler extends DefaultMessageHandler { final Logger logger = LoggerFactory.getLogger(MUCMessageHandler.class); private Conference conference; private Entity moduleDomain; public MUCMessageHandler(Conference conference, Entity moduleDomain) { this.conference = conference; this.moduleDomain = moduleDomain; } @Override protected boolean verifyNamespace(Stanza stanza) { // accept all messages sent to this module return true; } private Stanza createMessageErrorStanza(Entity from, Entity to, String id, StanzaErrorType type, StanzaErrorCondition errorCondition, Stanza stanza) { return MUCHandlerHelper.createErrorStanza("message", NamespaceURIs.JABBER_CLIENT, from, to, id, type.value(), errorCondition.value(), stanza.getInnerElements()); } @Override protected Stanza executeMessageLogic(MessageStanza stanza, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) { logger.debug("Received message for MUC"); Entity from = stanza.getFrom(); Entity roomWithNickJid = stanza.getTo(); Entity roomJid = roomWithNickJid.getBareJID(); MessageStanzaType type = stanza.getMessageType(); if (type != null && type == MessageStanzaType.GROUPCHAT) { // groupchat, message to a room // must not have a nick if (roomWithNickJid.getResource() != null) { return createMessageErrorStanza(roomJid, from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.BAD_REQUEST, stanza); } logger.debug("Received groupchat message to {}", roomJid); Room room = conference.findRoom(roomJid); if (room != null) { Occupant sendingOccupant = room.findOccupantByJID(from); // sender must be participant in room if (sendingOccupant != null) { Entity roomAndSendingNick = new EntityImpl(room.getJID(), sendingOccupant.getNick()); if (sendingOccupant.hasVoice()) { // relay message to all occupants in room try { if (stanza.getSubjects() != null && !stanza.getSubjects().isEmpty()) { // subject message if (!room.isRoomType(RoomType.OpenSubject) && !sendingOccupant.isModerator()) { // room only allows moderators to change the subject, and sender is not a moderator return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.AUTH, StanzaErrorCondition.FORBIDDEN, stanza); } } } catch (XMLSemanticError e) { // not a subject message, ignore exception } logger.debug("Relaying message to all room occupants"); for (Occupant occupent : room.getOccupants()) { logger.debug("Relaying message to {}", occupent); List<Attribute> replaceAttributes = new ArrayList<Attribute>(); replaceAttributes.add(new Attribute("from", roomAndSendingNick.getFullQualifiedName())); replaceAttributes.add(new Attribute("to", occupent.getJid().getFullQualifiedName())); relayStanza(occupent.getJid(), StanzaBuilder.createClone(stanza, true, replaceAttributes) .build(), serverRuntimeContext); } // add to discussion history room.getHistory().append(stanza, sendingOccupant); } else { return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.FORBIDDEN, stanza); } } else { return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.NOT_ACCEPTABLE, stanza); } } else { return createMessageErrorStanza(moduleDomain, from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.ITEM_NOT_FOUND, stanza); } } else if (type == null || type == MessageStanzaType.CHAT || type == MessageStanzaType.NORMAL) { // private message logger.debug("Received direct message to {}", roomWithNickJid); Room room = conference.findRoom(roomJid); if (room != null) { Occupant sendingOccupant = room.findOccupantByJID(from); // sender must be participant in room if(roomWithNickJid.equals(roomJid)) { // check x element if(stanza.getVerifier().onlySubelementEquals("x", NamespaceURIs.JABBER_X_DATA)) { // voice requests logger.debug("Received voice request for room {}", roomJid); handleVoiceRequest(from, sendingOccupant, room, stanza, serverRuntimeContext); } else if(stanza.getVerifier().onlySubelementEquals("x", NamespaceURIs.XEP0045_MUC_USER)) { // invites/declines return handleInvites(stanza, from, sendingOccupant, room, serverRuntimeContext); } } else if (roomWithNickJid.isResourceSet()) { if (sendingOccupant != null) { // got resource, private message for occupant Occupant receivingOccupant = room.findOccupantByNick(roomWithNickJid.getResource()); // must be sent to an existing occupant in the room if (receivingOccupant != null) { Entity roomAndSendingNick = new EntityImpl(room.getJID(), sendingOccupant.getNick()); logger.debug("Relaying message to {}", receivingOccupant); List<Attribute> replaceAttributes = new ArrayList<Attribute>(); replaceAttributes.add(new Attribute("from", roomAndSendingNick.getFullQualifiedName())); replaceAttributes .add(new Attribute("to", receivingOccupant.getJid().getFullQualifiedName())); relayStanza(receivingOccupant.getJid(), StanzaBuilder.createClone(stanza, true, replaceAttributes).build(), serverRuntimeContext); } else { // TODO correct error? return createMessageErrorStanza(moduleDomain, from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.ITEM_NOT_FOUND, stanza); } } else { // user must be occupant to send direct message return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.NOT_ACCEPTABLE, stanza); } } } else { return createMessageErrorStanza(moduleDomain, from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.ITEM_NOT_FOUND, stanza); } } return null; } private Stanza handleInvites(MessageStanza stanza, Entity from, Occupant sendingOccupant, Room room, ServerRuntimeContext serverRuntimeContext) { X x = X.fromStanza(stanza); if (x != null && x.getInvite() != null) { if (sendingOccupant != null) { // invite, forward modified invite try { Stanza invite = MUCHandlerHelper.createInviteMessageStanza(stanza, room.getPassword()); relayStanza(invite.getTo(), invite, serverRuntimeContext); } catch (EntityFormatException e) { // invalid format of invite element return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.JID_MALFORMED, stanza); } } else { // user must be occupant to send invite return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.NOT_ACCEPTABLE, stanza); } } else if (x != null && x.getDecline() != null) { // invite, forward modified decline try { Stanza decline = MUCHandlerHelper.createDeclineMessageStanza(stanza); relayStanza(decline.getTo(), decline, serverRuntimeContext); } catch (EntityFormatException e) { // invalid format of invite element return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.JID_MALFORMED, stanza); } } else { return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY, StanzaErrorCondition.UNEXPECTED_REQUEST, stanza); } return null; } private void handleVoiceRequest(Entity from, Occupant sendingOccupant, Room room, Stanza stanza, ServerRuntimeContext serverRuntimeContext) { List<XMLElement> dataXs = stanza.getInnerElementsNamed("x", NamespaceURIs.JABBER_X_DATA); XMLElement dataX = dataXs.get(0); // check if "request_allow" is set List<XMLElement> fields = dataX.getInnerElementsNamed("field", NamespaceURIs.JABBER_X_DATA); String requestAllow = getFieldValue(fields, "muc#request_allow"); if("true".equals(requestAllow)) { // submitted voice grant, only allowed by moderators if(sendingOccupant.isModerator()) { String requestNick = getFieldValue(fields, "muc#roomnick"); Occupant requestor = room.findOccupantByNick(requestNick); requestor.setRole(Role.Participant); // notify remaining users that user got role updated MucUserItem presenceItem = new MucUserItem(requestor.getAffiliation(), requestor.getRole()); for (Occupant occupant : room.getOccupants()) { Stanza presenceToRemaining = MUCStanzaBuilder.createPresenceStanza(requestor.getJidInRoom(), occupant.getJid(), null, NamespaceURIs.XEP0045_MUC_USER, presenceItem); relayStanza(occupant.getJid(), presenceToRemaining, serverRuntimeContext); } } } else if(requestAllow == null) { // no request allow, treat as voice request VoiceRequestForm requestForm = new VoiceRequestForm(from, sendingOccupant.getNick()); for(Occupant moderator : room.getModerators()) { Stanza request = StanzaBuilder.createMessageStanza(room.getJID(), moderator.getJid(), null, null) .addPreparedElement(requestForm.createFormXML()).build(); relayStanza(moderator.getJid(), request, serverRuntimeContext); } } } private String getFieldValue(List<XMLElement> fields, String var) { for(XMLElement field : fields) { if(var.equals(field.getAttributeValue("var"))) { try { return field.getSingleInnerElementsNamed("value", NamespaceURIs.JABBER_X_DATA).getInnerText().getText(); } catch (XMLSemanticError e) { return null; } } } return null; } 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); } } }