![]() |
General Programming »
Internet / Network »
Client/Server Development
Intermediate
License: The Code Project Open License (CPOL)
A MSN Messenger Library for Construction of Communication Based ApplicationsBy Derek BartramAn article presenting a library for producing communication based application utilising the MSN Messenger services and protocols. |
C++/CLI, C# (C# 1.0, C# 2.0, C# 3.0), Forth.NET, Windows (Win2K, WinXP, Win2003, Vista), .NET (.NET 1.0, .NET 1.1, .NET 2.0, .NET 3.0, .NET 3.5), Win32, Win64, Dev, Design
|
||||||||
|
Advanced Search |
|
|
|
||||||||||||||||

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:
The library currently does not support the following features*:
* These features may be added in later versions, please request other features and I will try and prioritise them.
"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.
All the code is centred around the MSNController (primarily), MSNSwitchboardController and MSNSwitchboard classes.
Creation requires no input parameters.
MSNController controller = new MSNController();
The MSNController class has the following properties;
The MSNController class also gives access to key other objects (primarily only while connected) via the following properties;
The MSNController class has the following methods;
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);
** Only fires when controller connected.
No access granted outside library; used for authenticating with Microsoft Password, and for handling authentication challenges.
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.
Has the following properties and methods;
*** Only modifies the internal data structure, does NOT modify the online version.
Has the following properties and methods;
*** Only modifies the internal data structure, does NOT modify the online version.
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;
This class handles a single conversation, and supports conversation plugins via the IMSNSwitchboardPlugin interface.
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.
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;
//Fire a status changed to start contact list synchronisation
statusComboBox_SelectionChanged(this, null);
}
else {
connectionButton.Content = "Connecting...";
connectionButton.IsEnabled = false;
}
}));
}
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 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>(); //username, treenode
private Dictionary<String, TreeViewItem> groupsTreeNodes =
new Dictionary<string, TreeViewItem>(); //group name, treenode
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];
//remove from old parent
((TreeViewItem)contactTvi.Parent).Items.Remove(contactTvi);
//add to new parent
groupTvi.Items.Add(contactTvi);
}
}));
}
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>(); //switchboard, associated ui element
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();
}));
}
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()
{
//recievedBox.AppendText(message.getUserPayload() + "\r\n");
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 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)
{
//do NOT update gui from this
switchboard.sendMessage(new MSNUserOutgoingMessage("Times New Roman", sendTextBox.Text));
sendTextBox.Text = "";
}
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()
{
//recievedBox.AppendText(message.getUserPayload() + "\r\n");
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;
}));
}
}
Plugins should implement the IMSNSwitchboardPlugin interface which has the following methods;
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); //don't display the message locally
message.setUserPayload("http://www.derek-bartram.co.uk"); //update send text
#region create local content
MSNUserOutgoingMessage localMessage = new MSNUserOutgoingMessage("Times New Roman", "Sent Website");
localMessage.setSend(false); //don't send it to remote user
localMessage.ProcessByPlugins = false; //don't process it through plugins as it has already been processed
switchboard.sendMessage(localMessage); //send it
#endregion
}
}
public void processIngoingMessage(MSNUserMessage message)
{
//do nothing, this plugin only affects outgoing messages
}
public MSNSwitchboard Switchboard
{
get
{
return switchboard;
}
set
{
switchboard = value;
}
}
#endregion
}
Version 1.0.0.0 - Initial build
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.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 24 Mar 2008 Editor: |
Copyright 2008 by Derek Bartram Everything else Copyright © CodeProject, 1999-2009 Web13 | Advertise on the Code Project |