/* * 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.xmpp; import java.util.ArrayList; import java.util.Collection; import java.util.Timer; import java.util.TimerTask; import net.sf.kraken.avatars.Avatar; import net.sf.kraken.protocols.xmpp.mechanisms.FacebookConnectSASLMechanism; import net.sf.kraken.protocols.xmpp.mechanisms.MySASLDigestMD5Mechanism; import net.sf.kraken.protocols.xmpp.packet.BuzzExtension; import net.sf.kraken.protocols.xmpp.packet.GoogleMailBoxPacket; import net.sf.kraken.protocols.xmpp.packet.GoogleMailNotifyExtension; import net.sf.kraken.protocols.xmpp.packet.GoogleNewMailExtension; import net.sf.kraken.protocols.xmpp.packet.GoogleUserSettingExtension; import net.sf.kraken.protocols.xmpp.packet.IQWithPacketExtension; import net.sf.kraken.protocols.xmpp.packet.ProbePacket; import net.sf.kraken.protocols.xmpp.packet.VCardUpdateExtension; import net.sf.kraken.registration.Registration; import net.sf.kraken.session.TransportSession; import net.sf.kraken.type.*; import org.apache.log4j.Logger; import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.smack.*; import org.jivesoftware.smack.Roster.SubscriptionMode; import org.jivesoftware.smack.filter.OrFilter; import org.jivesoftware.smack.filter.PacketExtensionFilter; import org.jivesoftware.smack.filter.PacketTypeFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Presence.Type; import org.jivesoftware.smack.provider.ProviderManager; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.ChatState; import org.jivesoftware.smackx.packet.ChatStateExtension; import org.jivesoftware.smackx.packet.VCard; import org.jivesoftware.util.Base64; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.util.NotFoundException; import org.xmpp.packet.JID; /** * Handles XMPP transport session. * * @author Daniel Henninger * @author Mehmet Ecevit */ public class XMPPSession extends TransportSession<XMPPBuddy> { static Logger Log = Logger.getLogger(XMPPSession.class); /** * Create an XMPP Session instance. * * @param registration Registration information used for logging in. * @param jid JID associated with this session. * @param transport Transport instance associated with this session. * @param priority Priority of this session. */ public XMPPSession(Registration registration, JID jid, XMPPTransport transport, Integer priority) { super(registration, jid, transport, priority); setSupportedFeature(SupportedFeature.attention); setSupportedFeature(SupportedFeature.chatstates); Log.debug("Creating "+getTransport().getType()+" session for " + registration.getUsername()); String connecthost; Integer connectport; String domain; connecthost = JiveGlobals.getProperty("plugin.gateway."+getTransport().getType()+".connecthost", (getTransport().getType().equals(TransportType.gtalk) ? "talk.google.com" : getTransport().getType().equals(TransportType.facebook) ? "chat.facebook.com" : "jabber.org")); connectport = JiveGlobals.getIntProperty("plugin.gateway."+getTransport().getType()+".connectport", 5222); if (getTransport().getType().equals(TransportType.gtalk)) { domain = "gmail.com"; } else if (getTransport().getType().equals(TransportType.facebook)) { //if (connecthost.equals("www.facebook.com")) { connecthost = "chat.facebook.com"; //} //if (connectport.equals(80)) { connectport = 5222; //} domain = "chat.facebook.com"; } else if (getTransport().getType().equals(TransportType.renren)) { connecthost = "talk.renren.com"; connectport = 5222; domain = "renren.com"; } else { domain = connecthost; } // For different domains other than 'gmail.com', which is given with Google Application services if (registration.getUsername().indexOf("@") > -1) { domain = registration.getUsername().substring( registration.getUsername().indexOf("@")+1 ); } // If administrator specified "*" for domain, allow user to connect to anything. if (connecthost.equals("*")) { connecthost = domain; } config = new ConnectionConfiguration(connecthost, connectport, domain); config.setCompressionEnabled(JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".usecompression", false)); if (getTransport().getType().equals(TransportType.facebook)) { //SASLAuthentication.supportSASLMechanism("PLAIN", 0); //config.setSASLAuthenticationEnabled(false); //config.setSecurityMode(ConnectionConfiguration.SecurityMode.enabled); } // instead, send the initial presence right after logging in. This // allows us to use a different presence mode than the plain old // 'available' as initial presence. config.setSendPresence(false); if (getTransport().getType().equals(TransportType.gtalk) && JiveGlobals.getBooleanProperty("plugin.gateway.gtalk.mailnotifications", true)) { ProviderManager.getInstance().addIQProvider(GoogleMailBoxPacket.MAILBOX_ELEMENT, GoogleMailBoxPacket.MAILBOX_NAMESPACE, new GoogleMailBoxPacket.Provider()); ProviderManager.getInstance().addExtensionProvider(GoogleNewMailExtension.ELEMENT_NAME, GoogleNewMailExtension.NAMESPACE, new GoogleNewMailExtension.Provider()); } } /* * XMPP connection */ public XMPPConnection conn = null; /** * XMPP listener */ private XMPPListener listener = null; /** * Run thread. */ private Thread runThread = null; /** * Instance that will handle all presence stanzas sent from the legacy * domain */ private XMPPPresenceHandler presenceHandler = null; /* * XMPP connection configuration */ private final ConnectionConfiguration config; /** * Timer to check for online status. */ public Timer timer = new Timer(); /** * Interval at which status is checked. */ private int timerInterval = 60000; // 1 minute /** * Mail checker */ MailCheck mailCheck; /** * XMPP Resource - the resource we are using (randomly generated) */ public String xmppResource = StringUtils.randomString(10); /** * Returns a full JID based off of a username passed in. * * If it already looks like a JID, returns what was passed in. * * @param username Username to turn into a JID. * @return Converted username. */ public String generateFullJID(String username) { if (username.indexOf("@") > -1) { return username; } if (getTransport().getType().equals(TransportType.gtalk)) { return username+"@"+"gmail.com"; } else if (getTransport().getType().equals(TransportType.facebook)) { return username+"@"+"chat.facebook.com"; } else if (getTransport().getType().equals(TransportType.renren)) { return username+"@"+"renren.com"; } else if (getTransport().getType().equals(TransportType.livejournal)) { return username+"@"+"livejournal.com"; } else { String connecthost = JiveGlobals.getProperty("plugin.gateway."+getTransport().getType()+".connecthost", (getTransport().getType().equals(TransportType.gtalk) ? "talk.google.com" : getTransport().getType().equals(TransportType.facebook) ? "chat.facebook.com" : "jabber.org")); return username+"@"+connecthost; } } /** * Returns a username based off of a registered name (possible JID) passed in. * * If it already looks like a username, returns what was passed in. * * @param regName Registered name to turn into a username. * @return Converted registered name. */ public String generateUsername(String regName) { if (regName.equals("{PLATFORM}")) { return JiveGlobals.getProperty("plugin.gateway.facebook.platform.apikey")+"|"+JiveGlobals.getProperty("plugin.gateway.facebook.platform.apisecret"); } else if (regName.indexOf("@") > -1) { if (getTransport().getType().equals(TransportType.gtalk)) { return regName; } else { return regName.substring(0, regName.indexOf("@")); } } else { if (getTransport().getType().equals(TransportType.gtalk)) { return regName+"@gmail.com"; } else { return regName; } } } /** * @see net.sf.kraken.session.TransportSession#logIn(net.sf.kraken.type.PresenceType, String) */ @Override public void logIn(PresenceType presenceType, String verboseStatus) { final org.jivesoftware.smack.packet.Presence presence = new org.jivesoftware.smack.packet.Presence(org.jivesoftware.smack.packet.Presence.Type.available); if (JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".avatars", true) && getAvatar() != null) { Avatar avatar = getAvatar(); // Same thing in this case, so lets go ahead and set them. avatar.setLegacyIdentifier(avatar.getXmppHash()); VCardUpdateExtension ext = new VCardUpdateExtension(); ext.setPhotoHash(avatar.getLegacyIdentifier()); presence.addExtension(ext); } final Presence.Mode pMode = ((XMPPTransport) getTransport()) .convertGatewayStatusToXMPP(presenceType); if (pMode != null) { presence.setMode(pMode); } if (verboseStatus != null && verboseStatus.trim().length() > 0) { presence.setStatus(verboseStatus); } setPendingPresenceAndStatus(presenceType, verboseStatus); if (!this.isLoggedIn()) { listener = new XMPPListener(this); presenceHandler = new XMPPPresenceHandler(this); runThread = new Thread() { @Override public void run() { String userName = generateUsername(registration.getUsername()); conn = new XMPPConnection(config); try { conn.getSASLAuthentication().registerSASLMechanism("DIGEST-MD5", MySASLDigestMD5Mechanism.class); if (getTransport().getType().equals(TransportType.facebook) && registration.getUsername().equals("{PLATFORM}")) { conn.getSASLAuthentication().registerSASLMechanism("X-FACEBOOK-PLATFORM", FacebookConnectSASLMechanism.class); conn.getSASLAuthentication().supportSASLMechanism("X-FACEBOOK-PLATFORM", 0); } Roster.setDefaultSubscriptionMode(SubscriptionMode.manual); conn.connect(); conn.addConnectionListener(listener); try { conn.addPacketListener(presenceHandler, new PacketTypeFilter(org.jivesoftware.smack.packet.Presence.class)); // Use this to filter out anything we don't care about conn.addPacketListener(listener, new OrFilter( new PacketTypeFilter(GoogleMailBoxPacket.class), new PacketExtensionFilter(GoogleNewMailExtension.ELEMENT_NAME, GoogleNewMailExtension.NAMESPACE) )); conn.login(userName, registration.getPassword(), xmppResource); conn.sendPacket(presence); // send initial presence. conn.getChatManager().addChatListener(listener); conn.getRoster().addRosterListener(listener); if (JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".avatars", !TransportType.facebook.equals(getTransport().getType())) && getAvatar() != null) { new Thread() { @Override public void run() { Avatar avatar = getAvatar(); VCard vCard = new VCard(); try { vCard.load(conn); vCard.setAvatar(Base64.decode(avatar.getImageData()), avatar.getMimeType()); vCard.save(conn); } catch (XMPPException e) { Log.debug("XMPP: Error while updating vcard for avatar change.", e); } catch (NotFoundException e) { Log.debug("XMPP: Unable to find avatar while setting initial.", e); } } }.start(); } setLoginStatus(TransportLoginStatus.LOGGED_IN); syncUsers(); if (getTransport().getType().equals(TransportType.gtalk) && JiveGlobals.getBooleanProperty("plugin.gateway.gtalk.mailnotifications", true)) { conn.sendPacket(new IQWithPacketExtension(generateFullJID(getRegistration().getUsername()), new GoogleUserSettingExtension(null, true, null), IQ.Type.SET)); conn.sendPacket(new IQWithPacketExtension(generateFullJID(getRegistration().getUsername()), new GoogleMailNotifyExtension())); mailCheck = new MailCheck(); timer.schedule(mailCheck, timerInterval, timerInterval); } } catch (XMPPException e) { Log.debug(getTransport().getType()+" user's login/password does not appear to be correct: "+getRegistration().getUsername(), e); setFailureStatus(ConnectionFailureReason.USERNAME_OR_PASSWORD_INCORRECT); sessionDisconnectedNoReconnect(LocaleUtils.getLocalizedString("gateway.xmpp.passwordincorrect", "kraken")); } } catch (XMPPException e) { Log.debug(getTransport().getType()+" user is not able to connect: "+getRegistration().getUsername(), e); setFailureStatus(ConnectionFailureReason.CAN_NOT_CONNECT); sessionDisconnected(LocaleUtils.getLocalizedString("gateway.xmpp.connectionfailed", "kraken")); } } }; runThread.start(); } } /** * @see net.sf.kraken.session.TransportSession#logOut() */ @Override public void logOut() { cleanUp(); sessionDisconnectedNoReconnect(null); } /** * @see net.sf.kraken.session.TransportSession#cleanUp() */ @Override public void cleanUp() { if (timer != null) { try { timer.cancel(); } catch (Exception e) { // Ignore } timer = null; } if (mailCheck != null) { try { mailCheck.cancel(); } catch (Exception e) { // Ignore } mailCheck = null; } if (conn != null) { try { conn.removeConnectionListener(listener); } catch (Exception e) { // Ignore } try { conn.removePacketListener(listener); } catch (Exception e) { // Ignore } try { conn.removePacketListener(presenceHandler); } catch (Exception e) { // Ignore } try { conn.getChatManager().removeChatListener(listener); } catch (Exception e) { // Ignore } try { conn.getRoster().removeRosterListener(listener); } catch (Exception e) { // Ignore } try { conn.disconnect(); } catch (Exception e) { // Ignore } } conn = null; listener = null; presenceHandler = null; if (runThread != null) { try { runThread.interrupt(); } catch (Exception e) { // Ignore } runThread = null; } } /** * @see net.sf.kraken.session.TransportSession#updateStatus(net.sf.kraken.type.PresenceType, String) */ @Override public void updateStatus(PresenceType presenceType, String verboseStatus) { setPresenceAndStatus(presenceType, verboseStatus); final org.jivesoftware.smack.packet.Presence presence = constructCurrentLegacyPresencePacket(); try { conn.sendPacket(presence); } catch (IllegalStateException e) { Log.debug("XMPP: Not connected while trying to change status."); } } /** * @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 mail = getTransport().convertJIDToID(jid); try { conn.getRoster().createEntry(mail, nickname, groups.toArray(new String[groups.size()])); RosterEntry entry = conn.getRoster().getEntry(mail); getBuddyManager().storeBuddy(new XMPPBuddy(getBuddyManager(), mail, nickname, entry.getGroups(), entry)); } catch (XMPPException ex) { Log.debug("XMPP: unable to add:"+ mail); } } /** * @see net.sf.kraken.session.TransportSession#removeContact(net.sf.kraken.roster.TransportBuddy) */ @Override public void removeContact(XMPPBuddy contact) { RosterEntry user2remove; String mail = getTransport().convertJIDToID(contact.getJID()); user2remove = conn.getRoster().getEntry(mail); try { conn.getRoster().removeEntry(user2remove); } catch (XMPPException ex) { Log.debug("XMPP: unable to remove:"+ mail); } } /** * @see net.sf.kraken.session.TransportSession#updateContact(net.sf.kraken.roster.TransportBuddy) */ @Override public void updateContact(XMPPBuddy contact) { RosterEntry user2Update; String mail = getTransport().convertJIDToID(contact.getJID()); user2Update = conn.getRoster().getEntry(mail); user2Update.setName(contact.getNickname()); Collection<String> newgroups = contact.getGroups(); if (newgroups == null) { newgroups = new ArrayList<String>(); } for (RosterGroup group : conn.getRoster().getGroups()) { if (newgroups.contains(group.getName())) { if (!group.contains(user2Update)) { try { group.addEntry(user2Update); } catch (XMPPException e) { Log.debug("XMPP: Unable to add roster item to group."); } } newgroups.remove(group.getName()); } else { if (group.contains(user2Update)) { try { group.removeEntry(user2Update); } catch (XMPPException e) { Log.debug("XMPP: Unable to delete roster item from group."); } } } } for (String group : newgroups) { RosterGroup newgroup = conn.getRoster().createGroup(group); try { newgroup.addEntry(user2Update); } catch (XMPPException e) { Log.debug("XMPP: Unable to add roster item to new group."); } } } /** * @see net.sf.kraken.session.TransportSession#acceptAddContact(JID) */ @Override public void acceptAddContact(JID jid) { final String userID = getTransport().convertJIDToID(jid); Log.debug("XMPP: accept-add contact: " + userID); final Presence accept = new Presence(Type.subscribed); accept.setTo(userID); conn.sendPacket(accept); } /** * @see net.sf.kraken.session.TransportSession#sendMessage(org.xmpp.packet.JID, String) */ @Override public void sendMessage(JID jid, String message) { Chat chat = conn.getChatManager().createChat(getTransport().convertJIDToID(jid), listener); try { chat.sendMessage(message); } catch (XMPPException e) { // Ignore } } /** * @see net.sf.kraken.session.TransportSession#sendChatState(org.xmpp.packet.JID, net.sf.kraken.type.ChatStateType) */ @Override public void sendChatState(JID jid, ChatStateType chatState) { final Presence presence = conn.getRoster().getPresence(jid.toString()); if (presence == null || presence.getType().equals(Presence.Type.unavailable)) { // don't send chat state to contacts that are offline. return; } Chat chat = conn.getChatManager().createChat(getTransport().convertJIDToID(jid), listener); try { ChatState state = ChatState.active; switch (chatState) { case active: state = ChatState.active; break; case composing: state = ChatState.composing; break; case paused: state = ChatState.paused; break; case inactive: state = ChatState.inactive; break; case gone: state = ChatState.gone; break; } Message message = new Message(); message.addExtension(new ChatStateExtension(state)); chat.sendMessage(message); } catch (XMPPException e) { // Ignore } } /** * @see net.sf.kraken.session.TransportSession#sendBuzzNotification(org.xmpp.packet.JID, String) */ @Override public void sendBuzzNotification(JID jid, String message) { Chat chat = conn.getChatManager().createChat(getTransport().convertJIDToID(jid), listener); try { Message m = new Message(); m.setTo(getTransport().convertJIDToID(jid)); m.addExtension(new BuzzExtension()); chat.sendMessage(m); } catch (XMPPException e) { // Ignore } } /** * Returns a (legacy/Smack-based) Presence stanza that represents the * current presence of this session. The Presence includes relevant Mode, * Status and VCardUpdate information. * * This method uses the fields {@link TransportSession#presence} and * {@link TransportSession#verboseStatus} to generate the result. * * @return A Presence packet representing the current presence state of this * session. */ public Presence constructCurrentLegacyPresencePacket() { final org.jivesoftware.smack.packet.Presence presence = new org.jivesoftware.smack.packet.Presence( org.jivesoftware.smack.packet.Presence.Type.available); final Presence.Mode pMode = ((XMPPTransport) getTransport()) .convertGatewayStatusToXMPP(this.presence); if (pMode != null) { presence.setMode(pMode); } if (verboseStatus != null && verboseStatus.trim().length() > 0) { presence.setStatus(verboseStatus); } final Avatar avatar = getAvatar(); if (avatar != null) { final VCardUpdateExtension ext = new VCardUpdateExtension(); ext.setPhotoHash(avatar.getLegacyIdentifier()); presence.addExtension(ext); } return presence; } /** * @see net.sf.kraken.session.TransportSession#updateLegacyAvatar(String, byte[]) */ @Override public void updateLegacyAvatar(String type, final byte[] data) { new Thread() { @Override public void run() { Avatar avatar = getAvatar(); VCard vCard = new VCard(); try { vCard.load(conn); vCard.setAvatar(data, avatar.getMimeType()); vCard.save(conn); avatar.setLegacyIdentifier(avatar.getXmppHash()); // Same thing in this case, so lets go ahead and set them. final org.jivesoftware.smack.packet.Presence presence = constructCurrentLegacyPresencePacket(); conn.sendPacket(presence); } catch (XMPPException e) { Log.debug("XMPP: Error while updating vcard for avatar change.", e); } } }.start(); } private void syncUsers() { for (RosterEntry entry : conn.getRoster().getEntries()) { getBuddyManager().storeBuddy(new XMPPBuddy(getBuddyManager(), entry.getUser(), entry.getName(), entry.getGroups(), entry)); // Facebook does not support presence probes in their XMPP implementation. See http://developers.facebook.com/docs/chat#features if (!TransportType.facebook.equals(getTransport().getType())) { //ProbePacket probe = new ProbePacket(this.getJID()+"/"+xmppResource, entry.getUser()); ProbePacket probe = new ProbePacket(null, entry.getUser()); Log.debug("XMPP: Sending the following probe packet: "+probe.toXML()); try { conn.sendPacket(probe); } catch (IllegalStateException e) { Log.debug("XMPP: Not connected while trying to send probe."); } } } try { getTransport().syncLegacyRoster(getJID(), getBuddyManager().getBuddies()); } catch (UserNotFoundException ex) { Log.error("XMPP: User not found while syncing legacy roster: ", ex); } getBuddyManager().activate(); // lets repoll the roster since smack seems to get out of sync... // we'll let the roster listener take care of this though. conn.getRoster().reload(); } private class MailCheck extends TimerTask { /** * Check GMail for new mail. */ @Override public void run() { if (getTransport().getType().equals(TransportType.gtalk) && JiveGlobals.getBooleanProperty("plugin.gateway.gtalk.mailnotifications", true)) { GoogleMailNotifyExtension gmne = new GoogleMailNotifyExtension(); gmne.setNewerThanTime(listener.getLastGMailThreadDate()); gmne.setNewerThanTid(listener.getLastGMailThreadId()); conn.sendPacket(new IQWithPacketExtension(generateFullJID(getRegistration().getUsername()), gmne)); } } } }