org.jivesoftware.openfire.muc.spi.LocalMUCRoom Maven / Gradle / Ivy
/*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* 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.
*/
package org.jivesoftware.openfire.muc.spi;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.dom4j.Element;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.cluster.NodeID;
import org.jivesoftware.openfire.event.GroupEventListener;
import org.jivesoftware.openfire.group.ConcurrentGroupList;
import org.jivesoftware.openfire.group.ConcurrentGroupMap;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupAwareList;
import org.jivesoftware.openfire.group.GroupAwareMap;
import org.jivesoftware.openfire.group.GroupJID;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.muc.CannotBeInvitedException;
import org.jivesoftware.openfire.muc.ConflictException;
import org.jivesoftware.openfire.muc.ForbiddenException;
import org.jivesoftware.openfire.muc.HistoryRequest;
import org.jivesoftware.openfire.muc.HistoryStrategy;
import org.jivesoftware.openfire.muc.MUCEventDispatcher;
import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.openfire.muc.MUCRoom;
import org.jivesoftware.openfire.muc.MUCRoomHistory;
import org.jivesoftware.openfire.muc.MultiUserChatService;
import org.jivesoftware.openfire.muc.NotAcceptableException;
import org.jivesoftware.openfire.muc.NotAllowedException;
import org.jivesoftware.openfire.muc.RegistrationRequiredException;
import org.jivesoftware.openfire.muc.RoomLockedException;
import org.jivesoftware.openfire.muc.ServiceUnavailableException;
import org.jivesoftware.openfire.muc.cluster.AddAffiliation;
import org.jivesoftware.openfire.muc.cluster.AddMember;
import org.jivesoftware.openfire.muc.cluster.BroadcastMessageRequest;
import org.jivesoftware.openfire.muc.cluster.BroadcastPresenceRequest;
import org.jivesoftware.openfire.muc.cluster.ChangeNickname;
import org.jivesoftware.openfire.muc.cluster.DestroyRoomRequest;
import org.jivesoftware.openfire.muc.cluster.OccupantAddedEvent;
import org.jivesoftware.openfire.muc.cluster.OccupantLeftEvent;
import org.jivesoftware.openfire.muc.cluster.RoomUpdatedEvent;
import org.jivesoftware.openfire.muc.cluster.UpdateOccupant;
import org.jivesoftware.openfire.muc.cluster.UpdateOccupantRequest;
import org.jivesoftware.openfire.muc.cluster.UpdatePresence;
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.cache.CacheFactory;
import org.jivesoftware.util.cache.ExternalizableUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.Presence;
/**
* Implementation of a chatroom that is being hosted by this JVM. A LocalMUCRoom could represent
* a persistent room which means that its configuration will be maintained in synch with its
* representation in the database.
*
* When running in a cluster each cluster node will have its own copy of local rooms. Persistent
* rooms will be loaded by each cluster node when starting up. Not persistent rooms will be copied
* from the senior cluster member. All room occupants will be copied from the senior cluster member
* too.
*
* @author Gaston Dombiak
*/
public class LocalMUCRoom implements MUCRoom, GroupEventListener {
private static final Logger Log = LoggerFactory.getLogger(LocalMUCRoom.class);
/**
* The service hosting the room.
*/
private MultiUserChatService mucService;
/**
* The occupants of the room accessible by the occupants nickname.
*/
private Map> occupantsByNickname = new ConcurrentHashMap<>();
/**
* The occupants of the room accessible by the occupants bare JID.
*/
private Map> occupantsByBareJID = new ConcurrentHashMap<>();
/**
* The occupants of the room accessible by the occupants full JID.
*/
private Map occupantsByFullJID = new ConcurrentHashMap<>();
/**
* The name of the room.
*/
private String name;
/**
* A lock to protect the room occupants.
*/
ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* The role of the room itself.
*/
private MUCRole role = new RoomRole(this);
/**
* The router used to send packets for the room.
*/
private PacketRouter router;
/**
* The start time of the chat.
*/
long startTime;
/**
* The end time of the chat.
*/
long endTime;
/**
* After a room has been destroyed it may remain in memory but it won't be possible to use it.
* When a room is destroyed it is immediately removed from the MultiUserChatService but it's
* possible that while the room was being destroyed it was being used by another thread so we
* need to protect the room under these rare circumstances.
*/
boolean isDestroyed = false;
/**
* ChatRoomHistory object.
*/
private MUCRoomHistory roomHistory;
/**
* Time when the room was locked. A value of zero means that the room is unlocked.
*/
private long lockedTime;
/**
* List of chatroom's owner. The list contains only bare jid.
*/
GroupAwareList owners = new ConcurrentGroupList<>();
/**
* List of chatroom's admin. The list contains only bare jid.
*/
GroupAwareList admins = new ConcurrentGroupList<>();
/**
* List of chatroom's members. The list contains only bare jid, mapped to a nickname.
*/
GroupAwareMap members = new ConcurrentGroupMap<>();
/**
* List of chatroom's outcast. The list contains only bare jid of not allowed users.
*/
private GroupAwareList outcasts = new ConcurrentGroupList<>();
/**
* The natural language name of the room.
*/
private String naturalLanguageName;
/**
* Description of the room. The owner can change the description using the room configuration
* form.
*/
private String description;
/**
* Indicates if occupants are allowed to change the subject of the room.
*/
private boolean canOccupantsChangeSubject;
/**
* Maximum number of occupants that could be present in the room. If the limit's been reached
* and a user tries to join, a not-allowed error will be returned.
*/
private int maxUsers;
/**
* List of roles of which presence will be broadcasted to the rest of the occupants. This
* feature is useful for implementing "invisible" occupants.
*/
private List rolesToBroadcastPresence = new ArrayList<>();
/**
* A public room means that the room is searchable and visible. This means that the room can be
* located using disco requests.
*/
private boolean publicRoom;
/**
* Persistent rooms are saved to the database to make sure that rooms configurations can be
* restored in case the server goes down.
*/
private boolean persistent;
/**
* Moderated rooms enable only participants to speak. Users that join the room and aren't
* participants can't speak (they are just visitors).
*/
private boolean moderated;
/**
* A room is considered members-only if an invitation is required in order to enter the room.
* Any user that is not a member of the room won't be able to join the room unless the user
* decides to register with the room (thus becoming a member).
*/
private boolean membersOnly;
/**
* Some rooms may restrict the occupants that are able to send invitations. Sending an
* invitation in a members-only room adds the invitee to the members list.
*/
private boolean canOccupantsInvite;
/**
* The password that every occupant should provide in order to enter the room.
*/
private String password = null;
/**
* Every presence packet can include the JID of every occupant unless the owner deactives this
* configuration.
*/
private boolean canAnyoneDiscoverJID;
/**
* The minimal role of persons that are allowed to send private messages in the room.
*/
private String canSendPrivateMessage;
/**
* Enables the logging of the conversation. The conversation in the room will be saved to the
* database.
*/
private boolean logEnabled;
/**
* Enables the logging of the conversation. The conversation in the room will be saved to the
* database.
*/
private boolean loginRestrictedToNickname;
/**
* Enables the logging of the conversation. The conversation in the room will be saved to the
* database.
*/
private boolean canChangeNickname;
/**
* Enables the logging of the conversation. The conversation in the room will be saved to the
* database.
*/
private boolean registrationEnabled;
/**
* Internal component that handles IQ packets sent by the room owners.
*/
private IQOwnerHandler iqOwnerHandler;
/**
* Internal component that handles IQ packets sent by moderators, admins and owners.
*/
private IQAdminHandler iqAdminHandler;
/**
* The last known subject of the room. This information is used to respond disco requests. The
* MUCRoomHistory class holds the history of the room together with the last message that set
* the room's subject.
*/
private String subject = "";
/**
* The ID of the room. If the room is temporary and does not log its conversation then the value
* will always be -1. Otherwise a value will be obtained from the database.
*/
private long roomID = -1;
/**
* The date when the room was created.
*/
private Date creationDate;
/**
* The last date when the room's configuration was modified.
*/
private Date modificationDate;
/**
* The date when the last occupant left the room. A null value means that there are occupants
* in the room at the moment.
*/
private Date emptyDate;
/**
* Indicates if the room is present in the database.
*/
private boolean savedToDB = false;
/**
* Do not use this constructor. It was added to implement the Externalizable
* interface required to work inside of a cluster.
*/
public LocalMUCRoom() {
}
/**
* Create a new chat room.
*
* @param chatservice the service hosting the room.
* @param roomname the name of the room.
* @param packetRouter the router for sending packets from the room.
*/
LocalMUCRoom(MultiUserChatService chatservice, String roomname, PacketRouter packetRouter) {
this.mucService = chatservice;
this.name = roomname;
this.naturalLanguageName = roomname;
this.description = roomname;
this.router = packetRouter;
this.startTime = System.currentTimeMillis();
this.creationDate = new Date(startTime);
this.modificationDate = new Date(startTime);
this.emptyDate = new Date(startTime);
this.canOccupantsChangeSubject = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.canOccupantsChangeSubject", false);
this.maxUsers = MUCPersistenceManager.getIntProperty(mucService.getServiceName(), "room.maxUsers", 30);
this.publicRoom = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.publicRoom", true);
this.persistent = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.persistent", false);
this.moderated = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.moderated", false);
this.membersOnly = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.membersOnly", false);
this.canOccupantsInvite = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.canOccupantsInvite", false);
this.canAnyoneDiscoverJID = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.canAnyoneDiscoverJID", true);
this.logEnabled = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.logEnabled", false);
this.loginRestrictedToNickname = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.loginRestrictedToNickname", false);
this.canChangeNickname = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.canChangeNickname", true);
this.registrationEnabled = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.registrationEnabled", true);
// TODO Allow to set the history strategy from the configuration form?
roomHistory = new MUCRoomHistory(this, new HistoryStrategy(mucService.getHistoryStrategy()));
this.iqOwnerHandler = new IQOwnerHandler(this, packetRouter);
this.iqAdminHandler = new IQAdminHandler(this, packetRouter);
// No one can join the room except the room's owner
this.lockedTime = startTime;
// Set the default roles for which presence is broadcast
rolesToBroadcastPresence.add("moderator");
rolesToBroadcastPresence.add("participant");
rolesToBroadcastPresence.add("visitor");
}
@Override
public String getName() {
return name;
}
@Override
public JID getJID() {
return new JID(getName(), getMUCService().getServiceDomain(), null);
}
@Override
public MultiUserChatService getMUCService() {
return mucService;
}
@Override
public void setMUCService(MultiUserChatService service) {
this.mucService = service;
}
@Override
public long getID() {
if (isPersistent() || isLogEnabled()) {
if (roomID == -1) {
roomID = SequenceManager.nextID(JiveConstants.MUC_ROOM);
}
}
return roomID;
}
@Override
public void setID(long roomID) {
this.roomID = roomID;
}
@Override
public Date getCreationDate() {
return creationDate;
}
@Override
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
@Override
public Date getModificationDate() {
return modificationDate;
}
@Override
public void setModificationDate(Date modificationDate) {
this.modificationDate = modificationDate;
}
@Override
public void setEmptyDate(Date emptyDate) {
// Do nothing if old value is same as new value
if (this.emptyDate == emptyDate) {
return;
}
this.emptyDate = emptyDate;
MUCPersistenceManager.updateRoomEmptyDate(this);
}
@Override
public Date getEmptyDate() {
return this.emptyDate;
}
@Override
public MUCRole getRole() {
return role;
}
/**
* @deprecated Prefer {@link #getOccupantsByNickname(String)} (user can be connected more than once)
*/
@Override
public MUCRole getOccupant(String nickname) throws UserNotFoundException {
if (nickname == null) {
throw new UserNotFoundException();
}
List roles = getOccupantsByNickname(nickname);
if (roles != null && roles.size() > 0) {
return roles.get(0);
}
throw new UserNotFoundException();
}
@Override
public List getOccupantsByNickname(String nickname) throws UserNotFoundException {
if (nickname == null) {
throw new UserNotFoundException();
}
List roles = occupantsByNickname.get(nickname.toLowerCase());
if (roles != null && roles.size() > 0) {
return roles;
}
throw new UserNotFoundException();
}
@Override
public List getOccupantsByBareJID(JID jid) throws UserNotFoundException {
List roles = occupantsByBareJID.get(jid);
if (roles != null && !roles.isEmpty()) {
return Collections.unmodifiableList(roles);
}
throw new UserNotFoundException();
}
@Override
public MUCRole getOccupantByFullJID(JID jid) {
MUCRole role = occupantsByFullJID.get(jid);
if (role != null) {
return role;
}
return null;
}
@Override
public Collection getOccupants() {
return Collections.unmodifiableCollection(occupantsByFullJID.values());
}
@Override
public int getOccupantsCount() {
return occupantsByNickname.size();
}
@Override
public boolean hasOccupant(String nickname) {
return occupantsByNickname.containsKey(nickname.toLowerCase());
}
@Override
public String getReservedNickname(JID jid) {
final JID bareJID = jid.asBareJID();
String answer = members.get(bareJID);
if (answer == null || answer.trim().length() == 0) {
return null;
}
return answer;
}
@Override
public MUCRole.Affiliation getAffiliation(JID jid) {
final JID bareJID = jid.asBareJID();
if (owners.includes(bareJID)) {
return MUCRole.Affiliation.owner;
}
else if (admins.includes(bareJID)) {
return MUCRole.Affiliation.admin;
}
else if (members.includesKey(bareJID)) {
return MUCRole.Affiliation.member;
}
else if (outcasts.includes(bareJID)) {
return MUCRole.Affiliation.outcast;
}
return MUCRole.Affiliation.none;
}
@Override
public LocalMUCRole joinRoom(String nickname, String password, HistoryRequest historyRequest,
LocalMUCUser user, Presence presence) throws UnauthorizedException,
UserAlreadyExistsException, RoomLockedException, ForbiddenException,
RegistrationRequiredException, ConflictException, ServiceUnavailableException,
NotAcceptableException {
if (((MultiUserChatServiceImpl)mucService).getMUCDelegate() != null) {
if (!((MultiUserChatServiceImpl)mucService).getMUCDelegate().joiningRoom(this, user.getAddress())) {
// Delegate said no, reject join.
throw new UnauthorizedException();
}
}
LocalMUCRole joinRole = null;
lock.writeLock().lock();
boolean clientOnlyJoin = false;
// A "client only join" here is one where the client is already joined, but has re-joined.
try {
// If the room has a limit of max user then check if the limit has been reached
if (!canJoinRoom(user)) {
throw new ServiceUnavailableException();
}
final JID bareJID = user.getAddress().asBareJID();
boolean isOwner = owners.includes(bareJID);
// If the room is locked and this user is not an owner raise a RoomLocked exception
if (isLocked()) {
if (!isOwner) {
throw new RoomLockedException();
}
}
// Check if the nickname is already used in the room
if (occupantsByNickname.containsKey(nickname.toLowerCase())) {
List occupants = occupantsByNickname.get(nickname.toLowerCase());
MUCRole occupant = occupants.size() > 0 ? occupants.get(0) : null;
if (occupant != null && !occupant.getUserAddress().toBareJID().equals(bareJID.toBareJID())) {
// Nickname is already used, and not by the same JID
throw new UserAlreadyExistsException();
}
// Is this client already joined with this nickname?
for (MUCRole mucRole : occupants) {
if (mucRole.getUserAddress().equals(user.getAddress())) {
clientOnlyJoin = true;
break;
}
}
}
// If the room is password protected and the provided password is incorrect raise a
// Unauthorized exception
if (isPasswordProtected()) {
if (password == null || !password.equals(getPassword())) {
throw new UnauthorizedException();
}
}
// If another user attempts to join the room with a nickname reserved by the first user
// raise a ConflictException
if (members.containsValue(nickname.toLowerCase())) {
if (!nickname.toLowerCase().equals(members.get(bareJID))) {
throw new ConflictException();
}
}
if (isLoginRestrictedToNickname()) {
String reservedNickname = members.get(bareJID);
if (reservedNickname != null && !nickname.toLowerCase().equals(reservedNickname)) {
throw new NotAcceptableException();
}
}
// Set the corresponding role based on the user's affiliation
MUCRole.Role role;
MUCRole.Affiliation affiliation;
if (isOwner) {
// The user is an owner. Set the role and affiliation accordingly.
role = MUCRole.Role.moderator;
affiliation = MUCRole.Affiliation.owner;
}
else if (mucService.isSysadmin(bareJID)) {
// The user is a system administrator of the MUC service. Treat him as an owner
// although he won't appear in the list of owners
role = MUCRole.Role.moderator;
affiliation = MUCRole.Affiliation.owner;
}
else if (admins.includes(bareJID)) {
// The user is an admin. Set the role and affiliation accordingly.
role = MUCRole.Role.moderator;
affiliation = MUCRole.Affiliation.admin;
}
// explicit outcast status has higher precedence than member status
else if (outcasts.contains(bareJID)) {
// The user is an outcast. Raise a "Forbidden" exception.
throw new ForbiddenException();
}
else if (members.includesKey(bareJID)) {
// The user is a member. Set the role and affiliation accordingly.
role = MUCRole.Role.participant;
affiliation = MUCRole.Affiliation.member;
}
// this checks if the user is an outcast implicitly (via a group)
else if (outcasts.includes(bareJID)) {
// The user is an outcast. Raise a "Forbidden" exception.
throw new ForbiddenException();
}
else {
// The user has no affiliation (i.e. NONE). Set the role accordingly.
if (isMembersOnly()) {
// The room is members-only and the user is not a member. Raise a
// "Registration Required" exception.
throw new RegistrationRequiredException();
}
role = (isModerated() ? MUCRole.Role.visitor : MUCRole.Role.participant);
affiliation = MUCRole.Affiliation.none;
}
if (!clientOnlyJoin) {
// Create a new role for this user in this room
joinRole = new LocalMUCRole(mucService, this, nickname, role,
affiliation, user, presence, router);
// Add the new user as an occupant of this room
List occupants = occupantsByNickname.get(nickname.toLowerCase());
if (occupants == null) {
occupants = new ArrayList<>();
occupantsByNickname.put(nickname.toLowerCase(), occupants);
}
occupants.add(joinRole);
// Update the tables of occupants based on the bare and full JID
List list = occupantsByBareJID.get(bareJID);
if (list == null) {
list = new ArrayList<>();
occupantsByBareJID.put(bareJID, list);
}
list.add(joinRole);
occupantsByFullJID.put(user.getAddress(), joinRole);
} else {
// Grab the existing one.
joinRole = (LocalMUCRole) occupantsByFullJID.get(user.getAddress());
}
}
finally {
lock.writeLock().unlock();
}
// Notify other cluster nodes that a new occupant joined the room
CacheFactory.doClusterTask(new OccupantAddedEvent(this, joinRole));
// Send presence of existing occupants to new occupant
sendInitialPresences(joinRole);
// It is assumed that the room is new based on the fact that it's locked and
// that it was locked when it was created.
boolean isRoomNew = isLocked() && creationDate.getTime() == lockedTime;
try {
// Send the presence of this new occupant to existing occupants
Presence joinPresence = joinRole.getPresence().createCopy();
broadcastPresence(joinPresence, true);
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
// If the room has just been created send the "room locked until configuration is
// confirmed" message
if (!isRoomNew && isLocked()) {
// http://xmpp.org/extensions/xep-0045.html#enter-locked
Presence presenceItemNotFound = new Presence(Presence.Type.error);
presenceItemNotFound.setError(PacketError.Condition.item_not_found);
presenceItemNotFound.setFrom(role.getRoleAddress());
joinRole.send(presenceItemNotFound);
}
if (historyRequest == null) {
Iterator history = roomHistory.getMessageHistory();
while (history.hasNext()) {
joinRole.send(history.next());
}
}
else {
historyRequest.sendHistory(joinRole, roomHistory);
}
Message roomSubject = roomHistory.getChangedSubject();
if (roomSubject != null) {
joinRole.send(roomSubject);
}
if (!clientOnlyJoin) {
// Update the date when the last occupant left the room
setEmptyDate(null);
// Fire event that occupant joined the room
MUCEventDispatcher.occupantJoined(getRole().getRoleAddress(),
user.getAddress(), joinRole.getNickname());
}
return joinRole;
}
/**
* Can a user join this room
*
* @param user the user attempting to join this room
* @return boolean
*/
private boolean canJoinRoom(LocalMUCUser user){
boolean isOwner = owners.includes(user.getAddress().asBareJID());
boolean isAdmin = admins.includes(user.getAddress().asBareJID());
return (!isDestroyed && (!hasOccupancyLimit() || isAdmin || isOwner || (getOccupantsCount() < getMaxUsers())));
}
/**
* Does this room have an occupancy limit?
*
* @return boolean
*/
private boolean hasOccupancyLimit(){
return getMaxUsers() != 0;
}
/**
* Sends presence of existing occupants to new occupant.
*
* @param joinRole the role of the new occupant in the room.
*/
private void sendInitialPresences(LocalMUCRole joinRole) {
for (MUCRole occupant : occupantsByFullJID.values()) {
if (occupant == joinRole) {
continue;
}
Presence occupantPresence = occupant.getPresence();
// Skip to the next occupant if we cannot send presence of this occupant
if (hasToCheckRoleToBroadcastPresence()) {
Element frag = occupantPresence.getChildElement("x",
"http://jabber.org/protocol/muc#user");
// Check if we can broadcast the presence for this role
if (!canBroadcastPresence(frag.element("item").attributeValue("role"))) {
continue;
}
}
// Don't include the occupant's JID if the room is semi-anon and the new occupant
// is not a moderator
if (!canAnyoneDiscoverJID() && MUCRole.Role.moderator != joinRole.getRole()) {
occupantPresence = occupantPresence.createCopy();
Element frag = occupantPresence.getChildElement("x",
"http://jabber.org/protocol/muc#user");
frag.element("item").addAttribute("jid", null);
}
joinRole.send(occupantPresence);
}
}
public void occupantAdded(OccupantAddedEvent event) {
// Create a proxy for the occupant that joined the room from another cluster node
RemoteMUCRole joinRole = new RemoteMUCRole(mucService, event);
JID bareJID = event.getUserAddress().asBareJID();
String nickname = event.getNickname();
List occupants = occupantsByNickname.get(nickname.toLowerCase());
// Do not add new occupant with one with same nickname already exists
if (occupants == null) {
occupants = new ArrayList<>();
occupantsByNickname.put(nickname.toLowerCase(), occupants);
} else {
// sanity check; make sure the nickname is owned by the same JID
if (occupants.size() > 0) {
JID existingJID = occupants.get(0).getUserAddress().asBareJID();
if (!bareJID.equals(existingJID)) {
Log.warn(MessageFormat.format("Conflict detected; {0} requested nickname '{1}'; already being used by {2}", bareJID, nickname, existingJID));
return;
}
}
}
// Add the new user as an occupant of this room
occupants.add(joinRole);
// Update the tables of occupants based on the bare and full JID
List list = occupantsByBareJID.get(bareJID);
if (list == null) {
list = new ArrayList<>();
occupantsByBareJID.put(bareJID, list);
}
list.add(joinRole);
occupantsByFullJID.put(event.getUserAddress(), joinRole);
// Update the date when the last occupant left the room
setEmptyDate(null);
if (event.isOriginator()) {
// Fire event that occupant joined the room
MUCEventDispatcher.occupantJoined(getRole().getRoleAddress(), event.getUserAddress(), joinRole.getNickname());
}
// Check if we need to send presences of the new occupant to occupants hosted by this JVM
if (event.isSendPresence()) {
for (MUCRole occupant : occupantsByFullJID.values()) {
if (occupant.isLocal()) {
occupant.send(event.getPresence().createCopy());
}
}
}
}
@Override
public void leaveRoom(MUCRole leaveRole) {
if (leaveRole.isLocal()) {
// Ask other cluster nodes to remove occupant from room
OccupantLeftEvent event = new OccupantLeftEvent(this, leaveRole);
CacheFactory.doClusterTask(event);
}
try {
Presence originalPresence = leaveRole.getPresence();
Presence presence = originalPresence.createCopy();
presence.setType(Presence.Type.unavailable);
presence.setStatus(null);
// Change (or add) presence information about roles and affiliations
Element childElement = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
if (childElement == null) {
childElement = presence.addChildElement("x", "http://jabber.org/protocol/muc#user");
}
Element item = childElement.element("item");
if (item == null) {
item = childElement.addElement("item");
}
item.addAttribute("role", "none");
// Check to see if the user's original presence is one we should broadcast
// a leave packet for. Need to check the original presence because we just
// set the role to "none" above, which is always broadcast.
if(!shouldBroadcastPresence(originalPresence)){
// Inform the leaving user that he/she has left the room
leaveRole.send(presence);
}
else {
if (getOccupantsByNickname(leaveRole.getNickname()).size() <= 1) {
// Inform the rest of the room occupants that the user has left the room
broadcastPresence(presence, false);
}
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
// Remove occupant from room and destroy room if empty and not persistent
OccupantLeftEvent event = new OccupantLeftEvent(this, leaveRole);
event.setOriginator(true);
event.run();
}
public void leaveRoom(OccupantLeftEvent event) {
MUCRole leaveRole = event.getRole();
if (leaveRole == null) {
return;
}
lock.writeLock().lock();
try {
// Removes the role from the room
removeOccupantRole(leaveRole, event.isOriginator());
// TODO Implement this: If the room owner becomes unavailable for any reason before
// submitting the form (e.g., a lost connection), the service will receive a presence
// stanza of type "unavailable" from the owner to the room@service/nick or room@service
// (or both). The service MUST then destroy the room, sending a presence stanza of type
// "unavailable" from the room to the owner including a element and reason
// (if provided) as defined under the "Destroying a Room" use case.
// Remove the room from the service only if there are no more occupants and the room is
// not persistent
if (occupantsByFullJID.isEmpty() && !isPersistent()) {
endTime = System.currentTimeMillis();
if (event.isOriginator()) {
mucService.removeChatRoom(name);
// Fire event that the room has been destroyed
MUCEventDispatcher.roomDestroyed(getRole().getRoleAddress());
}
}
if (occupantsByFullJID.isEmpty()) {
// Update the date when the last occupant left the room
setEmptyDate(new Date());
}
}
finally {
lock.writeLock().unlock();
}
}
/**
* Removes the role of the occupant from all the internal occupants collections. The role will
* also be removed from the user's roles.
*
* @param leaveRole the role to remove.
* @param originator true if this JVM is the one that originated the event.
*/
private void removeOccupantRole(MUCRole leaveRole, boolean originator) {
JID userAddress = leaveRole.getUserAddress();
// Notify the user that he/she is no longer in the room
leaveRole.destroy();
// Update the tables of occupants based on the bare and full JID
JID bareJID = userAddress.asBareJID();
String nickname = leaveRole.getNickname();
List occupants = occupantsByNickname.get(nickname.toLowerCase());
if (occupants != null) {
occupants.remove(leaveRole);
if (occupants.isEmpty()) {
occupantsByNickname.remove(nickname.toLowerCase());
}
}
List list = occupantsByBareJID.get(bareJID);
if (list != null) {
list.remove(leaveRole);
if (list.isEmpty()) {
occupantsByBareJID.remove(bareJID);
}
}
occupantsByFullJID.remove(userAddress);
if (originator) {
// Fire event that occupant left the room
MUCEventDispatcher.occupantLeft(getRole().getRoleAddress(), userAddress);
}
}
public void destroyRoom(DestroyRoomRequest destroyRequest) {
JID alternateJID = destroyRequest.getAlternateJID();
String reason = destroyRequest.getReason();
Collection removedRoles = new ArrayList<>();
lock.writeLock().lock();
try {
boolean hasRemoteOccupants = false;
// Remove each occupant
for (MUCRole leaveRole : occupantsByFullJID.values()) {
if (leaveRole != null) {
// Add the removed occupant to the list of removed occupants. We are keeping a
// list of removed occupants to process later outside of the lock.
if (leaveRole.isLocal()) {
removedRoles.add(leaveRole);
}
else {
hasRemoteOccupants = true;
}
removeOccupantRole(leaveRole, destroyRequest.isOriginator());
}
}
endTime = System.currentTimeMillis();
// Set that the room has been destroyed
isDestroyed = true;
if (destroyRequest.isOriginator()) {
if (hasRemoteOccupants) {
// Ask other cluster nodes to remove occupants since room is being destroyed
CacheFactory.doClusterTask(new DestroyRoomRequest(this, alternateJID, reason));
}
// Removes the room from the list of rooms hosted in the service
mucService.removeChatRoom(name);
}
}
finally {
lock.writeLock().unlock();
}
// Send an unavailable presence to each removed occupant
for (MUCRole removedRole : removedRoles) {
try {
// Send a presence stanza of type "unavailable" to the occupant
Presence presence = createPresence(Presence.Type.unavailable);
presence.setFrom(removedRole.getRoleAddress());
// A fragment containing the x-extension for room destruction.
Element fragment = presence.addChildElement("x",
"http://jabber.org/protocol/muc#user");
Element item = fragment.addElement("item");
item.addAttribute("affiliation", "none");
item.addAttribute("role", "none");
if (alternateJID != null) {
fragment.addElement("destroy").addAttribute("jid", alternateJID.toString());
}
if (reason != null && reason.length() > 0) {
Element destroy = fragment.element("destroy");
if (destroy == null) {
destroy = fragment.addElement("destroy");
}
destroy.addElement("reason").setText(reason);
}
removedRole.send(presence);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
if (destroyRequest.isOriginator()) {
// Remove the room from the DB if the room was persistent
MUCPersistenceManager.deleteFromDB(this);
// Fire event that the room has been destroyed
MUCEventDispatcher.roomDestroyed(getRole().getRoleAddress());
}
}
@Override
public void destroyRoom(JID alternateJID, String reason) {
DestroyRoomRequest destroyRequest = new DestroyRoomRequest(this, alternateJID, reason);
destroyRequest.setOriginator(true);
destroyRequest.run();
}
@Override
public Presence createPresence(Presence.Type presenceType) throws UnauthorizedException {
Presence presence = new Presence();
presence.setType(presenceType);
presence.setFrom(role.getRoleAddress());
return presence;
}
@Override
public void serverBroadcast(String msg) {
Message message = new Message();
message.setType(Message.Type.groupchat);
message.setBody(msg);
message.setFrom(role.getRoleAddress());
broadcast(message);
}
@Override
public void sendPublicMessage(Message message, MUCRole senderRole) throws ForbiddenException {
// Check that if the room is moderated then the sender of the message has to have voice
if (isModerated() && senderRole.getRole().compareTo(MUCRole.Role.participant) > 0) {
throw new ForbiddenException();
}
// Send the message to all occupants
message.setFrom(senderRole.getRoleAddress());
send(message);
// Fire event that message was received by the room
MUCEventDispatcher.messageReceived(getRole().getRoleAddress(), senderRole.getUserAddress(),
senderRole.getNickname(), message);
}
@Override
public void sendPrivatePacket(Packet packet, MUCRole senderRole) throws NotFoundException, ForbiddenException {
switch (senderRole.getRole()) { // intended fall-through
case none:
throw new ForbiddenException();
default:
case visitor:
if (canSendPrivateMessage().equals( "participants" )) throw new ForbiddenException();
case participant:
if (canSendPrivateMessage().equals( "moderators" )) throw new ForbiddenException();
case moderator:
if (canSendPrivateMessage().equals( "none" )) throw new ForbiddenException();
}
String resource = packet.getTo().getResource();
List occupants = occupantsByNickname.get(resource.toLowerCase());
if (occupants == null || occupants.size() == 0) {
throw new NotFoundException();
}
for (MUCRole occupant : occupants) {
packet.setFrom(senderRole.getRoleAddress());
occupant.send(packet);
if(packet instanceof Message) {
Message message = (Message) packet;
MUCEventDispatcher.privateMessageRecieved(occupant.getUserAddress(), senderRole.getUserAddress(),
message);
}
}
}
@Override
public void send(Packet packet) {
if (packet instanceof Message) {
broadcast((Message)packet);
}
else if (packet instanceof Presence) {
broadcastPresence((Presence)packet, false);
}
else if (packet instanceof IQ) {
IQ reply = IQ.createResultIQ((IQ) packet);
reply.setChildElement(((IQ) packet).getChildElement());
reply.setError(PacketError.Condition.bad_request);
router.route(reply);
}
}
/**
* Checks the role of the sender and returns true if the given presence should be broadcasted
*
* @param presence The presence to check
* @return true if the presence should be broadcast to the rest of the room
*/
private boolean shouldBroadcastPresence(Presence presence){
if (presence == null) {
return false;
}
if (hasToCheckRoleToBroadcastPresence()) {
Element frag = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
// Check if we can broadcast the presence for this role
if (!canBroadcastPresence(frag.element("item").attributeValue("role"))) {
return false;
}
}
return true;
}
/**
* Broadcasts the specified presence to all room occupants. If the presence belongs to a
* user whose role cannot be broadcast then the presence will only be sent to the presence's
* user. On the other hand, the JID of the user that sent the presence won't be included if the
* room is semi-anon and the target occupant is not a moderator.
*
* @param presence the presence to broadcast.
* @param isJoinPresence If the presence is sent in the context of joining the room.
*/
private void broadcastPresence(Presence presence, boolean isJoinPresence) {
if (presence == null) {
return;
}
if (!shouldBroadcastPresence(presence)) {
// Just send the presence to the sender of the presence
try {
for (MUCRole occupant : getOccupantsByNickname(presence.getFrom().getResource())) {
occupant.send(presence);
}
}
catch (UserNotFoundException e) {
// Do nothing
}
return;
}
// Broadcast presence to occupants hosted by other cluster nodes
BroadcastPresenceRequest request = new BroadcastPresenceRequest(this, presence, isJoinPresence);
CacheFactory.doClusterTask(request);
// Broadcast presence to occupants connected to this JVM
request = new BroadcastPresenceRequest(this, presence, isJoinPresence);
request.setOriginator(true);
request.run();
}
public void broadcast(BroadcastPresenceRequest presenceRequest) {
String jid = null;
Presence presence = presenceRequest.getPresence();
JID to = presence.getTo();
Element frag = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
// Don't include the occupant's JID if the room is semi-anon and the new occupant
// is not a moderator
if (!canAnyoneDiscoverJID()) {
jid = frag.element("item").attributeValue("jid");
}
for (MUCRole occupant : occupantsByFullJID.values()) {
if (!occupant.isLocal()) {
continue;
}
// Don't include the occupant's JID if the room is semi-anon and the new occupant
// is not a moderator
if (!canAnyoneDiscoverJID()) {
if (MUCRole.Role.moderator == occupant.getRole()) {
frag.element("item").addAttribute("jid", jid);
}
else {
frag.element("item").addAttribute("jid", null);
}
}
// Some status codes should only be included in the "self-presence", which is only sent to the user, but not to other occupants.
if (occupant.getPresence().getFrom().equals(to)) {
Presence selfPresence = presence.createCopy();
Element fragSelfPresence = selfPresence.getChildElement("x", "http://jabber.org/protocol/muc#user");
fragSelfPresence.addElement("status").addAttribute("code", "110");
// Only in the context of entering the room status code 100, 201 and 210 should be sent.
// http://xmpp.org/registrar/mucstatus.html
if (presenceRequest.isJoinPresence()) {
boolean isRoomNew = isLocked() && creationDate.getTime() == lockedTime;
if (canAnyoneDiscoverJID()) {
// // XEP-0045: Example 26.
// If the user is entering a room that is non-anonymous (i.e., which informs all occupants of each occupant's full JID as shown above), the service MUST warn the user by including a status code of "100" in the initial presence that the room sends to the new occupant
fragSelfPresence.addElement("status").addAttribute("code", "100");
}
if (isRoomNew) {
fragSelfPresence.addElement("status").addAttribute("code", "201");
}
}
occupant.send(selfPresence);
} else {
occupant.send(presence);
}
}
}
private void broadcast(Message message) {
// Broadcast message to occupants hosted by other cluster nodes
BroadcastMessageRequest request = new BroadcastMessageRequest(this, message, occupantsByFullJID.size());
CacheFactory.doClusterTask(request);
// Broadcast message to occupants connected to this JVM
request = new BroadcastMessageRequest(this, message, occupantsByFullJID.size());
request.setOriginator(true);
request.run();
}
public void broadcast(BroadcastMessageRequest messageRequest) {
Message message = messageRequest.getMessage();
// Add message to the room history
roomHistory.addMessage(message);
// Send message to occupants connected to this JVM
for (MUCRole occupant : occupantsByFullJID.values()) {
// Do not send broadcast messages to deaf occupants or occupants hosted in
// other cluster nodes
if (occupant.isLocal() && !occupant.isVoiceOnly()) {
occupant.send(message);
}
}
if (messageRequest.isOriginator() && isLogEnabled()) {
MUCRole senderRole = null;
JID senderAddress;
// convert the MUC nickname/role JID back into a real user JID
if (message.getFrom() != null && message.getFrom().getResource() != null) {
// get the first MUCRole for the sender
List occupants = occupantsByNickname.get(message.getFrom().getResource().toLowerCase());
senderRole = occupants == null ? null : occupants.get(0);
}
if (senderRole == null) {
// The room itself is sending the message
senderAddress = getRole().getRoleAddress();
}
else {
// An occupant is sending the message
senderAddress = senderRole.getUserAddress();
}
// Log the conversation
mucService.logConversation(this, message, senderAddress);
}
mucService.messageBroadcastedTo(messageRequest.getOccupants());
}
/**
* An empty role that represents the room itself in the chatroom. Chatrooms need to be able to
* speak (server messages) and so must have their own role in the chatroom.
*/
private class RoomRole implements MUCRole {
private MUCRoom room;
private RoomRole(MUCRoom room) {
this.room = room;
}
@Override
public Presence getPresence() {
return null;
}
@Override
public void setPresence(Presence presence) {
}
@Override
public void setRole(MUCRole.Role newRole) {
}
@Override
public MUCRole.Role getRole() {
return MUCRole.Role.moderator;
}
@Override
public void setAffiliation(MUCRole.Affiliation newAffiliation) {
}
@Override
public MUCRole.Affiliation getAffiliation() {
return MUCRole.Affiliation.owner;
}
@Override
public void changeNickname(String nickname) {
}
@Override
public String getNickname() {
return null;
}
@Override
public boolean isVoiceOnly() {
return false;
}
@Override
public boolean isLocal() {
return true;
}
@Override
public NodeID getNodeID() {
return XMPPServer.getInstance().getNodeID();
}
@Override
public MUCRoom getChatRoom() {
return room;
}
private JID crJID = null;
@Override
public JID getRoleAddress() {
if (crJID == null) {
crJID = new JID(room.getName(), mucService.getServiceDomain(), null, true);
}
return crJID;
}
@Override
public JID getUserAddress() {
return null;
}
@Override
public void send(Packet packet) {
room.send(packet);
}
@Override
public void destroy() {
}
}
@Override
public long getChatLength() {
return endTime - startTime;
}
/**
* Updates all the presences of the given user with the new affiliation and role information. Do
* nothing if the given jid is not present in the room. If the user has joined the room from
* several client resources, all his/her occupants' presences will be updated.
*
* @param jid the bare jid of the user to update his/her role.
* @param newAffiliation the new affiliation for the JID.
* @param newRole the new role for the JID.
* @return the list of updated presences of all the client resources that the client used to
* join the room.
* @throws NotAllowedException If trying to change the moderator role to an owner or an admin or
* if trying to ban an owner or an administrator.
*/
private List changeOccupantAffiliation(MUCRole senderRole, JID jid, MUCRole.Affiliation newAffiliation, MUCRole.Role newRole)
throws NotAllowedException {
List presences = new ArrayList<>();
// Get all the roles (i.e. occupants) of this user based on his/her bare JID
JID bareJID = jid.asBareJID();
List roles = occupantsByBareJID.get(bareJID);
if (roles == null) {
return presences;
}
// Collect all the updated presences of these roles
for (MUCRole role : roles) {
// TODO
// if (!isPrivilegedToChangeAffiliationAndRole(senderRole.getAffiliation(), senderRole.getRole(), role.getAffiliation(), role.getRole(), newAffiliation, newRole)) {
// throw new NotAllowedException();
// }
// Update the presence with the new affiliation and role
if (role.isLocal()) {
role.setAffiliation(newAffiliation);
role.setRole(newRole);
// Notify the other cluster nodes to update the occupant
CacheFactory.doClusterTask(new UpdateOccupant(this, role));
// Prepare a new presence to be sent to all the room occupants
presences.add(role.getPresence().createCopy());
}
else {
// Ask the cluster node hosting the occupant to make the changes. Note that if the change
// is not allowed a NotAllowedException will be thrown
Element element = (Element) CacheFactory.doSynchronousClusterTask(
new UpdateOccupantRequest(this, role.getNickname(), newAffiliation, newRole),
role.getNodeID().toByteArray());
if (element != null) {
// Prepare a new presence to be sent to all the room occupants
presences.add(new Presence(element, true));
}
else {
throw new NotAllowedException();
}
}
}
// Answer all the updated presences
return presences;
}
/**
* Updates the presence of the given user with the new role information. Do nothing if the given
* jid is not present in the room.
*
* @param jid the full jid of the user to update his/her role.
* @param newRole the new role for the JID.
* @return the updated presence of the user or null if none.
* @throws NotAllowedException If trying to change the moderator role to an owner or an admin.
*/
private Presence changeOccupantRole(JID jid, MUCRole.Role newRole) throws NotAllowedException {
// Try looking the role in the bare JID list
MUCRole role = occupantsByFullJID.get(jid);
// TODO
// if (!isPrivilegedToChangeAffiliationAndRole(senderRole.getAffiliation(), senderRole.getRole(), role.getAffiliation(), role.getRole(), newAffiliation, newRole)) {
// throw new NotAllowedException();
// }
if (role != null) {
if (role.isLocal()) {
// Update the presence with the new role
role.setRole(newRole);
// Notify the other cluster nodes to update the occupant
CacheFactory.doClusterTask(new UpdateOccupant(this, role));
// Prepare a new presence to be sent to all the room occupants
return role.getPresence().createCopy();
}
else {
// Ask the cluster node hosting the occupant to make the changes. Note that if the change
// is not allowed a NotAllowedException will be thrown
Element element = (Element) CacheFactory.doSynchronousClusterTask(
new UpdateOccupantRequest(this, role.getNickname(), null, newRole),
role.getNodeID().toByteArray());
if (element != null) {
// Prepare a new presence to be sent to all the room occupants
return new Presence(element, true);
}
else {
throw new NotAllowedException();
}
}
}
return null;
}
static boolean isPrivilegedToChangeAffiliationAndRole(MUCRole.Affiliation actorAffiliation, MUCRole.Role actorRole, MUCRole.Affiliation occupantAffiliation, MUCRole.Role occupantRole, MUCRole.Affiliation newAffiliation, MUCRole.Role newRole) {
switch (actorAffiliation) {
case owner:
// An owner has all privileges
return true;
case admin:
// If affiliation has not changed
if (occupantAffiliation == newAffiliation) {
// Only check, if the admin wants to modify an owner (e.g. revoke an owner's moderator role).
return occupantAffiliation != MUCRole.Affiliation.owner;
} else {
// An admin is not allowed to modify the admin or owner list.
return occupantAffiliation != MUCRole.Affiliation.owner && newAffiliation != MUCRole.Affiliation.admin && newAffiliation != MUCRole.Affiliation.owner;
}
default:
// Every other affiliation (member, none, outcast) is not allowed to change anything, except he's a moderator and he doesn't want to change affiliations.
if (actorRole == MUCRole.Role.moderator && occupantAffiliation == newAffiliation) {
// A moderator SHOULD NOT be allowed to revoke moderation privileges from someone with a higher affiliation than themselves
// (i.e., an unaffiliated moderator SHOULD NOT be allowed to revoke moderation privileges from an admin or an owner, and an admin SHOULD NOT be allowed to revoke moderation privileges from an owner).
if (occupantRole == MUCRole.Role.moderator && newRole != MUCRole.Role.moderator) {
return occupantAffiliation != MUCRole.Affiliation.owner && occupantAffiliation != MUCRole.Affiliation.admin;
}
}
return false;
}
}
@Override
public void addFirstOwner(JID bareJID) {
owners.add( bareJID.asBareJID() );
}
@Override
public List addOwner(JID jid, MUCRole sendRole) throws ForbiddenException {
final JID bareJID = jid.asBareJID();
lock.writeLock().lock();
try {
MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
if (MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
throw new ForbiddenException();
}
// Check if user is already an owner (explicitly)
if (owners.contains(bareJID)) {
// Do nothing
return Collections.emptyList();
}
owners.add(bareJID);
// Remove the user from other affiliation lists
if (removeAdmin(bareJID)) {
oldAffiliation = MUCRole.Affiliation.admin;
}
else if (removeMember(bareJID)) {
oldAffiliation = MUCRole.Affiliation.member;
}
else if (removeOutcast(bareJID)) {
oldAffiliation = MUCRole.Affiliation.outcast;
}
// Update the DB if the room is persistent
MUCPersistenceManager.saveAffiliationToDB(
this,
bareJID,
null,
MUCRole.Affiliation.owner,
oldAffiliation);
}
finally {
lock.writeLock().unlock();
}
// Update other cluster nodes with new affiliation
CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.owner));
// apply the affiliation change, assigning a new affiliation
// based on the group(s) of the affected user(s)
return applyAffiliationChange(getRole(), bareJID, null);
}
private boolean removeOwner(JID jid) {
return owners.remove(jid.asBareJID());
}
@Override
public List addAdmin(JID jid, MUCRole sendRole) throws ForbiddenException,
ConflictException {
final JID bareJID = jid.asBareJID();
lock.writeLock().lock();
try {
MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
if (MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
throw new ForbiddenException();
}
// Check that the room always has an owner
if (owners.contains(bareJID) && owners.size() == 1) {
throw new ConflictException();
}
// Check if user is already an admin
if (admins.contains(bareJID)) {
// Do nothing
return Collections.emptyList();
}
admins.add(bareJID);
// Remove the user from other affiliation lists
if (removeOwner(bareJID)) {
oldAffiliation = MUCRole.Affiliation.owner;
}
else if (removeMember(bareJID)) {
oldAffiliation = MUCRole.Affiliation.member;
}
else if (removeOutcast(bareJID)) {
oldAffiliation = MUCRole.Affiliation.outcast;
}
// Update the DB if the room is persistent
MUCPersistenceManager.saveAffiliationToDB(
this,
bareJID,
null,
MUCRole.Affiliation.admin,
oldAffiliation);
}
finally {
lock.writeLock().unlock();
}
// Update other cluster nodes with new affiliation
CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.admin));
// apply the affiliation change, assigning a new affiliation
// based on the group(s) of the affected user(s)
return applyAffiliationChange(getRole(), bareJID, null);
}
private boolean removeAdmin(JID bareJID) {
return admins.remove( bareJID.asBareJID() );
}
@Override
public List addMember(JID jid, String nickname, MUCRole sendRole)
throws ForbiddenException, ConflictException {
final JID bareJID = jid.asBareJID();
lock.writeLock().lock();
try {
MUCRole.Affiliation oldAffiliation = (members.containsKey(bareJID) ?
MUCRole.Affiliation.member : MUCRole.Affiliation.none);
if (isMembersOnly()) {
if (!canOccupantsInvite()) {
if (MUCRole.Affiliation.admin != sendRole.getAffiliation()
&& MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
throw new ForbiddenException();
}
}
}
else {
if (MUCRole.Affiliation.admin != sendRole.getAffiliation()
&& MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
throw new ForbiddenException();
}
}
// Check if the desired nickname is already reserved for another member
if (nickname != null && nickname.trim().length() > 0 && members.containsValue(nickname.toLowerCase())) {
if (!nickname.equals(members.get(bareJID))) {
throw new ConflictException();
}
} else if (isLoginRestrictedToNickname() && (nickname == null || nickname.trim().length() == 0)) {
throw new ConflictException();
}
// Check that the room always has an owner
if (owners.contains(bareJID) && owners.size() == 1) {
throw new ConflictException();
}
// Check if user is already an member
if (members.containsKey(bareJID)) {
// Do nothing
return Collections.emptyList();
}
// Associate the reserved nickname with the bareJID. If nickname is null then associate an
// empty string
members.put(bareJID, (nickname == null ? "" : nickname.toLowerCase()));
// Remove the user from other affiliation lists
if (removeOwner(bareJID)) {
oldAffiliation = MUCRole.Affiliation.owner;
}
else if (removeAdmin(bareJID)) {
oldAffiliation = MUCRole.Affiliation.admin;
}
else if (removeOutcast(bareJID)) {
oldAffiliation = MUCRole.Affiliation.outcast;
}
// Update the DB if the room is persistent
MUCPersistenceManager.saveAffiliationToDB(
this,
bareJID,
nickname,
MUCRole.Affiliation.member,
oldAffiliation);
}
finally {
lock.writeLock().unlock();
}
// Update other cluster nodes with new member
CacheFactory.doClusterTask(new AddMember(this, jid.toBareJID(), (nickname == null ? "" : nickname)));
// apply the affiliation change, assigning a new affiliation
// based on the group(s) of the affected user(s)
return applyAffiliationChange(getRole(), bareJID, null);
}
private boolean removeMember(JID jid) {
return members.remove(jid.asBareJID()) != null;
}
@Override
public List addOutcast(JID jid, String reason, MUCRole senderRole)
throws NotAllowedException, ForbiddenException, ConflictException {
final JID bareJID = jid.asBareJID();
lock.writeLock().lock();
try {
MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
if (MUCRole.Affiliation.admin != senderRole.getAffiliation()
&& MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
throw new ForbiddenException();
}
// Check that the room always has an owner
if (owners.contains(bareJID) && owners.size() == 1) {
throw new ConflictException();
}
// Check if user is already an outcast
if (outcasts.contains(bareJID)) {
// Do nothing
return Collections.emptyList();
}
// Update the affiliation lists
outcasts.add(bareJID);
// Remove the user from other affiliation lists
if (removeOwner(bareJID)) {
oldAffiliation = MUCRole.Affiliation.owner;
}
else if (removeAdmin(bareJID)) {
oldAffiliation = MUCRole.Affiliation.admin;
}
else if (removeMember(bareJID)) {
oldAffiliation = MUCRole.Affiliation.member;
}
// Update the DB if the room is persistent
MUCPersistenceManager.saveAffiliationToDB(
this,
bareJID,
null,
MUCRole.Affiliation.outcast,
oldAffiliation);
}
finally {
lock.writeLock().unlock();
}
// Update other cluster nodes with new affiliation
CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.outcast));
// apply the affiliation change, assigning a new affiliation
// based on the group(s) of the affected user(s)
return applyAffiliationChange(senderRole, bareJID, reason);
}
private boolean removeOutcast(JID bareJID) {
return outcasts.remove( bareJID.asBareJID() );
}
@Override
public List addNone(JID jid, MUCRole senderRole) throws ForbiddenException, ConflictException {
final JID bareJID = jid.asBareJID();
MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
boolean jidWasAffiliated = false;
lock.writeLock().lock();
try {
if (MUCRole.Affiliation.admin != senderRole.getAffiliation()
&& MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
throw new ForbiddenException();
}
// Check that the room always has an owner
if (owners.contains(bareJID) && owners.size() == 1) {
throw new ConflictException();
}
// Remove the jid from ALL the affiliation lists
if (removeOwner(bareJID)) {
oldAffiliation = MUCRole.Affiliation.owner;
jidWasAffiliated = true;
}
else if (removeAdmin(bareJID)) {
oldAffiliation = MUCRole.Affiliation.admin;
jidWasAffiliated = true;
}
else if (removeMember(bareJID)) {
oldAffiliation = MUCRole.Affiliation.member;
jidWasAffiliated = true;
}
else if (removeOutcast(bareJID)) {
oldAffiliation = MUCRole.Affiliation.outcast;
}
// Remove the affiliation of this user from the DB if the room is persistent
MUCPersistenceManager.removeAffiliationFromDB(this, bareJID, oldAffiliation);
}
finally {
lock.writeLock().unlock();
}
// Update other cluster nodes with new affiliation
CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.none));
if (jidWasAffiliated) {
// apply the affiliation change, assigning a new affiliation
// based on the group(s) of the affected user(s)
return applyAffiliationChange(senderRole, bareJID, null);
} else {
// no presence updates needed
return Collections.emptyList();
}
}
/**
* Evaluate the given JID to determine what the appropriate affiliation should be
* after a change has been made. Each affected user will be granted the highest
* affiliation they now possess, either explicitly or implicitly via membership
* in one or more groups. If the JID is a user, the effective affiliation is
* applied to each presence corresponding to that user. If the given JID is a group,
* each user in the group is evaluated to determine what their new affiliations will
* be. The returned presence updates will be broadcast to the occupants of the room.
*
* @param senderRole Typically the room itself, or an owner/admin
* @param affiliationJID The JID for the user or group that has been changed
* @param reason An optional reason to explain why a user was kicked from the room
* @return List of presence updates to be delivered to the room's occupants
*/
private List applyAffiliationChange(MUCRole senderRole, final JID affiliationJID, String reason) {
// Update the presence(s) for the new affiliation and inform all occupants
List affectedOccupants = new ArrayList<>();
// first, determine which actual (user) JIDs are affected by the affiliation change
if (GroupJID.isGroup(affiliationJID)) {
try {
Group group = GroupManager.getInstance().getGroup(affiliationJID);
// check each occupant to see if they are in the group that was changed
// if so, calculate a new affiliation (if any) for the occupant(s)
for (JID groupMember : group.getAll()) {
if (occupantsByBareJID.containsKey(groupMember)) {
affectedOccupants.add(groupMember);
}
}
} catch (GroupNotFoundException gnfe) {
Log.error("Error updating group presences for " + affiliationJID , gnfe);
}
} else {
if (occupantsByBareJID.containsKey(affiliationJID)) {
affectedOccupants.add(affiliationJID);
}
}
// now update each of the affected occupants with a new role/affiliation
MUCRole.Role newRole;
MUCRole.Affiliation newAffiliation;
List updatedPresences = new ArrayList<>();
// new role/affiliation may be granted via group membership
for (JID occupantJID : affectedOccupants) {
Log.info("Applying affiliation change for " + occupantJID);
boolean kickMember = false, isOutcast = false;
if (owners.includes(occupantJID)) {
newRole = MUCRole.Role.moderator;
newAffiliation = MUCRole.Affiliation.owner;
}
else if (admins.includes(occupantJID)) {
newRole = MUCRole.Role.moderator;
newAffiliation = MUCRole.Affiliation.admin;
}
// outcast trumps member when an affiliation is changed
else if (outcasts.includes(occupantJID)) {
newAffiliation = MUCRole.Affiliation.outcast;
newRole = MUCRole.Role.none;
kickMember = true;
isOutcast = true;
}
else if (members.includesKey(occupantJID)) {
newRole = MUCRole.Role.participant;
newAffiliation = MUCRole.Affiliation.member;
}
else if (isMembersOnly()) {
newRole = MUCRole.Role.none;
newAffiliation = MUCRole.Affiliation.none;
kickMember = true;
}
else {
newRole = isModerated() ? MUCRole.Role.visitor : MUCRole.Role.participant;
newAffiliation = MUCRole.Affiliation.none;
}
Log.info("New affiliation: " + newAffiliation);
try {
List thisOccupant = changeOccupantAffiliation(senderRole, occupantJID, newAffiliation, newRole);
if (kickMember) {
// If the room is members-only, remove the user from the room including a status
// code of 321 to indicate that the user was removed because of an affiliation change
// a status code of 301 indicates the user was removed as an outcast
for (Presence presence : thisOccupant) {
presence.setType(Presence.Type.unavailable);
presence.setStatus(null);
Element x = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
if (reason != null && reason.trim().length() > 0) {
x.element("item").addElement("reason").setText(reason);
}
x.addElement("status").addAttribute("code", isOutcast ? "301" : "321");
kickPresence(presence, senderRole.getUserAddress(), senderRole.getNickname());
}
}
updatedPresences.addAll(thisOccupant);
} catch (NotAllowedException e) {
Log.error("Error updating presences for " + occupantJID, e);
}
}
return updatedPresences;
}
@Override
public boolean isLocked() {
return lockedTime > 0;
}
@Override
public boolean isManuallyLocked() {
return lockedTime > 0 && creationDate.getTime() != lockedTime;
}
/**
* Handles occupants updating their presence in the chatroom. Assumes the user updates their presence whenever their
* availability in the room changes. This method should not be called to handle other presence related updates, such
* as nickname changes.
* {@inheritDoc}
*/
@Override
public void presenceUpdated(final MUCRole occupantRole, final Presence newPresence) {
final String occupantNickName = occupantRole.getNickname();
// Update the presence of the occupant on the local node with the occupant's new availability. Updates the
// local node first so the remote nodes receive presence that correctly reflects the occupant's new
// availability and previously existing role and affiliation with the room.
final UpdatePresence localUpdateRequest = new UpdatePresence(this, newPresence.createCopy(), occupantNickName);
localUpdateRequest.setOriginator(true);
localUpdateRequest.run();
// Get the new, updated presence for the occupant in the room. The presence reflects the occupant's updated
// availability and their existing association.
final Presence updatedPresence = occupantRole.getPresence().createCopy();
// Ask other cluster nodes to update the presence of the occupant. Uses the updated presence from the local
// MUC role.
final UpdatePresence clusterUpdateRequest = new UpdatePresence(this, updatedPresence, occupantNickName);
CacheFactory.doClusterTask(clusterUpdateRequest);
// Broadcast updated presence of occupant.
broadcastPresence(updatedPresence, false);
}
/**
* Updates the presence of an occupant with the new presence included in the request.
*
* @param updatePresence request to update an occupant's presence.
*/
public void presenceUpdated(UpdatePresence updatePresence) {
List occupants = occupantsByNickname.get(updatePresence.getNickname().toLowerCase());
if (occupants == null || occupants.size() == 0) {
Log.debug("LocalMUCRoom: Failed to update presence of room occupant. Occupant nickname: " + updatePresence.getNickname());
} else {
for (MUCRole occupant : occupants) {
occupant.setPresence(updatePresence.getPresence());
}
}
}
public void occupantUpdated(UpdateOccupant update) {
List occupants = occupantsByNickname.get(update.getNickname().toLowerCase());
if (occupants == null || occupants.size() == 0) {
Log.debug("LocalMUCRoom: Failed to update information of room occupant. Occupant nickname: " + update.getNickname());
} else {
for (MUCRole occupant : occupants) {
if (!occupant.isLocal()) {
occupant.setPresence(update.getPresence());
try {
occupant.setRole(update.getRole());
occupant.setAffiliation(update.getAffiliation());
} catch (NotAllowedException e) {
// Ignore. Should never happen with remote roles
}
}
else {
Log.error(MessageFormat.format("Ignoring update of local occupant with info from a remote occupant. "
+ "Occupant nickname: {0} new role: {1} new affiliation: {2}",
update.getNickname(), update.getRole(), update.getAffiliation()));
}
}
}
}
public Presence updateOccupant(UpdateOccupantRequest updateRequest) throws NotAllowedException {
Presence result = null;
List occupants = occupantsByNickname.get(updateRequest.getNickname().toLowerCase());
if (occupants == null || occupants.size() == 0) {
Log.debug("Failed to update information of local room occupant; nickname: " + updateRequest.getNickname());
} else {
for (MUCRole occupant : occupants) {
if (updateRequest.isAffiliationChanged()) {
occupant.setAffiliation(updateRequest.getAffiliation());
}
occupant.setRole(updateRequest.getRole());
// Notify the the cluster nodes to update the occupant
CacheFactory.doClusterTask(new UpdateOccupant(this, occupant));
if (result == null) {
result = occupant.getPresence();
}
}
}
return result;
}
public void memberAdded(AddMember addMember) {
JID bareJID = addMember.getBareJID();
removeOwner(bareJID);
removeAdmin(bareJID);
removeOutcast(bareJID);
// Associate the reserved nickname with the bareJID
members.put(addMember.getBareJID(), addMember.getNickname().toLowerCase());
}
public void affiliationAdded(AddAffiliation affiliation) {
JID affiliationJID = affiliation.getBareJID();
switch(affiliation.getAffiliation()) {
case owner:
removeMember(affiliationJID);
removeAdmin(affiliationJID);
removeOutcast(affiliationJID);
owners.add(affiliationJID);
break;
case admin:
removeMember(affiliationJID);
removeOwner(affiliationJID);
removeOutcast(affiliationJID);
admins.add(affiliationJID);
break;
case outcast:
removeMember(affiliationJID);
removeAdmin(affiliationJID);
removeOwner(affiliationJID);
outcasts.add(affiliationJID);
break;
case none:
default:
removeMember(affiliationJID);
removeAdmin(affiliationJID);
removeOwner(affiliationJID);
removeOutcast(affiliationJID);
break;
}
}
@Override
public void nicknameChanged(MUCRole occupantRole, Presence newPresence, String oldNick, String newNick) {
// Ask other cluster nodes to update the nickname of the occupant
ChangeNickname request = new ChangeNickname(this, oldNick, newNick, newPresence.createCopy());
CacheFactory.doClusterTask(request);
// Update the nickname of the occupant
request = new ChangeNickname(this, oldNick, newNick, newPresence.createCopy());
request.setOriginator(true);
request.run();
// Broadcast new presence of occupant
broadcastPresence(occupantRole.getPresence().createCopy(), false);
}
public void nicknameChanged(ChangeNickname changeNickname) {
List occupants = occupantsByNickname.get(changeNickname.getOldNick().toLowerCase());
if (occupants != null && occupants.size() > 0) {
for (MUCRole occupant : occupants) {
// Update the role with the new info
occupant.setPresence(changeNickname.getPresence());
occupant.changeNickname(changeNickname.getNewNick());
}
if (changeNickname.isOriginator()) {
// Fire event that user changed his nickname
MUCEventDispatcher.nicknameChanged(getRole().getRoleAddress(), occupants.get(0).getUserAddress(),
changeNickname.getOldNick(), changeNickname.getNewNick());
}
// Associate the existing MUCRole with the new nickname
occupantsByNickname.put(changeNickname.getNewNick().toLowerCase(), occupants);
// Remove the old nickname
occupantsByNickname.remove(changeNickname.getOldNick().toLowerCase());
}
}
@Override
public void changeSubject(Message packet, MUCRole role) throws ForbiddenException {
if ((canOccupantsChangeSubject() && role.getRole().compareTo(MUCRole.Role.visitor) < 0) ||
MUCRole.Role.moderator == role.getRole()) {
// Set the new subject to the room
subject = packet.getSubject();
MUCPersistenceManager.updateRoomSubject(this);
// Notify all the occupants that the subject has changed
packet.setFrom(role.getRoleAddress());
send(packet);
// Fire event signifying that the room's subject has changed.
MUCEventDispatcher.roomSubjectChanged(getJID(), role.getUserAddress(), subject);
// Let other cluster nodes that the room has been updated
CacheFactory.doClusterTask(new RoomUpdatedEvent(this));
}
else {
throw new ForbiddenException();
}
}
@Override
public String getSubject() {
return subject;
}
@Override
public void setSubject(String subject) {
this.subject = subject;
}
@Override
public void sendInvitation(JID to, String reason, MUCRole senderRole, List extensions)
throws ForbiddenException, CannotBeInvitedException {
if (!isMembersOnly() || canOccupantsInvite()
|| MUCRole.Affiliation.admin == senderRole.getAffiliation()
|| MUCRole.Affiliation.owner == senderRole.getAffiliation()) {
// If the room is not members-only OR if the room is members-only and anyone can send
// invitations or the sender is an admin or an owner, then send the invitation
Message message = new Message();
message.setFrom(role.getRoleAddress());
message.setTo(to);
if (((MultiUserChatServiceImpl)mucService).getMUCDelegate() != null) {
switch(((MultiUserChatServiceImpl)mucService).getMUCDelegate().sendingInvitation(this, to, senderRole.getUserAddress(), reason)) {
case HANDLED_BY_DELEGATE:
//if the delegate is taking care of it, there's nothing for us to do
return;
case HANDLED_BY_OPENFIRE:
//continue as normal if we're asked to handle it
break;
case REJECTED:
//we can't invite that person
throw new CannotBeInvitedException();
}
}
// Add a list of extensions sent with the original message invitation (if any)
if (extensions != null) {
for(Element element : extensions) {
element.setParent(null);
message.getElement().add(element);
}
}
Element frag = message.addChildElement("x", "http://jabber.org/protocol/muc#user");
// ChatUser will be null if the room itself (ie. via admin console) made the request
if (senderRole.getUserAddress() != null) {
frag.addElement("invite").addAttribute("from", senderRole.getUserAddress().toBareJID());
}
if (reason != null && reason.length() > 0) {
Element invite = frag.element("invite");
if (invite == null) {
invite = frag.addElement("invite");
}
invite.addElement("reason").setText(reason);
}
if (isPasswordProtected()) {
frag.addElement("password").setText(getPassword());
}
// Include the jabber:x:conference information for backward compatibility
frag = message.addChildElement("x", "jabber:x:conference");
frag.addAttribute("jid", role.getRoleAddress().toBareJID());
// Send the message with the invitation
router.route(message);
}
else {
throw new ForbiddenException();
}
}
@Override
public void sendInvitationRejection(JID to, String reason, JID sender) {
if (((MultiUserChatServiceImpl)mucService).getMUCDelegate() != null) {
switch(((MultiUserChatServiceImpl)mucService).getMUCDelegate().sendingInvitationRejection(this, to, sender, reason)) {
case HANDLED_BY_DELEGATE:
//if the delegate is taking care of it, there's nothing for us to do
return;
case HANDLED_BY_OPENFIRE:
//continue as normal if we're asked to handle it
break;
}
}
Message message = new Message();
message.setFrom(role.getRoleAddress());
message.setTo(to);
Element frag = message.addChildElement("x", "http://jabber.org/protocol/muc#user");
frag.addElement("decline").addAttribute("from", sender.toBareJID());
if (reason != null && reason.length() > 0) {
frag.element("decline").addElement("reason").setText(reason);
}
// Send the message with the invitation
router.route(message);
}
@Override
public IQOwnerHandler getIQOwnerHandler() {
return iqOwnerHandler;
}
@Override
public IQAdminHandler getIQAdminHandler() {
return iqAdminHandler;
}
@Override
public MUCRoomHistory getRoomHistory() {
return roomHistory;
}
@Override
public Collection getOwners() {
return Collections.unmodifiableList(owners);
}
@Override
public Collection getAdmins() {
return Collections.unmodifiableList(admins);
}
@Override
public Collection getMembers() {
return Collections.unmodifiableMap(members).keySet();
}
@Override
public Collection getOutcasts() {
return Collections.unmodifiableList(outcasts);
}
@Override
public Collection getModerators() {
List moderators = new ArrayList<>();
for (MUCRole role : occupantsByFullJID.values()) {
if (MUCRole.Role.moderator == role.getRole()) {
moderators.add(role);
}
}
return moderators;
}
@Override
public Collection getParticipants() {
List participants = new ArrayList<>();
for (MUCRole role : occupantsByFullJID.values()) {
if (MUCRole.Role.participant == role.getRole()) {
participants.add(role);
}
}
return participants;
}
@Override
public Presence addModerator(JID jid, MUCRole senderRole) throws ForbiddenException {
if (MUCRole.Affiliation.admin != senderRole.getAffiliation()
&& MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
throw new ForbiddenException();
}
// Update the presence with the new role and inform all occupants
try {
return changeOccupantRole(jid, MUCRole.Role.moderator);
}
catch (NotAllowedException e) {
// We should never receive this exception....in theory
return null;
}
}
@Override
public Presence addParticipant(JID jid, String reason, MUCRole senderRole)
throws NotAllowedException, ForbiddenException {
if (MUCRole.Role.moderator != senderRole.getRole()) {
throw new ForbiddenException();
}
// Update the presence with the new role and inform all occupants
Presence updatedPresence = changeOccupantRole(jid, MUCRole.Role.participant);
if (updatedPresence != null) {
Element frag = updatedPresence.getChildElement(
"x", "http://jabber.org/protocol/muc#user");
// Add the reason why the user was granted voice
if (reason != null && reason.trim().length() > 0) {
frag.element("item").addElement("reason").setText(reason);
}
}
return updatedPresence;
}
@Override
public Presence addVisitor(JID jid, MUCRole senderRole) throws NotAllowedException,
ForbiddenException {
if (MUCRole.Role.moderator != senderRole.getRole()) {
throw new ForbiddenException();
}
return changeOccupantRole(jid, MUCRole.Role.visitor);
}
@Override
public Presence kickOccupant(JID jid, JID actorJID, String actorNickname, String reason)
throws NotAllowedException {
// Update the presence with the new role and inform all occupants
Presence updatedPresence = changeOccupantRole(jid, MUCRole.Role.none);
if (updatedPresence != null) {
Element frag = updatedPresence.getChildElement(
"x", "http://jabber.org/protocol/muc#user");
// Add the status code 307 that indicates that the user was kicked
frag.addElement("status").addAttribute("code", "307");
// Add the reason why the user was kicked
if (reason != null && reason.trim().length() > 0) {
frag.element("item").addElement("reason").setText(reason);
}
// Effectively kick the occupant from the room
kickPresence(updatedPresence, actorJID, actorNickname);
//Inform the other occupants that user has been kicked
broadcastPresence(updatedPresence, false);
}
return updatedPresence;
}
/**
* Kicks the occupant from the room. This means that the occupant will receive an unavailable
* presence with the actor that initiated the kick (if any). The occupant will also be removed
* from the occupants lists.
*
* @param kickPresence the presence of the occupant to kick from the room.
* @param actorJID The JID of the actor that initiated the kick or null if the info
* @param nick The actor nickname.
* was not provided.
*/
private void kickPresence(Presence kickPresence, JID actorJID, String nick) {
// Get the role(s) to kick
List occupants = new ArrayList<>(occupantsByNickname.get(kickPresence.getFrom().getResource().toLowerCase()));
for (MUCRole kickedRole : occupants) {
// Add the actor's JID that kicked this user from the room
if (actorJID != null && actorJID.toString().length() > 0) {
Element frag = kickPresence.getChildElement(
"x", "http://jabber.org/protocol/muc#user");
Element actor = frag.element("item").addElement("actor");
actor.addAttribute("jid", actorJID.toBareJID());
if (nick != null) {
actor.addAttribute("nick", nick);
}
}
// Send the unavailable presence to the banned user
kickedRole.send(kickPresence);
// Remove the occupant from the room's occupants lists
OccupantLeftEvent event = new OccupantLeftEvent(this, kickedRole);
event.setOriginator(true);
event.run();
// Remove the occupant from the room's occupants lists
event = new OccupantLeftEvent(this, kickedRole);
CacheFactory.doClusterTask(event);
}
}
@Override
public boolean canAnyoneDiscoverJID() {
return canAnyoneDiscoverJID;
}
@Override
public void setCanAnyoneDiscoverJID(boolean canAnyoneDiscoverJID) {
this.canAnyoneDiscoverJID = canAnyoneDiscoverJID;
}
@Override
public String canSendPrivateMessage() {
return canSendPrivateMessage == null ? "anyone" : canSendPrivateMessage;
}
@Override
public void setCanSendPrivateMessage(String role) {
if ( role == null ) {
role = "(null)";
}
switch( role.toLowerCase() ) {
case "none":
case "moderators":
case "participants":
case "anyone":
this.canSendPrivateMessage = role.toLowerCase();
break;
default:
Log.warn( "Illegal value for muc#roomconfig_allowpm: '{}'. Defaulting to 'anyone'", role.toLowerCase() );
this.canSendPrivateMessage = "anyone";
}
}
@Override
public boolean canOccupantsChangeSubject() {
return canOccupantsChangeSubject;
}
@Override
public void setCanOccupantsChangeSubject(boolean canOccupantsChangeSubject) {
this.canOccupantsChangeSubject = canOccupantsChangeSubject;
}
@Override
public boolean canOccupantsInvite() {
return canOccupantsInvite;
}
@Override
public void setCanOccupantsInvite(boolean canOccupantsInvite) {
this.canOccupantsInvite = canOccupantsInvite;
}
@Override
public String getNaturalLanguageName() {
return naturalLanguageName;
}
@Override
public void setNaturalLanguageName(String naturalLanguageName) {
this.naturalLanguageName = naturalLanguageName;
}
@Override
public String getDescription() {
return description;
}
@Override
public void setDescription(String description) {
this.description = description;
}
@Override
public boolean isMembersOnly() {
return membersOnly;
}
@Override
public List setMembersOnly(boolean membersOnly) {
List presences = new ArrayList<>();
if (membersOnly && !this.membersOnly) {
// If the room was not members-only and now it is, kick occupants that aren't member
// of the room
for (MUCRole occupant : occupantsByFullJID.values()) {
if (occupant.getAffiliation().compareTo(MUCRole.Affiliation.member) > 0) {
try {
presences.add(kickOccupant(occupant.getRoleAddress(), null, null,
LocaleUtils.getLocalizedString("muc.roomIsNowMembersOnly")));
}
catch (NotAllowedException e) {
Log.error(e.getMessage(), e);
}
}
}
}
this.membersOnly = membersOnly;
return presences;
}
@Override
public boolean isLogEnabled() {
return logEnabled;
}
@Override
public void setLogEnabled(boolean logEnabled) {
this.logEnabled = logEnabled;
}
@Override
public void setLoginRestrictedToNickname(boolean restricted) {
this.loginRestrictedToNickname = restricted;
}
@Override
public boolean isLoginRestrictedToNickname() {
return loginRestrictedToNickname;
}
@Override
public void setChangeNickname(boolean canChange) {
this.canChangeNickname = canChange;
}
@Override
public boolean canChangeNickname() {
return canChangeNickname;
}
@Override
public void setRegistrationEnabled(boolean registrationEnabled) {
this.registrationEnabled = registrationEnabled;
}
@Override
public boolean isRegistrationEnabled() {
return registrationEnabled;
}
@Override
public int getMaxUsers() {
return maxUsers;
}
@Override
public void setMaxUsers(int maxUsers) {
this.maxUsers = maxUsers;
}
@Override
public boolean isModerated() {
return moderated;
}
@Override
public void setModerated(boolean moderated) {
this.moderated = moderated;
}
@Override
public String getPassword() {
return password;
}
@Override
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean isPasswordProtected() {
return password != null && password.trim().length() > 0;
}
@Override
public boolean isPersistent() {
return persistent;
}
@Override
public boolean wasSavedToDB() {
return isPersistent() && savedToDB;
}
@Override
public void setSavedToDB(boolean saved) {
this.savedToDB = saved;
}
@Override
public void setPersistent(boolean persistent) {
this.persistent = persistent;
}
@Override
public boolean isPublicRoom() {
return !isDestroyed && publicRoom;
}
@Override
public void setPublicRoom(boolean publicRoom) {
this.publicRoom = publicRoom;
}
@Override
public List getRolesToBroadcastPresence() {
return Collections.unmodifiableList(rolesToBroadcastPresence);
}
@Override
public void setRolesToBroadcastPresence(List rolesToBroadcastPresence) {
// TODO If the list changes while there are occupants in the room we must send available or
// unavailable presences of the affected occupants to the rest of the occupants
this.rolesToBroadcastPresence = rolesToBroadcastPresence;
}
/**
* Returns true if we need to check whether a presence could be sent or not.
*
* @return true if we need to check whether a presence could be sent or not.
*/
private boolean hasToCheckRoleToBroadcastPresence() {
// For performance reasons the check is done based on the size of the collection.
return rolesToBroadcastPresence.size() < 3;
}
@Override
public boolean canBroadcastPresence(String roleToBroadcast) {
return "none".equals(roleToBroadcast) || rolesToBroadcastPresence.contains(roleToBroadcast);
}
@Override
public void lock(MUCRole senderRole) throws ForbiddenException {
if (MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
throw new ForbiddenException();
}
if (isLocked()) {
// Do nothing if the room was already locked
return;
}
setLocked(true);
}
@Override
public void unlock(MUCRole senderRole) throws ForbiddenException {
if (MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
throw new ForbiddenException();
}
if (!isLocked()) {
// Do nothing if the room was already unlocked
return;
}
setLocked(false);
}
private void setLocked(boolean locked) {
if (locked) {
this.lockedTime = System.currentTimeMillis();
}
else {
this.lockedTime = 0;
}
MUCPersistenceManager.updateRoomLock(this);
}
/**
* Sets the date when the room was locked. Initially when the room is created it is locked so
* the locked date is the creation date of the room. Afterwards, the room may be manually
* locked and unlocked so the locked date may be in these cases different than the creation
* date. A Date with time 0 means that the the room is unlocked.
*
* @param lockedTime the date when the room was locked.
*/
void setLockedDate(Date lockedTime) {
this.lockedTime = lockedTime.getTime();
}
/**
* Returns the date when the room was locked. Initially when the room is created it is locked so
* the locked date is the creation date of the room. Afterwards, the room may be manually
* locked and unlocked so the locked date may be in these cases different than the creation
* date. When the room is unlocked a Date with time 0 is returned.
*
* @return the date when the room was locked.
*/
Date getLockedDate() {
return new Date(lockedTime);
}
@Override
public List addAdmins(List newAdmins, MUCRole senderRole)
throws ForbiddenException, ConflictException {
List answer = new ArrayList<>(newAdmins.size());
for (JID newAdmin : newAdmins) {
final JID bareJID = newAdmin.asBareJID();
if (!admins.contains(bareJID)) {
answer.addAll(addAdmin(bareJID, senderRole));
}
}
return answer;
}
@Override
public List addOwners(List newOwners, MUCRole senderRole)
throws ForbiddenException {
List answer = new ArrayList<>(newOwners.size());
for (JID newOwner : newOwners) {
final JID bareJID = newOwner.asBareJID();
if (!owners.contains(newOwner)) {
answer.addAll(addOwner(bareJID, senderRole));
}
}
return answer;
}
@Override
public void saveToDB() {
// Make the room persistent
MUCPersistenceManager.saveToDB(this);
if (!savedToDB) {
// Set that the room is now in the DB
savedToDB = true;
// Save the existing room owners to the DB
for (JID owner : owners) {
MUCPersistenceManager.saveAffiliationToDB(
this,
owner,
null,
MUCRole.Affiliation.owner,
MUCRole.Affiliation.none);
}
// Save the existing room admins to the DB
for (JID admin : admins) {
MUCPersistenceManager.saveAffiliationToDB(
this,
admin,
null,
MUCRole.Affiliation.admin,
MUCRole.Affiliation.none);
}
// Save the existing room members to the DB
for (JID bareJID : members.keySet()) {
MUCPersistenceManager.saveAffiliationToDB(this, bareJID, members.get(bareJID),
MUCRole.Affiliation.member, MUCRole.Affiliation.none);
}
// Save the existing room outcasts to the DB
for (JID outcast : outcasts) {
MUCPersistenceManager.saveAffiliationToDB(
this,
outcast,
null,
MUCRole.Affiliation.outcast,
MUCRole.Affiliation.none);
}
}
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeSafeUTF(out, name);
ExternalizableUtil.getInstance().writeLong(out, startTime);
ExternalizableUtil.getInstance().writeLong(out, lockedTime);
ExternalizableUtil.getInstance().writeSerializableCollection(out, owners);
ExternalizableUtil.getInstance().writeSerializableCollection(out, admins);
ExternalizableUtil.getInstance().writeSerializableMap(out, members);
ExternalizableUtil.getInstance().writeSerializableCollection(out, outcasts);
ExternalizableUtil.getInstance().writeSafeUTF(out, naturalLanguageName);
ExternalizableUtil.getInstance().writeSafeUTF(out, description);
ExternalizableUtil.getInstance().writeBoolean(out, canOccupantsChangeSubject);
ExternalizableUtil.getInstance().writeInt(out, maxUsers);
ExternalizableUtil.getInstance().writeStringList(out, rolesToBroadcastPresence);
ExternalizableUtil.getInstance().writeBoolean(out, publicRoom);
ExternalizableUtil.getInstance().writeBoolean(out, persistent);
ExternalizableUtil.getInstance().writeBoolean(out, moderated);
ExternalizableUtil.getInstance().writeBoolean(out, membersOnly);
ExternalizableUtil.getInstance().writeBoolean(out, canOccupantsInvite);
ExternalizableUtil.getInstance().writeSafeUTF(out, password);
ExternalizableUtil.getInstance().writeBoolean(out, canAnyoneDiscoverJID);
ExternalizableUtil.getInstance().writeBoolean(out, logEnabled);
ExternalizableUtil.getInstance().writeBoolean(out, loginRestrictedToNickname);
ExternalizableUtil.getInstance().writeBoolean(out, canChangeNickname);
ExternalizableUtil.getInstance().writeBoolean(out, registrationEnabled);
ExternalizableUtil.getInstance().writeSafeUTF(out, subject);
ExternalizableUtil.getInstance().writeLong(out, roomID);
ExternalizableUtil.getInstance().writeLong(out, creationDate.getTime());
ExternalizableUtil.getInstance().writeLong(out, modificationDate.getTime());
ExternalizableUtil.getInstance().writeBoolean(out, emptyDate != null);
if (emptyDate != null) {
ExternalizableUtil.getInstance().writeLong(out, emptyDate.getTime());
}
ExternalizableUtil.getInstance().writeBoolean(out, savedToDB);
ExternalizableUtil.getInstance().writeSafeUTF(out, mucService.getServiceName());
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = ExternalizableUtil.getInstance().readSafeUTF(in);
startTime = ExternalizableUtil.getInstance().readLong(in);
lockedTime = ExternalizableUtil.getInstance().readLong(in);
ExternalizableUtil.getInstance().readSerializableCollection(in, owners, getClass().getClassLoader());
ExternalizableUtil.getInstance().readSerializableCollection(in, admins, getClass().getClassLoader());
ExternalizableUtil.getInstance().readSerializableMap(in, members, getClass().getClassLoader());
ExternalizableUtil.getInstance().readSerializableCollection(in, outcasts, getClass().getClassLoader());
naturalLanguageName = ExternalizableUtil.getInstance().readSafeUTF(in);
description = ExternalizableUtil.getInstance().readSafeUTF(in);
canOccupantsChangeSubject = ExternalizableUtil.getInstance().readBoolean(in);
maxUsers = ExternalizableUtil.getInstance().readInt(in);
rolesToBroadcastPresence.addAll(ExternalizableUtil.getInstance().readStringList(in));
publicRoom = ExternalizableUtil.getInstance().readBoolean(in);
persistent = ExternalizableUtil.getInstance().readBoolean(in);
moderated = ExternalizableUtil.getInstance().readBoolean(in);
membersOnly = ExternalizableUtil.getInstance().readBoolean(in);
canOccupantsInvite = ExternalizableUtil.getInstance().readBoolean(in);
password = ExternalizableUtil.getInstance().readSafeUTF(in);
canAnyoneDiscoverJID = ExternalizableUtil.getInstance().readBoolean(in);
logEnabled = ExternalizableUtil.getInstance().readBoolean(in);
loginRestrictedToNickname = ExternalizableUtil.getInstance().readBoolean(in);
canChangeNickname = ExternalizableUtil.getInstance().readBoolean(in);
registrationEnabled = ExternalizableUtil.getInstance().readBoolean(in);
subject = ExternalizableUtil.getInstance().readSafeUTF(in);
roomID = ExternalizableUtil.getInstance().readLong(in);
creationDate = new Date(ExternalizableUtil.getInstance().readLong(in));
modificationDate = new Date(ExternalizableUtil.getInstance().readLong(in));
if (ExternalizableUtil.getInstance().readBoolean(in)) {
emptyDate = new Date(ExternalizableUtil.getInstance().readLong(in));
}
savedToDB = ExternalizableUtil.getInstance().readBoolean(in);
String subdomain = ExternalizableUtil.getInstance().readSafeUTF(in);
mucService = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(subdomain);
if (mucService == null) throw new IllegalArgumentException("MUC service not found for subdomain: " + subdomain);
roomHistory = new MUCRoomHistory(this, new HistoryStrategy(mucService.getHistoryStrategy()));
PacketRouter packetRouter = XMPPServer.getInstance().getPacketRouter();
this.iqOwnerHandler = new IQOwnerHandler(this, packetRouter);
this.iqAdminHandler = new IQAdminHandler(this, packetRouter);
router = packetRouter;
}
public void updateConfiguration(LocalMUCRoom otherRoom) {
startTime = otherRoom.startTime;
lockedTime = otherRoom.lockedTime;
owners = otherRoom.owners;
admins = otherRoom.admins;
members = otherRoom.members;
outcasts = otherRoom.outcasts;
naturalLanguageName = otherRoom.naturalLanguageName;
description = otherRoom.description;
canOccupantsChangeSubject = otherRoom.canOccupantsChangeSubject;
maxUsers = otherRoom.maxUsers;
rolesToBroadcastPresence = otherRoom.rolesToBroadcastPresence;
publicRoom = otherRoom.publicRoom;
persistent = otherRoom.persistent;
moderated = otherRoom.moderated;
membersOnly = otherRoom.membersOnly;
canOccupantsInvite = otherRoom.canOccupantsInvite;
password = otherRoom.password;
canAnyoneDiscoverJID = otherRoom.canAnyoneDiscoverJID;
logEnabled = otherRoom.logEnabled;
loginRestrictedToNickname = otherRoom.loginRestrictedToNickname;
canChangeNickname = otherRoom.canChangeNickname;
registrationEnabled = otherRoom.registrationEnabled;
subject = otherRoom.subject;
roomID = otherRoom.roomID;
creationDate = otherRoom.creationDate;
modificationDate = otherRoom.modificationDate;
emptyDate = otherRoom.emptyDate;
savedToDB = otherRoom.savedToDB;
mucService = otherRoom.mucService;
}
/*
* (non-Javadoc)
* @see org.jivesoftware.util.resultsetmanager.Result#getUID()
*/
@Override
public String getUID()
{
// name is unique for each one particular MUC service.
return name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((creationDate == null) ? 0 : creationDate.hashCode());
result = prime * result
+ ((description == null) ? 0 : description.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result
+ ((password == null) ? 0 : password.hashCode());
result = prime * result + (int) (roomID ^ (roomID >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
LocalMUCRoom other = (LocalMUCRoom) obj;
if (creationDate == null) {
if (other.creationDate != null)
return false;
} else if (!creationDate.equals(other.creationDate))
return false;
if (description == null) {
if (other.description != null)
return false;
} else if (!description.equals(other.description))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (roomID != other.roomID)
return false;
return true;
}
// overrides for important Group events
@Override
public void groupDeleting(Group group, Map params) {
// remove the group from this room's affiliations
GroupJID groupJID = group.getJID();
try {
addNone(groupJID, getRole());
} catch (Exception ex) {
Log.error("Failed to remove deleted group from affiliation lists: " + groupJID, ex);
}
}
@Override
public void groupModified(Group group, Map params) {
// check the affiliation lists for the old group jid, replace with a new group jid
if ("nameModified".equals(params.get("type"))) {
GroupJID originalJID = (GroupJID) params.get("originalJID");
GroupJID newJID = group.getJID();
try {
if (owners.contains(originalJID)) {
addOwner(newJID, getRole());
} else if (admins.contains(originalJID)) {
addAdmin(newJID, getRole());
} else if (outcasts.contains(originalJID)) {
addOutcast(newJID, null, getRole());
} else if (members.containsKey(originalJID)) {
addMember(newJID, null, getRole());
}
addNone(originalJID, getRole());
} catch (Exception ex) {
Log.error("Failed to update group affiliation for " + newJID, ex);
}
}
}
@Override
public void memberAdded(Group group, Map params) {
applyAffiliationChangeAndSendPresence(new JID((String)params.get("member")));
}
@Override
public void memberRemoved(Group group, Map params) {
applyAffiliationChangeAndSendPresence(new JID((String)params.get("member")));
}
@Override
public void adminAdded(Group group, Map params) {
applyAffiliationChangeAndSendPresence(new JID((String)params.get("admin")));
}
@Override
public void adminRemoved(Group group, Map params) {
applyAffiliationChangeAndSendPresence(new JID((String)params.get("admin")));
}
private void applyAffiliationChangeAndSendPresence(JID groupMember) {
List presences = applyAffiliationChange(getRole(), groupMember, null);
for (Presence presence : presences) {
send(presence);
}
}
@Override
public void groupCreated(Group group, Map params) {
// ignore
}
}