/*
* Hamsam - Instant Messaging API
* Copyright (C) 2003 Raghu K
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package hamsam.protocol.msn;
import hamsam.api.Buddy;
import hamsam.api.Conference;
import hamsam.api.Message;
import hamsam.api.Response;
import hamsam.api.TextComponent;
import hamsam.exception.HamsamException;
import hamsam.exception.IllegalArgumentException;
import hamsam.exception.IllegalStateException;
import hamsam.net.ProxyInfo;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Vector;
/**
* Notification server is where the entire session is maintained. Almost everything happens
* here, including state changes (user goes offline), list retrieval and changes, chat
* requests, and email notification.
*/
class NotificationServer extends MsnServer
{
/**
* Event processor for this server.
*/
private EventProcessor processor;
/**
* MsnProtocol instance that owns this server.
*/
private MsnProtocol protocol;
/**
* Maps a group ID to the group name.
*/
private Hashtable allGroups;
/**
* List of all buddies.
*/
private Vector allBuddies;
/**
* Buddies in allow list.
*/
private Vector allowList;
/**
* Buddies in block list.
*/
private Vector blockList;
/**
* This is a mapping of all conference objects to their
* corresponding switchboard sessions.
*/
private Hashtable confSBMap;
/**
* Constructs a notification server with a specified event processor
* and proxy information.
*
* @param protocol the MsnProtocol instance that owns this server.
* @param processor the event processor for this server.
* @param host the host name to connect to.
* @param port the TCP/IP port to connect to.
* @param info proxy information to be used for connections.
* @throws UnknownHostException if host is not known.
* @throws IOException if an I/O error occurs while connecting to the host.
* @throws IllegalStateException if info is not initialized properly.
*/
NotificationServer(MsnProtocol protocol, EventProcessor processor, String host, int port, ProxyInfo info) throws java.io.IOException, hamsam.exception.IllegalStateException
{
super(host, port, info);
this.protocol = protocol;
this.processor = processor;
this.allGroups = new Hashtable();
this.allBuddies = new Vector();
this.allowList = new Vector();
this.blockList = new Vector();
this.confSBMap = new Hashtable();
Command cmd = new Command("VER");
cmd.addParam("MSNP8");
sendToServer(cmd, "processVersion");
}
/**
* Process the reply for VER command.
*
* @param cmd the reply from the server.
*/
void processVersion(AbstractCommand cmd)
{
if(cmd instanceof Command)
{
if("VER".equals(cmd.getType()) && "MSNP8".equals(cmd.getParam(0)))
{
Command cvr = new Command("CVR");
cvr.addParam("0x0409");
String os = System.getProperty("os.name").toLowerCase();
if(os.length() > 3)
os = os.substring(0, 3);
cvr.addParam(os);
cvr.addParam(Util.urlDecode(System.getProperty("os.version")));
cvr.addParam(Util.urlEncode(System.getProperty("os.arch")));
cvr.addParam("Hamsam");
cvr.addParam("0.2.3");
cvr.addParam("Hamsam");
cvr.addParam(Util.urlEncode(this.protocol.getUsername()));
sendToServer(cvr, "processClientVersion");
return;
}
}
shutdown();
processor.connectFailed("Version transaction failed - " + cmd);
}
/**
* Process the reply for CVR command.
*
* @param cmd the reply from the server.
*/
void processClientVersion(AbstractCommand cmd)
{
if(cmd instanceof Command)
{
if("CVR".equals(cmd.getType()))
{
Command usr = new Command("USR");
usr.addParam("TWN");
usr.addParam("I"); // I for initiate
usr.addParam(protocol.getUsername());
sendToServer(usr, "processAuthInitial");
return;
}
}
shutdown();
processor.connectFailed("Client versioning was rejected - " + cmd);
}
/**
* Process the reply for initial USR command.
*
* @param cmd the reply from the server.
*/
void processAuthInitial(AbstractCommand cmd)
{
if(cmd instanceof Command)
{
if("USR".equals(cmd.getType()) && "TWN".equals(cmd.getParam(0)) && "S".equals(cmd.getParam(1)))
{
try
{
String challenge = cmd.getParam(2);
String str = Util.passportAuthenticate(
protocol.getUsername(),
protocol.getPassword(),
challenge,
protocol.getProxyInfo());
if(str == null)
{
processor.connectFailed("Passport authentication failed");
return;
}
Command usr = new Command("USR");
usr.addParam("TWN");
usr.addParam("S"); // S for subsequent message
usr.addParam(str);
sendToServer(usr, "processAuthSubsequent");
return;
}
catch(IOException e)
{
shutdown();
processor.connectFailed("I/O error while authenticating");
e.printStackTrace(); //TODO change this to log statement
return;
}
}
}
shutdown();
processor.connectFailed("Unknown authentication mechanism - " + cmd);
}
/**
* Process the reply for subsequent USR command.
*
* @param cmd the reply from the server.
*/
void processAuthSubsequent(AbstractCommand cmd)
{
if(cmd instanceof Command)
{
if("USR".equals(cmd.getType()) && "OK".equals(cmd.getParam(0)))
{
processor.connected();
// Now that we are logged in, try to get all lists
Command syn = new Command("SYN");
syn.addParam("0");
sendToServer(syn, "processSyncLists");
return;
}
}
shutdown();
processor.connectFailed("Authentication failed - " + cmd);
}
private int lstReplyCount;
private int lstReceivedCount;
/**
* Process the reply for SYN command.
*
* @param cmd the reply from the server.
*/
void processSyncLists(AbstractCommand cmd)
{
if(cmd instanceof Command)
{
if("SYN".equals(cmd.getType()))
{
lstReplyCount = Integer.parseInt(cmd.getParam(1));
}
}
}
/**
* Process the reply for BLP command.
*
* @param cmd the reply from the server.
*/
void processPrivacySetting(AbstractCommand cmd)
{
if(cmd instanceof Command)
{
if("BLP".equals(cmd.getType()) && "AL".equals(cmd.getParam(1)))
{
Command chg = new Command("CHG");
chg.addParam("NLN");
sendToServer(chg, "processInitialStatus");
return;
}
}
shutdown();
processor.connectFailed("Unable to update privacy setting - " + cmd);
}
/**
* Process the reply for CHG command.
*
* @param cmd the reply from the server.
*/
void processInitialStatus(AbstractCommand cmd)
{
if(cmd instanceof Command)
{
if("CHG".equals(cmd.getType()) && "NLN".equals(cmd.getParam(0)))
{
return;
}
else if("ILN".equals(cmd.getType()))
{
String statusCode = cmd.getParam(0);
String passport = cmd.getParam(1);
String alias = cmd.getParam(2);
if(statusCode != null && passport != null)
{
Buddy buddy = new Buddy(protocol, passport);
buddy.setAlias(alias);
buddy.setStatus(Util.getVerboseStatus(statusCode));
processor.buddyStatusChanged(buddy);
}
return;
}
}
shutdown();
processor.connectFailed("Unable to update initial status - " + cmd);
}
/**
* Change the status message.
*
* @param code the new status code.
*/
void changeStatus(String code)
{
Command chg = new Command("CHG");
chg.addParam(code);
sendToServer(chg, "doNothing");
}
/**
* Starts a switchboard session for conferences.
*
* @param conf the conference that uses this switchboard session.
*/
void startSBSession(Conference conf)
{
// request a switchboard transfer
Command xfr = new Command("XFR");
xfr.addParam("SB");
sendToServer(xfr, "processSwitchSB");
// save the conference object till we get a response
confSBMap.put(conf, new Object());
}
/**
* Process the response for a switchboard transfer request.
*
* @param cmd the reply from the server.
*/
void processSwitchSB(AbstractCommand cmd)
{
if(!(cmd instanceof Command) || !"XFR".equals(cmd.getType()) || !"SB".equals(cmd.getParam(0)))
return;
String server = cmd.getParam(1);
String hash = cmd.getParam(3);
if(server == null || hash == null)
return;
int index = server.indexOf(':');
if(index == -1)
return;
// Now find a pending conference request
Enumeration e = confSBMap.keys();
Conference conf = null;
while(e.hasMoreElements())
{
conf = (Conference) e.nextElement();
Object sb = confSBMap.get(conf);
if(!(sb instanceof SwitchboardServer))
break;
else
conf = null;
}
if(conf == null)
return;
try
{
String host = server.substring(0, index);
int port = Integer.parseInt(server.substring(index + 1));
SwitchboardServer sb = new SwitchboardServer(protocol, processor, hash, host, port, conf, protocol.getProxyInfo());
synchronized(confSBMap)
{
confSBMap.put(conf, sb);
confSBMap.notify();
}
}
catch(NumberFormatException nfe)
{
// TODO - when we implement API logging, this has to be changed
nfe.printStackTrace();
System.exit(1);
}
catch(IOException ioe)
{
Message msg = new Message();
msg.addComponent(new TextComponent("Unable to initiate an IM session: " + ioe.getMessage()));
processor.protocolMessageReceived(msg);
}
catch(IllegalStateException ise)
{
Message msg = new Message();
msg.addComponent(new TextComponent("Unable to initiate an IM session: " + ise.getMessage()));
processor.protocolMessageReceived(msg);
}
}
/**
* Quit from a conference.
*
* @param conf the conference to quit from
*/
void quitSBSession(Conference conf)
{
SwitchboardServer sb = (SwitchboardServer) confSBMap.get(conf);
if(sb != null)
{
Command out = new Command("OUT");
sb.sendToServer(out, "doNothing");
sb.shutdown();
confSBMap.remove(conf);
}
}
/**
* Send a conference message.
*
* @param conf the conference to send message to.
* @param message the message to be sent.
*/
void sendConferenceMessage(Conference conf, Message message)
{
SwitchboardServer sb = (SwitchboardServer) confSBMap.get(conf);
if(sb != null)
sb.sendMessage(message);
else
{
// TODO How do we notify this error condition???
}
}
/**
* Adds a buddy to the forward list.
*
* @param buddy the buddy to be added.
* @throws IllegalArgumentException if the group of the buddy is not specified.
*/
void addBuddyToForwardList(Buddy buddy) throws IllegalArgumentException
{
String group = buddy.getGroup();
if(group == null)
throw new IllegalArgumentException("Group name of the buddy is not specified");
Command add = new Command("ADD");
add.addParam("FL");
String username = buddy.getUsername();
add.addParam(username);
String alias = buddy.getAlias();
alias = alias == null ? username : alias;
add.addParam(alias);
Enumeration e = allGroups.keys();
while(e.hasMoreElements())
{
Integer groupID = (Integer) e.nextElement();
String groupName = (String) allGroups.get(groupID);
if(group.equals(groupName))
{
add.addParam(groupID.toString());
sendToServer(add, "processBuddyAdd");
return;
}
}
int groupID = addNewBuddyGroup(group);
if(groupID != -1)
{
add.addParam(String.valueOf(groupID));
sendToServer(add, "processBuddyAdd");
}
}
/**
* Removes a buddy from the forward list.
*
* @param buddy the buddy to be added.
* @throws IllegalArgumentException if the group of the buddy is not specified.
*/
void removeBuddyFromForwardList(Buddy buddy) throws IllegalArgumentException
{
String group = buddy.getGroup();
if(group == null)
throw new IllegalArgumentException("Group name of the buddy is not specified");
Command add = new Command("REM");
add.addParam("FL");
String username = buddy.getUsername();
add.addParam(username);
Enumeration e = allGroups.keys();
while(e.hasMoreElements())
{
Integer groupID = (Integer) e.nextElement();
String groupName = (String) allGroups.get(groupID);
if(group.equals(groupName))
{
add.addParam(groupID.toString());
sendToServer(add, "processBuddyRemove");
return;
}
}
processor.buddyDeleteFailed(buddy, "Buddy group does not exist - " + group);
}
/**
* Adds a buddy to the blocked list.
*
* @param buddy the buddy to be added.
*/
void addBuddyToBlockedList(Buddy buddy)
{
Command add = new Command("ADD");
add.addParam("BL");
String username = buddy.getUsername();
add.addParam(username);
String alias = buddy.getAlias();
alias = alias == null ? username : alias;
add.addParam(alias);
sendToServer(add, "processBuddyBlock");
}
/**
* Removes a buddy from the blocked list.
*
* @param buddy the buddy to be added.
*/
void removeBuddyFromBlockedList(Buddy buddy)
{
Command add = new Command("REM");
add.addParam("BL");
String username = buddy.getUsername();
add.addParam(username);
sendToServer(add, "processBuddyUnblock");
}
/**
* Create a new buddy group. This method blocks until it gets
* a reply from the MSN server.
*
* @param group the name of the group to be added
* @return the group id of the newly created group
*/
private int addNewBuddyGroup(String group)
{
Command adg = new Command("ADG");
adg.addParam(Util.urlEncode(group));
adg.addParam("0");
AbstractCommand cmd = sendToServer(adg);
if(cmd instanceof Command)
{
if("ADG".equals(cmd.getType()))
{
return Integer.parseInt(cmd.getParam(2));
}
}
return -1;
}
/**
* This method does nothing. It used for callback where we don't have
* anything to do.
*/
void doNothing(AbstractCommand cmd)
{
}
/**
* Processes messages received from MSN server.
*
* @param msg the message received from server.
*/
protected synchronized void processMessage(MsnMessage msg)
{
// Handle E-mail notifications
String type = msg.getHeaderField("Content-Type");
if("text/x-msmsgsinitialemailnotification".equals(type))
{
// This is the initial E-mail notification
String body = msg.getBody();
int linePos = body.indexOf("Inbox-Unread:");
if(linePos != -1)
{
int lineEnd = body.indexOf(body, linePos + 13);
if(lineEnd != -1)
{
int count = Integer.parseInt(body.substring(linePos + 13, lineEnd).trim());
processor.mailNotificationReceived(count, null, null);
}
}
}
else if("application/x-msmsgsemailnotification".equals(type))
{
// This is a real time E-mail notification
// TODO - add MIME parser
}
}
/**
* Processes asynchronous commands received from MSN server.
*
* @param cmd the command received from server.
*/
protected void processAsyncCommand(AbstractCommand cmd)
{
String type = cmd.getType();
if("LSG".equals(type))
{
Integer groupID = new Integer(cmd.getParam(0));
String groupName = Util.urlDecode(cmd.getParam(1));
allGroups.put(groupID, groupName);
}
else if("LST".equals(type))
{
String passport = cmd.getParam(0);
String alias = Util.urlDecode(cmd.getParam(1));
int memberLists = Integer.parseInt(cmd.getParam(2));
Integer[] memberGroups = null;
if(cmd.getParamCount() == 4)
{
StringTokenizer tok = new StringTokenizer(cmd.getParam(3), ",");
memberGroups = new Integer[tok.countTokens()];
for(int i = 0; tok.hasMoreTokens(); i++)
memberGroups[i] = new Integer(tok.nextToken());
}
if((memberLists & 1) != 0) // This buddy is in FL
{
for(int i = 0; i < memberGroups.length; i++)
{
String group = (String) allGroups.get(memberGroups[i]);
Buddy buddy = new Buddy(protocol, passport, group);
buddy.setAlias(alias);
allBuddies.add(buddy);
}
}
if((memberLists & 2) != 0) // This buddy is in AL
{
Buddy buddy = new Buddy(protocol, passport);
buddy.setAlias(alias);
allowList.add(buddy);
}
if((memberLists & 4) != 0) // This buddy is in BL
{
Buddy buddy = new Buddy(protocol, passport);
buddy.setAlias(alias);
blockList.add(buddy);
}
// Check if we received all LST commands
if(++lstReceivedCount == lstReplyCount)
{
// Send the buddy list to the client
Buddy[] buddies = (Buddy[]) allBuddies.toArray(new Buddy[0]);
processor.buddyListReceived(buddies);
Command blp = new Command("BLP");
blp.addParam("AL");
sendToServer(blp, "processPrivacySetting");
}
}
else if("CHL".equals(type))
{
String challenge = cmd.getParam(1);
if(challenge != null)
handleChallenge(challenge);
return;
}
else if("RNG".equals(type))
handleSBInvitation(cmd);
else if("NLN".equals(type))
{
String passport = cmd.getParam(1);
String alias = Util.urlDecode(cmd.getParam(2));
String status = Util.getVerboseStatus(cmd.getParam(0));
Buddy buddy = new Buddy(this.protocol, passport);
buddy.setAlias(alias);
buddy.setStatus(status);
processor.buddyStatusChanged(buddy);
}
else if("FLN".equals(type))
{
String passport = cmd.getParam(0);
Buddy buddy = new Buddy(this.protocol, passport);
buddy.setStatus(null);
processor.buddyStatusChanged(buddy);
}
}
/**
* Respond to a CHL from the MSN server.
*
* @param challenge the challege string sent by the server
*/
private void handleChallenge(String challenge)
{
try
{
String str = challenge + "Q1P7W2E4J9R8U3S5";
MsnQuery query = new MsnQuery(Util.getMD5Hash(str));
sendToServer(query, "processQueryResult");
}
catch(NoSuchAlgorithmException e)
{
shutdown();
processor.disconnected();
}
}
/**
* Process a swicthboard session invitation.
*
* @param cmd the RNG command sent by the server for invitation.
*/
private void handleSBInvitation(AbstractCommand cmd)
{
String sessionID = cmd.getParam(0);
String server = cmd.getParam(1);
String authString = cmd.getParam(3);
String passport = cmd.getParam(4);
String alias = Util.urlDecode(cmd.getParam(5));
try
{
// send the invitation
Buddy host = new Buddy(protocol, passport);
host.setAlias(alias);
Buddy[] buddies = new Buddy[] {host};
Conference conf = new Conference(protocol, host, buddies);
Response res = processor.conferenceInvitationReceived(conf, "");
if(res.isAccepted())
joinSBSession(sessionID, server, authString, conf);
}
catch(HamsamException e)
{
// this won't be thrown, ever
}
}
/**
* Process the reply for QRY command.
*
* @param cmd the reply from the server.
*/
void processQueryResult(AbstractCommand cmd)
{
if(cmd instanceof Command && "QRY".equals(cmd.getType()))
return;
shutdown();
processor.disconnected();
}
/**
* Returns an existing SB session with a single buddy.
*
* @param buddy the only buddy in this SB session.
* @return the SB server having this session.
*/
SwitchboardServer getExistingSBSession(Buddy buddy)
{
Enumeration e = confSBMap.elements();
while(e.hasMoreElements())
{
SwitchboardServer server = (SwitchboardServer) e.nextElement();
if(server.isIMSession(buddy))
return server;
}
return null;
}
/**
* Join a SB session when we are invited.
*
* @param sessionID session ID of this SB session.
* @param server the server to connect to in the form hostname:port
* @param authString the authentication string to be used for joining
* @param conf the conference object representing this SB session.
*/
void joinSBSession(String sessionID, String server, String authString, Conference conf)
{
int index = server.indexOf(':');
if(index == -1)
return;
try
{
String host = server.substring(0, index);
int port = Integer.parseInt(server.substring(index + 1));
ProxyInfo info = protocol.getProxyInfo();
SwitchboardServer sb = new SwitchboardServer(protocol, processor, host, port, info, conf);
confSBMap.put(conf, sb);
sb.join(authString, sessionID);
}
catch(IOException e)
{
Message msg = new Message();
msg.addComponent(new TextComponent("Unable to initiate an IM session: " + e.getMessage()));
processor.protocolMessageReceived(msg);
}
catch(IllegalStateException e)
{
Message msg = new Message();
msg.addComponent(new TextComponent("Unable to initiate an IM session: " + e.getMessage()));
processor.protocolMessageReceived(msg);
}
}
/**
* Returns an SB server corresponding to this conference. If nothing is
* created yet, wait till one becomes available.
*
* @param conf conference for which SB server is to be returned
* @return SB server for this conference object
*/
SwitchboardServer waitForSBSession(Conference conf)
{
if(!confSBMap.containsKey(conf))
return null;
else
{
while(true)
{
try
{
synchronized(confSBMap)
{
Object obj = confSBMap.get(conf);
if(obj != null && obj instanceof SwitchboardServer)
return (SwitchboardServer) obj;
else
confSBMap.wait();
}
}
catch(InterruptedException e)
{
// No one interrupts me
}
}
}
}
/**
* Invoked when an SB session terminates abnormally.
*
* @param server the SB server that terminated.
*/
void switchBoardTerminated(SwitchboardServer server)
{
Enumeration e = confSBMap.keys();
while(e.hasMoreElements())
{
Conference conf = (Conference) e.nextElement();
if(server.equals(confSBMap.get(conf)))
{
confSBMap.remove(conf);
return;
}
}
}
/**
* Invoked when the associated reader thread exits abnormally. This
* method shuts down the IM session.
*/
protected void readerExited()
{
shutdown();
processor.disconnected();
}
/**
* Invoked when the associated writer thread exits abnormally. This
* method shuts down the IM session.
*/
protected void writerExited()
{
shutdown();
processor.disconnected();
}
}