/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.protocol.irc; import java.io.*; import java.util.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; /** * Implementation of support for Persistent Presence for IRC. * * @author Danny van Heumen */ public class OperationSetPersistentPresenceIrcImpl extends AbstractOperationSetPersistentPresence<ProtocolProviderServiceIrcImpl> { /** * Logger. */ private static final Logger LOGGER = Logger .getLogger(OperationSetPersistentPresenceIrcImpl.class); /** * Root contact group for IRC contacts. */ private final ContactGroupIrcImpl rootGroup = new ContactGroupIrcImpl( this.parentProvider); /** * IRC implementation for OperationSetPersistentPresence. * * @param parentProvider IRC instance of protocol provider service. */ protected OperationSetPersistentPresenceIrcImpl( final ProtocolProviderServiceIrcImpl parentProvider) { super(parentProvider); } /** * Create a volatile contact. * * @param id contact id * @return returns instance of volatile contact */ private ContactIrcImpl createVolatileContact(final String id) { // Get non-persistent group for volatile contacts. ContactGroupIrcImpl volatileGroup = getNonPersistentGroup(); // Create volatile contact ContactIrcImpl newVolatileContact = new ContactIrcImpl(this.parentProvider, id, volatileGroup, IrcStatusEnum.ONLINE); volatileGroup.addContact(newVolatileContact); // Add nick to watch list of presence manager. final IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); if (connection != null) { // FIXME create private method that decides between adding to // persistent context directly or adding via presence manager connection.getPresenceManager().addNickWatch(id); } this.fireSubscriptionEvent(newVolatileContact, volatileGroup, SubscriptionEvent.SUBSCRIPTION_CREATED); return newVolatileContact; } /** * Get group for non-persistent contacts. * * @return returns group instance */ private ContactGroupIrcImpl getNonPersistentGroup() { String groupName = IrcActivator.getResources().getI18NString( "service.gui.NOT_IN_CONTACT_LIST_GROUP_NAME"); for (int i = 0; i < getRootGroup().countSubgroups(); i++) { ContactGroupIrcImpl gr = (ContactGroupIrcImpl) getRootGroup().getGroup(i); if (!gr.isPersistent() && gr.getGroupName().equals(groupName)) { return gr; } } ContactGroupIrcImpl volatileGroup = new ContactGroupIrcImpl(this.parentProvider, this.rootGroup, groupName); volatileGroup.setPersistent(false); this.rootGroup.addSubGroup(volatileGroup); this.fireServerStoredGroupEvent(volatileGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT); return volatileGroup; } /** * Get root contact group. * * @return returns root contact group */ public ContactGroupIrcImpl getRootGroup() { return rootGroup; } /** * Subscribes to presence updates for specified contact identifier. * * @param contactIdentifier the contact's identifier * @throws IllegalArgumentException on bad input * @throws IllegalStateException if disconnected * @throws OperationFailedException on failed operation */ @Override public void subscribe(final String contactIdentifier) throws IllegalArgumentException, IllegalStateException, OperationFailedException { subscribe(this.rootGroup, contactIdentifier); } /** * Subscribes to presence updates for specified contact identifier. * * @param parent contact group * @param contactIdentifier contact * @throws OperationFailedException if not implemented * @throws IllegalArgumentException on bad input * @throws IllegalStateException if disconnected */ @Override public void subscribe(final ContactGroup parent, final String contactIdentifier) throws IllegalArgumentException, IllegalStateException, OperationFailedException { if (contactIdentifier == null || contactIdentifier.isEmpty()) { throw new IllegalArgumentException( "contactIdentifier cannot be null or empty"); } if (!(parent instanceof ContactGroupIrcImpl)) { throw new IllegalArgumentException( "parent group must be an instance of ContactGroupIrcImpl"); } final ContactGroupIrcImpl contactGroup = (ContactGroupIrcImpl) parent; final IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); if (connection == null) { throw new IllegalStateException("not currently connected"); } // TODO show some kind of confirmation dialog before adding a contact, // since contacts in IRC are not always authenticated. // TODO verify id with IdentityService (future) to ensure that user is // authenticated before adding it (ACC 3: user is logged in, ACC 0: user // does not exist, ACC 1: account exists but user is not logged in) final ContactIrcImpl newContact = new ContactIrcImpl(this.parentProvider, contactIdentifier, contactGroup, IrcStatusEnum.OFFLINE); try { contactGroup.addContact(newContact); connection.getPresenceManager().addNickWatch(contactIdentifier); fireSubscriptionEvent(newContact, contactGroup, SubscriptionEvent.SUBSCRIPTION_CREATED); } catch (RuntimeException e) { LOGGER.debug("Failed to subscribe to contact.", e); fireSubscriptionEvent(newContact, contactGroup, SubscriptionEvent.SUBSCRIPTION_FAILED); } } /** * Unsubscribe for presence change events for specified contact. * * @param contact contact instance * @throws IllegalArgumentException on bad input * @throws IllegalStateException if disconnected * @throws OperationFailedException if something went wrong */ @Override public void unsubscribe(final Contact contact) throws IllegalArgumentException, IllegalStateException, OperationFailedException { if (!(contact instanceof ContactIrcImpl)) { throw new IllegalArgumentException( "contact must be instance of ContactIrcImpl"); } final ContactIrcImpl ircContact = (ContactIrcImpl) contact; final ContactGroupIrcImpl parentGroup = (ContactGroupIrcImpl) ircContact.getParentContactGroup(); try { final IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); if (connection != null) { connection.getPresenceManager().removeNickWatch( contact.getAddress()); } parentGroup.removeContact(ircContact); fireSubscriptionEvent(ircContact, parentGroup, SubscriptionEvent.SUBSCRIPTION_REMOVED); } catch (RuntimeException e) { LOGGER.debug("Failed to unsubscribe from contact.", e); fireSubscriptionEvent(ircContact, parentGroup, SubscriptionEvent.SUBSCRIPTION_FAILED); } } /** * Create a "server stored" contact group. (Which is not actually server * stored, but close enough ...) * * @param parent parent contact group * @param groupName new group's name * @throws OperationFailedException if not implemented */ @Override public void createServerStoredContactGroup(final ContactGroup parent, final String groupName) throws OperationFailedException { LOGGER.trace("createServerStoredContactGroup(...) called"); if (!(parent instanceof ContactGroupIrcImpl)) { throw new IllegalArgumentException( "parent is not an instance of ContactGroupIrcImpl"); } if (groupName == null || groupName.isEmpty()) { throw new IllegalArgumentException( "groupName cannot be null or empty"); } final ContactGroupIrcImpl parentGroup = (ContactGroupIrcImpl) parent; final ContactGroupIrcImpl newGroup = new ContactGroupIrcImpl(this.parentProvider, parentGroup, groupName); parentGroup.addSubGroup(newGroup); fireServerStoredGroupEvent(newGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT); } /** * Removing a contact group is currently not implemented. * * @param group contact group to remove * @throws OperationFailedException if not implemented */ @Override public void removeServerStoredContactGroup(final ContactGroup group) throws OperationFailedException { LOGGER.trace("removeServerStoredContactGroup called"); if (!(group instanceof ContactGroupIrcImpl)) { throw new IllegalArgumentException( "group must be an instance of ContactGroupIrcImpl"); } final ContactGroupIrcImpl ircGroup = (ContactGroupIrcImpl) group; ((ContactGroupIrcImpl) ircGroup.getParentContactGroup()) .removeSubGroup(ircGroup); fireServerStoredGroupEvent(ircGroup, ServerStoredGroupEvent.GROUP_REMOVED_EVENT); } /** * Rename contact group. * * @param group contact group to rename * @param newName new name */ @Override public void renameServerStoredContactGroup(final ContactGroup group, final String newName) { LOGGER.trace("renameServerStoredContactGroup called"); ((ContactGroupIrcImpl) group).setGroupName(newName); } /** * Moving contacts to a different group is currently not implemented. * * @param contactToMove contact to move * @param newParent new parent group * @throws OperationFailedException if not implemented */ @Override public void moveContactToGroup(final Contact contactToMove, final ContactGroup newParent) throws OperationFailedException { LOGGER.trace("moveContactToGroup called"); if (!(contactToMove instanceof ContactIrcImpl)) { throw new IllegalArgumentException( "contactToMove must be an instance of ContactIrcImpl"); } final ContactIrcImpl contact = (ContactIrcImpl) contactToMove; // remove contact from old parent contact group ((ContactGroupIrcImpl) contact.getParentContactGroup()) .removeContact(contact); // add contact to new parent contact group final ContactGroupIrcImpl newGroup = (ContactGroupIrcImpl) newParent; newGroup.addContact(contact); // update parent contact group in contact contact.setParentContactGroup(newGroup); } /** * Get group of contacts that have been discovered while using IRC. * * @return returns root contact group */ @Override public ContactGroup getServerStoredContactListRoot() { return this.rootGroup; } /** * Creates an unresolved contact for IRC. * * @param address contact address * @param persistentData persistent data for contact * @return returns newly created unresolved contact instance */ @Override public ContactIrcImpl createUnresolvedContact(final String address, final String persistentData) { return createUnresolvedContact(address, persistentData, this.rootGroup); } /** * Creates an unresolved contact for IRC. * * @param address contact address * @param persistentData persistent data for contact * @param parentGroup parent group to contact * @return returns newly created unresolved contact instance */ @Override public ContactIrcImpl createUnresolvedContact(final String address, final String persistentData, final ContactGroup parentGroup) { // FIXME actually make this thing unresolved until the first presence // update is received? if (address == null || address.isEmpty()) { throw new IllegalArgumentException( "address cannot be null or empty"); } if (!(parentGroup instanceof ContactGroupIrcImpl)) { throw new IllegalArgumentException( "Provided contact group is not an IRC contact group instance."); } final ContactGroupIrcImpl group = (ContactGroupIrcImpl) parentGroup; final ContactIrcImpl unresolvedContact = new ContactIrcImpl(this.parentProvider, address, (ContactGroupIrcImpl) parentGroup, IrcStatusEnum.OFFLINE); group.addContact(unresolvedContact); this.parentProvider.getIrcStack().getContext().nickWatchList .add(address); return unresolvedContact; } /** * Create a new unresolved contact group. * * @param groupUID unique group id * @param persistentData persistent data is currently not supported * @param parentGroup the parent group for the newly created contact group * @return returns new unresolved contact group */ @Override public ContactGroupIrcImpl createUnresolvedContactGroup( final String groupUID, final String persistentData, final ContactGroup parentGroup) { if (!(parentGroup instanceof ContactGroupIrcImpl)) { throw new IllegalArgumentException( "parentGroup is not a ContactGroupIrcImpl instance"); } final ContactGroupIrcImpl unresolvedGroup = new ContactGroupIrcImpl(this.parentProvider, (ContactGroupIrcImpl) parentGroup, groupUID); ((ContactGroupIrcImpl) parentGroup).addSubGroup(unresolvedGroup); return unresolvedGroup; } /** * Get current IRC presence status. * * The presence status currently is ONLINE or AWAY if we are connected or * OFFLINE if we aren't connected. The status is set to AWAY if an away * message is set. * * @return returns status ONLINE if connected and not away, or AWAY if * connected and an away message is set, or OFFLINE if not connected * at all */ @Override public PresenceStatus getPresenceStatus() { final IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); if (connection != null && connection.isConnected()) { return connection.getPresenceManager().isAway() ? IrcStatusEnum.AWAY : IrcStatusEnum.ONLINE; } else { return IrcStatusEnum.OFFLINE; } } /** * Set a new presence status corresponding to the provided arguments. * * @param status presence status * @param statusMessage message for the specified status */ @Override public void publishPresenceStatus(final PresenceStatus status, final String statusMessage) throws IllegalArgumentException, IllegalStateException, OperationFailedException { final IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); String message = statusMessage; if (connection == null) { throw new IllegalStateException("Connection is not available."); } if (message != null && message.isEmpty()) { // if we provide a message, make sure it isn't empty message = null; } if (status.getStatus() >= IrcStatusEnum.AVAILABLE_THRESHOLD) { connection.getPresenceManager().away(false, message); } else if (status.getStatus() >= IrcStatusEnum.AWAY_THRESHOLD) { connection.getPresenceManager().away(true, message); } } /** * Update (from IRC) containing the current presence status and message. * * @param previousStatus the previous presence status * @param status the current presence status */ void updatePresenceStatus(final PresenceStatus previousStatus, final PresenceStatus status) { // Note: Currently uses general PresenceStatus type parameters because // EasyMock throws a java.lang.NoClassDefFoundError: Could not // initialize class // net.java.sip.communicator.impl.protocol.irc. // OperationSetPersistentPresenceIrcImpl$$EnhancerByCGLIB$$403085ac // if IrcStatusEnum is used. I'm not sure why, though ... fireProviderStatusChangeEvent(previousStatus, status); } /** * Get set of statuses supported in IRC. * * @return returns iterator for supported statuses */ @Override public Iterator<PresenceStatus> getSupportedStatusSet() { final HashSet<PresenceStatus> statuses = new HashSet<PresenceStatus>(); final Iterator<IrcStatusEnum> supported = IrcStatusEnum.supportedStatusSet(); while (supported.hasNext()) { statuses.add(supported.next()); } return statuses.iterator(); } /** * Query contact status using WHOIS query to IRC server. * * @param contactIdentifier contact id * @return returns current presence status * @throws OperationFailedException in case of problems during query */ @Override public PresenceStatus queryContactStatus(final String contactIdentifier) throws IllegalArgumentException, IllegalStateException, OperationFailedException { final IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); if (connection == null) { throw new IllegalStateException("not connected"); } try { return connection.getPresenceManager().query(contactIdentifier); } catch (IOException e) { throw new OperationFailedException("Presence query failed.", OperationFailedException.NETWORK_FAILURE, e); } catch (InterruptedException e) { throw new OperationFailedException("Presence query interrupted.", OperationFailedException.GENERAL_ERROR, e); } } /** * Find a contact by its ID. * * @param contactID ID to look up * @return contact instance if found or null if nothing found */ @Override public ContactIrcImpl findContactByID(final String contactID) { return this.rootGroup.findContact(contactID); } /** * IRC does not support authorization handling, so this is not supported. * * @param handler authorization handler */ @Override public void setAuthorizationHandler(final AuthorizationHandler handler) { } /** * IRC will return the away message if AWAY status is active, or an empty * string if user is not away. * * @return returns empty string */ @Override public String getCurrentStatusMessage() { final IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); if (connection == null) { throw new IllegalStateException("Connection is not available."); } return connection.getPresenceManager().isAway() ? connection .getPresenceManager().getMessage() : ""; } /** * Find or create contact by ID. * * In IRC every chat room member is also a contact. Try to find a contact by * its ID. If a contact cannot be found, then create one. * * @param id id of the contact * @return returns instance of contact */ Contact findOrCreateContactByID(final String id) { Contact contact = findContactByID(id); if (contact == null) { contact = createVolatileContact(id); LOGGER.debug("No existing contact found. Created volatile contact" + " for nick name '" + id + "'."); } return contact; } /** * Update presence based for user's nick. * * @param nick the nick * @param newStatus the new status */ void updateNickContactPresence(final String nick, final PresenceStatus newStatus) { LOGGER.trace("Received presence update for nick '" + nick + "', status: " + newStatus.getStatus()); final Contact contact = findContactByID(nick); if (contact == null) { LOGGER.trace("null contact instance found: presence will not be " + "processed."); return; } if (!(contact instanceof ContactIrcImpl)) { throw new IllegalArgumentException( "Expected contact to be an IRC contact instance."); } final ContactIrcImpl contactIrc = (ContactIrcImpl) contact; final ContactGroup group = contact.getParentContactGroup(); final PresenceStatus previous = contactIrc.getPresenceStatus(); contactIrc.setPresenceStatus(newStatus); fireContactPresenceStatusChangeEvent(contact, group, previous); } /** * Update the nick/id for an IRC contact. * * @param oldNick the old nick * @param newNick the new nick */ void updateNick(final String oldNick, final String newNick) { ContactIrcImpl contact = findContactByID(oldNick); if (contact == null) { // Nick change is not meant for any known contact. Ignoring. return; } contact.setAddress(newNick); fireContactPropertyChangeEvent( ContactPropertyChangeEvent.PROPERTY_DISPLAY_NAME, contact, oldNick, newNick); } }