summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/isode/stroke/client/Client.java73
-rw-r--r--src/com/isode/stroke/client/DummyStanzaChannel.java115
-rw-r--r--src/com/isode/stroke/muc/MUC.java631
-rw-r--r--src/com/isode/stroke/muc/MUCBookmark.java151
-rw-r--r--src/com/isode/stroke/muc/MUCBookmarkManager.java171
-rw-r--r--src/com/isode/stroke/muc/MUCManager.java49
-rw-r--r--src/com/isode/stroke/muc/MUCRegistry.java47
7 files changed, 1237 insertions, 0 deletions
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<Stanza> sentStanzas = new Vector<Stanza>();
+ 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 <T extends Payload> 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> 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<ErrorPayload,JID, MUCOccupant.Affiliation> onAffiliationChangeFailed =
+ new Signal3<ErrorPayload,JID, MUCOccupant.Affiliation>();
+ public Signal1<ErrorPayload> onAffiliationListFailed = new Signal1<ErrorPayload>();
+ public Signal2<MUCOccupant.Affiliation, Vector<JID>> onAffiliationListReceived =
+ new Signal2<MUCOccupant.Affiliation, Vector<JID>>();
+ public Signal1<ErrorPayload> onConfigurationFailed = new Signal1<ErrorPayload>();
+ public Signal1<Form> onConfigurationFormReceived = new Signal1<Form>();
+ public Signal1<String> onJoinComplete = new Signal1<String>();
+ public Signal1<ErrorPayload> onJoinFailed = new Signal1<ErrorPayload>();
+ public Signal3<String, MUCOccupant.Affiliation, MUCOccupant.Affiliation> onOccupantAffiliationChanged =
+ new Signal3<String, MUCOccupant.Affiliation, MUCOccupant.Affiliation>();
+ public Signal1<MUCOccupant> onOccupantJoined = new Signal1<MUCOccupant>();
+
+ public Signal3<MUCOccupant, LeavingType, String> onOccupantLeft =
+ new Signal3<MUCOccupant, LeavingType, String>();
+ public Signal1<Presence> onOccupantPresenceChange = new Signal1<Presence>();
+ public Signal3<String, MUCOccupant, MUCOccupant.Role> onOccupantRoleChanged =
+ new Signal3<String, MUCOccupant, MUCOccupant.Role>();
+
+ public Signal3<ErrorPayload,JID,MUCOccupant.Role> onRoleChangeFailed =
+ new Signal3<ErrorPayload,JID,MUCOccupant.Role>();
+
+ private boolean createAsReservedIfNew;
+ private IQRouter iqRouter_;
+ private boolean joinComplete_;
+ private Date joinSince_;
+ private boolean joinSucceeded_;
+ private MUCRegistry mucRegistry;
+ private Map<String, MUCOccupant> occupants = new HashMap<String, MUCOccupant>();
+ 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<Presence>() {
+ @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<MUCOwnerPayload> request = new GenericRequest<MUCOwnerPayload>(
+ 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<MUCAdminPayload> request = new GenericRequest<MUCAdminPayload>(
+ IQ.Type.Set, getJID(), mucPayload, iqRouter_);
+ request.onResponse.connect(new Slot2<MUCAdminPayload, ErrorPayload>() {
+ @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<MUCAdminPayload> request = new GenericRequest<MUCAdminPayload>(
+ IQ.Type.Set, getJID(), mucPayload, iqRouter_);
+ request.onResponse.connect(new Slot2<MUCAdminPayload, ErrorPayload>() {
+ @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<MUCOwnerPayload> request = new GenericRequest<MUCOwnerPayload>(
+ IQ.Type.Set, getJID(), mucPayload, iqRouter_);
+ if (unlocking) {
+ request.onResponse.connect(new Slot2<MUCOwnerPayload, ErrorPayload>() {
+ @Override
+ public void call(MUCOwnerPayload p1, ErrorPayload p2) {
+ handleCreationConfigResponse(p1, p2);
+ }
+ });
+ }else {
+ request.onResponse.connect(new Slot2<MUCOwnerPayload, ErrorPayload>() {
+ @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<MUCOwnerPayload> request = new GenericRequest<MUCOwnerPayload>(
+ IQ.Type.Set, getJID(), mucPayload, iqRouter_);
+ request.onResponse.connect(new Slot2<MUCOwnerPayload, ErrorPayload>() {
+ @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<MUCAdminPayload> request = new GenericRequest<MUCAdminPayload>(
+ IQ.Type.Get, getJID(), mucPayload, iqRouter_);
+ request.onResponse.connect(new Slot2<MUCAdminPayload, ErrorPayload>() {
+ @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<MUCOwnerPayload> request = new GenericRequest<MUCOwnerPayload>(
+ IQ.Type.Get, getJID(), mucPayload, iqRouter_);
+ request.onResponse.connect(new Slot2<MUCOwnerPayload, ErrorPayload>() {
+ @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<JID> jids = new Vector<JID>();
+ 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 <message/>
+ //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<MUCOwnerPayload> request = new GenericRequest<MUCOwnerPayload>(IQ.Type.Set,
+ getJID(), mucOwnerPayload, iqRouter_);
+ request.onResponse.connect(new Slot2<MUCOwnerPayload, ErrorPayload>() {
+ @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<MUCBookmark> bookmarks_ = new Vector<MUCBookmark>();
+ private IQRouter iqRouter_;
+ private Storage storage = new Storage();
+
+ /**
+ * Constructor
+ * @param iqRouter IQ router,not null
+ */
+ public MUCBookmarkManager(IQRouter iqRouter) {
+ iqRouter_ = iqRouter;
+ ready_ = false;
+ GetPrivateStorageRequest<Storage> request = GetPrivateStorageRequest.create(storage,iqRouter_);
+ request.onResponse.connect(new Slot2<Storage, ErrorPayload>(){
+ @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<MUCBookmark> getBookmarks() {
+ return bookmarks_;
+ }
+
+ public Signal1<MUCBookmark> onBookmarkAdded = new Signal1<MUCBookmark>();
+ public Signal1<MUCBookmark> onBookmarkRemoved = new Signal1<MUCBookmark>();
+ /**
+ * 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<MUCBookmark> receivedBookmarks = new Vector<MUCBookmark>();
+ for (Storage.Room room : payload.getRooms()) {
+ receivedBookmarks.add(new MUCBookmark(room));
+ }
+
+ Vector<MUCBookmark> newBookmarks = new Vector<MUCBookmark>();
+ 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<MUCBookmark> 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<Storage> request = SetPrivateStorageRequest.create(storage, iqRouter_);
+ // FIXME: We should care about the result
+ /*request.onResponse.connect(new Slot1<ErrorPayload>() {
+ @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<JID> mucs = new Vector<JID>();
+}