/* * Copyright 2006-2010 Daniel Henninger. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), * a copy of which is included in this distribution. */ package net.sf.kraken.protocols.oscar; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import net.kano.joscar.ByteBlock; import net.kano.joscar.OscarTools; import net.kano.joscar.SeqNum; import net.kano.joscar.flapcmd.SnacCommand; import net.kano.joscar.net.ConnDescriptor; import net.kano.joscar.snac.SnacRequest; import net.kano.joscar.snac.SnacRequestListener; import net.kano.joscar.snaccmd.CapabilityBlock; import net.kano.joscar.snaccmd.ExtraInfoBlock; import net.kano.joscar.snaccmd.ExtraInfoData; import net.kano.joscar.snaccmd.InfoData; import net.kano.joscar.snaccmd.conn.ServiceRequest; import net.kano.joscar.snaccmd.conn.SetExtraInfoCmd; import net.kano.joscar.snaccmd.icbm.SendImIcbm; import net.kano.joscar.snaccmd.icbm.SendTypingNotification; import net.kano.joscar.snaccmd.icq.MetaShortInfoRequest; import net.kano.joscar.snaccmd.icq.OfflineMsgIcqRequest; import net.kano.joscar.snaccmd.loc.SetInfoCmd; import net.kano.joscar.snaccmd.mailcheck.MailCheckCmd; import net.kano.joscar.snaccmd.ssi.AuthFutureCmd; import net.kano.joscar.snaccmd.ssi.DeleteItemsCmd; import net.kano.joscar.ssiitem.BuddyItem; import net.kano.joscar.ssiitem.VisibilityItem; import net.sf.kraken.registration.Registration; import net.sf.kraken.roster.TransportBuddy; import net.sf.kraken.roster.TransportBuddyManager; import net.sf.kraken.session.TransportSession; import net.sf.kraken.type.ChatStateType; import net.sf.kraken.type.PresenceType; import net.sf.kraken.type.SupportedFeature; import net.sf.kraken.type.TransportType; import org.apache.log4j.Logger; import org.jivesoftware.openfire.roster.RosterItem; import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.util.NotFoundException; import org.xmpp.packet.JID; /** * Represents an OSCAR session. * * This is the interface with which the base transport functionality will * communicate with OSCAR (AIM/ICQ). * * Yeesh, this is the one I'm most familiar with and yet it's the ugliest. * This needs some housecleaning. * * @author Daniel Henninger */ public class OSCARSession extends TransportSession<OSCARBuddy> { static Logger Log = Logger.getLogger(OSCARSession.class); private BOSConnection bosConn = null; private LoginConnection loginConn = null; private final Set<ServiceConnection> services = new HashSet<ServiceConnection>(); private String propertyPrefix; private final SeqNum icqSeqNum = new SeqNum(0, Integer.MAX_VALUE); /** * Representation of the Server Sided Item hierarchy of this session. */ private final SSIHierarchy ssiHierarchy; /** * Initialize a new session object for OSCAR * * @param registration The registration information to use during login. * @param jid The JID associated with this session. * @param transport The transport that created this session. * @param priority Priority of this session. */ public OSCARSession(Registration registration, JID jid, OSCARTransport transport, Integer priority) { super(registration, jid, transport, priority); setSupportedFeature(SupportedFeature.chatstates); ssiHierarchy = new SSIHierarchy(this); this.propertyPrefix = "plugin.gateway."+transport.getType().toString(); OscarTools.setDefaultCharset(JiveGlobals.getProperty(this.propertyPrefix+".encoding", "ISO8859-1")); if (JiveGlobals.getBooleanProperty("plugin.gateway."+transport.getType()+".crosschat", true)) { MY_CAPS.add(CapabilityBlock.BLOCK_ICQCOMPATIBLE); } if (transport.getType().equals(TransportType.icq)) { MY_CAPS.add(CapabilityBlock.BLOCK_ICQ_UTF8); } } /** * Returns the Server Sided Item hierarchy of this session. * * @return Server Sided Item hierarchy */ public SSIHierarchy getSsiHierarchy() { return ssiHierarchy; } private final List<CapabilityBlock> MY_CAPS = new ArrayList<CapabilityBlock>(); public List<CapabilityBlock> getCapabilities() { return MY_CAPS; } /** * @see net.sf.kraken.session.TransportSession#logIn(net.sf.kraken.type.PresenceType, String) */ @Override public synchronized void logIn(PresenceType presenceType, String verboseStatus) { setPendingPresenceAndStatus(presenceType, verboseStatus); if (!isLoggedIn()) { loginConn = new LoginConnection(new ConnDescriptor( JiveGlobals.getProperty(propertyPrefix+".connecthost", (this.getTransport().getType().equals(TransportType.icq) ? "login.icq.com" : "login.oscar.aol.com")), JiveGlobals.getIntProperty(propertyPrefix+".connectport", 5190)), this); loginConn.connect(); } } /** * @see net.sf.kraken.session.TransportSession#logOut() */ @Override public synchronized void logOut() { cleanUp(); sessionDisconnectedNoReconnect(null); } /** * @see net.sf.kraken.session.TransportSession#cleanUp() */ @Override public synchronized void cleanUp() { if (loginConn != null) { try { loginConn.stopKeepAlive(); } catch (Exception e) { // Ignore. } try { loginConn.disconnect(); } catch (Exception e) { // Ignore. } loginConn = null; } if (bosConn != null) { try { bosConn.stopKeepAlive(); } catch (Exception e) { // Ignore. } try { bosConn.disconnect(); } catch (Exception e) { // Ignore. } bosConn = null; } for (ServiceConnection conn : getServiceConnections()) { try { conn.stopKeepAlive(); } catch (Exception e) { // Ignore. } try { conn.disconnect(); } catch (Exception e) { // Ignore. } try { services.remove(conn); } catch (Exception e) { // Ignore. } try { snacMgr.unregister(conn); } catch (Exception e) { // Ignore. } } } /** * @see net.sf.kraken.session.TransportSession#addContact(org.xmpp.packet.JID, String, java.util.ArrayList) */ @Override public void addContact(JID jid, String nickname, ArrayList<String> groups) { String legacyId = getTransport().convertJIDToID(jid); if (nickname == null || nickname.equals("")) { nickname = legacyId; } if (getTransport().getType().equals(TransportType.icq)) { request(new AuthFutureCmd(legacyId, null)); } // Syncing takes care of all the dirty work. ssiHierarchy.syncContactGroupsAndNickname(legacyId, nickname, groups); } /** * @see net.sf.kraken.session.TransportSession#removeContact(net.sf.kraken.roster.TransportBuddy) */ @Override public void removeContact(OSCARBuddy oscarBuddy) { String legacyId = getTransport().convertJIDToID(oscarBuddy.getJID()); for (BuddyItem i : oscarBuddy.getBuddyItems()) { if (i.getScreenname().equalsIgnoreCase(legacyId)) { request(new DeleteItemsCmd(i.toSsiItem())); } } } /** * @see net.sf.kraken.session.TransportSession#updateContact(net.sf.kraken.roster.TransportBuddy) */ @Override public void updateContact(OSCARBuddy contact) { String legacyId = getTransport().convertJIDToID(contact.getJID()); String nickname = contact.getNickname(); if (nickname == null || nickname.equals("")) { nickname = legacyId; } // Syncing takes care of all of the dirty work. ssiHierarchy.syncContactGroupsAndNickname(legacyId, nickname, (List<String>)contact.getGroups()); } /** * @see net.sf.kraken.session.TransportSession#acceptAddContact(JID) */ @Override public void acceptAddContact(JID jid) { final String userID = getTransport().convertJIDToID(jid); Log.debug("OSCAR: accept-adding is currently not implemented." + " Cannot accept-add: " + userID); } /** * @see net.sf.kraken.session.TransportSession#sendMessage(org.xmpp.packet.JID, String) */ @Override public void sendMessage(JID jid, String message) { SendImIcbm icbm = new SendImIcbm(getTransport().convertJIDToID(jid), message); // TODO: Should we consider checking to see if they really are offline? if (getTransport().getType().equals(TransportType.icq)) { icbm.setOffline(true); } request(icbm); } /** * @see net.sf.kraken.session.TransportSession#sendChatState(org.xmpp.packet.JID,net.sf.kraken.type.ChatStateType) */ @Override public void sendChatState(JID jid, ChatStateType chatState) { if (chatState.equals(ChatStateType.composing)) { request(new SendTypingNotification( getTransport().convertJIDToID(jid), SendTypingNotification.STATE_TYPING )); } else if (chatState.equals(ChatStateType.paused)) { request(new SendTypingNotification( getTransport().convertJIDToID(jid), SendTypingNotification.STATE_PAUSED )); } else if (chatState.equals(ChatStateType.inactive)) { request(new SendTypingNotification( getTransport().convertJIDToID(jid), SendTypingNotification.STATE_NO_TEXT )); } } /** * @see net.sf.kraken.session.TransportSession#sendBuzzNotification(org.xmpp.packet.JID, String) */ @Override public void sendBuzzNotification(JID jid, String message) { } /** * @see net.sf.kraken.session.TransportSession#updateLegacyAvatar(String, byte[]) */ @Override public void updateLegacyAvatar(String type, byte[] data) { ssiHierarchy.setIcon(type, data); } /** * Opens/creates a new BOS connection to a specific server and port, given a cookie. * * @param server Server to connect to. * @param port Port to connect to. * @param cookie Auth cookie. */ synchronized void startBosConn(String server, int port, ByteBlock cookie) { bosConn = new BOSConnection(new ConnDescriptor(server, port), this, cookie); bosConn.connect(); } /** * Registers the set of SNAC families that the given connection supports. * * @param conn FLAP connection to be registered. */ void registerSnacFamilies(BasicFlapConnection conn) { snacMgr.register(conn); } protected SnacManager snacMgr = new SnacManager(new PendingSnacListener() { public void dequeueSnacs(List<SnacRequest> pending) { for (SnacRequest request : pending) { handleRequest(request); } } }); synchronized void handleRequest(SnacRequest request) { Log.debug("Handling request "+request); int family = request.getCommand().getFamily(); if (snacMgr.isPending(family)) { snacMgr.addRequest(request); return; } BasicFlapConnection conn = snacMgr.getConn(family); if (conn != null) { conn.sendRequest(request); } else { // it's time to request a service if (!(request.getCommand() instanceof ServiceRequest)) { snacMgr.setPending(family, true); snacMgr.addRequest(request); request(new ServiceRequest(family)); } else { // TODO: Why does this occur a lot and yet not cause problems? Log.debug("eep! can't find a service redirector server."); } } } SnacRequest request(SnacCommand cmd) { Log.debug("Sending SNAC command: "+cmd); return request(cmd, null); } private SnacRequest request(SnacCommand cmd, SnacRequestListener listener) { Log.debug("Setting up SNAC request and listener: "+cmd+","+listener); SnacRequest req = new SnacRequest(cmd, listener); handleRequest(req); return req; } void connectToService(int snacFamily, String host, ByteBlock cookie) { Log.debug("Connection to service "+snacFamily+" on host "+host); ServiceConnection conn; if (snacFamily == MailCheckCmd.FAMILY_MAILCHECK) { conn = new EmailConnection(new ConnDescriptor(host, JiveGlobals.getIntProperty(propertyPrefix+".connectport", 5190)), this, cookie, snacFamily); } else { conn = new ServiceConnection(new ConnDescriptor(host, JiveGlobals.getIntProperty(propertyPrefix+".connectport", 5190)), this, cookie, snacFamily); } conn.connect(); } void serviceFailed(ServiceConnection conn) { Log.debug("OSCAR service failed: "+conn.toString()); } void serviceConnected(ServiceConnection conn) { Log.debug("OSCAR service connected: "+conn.toString()); services.add(conn); } public boolean isServiceConnected(ServiceConnection conn) { return services.contains(conn); } public Set<ServiceConnection> getServiceConnections() { return services; } void serviceReady(ServiceConnection conn) { Log.debug("OSCAR service ready: "+conn.toString()); snacMgr.dequeueSnacs(conn); } void serviceDied(ServiceConnection conn) { Log.debug("OSCAR service died: "+conn.toString()); services.remove(conn); snacMgr.unregister(conn); } /** * Apparently we now have the entire list, lets sync. */ void gotCompleteSSI() { ArrayList<Integer> nicknameRequests = new ArrayList<Integer>(); TransportBuddyManager<OSCARBuddy> manager = getBuddyManager(); for (OSCARBuddy buddy : manager.getBuddies()) { String nickname = buddy.getNickname(); buddy.populateGroupList(); for (BuddyItem buddyItem : buddy.getBuddyItems()) { if (buddyItem.isAwaitingAuth()) { buddy.setAskType(RosterItem.ASK_SUBSCRIBE); buddy.setSubType(RosterItem.SUB_NONE); } try { if (nickname.equalsIgnoreCase(buddyItem.getScreenname())) { Integer buddyUIN = Integer.parseInt(buddyItem.getScreenname()); Log.debug("REQUESTING SHORT INFO FOR "+buddyUIN); nicknameRequests.add(buddyUIN); } } catch (NumberFormatException e) { // Not an ICQ number then ;D } } } try { getTransport().syncLegacyRoster(getJID(), getBuddyManager().getBuddies()); } catch (UserNotFoundException e) { Log.debug("Unable to sync oscar contact list for " + getJID(), e); } getBuddyManager().activate(); request(new SetInfoCmd(InfoData.forCapabilities(getCapabilities()))); // if (JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".avatars", true) && getAvatar() != null) { // if (storedIconInfo == null || !StringUtils.encodeHex(storedIconInfo.getIconInfo().getData().toByteArray()).equals(getAvatar().getLegacyIdentifier())) { // try { // updateLegacyAvatar(getAvatar().getMimeType(), Base64.decode(getAvatar().getImageData())); // } // catch (NotFoundException e) { // // No avatar found, moving on // } // } // } updateStatus(getPresence(), getVerboseStatus()); ssiHierarchy.setVisibilityFlag(VisibilityItem.MASK_DISABLE_RECENT_BUDDIES); if (getTransport().getType().equals(TransportType.icq)) { request(new OfflineMsgIcqRequest(getUIN(), (int)nextIcqId())); } if (JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".mailnotifications", true)) { request(new ServiceRequest(MailCheckCmd.FAMILY_MAILCHECK)); } for (Integer uin : nicknameRequests) { MetaShortInfoRequest req = new MetaShortInfoRequest(getUIN(), (int)nextIcqId(), uin); Log.debug("Doing a MetaShortInfoRequest for "+uin+" as "+req); request(req); } } /** * @see net.sf.kraken.session.TransportSession#updateStatus(net.sf.kraken.type.PresenceType, String) */ @Override public void updateStatus(PresenceType presenceType, String verboseStatus) { if (getTransport().getType().equals(TransportType.icq)) { request(new SetExtraInfoCmd(((OSCARTransport)getTransport()).convertXMPPStatusToICQ(presenceType))); } if (presenceType != PresenceType.available && presenceType != PresenceType.chat) { String awayMsg = LocaleUtils.getLocalizedString("gateway.oscar.away", "kraken"); if (verboseStatus != null && verboseStatus.length() > 0) { awayMsg = verboseStatus; } request(new SetInfoCmd(InfoData.forAwayMessage(awayMsg))); if (!getTransport().getType().equals(TransportType.icq)) { presenceType = PresenceType.away; } } else { request(new SetInfoCmd(InfoData.forAwayMessage(InfoData.NOT_AWAY))); request(new SetExtraInfoCmd(new ExtraInfoBlock(ExtraInfoBlock.TYPE_AVAILMSG, ExtraInfoData.getAvailableMessageBlock(verboseStatus == null ? "" : verboseStatus)))); } setPresenceAndStatus(presenceType, verboseStatus); } /** * Retrieves the next ICQ id number and increments the counter. * @return The next ICQ id number. */ public long nextIcqId() { return icqSeqNum.next(); } /** * Retrieves a UIN in integer format for the session. * * @return The UIN in integer format. */ public int getUIN() { try { return Integer.parseInt(getRegistration().getUsername()); } catch (Exception e) { return -1; } } /** * Updates roster nickname information about a contact. * * @param sn Screenname/UIN of contact * @param nickname New nickname */ public void updateRosterNickname(String sn, String nickname) { try { TransportBuddy buddy = getBuddyManager().getBuddy(getTransport().convertIDToJID(sn)); buddy.setNickname(nickname); try { getTransport().addOrUpdateRosterItem(getJID(), buddy.getName(), buddy.getNickname(), buddy.getGroups()); } catch (UserNotFoundException e) { // Can't update something that's not really in our list. } } catch (NotFoundException e) { // Can't update something that's not really in our list. } } }