blob: 49c4b5a8ea7cb9b5e2a7d9a17618b6a938716cb8 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2008 Remy Suen and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Remy Suen <remy.suen@gmail.com> - initial API and implementation
* Cagatay Calli <ccalli@gmail.com> - https://bugs.eclipse.org/bugs/show_bug.cgi?id=196812
* David Pochet <pochet.david@wanadoo.fr> - https://bugs.eclipse.org/bugs/show_bug.cgi?id=195275
******************************************************************************/
package org.eclipse.ecf.protocol.msn;
import java.io.*;
import java.net.ConnectException;
import java.net.URLDecoder;
import java.util.ArrayList;
import org.eclipse.ecf.protocol.msn.events.ISessionListener;
import org.eclipse.ecf.protocol.msn.internal.encode.*;
import org.eclipse.ecf.protocol.msn.internal.net.ClientTicketRequest;
/**
* <p>
* The NotificationSession manages all incoming and outgoing packets that
* concerns status changes in contacts, client pings, challenge strings, and
* others.
* </p>
*
* <p>
* <b>Note:</b> This class/interface is part of an interim API that is still
* under development and expected to change significantly before reaching
* stability. It is being made available at this early stage to solicit feedback
* from pioneering adopters on the understanding that any code that uses this
* API will almost certainly be broken (repeatedly) as the API evolves.
* </p>
*/
final class NotificationSession extends DispatchSession {
private final ContactList list;
/**
* The ClientTicketRequest that this NotificationSession will use to obtain
* the client ticket associated with the user's MSN email address username
* and password.
*/
private ClientTicketRequest request;
/**
* The ResponseCommand used to process responses from the server.
*/
private ResponseCommand response;
private Thread pingingThread;
/**
* The address of the alternate server that has been read in that the client
* should try redirecting its notification session to connect to.
*/
private String alternateServer;
/**
* The user's MSN email address.
*/
private String username;
/**
* Creates a new NotificationSession that will connect to the given host.
*
* @param client
* the MsnClient to hook onto
*/
NotificationSession(MsnClient client) {
super(client);
list = client.getContactList();
listeners = new ArrayList();
request = new ClientTicketRequest();
}
/**
* Returns whether the user has connected with the notification server
* successfully or not. If the connecting process failed, {@link #reset()}
* should be called so that a connection attempt can be made to the server
* that the user has been redirected to.
*
* @param userEmail
* the user's MSN email address login
* @param password
* the user's password
* @return <tt>true</tt> if the login completed successfully,
* <tt>false</tt> otherwise
* @throws IOException
* If an I/O error occurs while attempting to authenticate with
* the servers
*/
boolean login(String userEmail, String password) throws IOException {
response = connect(userEmail);
if (response.getCommand().equals("USR")) { //$NON-NLS-1$
String ticket = request.getTicket(userEmail, password, response.getParam(3));
password = null;
if (ticket == null) {
throw new ConnectException("Wrong username and/or password."); //$NON-NLS-1$
}
write("USR", "TWN S " + ticket); //$NON-NLS-1$ //$NON-NLS-2$
ticket = null;
String input = super.read();
if (!input.startsWith("USR")) { //$NON-NLS-1$
throw new ConnectException("An error occurred while attempting to authenticate with the Tweener server."); //$NON-NLS-1$
}
retrieveBuddyList();
this.username = userEmail;
return true;
} else if (!response.getCommand().equals("XFR")) { //$NON-NLS-1$
throw new ConnectException("Unable to connect to the MSN server."); //$NON-NLS-1$
} else {
alternateServer = response.getParam(2);
return false;
}
}
private void retrieveBuddyList() throws IOException {
write("SYN", "0 0"); //$NON-NLS-1$ //$NON-NLS-2$
BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(), "UTF-8")); //$NON-NLS-1$
String input = reader.readLine();
while (input == null || !input.startsWith("SYN")) { //$NON-NLS-1$
input = reader.readLine();
}
String[] split = StringUtils.splitOnSpace(input);
int contacts = Integer.parseInt(split[4]);
while (!input.startsWith("LST")) { //$NON-NLS-1$
if (input.startsWith("PRP MFN")) { //$NON-NLS-1$
client.internalSetDisplayName(StringUtils.splitSubstring(input, " ", 2)); //$NON-NLS-1$
} else if (input.startsWith("LSG")) { //$NON-NLS-1$
split = StringUtils.splitOnSpace(input);
list.addGroup(split[2], new Group(URLDecoder.decode(split[1])));
}
input = reader.readLine();
}
int count = 0;
while (true) {
if (input.startsWith("LST")) { //$NON-NLS-1$
count++;
String[] contact = StringUtils.splitOnSpace(input);
String email = contact[1].substring(2);
// Check that email address is valid
if (email.indexOf('@') != -1) {
switch (contact.length) {
case 3 :
list.internalAddContact(email, email);
break;
case 5 :
list.addContact(email, email, contact[3].substring(2));
break;
default :
list.addContact(contact[2].substring(2), email, contact[3].substring(2), contact[5]);
break;
}
}
if (count == contacts) {
break;
}
}
input = reader.readLine();
}
write("CHG", client.getStatus().getLiteral() + " 268435488"); //$NON-NLS-1$ //$NON-NLS-2$
idle();
ping();
}
/**
* This method is invoked with another user has invited the client to a
* switchboard session. The created session will answer back and then invoke
* the {@link Session#idle()} method to begin processing incoming and
* outgoing requests.
*
* @param data
* a String array containing the request that the other user has
* sent to the client
* @throws IOException
* If an I/O error occurs while the ChatSession is created to
* handle this request.
*/
private void processSwitchboardRequest(String[] data) throws IOException {
ChatSession ss = new ChatSession(data[2], client);
ss.write("ANS", username + ' ' + data[4] + ' ' + data[1]); //$NON-NLS-1$
ss.read();
fireSwitchboardConnectedEvent(ss);
// ss.read();
ss.idle();
}
/**
* Sends a request to the notification server for a switchboard server's
* information.
*
* @return the ResponseCommand that represents the notification server's
* output
* @throws IOException
* If an I/O error occurred while reading or writing data.
*/
ResponseCommand getChatSession() throws IOException {
if (client.getStatus() == Status.APPEAR_OFFLINE || client.getStatus() == Status.OFFLINE) {
throw new ConnectException("Switchboards cannot be created when the user is hidden or offline."); //$NON-NLS-1$
}
write("XFR", "SB"); //$NON-NLS-1$ //$NON-NLS-2$
String command = response.getCommand();
while (command == null || !command.equals("XFR")) { //$NON-NLS-1$
command = response.getCommand();
}
return response;
}
/**
* Closes the connection to the original host and then open up a new
* connection to the redirected host. A new call to
* {@link #login(String, String)} should be made after this method has
* returned.
*/
void reset() throws IOException {
close();
openSocket(alternateServer);
request.setCancelled(false);
}
/**
* Read the contents of the packet being sent from the server and handle any
* events accordingly.
*/
String read() throws IOException {
String input = super.read();
if (input == null) {
return null;
}
if (input.indexOf("ILN") != -1) { //$NON-NLS-1$
String[] events = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < events.length; i++) {
if (!events[i].trim().equals("") //$NON-NLS-1$
&& events[i].substring(1, 3).equals("LN")) { //$NON-NLS-1$
String[] sub = StringUtils.split(events[i], " ", 3); //$NON-NLS-1$
String[] split = StringUtils.splitOnSpace(sub[2]);
changeContactInfo(split);
}
}
} else if (input.indexOf("FLN") != -1) { //$NON-NLS-1$
String[] events = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < events.length; i++) {
if (events[i].startsWith("FLN")) { //$NON-NLS-1$
setContactToOffline(events[i]);
}
}
} else if (input.indexOf("LN") != -1) { //$NON-NLS-1$
String[] events = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < events.length; i++) {
if (events[i].substring(0, 3).equals("NLN")) { //$NON-NLS-1$
String[] sub = StringUtils.splitOnSpace(events[i].substring(4));
changeContactInfo(sub);
} else if (events[i].substring(1, 3).equals("LN")) { //$NON-NLS-1$
String[] sub = StringUtils.split(events[i], " ", 3); //$NON-NLS-1$
String[] split = StringUtils.splitOnSpace(sub[2]);
changeContactInfo(split);
}
}
} else if (input.indexOf("CHL") != -1) { //$NON-NLS-1$
// the read input is a challenge string
String query = Challenge.createQuery(StringUtils.splitSubstring(input, " ", 2)); //$NON-NLS-1$
write("QRY", Challenge.PRODUCT_ID + ' ' + query.length() + "\r\n" //$NON-NLS-1$ //$NON-NLS-2$
+ query, false);
} else if (input.indexOf("RNG") != -1) { //$NON-NLS-1$
processSwitchboardRequest(StringUtils.splitOnSpace(input));
}
if (input.indexOf("XFR") != -1) { //$NON-NLS-1$
String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < split.length; i++) {
if (split[i].startsWith("XFR")) { //$NON-NLS-1$
response.process(split[i]);
}
}
}
if (input.indexOf("UBX") != -1) { //$NON-NLS-1$
String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < split.length; i++) {
if (split[i].startsWith("UBX")) { //$NON-NLS-1$
processContactData(split, i);
}
}
}
if (input.indexOf("ADC") != -1) { //$NON-NLS-1$
String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < split.length; i++) {
if (split[i].startsWith("ADC")) { //$NON-NLS-1$
String[] subSplit = StringUtils.splitOnSpace(split[i]);
if (subSplit[2].equals("FL")) { //$NON-NLS-1$
processContactAdded(subSplit[3].substring(2), subSplit[4].substring(2), subSplit[5].substring(2));
} else if (subSplit[2].equals("RL")) { //$NON-NLS-1$
processContactAddedUser(subSplit[3].substring(2));
}
}
}
}
if (input.indexOf("REM") != -1) { //$NON-NLS-1$
String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < split.length; i++) {
if (split[i].startsWith("REM")) { //$NON-NLS-1$
String[] subSplit = StringUtils.splitOnSpace(split[i]);
if (subSplit[2].equals("FL")) { //$NON-NLS-1$
processContactRemoved(subSplit[3]);
} else if (subSplit[2].equals("RL")) { //$NON-NLS-1$
processContactRemovedUser(subSplit[3]);
}
}
}
}
if (input.indexOf("OUT OTH") != -1) { //$NON-NLS-1$
String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < split.length; i++) {
if (split[i].startsWith("OUT OTH")) { //$NON-NLS-1$
close();
break;
}
}
}
return input;
}
void close() {
request.setCancelled(true);
if (pingingThread != null) {
pingingThread.interrupt();
}
super.close();
}
/**
* Create a new thread that will ping the host every sixty seconds to keep
* this connection alive.
*/
private void ping() {
pingingThread = new Thread() {
public void run() {
try {
while (true) {
sleep(60000);
write("PNG"); //$NON-NLS-1$
}
} catch (IOException e) {
// ignored
} catch (InterruptedException e) {
// ignored
}
}
};
pingingThread.start();
}
private void processContactAdded(String email, String contactName, String guid) {
list.addContact(email, contactName, guid);
}
private void processContactRemoved(String guid) {
list.fireContactRemoved(guid);
}
private void processContactAddedUser(String email) {
list.fireContactAddedUser(email);
}
private void processContactRemovedUser(String email) {
list.fireContactRemovedUser(email);
}
/**
* Checks whether a contact has changed his or her personal message or
* current media.
*
* @param eventString
* a String array containing the notification server's
* information
* @param index
* the index of the String array that processing should begin
*/
private void processContactData(String[] eventString, int index) {
if (eventString.length == index + 1 || StringUtils.splitSubstring(eventString[index], " ", 2) //$NON-NLS-1$
.equals("0")) { //$NON-NLS-1$
eventString = StringUtils.splitOnSpace(eventString[index]);
Contact contact = list.getContact(eventString[1]);
if (contact != null) {
contact.setPersonalMessage(""); //$NON-NLS-1$
}
return;
}
String data = eventString[index + 1];
eventString = StringUtils.splitOnSpace(eventString[index]);
Contact contact = list.getContact(eventString[1]);
contact.setPersonalMessage(data.substring(data.indexOf("<PSM>") + 5, //$NON-NLS-1$
data.indexOf("</PSM>"))); //$NON-NLS-1$
// TODO: set media
}
/**
* Changes the contact's status based on the string array that has been
* received from the notification server.
*
* @param eventString
* a formatted String literal obtained from an incoming message
*/
private void changeContactInfo(String[] eventString) {
// we are not interested in our own changes
if (!eventString[1].equals(client.getUserEmail())) {
Contact contact = list.getContact(eventString[1]);
contact.setStatus(Status.getStatus(eventString[0]));
contact.setDisplayName(eventString[2]);
}
}
/**
* Changes the contact specified by the notification server to be an offline
* status.
*
* @param eventString
* a formatted String literal obtained from an incoming message
*/
private void setContactToOffline(String eventString) {
String email = StringUtils.splitSubstring(eventString, " ", 1); //$NON-NLS-1$
list.getContact(email).setStatus(Status.OFFLINE);
}
/**
* Fires an event to all attached notification listeners to indicate that
* the specified chat session has been connected to.
*
* @param session
* the chat session that has been connected to
*/
private void fireSwitchboardConnectedEvent(ChatSession session) {
synchronized (listeners) {
for (int i = 0; i < listeners.size(); i++) {
((ISessionListener) listeners.get(i)).sessionConnected(session);
}
}
}
/**
* Adds the provided ISessionListener to this notification session.
*
* @param listener
* the listener to add
*/
public void addSessionListener(ISessionListener listener) {
if (listener != null) {
synchronized (listeners) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
}
}
/**
* Removes the specified ISessionListener from this notification session.
*
* @param listener
* the listener to remove
*/
public void removeSessionListener(ISessionListener listener) {
if (listener != null) {
synchronized (listeners) {
listeners.remove(listener);
}
}
}
}