blob: f1027748648fd2df07038d10efaeaa9a3cb9124b [file] [log] [blame]
/*
* 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.Collections;
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.protocol.StanzaBroker;
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 List<Stanza> createMessageErrorStanza(Entity from, Entity to, String id, StanzaErrorType type,
StanzaErrorCondition errorCondition, Stanza stanza) {
return Collections.singletonList(MUCHandlerHelper.createErrorStanza("message", NamespaceURIs.JABBER_CLIENT,
from, to, id, type.value(), errorCondition.value(), stanza.getInnerElements()));
}
@Override
protected List<Stanza> executeMessageLogic(MessageStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, StanzaBroker stanzaBroker) {
logger.debug("Received message for MUC");
Entity from = stanza.getFrom();
Entity roomWithNickJid = stanza.getTo();
Entity roomJid = roomWithNickJid.getBareJID();
MessageStanzaType type = stanza.getMessageType();
if (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) {
return createMessageErrorStanza(moduleDomain, from, stanza.getID(), StanzaErrorType.MODIFY,
StanzaErrorCondition.ITEM_NOT_FOUND, stanza);
}
Occupant sendingOccupant = room.findOccupantByJID(from);
// sender must be participant in room
if (sendingOccupant == null) {
return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY,
StanzaErrorCondition.NOT_ACCEPTABLE, stanza);
}
Entity roomAndSendingNick = new EntityImpl(room.getJID(), sendingOccupant.getNick());
if (!sendingOccupant.hasVoice()) {
return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY,
StanzaErrorCondition.FORBIDDEN, stanza);
}
// 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(),
stanzaBroker);
}
// add to discussion history
room.getHistory().append(stanza, sendingOccupant);
room.updateLastActivity();
} 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) {
return createMessageErrorStanza(moduleDomain, from, stanza.getID(), StanzaErrorType.MODIFY,
StanzaErrorCondition.ITEM_NOT_FOUND, stanza);
}
room.updateLastActivity();
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, stanzaBroker);
} else if (stanza.getVerifier().onlySubelementEquals("x", NamespaceURIs.XEP0045_MUC_USER)) {
// invites/declines
return handleInvites(stanza, from, sendingOccupant, room, stanzaBroker);
}
} else if (roomWithNickJid.isResourceSet()) {
if (sendingOccupant == null) {
// user must be occupant to send direct message
return createMessageErrorStanza(room.getJID(), from, stanza.getID(), StanzaErrorType.MODIFY,
StanzaErrorCondition.NOT_ACCEPTABLE, stanza);
}
// 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) {
// TODO correct error?
return createMessageErrorStanza(moduleDomain, from, stanza.getID(), StanzaErrorType.MODIFY,
StanzaErrorCondition.ITEM_NOT_FOUND, stanza);
}
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(), stanzaBroker);
}
}
return Collections.emptyList();
}
private List<Stanza> handleInvites(MessageStanza stanza, Entity from, Occupant sendingOccupant, Room room,
StanzaBroker stanzaBroker) {
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, stanzaBroker);
} 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, stanzaBroker);
} 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 Collections.emptyList();
}
private void handleVoiceRequest(Entity from, Occupant sendingOccupant, Room room, Stanza stanza,
StanzaBroker stanzaBroker) {
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, stanzaBroker);
}
}
} 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, stanzaBroker);
}
}
}
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, StanzaBroker stanzaBroker) {
try {
stanzaBroker.write(receiver, stanza, IgnoreFailureStrategy.INSTANCE);
} catch (DeliveryException e) {
logger.warn("presence relaying failed ", e);
}
}
}