diff options
author | rsuen | 2007-04-19 02:05:20 +0000 |
---|---|---|
committer | rsuen | 2007-04-19 02:05:20 +0000 |
commit | 6430110271c1e26a54f61f560a0e5df426c091c3 (patch) | |
tree | 4eb7b8401070ab377a2280bc616d06c4dffe93c1 | |
parent | 24d403c53095e4f3774c1082d168b845b7f53af8 (diff) | |
download | org.eclipse.ecf-6430110271c1e26a54f61f560a0e5df426c091c3.tar.gz org.eclipse.ecf-6430110271c1e26a54f61f560a0e5df426c091c3.tar.xz org.eclipse.ecf-6430110271c1e26a54f61f560a0e5df426c091c3.zip |
Initial commit.
31 files changed, 3779 insertions, 0 deletions
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/.classpath b/protocols/bundles/org.eclipse.ecf.protocol.msn/.classpath new file mode 100644 index 000000000..199fd6385 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/.classpath @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry path="src" kind="src"/> + <classpathentry path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/CDC-1.0%Foundation-1.0" kind="con"/> + <classpathentry path="bin" kind="output"/> +</classpath> diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/.cvsignore b/protocols/bundles/org.eclipse.ecf.protocol.msn/.cvsignore new file mode 100644 index 000000000..ba077a403 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/.cvsignore @@ -0,0 +1 @@ +bin diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/.project b/protocols/bundles/org.eclipse.ecf.protocol.msn/.project new file mode 100644 index 000000000..418534756 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/.project @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.eclipse.ecf.protocol.msn</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.pde.PluginNature</nature> + </natures> +</projectDescription> diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/.settings/org.eclipse.jdt.core.prefs b/protocols/bundles/org.eclipse.ecf.protocol.msn/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..d18917230 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Thu Jan 11 22:04:55 GMT 2007 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.1 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.3 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/META-INF/MANIFEST.MF b/protocols/bundles/org.eclipse.ecf.protocol.msn/META-INF/MANIFEST.MF new file mode 100644 index 000000000..a150005b8 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.ecf.protocol.msn +Bundle-Version: 0.3.1.qualifier +Bundle-Localization: plugin +Bundle-RequiredExecutionEnvironment: CDC-1.0/Foundation-1.0, + J2SE-1.3 +Export-Package: org.eclipse.ecf.protocol.msn, + org.eclipse.ecf.protocol.msn.events, + org.eclipse.ecf.protocol.msn.internal.encode;x-internal:=true, + org.eclipse.ecf.protocol.msn.internal.net;x-internal:=true +Bundle-Vendor: %providerName +Eclipse-LazyStart: false diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/about.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/about.html new file mode 100644 index 000000000..8f7767892 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/about.html @@ -0,0 +1,29 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
+<title>About</title>
+</head>
+<body lang="EN-US">
+<h2>About This Content</h2>
+
+<p>June 2, 2006</p>
+<h3>License</h3>
+
+<p>The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise
+indicated below, the Content is provided to you under the terms and conditions of the
+Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available
+at <a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a>.
+For purposes of the EPL, "Program" will mean the Content.</p>
+
+<p>If you did not receive this Content directly from the Eclipse Foundation, the Content is
+being redistributed by another party ("Redistributor") and different terms and conditions may
+apply to your use of any object code in the Content. Check the Redistributor's license that was
+provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
+indicated below, the terms and conditions of the EPL still apply to any source code in the Content
+and such source code may be obtained at <a href="http://www.eclipse.org">http://www.eclipse.org</a>.</p>
+
+</body>
+</html>
+
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/build.properties b/protocols/bundles/org.eclipse.ecf.protocol.msn/build.properties new file mode 100644 index 000000000..d97bbb1cd --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/build.properties @@ -0,0 +1,17 @@ +################################################################################ +# Copyright (c) 2007 Remy Suen +# 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 +################################################################################ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + plugin.properties,\ + about.html +src.includes = about.html diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/plugin.properties b/protocols/bundles/org.eclipse.ecf.protocol.msn/plugin.properties new file mode 100644 index 000000000..f7b7264f4 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/plugin.properties @@ -0,0 +1,13 @@ +################################################################################ +# Copyright (c) 2005, 2007 Remy Suen +# 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 +################################################################################ + +pluginName = MSN Protocol Implementation +providerName = Eclipse.org diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ChatSession.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ChatSession.java new file mode 100644 index 000000000..b60c25bd0 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ChatSession.java @@ -0,0 +1,396 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +import java.io.IOException; +import java.net.ConnectException; +import java.util.ArrayList; + +import org.eclipse.ecf.protocol.msn.events.IChatSessionListener; +import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils; + +/** + * <p> + * A ChatSession is a conversation that's held between two or more participants. + * </p> + * + * <p> + * As specified by {@link MsnClient}'s + * {@link MsnClient#disconnect() disconnect()} method, ChatSessions are not + * automatically disconnected when the client itself disconnects. However, + * clean-up will be performed automatically when a + * {@link IChatSessionListener#sessionTimedOut() sessionTimedOut()} event occurs + * or when the last user has left (as checked by monitoring the + * {@link IChatSessionListener#contactLeft(Contact) contactLeft(Contact)} + * event). + * </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> + */ +public final class ChatSession extends Session { + + /** + * The list of contacts that are currently a part of this session. Note that + * this does not include the client user. + */ + private final ArrayList contacts; + + private final ContactList contactList; + + private final String email = client.getUserEmail(); + + private boolean joined = false; + + /** + * Create a new ChatSession that connects to the given host. + * + * @param host + * the host to be connected to + * @param client + * the MsnClient to hook onto + * @throws IOException + * If an I/O error occurs while attempting to connect to the + * host + */ + ChatSession(String host, MsnClient client) throws IOException { + super(host, client); + listeners = new ArrayList(); + contacts = new ArrayList(); + contactList = client.getContactList(); + } + + /** + * Create a new ChatSession that will connect to the given server response + * using the specified username. + * + * @param host + * the host to be connected to + * @param client + * the MsnClient to hook onto + * @param username + * the username to authenticate with + * @param info + * the authentication info + * @throws IOException + * If an I/O error occurs while attempting to connect to the + * specified host + */ + ChatSession(String host, MsnClient client, String username, String info) + throws IOException { + this(host, client); + authenticate(username, info); + } + + /** + * This method attempts to authenticate the user with the switchboard server + * that it was instantiated to. + * + * @param username + * the user's MSN email address + * @param info + * the authentication information + * @throws IOException + */ + private void authenticate(String username, String info) throws IOException { + write("USR", username + ' ' + info); //$NON-NLS-1$ + String input = super.read(); + // FIXME: check if this indexOf(String) call can be replaced with + // startsWith(String) + if (input == null || input.indexOf("USR") == -1) { //$NON-NLS-1$ + throw new ConnectException("Authentication has failed with the " + + "switchboard server."); + } + idle(); + } + + public void close() { + try { + write("OUT"); //$NON-NLS-1$ + } catch (Exception e) { + // ignored + } + super.close(); + } + + /** + * Invites the user with the specified email to this chat session. + * + * @param email + * the user's email address + * @throws IOException + * If an I/O error occurs while attempting to send the + * invitation to the user + */ + public void invite(String email) throws IOException { + synchronized (contacts) { + for (int i = 0; i < contacts.size(); i++) { + if (((Contact) contacts.get(i)).getEmail().equals(email)) { + return; + } + } + } + write("CAL", email); //$NON-NLS-1$ + while (!joined) + ; + joined = false; + } + + /** + * Sends a notifying event to the listeners connected to this that the + * specified contact has joined this switchboard. + * + * @param contact + * the contact that has joined this session + */ + private void fireContactJoinedEvent(Contact contact) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IChatSessionListener) listeners.get(i)) + .contactJoined(contact); + } + } + } + + /** + * Informs the {@link IChatSessionListener}s attached to this that the + * given contact has left this session. + * + * @param contact + * the contact that left this session + */ + private void fireContactLeftEvent(Contact contact) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IChatSessionListener) listeners.get(i)).contactLeft(contact); + } + } + } + + /** + * This event is fired when the specified contact has started typing. + * + * @param contact + * the contact who is typing + */ + private void fireContactIsTypingEvent(Contact contact) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IChatSessionListener) listeners.get(i)) + .contactIsTyping(contact); + } + } + } + + /** + * This event is fired when a message has been received from the given user. + * + * @param contact + * the user that sent the message + * @param message + * the message that has been received + */ + private void fireMessageReceivedEvent(Contact contact, String message) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IChatSessionListener) listeners.get(i)).messageReceived( + contact, message); + } + } + } + + /** + * Notifies attached {@link ChatSessionListeners} that this session has now + * timed out. + */ + private void fireSessionTimedOutEvent() { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IChatSessionListener) listeners.get(i)).sessionTimedOut(); + } + } + } + + /** + * Look for a contact that is connected to this switchboard connected to the + * given email. Comparison is done with the String class's equals(String) + * method, so case sensitivity is an issue. + * + * @param email + * the email of the contact being sought after + * @return the contact that uses the specified email + * @throws IllegalArgumentException + * If the contact could not be found + */ + private Contact findContact(String email) throws IllegalArgumentException { + for (int i = 0; i < contacts.size(); i++) { + Contact contact = (Contact) contacts.get(i); + if (contact.getEmail().equals(email)) { + return contact; + } + } + throw new IllegalArgumentException("A contact with the email " + email + + " could not be found in this ChatSession."); + } + + /** + * Read the contents of the packet being sent from the server and handle any + * events accordingly. + * + * @return the String returned from {@link Session#read()} + */ + String read() throws IOException { + String input = super.read(); + if (input == null) { + return null; + } + + String[] lines = StringUtils.split(input, "\r\n"); //$NON-NLS-1$ + for (int i = 0; i < lines.length; i++) { + if (lines[i].startsWith("IRO")) { //$NON-NLS-1$ + String[] split = StringUtils.splitOnSpace(lines[i]); + Contact contact = contactList.getContact(split[4]); + if (contact == null) { + contact = new Contact(split[4], split[5]); + } + contacts.add(contact); + fireContactJoinedEvent(contact); + } else if (lines[i].startsWith("JOI")) { //$NON-NLS-1$ + String[] split = StringUtils.splitOnSpace(lines[i]); + Contact contact = contactList.getContact(split[2]); + if (contact == null) { + contact = new Contact(split[1], split[2]); + } + contacts.add(contact); + joined = true; + fireContactJoinedEvent(contact); + } else if (lines[i].startsWith("BYE")) { //$NON-NLS-1$ + String[] split = StringUtils.splitOnSpace(lines[i]); + if (split.length == 2) { + Contact contact = findContact(split[1]); + contacts.remove(contact); + fireContactLeftEvent(contact); + if (contacts.isEmpty()) { + close(); + } + } else { + fireSessionTimedOutEvent(); + close(); + } + } else if (lines[i].startsWith("MSG")) { //$NON-NLS-1$ + if (input.indexOf("TypingUser:") != -1) { //$NON-NLS-1$ + String trim = input.substring(input.indexOf("MSG")); //$NON-NLS-1$ + String content = StringUtils + .splitSubstring(trim, "\r\n", 3); //$NON-NLS-1$ + fireContactIsTypingEvent(findContact(StringUtils + .splitOnSpace(content)[1])); + } else if (input.indexOf("text/plain") != -1) { //$NON-NLS-1$ + int index = input.indexOf("ANS") == -1 ? 2 : 3; //$NON-NLS-1$ + String[] contents = StringUtils.split(input, "\r\n", index); //$NON-NLS-1$ + String[] split = StringUtils + .splitOnSpace(contents[index - 2]); + Contact contact = findContact(split[1]); + + int count = Integer.parseInt(split[3]); + split = StringUtils.split(contents[index - 1], "\r\n\r\n"); //$NON-NLS-1$ + + int text = count - (split[0].length() + 4); + fireMessageReceivedEvent(contact, split[1].substring(0, + text)); + } + } + } + + return input; + } + + /** + * <p> + * <b>Note:</b> This method will likely be modified in the future (renamed, + * change in return type, <tt>Contact[]</tt> <-> <tt>java.util.List</tt>, + * inclusion/exclusion of the current user, complete removal, etc.). Please + * use it at your own risk. + * </p> + * + * <p> + * This method returns the Contacts that are currently participating in this + * ChatSession. Note that this does not include the current user. + * </p> + */ + public Contact[] getParticipants() { + return (Contact[]) contacts.toArray(new Contact[contacts.size()]); + } + + /** + * Sends a message to the users connected to this chat session. + * + * @param message + * the message to be sent + * @throws IOException + * If an I/O occurs when sending the message to the server + */ + public void sendMessage(String message) throws IOException { + message = "MIME-Version: 1.0\r\n" //$NON-NLS-1$ + + "Content-Type: text/plain; charset=UTF-8\r\n" //$NON-NLS-1$ + + "X-MMS-IM-Format: FN=MS%20Sans%20Serif; EF=; CO=0; PF=0\r\n\r\n" //$NON-NLS-1$ + + message; + write("MSG", "N " + message.length() + "\r\n" + message, false); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + /** + * Notifies the participants of this chat session that the current user is + * typing a message. + * + * @throws IOException + * If an I/O occurs when sending the message to the server + */ + public void sendTypingNotification() throws IOException { + String message = "MIME-Version: 1.0\r\n" //$NON-NLS-1$ + + "Content-Type: text/x-msmsgscontrol\r\nTypingUser: " + email //$NON-NLS-1$ + + "\r\n\r\n\r\n"; //$NON-NLS-1$ + write("MSG", "U " + message.length() + "\r\n" + message, false); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + /** + * Adds a IChatSessionListener to this session. + * + * @param listener + * the listener to be added + */ + public void addChatSessionListener(IChatSessionListener listener) { + if (listener != null) { + synchronized (listeners) { + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + } + } + + /** + * Removes a IChatSessionListener from this session. + * + * @param listener + * the listener to be removed + */ + public void removeChatSessionListener(IChatSessionListener listener) { + if (listener != null) { + synchronized (listeners) { + listeners.remove(listener); + } + } + } +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Contact.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Contact.java new file mode 100644 index 000000000..6403129f6 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Contact.java @@ -0,0 +1,311 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.ecf.protocol.msn.events.IContactListener; +import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils; + +/** + * <p> + * This class represents a contact that a user has on his or her MSN list. + * </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> + */ +public final class Contact { + + /** + * The list of listeners that is attached to this. + */ + private final ArrayList listeners; + + /** + * The list of groups that this contact is in. + */ + private final ArrayList groups; + + /** + * The email address that is associated with this contact. + */ + private final String email; + + /** + * The guid of this contact. + */ + private final String guid; + + /** + * The displayed name of this contact, this is typically different from + * their {@link #email}. + */ + private String name; + + /** + * The personal message that the contact is currently displaying. + */ + private String personalMessage; + + /** + * The current status of this user. + * + * @see Status#ONLINE Status.ONLINE and others + */ + private Status status; + + /** + * Creates a new Contact with the given name and email address. + * @param email + * the user's email address + * @param name + * the contact's MSN nickname in raw format, the name will be URL + * decoded accordingly + */ + Contact(String email, String name) { + this(email, name, null); + } + + Contact(String email, String name, String guid) { + this.name = URLDecoder.decode(name); + this.email = email; + this.guid = guid; + this.status = Status.OFFLINE; + listeners = new ArrayList(); + groups = new ArrayList(); + } + + /** + * Invokes the {@link IContactListener#statusChanged(Status)} method on + * every listener within {@link #listeners}. This method is automatically + * invoked when {@link #setStatus(int)} is called. + * + * @param status + * the status that this contact has now switched to + */ + private void fireStatusChanged(Status status) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IContactListener) listeners.get(i)).statusChanged(status); + } + } + } + + /** + * Invokes the {@link IContactListener#nameChanged(Status)} method on every + * listener within {@link #listeners}. This method is automatically invoked + * when {@link #setDisplayName(String)} is called. + * + * @param name + * the new name of this contact + */ + private void fireNameChanged(String name) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IContactListener) listeners.get(i)).nameChanged(name); + } + } + } + + private void firePersonalMessageChanged(String message) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IContactListener) listeners.get(i)) + .personalMessageChanged(message); + } + } + } + + void add(Group group) { + groups.add(group); + } + + void remove() { + for (int i = 0; i < groups.size(); i++) { + ((Group) groups.get(i)).remove(this); + } + groups.clear(); + } + + /** + * Retrieves the groups that this contact is a part of. + * + * @return a collection of the groups that this contact is a member of + */ + public Collection getGroups() { + return Collections.unmodifiableCollection(groups); + } + + /** + * Sets this contact's status to the given status. Developers are highly + * discouraged from calling this method since if the user's status actually + * did change, the {@link IContactListener#nameChanged(Status)} method will + * be invoked on all the listeners attached to this contact. + * + * @param status + * the status that this contact is now in + */ + void setStatus(Status status) { + if (this.status != status) { + this.status = status; + fireStatusChanged(status); + } + } + + /** + * Retrieves the current status of this contact. + * + * @return the status that this contact currently is in + */ + public Status getStatus() { + return status; + } + + /** + * Sets the user name of this contact with the given name. Developers are + * highly discouraged from calling this method since if the value of + * <code>newName</code> differs from the current name, the + * {@link IContactListener#nameChanged(Status)} method will be invoked on + * all the listeners attached to this contact. + * + * @param newName + * the new user name of this Contact + */ + void setDisplayName(String newName) { + newName = URLDecoder.decode(newName); + if (!newName.equals(name)) { + this.name = newName; + fireNameChanged(newName); + } + } + + /** + * Gets the displayed name of this contact. + * + * @return the name that this contact uses + */ + public String getDisplayName() { + return name; + } + + /** + * Changes the contact's personal message to the provided message if the two + * messages differ. + * + * @param message + * the message the contact may have set to + */ + void setPersonalMessage(String message) { + message = StringUtils.xmlDecode(message); + if (!message.equals(personalMessage)) { + personalMessage = message; + firePersonalMessageChanged(message); + } + } + + /** + * Returns the personal message that this contact is currently using. + * + * @return the personal message in use + */ + public String getPersonalMessage() { + return personalMessage; + } + + /** + * Returns the email address of the user. + * + * @return the user's email address + */ + public String getEmail() { + return email; + } + + String getGuid() { + return guid; + } + + /** + * Adds a IContactListener to this contact. + * + * @param listener + * the listener to be added + */ + public void addContactListener(IContactListener listener) { + if (listener != null) { + synchronized (listeners) { + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + } + } + + /** + * Removes a IContactListener from this contact. + * + * @param listener + * the listener to be removed + */ + public void removeContactListener(IContactListener listener) { + if (listener != null) { + synchronized (listeners) { + listeners.remove(listener); + } + } + } + + /** + * Returns this contact's email address that's being used for identification + * purposes in MSN. + * + * @return the contact's email address + */ + public String toString() { + return email; + } + + /** + * Returns whether the specified object is equal to this. An object is equal + * to this if it is also a <tt>Contact</tt> and its email addresses is + * equal to this contact's email address. + * + * @return <code>true</code> if the argument is a <tt>Contact</tt> and + * also has the same email address as this + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof Contact) { + return email.equals(((Contact) obj).email); + } else { + return false; + } + } + + /** + * Returns a unique integer hash code for this contact. + * + * @return a integer hash code that represents this contact + */ + public int hashCode() { + return 31 * -1 + email.hashCode(); + } +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ContactList.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ContactList.java new file mode 100644 index 000000000..bc73fcd43 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ContactList.java @@ -0,0 +1,284 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.ecf.protocol.msn.events.IContactListListener; +import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils; + +/** + * <p> + * A ContactList stores a list of {@link Contact}s. + * </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> + */ +public final class ContactList { + + private final Map groups; + + private final ArrayList contacts; + + /** + * The list of listeners that is associated with this. + */ + private final ArrayList listeners; + + private final MsnClient client; + + /** + * Creates a new ContactList to store Contacts. + * + * @param client + * the client that this list is for + */ + ContactList(MsnClient client) { + this.client = client; + groups = new HashMap(); + contacts = new ArrayList(); + listeners = new ArrayList(); + } + + /** + * Notifies all listeners attached to this contact list that the given + * contact has been added. + * + * @param contact + * the contact that has been added + */ + private void fireContactAdded(Contact contact) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IContactListListener) listeners.get(i)).contactAdded(contact); + } + } + } + + void fireContactRemoved(String guid) { + Contact contact = findContactByGuid(guid); + if (!contact.getGroups().isEmpty()) { + contact.remove(); + } + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IContactListListener) listeners.get(i)) + .contactRemoved(contact); + } + } + } + + void fireContactAddedUser(String email) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IContactListListener) listeners.get(i)) + .contactAddedUser(email); + } + } + } + + void fireContactRemovedUser(String email) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IContactListListener) listeners.get(i)) + .contactRemovedUser(email); + } + } + } + + private void fireGroupAdded(Group group) { + synchronized (listeners) { + for (int i = 0; i < listeners.size(); i++) { + ((IContactListListener) listeners.get(i)).groupAdded(group); + } + } + } + + void internalAddContact(String email, String contactName) { + addContact(email, contactName, null); + } + + void addContact(String email, String contactName, String guid) { + Contact contact; + if (guid == null) { + contact = new Contact(email, contactName); + contacts.add(contact); + } else { + contact = findContactByGuid(guid); + if (contact == null) { + contact = new Contact(email, contactName, guid); + } + contacts.add(contact); + } + fireContactAdded(contact); + } + + void addContact(String contactName, String email, String guid, + String groupGUID) { + Contact contact = new Contact(email, contactName, guid); + contacts.add(contact); + + String[] split = StringUtils.split(groupGUID, ','); + for (int i = 0; i < split.length; i++) { + ((Group) groups.get(split[i])).add(contact); + } + + fireContactAdded(contact); + } + + void addGroup(String guid, Group group) { + groups.put(guid, group); + fireGroupAdded(group); + } + + /** + * Adds the contact with the specified email to this list. + * + * @param email + * the contact's email address + * @param userName + * the name to be assigned to this contact, or <tt>null</tt> if + * one does not need to be assigned + * @throws IOException + * If an I/O error occurs while attempting to send the request + * to the server + */ + public void addContact(String email, String userName) throws IOException { + if (userName == null || userName.equals("")) { //$NON-NLS-1$ + client.add(email, email); + } else { + client.add(email, userName); + } + } + + /** + * Removes the specified contact from the user's list. + * + * @param contact + * the contact to remove + * @throws IOException + * If an I/O error occurs while attempting to send the request + * to the server + */ + public void removeContact(Contact contact) throws IOException { + client.remove(contact); + } + + /** + * Removes the specified group from the user's list. + * + * @param group + * the group to remove + * @throws IOException + * If an I/O error occurs while attempting to send the request + * to the server + */ + public void removeGroup(Group group) throws IOException { + String guid = getGuid(group); + if (guid != null) { + client.remove(guid); + } + } + + /** + * Returns the contact that uses the specified email address. The search + * performed is case-sensitive. + * + * @param email + * the email address of the desired contact + * @return the contact that is associated with the given email address, or + * <code>null</code> if none could be found + */ + public Contact getContact(String email) { + for (int i = 0; i < contacts.size(); i++) { + Contact contact = (Contact) contacts.get(i); + if (contact.getEmail().equals(email)) { + return contact; + } + } + return null; + } + + private Contact findContactByGuid(String guid) { + for (int i = 0; i < contacts.size(); i++) { + Contact contact = (Contact) contacts.get(i); + if (guid.equals(contact.getGuid())) { + return contact; + } + } + return null; + } + + public Collection getContacts() { + return Collections.unmodifiableCollection(contacts); + } + + public Collection getGroups() { + return Collections.unmodifiableCollection(groups.values()); + } + + String getGuid(Group group) { + for (Iterator it = groups.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + if (entry.getValue() == group) { + return (String) entry.getKey(); + } + } + return null; + } + + /** + * Adds a IContactListener to this. + * + * @param listener + * the listener to be added + */ + public void addContactListListener(IContactListListener listener) { + if (listener != null) { + synchronized (listeners) { + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + } + } + + /** + * Removes a IContactListener from this. + * + * @param listener + * the listener to be removed + */ + public void removeContactsListListener(IContactListListener listener) { + if (listener != null) { + synchronized (listeners) { + listeners.remove(listener); + } + } + } + + public String toString() { + return contacts.toString(); + } + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/DispatchSession.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/DispatchSession.java new file mode 100644 index 000000000..e533f67e5 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/DispatchSession.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +import java.io.IOException; +import java.net.ConnectException; + +import org.eclipse.ecf.protocol.msn.internal.encode.ResponseCommand; + +/** + * <p> + * The DispatchSession class connects to the dispatch server and retrieves the + * address of the notification server for the {@link NotificationSession} class + * to connect to. It currently does not serve any other purpose. + * </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> + */ +class DispatchSession extends Session { + + DispatchSession(MsnClient client) { + super(client); + } + + /** + * Creates a new DispatchSocket to connect to the given hostname and port. + * + * @param hostname + * the host to be connected to + * @param port + * the corresponding port number + * @param client + * the client that that invoked this dispatch session + * @throws IOException + * If an I/O error occurs while attempting to open the + * SocketChannel + */ + DispatchSession(String hostname, int port) throws IOException { + super(hostname, port, null); + } + + /** + * Connects to the server specified during this DispatchSession's + * construction and attempts to retrieve a viable notification server + * address. + * + * @param username + * the name to use for authentication + * @return a ResponseCommand which holds the information received from the + * dispatch server + * @throws ConnectException + * If the MSN servers did not respond as expected. + * @throws IOException + * If an I/O error occurs during the read or write operations + */ + ResponseCommand connect(String username) throws ConnectException, + IOException { + write("VER", "MSNP11 CVR0"); //$NON-NLS-1$ //$NON-NLS-2$ + String input = super.read(); + if (!input.startsWith("VER")) { //$NON-NLS-1$ + // TODO: throw a more descriptive exception + throw new ConnectException("The server did not respond properly."); + } + + write("CVR", "0x040c winnt 5.1 i386 MSNMSGR 7.0.0813 msmsgs " //$NON-NLS-1$ //$NON-NLS-2$ + + username); + input = super.read(); + if (!input.startsWith("CVR")) { //$NON-NLS-1$ + // TODO: throw a more descriptive exception + throw new ConnectException("The server did not respond properly."); + } + + write("USR", "TWN I " + username); //$NON-NLS-1$ //$NON-NLS-2$ + return new ResponseCommand(super.read()); + } + + /** + * Attempts to authenticate the given username with the MSN dispatch server. + * + * @param username + * the username to be authenticated with + * @return the hostname of the notification server + * @throws ConnectException + * If the MSN servers did not respond as expected. + * @throws IOException + * If an I/O error occurs during the read or write operations + */ + String authenticate(String username) throws ConnectException, IOException { + ResponseCommand received = connect(username); + if (!received.getCommand().equals("XFR")) { //$NON-NLS-1$ + throw new ConnectException("The server did not respond properly."); + } + return received.getParam(2); + } + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Group.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Group.java new file mode 100644 index 000000000..e8debb1c1 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Group.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * <p> + * A Group is a collection of {@link Contact}s within a {@link ContactList}. A + * <tt>Contact</tt> can be in zero or more <tt>Group</tt>s. + * </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> + */ +public final class Group { + + private final List contacts; + + /** + * The name of this group. + */ + private final String name; + + /** + * Create a new group with the specified name. + * + * @param name + * the name of the group + */ + Group(String name) { + this.name = name; + contacts = new ArrayList(); + } + + void add(Contact contact) { + contacts.add(contact); + contact.add(this); + } + + void remove(Contact contact) { + contacts.remove(contact); + } + + /** + * Returns whether the specified contact is in this group. + * + * @return <tt>true</tt> if the contact is in this group, <tt>false</tt> + * otherwise + */ + public boolean contains(Contact contact) { + return contacts.contains(contact); + } + + public Collection getContacts() { + return Collections.unmodifiableCollection(contacts); + } + + /** + * Returns the name of this group. + * + * @return this group's name + */ + public String getName() { + return name; + } + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/MsnClient.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/MsnClient.java new file mode 100644 index 000000000..08e0dd546 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/MsnClient.java @@ -0,0 +1,339 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.Iterator; + +import org.eclipse.ecf.protocol.msn.events.ISessionListener; +import org.eclipse.ecf.protocol.msn.internal.encode.ResponseCommand; +import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils; + +/** + * <p> + * The MsnClient class allows a developer to easily create a client that will + * authenticate the user and establish a connection with the MSN servers. + * </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> + */ +public final class MsnClient { + + /** + * The default hostname that will be used to connect to the MSN servers - + * messenger.hotmail.com + */ + private static final String HOSTNAME = "messenger.hotmail.com"; //$NON-NLS-1$ + + /** + * The default port that will be used to connect to the MSN servers - 1863 + */ + private static final int PORT = 1863; + + /** + * The NotificationSession that will be connect to the notification server + * to handle most non-messaging related incoming and outgoing requests. + */ + private NotificationSession notification; + + /** + * The list of contacts that are on this user's list. + */ + private final ContactList list; + + /** + * The user's email address. + */ + private String username; + + /** + * The name the user displays to other contacts. + */ + private String displayName; + + /** + * The hostname to use to connect to the dispatch server. + */ + private String hostname; + + /** + * The user's personal message. + */ + private String personalMessage = ""; //$NON-NLS-1$ + + /** + * The media that the user is currently playing. + */ + private String currentMedia = "";//$NON-NLS-1$ + + /** + * The port to use to connect to the dispatch server. + */ + private int port; + + /** + * The current status of the user. + */ + private Status status; + + /** + * Instantiate a new MsnClient that defaults to setting the user as being + * online and available when signing in. + * + * @throws IOException + * If an I/O error occurred during instantiation of the + * DispatchSession. + */ + public MsnClient() { + this(Status.ONLINE); + } + + /** + * Instantiate a new MsnClient that set the user to the specified status + * when signing in. + * + * @param initialStatus + * the status that a user would like to sign on to the servers + * as, refer to {@link Status#ONLINE} and other static variables + * for the available options. + */ + public MsnClient(Status initialStatus) { + status = initialStatus; + hostname = HOSTNAME; + port = PORT; + list = new ContactList(this); + notification = new NotificationSession(this); + } + + /** + * Connects the client to the MSN servers. + * + * @param username + * the user's email address that is associated with an MSN + * account + * @param password + * the email's corresponding password + * @throws IOException + * If an I/O error occurred while connecting to the dispatch or + * notification servers. + */ + public void connect(String username, String password) throws IOException { + this.username = username; + DispatchSession dispatch = new DispatchSession(hostname, port); + // get the address of the notification server by first authenticating + // ourselves + String address = dispatch.authenticate(username); + // close the session + dispatch.close(); + // connect the notification session to the received address + notification.openSocket(address); + try { + // keep looping until we've connected successfully + while (!notification.login(username, password)) { + notification.reset(); + } + } catch (RuntimeException e) { + if (!notification.isClosed()) { + throw e; + } + } catch (IOException e) { + if (!notification.isClosed()) { + throw e; + } + } + } + + /** + * Disconnects the user from the MSN servers. Please note that any + * {@link ChatSession}s that may have been created since the client was + * instantiated are not disconnected automatically in this method. + */ + public void disconnect() { + if (notification != null) { + try { + notification.write("OUT"); //$NON-NLS-1$ + } catch (Exception e) { + // ignored since we're disconnecting anyway + } + notification.close(); + } + notification = null; + } + + /** + * Changes the user's status to the provided status flag. + * + * @param status + * the status that the user wishes to change to + */ + public void setStatus(Status status) throws IOException { + if (this.status != status) { + if (status == Status.OFFLINE) { + disconnect(); + } else { + notification.write("CHG", status.getLiteral() + " 268435488"); //$NON-NLS-1$ //$NON-NLS-2$ + } + this.status = status; + } + } + + /** + * Returns the status that the user is currently in. + * + * @return the user's current status + */ + public Status getStatus() { + return status; + } + + void add(String email, String userName) throws IOException { + notification.write("ADC", "FL N=" + email + " F=" + userName); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + void remove(Contact contact) throws IOException { + String guid = contact.getGuid(); + for (Iterator it = contact.getGroups().iterator(); it.hasNext();) { + notification.write("REM", "FL " + guid + " " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + list.getGuid((Group) it.next())); + } + notification.write("REM", "FL " + guid); //$NON-NLS-1$ //$NON-NLS-2$ + } + + void remove(String guid) throws IOException { + notification.write("RMG", "FL " + guid); //$NON-NLS-1$ //$NON-NLS-2$ + } + + /** + * Returns the contact list that is associated with this client. + * + * @return this client's contact list + */ + public ContactList getContactList() { + return list; + } + + /** + * Creates a {@link ChatSession} to connect to the specified contact. + * + * @param email + * the contact to connect to + * @return the created ChatSession + * @throws IOException + * If an I/O error occurred + */ + public ChatSession createChatSession(String email) throws IOException { + ResponseCommand cmd = notification.getChatSession(); + ChatSession cs = new ChatSession(cmd.getParam(2), this, username, cmd + .getParam(4)); + // reset the ResponseCommand so that the next XFR request won't conflict + cmd.process(null); + cs.invite(email); + return cs; + } + + void internalSetDisplayName(String newName) { + displayName = newName; + } + + /** + * Sets the display name of this user. + * + * @param newName + * the new name of this user + */ + public void setDisplayName(String newName) throws IOException { + notification.write("PRP", "MFN " + URLEncoder.encode(newName)); //$NON-NLS-1$ //$NON-NLS-2$ + displayName = newName; + } + + /** + * Returns the displayed name of this user. + * + * @return the name that this user is using + */ + public String getDisplayName() { + return displayName; + } + + /** + * Returns the user's account's email address. + * + * @return the email address the user is using for MSN login + */ + public String getUserEmail() { + return username; + } + + private void sendStatusData() throws IOException { + String message = "<Data><PSM>" + personalMessage //$NON-NLS-1$ + + "</PSM><CurrentMedia>" + currentMedia //$NON-NLS-1$ + + "</CurrentMedia></Data>"; //$NON-NLS-1$ + notification.write("UUX", message.length() + "\r\n" //$NON-NLS-1$ //$NON-NLS-2$ + + message, false); + } + + /** + * Sets the user's personal message to the specified string. + * + * @param personalMessage + * the new message to use as the user's personal message + * @throws IOException + * If an I/O error occurred while sending the data to the + * notification server + */ + public void setPersonalMessage(String personalMessage) throws IOException { + if (personalMessage == null) { + personalMessage = ""; //$NON-NLS-1$ + } else { + personalMessage = StringUtils.xmlEncode(personalMessage); + } + if (!this.personalMessage.equals(personalMessage)) { + this.personalMessage = personalMessage; + sendStatusData(); + } + } + + /** + * Retrieves the user's current personal message. + * + * @return the personal message that the user is currently using + */ + public String getPersonalMessage() { + return StringUtils.xmlDecode(personalMessage); + } + + /** + * Add an ISessionListener to this client. + * + * @param listener + * the listener to be added + */ + public void addSessionListener(ISessionListener listener) { + notification.addSessionListener(listener); + } + + /** + * Removes an ISessionListener from this client. + * + * @param listener + * the listener to be removed + */ + public void removeSessionListener(ISessionListener listener) { + notification.removeSessionListener(listener); + } +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/NotificationSession.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/NotificationSession.java new file mode 100644 index 000000000..2749d6698 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/NotificationSession.java @@ -0,0 +1,499 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ConnectException; +import java.util.ArrayList; + +import org.eclipse.ecf.protocol.msn.events.ISessionListener; +import org.eclipse.ecf.protocol.msn.internal.encode.Challenge; +import org.eclipse.ecf.protocol.msn.internal.encode.ResponseCommand; +import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils; +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 username + * 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 username, String password) throws IOException { + response = connect(username); + if (response.getCommand().equals("USR")) { //$NON-NLS-1$ + String ticket = request.getTicket(username, password, response + .getParam(3)); + password = null; + + 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."); + } + + retrieveBuddyList(); + this.username = username; + return true; + } else if (!response.getCommand().equals("XFR")) { //$NON-NLS-1$ + throw new ConnectException("Unable to connect to the MSN server."); + } 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())); + 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(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); + 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."); + } + 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.split(events[i], " ", 3); //$NON-NLS-1$ + String[] split = StringUtils.splitOnSpace(sub[2] + .substring(4)); + changeContactInfo(split); + } 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); + } + } + } +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Session.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Session.java new file mode 100644 index 000000000..db74ced2f --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Session.java @@ -0,0 +1,299 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.ArrayList; + +/** + * <p> + * An abstract base class that all other sessions should extend. This class + * provides common methods that a session will need to use such as reading and + * writing information from and to a socket. + * </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> + */ +abstract class Session { + + /** + * The client that this session is attached to. + */ + final MsnClient client; + + /** + * The list of listeners that have been connected to this session. + */ + ArrayList listeners; + + private final byte[] buffer = new byte[1024]; + + private Socket socket; + + private InputStream is; + + private OutputStream os; + + /** + * The number of transactions that has been transferred through this session + * thus far. This value will automatically increment when + * {@link #write(String, String)} or {@link #write(String, String, boolean)} + * has been invoked. + */ + private long transactionID = 0; + + /** + * A thread used to wait indefinitely for incoming messages from the server. + */ + private IdleThread idleThread; + + private boolean closed = false; + + Session(MsnClient client) { + this.client = client; + } + + /** + * Creates a session instance that will be connected to the given host. The + * string must be of the form '12.345.678.9:1234'. + * + * @param host + * the host to connect to + * @param client + * the client to hook onto + * @throws IOException + * If an I/O error occurred while attempting to connect to the + * given host. + */ + Session(String host, MsnClient client) throws IOException { + this.client = client; + openSocket(host); + } + + /** + * Creates a session instance that will be connected to the given host and + * port. + * + * @param ip + * the host to connect to + * @param port + * the port to connect to + * @param client + * the client to hook onto + * @throws IOException + * If an I/O error occurred while attempting to connect to the + * port at the given host. + */ + Session(String ip, int port, MsnClient client) throws IOException { + this.client = client; + openSocket(ip, port); + } + + /** + * Opens a connection to the specified host. + * + * @param host + * the host to connect to + * @throws IOException + * If an error occurs while attempting to connect to the host + */ + final void openSocket(String host) throws IOException { + closed = false; + int index = host.indexOf(':'); + openSocket(host.substring(0, index), Integer.parseInt(host + .substring(index + 1))); + } + + final void openSocket(String ip, int port) throws IOException { + closed = false; + socket = new Socket(ip, port); + is = socket.getInputStream(); + os = socket.getOutputStream(); + } + + final InputStream getInputStream() { + return is; + } + + /** + * Reads data from the channel and returns it as a String. + * + * @return the contents that have been read, or <code>null</code> if + * nothing is currently available + * @throws IOException + * If an I/O error occurred while reading from the channel. + */ + String read() throws IOException { + int read = is.read(buffer); + if (read < 1) { + return null; + } else { + return new String(buffer, 0, read).trim(); + } + } + + /** + * This method writes the given string input onto the channel. The carriage + * return and newline characters may be appended depending on the value of + * <code>newline</code>. + * + * @param input + * the String to be written + * @param newline + * <code>true</code> if a '\r\n' should be appended at the end + * of the write + * @throws IOException + * If an I/O error occurs while attempting to write to the + * channel. + */ + private final void write(String input, boolean newline) throws IOException { + byte[] bytes = newline ? (input + "\r\n").getBytes() : input.getBytes(); //$NON-NLS-1$ + os.write(bytes); + os.flush(); + } + + /** + * This method is synonymous with {@link #write(String, boolean)} with the + * exception that this method will always insert the carriage return and + * newline characters. + * + * @param input + * the String to be written + * @throws IOException + * If an I/O error occurs while attempting to write to the + * channel. + */ + final void write(String input) throws IOException { + write(input, true); + } + + /** + * Writes the given command with the specified parameters to the channel. A + * transaction identification number will also be inserted between the + * command and its parameters. A carriage return and a newline character + * will be inserted if <tt>newline</tt> is true. + * + * @param command + * the command to be inserted + * @param parameters + * additional parameters that are associated with the command + * @param newline + * <tt>true</tt> if a <tt>"\r\n"</tt> should be appended at + * the end of the write + * @throws IOException + * If an I/O error occurs while attempting to write to the + * channel. + */ + final void write(String command, String parameters, boolean newline) + throws IOException { + transactionID++; + write(command + ' ' + transactionID + ' ' + parameters, newline); + } + + /** + * This method is synonymous with {@link #write(String, String, boolean)} + * with the exception that this method will always insert the carriage + * return and newline characters. + * + * @param command + * the command to be inserted + * @param parameters + * additional parameters that are associated with the command + * @throws IOException + * If an I/O error occurs while attempting to write to the + * channel. + */ + final void write(String command, String parameters) throws IOException { + write(command, parameters, true); + } + + void close() { + closed = false; + if (idleThread != null) { + idleThread.interrupt(); + idleThread = null; + } + + if (socket != null) { + try { + socket.close(); + } catch (Exception e) { + // ignored + } + socket = null; + } + + if (is != null) { + try { + is.close(); + } catch (Exception e) { + // ignored + } + is = null; + } + + if (os != null) { + try { + os.close(); + } catch (Exception e) { + // ignored + } + os = null; + } + } + + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + + /** + * Wait in an infinite loop for information to be read in. + */ + final void idle() { + if (idleThread == null || !idleThread.isAlive()) { + idleThread = new IdleThread(); + idleThread.start(); + } + } + + final boolean isClosed() { + return closed; + } + + /** + * IdleThread waits for an indefinite amount of time for incoming messages. + */ + private class IdleThread extends Thread { + + /** + * Begin waiting for incoming messages indefinitely. + */ + public void run() { + while (!isInterrupted()) { + try { + read(); + } catch (IOException e) { + return; + } + } + } + } +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Status.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Status.java new file mode 100644 index 000000000..b435f5e27 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Status.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn; + +/** + * <p> + * The Status class represents the different states that a user can be in. + * </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> + */ +public final class Status { + + public static final Status ONLINE = new Status("NLN"); //$NON-NLS-1$ + + public static final Status BUSY = new Status("BSY"); //$NON-NLS-1$ + + public static final Status BE_RIGHT_BACK = new Status("BRB"); //$NON-NLS-1$ + + public static final Status AWAY = new Status("AWY"); //$NON-NLS-1$ + + public static final Status IDLE = new Status("IDL"); //$NON-NLS-1$ + + public static final Status ON_THE_PHONE = new Status("PHN"); //$NON-NLS-1$ + + public static final Status OUT_TO_LUNCH = new Status("LUN"); //$NON-NLS-1$ + + public static final Status APPEAR_OFFLINE = new Status("HDN"); //$NON-NLS-1$ + + public static final Status OFFLINE = new Status(null); + + private String literal; + + static Status getStatus(String literal) { + if (literal.equals("NLN")) { //$NON-NLS-1$ + return ONLINE; + } else if (literal.equals("AWY")) { //$NON-NLS-1$ + return AWAY; + } else if (literal.equals("IDL")) { //$NON-NLS-1$ + return IDLE; + } else if (literal.equals("BSY")) { //$NON-NLS-1$ + return BUSY; + } else if (literal.equals("BRB")) { //$NON-NLS-1$ + return BE_RIGHT_BACK; + } else if (literal.equals("PHN")) { //$NON-NLS-1$ + return ON_THE_PHONE; + } else if (literal.equals("LUN")) { //$NON-NLS-1$ + return OUT_TO_LUNCH; + } else if (literal.equals("HDN")) { //$NON-NLS-1$ + return APPEAR_OFFLINE; + } else { + throw new IllegalArgumentException("Unknown literal: " + literal); + } + } + + private Status(String literal) { + this.literal = literal; + } + + String getLiteral() { + return literal; + } + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IChatSessionListener.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IChatSessionListener.java new file mode 100644 index 000000000..448250b95 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IChatSessionListener.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.events; + +import java.util.EventListener; + +import org.eclipse.ecf.protocol.msn.ChatSession; +import org.eclipse.ecf.protocol.msn.Contact; + +/** + * <p> + * The IChatSessionListener monitors the events that are occurring within a + * {@link ChatSession}. + * </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> + */ +public interface IChatSessionListener extends EventListener { + + /** + * This method is called when a contact joins the session. + * + * @param contact + * the contact that has joined + */ + public void contactJoined(Contact contact); + + /** + * This method is called when a contact leaves the session. + * + * @param contact + * the contact that has left + */ + public void contactLeft(Contact contact); + + /** + * This method is called when a contact begins typing. + * + * @param contact + * the contact that is currently typing + */ + public void contactIsTyping(Contact contact); + + /** + * This method is called when a message has been received from a contact. + * + * @param contact + * the contact that has sent out a message + * @param message + * the message that has been sent + */ + public void messageReceived(Contact contact, String message); + + /** + * This method is called when the session has timed out. + */ + public void sessionTimedOut(); + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListListener.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListListener.java new file mode 100644 index 000000000..d52fdc0fd --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListListener.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.events; + +import org.eclipse.ecf.protocol.msn.Contact; +import org.eclipse.ecf.protocol.msn.Group; + +/** + * <p> + * The IContactListListener monitors events pertaining to the addition and + * removal of contacts on the user's client. + * </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> + */ +public interface IContactListListener { + + /** + * This method is invoked when a contact has been added to the user's list. + * + * @param contact + * the contact that has been added + */ + public void contactAdded(Contact contact); + + /** + * This method is invoked when a contact has been removed from the user's + * list. + * + * @param contact + * the contact that has been added + */ + public void contactRemoved(Contact contact); + + /** + * This method is invoked when a contact has added the user to his or her + * contact list. + * + * @param email + * the email of the contact + */ + public void contactAddedUser(String email); + + /** + * This method is invoked when a contact has removed the user from his or + * her contact list. + * + * @param email + * the email of the contact + */ + public void contactRemovedUser(String email); + + /** + * This method is invoked when a group has been added to the contact list. + * + * @param group + * the group that has been added + */ + public void groupAdded(Group group); + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListener.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListener.java new file mode 100644 index 000000000..29763c61a --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListener.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.events; + +import org.eclipse.ecf.protocol.msn.Status; + +/** + * <p> + * The IContactListener interface defines methods that developers can listen for + * which pertains to the {@link org.eclipse.ecf.protocol.msn.Contact} class. + * </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> + */ +public interface IContactListener { + + /** + * This method is called when contact has changed his or her user name. + * + * @param name + * the new name that the contact is using + */ + public void nameChanged(String name); + + /** + * This method is called when the user changes his or her personal message. + * + * @param personalMessage + * the new message that the contact is displaying + */ + public void personalMessageChanged(String personalMessage); + + /** + * This method is called when the contact has changed his or her status. + * + * @param status + * the status that the contact has now switched to + */ + public void statusChanged(Status status); + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/ISessionListener.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/ISessionListener.java new file mode 100644 index 000000000..a154fc8fb --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/ISessionListener.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.events; + +import org.eclipse.ecf.protocol.msn.ChatSession; + +/** + * <p> + * A listener that can be used to monitor the creation of {@link ChatSession}s. + * This is used for listening for incoming instant messages from a party that + * the current user has not already established a chat session with. + * </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> + */ +public interface ISessionListener { + + /** + * A method that is called when a chat session has been created because of a + * request from an external host. + * + * @param chatSession + * the created chat session + */ + public void sessionConnected(ChatSession chatSession); + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/package.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/package.html new file mode 100644 index 000000000..fb4fade75 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/package.html @@ -0,0 +1,5 @@ +<html> +<body> +Provides listener interfaces for handling events received from the MSN network. +</body> +</html> diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Base64.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Base64.java new file mode 100644 index 000000000..441cd6021 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Base64.java @@ -0,0 +1,289 @@ +/******************************************************************************* + * Copyright (c) Robert Harder + * 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: + * Robert Harder <rob@iharder.net> - initial API and implementation + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.internal.encode; + +/** + * <p>This file is distributed under the + * <a href="http://www.opensource.org/licenses/eclipse-1.0.php">Eclipse Public License</a>. + * A warm "Thank You" goes out to the open source community for all their contributions + * to the common good.</p> + * + * <p>Encodes and decodes to and from Base64 notation.</p> + * <p>Homepage: <a href="http://iharder.net/base64">http://iharder.net/base64</a>.</p> + * + * <p>The <tt>options</tt> parameter, which appears in a few places, is used to pass + * several pieces of information to the encoder. In the "higher level" methods such as + * encodeBytes( bytes, options ) the options parameter can be used to indicate such + * things as first gzipping the bytes before encoding them, not inserting linefeeds + * (though that breaks strict Base64 compatibility), and encoding using the URL-safe + * and Ordered dialects.</p> + * + * <p>The constants defined in Base64 can be OR-ed together to combine options, so you + * might make a call like this:</p> + * + * <code>String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DONT_BREAK_LINES );</code> + * + * <p>to compress the data before encoding it and then making the output have no newline characters.</p> + * + * + * <p> + * Change Log: + * </p> + * <ul> + * <li>v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug + * when using very small files (~< 40 bytes).</li> + * <li>v2.2 - Added some helper methods for encoding/decoding directly from + * one file to the next. Also added a main() method to support command line + * encoding/decoding from one file to the next. Also added these Base64 dialects: + * <ol> + * <li>The default is RFC3548 format.</li> + * <li>Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates + * URL and file name friendly format as described in Section 4 of RFC3548. + * http://www.faqs.org/rfcs/rfc3548.html</li> + * <li>Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates + * URL and file name friendly format that preserves lexical ordering as described + * in http://www.faqs.org/qa/rfcc-1940.html</li> + * </ol> + * Special thanks to Jim Kellerman at <a href="http://www.powerset.com/">http://www.powerset.com/</a> + * for contributing the new Base64 dialects. + * </li> + * + * <li>v2.1 - Cleaned up javadoc comments and unused variables and methods. Added + * some convenience methods for reading and writing to and from files.</li> + * <li>v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems + * with other encodings (like EBCDIC).</li> + * <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the + * encoded data was a single byte.</li> + * <li>v2.0 - I got rid of methods that used booleans to set options. + * Now everything is more consolidated and cleaner. The code now detects + * when data that's being decoded is gzip-compressed and will decompress it + * automatically. Generally things are cleaner. You'll probably have to + * change some method calls that you were making to support the new + * options format (<tt>int</tt>s that you "OR" together).</li> + * <li>v1.5.1 - Fixed bug when decompressing and decoding to a + * byte[] using <tt>decode( String s, boolean gzipCompressed )</tt>. + * Added the ability to "suspend" encoding in the Output Stream so + * you can turn on and off the encoding if you need to embed base64 + * data in an otherwise "normal" stream (like an XML file).</li> + * <li>v1.5 - Output stream pases on flush() command but doesn't do anything itself. + * This helps when using GZIP streams. + * Added the ability to GZip-compress objects before encoding them.</li> + * <li>v1.4 - Added helper methods to read/write files.</li> + * <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li> + * <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream + * where last buffer being read, if not completely full, was not returned.</li> + * <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.</li> + * <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li> + * </ul> + * + * <p> + * Please visit <a href="http://iharder.net/base64">http://iharder.net/base64</a> + * periodically to check for updates or to contribute improvements. + * </p> + * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.2.1 + */ +public class Base64 +{ + + +/* ******** P R I V A T E F I E L D S ******** */ + + + /** Maximum line length (76) of Base64 output. */ + private final static int MAX_LINE_LENGTH = 76; + + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte)'='; + + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte)'\n'; + + +/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + /** The 64 valid Base64 values. */ + //private final static byte[] ALPHABET; + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private final static byte[] _STANDARD_ALPHABET = + { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' + }; + + + + /** Defeats instantiation. */ + private Base64(){} + + +/* ******** E N C O D I N G M E T H O D S ******** */ + + + /** + * <p>Encodes up to three bytes of the array <var>source</var> + * and writes the resulting four Base64 bytes to <var>destination</var>. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * <var>srcOffset</var> and <var>destOffset</var>. + * This method does not check to make sure your arrays + * are large enough to accomodate <var>srcOffset</var> + 3 for + * the <var>source</var> array or <var>destOffset</var> + 4 for + * the <var>destination</var> array. + * The actual number of significant bytes in your array is + * given by <var>numSigBytes</var>.</p> + * <p>This is the lowest level of the encoding methods with + * all possible parameters.</p> + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the <var>destination</var> array + * @since 1.3 + */ + private static byte[] encode3to4( + byte[] source, int srcOffset, int numSigBytes, + byte[] destination, int destOffset ) + { + byte[] ALPHABET = _STANDARD_ALPHABET; + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) + | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) + | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); + + switch( numSigBytes ) + { + case 3: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; + return destination; + + case 2: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + case 1: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = EQUALS_SIGN; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + * @param source The data to convert + * @since 1.4 + */ + public static String encodeBytes( byte[] source ) + { + return encodeBytes( source, 0, source.length ); + } // end encodeBytes + + + + /** + * Encodes a byte array into Base64 notation. + * <p> + * Valid options:<pre> + * GZIP: gzip-compresses object before encoding it. + * DONT_BREAK_LINES: don't break lines at 76 characters + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * <p> + * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or + * <p> + * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code> + * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes( byte[] source, int off, int len ) + { + + int len43 = len * 4 / 3; + byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + ]; // New lines + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for( ; d < len2; d+=3, e+=4 ) + { + encode3to4( source, d+off, 3, outBuff, e ); + + lineLength += 4; + if(lineLength == MAX_LINE_LENGTH ) + { + outBuff[e+4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if( d < len ) + { + encode3to4( source, d+off, len - d, outBuff, e ); + e += 4; + } // end if: some padding needed + + return new String( outBuff, 0, e ); + + } // end encodeBytes + + +} // end class Base64 diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Challenge.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Challenge.java new file mode 100644 index 000000000..2a7b2a24a --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Challenge.java @@ -0,0 +1,198 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.internal.encode; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public final class Challenge { + + public static final String PRODUCT_ID = "PROD0090YUAUV{2B"; //$NON-NLS-1$ + + private static final String PRODUCT_KEY = "YMM8C_H7KCQ2S_KL"; //$NON-NLS-1$ + + private static MessageDigest instance; + + static { + try { + instance = MessageDigest.getInstance("MD5"); //$NON-NLS-1$ + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("No MD5 digest found"); + } + } + + public static String createQuery(String challenge) { + String[] s = computeMD5DigestAsStringArray((challenge + PRODUCT_KEY) + .getBytes()); + String md5Hash = computeMD5Digest((challenge + PRODUCT_KEY).getBytes()); + int[] md5 = new int[4]; + for (int i = 0; i < 4; i++) { + md5[i] = Integer.parseInt(s[i], 16); + } + + String chl = challenge + PRODUCT_ID; + while (chl.length() % 8 != 0) { + chl += '0'; + } + + char[] array = chl.toCharArray(); + String[] values = new String[chl.length() / 4]; + for (int i = 0; i < array.length; i += 4) { + int j = array[i + 3]; + String value = Integer.toHexString(j); + j = array[i + 2]; + value += Integer.toHexString(j); + j = array[i + 1]; + value += Integer.toHexString(j); + j = array[i]; + value += Integer.toHexString(j); + values[i / 4] = value; + } + + int[] ints = new int[values.length]; + for (int i = 0; i < values.length; i++) { + ints[i] = Integer.parseInt(values[i], 16); + } + + long high = 0; + long low = 0; + for (int i = 0; i < ints.length; i += 2) { + long temp = ints[i]; + temp = (temp * 0xe79a9c1L) % 0x7fffffff; + temp += high; + temp = md5[0] * temp + md5[1]; + temp = temp % 0x7fffffff; + + high = ints[i + 1]; + high = (high + temp) % 0x7fffffff; + high = md5[2] * high + md5[3]; + high = high % 0x7fffffff; + + low = low + high + temp; + } + + high = (high + md5[1]) % 0x7fffffff; + low = (low + md5[3]) % 0x7fffffff; + + String highString = Long.toHexString(high); + String lowString = Long.toHexString(low); + + while (highString.length() < 8) { + highString = '0' + highString; + } + + while (lowString.length() < 8) { + lowString = '0' + lowString; + } + + highString = highString.substring(6, 8) + highString.substring(4, 6) + + highString.substring(2, 4) + highString.substring(0, 2); + lowString = lowString.substring(6, 8) + lowString.substring(4, 6) + + lowString.substring(2, 4) + lowString.substring(0, 2); + + high = Long.parseLong(highString, 16); + low = Long.parseLong(lowString, 16); + + String first = Long.toHexString((Long.parseLong( + md5Hash.substring(0, 8), 16) ^ high)); + String second = Long.toHexString((Long.parseLong(md5Hash.substring(8, + 16), 16) ^ low)); + String third = Long.toHexString((Long.parseLong(md5Hash.substring(16, + 24), 16) ^ high)); + String fourth = Long.toHexString((Long.parseLong(md5Hash.substring(24, + 32), 16) ^ low)); + + while (first.length() < 8) { + first = '0' + first; + } + + while (second.length() < 8) { + second = '0' + second; + } + + while (third.length() < 8) { + third = '0' + third; + } + + while (fourth.length() < 8) { + fourth = '0' + fourth; + } + + return first + second + third + fourth; + } + + /** + * Computes the MD5 digest of a string given its bytes. + * + * @param bytes + * the bytes of the string to be digested + * @return the MD5 digest of the original string + */ + private static final String computeMD5Digest(byte[] bytes) { + byte[] hash = instance.digest(bytes); + StringBuffer buffer = new StringBuffer(); + synchronized (buffer) { + for (int i = 0; i < hash.length; i++) { + int value = 0xff & hash[i]; + if (value < 16) { + buffer.append('0').append(Integer.toHexString(value)); + } else { + buffer.append(Integer.toHexString(value)); + } + } + return buffer.toString(); + } + } + + private static final String[] computeMD5DigestAsStringArray(byte[] bytes) { + byte[] hash = instance.digest(bytes); + StringBuffer buffer = new StringBuffer(); + synchronized (buffer) { + for (int i = 0; i < hash.length; i++) { + int value = 0xff & hash[i]; + if (value < 16) { + buffer.append('0').append(Integer.toHexString(value)); + } else { + buffer.append(Integer.toHexString(value)); + } + } + } + + String result = buffer.toString(); + String[] results = new String[4]; + results[0] = result.substring(0, 8); + results[1] = result.substring(8, 16); + results[2] = result.substring(16, 24); + results[3] = result.substring(24, 32); + + for (int i = 0; i < 4; i++) { + char[] array = results[i].toCharArray(); + char[] swapped = new char[8]; + for (int j = 0; j < 8; j += 2) { + swapped[7 - j] = array[j + 1]; + swapped[6 - j] = array[j]; + } + results[i] = new String(swapped); + } + + for (int i = 0; i < 4; i++) { + long l = Long.parseLong(results[i], 16); + l = l & 0x7fffffff; + if (l < 0x10000000) { + results[i] = '0' + Long.toHexString(l); + } else { + results[i] = Long.toHexString(l); + } + } + + return results; + } +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Encryption.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Encryption.java new file mode 100644 index 000000000..3634156b7 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Encryption.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.internal.encode; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * This class provides static methods to compute the SHA digest of strings. + */ +public class Encryption { + + private static MessageDigest shaDigest; + + static { + try { + shaDigest = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$ + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(); + } + } + + /** + * Computes the SHA hash of the bytes provided and then truncate it down to + * a length of twenty. A base 64 encoding of the twenty character string + * literal is then computed and returned. + * + * @param bytes + * the bytes to be computed from + * @return a base 64 encoding of a twenty character SHA hash of the bytes + * provided + */ + public static String computeSHA(byte[] bytes) { + synchronized (shaDigest) { + bytes = shaDigest.digest(bytes); + } + + StringBuffer buffer = new StringBuffer(); + synchronized (buffer) { + for (int i = 0; i < 20; i++) { + if (0 < bytes[i] && bytes[i] < 16) { + buffer.append('0'); + } + buffer.append(Integer.toHexString((0xff & bytes[i]))); + } + + bytes = new byte[20]; + for (int i = 0; i < 20; i++) { + bytes[i] = (byte) Integer.parseInt(buffer.substring(i * 2, + i * 2 + 2), 16); + } + } + + return Base64.encodeBytes(bytes); + } + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/ResponseCommand.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/ResponseCommand.java new file mode 100644 index 000000000..e6d6244ab --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/ResponseCommand.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.internal.encode; + +/** + * The ResponseCommand class processes one line of simple output from the MSN + * servers. It provides methods to handle the output easier by separating the + * command and its parameters. + */ +public class ResponseCommand { + + /** + * The command of the output, there is a large variety of commands in use + * currently and are all three characters long in uppercase. + */ + private String cmd; + + /** + * The parameters that was sent with the command. + */ + private String[] params; + + /** + * Creates a new ResponseCommand that will process the given line. The + * constructor does not do anything outside of calling + * + * @link #process(String) on the given line. + * + * @param line + * the line that this should represent + */ + public ResponseCommand(String line) { + process(line); + } + + /** + * Process the given line. It will store the first three characters as the + * command and the rest of the line will be split by a single space and + * stored as a String array. If <code>line</code> is null, both the + * command and the String array will store null pointers. + * + * @param line + * the line to be processed + */ + public void process(String line) { + if (line == null) { + cmd = null; + params = null; + } else { + cmd = line.substring(0, 3); + params = StringUtils.splitOnSpace(line.substring(4)); + } + } + + /** + * Returns the command of this line. + * + * @return the three character command + */ + public String getCommand() { + return cmd; + } + + /** + * Returns the string literal stored at the given index in params. If the + * first parameter is desired, a 0 should be passed. + * + * @param index + * the parameter at the given index + * @return the desired parameter that is at the given index + */ + public String getParam(int index) { + return params[index]; + } + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/StringUtils.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/StringUtils.java new file mode 100644 index 000000000..1cd3b970e --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/StringUtils.java @@ -0,0 +1,224 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.internal.encode; + +import java.util.ArrayList; + +/** + * <p> + * The StringUtils class provides static methods that helps make string + * manipulation easy. The primary functionality it is meant to provide is the + * ability to split a string into a string array based on a given delimiter. + * This functionality is meant to take the place of the split(String) and + * split(String, int) method that was introduced in J2SE-1.4. Please note, + * however, that the splitting performed by this class simply splits the string + * based on the delimiter and does not perform any regular expression matching + * like the split methods provided in J2SE-1.4. + * </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> + */ +public final class StringUtils { + + public static final String[] splitOnSpace(String string) { + int index = string.indexOf(' '); + if (index == -1) { + return new String[] { string }; + } + + ArrayList split = new ArrayList(); + while (index != -1) { + split.add(string.substring(0, index)); + string = string.substring(index + 1); + index = string.indexOf(' '); + } + + if (!string.equals("")) { //$NON-NLS-1$ + split.add(string); + } + + return (String[]) split.toArray(new String[split.size()]); + } + + public static final String[] split(String string, char character) { + int index = string.indexOf(character); + if (index == -1) { + return new String[] { string }; + } + + ArrayList split = new ArrayList(); + while (index != -1) { + split.add(string.substring(0, index)); + string = string.substring(index + 1); + index = string.indexOf(character); + } + + if (!string.equals("")) { //$NON-NLS-1$ + split.add(string); + } + + return (String[]) split.toArray(new String[split.size()]); + } + + public static final String[] split(String string, String delimiter) { + int index = string.indexOf(delimiter); + if (index == -1) { + return new String[] { string }; + } + + int length = delimiter.length(); + ArrayList split = new ArrayList(); + while (index != -1) { + split.add(string.substring(0, index)); + string = string.substring(index + length); + index = string.indexOf(delimiter); + } + + if (!string.equals("")) { //$NON-NLS-1$ + split.add(string); + } + + return (String[]) split.toArray(new String[split.size()]); + } + + public static final String[] split(String string, String delimiter, + int limit) { + int index = string.indexOf(delimiter); + if (index == -1) { + return new String[] { string }; + } + + int count = 0; + int length = delimiter.length(); + ArrayList split = new ArrayList(limit); + while (index != -1 && count < limit - 1) { + split.add(string.substring(0, index)); + string = string.substring(index + length); + index = string.indexOf(delimiter); + count++; + } + + if (!string.equals("")) { //$NON-NLS-1$ + split.add(string); + } + + return (String[]) split.toArray(new String[split.size()]); + } + + public static final String splitSubstring(String string, String delimiter, + int pos) { + int index = string.indexOf(delimiter); + if (index == -1) { + return string; + } + + int count = 0; + int length = delimiter.length(); + while (count < pos) { + string = string.substring(index + length); + index = string.indexOf(delimiter); + count++; + } + + return index == -1 ? string : string.substring(0, index); + } + + public static final String xmlDecode(String string) { + if (string.equals("")) { //$NON-NLS-1$ + return string; + } + + int index = string.indexOf("&"); //$NON-NLS-1$ + while (index != -1) { + string = string.substring(0, index) + '&' + + string.substring(index + 5); + index = string.indexOf("&", index + 1); //$NON-NLS-1$ + } + + index = string.indexOf("""); //$NON-NLS-1$ + while (index != -1) { + string = string.substring(0, index) + '"' + + string.substring(index + 6); + index = string.indexOf(""", index + 1); //$NON-NLS-1$ + } + + index = string.indexOf("'"); //$NON-NLS-1$ + while (index != -1) { + string = string.substring(0, index) + '\'' + + string.substring(index + 6); + index = string.indexOf("'", index + 1); //$NON-NLS-1$ + } + + index = string.indexOf("<"); //$NON-NLS-1$ + while (index != -1) { + string = string.substring(0, index) + '<' + + string.substring(index + 4); + index = string.indexOf("<", index + 1); //$NON-NLS-1$ + } + + index = string.indexOf(">"); //$NON-NLS-1$ + while (index != -1) { + string = string.substring(0, index) + '>' + + string.substring(index + 4); + index = string.indexOf(">", index + 1); //$NON-NLS-1$ + } + return string; + } + + public static final String xmlEncode(String string) { + if (string.equals("")) { //$NON-NLS-1$ + return string; + } + + int index = string.indexOf('&'); + while (index != -1) { + string = string.substring(0, index) + "&" //$NON-NLS-1$ + + string.substring(index + 1); + index = string.indexOf('&', index + 1); + } + + index = string.indexOf('"'); + while (index != -1) { + string = string.substring(0, index) + """ //$NON-NLS-1$ + + string.substring(index + 1); + index = string.indexOf('"', index + 1); + } + + index = string.indexOf('\''); + while (index != -1) { + string = string.substring(0, index) + "'" //$NON-NLS-1$ + + string.substring(index + 1); + index = string.indexOf('\'', index + 1); + } + + index = string.indexOf('<'); + while (index != -1) { + string = string.substring(0, index) + "<" //$NON-NLS-1$ + + string.substring(index + 1); + index = string.indexOf('<', index + 1); + } + + index = string.indexOf('>'); + while (index != -1) { + string = string.substring(0, index) + ">" //$NON-NLS-1$ + + string.substring(index + 1); + index = string.indexOf('>', index + 1); + } + return string; + } + +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/package.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/package.html new file mode 100644 index 000000000..13014e71e --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/package.html @@ -0,0 +1,5 @@ +<html> +<body> +Package containing classes that are used internally by the protocol implementation. Client developers should not need to use these classes at all. +</body> +</html> diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/ClientTicketRequest.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/ClientTicketRequest.java new file mode 100644 index 000000000..d1701b9e9 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/ClientTicketRequest.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 Remy Suen + * 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 + ******************************************************************************/ +package org.eclipse.ecf.protocol.msn.internal.net; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; + +import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils; + +/** + * The ClientTicketRequest class authenticates the user through Passport. This + * is a necessary procedure during the NotificationSession authentication + * process. + */ +public final class ClientTicketRequest { + + /** + * This String value holds the URL of the Passport Nexus page - + * https://nexus.passport.com/rdr/pprdr.asp + */ + private static final String PASSPORT_NEXUS = "https://nexus.passport.com/rdr/pprdr.asp"; //$NON-NLS-1$ + + /** + * The connection that will be used to perform all http requests. + */ + private HttpURLConnection request; + + /** + * TODO: documentation + */ + private String daLoginURL; + + private boolean cancelled = false; + + /** + * Creates a new ClientTicketRequest object with http redirects set to true. + */ + public ClientTicketRequest() { + HttpURLConnection.setFollowRedirects(true); + } + + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + /** + * Retrieves information from {@link #PASSPORT_NEXUS} and stores it in + * {@link #passportInfo}. + * + * @return <code>true</code> if the retrieval process completed + * successfully + * @throws IOException + * If an I/O error occurs while attempting to connect to the + * Passport Nexus page + */ + private boolean getLoginServerAddress() throws IOException { + request = (HttpURLConnection) new URL(PASSPORT_NEXUS).openConnection(); + if (request.getResponseCode() == HttpURLConnection.HTTP_OK) { + daLoginURL = StringUtils.splitSubstring(request + .getHeaderField("PassportURLs"), ",", 1); //$NON-NLS-1$ //$NON-NLS-2$ + daLoginURL = "https://" //$NON-NLS-1$ + + daLoginURL.substring(daLoginURL.indexOf('=') + 1); + request.disconnect(); + return true; + } + request.disconnect(); + return false; + } + + /** + * Retrieves the client ticket that is associated with the given username, + * password, and challenge string. + * + * @param username + * the user's email address + * @param password + * the user's password + * @param challengeString + * the challenge string received from the notification session + * @return the client ticket + * @throws IOException + * If an I/O error occurs while connecting to the Passport Nexus + * page or when getting the response codes from the connection + */ + public synchronized String getTicket(String username, String password, + String challengeString) throws IOException { + if (getLoginServerAddress()) { + username = URLEncoder.encode(username); + password = URLEncoder.encode(password); + try { + while (!cancelled) { + request = (HttpURLConnection) new URL(daLoginURL) + .openConnection(); + request.setRequestProperty("Authorization", //$NON-NLS-1$ + "Passport1.4 OrgVerb=GET,OrgURL=http%3A%2F%2Fmessenger%2Emsn%2Ecom,sign-in=" //$NON-NLS-1$ + + username + ",pwd=" + password + ',' //$NON-NLS-1$ + + challengeString); + if (request.getResponseCode() == HttpURLConnection.HTTP_OK) { + password = null; + String authenticationInfo = request + .getHeaderField("Authentication-Info"); //$NON-NLS-1$ + int start = authenticationInfo.indexOf('\''); + int end = authenticationInfo.lastIndexOf('\''); + request.disconnect(); + return authenticationInfo.substring(start + 1, end); + } else if (request.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) { + daLoginURL = request.getHeaderField("Location"); //$NON-NLS-1$ + // truncate the uri as the received string is of the + // form [http://www.msn.com/] + daLoginURL = daLoginURL.substring(1, daLoginURL + .length() - 1); + } + } + } catch (Exception e) { + if (request.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { + return "401"; //$NON-NLS-1$ + } + e.printStackTrace(); + } finally { + request.disconnect(); + } + } + return "0"; //$NON-NLS-1$ + } +} diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/package.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/package.html new file mode 100644 index 000000000..a77a57fab --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/package.html @@ -0,0 +1,5 @@ +<html> +<body> +Provides classes that deals with connecting to websites or servers that are not directly related to MSN. +</body> +</html> diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/package.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/package.html new file mode 100644 index 000000000..22bd8df44 --- /dev/null +++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/package.html @@ -0,0 +1,5 @@ +<html> +<body> +Provides support for connecting to the MSN servers used by Windows Live Messenger. +</body> +</html> |