From 9ead0fdcca595df9dc3f4143122776b398dbe405 Mon Sep 17 00:00:00 2001 From: Tarun Gupta Date: Tue, 14 Jul 2015 20:45:09 +0530 Subject: Completes MUC in accordance with Swiften. Updates MUC to be an abstract class. Updates MUCBookmark, MUCManager, DirectedPresenceSender. Adds MUCImpl. License: This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details. Test-Information: Test updated for MUC, in accordance with Swiften, which passes. Added MockMUC too. Change-Id: If5aa672e8adc093162d3c9890c8437d3edadea68 diff --git a/src/com/isode/stroke/muc/MUC.java b/src/com/isode/stroke/muc/MUC.java index ed3c34a..9806e34 100644 --- a/src/com/isode/stroke/muc/MUC.java +++ b/src/com/isode/stroke/muc/MUC.java @@ -1,7 +1,14 @@ /* - * Copyright (c) 2010-2015, Isode Limited, London, England. + * Copyright (c) 2010-2014 Isode Limited. * All rights reserved. + * See the COPYING file for more information. */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + package com.isode.stroke.muc; import java.util.Collections; @@ -37,614 +44,65 @@ 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(); - - public final Signal onUnlocked = new Signal(); - - 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); - } - - public Map getOccupants() { - return Collections.unmodifiableMap(occupants); - } - - /** - * 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, "", false, false); - } - - /** - * 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 - * @param isImpromptu - */ - public void invitePerson(JID person, String reason, boolean isImpromptu) { - invitePerson(person, reason, isImpromptu, false); - } - - - /** - * 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 - * @param isImpromptu - * @param isReuseChat - */ - public void invitePerson(JID person, String reason, boolean isImpromptu, boolean isReuseChat) { - Message message = new Message(); - message.setTo(person); - message.setType(Message.Type.Normal); - MUCInvitationPayload invite = new MUCInvitationPayload(); - invite.setReason(reason); - invite.setIsImpromptu(isImpromptu); - invite.setIsContinuation(isReuseChat); - 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; - } - - public 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(); - } -} +public abstract class MUC { + + public enum JoinResult { JoinSucceeded, JoinFailed }; + public enum LeavingType { LeavePart, LeaveKick, LeaveBan, LeaveDestroy, LeaveNotMember, Disconnect }; + + /** + * Returns the (bare) JID of the MUC. + */ + public abstract JID getJID(); + + /** + * Returns if the room is unlocked and other people can join the room. + * @return True if joinable by others; false otherwise. + */ + public abstract boolean isUnlocked(); + + public abstract void joinAs(final String nick); + public abstract void joinWithContextSince(final String nick, final Date since); + /*public abstract void queryRoomInfo(); */ + /*public abstract void queryRoomItems(); */ + /*public abstract String getCurrentNick(); */ + public abstract Map getOccupants(); + public abstract void changeNickname(final String newNickname); + public abstract void part(); + /*public abstract void handleIncomingMessage(Message::ref message); */ + /** Expose public so it can be called when e.g. user goes offline */ + public abstract void handleUserLeft(LeavingType l); + /** Get occupant information*/ + public abstract MUCOccupant getOccupant(final String nick); + public abstract boolean hasOccupant(final String nick); + public abstract void kickOccupant(final JID jid); + public abstract void changeOccupantRole(final JID jid, MUCOccupant.Role role); + public abstract void requestAffiliationList(MUCOccupant.Affiliation aff); + public abstract void changeAffiliation(final JID jid, MUCOccupant.Affiliation affiliation); + public abstract void changeSubject(final String subject); + public abstract void requestConfigurationForm(); + public abstract void configureRoom(Form f); + public abstract void cancelConfigureRoom(); + public abstract void destroyRoom(); + /** Send an invite for the person to join the MUC */ + public abstract void invitePerson(final JID person, final String reason, boolean isImpromptu, boolean isReuseChat); + public abstract void setCreateAsReservedIfNew(); + public abstract void setPassword(final String password); + + public final Signal1 onJoinComplete = new Signal1(); + public final Signal1 onJoinFailed = new Signal1(); + public final Signal3 onRoleChangeFailed = new Signal3(); + public final Signal3 onAffiliationChangeFailed = new Signal3(); + public final Signal1 onConfigurationFailed = new Signal1(); + public final Signal1 onAffiliationListFailed = new Signal1(); + public final Signal1 onOccupantPresenceChange = new Signal1(); + public final Signal3 onOccupantRoleChanged = new Signal3(); + public final Signal3 onOccupantAffiliationChanged = new Signal3(); + public final Signal1 onOccupantJoined = new Signal1(); + public final Signal2 onOccupantNicknameChanged = new Signal2(); + public final Signal3 onOccupantLeft = new Signal3(); + public final Signal1 onConfigurationFormReceived = new Signal1(); + public final Signal2 > onAffiliationListReceived = new Signal2 >(); + public final Signal onUnlocked = new Signal(); + /* public final Signal1 onInfoResult; */ + /* public final Signal1 onItemsResult; */ +} \ No newline at end of file diff --git a/src/com/isode/stroke/muc/MUCBookmark.java b/src/com/isode/stroke/muc/MUCBookmark.java index 42b9dff..e4a306c 100644 --- a/src/com/isode/stroke/muc/MUCBookmark.java +++ b/src/com/isode/stroke/muc/MUCBookmark.java @@ -16,7 +16,7 @@ import com.isode.stroke.jid.JID; */ public class MUCBookmark { private JID room_; - private String name_; + private String name_ = ""; private String nick_; private String password_; private boolean autojoin_; @@ -136,7 +136,7 @@ public class MUCBookmark { 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(!checkEqualsWhenNull(rhs.password_,password_)) return false; if(!rhs.autojoin_ != autojoin_) return false; return true; } diff --git a/src/com/isode/stroke/muc/MUCImpl.java b/src/com/isode/stroke/muc/MUCImpl.java new file mode 100644 index 0000000..88d98a1 --- /dev/null +++ b/src/com/isode/stroke/muc/MUCImpl.java @@ -0,0 +1,736 @@ +/* + * Copyright (c) 2010-2015, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.muc; + +import java.util.Collections; +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.CapsInfo; +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.Signal; +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 MUCImpl extends MUC { + + 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 = new JID(); + private String password; + private DirectedPresenceSender presenceSender; + private StanzaChannel stanzaChannel; + private boolean unlocking; + private boolean isUnlocked_; + private SignalConnection scopedConnection_; + private Presence joinRequestPresence_ = new Presence(); + + /** + * 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 MUCImpl(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; + this.isUnlocked_ = false; + scopedConnection_ = 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(p1,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(p1,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(); + } + + /** + * Returns if the room is unlocked and other people can join the room. + * @return True if joinable by others; false otherwise. + */ + public boolean isUnlocked() { + return isUnlocked_; + } + + /** + * 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); + } + + public Map getOccupants() { + return occupants; + } + + /** + * 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, "", false, false); + } + + /** + * 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 + * @param isImpromptu + */ + public void invitePerson(JID person, String reason, boolean isImpromptu) { + invitePerson(person, reason, isImpromptu, false); + } + + + /** + * 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 + * @param isImpromptu + * @param isReuseChat + */ + public void invitePerson(JID person, String reason, boolean isImpromptu, boolean isReuseChat) { + Message message = new Message(); + message.setTo(person); + message.setType(Message.Type.Normal); + MUCInvitationPayload invite = new MUCInvitationPayload(); + invite.setReason(reason); + invite.setIsImpromptu(isImpromptu); + invite.setIsContinuation(isReuseChat); + 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); + } + + public void changeNickname(final String newNickname) { + Presence changeNicknamePresence = new Presence(); + changeNicknamePresence.setTo(new JID(ownMUCJID.toBare().toString() + "/" + newNickname)); + presenceSender.sendPresence(changeNicknamePresence); + } + + /** + * 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. */ + isUnlocked_ = true; + onUnlocked.emit(); + } + } + + 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().equals(Presence.Type.Error)) { + String reason = ""; + onJoinFailed.emit(presence.getPayload(new ErrorPayload())); + return; + } + else { + joinSucceeded_ = true; + presenceSender.addDirectedPresenceReceiver(ownMUCJID, + DirectedPresenceSender.SendPresence.DontSendPresence); + if ((presenceSender.getLastSentUndirectedPresence() != null) && !(isEqualExceptID(presenceSender.getLastSentUndirectedPresence(), joinRequestPresence_))) { + // our presence changed between join request and join complete, send current presence to MUC + Presence latestPresence = new Presence(presenceSender.getLastSentUndirectedPresence()); + latestPresence.setTo(ownMUCJID); + presenceSender.sendPresence(latestPresence); + } + } + } + + 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().equals(Presence.Type.Unavailable)) { + LeavingType type = LeavingType.LeavePart; + String newNickname = null; + 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; + } else if (status.code == 303) { + if (mucPayload.getItems().size() == 1) { + newNickname = mucPayload.getItems().get(0).nick; + } + } + } + } + + if (newNickname != null) { + if (occupants.containsKey(nick)) { + MUCOccupant occupant = occupants.get(nick); + occupants.remove(nick); + occupant.setNick(newNickname); + occupants.put(newNickname, occupant); + onOccupantNicknameChanged.emit(nick, newNickname); + } + } + else { + if (presence.getFrom().equals(ownMUCJID)) { + handleUserLeft(type); + return; + } else { + if (occupants.containsKey(nick)) { + //TODO: part type + MUCOccupant occupant = occupants.get(nick); + occupants.remove(nick); + onOccupantLeft.emit(occupant, type, ""); + } + } + } + } + else if (presence.getType().equals(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().equals(role)) { + onOccupantRoleChanged.emit(nick, occupant, oldOccupant.getRole()); + } + if (!oldOccupant.getAffiliation().equals(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_) { + boolean isLocked = false; + 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); + } + } + if (status.code == 201) { + isLocked = true; + /* 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 { + // Accept default room configuration and create an instant room http://xmpp.org/extensions/xep-0045.html#createroom-instant + 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(); + } + } + } + if (joinComplete_ && !isLocked) { + onJoinComplete.emit(getOwnNick()); + } + if (!isLocked && !isUnlocked_ && (presence.getFrom().equals(ownMUCJID))) { + isUnlocked_ = true; + onUnlocked.emit(); + } + } + } + + private void handleOccupantRoleChangeResponse(MUCAdminPayload ref , ErrorPayload error, JID jid, MUCOccupant.Role role) { + if (error != null) { + onRoleChangeFailed.emit(error, jid, role); + } + } + + /** + * This function compares two Presence elements for equality based on to, from, status, show and entity capability information. + * @return True if equal; else otherwise. + */ + private static boolean isEqualExceptID(final Presence lhs, final Presence rhs) { + boolean isEqual = false; + if(lhs == null || rhs == null) { + isEqual = (lhs == null && rhs == null); + } + else if (lhs.getFrom().equals(rhs.getFrom()) && lhs.getTo().equals(rhs.getTo()) && lhs.getStatus().equals(rhs.getStatus()) && lhs.getShow().equals(rhs.getShow())) { + CapsInfo lhsCaps = lhs.getPayload(new CapsInfo()); + CapsInfo rhsCaps = rhs.getPayload(new CapsInfo()); + + if (lhsCaps != null && rhsCaps != null) { + isEqual = (lhsCaps.equals(rhsCaps)); + } + else { + isEqual = (lhsCaps == null && rhsCaps == null); + } + } + return isEqual; + } + + 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 = (presenceSender.getLastSentUndirectedPresence() != null ? new Presence(presenceSender.getLastSentUndirectedPresence()) : new Presence()); + 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); + joinRequestPresence_ = joinPresence; + presenceSender.sendPresence(joinPresence); + } + + + private boolean isFromMUC(final JID j) { + return ownMUCJID.compare(j, CompareType.WithoutResource) == 0; + } + + public 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; + isUnlocked_ = 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() { + if (scopedConnection_ != null) { + scopedConnection_.onDestroyed.emit(); + scopedConnection_ = null; + } + } + + protected void finalize() throws Throwable { + try { + disconnect(); + } + finally { + super.finalize(); + } + } + + //TODO: Invites(direct/mediated) + + //TODO: requesting membership + + //TODO: get member list + + //TODO: request voice + + //TODO: moderator use cases + + //TODO: Admin use cases + + //TODO: Owner use cases + +} diff --git a/src/com/isode/stroke/muc/MUCManager.java b/src/com/isode/stroke/muc/MUCManager.java index 6a7757a..5d5ccba 100644 --- a/src/com/isode/stroke/muc/MUCManager.java +++ b/src/com/isode/stroke/muc/MUCManager.java @@ -44,6 +44,6 @@ public class MUCManager { * @return MUC room, not null */ public MUC createMUC(JID jid) { - return new MUC(stanzaChannel_, iqRouter_, presenceSender_, jid, mucRegistry_); + return new MUCImpl(stanzaChannel_, iqRouter_, presenceSender_, jid, mucRegistry_); } } diff --git a/src/com/isode/stroke/presence/DirectedPresenceSender.java b/src/com/isode/stroke/presence/DirectedPresenceSender.java index a08a2b8..5af85ef 100644 --- a/src/com/isode/stroke/presence/DirectedPresenceSender.java +++ b/src/com/isode/stroke/presence/DirectedPresenceSender.java @@ -78,7 +78,7 @@ public class DirectedPresenceSender implements PresenceSender { return; } sender_.sendPresence(presence); - if (presence.getTo() == null || !presence.getTo().isValid()) { + if (!presence.getTo().isValid()) { Presence presenceCopy = new Presence(presence); for(JID jid : directedPresenceReceivers_) { presenceCopy.setTo(jid); @@ -96,7 +96,7 @@ public class DirectedPresenceSender implements PresenceSender { */ public Presence getLastSentUndirectedPresence(){ if(lastSentUndirectedPresence_ == null) { - return new Presence(); + return null; }else { return new Presence(lastSentUndirectedPresence_); } diff --git a/test/com/isode/stroke/muc/MUCTest.java b/test/com/isode/stroke/muc/MUCTest.java index 4b845c0..3de840c 100644 --- a/test/com/isode/stroke/muc/MUCTest.java +++ b/test/com/isode/stroke/muc/MUCTest.java @@ -26,10 +26,14 @@ 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.elements.CapsInfo; +import com.isode.stroke.elements.VCard; import com.isode.stroke.jid.JID; import com.isode.stroke.presence.DirectedPresenceSender; import com.isode.stroke.presence.StanzaChannelPresenceSender; import com.isode.stroke.queries.IQRouter; +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.signals.Slot2; /** * Unit tests for MUC @@ -38,14 +42,15 @@ import com.isode.stroke.queries.IQRouter; public class MUCTest { private DummyStanzaChannel channel; private IQRouter router; - MUCRegistry mucRegistry; - StanzaChannelPresenceSender stanzaChannelPresenceSender; - DirectedPresenceSender presenceSender; + private MUCRegistry mucRegistry; + private StanzaChannelPresenceSender stanzaChannelPresenceSender; + private DirectedPresenceSender presenceSender; private static class JoinResult { - String nick; + String nick = ""; ErrorPayload error; }; - private Vector joinResults; + private Vector joinResults = new Vector(); + private int nickChanges; @Before public void setUp() { @@ -54,6 +59,7 @@ public class MUCTest { mucRegistry = new MUCRegistry(); stanzaChannelPresenceSender = new StanzaChannelPresenceSender(channel); presenceSender = new DirectedPresenceSender(stanzaChannelPresenceSender); + nickChanges = 0; } @Test @@ -92,6 +98,20 @@ public class MUCTest { } @Test + public void testJoin_NoPresenceChangeDuringJoinDoesNotResendAfterJoinSuccess() { + MUC testling = createMUC(new JID("foo@bar.com")); + testling.joinAs("Alice"); + + receivePresence(new JID("foo@bar.com/Rabbit"), "Here"); + + assertEquals(1, channel.sentStanzas.size()); + Presence p = channel.getStanzaAtIndex(new Presence(), 0); + assertTrue(p != null); + assertEquals(new JID("foo@bar.com/Alice"), p.getTo()); + assertEquals("", p.getStatus()); + } + + @Test public void testCreateInstant() { MUC testling = createMUC(new JID("rabbithole@wonderland.lit")); testling.joinAs("Alice"); @@ -120,14 +140,13 @@ public class MUCTest { Presence initialPresence = new Presence(); initialPresence.setStatus(""); - //TODO: after vcard is ported this can be uncommented - /*VCard vcard = new VCard(); - vcard.setPhoto(createByteArray("15c30080ae98ec48be94bf0e191d43edd06e500a")); + VCard vcard = new VCard(); + vcard.setPhoto(new ByteArray("15c30080ae98ec48be94bf0e191d43edd06e500a")); initialPresence.addPayload(vcard); - CapsInfo caps = boost::make_shared(); + CapsInfo caps = new CapsInfo(); caps.setNode("http://swift.im"); caps.setVersion("p2UP0DrcVgKM6jJqYN/B92DKK0o="); - initialPresence.addPayload(caps);*/ + initialPresence.addPayload(caps); channel.sendPresence(initialPresence); @@ -136,8 +155,8 @@ public class MUCTest { 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.addPayload(vcard); + serverRespondsLocked.addPayload(caps); serverRespondsLocked.setStatus(""); MUCUserPayload mucPayload = new MUCUserPayload(); MUCItem myItem = new MUCItem(); @@ -153,11 +172,94 @@ public class MUCTest { assertTrue(iq.getPayload(new MUCOwnerPayload()) != null); assertTrue(iq.getPayload(new MUCOwnerPayload()).getForm() != null); assertEquals(Form.Type.SUBMIT_TYPE, iq.getPayload(new MUCOwnerPayload()).getForm().getType()); -} + } + + @Test + public void testNicknameChange() { + MUC testling = createMUC(new JID("foo@bar.com")); + // Join as Rabbit + testling.joinAs("Rabbit"); + + // Rabbit joins + Presence rabbitJoins = new Presence(); + rabbitJoins.setTo(new JID("test@swift.im/6913d576d55f0b67")); + rabbitJoins.setFrom(new JID(testling.getJID().toString() + "/Rabbit")); + channel.onPresenceReceived.emit(rabbitJoins); + assertEquals(true, testling.hasOccupant("Rabbit")); + // Alice joins + Presence aliceJoins = new Presence(); + aliceJoins.setTo(new JID("test@swift.im/6913d576d55f0b67")); + aliceJoins.setFrom(new JID(testling.getJID().toString() + "/Alice")); + channel.onPresenceReceived.emit(aliceJoins); + assertEquals(true, testling.hasOccupant("Alice")); + + // Change nick to Dodo + testling.changeNickname("Dodo"); + Presence stanza = channel.getStanzaAtIndex(new Presence(), 1); + assertNotNull(stanza); + assertEquals("Dodo", stanza.getTo().getResource()); + + // Alice changes nick to Alice2 + stanza = new Presence(); + stanza.setFrom(new JID("foo@bar.com/Alice")); + stanza.setTo(router.getJID()); + stanza.setType(Presence.Type.Unavailable); + MUCUserPayload mucPayload = new MUCUserPayload(); + MUCItem myItem = new MUCItem(); + myItem.affiliation = MUCOccupant.Affiliation.Member; + myItem.nick = "Alice2"; + myItem.role = MUCOccupant.Role.Participant; + mucPayload.addItem(myItem); + mucPayload.addStatusCode(new MUCUserPayload.StatusCode(303)); + stanza.addPayload(mucPayload); + channel.onPresenceReceived.emit(stanza); + assertEquals(1, nickChanges); + assertEquals(false, testling.hasOccupant("Alice")); + assertEquals(true, testling.hasOccupant("Alice2")); + + // We (Rabbit) change nick to Robot + stanza = new Presence(); + stanza.setFrom(new JID("foo@bar.com/Rabbit")); + stanza.setTo(router.getJID()); + stanza.setType(Presence.Type.Unavailable); + mucPayload = new MUCUserPayload(); + myItem.affiliation = MUCOccupant.Affiliation.Member; + myItem.nick = "Robot"; + myItem.role = MUCOccupant.Role.Participant; + mucPayload.addItem(myItem); + mucPayload.addStatusCode(new MUCUserPayload.StatusCode(303)); + stanza.addPayload(mucPayload); + channel.onPresenceReceived.emit(stanza); + assertEquals(2, nickChanges); + assertEquals(false, testling.hasOccupant("Rabbit")); + assertEquals(true, testling.hasOccupant("Robot")); + } + + /*void testJoin_Success() { + MUC::ref testling = createMUC(JID("foo@bar.com")); + testling.onJoinFinished.connect(boost::bind(&MUCTest::handleJoinFinished, this, _1, _2)); + testling.joinAs("Alice"); + receivePresence(JID("foo@bar.com/Rabbit"), "Here"); + + CPPUNIT_ASSERT_EQUAL(1, static_cast(joinResults.size())); + CPPUNIT_ASSERT_EQUAL(std::string("Alice"), joinResults[0].nick); + CPPUNIT_ASSERT(joinResults[0].error); + } + + void testJoin_Fail() { + //CPPUNIT_ASSERT(!mucRegistry.isMUC(JID("foo@bar.com"))); + }*/ private MUC createMUC(JID jid) { - return new MUC(channel, router, presenceSender, jid, mucRegistry); + MUC muc = new MUCImpl(channel, router, presenceSender, jid, mucRegistry); + muc.onOccupantNicknameChanged.connect(new Slot2() { + @Override + public void call(String s1, String s2) { + handleOccupantNicknameChanged(s1, s2); + } + }); + return muc; } private void handleJoinFinished(String nick, ErrorPayload error) { @@ -170,6 +272,13 @@ public class MUCTest { private void receivePresence(JID jid, String status) { Presence p = new Presence(status); p.setFrom(jid); + //MUCUserPayload mucUserPayload = new MUCUserPayload(); + //mucUserPayload.addItem(item); + //p.addPayload(mucUserPayload); channel.onPresenceReceived.emit(p); } + + private void handleOccupantNicknameChanged(final String s1, final String s2) { + nickChanges++; + } } diff --git a/test/com/isode/stroke/muc/MockMUC.java b/test/com/isode/stroke/muc/MockMUC.java new file mode 100644 index 0000000..1b695d2 --- /dev/null +++ b/test/com/isode/stroke/muc/MockMUC.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2013 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.muc; + +import java.util.Collections; +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.CapsInfo; +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.Signal; +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; + +public class MockMUC extends MUC { + + private JID ownMUCJID = new JID(); + private Map occupants_ = new HashMap(); + + public MockMUC(JID muc) { + ownMUCJID = muc; + } + + /** + * Cause a user to appear to have entered the room. For testing only. + */ + public void insertOccupant(final MUCOccupant occupant) { + occupants_.put(occupant.getNick(), occupant); + onOccupantJoined.emit(occupant); + } + + /** + * Returns the (bare) JID of the MUC. + */ + public JID getJID() { + return ownMUCJID.toBare(); + } + + /** + * Returns if the room is unlocked and other people can join the room. + * @return True if joinable by others; false otherwise. + */ + public boolean isUnlocked() { + return true; + } + + public void joinAs(final String nick) {} + public void joinWithContextSince(final String nick, final Date since) {} + /*public void queryRoomInfo(); */ + /*public void queryRoomItems(); */ + /*public String getCurrentNick(); */ + public Map getOccupants() { + return occupants_; + } + + public void changeNickname(final String newNickname) {} + public void part() {} + /*public void handleIncomingMessage(Message::ref message); */ + /** Expose public so it can be called when e.g. user goes offline */ + public void handleUserLeft(LeavingType l) {} + + /** + * Get occupant information. + */ + public MUCOccupant getOccupant(final String nick) { + return occupants_.get(nick); + } + public boolean hasOccupant(final String nick){ + return occupants_.containsKey(nick); + } + + public void kickOccupant(final JID jid) {} + + public void changeOccupantRole(final JID jid, MUCOccupant.Role newRole) { + String resource = jid.getResource(); + if(occupants_.containsKey(resource)) { + MUCOccupant old = occupants_.get(resource); + occupants_.remove(resource); + occupants_.put(resource, new MUCOccupant(old.getNick(), newRole, old.getAffiliation())); + onOccupantRoleChanged.emit(resource, occupants_.get(resource), old.getRole()); + } + } + + public void requestAffiliationList(MUCOccupant.Affiliation aff) {} + + public void changeAffiliation(final JID jid, MUCOccupant.Affiliation newAffilation) { + String resource = jid.getResource(); + if(occupants_.containsKey(resource)) { + MUCOccupant old = occupants_.get(resource); + occupants_.remove(resource); + occupants_.put(resource, new MUCOccupant(old.getNick(), old.getRole(), newAffilation)); + onOccupantAffiliationChanged.emit(resource, newAffilation, old.getAffiliation()); + } + } + + public void changeSubject(final String subject) {} + public void requestConfigurationForm() {} + public void configureRoom(Form f) {} + public void cancelConfigureRoom() {} + public void destroyRoom() {} + /** Send an invite for the person to join the MUC */ + public void invitePerson(final JID person, final String reason, boolean isImpromptu, boolean isReuseChat) {} + public void setCreateAsReservedIfNew() {} + public void setPassword(final String password) {} + + protected boolean isFromMUC(final JID j) { + return (ownMUCJID.compare(j, JID.CompareType.WithoutResource) == 0); + } + + protected String getOwnNick() { + return ownMUCJID.getResource(); + } +} \ No newline at end of file -- cgit v0.10.2-6-g49f6