From 19340ddc7dc929aad094eed3f6a3cd7f84d86a4b Mon Sep 17 00:00:00 2001 From: Gurmeen Bindra Date: Mon, 23 Apr 2012 16:06:39 +0100 Subject: MUC Administration related classes This change ports the MUC Administration related classes from Swiften to stroke. Also includes the MUC initialisation code in the CoreClient. Test-information: tested the ported unit tests diff --git a/src/com/isode/stroke/client/Client.java b/src/com/isode/stroke/client/Client.java new file mode 100644 index 0000000..69c893f --- /dev/null +++ b/src/com/isode/stroke/client/Client.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tronçon. + * All rights reserved. + */ +package com.isode.stroke.client; + +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.jid.JID; +import com.isode.stroke.muc.MUCManager; +import com.isode.stroke.muc.MUCRegistry; +import com.isode.stroke.network.NetworkFactories; +import com.isode.stroke.presence.DirectedPresenceSender; +import com.isode.stroke.presence.StanzaChannelPresenceSender; + +/** + * Provides the core functionality for writing XMPP client software. + * + * Besides connecting to an XMPP server, this class also provides interfaces for + * performing most tasks on the XMPP network. + */ + +public class Client extends CoreClient { + + private MUCManager mucManager; + private MUCRegistry mucRegistry; + private DirectedPresenceSender directedPresenceSender; + private StanzaChannelPresenceSender stanzaChannelPresenceSender; + + /** + * Constructor. + * + * @param eventLoop Event loop used by the class, must not be null. The + * Client creates threads to do certain tasks. However, it + * posts events that it expects to be done in the application's + * main thread to this eventLoop. The application should + * use an appropriate EventLoop implementation for the application type. This + * EventLoop is just a way for the Client to pass these + * events back to the main thread, and should not be used by the + * application for its own purposes. + * @param jid User JID used to connect to the server, must not be null + * @param password User password to use, must not be null + * @param networkFactories An implementation of network interaction, must + * not be null. + */ + public Client(EventLoop eventLoop, JID jid, String password, NetworkFactories networkFactories) { + super(eventLoop,jid, password, networkFactories); + stanzaChannelPresenceSender = new StanzaChannelPresenceSender(getStanzaChannel()); + directedPresenceSender = new DirectedPresenceSender(stanzaChannelPresenceSender); + + mucRegistry = new MUCRegistry(); + mucManager = new MUCManager(getStanzaChannel(), getIQRouter(), directedPresenceSender, mucRegistry); + } + + /** + * Get the manager for multi user chat rooms + * @return MUC manager, not null + */ + public MUCManager getMUCManager() { + return mucManager; + } + + /** + * Get the registry for multi user chat rooms + * @return MUC registry, not null + */ + public MUCRegistry getMUCRegistry() { + return mucRegistry; + } +} diff --git a/src/com/isode/stroke/client/DummyStanzaChannel.java b/src/com/isode/stroke/client/DummyStanzaChannel.java new file mode 100644 index 0000000..25fb817 --- /dev/null +++ b/src/com/isode/stroke/client/DummyStanzaChannel.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ +package com.isode.stroke.client; + +import java.util.Vector; + +import com.isode.stroke.elements.IQ; +import com.isode.stroke.elements.Message; +import com.isode.stroke.elements.Payload; +import com.isode.stroke.elements.Presence; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.jid.JID; + +/** + * Dummy Stanza Channel for Unit Testing + * + */ +public class DummyStanzaChannel extends StanzaChannel { + + public Vector sentStanzas = new Vector(); + public boolean available_; + + public DummyStanzaChannel() { + available_ = true; + } + + public void sendStanza(Stanza stanza) { + sentStanzas.add(stanza); + } + + public void setAvailable(boolean available) { + available_ = available; + onAvailableChanged.emit(available); + } + + public void sendIQ(IQ iq) { + sentStanzas.add(iq); + } + + public void sendMessage(Message message) { + sentStanzas.add(message); + } + + public void sendPresence(Presence presence) { + sentStanzas.add(presence); + } + + public String getNewIQID() { + return "test-id"; + } + + public boolean isAvailable() { + return available_; + } + + public boolean getStreamManagementEnabled() { + return false; + } + + public boolean isRequestAtIndex(int index, JID jid, IQ.Type type, T plType) { + if (index >= sentStanzas.size()) { + return false; + } + Stanza stanza = (sentStanzas.get(index)); + IQ iqStanza = null; + if(stanza instanceof IQ) { + iqStanza = (IQ)(sentStanzas.get(index)); + } + return iqStanza != null && iqStanza.getType() == type && iqStanza.getTo().equals(jid) + && iqStanza.getPayload(plType) != null; + } + + public boolean isResultAtIndex(int index, String id) { + if (index >= sentStanzas.size()) { + return false; + } + Stanza stanza = (sentStanzas.get(index)); + IQ iqStanza = null; + if(stanza instanceof IQ) { + iqStanza = (IQ)(sentStanzas.get(index)); + } + return iqStanza != null && iqStanza.getType() == IQ.Type.Result && iqStanza.getID().equals(id) ; +} + + + public boolean isErrorAtIndex(int index, String id) { + if (index >= sentStanzas.size()) { + return false; + } + Stanza stanza = (sentStanzas.get(index)); + IQ iqStanza = null; + if(stanza instanceof IQ) { + iqStanza = (IQ)(sentStanzas.get(index)); + } + return iqStanza != null && iqStanza.getType() == IQ.Type.Error && iqStanza.getID().equals(id); + } + + public T getStanzaAtIndex(T object,int index) { + if (sentStanzas.size() <= index) { + return null; + } + Stanza stanza = sentStanzas.get(index); + T obj = null; + if(object.getClass().isAssignableFrom(stanza.getClass())) { + return (T)(sentStanzas.get(index)); + } + return null; + } +} diff --git a/src/com/isode/stroke/muc/MUC.java b/src/com/isode/stroke/muc/MUC.java new file mode 100644 index 0000000..8df1b89 --- /dev/null +++ b/src/com/isode/stroke/muc/MUC.java @@ -0,0 +1,631 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Kevin Smith + * All rights reserved. + */ +package com.isode.stroke.muc; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Vector; + +import com.isode.stroke.client.StanzaChannel; +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.elements.Form; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.elements.MUCAdminPayload; +import com.isode.stroke.elements.MUCDestroyPayload; +import com.isode.stroke.elements.MUCInvitationPayload; +import com.isode.stroke.elements.MUCItem; +import com.isode.stroke.elements.MUCOccupant; +import com.isode.stroke.elements.MUCOwnerPayload; +import com.isode.stroke.elements.MUCPayload; +import com.isode.stroke.elements.MUCUserPayload; +import com.isode.stroke.elements.Message; +import com.isode.stroke.elements.Presence; +import com.isode.stroke.jid.JID; +import com.isode.stroke.jid.JID.CompareType; +import com.isode.stroke.presence.DirectedPresenceSender; +import com.isode.stroke.queries.GenericRequest; +import com.isode.stroke.queries.IQRouter; +import com.isode.stroke.signals.Signal1; +import com.isode.stroke.signals.Signal2; +import com.isode.stroke.signals.Signal3; +import com.isode.stroke.signals.SignalConnection; +import com.isode.stroke.signals.Slot1; +import com.isode.stroke.signals.Slot2; + +/** + * Class representing multi user chat room + * + */ +public class MUC { + + public enum JoinResult { JoinFailed, JoinSucceeded }; + public enum LeavingType { Disconnect, LeaveBan, LeaveDestroy, LeaveKick, LeaveNotMember, LeavePart }; + + public Signal3 onAffiliationChangeFailed = + new Signal3(); + public Signal1 onAffiliationListFailed = new Signal1(); + public Signal2> onAffiliationListReceived = + new Signal2>(); + public Signal1 onConfigurationFailed = new Signal1(); + public Signal1
onConfigurationFormReceived = new Signal1(); + public Signal1 onJoinComplete = new Signal1(); + public Signal1 onJoinFailed = new Signal1(); + public Signal3 onOccupantAffiliationChanged = + new Signal3(); + public Signal1 onOccupantJoined = new Signal1(); + + public Signal3 onOccupantLeft = + new Signal3(); + public Signal1 onOccupantPresenceChange = new Signal1(); + public Signal3 onOccupantRoleChanged = + new Signal3(); + + public Signal3 onRoleChangeFailed = + new Signal3(); + + private boolean createAsReservedIfNew; + private IQRouter iqRouter_; + private boolean joinComplete_; + private Date joinSince_; + private boolean joinSucceeded_; + private MUCRegistry mucRegistry; + private Map occupants = new HashMap(); + private JID ownMUCJID; + private String password; + private DirectedPresenceSender presenceSender; + private StanzaChannel stanzaChannel; + private boolean unlocking; + private SignalConnection signalPresRcvd; + + /** + * Create a MUC Session + * @param stanzaChannel stanza channel, not null + * @param iqRouter IQ stanza router, not null + * @param presenceSender Presence Sender, not null + * @param muc JID of the chat room, not null + * @param mucRegistry MUC registry, not null + * + * @see #disconnect() + */ + public MUC(StanzaChannel stanzaChannel, IQRouter iqRouter, + DirectedPresenceSender presenceSender, final JID muc, + MUCRegistry mucRegistry) { + ownMUCJID = muc; + this.stanzaChannel = stanzaChannel; + this.iqRouter_ = iqRouter; + this.presenceSender = presenceSender; + this.mucRegistry = mucRegistry; + this.createAsReservedIfNew = false; + this.unlocking = false; + signalPresRcvd = this.stanzaChannel.onPresenceReceived.connect( + new Slot1() { + @Override + public void call(Presence p1) { + handleIncomingPresence(p1); + } + }); + } + + /** + * Cancel the command for configuring room + */ + public void cancelConfigureRoom() { + MUCOwnerPayload mucPayload = new MUCOwnerPayload(); + mucPayload.setPayload(new Form(Form.Type.CANCEL_TYPE)); + GenericRequest request = new GenericRequest( + IQ.Type.Set, getJID(), mucPayload, iqRouter_); + request.send(); + } + + /** + * Change the affiliation of the given Jabber ID. + * It must be called with the real JID, not the room JID. + * @param jid real jabber ID, not null + * @param affiliation new affiliation, not null + */ + public void changeAffiliation(final JID jid, final MUCOccupant.Affiliation affiliation) { + final MUCAdminPayload mucPayload = new MUCAdminPayload(); + MUCItem item = new MUCItem(); + item.affiliation = affiliation; + item.realJID = jid.toBare(); + mucPayload.addItem(item); + GenericRequest request = new GenericRequest( + IQ.Type.Set, getJID(), mucPayload, iqRouter_); + request.onResponse.connect(new Slot2() { + @Override + public void call(MUCAdminPayload p1, ErrorPayload p2) { + handleAffiliationChangeResponse(mucPayload,p2,jid,affiliation); + } + }); + request.send(); + } + + /** + * Change the role of the specified occupant. It must be + * called with the room JID, not the real JID. + * @param jid Jabber ID of the occupant in the chat room, not null + * @param role new role, not null + */ + public void changeOccupantRole(final JID jid, final MUCOccupant.Role role) { + final MUCAdminPayload mucPayload = new MUCAdminPayload(); + MUCItem item = new MUCItem(); + item.role = role; + item.nick = jid.getResource(); + mucPayload.addItem(item); + GenericRequest request = new GenericRequest( + IQ.Type.Set, getJID(), mucPayload, iqRouter_); + request.onResponse.connect(new Slot2() { + @Override + public void call(MUCAdminPayload p1, ErrorPayload p2) { + handleOccupantRoleChangeResponse(mucPayload,p2,jid,role); + } + }); + request.send(); + } + + /** + * Change the subject of the chat room + * @param subject new subject, not null + */ + public void changeSubject(String subject) { + Message message = new Message(); + message.setSubject(subject); + message.setType(Message.Type.Groupchat); + message.setTo(ownMUCJID.toBare()); + stanzaChannel.sendMessage(message); + } + + /** + * Configure a chat room room + * @param form form to be used for configuration, not null + */ + public void configureRoom(Form form) { + MUCOwnerPayload mucPayload = new MUCOwnerPayload(); + mucPayload.setPayload(form); + GenericRequest request = new GenericRequest( + IQ.Type.Set, getJID(), mucPayload, iqRouter_); + if (unlocking) { + request.onResponse.connect(new Slot2() { + @Override + public void call(MUCOwnerPayload p1, ErrorPayload p2) { + handleCreationConfigResponse(p1, p2); + } + }); + }else { + request.onResponse.connect(new Slot2() { + @Override + public void call(MUCOwnerPayload p1, ErrorPayload p2) { + handleConfigurationResultReceived(p1, p2); + } + }); + } + request.send(); + } + + /** + * Destroy the chat room + */ + public void destroyRoom() { + MUCOwnerPayload mucPayload = new MUCOwnerPayload(); + MUCDestroyPayload mucDestroyPayload = new MUCDestroyPayload(); + mucPayload.setPayload(mucDestroyPayload); + GenericRequest request = new GenericRequest( + IQ.Type.Set, getJID(), mucPayload, iqRouter_); + request.onResponse.connect(new Slot2() { + @Override + public void call(MUCOwnerPayload p1, ErrorPayload p2) { + handleConfigurationResultReceived(p1,p2); + } + }); + request.send(); + } + + /** + * Returns the (bare) JID of the MUC. + * @return bare JID(i.e. without resource) + */ + public JID getJID() { + return ownMUCJID.toBare(); + } + + /** + * Get the MUC occupant with the given nick name + * @param nick nick name, not null + * @return MUC occupant if it exists or null if not + */ + public MUCOccupant getOccupant(String nick) { + return occupants.get(nick); + } + + /** + * Determine if the room contains occupant with given nick + * @param nick given nick + * @return true if the occupant exists, false otherwise + */ + public boolean hasOccupant(String nick) { + return occupants.containsKey(nick); + } + + /** + * Invite the person with give JID to the chat room + * @param person jabber ID o the person to invite,not nul + */ + public void invitePerson(JID person) { + invitePerson(person,""); + } + + /** + * Send an invite for the person to join the MUC + * @param person jabber ID of the person to invite, not null + * @param reason join reason, not null + */ + public void invitePerson(JID person, String reason) { + Message message = new Message(); + message.setTo(person); + message.setType(Message.Type.Normal); + MUCInvitationPayload invite = new MUCInvitationPayload(); + invite.setReason(reason); + invite.setJID(ownMUCJID.toBare()); + message.addPayload(invite); + stanzaChannel.sendMessage(message); + } + + /** + * Join the MUC with default context. + * @param nick nick name of the user, not null + */ + public void joinAs(String nick) { + joinSince_ = null; + internalJoin(nick); + } + + /** + * Join the MUC with context since date. + * @param nick nick name, not null + * @param since date since the nick joined, not null + */ + public void joinWithContextSince(String nick, Date since) { + joinSince_ = since; + internalJoin(nick); + } + + /** + * Kick the given occupant out of the chat room + * @param jid jabber ID of the user to kick, not null + */ + public void kickOccupant(JID jid) { + changeOccupantRole(jid, MUCOccupant.Role.NoRole); + } + + /** + * Leave the chat room + */ + public void part() { + presenceSender.removeDirectedPresenceReceiver(ownMUCJID, + DirectedPresenceSender.SendPresence.AndSendPresence); + mucRegistry.removeMUC(getJID()); + } + + /** + * Send a request to get a list of users for the given affiliation + * @param affiliation affiliation, not null + */ + public void requestAffiliationList(final MUCOccupant.Affiliation affiliation) { + MUCAdminPayload mucPayload = new MUCAdminPayload(); + MUCItem item = new MUCItem(); + item.affiliation = affiliation; + mucPayload.addItem(item); + GenericRequest request = new GenericRequest( + IQ.Type.Get, getJID(), mucPayload, iqRouter_); + request.onResponse.connect(new Slot2() { + @Override + public void call(MUCAdminPayload p1, ErrorPayload p2) { + handleAffiliationListResponse(p1,p2,affiliation); + } + }); + request.send(); + } + + /** + * Send a request for getting form for configuring a room + */ + public void requestConfigurationForm() { + MUCOwnerPayload mucPayload = new MUCOwnerPayload(); + GenericRequest request = new GenericRequest( + IQ.Type.Get, getJID(), mucPayload, iqRouter_); + request.onResponse.connect(new Slot2() { + @Override + public void call(MUCOwnerPayload p1, ErrorPayload p2) { + handleConfigurationFormReceived(p1,p2); + } + }); + request.send(); + } + + /** + * Set the reserved status of room to true. + * By default a new room with the default configuration is created. + * The effect of calling this function is to leave the room in reserved state + * but not configured so that it can be configured later. + */ + public void setCreateAsReservedIfNew() { + createAsReservedIfNew = true; + } + + /** + * Set the password used for entering the room. + * @param newPassword password, can be null + */ + public void setPassword(String newPassword) { + password = newPassword; + } + + /** + * Get the nick name of the MUC room + * @return nick name, can be null + */ + private String getOwnNick() { + return ownMUCJID.getResource(); + } + + private void handleAffiliationChangeResponse(MUCAdminPayload ref, + ErrorPayload error, JID jid, MUCOccupant.Affiliation affiliation) { + if (error != null) { + onAffiliationChangeFailed.emit(error, jid, affiliation); + } + } + + private void handleAffiliationListResponse(MUCAdminPayload payload, + ErrorPayload error, MUCOccupant.Affiliation affiliation) { + if (error != null) { + onAffiliationListFailed.emit(error); + } else { + Vector jids = new Vector(); + for (MUCItem item : payload.getItems()) { + if (item.realJID != null) { + jids.add(item.realJID); + } + } + onAffiliationListReceived.emit(affiliation, jids); + } + } + + private void handleConfigurationFormReceived(MUCOwnerPayload payload, + ErrorPayload error) { + Form form = null; + if (payload != null) { + form = payload.getForm(); + } + if (error != null || form == null) { + onConfigurationFailed.emit(error); + } else { + onConfigurationFormReceived.emit(form); + } + } + + private void handleConfigurationResultReceived( + MUCOwnerPayload payload, ErrorPayload error) { + if (error != null) { + onConfigurationFailed.emit(error); + } + } + + private void handleCreationConfigResponse(MUCOwnerPayload ref , ErrorPayload error) { + unlocking = false; + if (error != null) { + presenceSender.removeDirectedPresenceReceiver(ownMUCJID, + DirectedPresenceSender.SendPresence.AndSendPresence); + onJoinFailed.emit(error); + } else { + onJoinComplete.emit(getOwnNick()); /* Previously, this wasn't needed here, + as the presence duplication bug caused an emit elsewhere. */ + } + } + + private void handleIncomingPresence(Presence presence) { + if (!isFromMUC(presence.getFrom())) { + return; + } + + MUCUserPayload mucPayload = null; + + MUCUserPayload dummyUserPayload = new MUCUserPayload(); + for (MUCUserPayload payload : presence.getPayloads(dummyUserPayload)) { + if (!payload.getItems().isEmpty() || !payload.getStatusCodes().isEmpty()) { + mucPayload = payload; + } + } + + // On the first incoming presence, check if our join has succeeded + // (i.e. we start getting non-error presence from the MUC) or not + if (!joinSucceeded_) { + if(presence.getType() == Presence.Type.Error) { + onJoinFailed.emit(presence.getPayload(new ErrorPayload())); + return; + } else { + joinSucceeded_ = true; + presenceSender.addDirectedPresenceReceiver(ownMUCJID, + DirectedPresenceSender.SendPresence.AndSendPresence); + } + } + + String nick = presence.getFrom().getResource(); + if (nick == null || nick.isEmpty()) { + return; + } + MUCOccupant.Role role = MUCOccupant.Role.NoRole; + MUCOccupant.Affiliation affiliation= MUCOccupant.Affiliation.NoAffiliation; + JID realJID = null; + if (mucPayload != null && mucPayload.getItems().size() > 0) { + role = mucPayload.getItems().get(0).role != null + ? mucPayload.getItems().get(0).role : MUCOccupant.Role.NoRole; + affiliation = mucPayload.getItems().get(0).affiliation != null + ? mucPayload.getItems().get(0).affiliation : MUCOccupant.Affiliation.NoAffiliation; + realJID = mucPayload.getItems().get(0).realJID; + } + + //100 is non-anonymous + //TODO: 100 may also be specified in a + //170 is room logging to http + //TODO: Nick changes + + if (presence.getType() == Presence.Type.Unavailable) { + LeavingType type = LeavingType.LeavePart; + if (mucPayload != null) { + if (mucPayload.getPayload() instanceof MUCDestroyPayload) { + type = LeavingType.LeaveDestroy; + } else for (MUCUserPayload.StatusCode status : mucPayload.getStatusCodes()) { + if (status.code == 307) { + type = LeavingType.LeaveKick; + } else if (status.code == 301) { + type = LeavingType.LeaveBan; + } else if (status.code == 321) { + type = LeavingType.LeaveNotMember; + } + } + } + + if (presence.getFrom().equals(ownMUCJID)) { + handleUserLeft(type); + return; + }else { + if (occupants.containsKey(nick)) { + //TODO: part type + onOccupantLeft.emit(occupants.get(nick), type, ""); + occupants.remove(nick); + } + } + } else if (presence.getType() == Presence.Type.Available) { + MUCOccupant occupant = new MUCOccupant(nick, role, affiliation); + boolean isJoin = true; + if (realJID != null) { + occupant.setRealJID(realJID); + } + if (occupants.containsKey(nick)) { + isJoin = false; + MUCOccupant oldOccupant = occupants.get(nick); + if (oldOccupant.getRole() != role) { + onOccupantRoleChanged.emit(nick, occupant, oldOccupant.getRole()); + } + if (oldOccupant.getAffiliation() != affiliation) { + onOccupantAffiliationChanged.emit(nick, affiliation, oldOccupant.getAffiliation()); + } + occupants.remove(nick); + } + occupants.put(nick, occupant); + + if (isJoin) { + onOccupantJoined.emit(occupant); + } + onOccupantPresenceChange.emit(presence); + } + + if (mucPayload != null && !joinComplete_) { + for (MUCUserPayload.StatusCode status : mucPayload.getStatusCodes()) { + if(status.code == 110) { + /* Simply knowing this is your presence is enough, 210 doesn't seem to be necessary. */ + joinComplete_ = true; + if (!ownMUCJID.equals(presence.getFrom())) { + presenceSender.removeDirectedPresenceReceiver(ownMUCJID, DirectedPresenceSender.SendPresence.DontSendPresence); + ownMUCJID = presence.getFrom(); + presenceSender.addDirectedPresenceReceiver(ownMUCJID, DirectedPresenceSender.SendPresence.AndSendPresence); + } + onJoinComplete.emit(getOwnNick()); + } + if (status.code == 201) { + /* Room is created and locked */ + /* Currently deal with this by making an instant room */ + if (!ownMUCJID.equals(presence.getFrom())) { + presenceSender.removeDirectedPresenceReceiver(ownMUCJID, DirectedPresenceSender.SendPresence.DontSendPresence); + ownMUCJID = presence.getFrom(); + presenceSender.addDirectedPresenceReceiver(ownMUCJID, DirectedPresenceSender.SendPresence.AndSendPresence); + } + if (createAsReservedIfNew) { + unlocking = true; + requestConfigurationForm(); + } else { + MUCOwnerPayload mucOwnerPayload = new MUCOwnerPayload(); + presenceSender.addDirectedPresenceReceiver(ownMUCJID, DirectedPresenceSender.SendPresence.DontSendPresence); + mucOwnerPayload.setPayload(new Form(Form.Type.SUBMIT_TYPE)); + GenericRequest request = new GenericRequest(IQ.Type.Set, + getJID(), mucOwnerPayload, iqRouter_); + request.onResponse.connect(new Slot2() { + @Override + public void call(MUCOwnerPayload p1,ErrorPayload p2) { + handleCreationConfigResponse(p1,p2); + + } + }); + request.send(); + } + } + } + } + } + + private void handleOccupantRoleChangeResponse(MUCAdminPayload ref , ErrorPayload error, JID jid, MUCOccupant.Role role) { + if (error != null) { + onRoleChangeFailed.emit(error, jid, role); + } + } + private void internalJoin(String nick) { + //TODO: history request + joinComplete_ = false; + joinSucceeded_ = false; + + mucRegistry.addMUC(getJID()); + + ownMUCJID = new JID(ownMUCJID.getNode(), ownMUCJID.getDomain(), nick); + + Presence joinPresence = new Presence(presenceSender.getLastSentUndirectedPresence()); + if(joinPresence.getType() != Presence.Type.Available) { + throw new RuntimeException("From[" + joinPresence.getFrom() + "] and" + + " To[" + joinPresence.getTo() + "] is not available"); + } + joinPresence.setTo(ownMUCJID); + MUCPayload mucPayload = new MUCPayload(); + if (joinSince_ != null) { + mucPayload.setSince(joinSince_); + } + if (password != null) { + mucPayload.setPassword(password); + } + joinPresence.addPayload(mucPayload); + presenceSender.sendPresence(joinPresence); + } + + + private boolean isFromMUC(final JID j) { + return ownMUCJID.compare(j, CompareType.WithoutResource) == 0; + } + + private void handleUserLeft(LeavingType type) { + String resource = ownMUCJID.getResource(); + if (occupants.containsKey(resource)) { + MUCOccupant me = occupants.get(resource); + occupants.remove(resource); + onOccupantLeft.emit(me, type, ""); + } + occupants.clear(); + joinComplete_ = false; + joinSucceeded_ = false; + presenceSender.removeDirectedPresenceReceiver(ownMUCJID, + DirectedPresenceSender.SendPresence.DontSendPresence); + } + + /** + * Disconnect signals for this MUC. + * This method should be called when the MUC object is no longer in use + * so as to enable the garbage collector to remove this object from used space. + */ + public void disconnect() { + signalPresRcvd.onDestroyed.emit(); + } +} diff --git a/src/com/isode/stroke/muc/MUCBookmark.java b/src/com/isode/stroke/muc/MUCBookmark.java new file mode 100644 index 0000000..64f3d9d --- /dev/null +++ b/src/com/isode/stroke/muc/MUCBookmark.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tronçon. + * All rights reserved. + */ + +package com.isode.stroke.muc; + +import com.isode.stroke.elements.Storage; +import com.isode.stroke.elements.Storage.Room; +import com.isode.stroke.jid.JID; + +/** + * Class representing a Bookmark to mult-user chatrooms. + * The chatroom bookmarking function includes the ability to auto-join rooms on login. + * + */ +public class MUCBookmark { + private JID room_; + private String name_; + private String nick_; + private String password_; + private boolean autojoin_; + + /** + * Constructor + * @param room storage room, not null + */ + public MUCBookmark(Storage.Room room) { + this.name_ = room.name; + this.room_ = room.jid; + this.nick_ = room.nick; + this.password_ = room.password; + this.autojoin_ = room.autoJoin; + } + + /** + * Constructor + * @param room room jabber id, not null + * @param bookmarkName name of bookmark, can be null + */ + MUCBookmark(JID room, String bookmarkName) { + this.room_ = room; + this.name_ = bookmarkName; + this.autojoin_ = false; + } + + /** + * Set the autojoin value which determines whether the client should + * automatically join the conference room on login. + * @param enabled true to enable and false otherwise + */ + public void setAutojoin(boolean enabled) { + autojoin_ = enabled; + } + + /** + * get the autojoin attribute value + * @return true or false + */ + public boolean getAutojoin() { + return autojoin_; + } + + /** + * Set the user's preferred roomnick for the chatroom. + * @param nick nickname, can be null + */ + public void setNick(String nick) { + nick_ = nick; + } + + /** + * Set an unencrypted string for the password needed to enter a password-protected room. + * For security reasons, use of this element is NOT RECOMMENDED. + * @param password password, can be null + */ + public void setPassword(String password) { + password_ = password; + } + + /** + * get the user's nick name + * @return nick name, can be null + */ + public String getNick() { + return nick_; + } + + /** + * Get the room password + * @return room password, can be null + */ + public String getPassword() { + return password_; + } + + /** + * Get the bookmark name + * @return bookmark name, can be null + */ + public String getName() { + return name_; + } + + /** + * Get the room's jabber ID + * @return room JID, not null + */ + public JID getRoom() { + return room_; + } + + /** + * Convert the bookmark to a room object + * @return room object, not null + */ + public Room toStorage() { + Storage.Room room = new Storage.Room(); + room.name = name_; + room.jid = room_; + if (nick_ != null) { + room.nick = nick_; + } + if (password_ != null) { + room.password = password_; + } + room.autoJoin = autojoin_; + return room; + } + + @Override + public boolean equals(Object obj) { + if(this == obj) return true; + if(!(obj instanceof MUCBookmark)) return false; + MUCBookmark rhs = (MUCBookmark)obj; + if(!checkEqualsWhenNull(rhs.room_,room_)) return false; + if(!checkEqualsWhenNull(rhs.name_,name_)) return false; + if(!checkEqualsWhenNull(rhs.nick_,nick_)) return false; + if(!checkEqualsWhenNull(rhs.password_,password_)) return false; + if(!rhs.autojoin_ != autojoin_) return false; + return true; + } + + private static boolean checkEqualsWhenNull(Object thisObj, Object otherObj){ + return thisObj == null ? otherObj == null : thisObj.equals(otherObj); + } +} diff --git a/src/com/isode/stroke/muc/MUCBookmarkManager.java b/src/com/isode/stroke/muc/MUCBookmarkManager.java new file mode 100644 index 0000000..78bddea --- /dev/null +++ b/src/com/isode/stroke/muc/MUCBookmarkManager.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tronçon. + * All rights reserved. + */ +package com.isode.stroke.muc; + +import java.util.Vector; + +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.elements.Storage; +import com.isode.stroke.queries.IQRouter; +import com.isode.stroke.queries.requests.GetPrivateStorageRequest; +import com.isode.stroke.queries.requests.SetPrivateStorageRequest; +import com.isode.stroke.signals.Signal; +import com.isode.stroke.signals.Signal1; +import com.isode.stroke.signals.Slot2; + +/** + * Class representing a manager for MUC Bookmarks + * + */ +public class MUCBookmarkManager { + private boolean ready_; + private Vector bookmarks_ = new Vector(); + private IQRouter iqRouter_; + private Storage storage = new Storage(); + + /** + * Constructor + * @param iqRouter IQ router,not null + */ + public MUCBookmarkManager(IQRouter iqRouter) { + iqRouter_ = iqRouter; + ready_ = false; + GetPrivateStorageRequest request = GetPrivateStorageRequest.create(storage,iqRouter_); + request.onResponse.connect(new Slot2(){ + @Override + public void call(Storage p1, ErrorPayload p2) { + handleBookmarksReceived(p1, p2); + } + }); + request.send(); + } + + /** + * Replace the bookmark + * @param oldBookmark bookmark to be replaced, not null + * @param newBookmark bookmark to replace with + */ + public void replaceBookmark(MUCBookmark oldBookmark, MUCBookmark newBookmark) { + if (!ready_) return; + for (int i = 0; i < bookmarks_.size(); i++) { + if (bookmarks_.get(i).equals(oldBookmark)) { + bookmarks_.add(i, newBookmark); + flush(); + onBookmarkRemoved.emit(oldBookmark); + onBookmarkAdded.emit(newBookmark); + return; + } + } + } + + /** + * Add a bookmark + * @param bookmark bookmark to be added, not null + */ + public void addBookmark(MUCBookmark bookmark) { + if (!ready_) return; + bookmarks_.add(bookmark); + onBookmarkAdded.emit(bookmark); + flush(); + } + + /** + * Remove the given bookmark + * @param bookmark bookmark to be removed + */ + public void removeBookmark(MUCBookmark bookmark) { + if (!ready_) return; + for (MUCBookmark mb : bookmarks_) { + if (mb.equals(bookmark)) { + bookmarks_.remove(mb); + onBookmarkRemoved.emit(bookmark); + break; + } + } + flush(); + } + + /** + * Get a list of bookmarks + * @return list of bookmarks, can be empty but not null + */ + public Vector getBookmarks() { + return bookmarks_; + } + + public Signal1 onBookmarkAdded = new Signal1(); + public Signal1 onBookmarkRemoved = new Signal1(); + /** + * When server bookmarks are ready to be used (request response has been received). + */ + public Signal onBookmarksReady = new Signal(); + + private void handleBookmarksReceived(Storage payload, ErrorPayload error) { + if (error != null) { + return; + } + + ready_ = true; + onBookmarksReady.emit(); + storage = payload; + + Vector receivedBookmarks = new Vector(); + for (Storage.Room room : payload.getRooms()) { + receivedBookmarks.add(new MUCBookmark(room)); + } + + Vector newBookmarks = new Vector(); + for (MUCBookmark oldBookmark : bookmarks_) { + if (containsEquivalent(receivedBookmarks,oldBookmark)) { + newBookmarks.add(oldBookmark); + } else { + onBookmarkRemoved.emit(oldBookmark); + } + } + + for (MUCBookmark newBookmark : receivedBookmarks) { + if (!containsEquivalent(bookmarks_,newBookmark)) { + newBookmarks.add(newBookmark); + onBookmarkAdded.emit(newBookmark); + } + } + bookmarks_ = newBookmarks; + } + + private boolean containsEquivalent(Vector list, MUCBookmark bookmark) { + for(MUCBookmark mb : list) { + if(mb.equals(bookmark)) { + return true; + } + } + return false; + } + + + private void flush() { + if (storage == null) { + storage = new Storage(); + } + // Update the storage element + storage.clearRooms(); + for(MUCBookmark bookmark :bookmarks_) { + storage.addRoom(bookmark.toStorage()); + } + + // Send an iq to save the storage element + SetPrivateStorageRequest request = SetPrivateStorageRequest.create(storage, iqRouter_); + // FIXME: We should care about the result + /*request.onResponse.connect(new Slot1() { + @Override + public void call(ErrorPayload p1) { + } + });*/ + request.send(); + } +} diff --git a/src/com/isode/stroke/muc/MUCManager.java b/src/com/isode/stroke/muc/MUCManager.java new file mode 100644 index 0000000..6a7757a --- /dev/null +++ b/src/com/isode/stroke/muc/MUCManager.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tronçon. + * All rights reserved. + */ +package com.isode.stroke.muc; + +import com.isode.stroke.client.StanzaChannel; +import com.isode.stroke.jid.JID; +import com.isode.stroke.presence.DirectedPresenceSender; +import com.isode.stroke.queries.IQRouter; + +/** + * Class representing a manager for Multi user chat + * + */ +public class MUCManager { + private StanzaChannel stanzaChannel_; + private IQRouter iqRouter_; + private DirectedPresenceSender presenceSender_; + private MUCRegistry mucRegistry_; + + /** + * Create the MUC manager + * @param stanzaChannel stanza channel, not null + * @param iqRouter IQ router, not null + * @param presenceSender Presence sender, not null + * @param mucRegistry MUC Registry, not null + */ + public MUCManager(StanzaChannel stanzaChannel, IQRouter iqRouter, + DirectedPresenceSender presenceSender, MUCRegistry mucRegistry) { + stanzaChannel_ = stanzaChannel; + iqRouter_ = iqRouter; + presenceSender_ = presenceSender; + mucRegistry_ = mucRegistry; + } + + /** + * Create a multi user chat room + * @param jid Room Jabber ID, not null + * @return MUC room, not null + */ + public MUC createMUC(JID jid) { + return new MUC(stanzaChannel_, iqRouter_, presenceSender_, jid, mucRegistry_); + } +} diff --git a/src/com/isode/stroke/muc/MUCRegistry.java b/src/com/isode/stroke/muc/MUCRegistry.java new file mode 100644 index 0000000..984b2ba --- /dev/null +++ b/src/com/isode/stroke/muc/MUCRegistry.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tronçon + * All rights reserved. + */ +package com.isode.stroke.muc; + +import java.util.Vector; + +import com.isode.stroke.jid.JID; + +/** + * Class representing a MUC Registry + * + */ +public class MUCRegistry { + + /** + * Add the JID of a multi-user chat room to the registry + * @param j JID of the room, not null + */ + public void addMUC(JID j) { + mucs.add(j); + } + + /** + * Determine if the given JID is contained in the Jabber ID + * @param j Jabber ID, not null + * @return true if it exists in the Registry and false otherwise + */ + public boolean isMUC(JID j){ + return mucs.contains(j); + } + + /** + * Remove the Jabber ID from the registry + * @param j Jabber ID to remove, not null + */ + public void removeMUC(JID j) { + mucs.remove(j); + } + + private Vector mucs = new Vector(); +} diff --git a/test/com/isode/stroke/muc/MUCTest.java b/test/com/isode/stroke/muc/MUCTest.java new file mode 100644 index 0000000..4b845c0 --- /dev/null +++ b/test/com/isode/stroke/muc/MUCTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tronçon. + * All rights reserved. + */ +package com.isode.stroke.muc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Vector; + +import org.junit.Before; +import org.junit.Test; + +import com.isode.stroke.client.DummyStanzaChannel; +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.elements.Form; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.elements.MUCItem; +import com.isode.stroke.elements.MUCOccupant; +import com.isode.stroke.elements.MUCOwnerPayload; +import com.isode.stroke.elements.MUCUserPayload; +import com.isode.stroke.elements.Presence; +import com.isode.stroke.jid.JID; +import com.isode.stroke.presence.DirectedPresenceSender; +import com.isode.stroke.presence.StanzaChannelPresenceSender; +import com.isode.stroke.queries.IQRouter; + +/** + * Unit tests for MUC + * + */ +public class MUCTest { + private DummyStanzaChannel channel; + private IQRouter router; + MUCRegistry mucRegistry; + StanzaChannelPresenceSender stanzaChannelPresenceSender; + DirectedPresenceSender presenceSender; + private static class JoinResult { + String nick; + ErrorPayload error; + }; + private Vector joinResults; + + @Before + public void setUp() { + channel = new DummyStanzaChannel(); + router = new IQRouter(channel); + mucRegistry = new MUCRegistry(); + stanzaChannelPresenceSender = new StanzaChannelPresenceSender(channel); + presenceSender = new DirectedPresenceSender(stanzaChannelPresenceSender); + } + + @Test + public void testJoin() { + MUC testling = createMUC(new JID("foo@bar.com")); + testling.joinAs("Alice"); + + assertTrue(mucRegistry.isMUC(new JID("foo@bar.com"))); + Presence p = channel.getStanzaAtIndex(new Presence(),0); + assertTrue(p != null); + assertEquals(new JID("foo@bar.com/Alice"), p.getTo()); + } + + @Test + public void testJoin_ChangePresenceDuringJoinDoesNotSendPresenceBeforeJoinSuccess() { + MUC testling = createMUC(new JID("foo@bar.com")); + testling.joinAs("Alice"); + + presenceSender.sendPresence(new Presence("Test")); + assertEquals(2, channel.sentStanzas.size()); + } + + @Test + public void testJoin_ChangePresenceDuringJoinResendsPresenceAfterJoinSuccess() { + MUC testling = createMUC(new JID("foo@bar.com")); + testling.joinAs("Alice"); + + presenceSender.sendPresence(new Presence("Test")); + receivePresence(new JID("foo@bar.com/Rabbit"), "Here"); + + assertEquals(3, channel.sentStanzas.size()); + Presence p = channel.getStanzaAtIndex(new Presence(),2); + assertTrue(p != null); + assertEquals(new JID("foo@bar.com/Alice"), p.getTo()); + assertEquals("Test", p.getStatus()); + } + + @Test + public void testCreateInstant() { + MUC testling = createMUC(new JID("rabbithole@wonderland.lit")); + testling.joinAs("Alice"); + Presence serverRespondsLocked = new Presence(); + serverRespondsLocked.setFrom(new JID("rabbithole@wonderland.lit/Alice")); + MUCUserPayload mucPayload = new MUCUserPayload(); + MUCItem myItem = new MUCItem(); + myItem.affiliation = MUCOccupant.Affiliation.Owner; + myItem.role = MUCOccupant.Role.Moderator; + mucPayload.addItem(myItem); + mucPayload.addStatusCode(new MUCUserPayload.StatusCode(110)); + mucPayload.addStatusCode(new MUCUserPayload.StatusCode(201)); + serverRespondsLocked.addPayload(mucPayload); + channel.onPresenceReceived.emit(serverRespondsLocked); + assertEquals(2, channel.sentStanzas.size()); + IQ iq = channel.getStanzaAtIndex(new IQ(),1); + assertTrue(iq != null); + MUCOwnerPayload ownerPl = new MUCOwnerPayload(); + assertNotNull(iq.getPayload(ownerPl)); + assertNotNull(iq.getPayload(ownerPl).getForm()); + assertEquals(Form.Type.SUBMIT_TYPE, iq.getPayload(ownerPl).getForm().getType()); + } + + @Test + public void testReplicateBug() { + Presence initialPresence = new Presence(); + initialPresence.setStatus(""); + + //TODO: after vcard is ported this can be uncommented + /*VCard vcard = new VCard(); + vcard.setPhoto(createByteArray("15c30080ae98ec48be94bf0e191d43edd06e500a")); + initialPresence.addPayload(vcard); + CapsInfo caps = boost::make_shared(); + caps.setNode("http://swift.im"); + caps.setVersion("p2UP0DrcVgKM6jJqYN/B92DKK0o="); + initialPresence.addPayload(caps);*/ + + channel.sendPresence(initialPresence); + + MUC testling = createMUC(new JID("test@rooms.swift.im")); + testling.joinAs("Test"); + Presence serverRespondsLocked = new Presence(); + serverRespondsLocked.setFrom(new JID("test@rooms.swift.im/Test")); + serverRespondsLocked.setTo(new JID("test@swift.im/6913d576d55f0b67")); + //serverRespondsLocked.addPayload(vcard); + //serverRespondsLocked.addPayload(caps); + serverRespondsLocked.setStatus(""); + MUCUserPayload mucPayload = new MUCUserPayload(); + MUCItem myItem = new MUCItem(); + myItem.affiliation = MUCOccupant.Affiliation.Owner; + myItem.role = MUCOccupant.Role.Moderator; + mucPayload.addItem(myItem); + mucPayload.addStatusCode(new MUCUserPayload.StatusCode(201)); + serverRespondsLocked.addPayload(mucPayload); + channel.onPresenceReceived.emit(serverRespondsLocked); + assertEquals(3, channel.sentStanzas.size()); + IQ iq = channel.getStanzaAtIndex(new IQ(),2); + assertTrue(iq != null); + assertTrue(iq.getPayload(new MUCOwnerPayload()) != null); + assertTrue(iq.getPayload(new MUCOwnerPayload()).getForm() != null); + assertEquals(Form.Type.SUBMIT_TYPE, iq.getPayload(new MUCOwnerPayload()).getForm().getType()); +} + + + private MUC createMUC(JID jid) { + return new MUC(channel, router, presenceSender, jid, mucRegistry); + } + + private void handleJoinFinished(String nick, ErrorPayload error) { + JoinResult r = new JoinResult(); + r.nick = nick; + r.error = error; + joinResults.add(r); + } + + private void receivePresence(JID jid, String status) { + Presence p = new Presence(status); + p.setFrom(jid); + channel.onPresenceReceived.emit(p); + } +} -- cgit v0.10.2-6-g49f6