/* * Jicofo, the Jitsi Conference Focus. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.jitsi.impl.protocol.xmpp; import net.java.sip.communicator.impl.protocol.jabber.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.Message; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.Logger; import org.jitsi.protocol.xmpp.*; import org.jivesoftware.smack.*; import org.jivesoftware.smack.packet.*; import org.jivesoftware.smackx.*; import org.jivesoftware.smackx.muc.*; import org.jivesoftware.smackx.packet.*; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.muc.MUCRoom; import java.util.*; import java.util.concurrent.*; /** * Stripped implementation of <tt>ChatRoom</tt> using Smack library. * * @author Pawel Domas */ public class ChatRoomImpl extends AbstractChatRoom { /** * The logger used by this class. */ private final static Logger logger = Logger.getLogger(ChatRoomImpl.class); /** * Parent MUC operation set. */ private final OperationSetMultiUserChatImpl opSet; /** * Chat room name. */ private final String roomName; /** * Openfire MUCRoom that is hosting Jitsi Meet conference. BAO */ private MUCRoom room = null; /** * Smack multi user chat backend instance. */ private MultiUserChat muc; /** * Our nickname. */ private String myNickName; /** * Member presence listeners. */ private CopyOnWriteArrayList<ChatRoomMemberPresenceListener> listeners = new CopyOnWriteArrayList<ChatRoomMemberPresenceListener>(); /** * Local user role listeners. */ private CopyOnWriteArrayList<ChatRoomLocalUserRoleListener> localUserRoleListeners = new CopyOnWriteArrayList<ChatRoomLocalUserRoleListener>(); /** * HACK: * We need to have participant presence received, before firing * participant "joined" event to know MUC participant real JID from the * start. However Smack seems to be unpredictable(or XMPP server - not * sure) on the order of "member joined" and "presence packet" events. * So if "member joined" is fired before presence then we cache * participant in {@link #earlyParticipant}. If opposite order takes * place that is presence is received before "member joined" event * we cache Presence in <tt>onJoinPresence</tt>. {@link * ChatRoomMemberPresenceChangeEvent#MEMBER_JOINED} is fired when we have * first Presence packet and Smack "member joined" event has been fired. */ private final Map<String, Presence> onJoinPresence = new HashMap<String,Presence>(); private final List<String> earlyParticipant = new ArrayList<String>(); /** * Nickname to member impl class map. */ private final Map<String, ChatMemberImpl> members = new HashMap<String, ChatMemberImpl>(); /** * Local user role. */ private ChatRoomMemberRole role; /** * Stores our last MUC presence packet for future update. */ private Presence lastPresenceSent; private void joinAs(String nickname, String password) throws OperationFailedException { try { muc.addPresenceInterceptor(new PacketInterceptor() { @Override public void interceptPacket(Packet packet) { if (packet instanceof Presence) { lastPresenceSent = (Presence) packet; } } }); String roomPassword = null; if (room != null && room.isPasswordProtected()) { roomPassword = room.getPassword(); } logger.info("joinAs " + nickname + " " + password + " " + roomPassword); try { if (password == null) { if (roomPassword != null) muc.join(nickname, roomPassword); else muc.join(nickname); } else muc.join(nickname, password); } catch (Exception e) { muc.create(nickname); // BAO } this.myNickName = nickname; // Make the room non-anonymous, so that others can // recognize focus JID Form config = muc.getConfigurationForm(); /*Iterator<FormField> fields = config.getFields(); while (fields.hasNext()) { FormField field = fields.next(); logger.info("FORM: " + field.toXML()); }*/ Form answer = config.createAnswerForm(); // Room non-anonymous FormField whois = new FormField("muc#roomconfig_whois"); whois.addValue("anyone"); answer.addField(whois); // Room moderated //FormField roomModerated // = new FormField("muc#roomconfig_moderatedroom"); //roomModerated.addValue("true"); //answer.addField(roomModerated); // Only participants can send private messages //FormField onlyParticipantsPm // = new FormField("muc#roomconfig_allowpm"); //onlyParticipantsPm.addValue("participants"); //answer.addField(onlyParticipantsPm); // Presence broadcast //FormField presenceBroadcast // = new FormField("muc#roomconfig_presencebroadcast"); //presenceBroadcast.addValue("participant"); //answer.addField(presenceBroadcast); // Get member list //FormField getMemberList // = new FormField("muc#roomconfig_getmemberlist"); //getMemberList.addValue("participant"); //answer.addField(getMemberList); // Public logging //FormField publicLogging // = new FormField("muc#roomconfig_enablelogging"); //publicLogging.addValue("false"); //answer.addField(publicLogging); muc.sendConfigurationForm(answer); } catch (XMPPException e) { throw new OperationFailedException( "Failed to join the room", OperationFailedException.GENERAL_ERROR, e); } } /** * Creates new instance of <tt>ChatRoomImpl</tt>. * * @param parentChatOperationSet parent multi user chat operation set. * @param roomName the name of the chat room that will be handled by * new <tt>ChatRoomImpl</tt>instance. */ public ChatRoomImpl(OperationSetMultiUserChatImpl parentChatOperationSet, String roomName) { this.opSet = parentChatOperationSet; this.roomName = roomName; muc = new MultiUserChat( parentChatOperationSet.getConnection(), roomName); muc.addParticipantStatusListener(new MemberListener()); muc.addParticipantListener(new ParticipantListener()); String roomId = roomName; int pos = roomId.indexOf("@"); if (pos > -1) roomId = roomId.substring(0, pos); this.room = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService("conference").getChatRoom(roomId); } @Override public String getName() { return roomName; } @Override public String getIdentifier() { return null; } @Override public void join() throws OperationFailedException { logger.info("before join as"); joinAs(getParentProvider().getAccountID().getAccountDisplayName()); logger.info("after join as"); } @Override public void join(byte[] password) throws OperationFailedException { try { String pass = new String(password, "UTF-8"); joinAs(getParentProvider().getAccountID().getAccountDisplayName(), pass); } catch (Exception e) { } } public void join(String password) { try { joinAs(getParentProvider().getAccountID().getAccountDisplayName(), password); } catch (Exception e) { } } @Override public void joinAs(String nickname, byte[] password) throws OperationFailedException { try { String pass = new String(password, "UTF-8"); joinAs(nickname, pass); } catch (Exception e) { } } @Override public void joinAs(String nickname) throws OperationFailedException { joinAs(nickname, (String)null); } @Override public boolean isJoined() { return muc.isJoined(); } @Override public void leave() { muc.leave(); } @Override public String getSubject() { return muc.getSubject(); } @Override public void setSubject(String subject) throws OperationFailedException { } @Override public String getUserNickname() { return myNickName; } @Override public ChatRoomMemberRole getUserRole() { if(this.role == null) { Occupant o = muc.getOccupant( muc.getRoom() + "/" + muc.getNickname()); if(o == null) return ChatRoomMemberRole.GUEST; else this.role = ChatRoomJabberImpl.smackRoleToScRole( o.getRole(), o.getAffiliation()); } return this.role; } /** * Resets cached role instance so that it will be refreshed when {@link * #getUserRole()} is called. */ private void resetCachedUserRole() { role = null; } /** * Resets cached role instance for given participant. * @param participant full mucJID of the participant for whom we want to * reset cached role instance. */ private void resetRoleForParticipant(String participant) { if (participant.endsWith("/" + myNickName)) { resetCachedUserRole(); } else { ChatMemberImpl member = members.get(participant); if (member != null) { member.resetCachedRole(); } else { logger.error( "Role reset for: " + participant + " who does not exist"); } } } @Override public void setLocalUserRole(ChatRoomMemberRole role) throws OperationFailedException { // Method not used but log error just in case to spare debugging logger.error("setLocalUserRole not implemented"); } /** * Creates the corresponding ChatRoomLocalUserRoleChangeEvent and notifies * all <tt>ChatRoomLocalUserRoleListener</tt>s that local user's role has * been changed in this <tt>ChatRoom</tt>. * * @param previousRole the previous role that local user had * @param newRole the new role the local user gets * @param isInitial if <tt>true</tt> this is initial role set. */ private void fireLocalUserRoleEvent(ChatRoomMemberRole previousRole, ChatRoomMemberRole newRole, boolean isInitial) { ChatRoomLocalUserRoleChangeEvent evt = new ChatRoomLocalUserRoleChangeEvent( this, previousRole, newRole, isInitial); if (logger.isTraceEnabled()) logger.trace("Will dispatch the following ChatRoom event: " + evt); for (ChatRoomLocalUserRoleListener listener : localUserRoleListeners) listener.localUserRoleChanged(evt); } /** * Sets the new role for the local user in the context of this chat room. * * @param role the new role to be set for the local user * @param isInitial if <tt>true</tt> this is initial role set. */ public void setLocalUserRole(ChatRoomMemberRole role, boolean isInitial) { fireLocalUserRoleEvent(getUserRole(), role, isInitial); this.role = role; } @Override public void setUserNickname(String nickname) throws OperationFailedException { } @Override public void addMemberPresenceListener( ChatRoomMemberPresenceListener listener) { listeners.add(listener); } @Override public void removeMemberPresenceListener( ChatRoomMemberPresenceListener listener) { listeners.remove(listener); } @Override public void addLocalUserRoleListener(ChatRoomLocalUserRoleListener listener) { localUserRoleListeners.add(listener); } @Override public void removelocalUserRoleListener( ChatRoomLocalUserRoleListener listener) { localUserRoleListeners.remove(listener); } @Override public void addMemberRoleListener(ChatRoomMemberRoleListener listener) { } @Override public void removeMemberRoleListener(ChatRoomMemberRoleListener listener) { } @Override public void addPropertyChangeListener( ChatRoomPropertyChangeListener listener) { } @Override public void removePropertyChangeListener( ChatRoomPropertyChangeListener listener) { } @Override public void addMemberPropertyChangeListener( ChatRoomMemberPropertyChangeListener listener) { } @Override public void removeMemberPropertyChangeListener( ChatRoomMemberPropertyChangeListener listener) { } @Override public void invite(String userAddress, String reason) { } @Override public List<ChatRoomMember> getMembers() { return new ArrayList<ChatRoomMember>(members.values()); } @Override public int getMembersCount() { return muc.getOccupantsCount(); } @Override public void addMessageListener(ChatRoomMessageListener listener) { } @Override public void removeMessageListener(ChatRoomMessageListener listener) { } @Override public Message createMessage(byte[] content, String contentType, String contentEncoding, String subject) { return null; } @Override public Message createMessage(String messageText) { return null; } @Override public void sendMessage(Message message) throws OperationFailedException { } @Override public ProtocolProviderService getParentProvider() { return opSet.getProtocolProvider(); } @Override public Iterator<ChatRoomMember> getBanList() throws OperationFailedException { return null; } @Override public void banParticipant(ChatRoomMember chatRoomMember, String reason) throws OperationFailedException { } @Override public void kickParticipant(ChatRoomMember chatRoomMember, String reason) throws OperationFailedException { } @Override public ChatRoomConfigurationForm getConfigurationForm() throws OperationFailedException { return null; } @Override public boolean isSystem() { return false; } @Override public boolean isPersistent() { return false; } @Override public Contact getPrivateContactByNickname(String name) { return null; } @Override public void grantAdmin(String address) { try { muc.grantAdmin(address); } catch (XMPPException e) { throw new RuntimeException(e); } } @Override public void grantMembership(String address) { try { muc.grantMembership(address); } catch (XMPPException e) { throw new RuntimeException(e); } } @Override public void grantModerator(String nickname) { try { muc.grantModerator(nickname); } catch (XMPPException e) { throw new RuntimeException(e); } } @Override public void grantOwnership(String address) { logger.info("Grant owner to " + address); // Have to construct the IQ manually as Smack version used here seems // to be using wrong namespace(muc#owner instead of muc#admin) // which does not work with the Prosody. MUCAdmin admin = new MUCAdmin(); admin.setType(IQ.Type.SET); admin.setTo(roomName); MUCAdmin.Item item = new MUCAdmin.Item("owner", null); item.setJid(address); admin.addItem(item); XmppProtocolProvider provider = (XmppProtocolProvider) getParentProvider(); XmppConnection connection = provider.getConnectionAdapter(); IQ reply = (IQ) connection.sendPacketAndGetReply(admin); if (reply.getType() != IQ.Type.RESULT) { // FIXME: we should have checked exceptions for all operations in // ChatRoom interface which are expected to fail. // OperationFailedException maybe ? throw new RuntimeException( "Failed to grant owner: " + reply.getError()); } } @Override public void grantVoice(String nickname) { } @Override public void revokeAdmin(String address) { } @Override public void revokeMembership(String address) { } @Override public void revokeModerator(String nickname) { } @Override public void revokeOwnership(String address) { } @Override public void revokeVoice(String nickname) { } @Override public ConferenceDescription publishConference(ConferenceDescription cd, String name) { return null; } @Override public void updatePrivateContactPresenceStatus(String nickname) { } @Override public void updatePrivateContactPresenceStatus(Contact contact) { } @Override public boolean destroy(String reason, String alternateAddress) { try { muc.destroy(reason, alternateAddress); } catch (XMPPException e) { //FIXME: should not be runtime, but OperationFailed and included in // interface signature(see also other methods catching XMPPException // in this class) throw new RuntimeException(e); } return false; } @Override public List<String> getMembersWhiteList() { return null; } @Override public void setMembersWhiteList(List<String> members) { } private void notifyParticipantJoined(ChatMemberImpl member) { ChatRoomMemberPresenceChangeEvent event = new ChatRoomMemberPresenceChangeEvent( this, member, ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED, null); for (ChatRoomMemberPresenceListener l : listeners) { l.memberPresenceChanged(event); } } private void notifyParticipantLeft(ChatMemberImpl member) { ChatRoomMemberPresenceChangeEvent event = new ChatRoomMemberPresenceChangeEvent( this, member, ChatRoomMemberPresenceChangeEvent.MEMBER_LEFT, null); for (ChatRoomMemberPresenceListener l : listeners) { l.memberPresenceChanged(event); } } private void notifyParticipantKicked(ChatMemberImpl member) { ChatRoomMemberPresenceChangeEvent event = new ChatRoomMemberPresenceChangeEvent( this, member, ChatRoomMemberPresenceChangeEvent.MEMBER_KICKED, null); for (ChatRoomMemberPresenceListener l : listeners) { l.memberPresenceChanged(event); } } public Occupant getOccupant(ChatMemberImpl chatMemeber) { return muc.getOccupant(chatMemeber.getContactAddress()); } /** * Returns the MUCUser packet extension included in the packet or * <tt>null</tt> if none. * * @param packet the packet that may include the MUCUser extension. * @return the MUCUser found in the packet. */ private MUCUser getMUCUserExtension(Packet packet) { if (packet != null) { // Get the MUC User extension return (MUCUser) packet.getExtension( "x", "http://jabber.org/protocol/muc#user"); } return null; } public void sendPresenceExtension(PacketExtension extension) { if (lastPresenceSent == null) { logger.error("No presence packet obtained yet"); return; } XmppProtocolProvider xmppProtocolProvider = (XmppProtocolProvider) getParentProvider(); // Remove old PacketExtension old = lastPresenceSent.getExtension( extension.getElementName(), extension.getNamespace()); if (old != null) { lastPresenceSent.removeExtension(old); } // Add new lastPresenceSent.addExtension(extension); XmppConnection connection = xmppProtocolProvider.getConnectionAdapter(); if (connection == null) { logger.error("Failed to send presence extension - no connection"); return; } connection.sendPacket(lastPresenceSent); } private ChatMemberImpl addMember(String participant) { ChatMemberImpl newMember; synchronized (members) { if (members.containsKey(participant)) { logger.error(participant + " already in " + roomName); return null; } newMember = new ChatMemberImpl(participant, ChatRoomImpl.this); members.put(participant, newMember); } return newMember; } class MemberListener implements ParticipantStatusListener { @Override public void joined(String participant) { //logger.info(Thread.currentThread()+"JOINED ROOM: "+participant); Presence peerPresence = onJoinPresence.get(participant); if (peerPresence == null) { earlyParticipant.add(participant); return; } onJoinPresence.remove(participant); ChatMemberImpl member = addMember(participant); if (member != null) { member.processPresence(peerPresence); notifyParticipantJoined(member); } } private ChatMemberImpl removeMember(String participant) { ChatMemberImpl removed = members.remove(participant); if (removed == null) logger.error(participant + " not in " + roomName); return removed; } @Override public void left(String participant) { ChatMemberImpl member; synchronized (members) { member = removeMember(participant); } if (member != null) { notifyParticipantLeft(member); } } @Override public void kicked(String participant, String s2, String s3) { if (logger.isTraceEnabled()) logger.trace("Kicked: " + participant + ", " + s2 +", " + s3); ChatMemberImpl member; synchronized (members) { member = removeMember(participant); } if (member == null) { logger.error( "Kicked participant does not exist: " + participant); return; } notifyParticipantKicked(member); } @Override public void voiceGranted(String s) { if (logger.isTraceEnabled()) logger.trace("Voice granted: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void voiceRevoked(String s) { if (logger.isTraceEnabled()) logger.trace("Voice revoked: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void banned(String s, String s2, String s3) { if (logger.isTraceEnabled()) logger.trace("Banned: " + s + ", " + s2 + ", " + s3); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void membershipGranted(String s) { if (logger.isTraceEnabled()) logger.trace("Membership granted: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void membershipRevoked(String s) { if (logger.isTraceEnabled()) logger.trace("Membership revoked: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void moderatorGranted(String s) { if (logger.isTraceEnabled()) logger.trace("Moderator granted: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void moderatorRevoked(String s) { if (logger.isTraceEnabled()) logger.trace("Moderator revoked: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void ownershipGranted(String s) { if (logger.isTraceEnabled()) logger.trace("Ownership granted: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void ownershipRevoked(String s) { if (logger.isTraceEnabled()) logger.trace("Ownership revoked: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void adminGranted(String s) { if (logger.isTraceEnabled()) logger.trace("Admin granted: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void adminRevoked(String s) { if (logger.isTraceEnabled()) logger.trace("Admin revoked: " + s); // We do not fire events - not required for now resetRoleForParticipant(s); } @Override public void nicknameChanged(String oldNickname, String newNickname) { logger.error("nicknameChanged - NOT IMPLEMENTED"); /*synchronized (members) { removeMember(oldNickname); addMember(newNickname); }*/ } } class ParticipantListener implements PacketListener { /** * Processes an incoming presence packet. * @param packet the incoming packet. */ @Override public void processPacket(Packet packet) { if (packet == null || !(packet instanceof Presence) || packet.getError() != null) { logger.warn("Unable to handle packet: " + packet); return; } Presence presence = (Presence) packet; String ourOccupantJid = muc.getRoom() + "/" + muc.getNickname(); if (logger.isDebugEnabled()) { logger.debug("Presence received " + presence.toXML()); } if (ourOccupantJid.equals(presence.getFrom())) processOwnPresence(presence); else processOtherPresence(presence); } /** * Processes a <tt>Presence</tt> packet addressed to our own occupant * JID. * @param presence the packet to process. */ private void processOwnPresence(Presence presence) { MUCUser mucUser = getMUCUserExtension(presence); if (mucUser != null) { String affiliation = mucUser.getItem().getAffiliation(); String role = mucUser.getItem().getRole(); // this is the presence for our member initial role and // affiliation, as smack do not fire any initial // events lets check it and fire events ChatRoomMemberRole jitsiRole = ChatRoomJabberImpl.smackRoleToScRole(role, affiliation); /*if(jitsiRole == ChatRoomMemberRole.MODERATOR || jitsiRole == ChatRoomMemberRole.OWNER || jitsiRole == ChatRoomMemberRole.ADMINISTRATOR) {*/ setLocalUserRole(jitsiRole, true); //} /*if(!presence.isAvailable() && "none".equalsIgnoreCase(affiliation) && "none".equalsIgnoreCase(role)) { MUCUser.Destroy destroy = mucUser.getDestroy(); if(destroy == null) { // the room is unavailable to us, there is no // message we will just leave leave(); } else { leave(destroy.getReason(), destroy.getJid()); } }*/ } } /** * Process a <tt>Presence</tt> packet sent by one of the other room * occupants. */ private void processOtherPresence(Presence presence) { String participant = presence.getFrom(); ChatMemberImpl member = members.get(presence.getFrom()); if (member == null) { logger.warn( "Received presence for non-existing member: " + presence.toXML()); if (earlyParticipant.contains(participant)) { earlyParticipant.remove(participant); member = addMember(participant); if (member != null) { member.processPresence(presence); notifyParticipantJoined(member); } } else { onJoinPresence.put(presence.getFrom(), presence); } return; } member.processPresence(presence); } } }