
rocks.xmpp.extensions.muc.ChatRoom Maven / Gradle / Ivy
Show all versions of xmpp-extensions-client Show documentation
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.extensions.muc;
import rocks.xmpp.addr.Jid;
import rocks.xmpp.core.session.SendTask;
import rocks.xmpp.core.session.XmppSession;
import rocks.xmpp.core.stanza.MessageEvent;
import rocks.xmpp.core.stanza.PresenceEvent;
import rocks.xmpp.core.stanza.model.IQ;
import rocks.xmpp.core.stanza.model.Message;
import rocks.xmpp.core.stanza.model.Presence;
import rocks.xmpp.extensions.data.model.DataForm;
import rocks.xmpp.extensions.delay.model.DelayedDelivery;
import rocks.xmpp.extensions.disco.ServiceDiscoveryManager;
import rocks.xmpp.extensions.disco.model.info.Identity;
import rocks.xmpp.extensions.disco.model.info.InfoNode;
import rocks.xmpp.extensions.muc.conference.model.DirectInvitation;
import rocks.xmpp.extensions.muc.model.Actor;
import rocks.xmpp.extensions.muc.model.Affiliation;
import rocks.xmpp.extensions.muc.model.DiscussionHistory;
import rocks.xmpp.extensions.muc.model.Item;
import rocks.xmpp.extensions.muc.model.Muc;
import rocks.xmpp.extensions.muc.model.MucFeature;
import rocks.xmpp.extensions.muc.model.RequestVoice;
import rocks.xmpp.extensions.muc.model.Role;
import rocks.xmpp.extensions.muc.model.RoomConfiguration;
import rocks.xmpp.extensions.muc.model.RoomInfo;
import rocks.xmpp.extensions.muc.model.RoomRegistration;
import rocks.xmpp.extensions.muc.model.admin.MucAdmin;
import rocks.xmpp.extensions.muc.model.owner.MucOwner;
import rocks.xmpp.extensions.muc.model.user.Decline;
import rocks.xmpp.extensions.muc.model.user.Invite;
import rocks.xmpp.extensions.muc.model.user.MucUser;
import rocks.xmpp.extensions.muc.model.user.Status;
import rocks.xmpp.extensions.register.model.Registration;
import rocks.xmpp.im.chat.Chat;
import rocks.xmpp.util.XmppUtils;
import rocks.xmpp.util.concurrent.AsyncResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
/**
* Represents a multi-user chat room.
*
* Use this class to enter a chat room, to send and receive messages, invite others and to manage members (e.g. kick or ban a user, grant admin status, etc.).
*
* @author Christian Schudt
*/
public final class ChatRoom extends Chat implements Comparable {
private final Set> invitationDeclineListeners = new CopyOnWriteArraySet<>();
private final Set> subjectChangeListeners = new CopyOnWriteArraySet<>();
private final Set> occupantListeners = new CopyOnWriteArraySet<>();
private final Map occupantMap = new HashMap<>();
private final ServiceDiscoveryManager serviceDiscoveryManager;
private final MultiUserChatManager multiUserChatManager;
private final String name;
private final Jid roomJid;
private final XmppSession xmppSession;
private final Consumer messageListener;
private final Consumer presenceListener;
private String nick;
private boolean entered;
ChatRoom(final Jid roomJid, String name, XmppSession xmppSession, ServiceDiscoveryManager serviceDiscoveryManager, MultiUserChatManager multiUserChatManager) {
if (Objects.requireNonNull(roomJid).getLocal() == null) {
throw new IllegalArgumentException("roomJid must have a local part.");
}
if (roomJid.getResource() != null) {
throw new IllegalArgumentException("roomJid must not have a resource part: " + roomJid.getResource());
}
this.name = name;
this.roomJid = roomJid;
this.xmppSession = xmppSession;
this.serviceDiscoveryManager = serviceDiscoveryManager;
this.multiUserChatManager = multiUserChatManager;
this.messageListener = e -> {
Message message = e.getMessage();
if (message.getFrom().asBareJid().equals(roomJid)) {
if (message.getType() == Message.Type.GROUPCHAT) {
// This is a stanza from the room JID (or from the occupant JID of the entity that set the subject), with a element but no element
if (message.getSubject() != null && message.getBody() == null) {
XmppUtils.notifyEventListeners(subjectChangeListeners, new SubjectChangeEvent(ChatRoom.this, message.getSubject(), message.getFrom().getResource(), message.hasExtension(DelayedDelivery.class), DelayedDelivery.sendDate(message)));
} else {
XmppUtils.notifyEventListeners(inboundMessageListeners, new MessageEvent(ChatRoom.this, message, true));
}
} else {
MucUser mucUser = message.getExtension(MucUser.class);
if (mucUser != null) {
Decline decline = mucUser.getDecline();
if (decline != null) {
XmppUtils.notifyEventListeners(invitationDeclineListeners, new InvitationDeclineEvent(ChatRoom.this, roomJid, decline.getFrom(), decline.getReason()));
}
}
}
}
};
this.presenceListener = e -> {
Presence presence = e.getPresence();
// If the presence came from the room.
if (presence.getFrom() != null && presence.getFrom().asBareJid().equals(roomJid)) {
MucUser mucUser = presence.getExtension(MucUser.class);
if (mucUser != null) {
String nick = presence.getFrom().getResource();
if (nick != null) {
boolean isSelfPresence = isSelfPresence(presence, nick);
Occupant occupant = new Occupant(presence, isSelfPresence);
if (presence.isAvailable()) {
Occupant previousOccupant = occupantMap.put(nick, occupant);
OccupantEvent.Type type;
// A new occupant entered the room.
if (previousOccupant == null) {
// Only notify about "joins", if it's not our own join and we are already in the room.
if (!isSelfPresence && hasEntered()) {
type = OccupantEvent.Type.ENTERED;
} else {
type = OccupantEvent.Type.STATUS_CHANGED;
}
} else {
type = OccupantEvent.Type.STATUS_CHANGED;
}
XmppUtils.notifyEventListeners(occupantListeners, new OccupantEvent(ChatRoom.this, occupant, type, null, null, null));
} else if (presence.getType() == Presence.Type.UNAVAILABLE) {
// Occupant has exited the room.
occupantMap.remove(nick);
OccupantEvent occupantEvent = null;
if (mucUser.getItem() != null) {
Actor actor = mucUser.getItem().getActor();
String reason = mucUser.getItem().getReason();
if (!mucUser.getStatusCodes().isEmpty()) {
if (mucUser.getStatusCodes().contains(Status.KICKED)) {
occupantEvent = new OccupantEvent(ChatRoom.this, occupant, OccupantEvent.Type.KICKED, actor, reason, null);
} else if (mucUser.getStatusCodes().contains(Status.BANNED)) {
occupantEvent = new OccupantEvent(ChatRoom.this, occupant, OccupantEvent.Type.BANNED, actor, reason, null);
} else if (mucUser.getStatusCodes().contains(Status.MEMBERSHIP_REVOKED)) {
occupantEvent = new OccupantEvent(ChatRoom.this, occupant, OccupantEvent.Type.MEMBERSHIP_REVOKED, actor, reason, null);
} else if (mucUser.getStatusCodes().contains(Status.NICK_CHANGED)) {
occupantEvent = new OccupantEvent(ChatRoom.this, occupant, OccupantEvent.Type.NICKNAME_CHANGED, actor, reason, null);
} else if (mucUser.getStatusCodes().contains(Status.SERVICE_SHUT_DOWN)) {
occupantEvent = new OccupantEvent(ChatRoom.this, occupant, OccupantEvent.Type.SYSTEM_SHUTDOWN, actor, reason, null);
}
} else if (mucUser.getDestroy() != null) {
occupantEvent = new OccupantEvent(ChatRoom.this, occupant, OccupantEvent.Type.ROOM_DESTROYED, actor, mucUser.getDestroy().getReason(), mucUser.getDestroy().getJid());
} else {
occupantEvent = new OccupantEvent(ChatRoom.this, occupant, OccupantEvent.Type.EXITED, null, null, null);
}
} else {
occupantEvent = new OccupantEvent(ChatRoom.this, occupant, OccupantEvent.Type.EXITED, null, null, null);
}
if (occupantEvent != null) {
XmppUtils.notifyEventListeners(occupantListeners, occupantEvent);
}
if (isSelfPresence) {
userHasExited();
}
}
}
}
}
};
}
void initialize() {
xmppSession.addSessionStatusListener(e -> {
if (e.getStatus() == XmppSession.Status.CLOSED) {
invitationDeclineListeners.clear();
subjectChangeListeners.clear();
occupantListeners.clear();
inboundMessageListeners.clear();
occupantMap.clear();
}
});
}
private void userHasExited() {
xmppSession.removeInboundMessageListener(messageListener);
xmppSession.removeInboundPresenceListener(presenceListener);
synchronized (this) {
entered = false;
nick = null;
multiUserChatManager.roomExited(this);
occupantMap.clear();
}
}
private synchronized boolean isSelfPresence(Presence presence, final String currentNick) {
boolean isSelfPresence = false;
MucUser mucUser = presence.getExtension(MucUser.class);
if (mucUser != null) {
// If the presence is self-presence (110) or if the service assigned another nickname (210) to the user (but didn't include 110).
boolean nicknameChanged = mucUser.getStatusCodes().contains(Status.SERVICE_HAS_ASSIGNED_OR_MODIFIED_NICK);
if (nicknameChanged) {
nick = presence.getFrom().getResource();
}
isSelfPresence = mucUser.getStatusCodes().contains(Status.SELF_PRESENCE) || nicknameChanged;
}
String usedNick = nick != null ? nick : currentNick;
return isSelfPresence || usedNick != null && presence.getFrom() != null && usedNick.equals(presence.getFrom().getResource());
}
/**
* Adds a invitation decline listener, which allows to listen for invitation declines.
*
* @param invitationDeclineListener The listener.
* @see #removeInvitationDeclineListener(Consumer)
*/
public void addInvitationDeclineListener(Consumer invitationDeclineListener) {
invitationDeclineListeners.add(invitationDeclineListener);
}
/**
* Removes a previously added invitation decline listener.
*
* @param invitationDeclineListener The listener.
* @see #addInvitationDeclineListener(Consumer)
*/
public void removeInvitationDeclineListener(Consumer invitationDeclineListener) {
invitationDeclineListeners.remove(invitationDeclineListener);
}
/**
* Adds a subject change listener, which allows to listen for subject changes.
*
* @param subjectChangeListener The listener.
* @see #removeSubjectChangeListener(Consumer)
*/
public void addSubjectChangeListener(Consumer subjectChangeListener) {
subjectChangeListeners.add(subjectChangeListener);
}
/**
* Removes a previously added subject change listener.
*
* @param subjectChangeListener The listener.
* @see #addSubjectChangeListener(Consumer)
*/
public void removeSubjectChangeListener(Consumer subjectChangeListener) {
subjectChangeListeners.remove(subjectChangeListener);
}
/**
* Adds an occupant listener, which allows to listen for presence changes of occupants, e.g. "joins" and "leaves".
*
* @param occupantListener The listener.
* @see #removeOccupantListener(Consumer)
*/
public void addOccupantListener(Consumer occupantListener) {
occupantListeners.add(occupantListener);
}
/**
* Removes a previously added occupant listener.
*
* @param occupantListener The listener.
* @see #addOccupantListener(Consumer)
*/
public void removeOccupantListener(Consumer occupantListener) {
occupantListeners.remove(occupantListener);
}
/**
* Enters the room.
*
* @param nick The nickname.
* @return The async result with the self-presence returned by the chat room.
*/
public AsyncResult enter(String nick) {
return enter(nick, null, null);
}
/**
* Enters the room with a password.
*
* @param nick The nickname.
* @param password The password.
* @return The async result with the self-presence returned by the chat room.
*/
public AsyncResult enter(String nick, String password) {
return enter(nick, password, null);
}
/**
* Enters the room and requests history messages.
*
* @param nick The nickname.
* @param history The history.
* @return The async result with the self-presence returned by the chat room.
*/
public AsyncResult enter(String nick, DiscussionHistory history) {
return enter(nick, null, history);
}
/**
* Enters the room with a password and requests history messages.
*
* @param nick The nickname.
* @param password The password.
* @param history The history.
* @return The async result with the self-presence returned by the chat room.
*/
public synchronized AsyncResult enter(final String nick, String password, DiscussionHistory history) {
Objects.requireNonNull(nick, "nick must not be null.");
if (entered) {
throw new IllegalStateException("You already entered this room.");
}
xmppSession.addInboundMessageListener(messageListener);
xmppSession.addInboundPresenceListener(presenceListener);
final Presence enterPresence = new Presence(roomJid.withResource(nick));
enterPresence.getExtensions().add(Muc.withPasswordAndHistory(password, history));
this.nick = nick;
return xmppSession.sendAndAwaitPresence(enterPresence, presence -> {
Jid room = presence.getFrom().asBareJid();
return room.equals(roomJid) && isSelfPresence(presence, nick);
}).whenComplete((presence, e) -> {
if (e != null) {
xmppSession.removeInboundMessageListener(messageListener);
xmppSession.removeInboundPresenceListener(presenceListener);
} else {
multiUserChatManager.roomEntered(this, nick);
synchronized (this) {
entered = true;
}
}
});
}
/**
* Changes the room subject.
*
* @param subject The subject.
* @return The async result with the message returned by the chat room.
*/
public AsyncResult changeSubject(final String subject) {
Message message = new Message(roomJid, Message.Type.GROUPCHAT, null, subject, null);
return xmppSession.sendAndAwaitMessage(message, message1 -> message1.getSubject() != null && message1.getSubject().equals(subject));
}
/**
* Sends a message to the room.
*
* @param message The message text.
* @return The sent message.
*/
@Override
public SendTask sendMessage(String message) {
Message m = new Message(roomJid, Message.Type.GROUPCHAT, message);
return xmppSession.sendMessage(m);
}
/**
* Sends a message to the room.
*
* @param message The message.
* @return The sent message.
*/
@Override
public SendTask sendMessage(Message message) {
Message m = new Message(roomJid, Message.Type.GROUPCHAT, message.getBodies(), message.getSubjects(), message.getThread(), message.getParentThread(), message.getId(), message.getFrom(), message.getLanguage(), message.getExtensions(), message.getError());
return xmppSession.sendMessage(m);
}
/**
* Changes the nickname.
*
* @param newNickname The new nickname.
* @return The async result with the presence returned by the chat room.
* @see 7.6 Changing Nickname
*/
public synchronized AsyncResult changeNickname(String newNickname) {
if (!entered) {
throw new IllegalStateException("You must have entered the room to change your nickname.");
}
final Presence changeNickNamePresence = new Presence(roomJid.withResource(newNickname));
return xmppSession.sendAndAwaitPresence(changeNickNamePresence, presence -> presence.getFrom().equals(changeNickNamePresence.getTo()));
}
/**
* Changes the availability status.
*
* @param show The 'show' value.
* @param status The status.
* @see 7.7 Changing Availability Status
*/
public synchronized void changeAvailabilityStatus(Presence.Show show, String status) {
if (!entered) {
throw new IllegalStateException("You must have entered the room to change the availability status.");
}
xmppSession.send(new Presence(roomJid.withResource(nick), show, status));
}
/**
* Invites another user to the room. The invitation will be mediated by the room.
*
* @param invitee The invitee.
* @param reason The reason.
* @see 7.8 Inviting Another User to a Room
*/
public void invite(Jid invitee, String reason) {
invite(invitee, reason, false);
}
/**
* Invites another user to the room. The invitation will be either mediated by the room or direct.
*
* @param invitee The invitee.
* @param reason The reason.
* @param direct True, if the message is sent directly to the invitee; false if it is mediated by the room.
* @see 7.8.1 Direct Invitation
* @see 7.8.2 Mediated Invitation
*/
public void invite(Jid invitee, String reason, boolean direct) {
Message message;
if (direct) {
message = new Message(invitee, Message.Type.NORMAL);
message.addExtension(new DirectInvitation(roomJid, null, reason));
} else {
message = new Message(roomJid, Message.Type.NORMAL);
message.addExtension(MucUser.withInvites(new Invite(invitee, reason)));
}
xmppSession.send(message);
}
/**
* Gets the data form necessary to register with the room.
*
* @return The async result with the data form.
* @see 7.10 Registering with a Room
* @see RoomRegistration
*/
public AsyncResult getRegistrationForm() {
return xmppSession.query(IQ.get(roomJid, Registration.empty())).thenApply(result -> {
Registration registration = result.getExtension(Registration.class);
if (registration != null) {
return registration.getRegistrationForm();
}
return null;
});
}
/**
* Registers with the room.
*
* @param registration The registration.
* @return The async result.
* @see 7.10 Registering with a Room
* @see RoomRegistration
*/
public AsyncResult register(Registration registration) {
Objects.requireNonNull(registration, "registration must not be null.");
if (registration.getRegistrationForm() != null) {
if (registration.getRegistrationForm().getType() != DataForm.Type.SUBMIT) {
throw new IllegalArgumentException("Data Form must be of type 'submit'");
}
if (!"http://jabber.org/protocol/muc#register".equals(registration.getRegistrationForm().getFormType())) {
throw new IllegalArgumentException("Data Form is not of type 'http://jabber.org/protocol/muc#register'");
}
}
return xmppSession.query(IQ.set(roomJid, registration));
}
/**
* Gets your reserved room nickname.
*
* @return The async result with the reserved nickname or null, if you don't have a reserved nickname.
*/
public AsyncResult discoverReservedNickname() {
ServiceDiscoveryManager serviceDiscoveryManager = xmppSession.getManager(ServiceDiscoveryManager.class);
return serviceDiscoveryManager.discoverInformation(roomJid, "x-roomuser-item").thenApply(infoNode -> {
if (infoNode != null) {
for (Identity identity : infoNode.getIdentities()) {
if ("conference".equals(identity.getCategory()) && "text".equals(identity.getType())) {
return identity.getName();
}
}
}
return null;
});
}
/**
* Requests voice in a moderated room.
*
* @see 7.13 Requesting Voice
*/
public void requestVoice() {
Message message = new Message(roomJid);
RequestVoice requestVoice = RequestVoice.builder().role(Role.PARTICIPANT).build();
message.addExtension(requestVoice.getDataForm());
xmppSession.send(message);
}
/**
* Exits the room.
*
* @return The async result.
* @see 7.14 Exiting a Room
*/
public AsyncResult exit() {
return exit(null);
}
/**
* Exits the room with a custom message.
*
* @param message The exit message.
* @return The async result.
* @see 7.14 Exiting a Room
*/
public synchronized AsyncResult exit(String message) {
if (!entered) {
return new AsyncResult<>(CompletableFuture.completedFuture(null));
}
// Store the current nick, to determine self-presence (because nick gets null before determining self-presence).
final String usedNick = nick;
return xmppSession.sendAndAwaitPresence(new Presence(roomJid.withResource(nick), Presence.Type.UNAVAILABLE, message), presence -> {
Jid room = presence.getFrom().asBareJid();
return !presence.isAvailable() && room.equals(roomJid) && isSelfPresence(presence, usedNick);
}).handle((result, throwable) -> {
userHasExited();
return null;
});
}
/**
* Indicates, if you have entered the room. When you exit the room, this method returns false.
*
* @return If you entered the room.
* @see #enter(String)
* @see #exit()
*/
public final synchronized boolean hasEntered() {
return entered;
}
/**
* Gets the voice list.
*
* @return The async result with the voice list.
* @see 8.5 Modifying the Voice List
*/
public AsyncResult> getVoiceList() {
return xmppSession.query(IQ.get(roomJid, MucAdmin.withItem(Role.PARTICIPANT, null, null))).thenApply(result -> {
MucAdmin mucAdmin = result.getExtension(MucAdmin.class);
return mucAdmin.getItems();
});
}
/**
* Changes multiple affiliations or roles.
*
* @param items The items.
* @return The async result.
* @see 8.5 Modifying the Voice List
* @see 9.5 Modifying the Member List
* @see 9.8 Modifying the Moderator List
* @see 10.5 Modifying the Owner List
* @see 10.8 Modifying the Admin List
*/
public AsyncResult changeAffiliationsOrRoles(List items) {
return xmppSession.query(IQ.set(roomJid, MucAdmin.withItems(items)));
}
/**
* Gets the ban list.
*
* @return The async result with the ban list.
* @see 9.2 Modifying the Ban List
*/
public AsyncResult> getBanList() {
return xmppSession.query(IQ.get(roomJid, MucAdmin.withItem(Affiliation.OUTCAST, null, null))).thenApply(result -> {
MucAdmin mucAdmin = result.getExtension(MucAdmin.class);
return mucAdmin.getItems();
});
}
/**
* Changes the affiliation for an user.
*
* Use this method for one of the following use cases:
*
*
* - Banning a User (affiliation = {@link Affiliation#OUTCAST})
* - Granting Membership (affiliation = {@link Affiliation#MEMBER})
* - Revoking Membership (affiliation = {@link Affiliation#NONE})
* - Granting Admin Status (affiliation = {@link Affiliation#ADMIN})
* - Revoking Admin Status (affiliation = {@link Affiliation#MEMBER})
* - Granting Owner Status (affiliation = {@link Affiliation#OWNER})
* - Revoking Owner Status (affiliation = {@link Affiliation#ADMIN})
*
*
* @param affiliation The new affiliation for the user.
* @param user The user.
* @param reason The reason.
* @return The async result.
* @see 9.1 Banning a User
* @see 9.3 Granting Membership
* @see 9.4 Revoking Membership
* @see 10.3 Granting Owner Status
* @see 10.4 Revoking Owner Status
* @see 10.6 Granting Admin Status
* @see 10.7 Revoking Admin Status
*/
public final AsyncResult changeAffiliation(Affiliation affiliation, Jid user, String reason) {
return xmppSession.query(IQ.set(roomJid, MucAdmin.withItem(affiliation, user, null, reason)));
}
private AsyncResult changeAffiliation(Affiliation affiliation, Jid user, String nick, String reason) {
return xmppSession.query(IQ.set(roomJid, MucAdmin.withItem(affiliation, user, nick, reason)));
}
/**
* Bans a user. Note that you must be an owner or admin of the room.
*
* @param user The user.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 9.1 Banning a User
*/
public final AsyncResult banUser(Jid user, String reason) {
return changeAffiliation(Affiliation.OUTCAST, user, reason);
}
/**
* Grants membership to a user. Note that you must be an owner or admin of the room.
*
* @param user The user.
* @param nick The nick (optional). That nick becomes the user's default nick in the room if that functionality is supported by the implementation. May be null
.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 9.3 Granting Membership
* @see #revokeMembership(Jid, String)
*/
public final AsyncResult grantMembership(Jid user, String nick, String reason) {
return changeAffiliation(Affiliation.MEMBER, user, nick, reason);
}
/**
* Revokes a user's membership. Note that you must be an owner or admin of the room.
*
* @param user The user.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 9.4 Revoking Membership
* @see #grantMembership(Jid, String, String)
*/
public final AsyncResult revokeMembership(Jid user, String reason) {
return changeAffiliation(Affiliation.NONE, user, reason);
}
/**
* Grants owner status to a user. Note that you must be an owner of the room.
*
* @param user The user.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 10.3 Granting Owner Status
* @see #revokeOwnerStatus(Jid, String)
*/
public final AsyncResult grantOwnerStatus(Jid user, String reason) {
return changeAffiliation(Affiliation.OWNER, user, reason);
}
/**
* Revokes a user's owner status. The new status of the user will be 'admin'.
* Note that you must be an owner of the room.
* This method does basically the same as {@link #grantAdminStatus(Jid, String)}}.
*
* @param user The user.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 10.4 Revoking Owner Status
* @see #grantOwnerStatus(Jid, String)
*/
public final AsyncResult revokeOwnerStatus(Jid user, String reason) {
return changeAffiliation(Affiliation.ADMIN, user, reason);
}
/**
* Grants admin status to a user. Note that you must be an owner of the room.
* This method does basically the same as {@link #revokeOwnerStatus(Jid, String)}.
*
* @param user The user.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 10.6 Granting Admin Status
* @see #revokeAdminStatus(Jid, String)
*/
public final AsyncResult grantAdminStatus(Jid user, String reason) {
return changeAffiliation(Affiliation.ADMIN, user, reason);
}
/**
* Revokes a user's admin status. The new status of the user will be 'member'.
* Note that you must be an owner of the room.
*
* @param user The user.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 10.7 Revoking Admin Status
* @see #grantAdminStatus(Jid, String)
*/
public final AsyncResult revokeAdminStatus(Jid user, String reason) {
return changeAffiliation(Affiliation.MEMBER, user, reason);
}
/**
* Changes the role for an occupant.
*
* Use this method for one of the following use cases:
*
*
* - Kicking an Occupant (role = {@link Role#NONE})
* - Granting Voice to a Visitor (role = {@link Role#PARTICIPANT})
* - Revoking Voice from a Participant (affiliation = {@link Role#VISITOR})
* - Granting Moderator Status (role = {@link Role#MODERATOR})
* - Revoking Moderator Status (role = {@link Role#PARTICIPANT})
*
*
* @param role The new role for the user.
* @param nickname The occupant's nickname.
* @param reason The reason.
* @return The async result.
* @see 8.2 Kicking an Occupant
* @see 8.3 Granting Voice to a Visitor
* @see 8.4 Revoking Voice from a Participant
* @see 9.6 Granting Moderator Status
* @see 9.7 Revoking Moderator Status
*/
public final AsyncResult changeRole(Role role, String nickname, String reason) {
return xmppSession.query(IQ.set(roomJid, MucAdmin.withItem(role, nickname, reason)));
}
/**
* Kicks an occupant from the room. Note that you must be a moderator in the room.
*
* @param nickname The nickname.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 8.2 Kicking an Occupant
*/
public final AsyncResult kickOccupant(String nickname, String reason) {
return changeRole(Role.NONE, nickname, reason);
}
/**
* Grants voice to a visitor. Note that you must be a moderator in the room.
* This method does basically the same as {@link #revokeModeratorStatus(String, String)}}}.
*
* @param nickname The nickname.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 8.3 Granting Voice to a Visitor
* @see #revokeVoice(String, String)
*/
public final AsyncResult grantVoice(String nickname, String reason) {
return changeRole(Role.PARTICIPANT, nickname, reason);
}
/**
* Revokes voice from a participant. Note that you must be a moderator in the room.
*
* @param nickname The nickname.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 8.4 Revoking Voice from a Participant
* @see #grantVoice(String, String)
*/
public final AsyncResult revokeVoice(String nickname, String reason) {
return changeRole(Role.VISITOR, nickname, reason);
}
/**
* Grants moderator status to a participant or visitor. Note that you must be an admin in the room.
*
* @param nickname The nickname.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 9.6 Granting Moderator Status
*/
public final AsyncResult grantModeratorStatus(String nickname, String reason) {
return changeRole(Role.MODERATOR, nickname, reason);
}
/**
* Revokes moderator status from a participant or visitor. Note that you must be an admin in the room.
* This method does basically the same as {@link #grantVoice(String, String)}.
*
* @param nickname The nickname.
* @param reason The reason (optional). May be null
.
* @return The async result.
* @see 9.7 Revoking Moderator Status
*/
public final AsyncResult revokeModeratorStatus(String nickname, String reason) {
return changeRole(Role.PARTICIPANT, nickname, reason);
}
/**
* Gets the owners of the room.
*
* @return The async result with the owners.
* @see 9.5 Modifying the Member List
*/
public AsyncResult> getOwners() {
return getByAffiliation(Affiliation.OWNER);
}
/**
* Gets the outcasts of the room.
*
* @return The async result with the outcasts.
* @see 9.5 Modifying the Member List
*/
public AsyncResult> getOutcasts() {
return getByAffiliation(Affiliation.OUTCAST);
}
/**
* Gets the admins of the room.
*
* @return The async result with the admins.
* @see 9.5 Modifying the Member List
*/
public AsyncResult> getAdmins() {
return getByAffiliation(Affiliation.ADMIN);
}
/**
* Gets the members of the room.
*
* In the context of a members-only room, the member list is essentially a "whitelist" of people who are allowed to enter the room.
*
*
* In the context of an open room, the member list is simply a list of users (bare JID and reserved nick) who are registered with the room.
*
*
* @return The async result with the members.
* @see 9.5 Modifying the Member List
*/
public AsyncResult> getMembers() {
return getByAffiliation(Affiliation.MEMBER);
}
private AsyncResult> getByAffiliation(Affiliation affiliation) {
return xmppSession.query(IQ.get(roomJid, MucAdmin.withItem(affiliation, null, null))).thenApply(result -> {
MucAdmin mucAdmin = result.getExtension(MucAdmin.class);
return mucAdmin.getItems();
});
}
/**
* Gets the moderators.
*
* @return The async result with the moderators.
* @see 9.8 Modifying the Moderator List
*/
public AsyncResult> getModerators() {
return xmppSession.query(IQ.get(roomJid, MucAdmin.withItem(Role.MODERATOR, null, null))).thenApply(result -> {
MucAdmin mucAdmin = result.getExtension(MucAdmin.class);
return mucAdmin.getItems();
});
}
/**
* Creates an instant room.
*
* @return The async result.
* @see 10.1.2 Creating an Instant Room
* @deprecated This method is flawed. Simply enter the room.
*/
@Deprecated
public synchronized AsyncResult createRoom() {
return enter(nick).thenCompose(presence ->
xmppSession.query(IQ.set(roomJid, MucOwner.withConfiguration(new DataForm(DataForm.Type.SUBMIT)))));
}
/**
* Gets the room information for this chat room.
*
* @return The async result with the room info.
* @see 6.4 Querying for Room Information
*/
public AsyncResult getRoomInformation() {
return serviceDiscoveryManager.discoverInformation(roomJid).thenApply(infoNode -> {
Identity identity = null;
Set mucFeatures = new HashSet<>();
RoomInfo roomInfo = null;
if (infoNode != null) {
Set identities = infoNode.getIdentities();
Iterator iterator = identities.iterator();
if (iterator.hasNext()) {
identity = iterator.next();
}
for (String feature : infoNode.getFeatures()) {
for (MucFeature mucFeature : MucFeature.values()) {
if (mucFeature.getServiceDiscoveryFeature().equals(feature)) {
mucFeatures.add(mucFeature);
}
}
}
for (DataForm dataForm : infoNode.getExtensions()) {
String formType = dataForm.getFormType();
if (RoomInfo.FORM_TYPE.equals(formType)) {
roomInfo = new RoomInfo(dataForm);
break;
}
}
}
return new RoomInformation(identity, mucFeatures, roomInfo);
});
}
/**
* Gets the occupants in this room, i.e. their nicknames. This method should be used, when you are not yet in the room.
*
* @return The async result with the occupants.
* @see 6.5 Querying for Room Items
* @see #getOccupants()
*/
public AsyncResult> discoverOccupants() {
return serviceDiscoveryManager.discoverItems(roomJid).thenApply(itemNode -> {
List occupants = new ArrayList<>();
List items = itemNode.getItems();
items.stream().filter(item -> item.getJid() != null).forEach(item -> {
String nickname = item.getJid().getResource();
if (nickname != null) {
occupants.add(nickname);
}
});
return occupants;
});
}
/**
* Gets the occupants, while being in the room.
*
* @return The occupants.
*/
public Collection getOccupants() {
return occupantMap.values();
}
/**
* Gets an occupant by nickname.
*
* @param nickname The occupant's nickname.
* @return The occupant.
*/
public Occupant getOccupant(String nickname) {
return occupantMap.get(nickname);
}
/**
* Gets the configuration form for the room.
* You can wrap the form into {@link RoomConfiguration} for easier processing.
*
* Use this method if you want to create a reserved room or configure an existing room.
*
*
* @return The async result with the configuration form.
* @see RoomConfiguration
* @see 10.1.3 Creating a Reserved Room
* @see #configure(RoomConfiguration)
*/
public AsyncResult getConfigurationForm() {
return xmppSession.query(IQ.get(roomJid, MucOwner.empty())).thenApply(result -> {
MucOwner mucOwner = result.getExtension(MucOwner.class);
return mucOwner.getConfigurationForm();
});
}
/**
* Configures this room.
*
* @param roomConfiguration The async result with the room configuration form.
* @return The async result.
* @see 10.1.3 Creating a Reserved Room
* @see #getConfigurationForm()
*/
public AsyncResult configure(RoomConfiguration roomConfiguration) {
Objects.requireNonNull(roomConfiguration, "roomConfiguration must not be null.");
MucOwner mucOwner = MucOwner.withConfiguration((roomConfiguration.getDataForm()));
return xmppSession.query(IQ.set(roomJid, mucOwner));
}
/**
* Gets the name for this room.
*
* @return The room name.
*/
public final String getName() {
return name;
}
/**
* Gets the nickname in this room. Usually this is the nick used to enter the room, but can also be a nickname assigned by the chat service.
*
* @return The nickname in this room or {@code null}, if not entered.
*/
public final synchronized String getNick() {
return nick;
}
/**
* Destroys the room.
*
* @param reason The reason for the room destruction.
* @return The async result.
* @see 10.9 Destroying a Room
*/
public AsyncResult destroy(String reason) {
MucOwner mucOwner = MucOwner.withDestroy(roomJid, reason);
return xmppSession.query(IQ.set(roomJid, mucOwner));
}
/**
* Destroys the room.
*
* @return The async result.
* @see 10.9 Destroying a Room
*/
public final AsyncResult destroy() {
return destroy(null);
}
/**
* Gets the room address.
*
* @return The room address.
*/
public Jid getAddress() {
return roomJid;
}
/**
* Discovers the allowable traffic, i.e. the allowed extensions.
*
* @return The async result with the list of allowable features.
* @see 17.1.1 Allowable Traffic
*/
public AsyncResult> discoverAllowableTraffic() {
return serviceDiscoveryManager.discoverInformation(roomJid, "http://jabber.org/protocol/muc#traffic").thenApply(InfoNode::getFeatures);
}
@Override
public String toString() {
return roomJid.toString();
}
/**
* Compares this chat service first by their name and then by their service address.
*
* @param o The other chat service.
* @return The comparison result.
*/
@Override
public int compareTo(ChatRoom o) {
if (this == o) {
return 0;
}
if (o != null) {
int result;
// First compare name.
if (name != null) {
result = o.name != null ? name.compareTo(o.name) : -1;
} else {
result = o.name != null ? 1 : 0;
}
// If the names are equal, compare addresses.
if (result == 0) {
return roomJid.compareTo(o.roomJid);
}
return result;
}
return -1;
}
}