Introduction
This article presents a generalised MSN Messenger framework for producing MSN Messenger style applications (both as standalone applications and integrated into large applications (such as Microsoft Groove). Communications is an essential part of modern software infrastructure and the use of this library removes the requirement for complex programming of server/client design but using an existing technology that is broadly well supported.
This article does not intend to produce a replacement MSN client or server, but rather how to use the included library to quickly and easily produce one. The demonstration application shows the bare minimum required to produce a communications application, however the CIRIP application (also linked above) shows a much more sophisticated and complex implementation.
This library supports MSNP8 and MSNP9 version protocols, and may be used to produce MSN clients in any .NET language from version 1.0 onwards (including .NET 3.5 & Windows Presentation Foundation, as used in the demo).
The library currently supports the following features:
- Login/Logout from MSN network
- User status
- User friendly name
- Contact list synchronisation (read only, it does not support adding new contacts or moving contacts around groups yet)
- Contact friendly name, phone numbers, and status
- Conversations, sending and receiving messages (including font information and typing messages)
- Inviting addition users to conversations
- Conversation plugins
The library currently does not support the following features*:
- File transfer
- Contact pictures (as well as locally)
- Nudges
- Emoticons (although may easily be added via a plugin)
- Activities
- Proxy support (unless set via Internet Explorer)
* These features may be added in later versions, please request other features and I will try and prioritise them.
Background
"MSN Messenger is a freeware instant messaging client that was developed and distributed by Microsoft in 1999 to 2005 and in 2007 for computers running the Microsoft Windows operating system (except Windows Vista), and aimed towards home users. It was renamed Windows Live Messenger in February 2006 as part of Microsoft's Windows Live series of online services and software.
MSN Messenger is often used to refer to the .NET Messenger Service (the protocols and server that allow the system to operate) rather than any particular client.
MSN Messenger uses the Microsoft Notification Protocol (MSNP) over TCP (and optionally over HTTP to deal with proxies) to connect to the .NET Messenger Service — a service offered on port 1863 of messenger.hotmail.com. Its current version is 13 (MSNP13), used by MSN Messenger version 7.5 and other third-party clients. The protocol is not completely secret; Microsoft disclosed version 2 (MSNP2) to developers in 1999 in an Internet Draft, but never released versions 8, 9, 10, 11 or 12 to the public. .NET Messenger Service servers currently only accept protocol versions from 8 and on, so the syntax of new commands from versions 8, 9, 10, 11 and 12 is only known by using sniffers like Wireshark. MSNP13 will be the protocol used in Windows Live Messenger. This program is still not compatible with Mac OS X's browser as of yet." Wikipedia --- MSN Messenger
For a more in-depth look at the MSN protocols see http://www.hypothetic.org/docs/msn/index.php; this article does not cover the MSN protocols at all as it is intended to remove any need for prior knowledge on the subject.
Code Structure
All the code is centred around the MSNController (primarily), MSNSwitchboardController and MSNSwitchboard classes.
MSNController
Creation requires no input parameters.
MSNController controller = new MSNController();
The MSNController class has the following properties;
- String Username {get; set;} - Gets or sets the username of the local client; must be set prior to logging in or authentication will fail.
- String Password {get; set;} - Gets or sets the password of the local client; must be set prior to logging in or authentication will fail. Additionally the get property only returns *s, i.e. if .Password is set to thePassword, the get call will return ***********.
- String FriendlyName {get; set;} - Gets or sets the friendly name of local client. Must not be set until after the controller has connected.
- MSNEnumerations.UserStatus Status {get; set;} - Gets or sets the status of the local client (e.g. online, busy, appear offline, etc). The set property must only be called once logged in or an exception may be thrown (do not rely on this behaviour)
- MSNEnumerations.LoginStatus LoginStatus {get; set;} - Gets or sets the connection status, only accepts valid combinations for the set property (e.g. requesting a status of logged in throws an exception when the controller is already logged in).
The MSNController class also gives access to key other objects (primarily only while connected) via the following properties;
- MSNContactsList ContactsList {get;} - Gets the MSNContactsList object which contains a synchronised version of the contact list.
- MSNSwitchboardController SwitchboardController {get;} - Gets the MSNSwitchboardController which handles conversation creation (and re-creation).
The MSNController class has the following methods;
- void loginMSNClient() - depreciated, logs in the controller.
- void logoutMSNClient() - depreciated, logs out the controller.
- void startConversation(List<String> initialUsers) - starts a new conversation with the following usernames. The usernames specified must already be in the contacts list or the conversation will start without that contact.
The primary hook-ups for MSNController are via the following events (while polling will work correctly, it is highly recommended that the events are used instead, furthermore it is better practice to set variables via the property and then update the user interface (UI) on the relevant event);
- event MSNEventDelegates.StatusChangedEventDelegate LocalClientStatusChanged - Fired when the local client's status changes (online, busy, away, etc.).**
- event MSNEventDelegates.FriendlyNameChangedEventDelegate LocalFriendlyNameChanged - Fired when the local client's friendly name changes; also fired once after logging in to indicate initial friendly name.**
- event MSNEventDelegates.LoginStatusChangedEventDelegate LoginStatusChanged - Fired when the controller connects, disconnects, or is connecting. A status change from logged_out to logging_in to logged_out indicates an authentication failure.
- event MSNEventDelegates.LoginSettingsChangedEventDelegate LoginSettingsChanged - Fired when the Username or Password property is changed.
- event MSNEventDelegates.FriendlyNameChangedEventDelegate FriendlyNameChanged - Fired when an online contact's friendly name has been changed. Also fired on initial connection to specify initial friendly name.**
- event MSNEventDelegates.PhoneNumberChangedEventDelegate PhoneNumberChanged - Fired when an online contact's phone number is changed. Also fired during initial contacts list synchronisation.**
- event MSNEventDelegates.GroupModifiedEventDelegate GroupModified - Fired when a groups is either added or removed. Also fired during initial contacts list synchronisation.**
- event MSNEventDelegates.GroupMemberChangedEventDelegate GroupMemberChanged - Fired when a contact is either added or removed from a group. Also fired during initial contacts list synchronisation.**
- event MSNEventDelegates.ContactStatusChangedEventDelegate ContactStatusChanged - Fired when a contact's status changes.**
- event MSNEventDelegates.ContactAddedEventDelegate ContactAdded - Fired when a contact is added or removed from the contacts list. Also fired during initial contacts list synchronisation.**
- event MSNEventDelegates.MasterListContactAdded MasterListContactAdded - Fired when a contact is added or removed from the forward or reverse lists (i.e. blocked users, users who have requested to add you to their contacts list).**
** Only fires when controller connected.
MSNAuthentication
No access granted outside library; used for authenticating with Microsoft Password, and for handling authentication challenges.
MSNContactsList
Maintains a synchronised version of the contacts list while connected. Group data is wrapped via the MSNGroup class, and contact data (not including local user) is wrapped via the MSNContact class.
- Dictionary<String, MSNContact> Contacts {get;} - returns a reference to a dictionary of contacts via username.
- Dictionary<String, MSNGroup> Groups {get;} - returns a reference to a dictionary of groups via group name.
MSNContact
Has the following properties and methods;
- String Username {get;} - Gets the username of the contact.
- String FriendlyName {get; set;} - Gets or sets the friendly name of the contact.***
- MSNEnumerations.UserStatus Status {get; set;} - Gets or sets the status of the contact.***
- MSNListenableList<MSNGroup> Groups {get;} - Gets a list of groups the contact is a member of.***
- void setListMember(MSNEnumbers.ContactLists list, bool member) - Adds or removes a user from a contacts list (e.g. blocked) .***
- bool getListMember(MSNEnumerations.ContactLists list) - Returns true if the contact is on the specified list, otherwise false.
- void setPhone(MSNEnumerations.PhoneTypes phoneType, String value) - Sets the specified phone number.***
- String getPhone(MSNEnumerations.PhoneTypes phoneType) - Gets the specified phone number for the contact or "" if unset.
*** Only modifies the internal data structure, does NOT modify the online version.
MSNGroup
Has the following properties and methods;
- String GroupName {get;} - Gets the friendly name for the group.
- int GroupNumber {get;} - Gets the group identification number.
- MSNListenableList<MsnContact> Contacts {get;} - Gets a list of contacts in the group.
*** Only modifies the internal data structure, does NOT modify the online version.
MSNSwitchboardController
This class initiates new conversations (and by extension conversation windows). Only two events form the class but both must be handled if conversations are to be allowed;
- event MSNEventDelegates.SwitchboardCreated SwitchboardCreated - Generated when a new conversation is started (either by the local user or a contact). Note that the event is generated when a contact opens a conversation window and NOT when the first message is sent, therefore if the conversation window is opened on this event then it is possible that no message is received if the contact opens a conversation window and then immediately closes it again without sending a message.
- event MSNEventDelegates.SwitchboardReCreated SwitchboardReCreated - Generated when a conversation has closed and is reopened; e.g. a contact starts a conversation, closes his/her conversation window and then re-opens the conversation window. The same applies to the SwitchboardCreated event where the event is called when the remove window is created and not a message sent.
MSNSwitchboard
This class handles a single conversation, and supports conversation plugins via the IMSNSwitchboardPlugin interface.
- event MSNEventDelegates.SwitchboardUserConnected UserConnected - Fired when a contact joins the conversation, both the initial contact the conversation is started with and any additional users.
- event MSNEventDelegates.MessageRecieved MessageRecieved - Fired when a message has been received and has been processed by the plugins (and that the message is set for display, default true).
- event MSNEventDelegates.MessageSent MessageSent - Fired when a message has been processed by the plugins and then sent (and that the message is set for display, default true).
- void sendMessage(MSNUserMessage message) - Sends the specified message (after plugin processing).
- void invite(String username) - Invites the specified username to the conversation (username must be in the contacts list).
- void closeConversation() - Closes the conversation (although the conversation may be restarted via the SwitchboardReCreated event in the MSNSwitchboardController).
- List<String> getConnectedUsers() - Returns a list of usernames currently in the conversation (not including those which have been invited but have not yet joined).
- MSNListenableList<IMSNSwitchboardPlugin> Plugins {get;} - Returns a list of current plugins, plugins should be added to this list.
Note that the conversation is not automatically terminated when the MSNController is disconnected; hence it is possible to logout and still chat to existing conversation members.
Walkthroughs
Connecting to MSN network
- Make MSNController object (controller)
- Set username via controller.Username
- LoginSettingsChanged event fired
- Set password via controller.Password
- LoginSettingsChanged event fired
- Call controller.LoginStatus = MSNEnumerations.LoggedIn
- Library attempts connection
- LoginStatusChanged fires with MSNEnumerations.LoggingIn
- LoginStatusChanged fires with MSNEnumerations.LoggedIn
- LoggedIn handled, client sends initial user status via controller.UserStatus
- LocalClientStatusChanged event fires
- Contacts list starts to synchronise, via ContactAdded, GroupModified, GroupMemberChanged, ContactStatusChanged, and FriendlyNameChanged events
Starting a Conversation
- Call controller.startConversation with list of usernames to join conversation initially
- SwitchboardCreated event called with a new switchboard
- Client code creates a new conversation window from event switchboard
- UserConnected event called once for each conversation member
Sending a Message (given an existing conversation)
- User types a new message and client converts it to a MSNUserOutgoingMessage
- Client sends message via switchboard.sendMessage(MSNUserMessage message)
- Library passes message through each plugin in turn
- If message.getSend() is true message send into MSN network
- If message.getDisplay() is true then MessageSent event fired
Receiving a Message (given an existing conversation)
- Contact sends a message via the conversation switchboard
- Library converts to a MSNUserIncommingMessage
- Library passes through each plugin in turn
- If message.getDisplay() is true then MessageRecieved event fired
Using the Code
Main Application Window
Interface Components
- TextBox usernameTextBox - local user's username
- PasswordBox passwordBox - local user's password
- Button connectionButton - used for connecting/disconnecting MSN network
- DropDownBox statusBox - used for setting local user status (e.g. online, busy, away, etc.)
- TreeView contactsTreeView - used for displaying contacts / groups
- TreeViewItem contactsTreeViewItem - base node of contactsTreeView, displays 'Contacts' in Header
Connecting
In the connectionButton click handler set username and password into controller and set connection status.
private void connectionButton_Click(object sender, RoutedEventArgs e)
{
if (controller.LoginStatus == MSNEnumerations.LoginStatus.LOGGED_OUT) {
controller.LoginStatus = MSNEnumerations.LoginStatus.LOGGED_IN;
}
else if (controller.LoginStatus == MSNEnumerations.LoginStatus.LOGGED_IN) {
controller.Username = usernameTextBox.Text;
controller.Password= passwordBox.Password;
controller.LoginStatus = MSNEnumerations.LoginStatus.LOGGED_OUT;
}
}
Handle LoginStatusChanged; update connection button text, on disconnected clear data structures and on connected set a new status. Additionally it is recommended to disable the connection button while connecting to prevent the user from attempting to connect while already connecting (especially when the internet connection is slow). Note that the event handler is NOT on the event dispatch thread, required for updating UI components.
private void controller_LoginStatusChanged(MSNEnumerations.LoginStatus newStatus)
{
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
if (controller.LoginStatus == MSNEnumerations.LoginStatus.LOGGED_OUT) {
connectionButton.Content = "Connect";
connectionButton.IsEnabled = true;
statusComboBox_SelectionChanged(this, null);
baseNode.Items.Clear();
contactsTreeNodes.Clear();
groupsTreeNodes.Clear();
conversationWindows.Clear();
}
else if (controller.LoginStatus == MSNEnumerations.LoginStatus.LOGGED_IN) {
connectionButton.Content = "Disconnect";
connectionButton.IsEnabled = true;
statusComboBox_SelectionChanged(this, null);
}
else {
connectionButton.Content = "Connecting...";
connectionButton.IsEnabled = false;
}
}));
}
User status
Call controller.Status with a valid status enumeration object. Note that this event handling is not ideal since if user status is changed by a plugin the change will not be reflected in the UI. The UI should be update via the relevant event handler.
private void statusComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (statusComboBox.SelectedItem != null &&
controller != null &&
controller.LoginStatus == MSNEnumerations.LoginStatus.LOGGED_IN) {
String status = ((ComboBoxItem)(statusComboBox.SelectedItem)).Content.ToString().Trim();
MSNEnumerations.UserStatus userStatus = MSNEnumerations.UserStatus.online;
if (status.Equals("Away")) {
userStatus = MSNEnumerations.UserStatus.away;
}
else if (status.Equals("Busy")) {
userStatus = MSNEnumerations.UserStatus.busy;
}
else if (status.Equals("Appear Offline")) {
userStatus = MSNEnumerations.UserStatus.offline;
}
if (controller != null) {
controller.Status = userStatus;
}
}
}
Contacts list synchronisation
Contacts information is stored both in the UI and in two dictionaries (improves performance significantly) as below.
private Dictionary<String, TreeViewItem> contactsTreeNodes =
new Dictionary<string, TreeViewItem>();
private Dictionary<String, TreeViewItem> groupsTreeNodes =
new Dictionary<string, TreeViewItem>();
Handle contacts added or removed; if added create a new TreeViewItem and attach to contactsTreeViewItem. The header may contain the username until the friendly name is known, however the username should also be stored in the tag to allow simple username extraction based upon the node (useful for starting conversations later). Removal involves removing the node from its parent node and the data structure.
private void controller_ContactAdded(string username, bool added)
{
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
if (added) {
TreeViewItem tvi = new TreeViewItem();
tvi.Header = username;
tvi.Tag = username;
tvi.ToolTip = new ToolTip() { Content = username };
tvi.MouseDoubleClick += new MouseButtonEventHandler(tvi_MouseDoubleClick);
contactsTreeNodes.Add(username, tvi);
baseNode.Items.Add(tvi);
}
else {
TreeViewItem tvi = contactsTreeNodes[username];
((TreeViewItem)tvi.Parent).Items.Remove(tvi);
contactsTreeNodes.Remove(username);
}
}));
}
Note that a double click handler has been added to the event handler for starting conversations (see below).
Handle contact friendly name change by simply looking up the relevant node from the contacts dictionary and updating its header.
private void controller_FriendlyNameChanged(string username, string friendlyName)
{
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
TreeViewItem tvi = contactsTreeNodes[username];
tvi.Header = friendlyName;
}));
}
Groups are created and modified in a similar manner.
private void controller_GroupModified(string groupName, bool added)
{
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
if (added) {
TreeViewItem tvi = new TreeViewItem();
tvi.Header = groupName;
tvi.IsExpanded = true;
groupsTreeNodes.Add(groupName, tvi);
baseNode.Items.Add(tvi);
}
else {
TreeViewItem tvi = contactsTreeNodes[groupName];
((TreeViewItem)tvi.Parent).Items.Remove(tvi);
groupsTreeNodes.Remove(groupName);
}
}));
}
private void controller_GroupMemberChanged(string username, string groupName, bool added)
{
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
if (added) {
TreeViewItem contactTvi = contactsTreeNodes[username];
TreeViewItem groupTvi = groupsTreeNodes[groupName];
((TreeViewItem)contactTvi.Parent).Items.Remove(contactTvi);
groupTvi.Items.Add(contactTvi);
}
}));
}
Starting a Conversation
Starting a conversation is handled by the double click handler for contact nodes; the username is in the node's Tag property.
private void tvi_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (controller != null &&
controller.LoginStatus == MSNEnumerations.LoginStatus.LOGGED_IN) {
TreeViewItem tvi = (TreeViewItem)sender;
String name = null;
if (tvi.Tag != null) {
name = tvi.Tag.ToString();
}
List<String> users = new List<string>();
users.Add(name);
controller.startConversation(users);
}
}
Conversation windows (ConversationWindow) are created in the SwitchboardCreated event and reopened in the SwitchboardReCreated event handlers. For ease of lookup for the SwitchboardReCreated event handler a dictionary of MSNSwitchboard, ConversationWindow is added.
private Dictionary<MSNSwitchboard, ConversationWindow> conversationWindows =
new Dictionary<MSNSwitchboard, ConversationWindow>();
private void SwitchboardController_SwitchboardReCreated(MSNSwitchboard switchboard)
{
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
ConversationWindow window = conversationWindows[switchboard];
window.Show();
}));
}
private void SwitchboardController_SwitchboardCreated(MSNSwitchboard switchboard)
{
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
ConversationWindow window = new ConversationWindow(controller, switchboard);
conversationWindows.Add(switchboard, window);
window.Show();
}));
}
Conversation Window
User Interface Components
- System.Windows.WebBrowser conversationFrame - used to display the messages. A Frame was not used as it currently does not support html from memory, but rather only Uri (web or local).
- TextBox sendTextBox - for the user to type outgoing messages.
- Button sendButton - used to send the contents of sendTextBox; should be restricted to maximum message length (100 chars).
Users Connected
Uses joining the conversation should be indicated in the conversationFrame unless it is the initial user joining. The window title is also updated. ConnectedUsers is a list of the connected users. conversationHTML is a String containing the complete message history, which is then updated to the display via conversationFrame.DocumentText.
private void switchboard_UserConnected(string username, bool joined)
{
Console.WriteLine("CONNECTED " + username);
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
if (joined) {
if (connectedUsers.Count > 0) {
conversationHTML += username + " connected...
";
conversationFrame.DocumentText = conversationHTML;
}
connectedUsers.Add(username);
this.Title += " - " + username;
}
else {
if (connectedUsers.Count > 1) {
conversationHTML += username + " disconnected...
";
conversationFrame.DocumentText = conversationHTML;
}
connectedUsers.Remove(username);
this.Title.Replace(" - " + username, "");
}
}));
}
Sending Messages
Sending messages is performed via the sendButton click handler. Additionally when the user is typing in the sendTextBox MSNUserTypingMessages should be sent so that the contact's UI may show 'username' is typing. Similarly the MessageRecieved handler should handle these messages.
private void sendButton_Click(object sender, RoutedEventArgs e)
{
switchboard.sendMessage(new MSNUserOutgoingMessage("Times New Roman", sendTextBox.Text));
sendTextBox.Text = "";
}
Message Display
The message display is updated only by the MessageSent and MessageRecieved event handlers (and NOT the sendButton click event handler), so that any content may be first processed by any plugins.
private void switchboard_MessageSent(MSNUserMessage message)
{
if (message.getMessageType() == MSNEnumerations.UserMessageType.outgoing_text_message) {
Console.WriteLine("MESSAGE>>> " + message.getUserPayload());
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
conversationHTML += controller.FriendlyName + " says: "
+ "<font color=\"Gray\">" + message.getUserPayload() + "</font><br />";
conversationFrame.DocumentText = conversationHTML;
}));
}
}
private void switchboard_MessageRecieved(MSNUserMessage message)
{
if (message.getMessageType() == MSNEnumerations.UserMessageType.incomming_text_message) {
Console.WriteLine("MESSAGE<<< " + message.getUserPayload());
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
new RefreshDelegate(delegate()
{
conversationHTML += controller.ContactsList.Contacts[message.getUsername()].FriendlyName + " says: "
+ "<font color=\"Gray\">" + message.getUserPayload() + "</font><br />";
conversationFrame.DocumentText = conversationHTML;
}));
}
}
Writing a Plugin
Plugins should implement the IMSNSwitchboardPlugin interface which has the following methods;
- void processOutgoingMessage(MSNUserMessage message) - called after the UI calls sendMessage but before the message is actually sent onto the MSN network.
- void processIngoingMessage(MSNUserMessage message) - called after the library receives a message from the MSN network but before it is sent to the UI via the MessageRecieved event.
- MSNSwitchboard Switchboard {get; set;} - used to set the MSNSwitchboard corresponding to the conversation.
An Example Plugin
This plugin replaces any outgoing message equalling @website with http://www.derek-bartram.co.uk and displays sent website address on the local client.
public class WebsitePlugin : IMSNSwitchboardPlugin
{
private MSNSwitchboard switchboard = null;
#region IMSNSwitchboardPlugin Members
public void processOutgoingMessage(MSNUserMessage message)
{
if (switchboard != null &&
message.getMessageType() == MSNEnumerations.UserMessageType.outgoing_text_message &&
message.getUserPayload().Equals("@website"))
{
message.setDisplay(false);
message.setUserPayload("http://www.derek-bartram.co.uk");
#region create local content
MSNUserOutgoingMessage localMessage = new MSNUserOutgoingMessage("Times New Roman", "Sent Website");
localMessage.setSend(false);
localMessage.ProcessByPlugins = false;
switchboard.sendMessage(localMessage);
#endregion
}
}
public void processIngoingMessage(MSNUserMessage message)
{
}
public MSNSwitchboard Switchboard
{
get
{
return switchboard;
}
set
{
switchboard = value;
}
}
#endregion
}
History
Version 1.0.0.0 - Initial build
Additional Licensing Notes
Please feel free to use this in your work, however please be aware that a modified The Code Project Open License (CPOL) is in use; basically it is the same as the standard license except that this code must not be used for commercial or not-for-profit commercial use without prior authorisation. Please see license.txt or license.pdf in the included source and demo files.