/*
* 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.icq;
import java.util.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.icqconstants.*;
import net.java.sip.communicator.util.*;
import net.kano.joscar.*;
import net.kano.joscar.flapcmd.*;
import net.kano.joscar.snac.*;
import net.kano.joscar.snaccmd.*;
import net.kano.joscar.snaccmd.conn.*;
import net.kano.joscar.snaccmd.error.*;
import net.kano.joscar.snaccmd.loc.*;
import net.kano.joustsim.*;
import net.kano.joustsim.oscar.*;
import net.kano.joustsim.oscar.oscar.service.bos.*;
import net.kano.joustsim.oscar.oscar.service.buddy.*;
import net.kano.joustsim.oscar.oscar.service.icon.*;
import net.kano.joustsim.oscar.oscar.service.info.*;
import net.kano.joustsim.oscar.oscar.service.ssi.*;
import net.kano.joustsim.trust.*;
/**
* The ICQ implementation of a Persistent Presence Operation set. This class
* manages our own presence status as well as subscriptions for the presence
* status of our buddies. It also offers methods for retrieving and modifying
* the buddy contact list and adding listeners for changes in its layout.
*
* @author Emil Ivov
*/
public class OperationSetPersistentPresenceIcqImpl
extends AbstractOperationSetPersistentPresence<ProtocolProviderServiceIcqImpl>
{
private static final Logger logger =
Logger.getLogger(OperationSetPersistentPresenceIcqImpl.class);
/**
* This one should actually be in joscar. But since it isn't we might as
* well define it here.
*/
private static final long ICQ_ONLINE_MASK = 0x01000000L;
/**
* The IcqContact representing the local protocol provider.
*/
private ContactIcqImpl localContact = null;
/**
* The listener that would react upon changes of the registration state of
* our provider
*/
private RegistrationStateListener registrationStateListener
= new RegistrationStateListener();
/**
* The listener that would receive joust sim status updates for budddies in
* our contact list
*/
private JoustSimBuddyServiceListener joustSimBuddySerListener
= new JoustSimBuddyServiceListener();
/**
* emcho: I think Bos stands for Buddy Online-status Service ... or at least
* it seems like a plausible translation. This listener follows changes
* in our own presence status and translates them in the corresponding
* protocol provider events.
*/
private JoustSimBosListener joustSimBosListener = new JoustSimBosListener();
/**
* Contains our current status message. Note that this field would only
* be changed once the server has confirmed the new status message and
* not immediately upon setting a new one..
*/
private String currentStatusMessage = "";
/**
* The presence status that we were last notified of etnering.
*/
private long currentIcqStatus = -1;
private AuthorizationHandler authorizationHandler = null;
private AuthListener authListener = new AuthListener();
/**
* The timer scheduling task that will query awaiting authorization
* contacts for their status
*/
private Timer presenceQueryTimer = null;
/**
* Interval between queries for awaiting authorization
* contact statuses
*/
private long PRESENCE_QUERY_INTERVAL = 120000l;
/**
* Used to request authorization when a user comes online
* and haven't granted one
*/
private OperationSetExtendedAuthorizationsIcqImpl opSetExtendedAuthorizations = null;
/**
* Buddies seen availabel
*/
private final List<String> buddiesSeenAvailable = new Vector<String>();
/**
* The array list we use when returning from the getSupportedStatusSet()
* method.
*/
private final List<PresenceStatus> supportedPresenceStatusSet = new ArrayList<PresenceStatus>();
/**
* A map containing bindings between SIP Communicator's icq presence status
* instances and ICQ status codes
*/
private static final Map<PresenceStatus, Long> scToIcqStatusMappings
= new Hashtable<PresenceStatus, Long>();
static{
scToIcqStatusMappings.put(IcqStatusEnum.AWAY,
FullUserInfo.ICQSTATUS_AWAY);
scToIcqStatusMappings.put(IcqStatusEnum.DO_NOT_DISTURB,
FullUserInfo.ICQSTATUS_DND);
scToIcqStatusMappings.put(IcqStatusEnum.FREE_FOR_CHAT,
FullUserInfo.ICQSTATUS_FFC);
scToIcqStatusMappings.put(IcqStatusEnum.INVISIBLE,
FullUserInfo.ICQSTATUS_INVISIBLE);
scToIcqStatusMappings.put(IcqStatusEnum.NOT_AVAILABLE,
FullUserInfo.ICQSTATUS_NA);
scToIcqStatusMappings.put(IcqStatusEnum.OCCUPIED,
FullUserInfo.ICQSTATUS_OCCUPIED);
scToIcqStatusMappings.put(IcqStatusEnum.ONLINE,
ICQ_ONLINE_MASK);
}
private static final Map<PresenceStatus, Long> scToAimStatusMappings
= new Hashtable<PresenceStatus, Long>();
static{
scToAimStatusMappings.put(AimStatusEnum.AWAY,
FullUserInfo.ICQSTATUS_AWAY);
scToAimStatusMappings.put(AimStatusEnum.INVISIBLE,
FullUserInfo.ICQSTATUS_INVISIBLE);
scToAimStatusMappings.put(AimStatusEnum.ONLINE,
ICQ_ONLINE_MASK);
}
/**
* The server stored contact list that will be encapsulating joustsim's
* buddy list.
*/
private ServerStoredContactListIcqImpl ssContactList = null;
/**
* Creates a new Presence OperationSet over the specified icq provider.
* @param icqProvider IcqProtocolProviderServiceImpl
* @param uin the UIN of our account.
*/
protected OperationSetPersistentPresenceIcqImpl(
ProtocolProviderServiceIcqImpl icqProvider,
String uin)
{
super(icqProvider);
ssContactList = new ServerStoredContactListIcqImpl( this , icqProvider);
//add a listener that'll follow the provider's state.
parentProvider.addRegistrationStateChangeListener(
registrationStateListener);
}
/**
* Get the PresenceStatus for a particular contact. This method is not meant
* to be used by the user interface (which would simply register as a
* presence listener and always follow contact status) but rather by other
* plugins that may for some reason need to know the status of a particular
* contact.
* <p>
* @param contactIdentifier the dientifier of the contact whose status we're
* interested in.
* @return PresenceStatus the <tt>PresenceStatus</tt> of the specified
* <tt>contact</tt>
* @throws java.lang.IllegalStateException if the provider is not signed
* on ICQ
* @throws java.lang.IllegalArgumentException if <tt>contact</tt> is not
* a valid <tt>IcqContact</tt>
*/
public PresenceStatus queryContactStatus(String contactIdentifier)
throws IllegalStateException, IllegalArgumentException
{
assertConnected();
//these are commented since we now use identifiers.
// if (!(contact instanceof ContactIcqImpl))
// throw new IllegalArgumentException(
// "Cannont get status for a non-ICQ contact! ("
// + contact + ")");
//
// ContactIcqImpl contactImpl = (ContactIcqImpl)contact;
StatusResponseRetriever responseRetriever =
new StatusResponseRetriever();
GetInfoCmd getInfoCmd =
new GetInfoCmd(GetInfoCmd.CMD_USER_INFO, contactIdentifier);
parentProvider.getAimConnection().getInfoService().getOscarConnection()
.sendSnacRequest(getInfoCmd, responseRetriever);
synchronized(responseRetriever)
{
try{
responseRetriever.wait(10000);
}
catch (InterruptedException ex){
//we don't care
}
}
return statusLongToPresenceStatus(responseRetriever.status);
}
/**
* Converts the specified icqstatus to one of the status fields of the
* IcqStatusEnum class.
*
* @param icqStatus the icqStatus as retured in FullUserInfo by the joscar
* stack
* @return a PresenceStatus instance representation of the "long" icqStatus
* parameter. The returned result is one of the IcqStatusEnum fields.
*/
private PresenceStatus statusLongToPresenceStatus(long icqStatus)
{
if(parentProvider.USING_ICQ)
{
// Fixed order of status checking
// The order does matter, as the icqStatus consists of more than one
// status for example DND = OCCUPIED | DND | AWAY
if(icqStatus == -1)
{
return IcqStatusEnum.OFFLINE;
}
else if ( (icqStatus & FullUserInfo.ICQSTATUS_INVISIBLE ) != 0)
{
return IcqStatusEnum.INVISIBLE;
}
else if ( (icqStatus & FullUserInfo.ICQSTATUS_DND ) != 0)
{
return IcqStatusEnum.DO_NOT_DISTURB;
}
else if ( (icqStatus & FullUserInfo.ICQSTATUS_OCCUPIED ) != 0)
{
return IcqStatusEnum.OCCUPIED;
}
else if ( (icqStatus & FullUserInfo.ICQSTATUS_NA ) != 0)
{
return IcqStatusEnum.NOT_AVAILABLE;
}
else if ( (icqStatus & FullUserInfo.ICQSTATUS_AWAY ) != 0)
{
return IcqStatusEnum.AWAY;
}
else if ( (icqStatus & FullUserInfo.ICQSTATUS_FFC ) != 0)
{
return IcqStatusEnum.FREE_FOR_CHAT;
}
// FIXED: Issue 70
// Incomplete status information in ICQ
// if none of the statuses is satisfied
// then the default is Online
// there is no such status send from the server as Offline
// when received error from server, after a query
// the status is -1 so Offline
// else if ((icqStatus & ICQ_ONLINE_MASK) == 0 )
// {
// return IcqStatusEnum.OFFLINE;
// }
return IcqStatusEnum.ONLINE;
}
else
{
if(icqStatus == -1)
{
return AimStatusEnum.OFFLINE;
}
else if ( (icqStatus & FullUserInfo.ICQSTATUS_INVISIBLE ) != 0)
{
return AimStatusEnum.INVISIBLE;
}
else if ( (icqStatus & FullUserInfo.ICQSTATUS_AWAY ) != 0)
{
return AimStatusEnum.AWAY;
}
return AimStatusEnum.ONLINE;
}
}
/**
* Converts the specified IcqStatusEnum member to the corresponding ICQ
* flag.
*
* @param status the icqStatus as retured in FullUserInfo by the joscar
* stack
* @return a PresenceStatus instance representation of the "long" icqStatus
* parameter. The returned result is one of the IcqStatusEnum fields.
*/
private long presenceStatusToStatusLong(PresenceStatus status)
{
if(parentProvider.USING_ICQ)
return scToIcqStatusMappings.get(status).longValue();
else
return scToAimStatusMappings.get(status).longValue();
}
/**
* Adds a subscription for the presence status of the contact corresponding
* to the specified contactIdentifier. Apart from an exception in the case
* of an immediate failure, the method won't return any indication of
* success or failure. That would happen later on through a
* SubscriptionEvent generated by one of the methods of the
* SubscriptionListener.
* <p>
* This subscription is not going to be persistent (as opposed to
* subscriptions added from the OperationSetPersistentPresence.subscribe()
* method)
* <p>
* @param contactIdentifier the identifier of the contact whose status
* updates we are subscribing for.
* <p>
* @throws OperationFailedException with code NETWORK_FAILURE if subscribing
* fails due to errors experienced during network communication
* @throws IllegalArgumentException if <tt>contact</tt> is not a contact
* known to the underlying protocol provider
* @throws IllegalStateException if the underlying protocol provider is not
* registered/signed on a public service.
*/
public void subscribe(String contactIdentifier)
throws IllegalArgumentException,
IllegalStateException,
OperationFailedException
{
assertConnected();
ssContactList.addContact(contactIdentifier);
}
/**
* Creates a non persistent contact for the specified address. This would
* also create (if necessary) a group for volatile contacts that would not
* be added to the server stored contact list. The volatile contact would
* remain in the list until it is really added to the contact list or
* until the application is terminated.
* @param uin the UIN/Screenname of the contact to create.
* @return the newly created volatile <tt>ContactIcqImpl</tt>
*/
public ContactIcqImpl createVolatileContact(String uin)
{
return ssContactList.createVolatileContact(new Screenname(uin));
}
/**
* Creates and returns a unresolved contact from the specified
* <tt>address</tt> and <tt>persistentData</tt>. The method will not try
* to establish a network connection and resolve the newly created Contact
* against the server. The protocol provider may will later try and resolve
* the contact. When this happens the corresponding event would notify
* interested subscription listeners.
*
* @param address an identifier of the contact that we'll be creating.
* @param persistentData a String returned Contact's getPersistentData()
* method during a previous run and that has been persistently stored
* locally.
* @param parentGroup the group that the unresolved contact should belong to.
* @return the unresolved <tt>Contact</tt> created from the specified
* <tt>address</tt> and <tt>persistentData</tt>
*
* @throws java.lang.IllegalArgumentException if <tt>parentGroup</tt> is not
* an instance of ContactGroupIcqImpl
*/
public Contact createUnresolvedContact(String address,
String persistentData,
ContactGroup parentGroup)
throws IllegalArgumentException
{
if(! (parentGroup instanceof ContactGroupIcqImpl) )
throw new IllegalArgumentException(
"Argument is not an icq contact group (group="
+ parentGroup + ")");
ContactIcqImpl contact =
ssContactList.createUnresolvedContact(
(ContactGroupIcqImpl)parentGroup, new Screenname(address));
contact.setPersistentData(persistentData);
return contact;
}
/**
* Creates and returns a unresolved contact from the specified
* <tt>address</tt> and <tt>persistentData</tt>. The method will not try
* to establish a network connection and resolve the newly created Contact
* against the server. The protocol provider may will later try and resolve
* the contact. When this happens the corresponding event would notify
* interested subscription listeners.
*
* @param address an identifier of the contact that we'll be creating.
* @param persistentData a String returned Contact's getPersistentData()
* method during a previous run and that has been persistently stored
* locally.
*
* @return the unresolved <tt>Contact</tt> created from the specified
* <tt>address</tt> and <tt>persistentData</tt>
*/
public Contact createUnresolvedContact(String address,
String persistentData)
{
return createUnresolvedContact( address
, persistentData
, getServerStoredContactListRoot());
}
/**
* Creates and returns a unresolved contact group from the specified
* <tt>address</tt> and <tt>persistentData</tt>. The method will not try
* to establish a network connection and resolve the newly created
* <tt>ContactGroup</tt> against the server or the contact itself. The
* protocol provider will later resolve the contact group. When this happens
* the corresponding event would notify interested subscription listeners.
*
* @param groupUID an identifier, returned by ContactGroup's getGroupUID,
* that the protocol provider may use in order to create the group.
* @param persistentData a String returned ContactGroups's getPersistentData()
* method during a previous run and that has been persistently stored
* locally.
* @param parentGroup the group under which the new group is to be created
* or null if this is group directly underneath the root.
* @return the unresolved <tt>ContactGroup</tt> created from the specified
* <tt>uid</tt> and <tt>persistentData</tt>
*/
public ContactGroup createUnresolvedContactGroup(String groupUID,
String persistentData, ContactGroup parentGroup)
{
//silently ignore the parent group. ICQ does not support subgroups so
//parentGroup is supposed to be root. if it is not however we're not
//going to complain because we're cool :).
return ssContactList.createUnresolvedContactGroup(groupUID);
}
/**
* Persistently adds a subscription for the presence status of the contact
* corresponding to the specified contactIdentifier and indicates that it
* should be added to the specified group of the server stored contact list.
* Note that apart from an exception in the case of an immediate failure,
* the method won't return any indication of success or failure. That would
* happen later on through a SubscriptionEvent generated by one of the
* methods of the SubscriptionListener.
* <p>
* @param contactIdentifier the contact whose status updates we are subscribing
* for.
* @param parent the parent group of the server stored contact list where
* the contact should be added.
* <p>
* <p>
* @throws OperationFailedException with code NETWORK_FAILURE if subscribing
* fails due to errors experienced during network communication
* @throws IllegalArgumentException if <tt>contact</tt> or
* <tt>parent</tt> are not a contact known to the underlying protocol
* provider.
* @throws IllegalStateException if the underlying protocol provider is not
* registered/signed on a public service.
*/
public void subscribe(ContactGroup parent, String contactIdentifier)
throws IllegalArgumentException,
IllegalStateException,
OperationFailedException
{
assertConnected();
if(! (parent instanceof ContactGroupIcqImpl) )
throw new IllegalArgumentException(
"Argument is not an icq contact group (group=" + parent + ")");
ssContactList.addContact((ContactGroupIcqImpl)parent, contactIdentifier);
}
/**
* Removes a subscription for the presence status of the specified contact.
* @param contact the contact whose status updates we are unsubscribing from.
*
* @throws OperationFailedException with code NETWORK_FAILURE if unsubscribing
* fails due to errors experienced during network communication
* @throws IllegalArgumentException if <tt>contact</tt> is not a contact
* known to this protocol provider or is not an ICQ contact
* @throws IllegalStateException if the underlying protocol provider is not
* registered/signed on a public service.
*/
public void unsubscribe(Contact contact) throws IllegalArgumentException,
IllegalStateException, OperationFailedException
{
assertConnected();
if(! (contact instanceof ContactIcqImpl) )
throw new IllegalArgumentException(
"Argument is not an icq contact (contact=" + contact + ")");
ContactIcqImpl contactIcqImpl = (ContactIcqImpl)contact;
ContactGroupIcqImpl contactGroup
= ssContactList.findContactGroup(contactIcqImpl);
if (contactGroup == null)
throw new IllegalArgumentException(
"The specified contact was not found on the local "
+"contact/subscription list: " + contact);
if(!contactIcqImpl.isPersistent())
{
contactGroup.removeContact(contactIcqImpl);
fireSubscriptionEvent(contactIcqImpl,
contactGroup,
SubscriptionEvent.SUBSCRIPTION_REMOVED);
return;
}
if (logger.isTraceEnabled())
logger.trace("Going to remove contact from ss-list : " + contact);
if( !contactGroup.isPersistent()
&& contactIcqImpl.getJoustSimBuddy().isAwaitingAuthorization())
{
// this is contact in AwaitingAuthorization group
// we must find the original parent and remove it from there
ContactGroupIcqImpl origParent =
ssContactList.findGroup(contactIcqImpl.getJoustSimBuddy());
if(origParent != null)
{
origParent.getJoustSimSourceGroup().
deleteBuddy(contactIcqImpl.getJoustSimBuddy());
}
}
else
{
MutableGroup joustSimContactGroup = contactGroup.getJoustSimSourceGroup();
joustSimContactGroup.deleteBuddy(contactIcqImpl.getJoustSimBuddy());
}
}
/**
* Returns a reference to the contact with the specified ID in case we have
* a subscription for it and null otherwise/
* @param contactID a String identifier of the contact which we're seeking a
* reference of.
* @return a reference to the Contact with the specified
* <tt>contactID</tt> or null if we don't have a subscription for the
* that identifier.
*/
public Contact findContactByID(String contactID)
{
return ssContactList.findContactByScreenName(contactID);
}
/**
* Requests the provider to enter into a status corresponding to the
* specified paramters. Note that calling this method does not necessarily
* imply that the requested status would be entered. This method would
* return right after being called and the caller should add itself as
* a listener to this class in order to get notified when the state has
* actually changed.
*
* @param status the PresenceStatus as returned by getRequestableStatusSet
* @param statusMessage the message that should be set as the reason to
* enter that status
* @throws IllegalArgumentException if the status requested is not a valid
* PresenceStatus supported by this provider.
* @throws java.lang.IllegalStateException if the provider is not currently
* registered.
* @throws OperationFailedException with code NETWORK_FAILURE if publishing
* the status fails due to a network error.
*/
public void publishPresenceStatus(PresenceStatus status,
String statusMessage) throws
IllegalArgumentException, IllegalStateException,
OperationFailedException
{
assertConnected();
if (!(status instanceof IcqStatusEnum || status instanceof AimStatusEnum))
throw new IllegalArgumentException(
status + " is not a valid ICQ/AIM status");
if (logger.isDebugEnabled())
logger.debug("Will set status: " + status);
MainBosService bosService
= parentProvider.getAimConnection().getBosService();
if(!parentProvider.USING_ICQ)
{
if(status.equals(AimStatusEnum.AWAY))
{
if(getPresenceStatus().equals(AimStatusEnum.INVISIBLE))
bosService.setVisibleStatus(true);
bosService.getOscarConnection().sendSnac(new SetInfoCmd(
new InfoData(null, "I'm away!", null, null)));
}
else if(status.equals(AimStatusEnum.INVISIBLE))
{
if(getPresenceStatus().equals(AimStatusEnum.AWAY))
bosService.getOscarConnection().sendSnac(new SetInfoCmd(
new InfoData(null, InfoData.NOT_AWAY, null, null)));
bosService.setVisibleStatus(false);
}
else if(status.equals(AimStatusEnum.ONLINE))
{
if(getPresenceStatus().equals(AimStatusEnum.INVISIBLE))
bosService.setVisibleStatus(true);
else if(getPresenceStatus().equals(AimStatusEnum.AWAY))
{
bosService.getOscarConnection().sendSnac(new SetInfoCmd(
new InfoData(null, InfoData.NOT_AWAY, null, null)));
}
}
}
else
{
long icqStatus = presenceStatusToStatusLong(status);
if (logger.isDebugEnabled())
logger.debug("Will set status: " + status + " long=" + icqStatus);
bosService.getOscarConnection().sendSnac(new SetExtraInfoCmd(icqStatus));
if(status.equals(IcqStatusEnum.AWAY))
parentProvider.getAimConnection().getInfoService().
setAwayMessage(statusMessage);
else
bosService.setStatusMessage(statusMessage);
}
//so that everyone sees the change.
queryContactStatus(
parentProvider.getAimConnection().getScreenname().getFormatted());
}
/**
* Returns the status message that was confirmed by the serfver
* @return the last status message that we have requested and the aim server
* has confirmed.
*/
public String getCurrentStatusMessage()
{
return this.currentStatusMessage;
}
/**
* Returns the protocol specific contact instance representing the local
* user.
*
* @return the Contact (address, phone number, or uin) that the Provider
* implementation is communicating on behalf of.
*/
public Contact getLocalContact()
{
return localContact;
}
/**
* Creates a group with the specified name and parent in the server stored
* contact list.
* @param groupName the name of the new group to create.
* @param parent the group where the new group should be created
*
* @throws OperationFailedException with code NETWORK_FAILURE if unsubscribing
* fails due to errors experienced during network communication
* @throws IllegalArgumentException if <tt>contact</tt> is not a contact
* known to the underlying protocol provider
* @throws IllegalStateException if the underlying protocol provider is not
* registered/signed on a public service.
*/
public void createServerStoredContactGroup(ContactGroup parent,
String groupName)
{
assertConnected();
if (!parent.canContainSubgroups())
throw new IllegalArgumentException(
"The specified contact group cannot contain child groups. Group:"
+ parent );
ssContactList.createGroup(groupName);
}
/**
* Removes the specified group from the server stored contact list.
* @param group the group to remove.
*
* @throws OperationFailedException with code NETWORK_FAILURE if deleting
* the group fails because of a network error.
* @throws IllegalArgumentException if <tt>parent</tt> is not a contact
* known to the underlying protocol provider.
* @throws IllegalStateException if the underlying protocol provider is not
* registered/signed on a public service.
*/
public void removeServerStoredContactGroup(ContactGroup group)
{
assertConnected();
if( !(group instanceof ContactGroupIcqImpl) )
throw new IllegalArgumentException(
"The specified group is not an icq contact group: " + group);
ssContactList.removeGroup((ContactGroupIcqImpl)group);
}
/**
* Renames the specified group from the server stored contact list. This
* method would return before the group has actually been renamed. A
* <tt>ServerStoredGroupEvent</tt> would be dispatched once new name
* has been acknowledged by the server.
*
* @param group the group to rename.
* @param newName the new name of the group.
*
* @throws OperationFailedException with code NETWORK_FAILURE if deleting
* the group fails because of a network error.
* @throws IllegalArgumentException if <tt>parent</tt> is not a contact
* known to the underlying protocol provider.
* @throws IllegalStateException if the underlying protocol provider is not
* registered/signed on a public service.
*/
public void renameServerStoredContactGroup(
ContactGroup group, String newName)
{
assertConnected();
if( !(group instanceof ContactGroupIcqImpl) )
throw new IllegalArgumentException(
"The specified group is not an icq contact group: " + group);
ssContactList.renameGroup((ContactGroupIcqImpl)group, newName);
}
/**
* Removes the specified contact from its current parent and places it
* under <tt>newParent</tt>.
* @param contactToMove the <tt>Contact</tt> to move
* @param newParent the <tt>ContactGroup</tt> where <tt>Contact</tt>
* would be placed.
*/
public void moveContactToGroup(Contact contactToMove,
ContactGroup newParent)
{
assertConnected();
if( !(contactToMove instanceof ContactIcqImpl) )
throw new IllegalArgumentException(
"The specified contact is not an icq contact." + contactToMove);
if( !(newParent instanceof ContactGroupIcqImpl) )
throw new IllegalArgumentException(
"The specified group is not an icq contact group."
+ newParent);
ssContactList.moveContact((ContactIcqImpl)contactToMove,
(ContactGroupIcqImpl)newParent);
}
/**
* Returns a snapshot ieves a server stored list of subscriptions/contacts that have been
* made previously. Note that the contact list returned by this method may
* be incomplete as it is only a snapshot of what has been retrieved through
* the network up to the moment when the method is called.
* @return a ConactGroup containing all previously made subscriptions stored
* on the server.
*/
ServerStoredContactListIcqImpl getServerStoredContactList()
{
return ssContactList;
}
/**
* Returns a PresenceStatus instance representing the state this provider
* is currently in.
*
* @return PresenceStatus
*/
public PresenceStatus getPresenceStatus()
{
return statusLongToPresenceStatus(currentIcqStatus);
}
/**
* Returns the set of PresenceStatus objects that a user of this service
* may request the provider to enter.
*
* @return Iterator a PresenceStatus array containing "enterable" status
* instances.
*/
public Iterator<PresenceStatus> getSupportedStatusSet()
{
if(supportedPresenceStatusSet.size() == 0)
{
if(parentProvider.USING_ICQ)
{
supportedPresenceStatusSet.add(IcqStatusEnum.ONLINE);
supportedPresenceStatusSet.add(IcqStatusEnum.DO_NOT_DISTURB);
supportedPresenceStatusSet.add(IcqStatusEnum.FREE_FOR_CHAT);
supportedPresenceStatusSet.add(IcqStatusEnum.NOT_AVAILABLE);
supportedPresenceStatusSet.add(IcqStatusEnum.OCCUPIED);
supportedPresenceStatusSet.add(IcqStatusEnum.AWAY);
supportedPresenceStatusSet.add(IcqStatusEnum.INVISIBLE);
supportedPresenceStatusSet.add(IcqStatusEnum.OFFLINE);
}
else
{
supportedPresenceStatusSet.add(AimStatusEnum.ONLINE);
supportedPresenceStatusSet.add(AimStatusEnum.AWAY);
supportedPresenceStatusSet.add(AimStatusEnum.INVISIBLE);
supportedPresenceStatusSet.add(AimStatusEnum.OFFLINE);
}
}
return supportedPresenceStatusSet.iterator();
}
/**
* Handler for incoming authorization requests. An authorization request
* notifies the user that someone is trying to add her to their contact list
* and requires her to approve or reject authorization for that action.
* @param handler an instance of an AuthorizationHandler for authorization
* requests coming from other users requesting permission add us to their
* contact list.
*/
public void setAuthorizationHandler(AuthorizationHandler handler)
{
/** @todo
* method to be removed and AuthorizationHandler to be set
* set upon creation of the
* provider so that there could be only one.
*
**/
this.authorizationHandler = handler;
parentProvider.getAimConnection().getSsiService().
addBuddyAuthorizationListener(authListener);
}
/**
* The StatusResponseRetriever is used as a one time handler for responses
* to requests sent through the sendSnacRequest method of one of joustsim's
* Services. The StatusResponseRetriever would ignore everything apart from
* the first response, which will be stored in the status field. In the
* case of a timeout, the status would remain on -1. Both a response and
* a timeout would make the StatusResponseRetriever call its notifyAll
* method so that those that are waiting on it be notified.
*/
private static class StatusResponseRetriever extends SnacRequestAdapter
{
private boolean ran = false;
private long status = -1;
@Override
public void handleResponse(SnacResponseEvent e) {
SnacCommand snac = e.getSnacCommand();
synchronized(this) {
if (ran) return;
ran = true;
}
if (snac instanceof UserInfoCmd)
{
UserInfoCmd uic = (UserInfoCmd) snac;
FullUserInfo userInfo = uic.getUserInfo();
if (userInfo != null)
{
this.status = userInfo.getIcqStatus();
// StatusResponseRetriever is used when query for
// user status if status is not set (is -1)
// this means user is offline.
//if (this.status == -1)
// status = ICQ_ONLINE_MASK;
synchronized(this){
this.notifyAll();
}
}
}
else if( snac instanceof SnacError)
{
//this is most probably a CODE_USER_UNAVAILABLE, but
//whatever it is it means that to us the buddy in question
//is as good as offline so leave status at -1 and notify.
if (logger.isDebugEnabled())
logger.debug("status is" + status);
synchronized(this){
this.notifyAll();
}
}
}
@Override
public void handleTimeout(SnacRequestTimeoutEvent event) {
synchronized(this) {
if (ran) return;
ran = true;
notifyAll();
}
}
}
/**
* Utility method throwing an exception if the icq stack is not properly
* initialized.
* @throws java.lang.IllegalStateException if the underlying ICQ stack is
* not registered and initialized.
*/
private void assertConnected() throws IllegalStateException
{
if (parentProvider == null)
throw new IllegalStateException(
"The icq provider must be non-null and signed on the ICQ "
+"service before being able to communicate.");
if (!parentProvider.isRegistered())
throw new IllegalStateException(
"The icq provider must be signed on the ICQ service before "
+"being able to communicate.");
}
/**
* Returns the root group of the server stored contact list. Most often this
* would be a dummy group that user interface implementations may better not
* show.
*
* @return the root ContactGroup for the ContactList stored by this service.
*/
public ContactGroup getServerStoredContactListRoot()
{
return ssContactList.getRootGroup();
}
/**
* Registers a listener that would receive events upong changes in server
* stored groups.
* @param listener a ServerStoredGroupChangeListener impl that would receive
* events upong group changes.
*/
@Override
public void addServerStoredGroupChangeListener(
ServerStoredGroupListener listener)
{
ssContactList.addGroupListener(listener);
}
/**
* Removes the specified group change listener so that it won't receive
* any further events.
* @param listener the ServerStoredGroupChangeListener to remove
*/
@Override
public void removeServerStoredGroupChangeListener(
ServerStoredGroupListener listener)
{
ssContactList.removeGroupListener(listener);
}
/**
* Notify all provider presence listeners of the corresponding event change
*
* @param oldStatusL
* the status our icq stack had so far
* @param newStatusL
* the status we have from now on
*/
void fireProviderPresenceStatusChangeEvent(long oldStatusL, long newStatusL)
{
PresenceStatus oldStatus = statusLongToPresenceStatus(oldStatusL);
PresenceStatus newStatus = statusLongToPresenceStatus(newStatusL);
if (oldStatus.equals(newStatus))
if (logger.isDebugEnabled())
logger.debug(
"Ignored prov stat. change evt. old==new = "
+ oldStatus);
else
fireProviderStatusChangeEvent(oldStatus, newStatus);
}
/**
* Our listener that will tell us when we're registered to icq and joust
* sim is ready to accept us as a listener.
*/
private class RegistrationStateListener
implements RegistrationStateChangeListener
{
/**
* The method is called by a ProtocolProvider implementation whenver
* a change in the registration state of the corresponding provider had
* occurred.
* @param evt ProviderStatusChangeEvent the event describing the status
* change.
*/
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
if (logger.isDebugEnabled())
logger.debug("The ICQ provider changed state from: "
+ evt.getOldState()
+ " to: " + evt.getNewState());
if(evt.getNewState() == RegistrationState.FINALIZING_REGISTRATION)
{
if (logger.isDebugEnabled())
logger.debug("adding a Bos Service Listener");
parentProvider.getAimConnection().getBosService()
.addMainBosServiceListener(joustSimBosListener);
ssContactList.init(
parentProvider.getAimConnection().getSsiService());
// /**@todo implement the following
parentProvider.getAimConnection().getBuddyService()
.addBuddyListener(joustSimBuddySerListener);
// @todo we really need this for following the status of our
// contacts and we really need it here ...*/
parentProvider.getAimConnection().getBuddyInfoManager()
.addGlobalBuddyInfoListener(new GlobalBuddyInfoListener());
parentProvider.getAimConnection().getExternalServiceManager().
getIconServiceArbiter().addIconRequestListener(
new IconUpdateListener());
parentProvider.getAimConnection().getInfoService().
addInfoListener(new AwayMessageListener());
}
else if(evt.getNewState() == RegistrationState.REGISTERED)
{
if(parentProvider.USING_ICQ)
{
opSetExtendedAuthorizations =
(OperationSetExtendedAuthorizationsIcqImpl) parentProvider
.getOperationSet(OperationSetExtendedAuthorizations.class);
if(presenceQueryTimer == null)
presenceQueryTimer = new Timer();
else
{
// cancel any previous jobs and create new timer
presenceQueryTimer.cancel();
presenceQueryTimer = new Timer();
}
AwaitingAuthorizationContactsPresenceTimer
queryTask = new AwaitingAuthorizationContactsPresenceTimer();
// start after 15 seconds. wait for login to be completed and
// list and statuses to be gathered
presenceQueryTimer.scheduleAtFixedRate(
queryTask, 15000, PRESENCE_QUERY_INTERVAL);
}
}
else if(evt.getNewState() == RegistrationState.UNREGISTERED
|| evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED
|| evt.getNewState() == RegistrationState.CONNECTION_FAILED)
{
if(presenceQueryTimer != null)
{
presenceQueryTimer.cancel();
presenceQueryTimer = null;
}
//since we are disconnected, we won't receive any further status
//updates so we need to change by ourselves our own status as
//well as set to offline all contacts in our contact list that
//were online
//start by notifying that our own status has changed
long oldStatus = currentIcqStatus;
currentIcqStatus = -1;
//only notify of an event change if there was really one.
if( oldStatus != -1 )
fireProviderPresenceStatusChangeEvent(oldStatus,
currentIcqStatus);
//send event notifications saying that all our buddies are
//offline. The icq protocol does not implement top level buddies
//nor subgroups for top level groups so a simple nested loop
//would be enough.
Iterator<ContactGroup> groupsIter
= getServerStoredContactListRoot().subgroups();
while(groupsIter.hasNext())
{
ContactGroup group = groupsIter.next();
Iterator<Contact> contactsIter = group.contacts();
while(contactsIter.hasNext())
{
ContactIcqImpl contact
= (ContactIcqImpl)contactsIter.next();
PresenceStatus oldContactStatus
= contact.getPresenceStatus();
if(!oldContactStatus.isOnline())
continue;
if(parentProvider.USING_ICQ)
{
contact.updatePresenceStatus(IcqStatusEnum.OFFLINE);
fireContactPresenceStatusChangeEvent(
contact
, contact.getParentContactGroup()
, oldContactStatus, IcqStatusEnum.OFFLINE);
}
else
{
contact.updatePresenceStatus(AimStatusEnum.OFFLINE);
fireContactPresenceStatusChangeEvent(
contact
, contact.getParentContactGroup()
, oldContactStatus, AimStatusEnum.OFFLINE);
}
}
}
}
}
}
/**
* The listeners that would monitor the joust sim stack for changes in our
* own presence status.
*/
private class JoustSimBosListener implements MainBosServiceListener
{
/**
* Notifications of exta information such as avail message, icon hash
* or certificate.
* @param extraInfos List
*/
public void handleYourExtraInfo(List<ExtraInfoBlock> extraInfos)
{
if (logger.isDebugEnabled())
logger.debug("Got extra info: " + extraInfos);
// @xxx we should one day probably do something here, like check
// whether the status message has been changed for example.
for (ExtraInfoBlock block : extraInfos)
{
if (block.getType() == ExtraInfoBlock.TYPE_AVAILMSG){
String statusMessage = ExtraInfoData.readAvailableMessage(
block.getExtraData());
if (logger.isDebugEnabled())
logger.debug("Received a status message:" + statusMessage);
if ( getCurrentStatusMessage().equals(statusMessage)){
if (logger.isDebugEnabled())
logger.debug("Status message is same as old. Ignoring");
return;
}
String oldStatusMessage = getCurrentStatusMessage();
currentStatusMessage = statusMessage;
fireProviderStatusMessageChangeEvent(
oldStatusMessage, getCurrentStatusMessage());
}
}
}
/**
* Fires the corresponding presence status chagne event. Note that this
* method will be called once per sendSnac packet. When setting a new
* status we generally send three packets - 1 for the status and 2 for
* the status message. Make sure that only one event goes outside of
* this package.
*
* @param service the source MainBosService instance
* @param userInfo our own info
*/
public void handleYourInfo(MainBosService service,
FullUserInfo userInfo)
{
if (logger.isDebugEnabled())
logger.debug("Received our own user info: " + userInfo);
if (logger.isDebugEnabled())
logger.debug("previous status was: " + currentIcqStatus);
if (logger.isDebugEnabled())
logger.debug("new status is: " + userInfo.getIcqStatus());
//update the last received field.
long oldStatus = currentIcqStatus;
if(parentProvider.USING_ICQ)
{
currentIcqStatus = userInfo.getIcqStatus();
//it might happen that the info here is -1 (in case we're going back
//to online). Yet the fact that we're getting the event means
//that we're very much online so make sure we change accordingly
if (currentIcqStatus == -1 )
currentIcqStatus = ICQ_ONLINE_MASK;
//only notify of an event change if there was really one.
if( oldStatus != currentIcqStatus)
fireProviderPresenceStatusChangeEvent(oldStatus,
currentIcqStatus);
}
else
{
if(userInfo.getAwayStatus() != null
&& userInfo.getAwayStatus().equals(Boolean.TRUE))
{
currentIcqStatus = presenceStatusToStatusLong(AimStatusEnum.AWAY);
}
else if(userInfo.getIcqStatus() != -1)
{
currentIcqStatus = userInfo.getIcqStatus();
}
else // online status
currentIcqStatus = ICQ_ONLINE_MASK;
if( oldStatus != currentIcqStatus )
fireProviderPresenceStatusChangeEvent(oldStatus,
currentIcqStatus);
}
}
}
/**
* Listens for status updates coming from the joust sim statck and generates
* the corresponding sip-communicator events.
* @author Emil Ivov
*/
private class JoustSimBuddyServiceListener implements BuddyServiceListener
{
/**
* Updates the last received status in the corresponding contact
* and fires a contact presence status change event.
* @param service the BuddyService that generated the exception
* @param buddy the Screenname of the buddy whose status update we're
* receiving
* @param info the FullUserInfo containing the new status of the
* corresponding contact
*/
public void gotBuddyStatus(BuddyService service, Screenname buddy,
FullUserInfo info)
{
if (logger.isDebugEnabled())
logger.debug("Received a status update for buddy=" + buddy);
if (logger.isDebugEnabled())
logger.debug("Updated user info is " + info);
ContactIcqImpl sourceContact
= ssContactList.findContactByScreenName(buddy.getFormatted());
if(sourceContact == null){
logger.warn("No source contact found for screenname=" + buddy);
return;
}
PresenceStatus oldStatus
= sourceContact.getPresenceStatus();
PresenceStatus newStatus = null;
if(!parentProvider.USING_ICQ)
{
Boolean awayStatus = info.getAwayStatus();
if(awayStatus == null || awayStatus.equals(Boolean.FALSE))
newStatus = AimStatusEnum.ONLINE;
else
newStatus = AimStatusEnum.AWAY;
}
else
newStatus = statusLongToPresenceStatus(info.getIcqStatus());
sourceContact.updatePresenceStatus(newStatus);
ContactGroupIcqImpl parent
= ssContactList.findContactGroup(sourceContact);
Iterable<ExtraInfoBlock> extraInfoBlocks
= info.getExtraInfoBlocks();
if(extraInfoBlocks != null){
for (ExtraInfoBlock block : extraInfoBlocks)
{
if (block.getType() == ExtraInfoBlock.TYPE_AVAILMSG)
{
String status = ExtraInfoData.readAvailableMessage(
block.getExtraData());
if (logger.isInfoEnabled())
logger.info("Status Message is: " + status + ".");
sourceContact.setStatusMessage(status);
}
}
}
if (logger.isDebugEnabled())
logger.debug("Will Dispatch the contact status event.");
fireContactPresenceStatusChangeEvent(sourceContact, parent,
oldStatus, newStatus);
}
/**
* Updates the last received status in the corresponding contact
* and fires a contact presence status change event.
*
* @param service the BuddyService that generated the exception
* @param buddy the Screenname of the buddy whose status update we're
* receiving
*/
public void buddyOffline(BuddyService service, Screenname buddy)
{
if (logger.isDebugEnabled())
logger.debug("Received a status update for buddy=" + buddy);
ContactIcqImpl sourceContact
= ssContactList.findContactByScreenName(buddy.getFormatted());
if(sourceContact == null)
return;
PresenceStatus oldStatus
= sourceContact.getPresenceStatus();
PresenceStatus newStatus = null;
if(parentProvider.USING_ICQ)
newStatus = IcqStatusEnum.OFFLINE;
else
newStatus = AimStatusEnum.OFFLINE;
sourceContact.updatePresenceStatus(newStatus);
ContactGroupIcqImpl parent
= ssContactList.findContactGroup(sourceContact);
fireContactPresenceStatusChangeEvent(sourceContact, parent,
oldStatus, newStatus);
}
}
/**
* Apart from login - does nothing so far.
*/
private class GlobalBuddyInfoListener extends GlobalBuddyInfoAdapter{
@Override
public void receivedStatusUpdate(BuddyInfoManager manager,
Screenname buddy, BuddyInfo info)
{
String statusMessage = info.getStatusMessage();
if (logger.isDebugEnabled())
logger.debug("buddy=" + buddy);
if (logger.isDebugEnabled())
logger.debug("info.getAwayMessage()=" + info.getAwayMessage());
if (logger.isDebugEnabled())
logger.debug("info.getOnlineSince()=" + info.getOnlineSince());
if (logger.isDebugEnabled())
logger.debug("info.getStatusMessage()=" + statusMessage);
ContactIcqImpl sourceContact
= ssContactList.findContactByScreenName(buddy.getFormatted());
if(sourceContact != null)
{
sourceContact.setStatusMessage(statusMessage);
}
}
}
private class AuthListener
implements BuddyAuthorizationListener
{
public void authorizationDenied(Screenname screenname, String reason)
{
if (logger.isTraceEnabled())
logger.trace("authorizationDenied from " + screenname);
Contact srcContact = findContactByID(screenname.getFormatted());
authorizationHandler.processAuthorizationResponse(
new AuthorizationResponse(AuthorizationResponse.REJECT, reason)
, srcContact);
try
{
unsubscribe(srcContact);
} catch (OperationFailedException ex)
{
logger.error("cannot remove denied contact : " + srcContact, ex);
}
}
public void authorizationAccepted(Screenname screenname, String reason)
{
if (logger.isTraceEnabled())
logger.trace("authorizationAccepted from " + screenname);
Contact srcContact = findContactByID(screenname.getFormatted());
authorizationHandler.processAuthorizationResponse(
new AuthorizationResponse(AuthorizationResponse.ACCEPT, reason)
, srcContact);
}
public void authorizationRequestReceived(Screenname screenname,
String reason)
{
if (logger.isTraceEnabled())
logger.trace("authorizationRequestReceived from " + screenname);
Contact srcContact = findContactByID(screenname.getFormatted());
if(srcContact == null)
srcContact = createVolatileContact(screenname.getFormatted());
AuthorizationRequest authRequest = new AuthorizationRequest();
authRequest.setReason(reason);
AuthorizationResponse authResponse =
authorizationHandler.processAuthorisationRequest(
authRequest, srcContact);
if (authResponse.getResponseCode() == AuthorizationResponse.IGNORE)
return;
parentProvider.getAimConnection().getSsiService().
replyBuddyAuthorization(
screenname,
authResponse.getResponseCode() == AuthorizationResponse.ACCEPT,
authResponse.getReason());
}
public boolean authorizationRequired(Screenname screenname, Group parentGroup)
{
if (logger.isTraceEnabled())
logger.trace("authorizationRequired from " + screenname);
if (logger.isTraceEnabled())
logger.trace("finding buddy : " + screenname);
ContactIcqImpl srcContact =
ssContactList.findContactByScreenName(screenname.getFormatted());
if(srcContact == null)
{
ContactGroupIcqImpl parent =
ssContactList.findContactGroup(parentGroup);
srcContact = ssContactList.
createUnresolvedContact(parent, screenname);
Buddy buddy = srcContact.getJoustSimBuddy();
if(buddy instanceof VolatileBuddy)
((VolatileBuddy)buddy).setAwaitingAuthorization(true);
}
AuthorizationRequest authRequest =
authorizationHandler.createAuthorizationRequest(
srcContact);
if(authRequest == null)
return false;
parentProvider.getAimConnection().getSsiService().
sendFutureBuddyAuthorization(screenname, authRequest.getReason());
parentProvider.getAimConnection().getSsiService().
requestBuddyAuthorization(screenname, authRequest.getReason());
return true;
}
public void futureAuthorizationGranted(Screenname screenname,
String reason)
{
if (logger.isTraceEnabled())
logger.trace("futureAuthorizationGranted from " + screenname);
}
public void youWereAdded(Screenname screenname)
{
if (logger.isTraceEnabled())
logger.trace("youWereAdded from " + screenname);
}
}
/**
* Notified if buddy icon is changed
*/
private class IconUpdateListener
implements IconRequestListener
{
public void buddyIconCleared(IconService iconService,
Screenname screenname,
ExtraInfoData extraInfoData)
{
updateBuddyIcon(screenname, null);
}
public void buddyIconUpdated(IconService iconService,
Screenname screenname,
ExtraInfoData extraInfoData,
ByteBlock byteBlock)
{
if(byteBlock != null)
{
updateBuddyIcon(screenname, byteBlock.toByteArray());
}
}
/**
* Changes the Contact image
* @param screenname the contact screenname
* @param icon byte array representing the image
*/
private void updateBuddyIcon(Screenname screenname, byte[] icon)
{
ContactIcqImpl contact = ssContactList.findContactByScreenName(
screenname.getFormatted());
if(contact != null)
{
byte[] oldIcon = contact.getImage();
contact.setImage(icon);
fireContactPropertyChangeEvent(
ContactPropertyChangeEvent.PROPERTY_IMAGE,
contact,
oldIcon,
icon);
}
}
}
private class AwaitingAuthorizationContactsPresenceTimer
extends TimerTask
{
@Override
public void run()
{
if (logger.isTraceEnabled())
logger.trace("Running status retreiver for AwaitingAuthorizationContacts");
Iterator<ContactGroup> groupsIter
= getServerStoredContactListRoot().subgroups();
while(groupsIter.hasNext())
{
ContactGroup group = groupsIter.next();
Iterator<Contact> contactsIter = group.contacts();
while(contactsIter.hasNext())
{
ContactIcqImpl sourceContact
= (ContactIcqImpl)contactsIter.next();
if(!sourceContact.getJoustSimBuddy()
.isAwaitingAuthorization())
continue;
String sourceContactAddress = sourceContact.getAddress();
PresenceStatus newStatus
= queryContactStatus(sourceContactAddress);
PresenceStatus oldStatus = sourceContact.getPresenceStatus();
if(newStatus.equals(oldStatus))
continue;
sourceContact.updatePresenceStatus(newStatus);
fireContactPresenceStatusChangeEvent(
sourceContact,
sourceContact.getParentContactGroup(),
oldStatus,
newStatus);
if (!newStatus.equals(IcqStatusEnum.OFFLINE)
&& !buddiesSeenAvailable.contains(sourceContactAddress))
{
buddiesSeenAvailable.add(sourceContactAddress);
try
{
AuthorizationRequest req =
new AuthorizationRequest();
req.setReason("I'm resending my request. " +
"Please authorize me!");
opSetExtendedAuthorizations
.reRequestAuthorization(req, sourceContact);
} catch (OperationFailedException ex)
{
logger.error("failed to reRequestAuthorization", ex);
}
}
}
}
}
}
/**
* Notifies for changes in away message
*/
private class AwayMessageListener
implements InfoServiceListener
{
public void handleAwayMessage(
InfoService service, Screenname buddy, String awayMessage)
{
ContactIcqImpl sourceContact
= ssContactList.findContactByScreenName(buddy.getFormatted());
if(sourceContact != null)
{
sourceContact.setStatusMessage(awayMessage);
}
}
public void handleUserProfile(InfoService service, Screenname buddy,
String userInfo){}
public void handleCertificateInfo(InfoService service, Screenname buddy,
BuddyCertificateInfo certInfo){}
public void handleInvalidCertificates(InfoService service, Screenname buddy,
CertificateInfo origCertInfo, Throwable ex){}
public void handleDirectoryInfo(InfoService service, Screenname buddy,
DirInfo dirInfo){}
}
}