Archive for December 2009

Top2OneNote Addin – Making my digital notepad notes searchable

Last week I bought a nice gadget at our discount supermarket (Aldi). The gadget is called a Digital notepad and makes it possible to record your notes while writing them down on paper. Your writing is stored on the internal memory as vector data. There is a lot of software supplied, but using them is not that straightforward.

Digital notepad

Most of the software supplied with this gadget wants to convert the note to clear text. But because of my bad handwriting, most of the time the converted text makes no sense. I also want to keep the drawing which I added to my notes. So instead of converting it to text, I would be happy if I can archive the notes and make them searchable.

Microsoft OneNote gives Tablet PC users the option to add handwritten notes to there OneNote notebook. Those notes are made searchable, while the handwritten note still exists. Instead of using a Tablet PC I want to convert my Digital Notepad content to OneNote handwritten notes. And therefore I developed a small OneNote addin which makes this possible. The source code together with the installer can be found at http://top2onenote.codeplex.com. Within this blogpost, I will explain some of the steps I took to make this addin possible.

Reading a TOP file

There is less information available about the file format that is generated by the Digital Notepad. The file extension is called .Top and it looks like it is a format invented by Waltop International Corp. When searching on the Internet for more information I found this blogpost together with some perl and python scripts to convert a Top file to SVG. I will use the logic of these scripts to convert my notes to OneNote.

Develop an OneNote AddIn

Visual Studio 2008 gives you the possiblity to develop Office Add Ins. There are different project templates supplied for Word, Excel and Outlook… but not for OneNote. There are no Visual Studio templates available for OneNote, so developing a AddIn is not that easy. Again Google gives me a nice search result, a blogpost about creating a toolbar addin for OneNote 2007: http://blogs.msdn.com/descapa/archive/2006/08/31/734298.aspx. There is a step by step tutorial supplied which will explain the different steps to develop a toolbar Addin which will popup a Hello world message. The AddIn will make use of COM Interop, which makes it possible to use C#.

The interface which should be implemented does only contains two methods. I found out that using this interface will only gives you the active OneNote page. With the OneNote.ApplicationClass (part of the Microsoft.Office.OneNote.Interop assembly) you will update the OneNote page outside OneNote. This means that the functionality of your addin does not run within OneNote, but inside a DllHost container.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using OneNote = Microsoft.Office.Interop.OneNote;

namespace Top2OneNoteAddIn
{
   [ComImport, Guid("C9590FA7-2132-47fb-9A78-AF0BF19AF4E6")]
   public interface IOneNoteAddIn
   {
      bool OnClick([In] String strActivePageID);

      bool OnEvent([In] OneNote.OneNoteAddIn_Event evt, [In] String strParameter);
   }
}

Note: together with OneNote 2007 I also installed the OneNote 2010 Beta. It seems like the AddIn functionality will change, because the addin is shown on the toolbar but clicking on it does not trigger the functionality. I think that with the release of OneNote 2010 that I will be easier to develop OneNote Addins, probably with the use of a Visual Studio template.

OpenFileDialog issue

The AddIn should, when clicked, open up a file dialog where the user can select the .top file which should be imported. Because the plugin does not run for within OneNote, the OpenFileDialog was shown below the OneNote window. I found a solution by supplying the Windows handler of OneNote as owner of the OpenFileDialog. This way the dialog is shown on top of OneNote

Process[] procs = Process.GetProcessesByName("OneNote");
IntPtr hwnd = procs[0].MainWindowHandle;

OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = "top files (*.top)|*.top|All files (*.*)|*.*";
dialog.Title = "Select a top file";
dialog.ShowDialog(new WindowWrapper(hwnd));

A little WindowWrapper class is added to the solution, which implements the IWin32Window interface making it possible to supply the window handler as argument to the ShowDialog method.

public class WindowWrapper : System.Windows.Forms.IWin32Window
{
        private IntPtr _hwnd;

        public WindowWrapper(IntPtr handle)
        {
            _hwnd = handle;
        }

        #region IWin32Window Members

        public IntPtr Handle
        {
            get { return _hwnd; }
        }

        #endregion
}

InkAnalyzer

The vector data from the top file is converted to a collection of Stroke objects (for which the constructor gets a StylusPointCollection as argument). Those stoke objects are set to the InkAnalyzer. The Analyze method performs layout analysis, writing and drawing classification, and handwriting recognition. The result is given back as an AnalysisStatus object which will be used for building the XML structure used to update the OneNote page.

InkAnalyzer analyzer = new InkAnalyzer();
analyzer.AddStroke(stroke);

AnalysisStatus status = analyzer.Analyze();
if (status.Successful)
{
   Console.WriteLine(analyzer.GetRecognizedString());
}

The AddStroke method has overloaded method for which you can specify the languageId of the stroke. By default the locale settings of the active thread will be used. In my case the Dutch language id was used (because of my region settings). I use English as my OS display language. When the Dutch language pack is not installed, the result of the analyzer will not be successful. So keep in mind to check if the correct language packs are installed to make handwritten recognition possible. The method analyzer.GetInkRecognizersByPriority() gives back a list of available InkRecognizers for the different languages.

Building the OneNote XML

You can update OneNote pages by providing a XML structure. Adding or updating depends if objectids are supplied. If this is the case, the object will be updated. An example of the XML used to add the handwritten notes to OneNote

<?xml version="1.0"?>
<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2007/onenote"
        ID="{C6CCDE4D-7F47-46FA-9DD6-0A4CEC503E86}{1}{B0}">
	<one:Outline>
		<one:Position x="209.9905395507812" y="126.0"/>
		<one:Size width="245.2818908691406" height="125.3196716308594"/>
		<one:OEChildren>
			<one:OE>
				<one:InkWord recognizedText="hallo">
					<one:Data>AMIGHQSQBOYCAYABKgAaH4tCcEWEdYC+0EXZBFjPVIrml8VPjwb4utLhmyK/
7HHMzCKxQYkOuxMs7nqh3rsTi+Ves0icm6ElWrdREvqHlTLy7BdBuMAWv/Mr9HrEZm2nDbJYSZodTkA0BmwQRVOAL
HoY1USZZUo0BjFbMxSVQJ6SzJhLtWqQHBLFWlUDC0gURSNGI1cNAAAABQcLZU9mZ2hpGRQyCACAHgIh4uJBMwgA
4BICSvPiQRGrqtNBZAMVRgBAShs20YdmzeA0YdmxgA6YdmsBow7GLAB0w7Nm8AAATxMEawMVRgBAahA+gEXPAA
ARXQADggAAIqIAAAqKAW+C/gdb+B19kqxUublEssCwCSiblSgFlABLLFSyypYWLLFlliiC/gR7+BH7ACTZc3LJqblks2W
CzZQ2VNliyklZSMkzVmrKsVKSgNyygAoAESBQIxrXX3TKARMEEgARIKFVgU7sSj5OjNx+XqrpdDQFFEYAQAAABRRGA
QAAAAoAESDcOupAmua8QApiMYL+CJP4IlAAAEoAAEoAgv4BG/gEfKycSbsm5di0qry2bEGy8tUKABEg4IA52V90ygE
TBBIAESCouC8UZctFT6fx0sc+lOXuBRRGAEAAAAUURgFAAAAKP0Aj06oIUjE4DWAKWiGC/gnj+CeQBBZZQACC/gEj
+ARWM5yUt25ddnaFiwAKABEgIH932l90ygETBBIAESAf+viILJADTKojJ+vOFaLHBRRGAEAAAAUURgGAAAAJFiKA30M
Qcv/ZAAp0SYL+BFP4EVAACUALAkJRU3NypSbCwUCC/gDT+ANeJMktzPHc7Le7dtVaNy7WOIgkuJbmpZGrZQAKAB
EgYAtv1V90ygETBBIAESAUVWvkl8H9TbQgjxGuoKlABRRGAEAAAAUURgDAAAAKP0Ai63IF0ksIC+AKggFlgv4MK/gw
25SpYsFlhLAESwlsmybLNypbKAssqWUlllixZYsAgv4EY/gRkAlgZYWWFlEs2WaFUTZVkqJUuUlgAssssVAACgARIGBI
qdtfdMoBEwQSABEgk453gdolgkGItwM1/odLkQUURgBAAAAFFEYBwAAACgARIGLKJkG7kLtA
					</one:Data>
				</one:InkWord>
			</one:OE>
		</one:OEChildren>
	</one:Outline>
	<one:Outline>
		<one:Position x="225.0141754150391" y="461.2535400390625"/>
		<one:Size width="680.25830078125" height="80.7023696899414"/>
		<one:OEChildren>
			<one:OE>
				<one:InkWord recognizedText="dit">...
					</one:Data>
				</one:InkWord>
				<one:InkWord recognizedText="is">
					<one:Data>...
					</one:Data>
				</one:InkWord>
				<one:InkWord recognizedText="een">
					<one:Data>...</one:Data>
				</one:InkWord>
				<one:InkWord recognizedText="test">
					<one:Data>...
					</one:Data>
				</one:InkWord>
			</one:OE>
		</one:OEChildren>
	</one:Outline>
</one:Page>

Only the data for the first word is shown in this example to keep things clear. At the root element (Page) the ID of the existing page (provided as argument of the OnClick method) to define that the content should be inserted to the page.The InkWord data are stored as Ink Serialized Format with BASE64 encoding and placed together with the recognizedText to make searching within the handwritten notes possible.

InkWordNode iwNode = node as InkWordNode;

using (MemoryStream ms = new MemoryStream())
{
   iwNode.Strokes.Save(ms);
   byte[] isfBytes = ms.ToArray();
   dataNode.SetValue(Convert.ToBase64String(isfBytes));
}

Updating OneNote

Updating the OneNote page is done by creating an instance of the OneNote.ApplicationClass. The UpdatePageContent method is used whereby the created XML structure is supplied as argument. There is a little delay between executing the importer and when the page is updated. The cause of this delay is probally that the OneNote page is updated outside OneNote and that it will be triggered to refresh the page content.

OneNote.ApplicationClass onApplication = new OneNote.ApplicationClass();

XNamespace one = "http://schemas.microsoft.com/office/onenote/2007/onenote";
XElement root = new XElement(one + "Page", new XAttribute(XNamespace.Xmlns + "one", one));
root.Add(new XAttribute("ID", strActivePageID));

BuildXML(analyzer.RootNode, root, one);

onApplication.UpdatePageContent(root.ToString(), DateTime.MinValue);

What’s next?

The result of the importer is the same when writing the text directly with the use of a Tablet PC. Probably the results of a Tablet PC are maybe even more secure, but that’s also the reason that I don’t want to convert the handwritten notes to text.

Search within a handwritten note

Search within a handwritten note

The importer could be optimized in different ways. Some issues/ideas to improve this add in

  • Language selection – selected by import or set as a preference
  • Insert at cursor position – currently the location is set to the top left. It is possible to retrieve the cursor position by requesting the page content (GetPageContent) with the use of the OneNote.ApplicationClass. The location of the cursor is marked as a selection inside the retrieved XML structure
  • Improve text recognition – the InkAnalyzer has different properties to improve the text recognition. This way better results are achieved
  • Drag and drop support – instead of using the open file dialog, it would be nice if you can just drag the top file on your OneNote page

Like I mentioned, you can find the source at http://top2onenote.codeplex.com. It is not my goal to put a lot of effort in the development of this AddIn. If you are interested in improving this AddIn, send some feedback and I can add you as contributor to this project. If you have other questions, ask them and I will try to answer them.

XMPP and WCF .Net – Connecting a XMPP client to Openfire

With Openfire installed (http://www.johandekoning.nl/index.php/2009/11/30/xmpp-and-wcf-net-installation-of-openfire/) it is time to create some users and connect with a XMPP client to the server.

User management

Because of the simple installation, users created on the Openfire server are stored inside the embedded database. To add new users, make sure that the server runs and open the administration console (default address http://localhost:9090).

Note: I already mentioned this in my previous blog. Within my Vista environment I had to make sure that I start the Openfire server as administrator

Within the Administration Console, you will find the Users/Groups section at the top of the page. When you open this section you will get an overview of the users created on the Openfire server

User Summary

User Summary

Click on the Create New User option on the left, to create a new user. The Create User page needs some information for the new user. Provide this information and click create user to add the user. Or choose Create & Create another if you want to create another user after this user account is added.

Create User

Create User

I create two users which are called user1 and user2. I gave the the same password as their username. The overview shows the following user list.

User Summary (users created)

User Summary (users created)

One of the core extensions of the XMPP protocol is the ability to have a user list which is stored on the server (which is called a Roster). Within the Administration Console you can manage those lists, to make it possible that user1 has user2 as a contact and visa versa. This is off course also possible from within a XMPP client where you can add new users.

To manage a contacts list (roster) for a user, click on the username (of the user you want to manage) from the overview. You will get the User Properties screen, with some basic information.

User properties

User properties

At the left you will find a Roster option. When you click that option, you get an overview of the roster items. These items can be other users, or groups of users. Click the Add New Item (at the right of the screen) to add user 2 to the roster items of user 1.

Add roster item

Add roster item

When added, the user roster overview will show user 2. Perform the same action for user 2 but this time add user 1 to the roster list.

User Roster

User Roster

When both users are on both list, the user configuration part is finished. I know it is very basic, but this is jus for development. When can now use a XMPP client to make a connection with our server.

Using Psi (XMPP Client)

There are a lot of XMPP clients available on the Internet. I use Psi because it has a nice XML logging feature, to make the XMPP stream visible. I can use this sample stream for development of a XMPP client in .Net without the use of WCF. You can download the Psi client at http://psi-im.org/download/

When you start the client for the first time, it will show you a message that you need to set up an account. Click on the Use existing account option, because our test accounts are already available on the server.

Account setup

Account setup

On the first tab (Account) fill in the Jabber ID (JID) and password. The jabber ids for our users are user1@localfire and user2@localfire . The password are user1 and user2.

Account Tab

Account Tab

At the connection tab, we need to specify our server location. This is localhost when it runs on the same machine where you installed this client. For testing purposes we will not use an encrypt connection (Never).

Connection tab

Connection tab

Click save to store the account information. The main screen of Psi will be visible whereby the user status is set to Offline.

Psi main screen

Psi main screen

Before we connect to our Openfire server, I want to enable the XML console to get an idea of the stream which is send between client and server. Right click on the first item of the list (openfire) and select Open XML Console… Within the XML Console, mark the enable checbox at the bottem left. To get the client connected, select from the dropdownlist at the bottom of the main screen, the Online status. The client will connect to the server and the stream will be made visible in the XML Console dialog.

XML Console

XML Console

The XMPP stream

I connected another client to the server as user 2. Between both users i send a message with the text “Hello” (User 1 -> User 2) and “Goodbye” (User 2 -> User 1). The presence of user 1 will be set to Offline and the application will be closed. Below you find the captured XMPP stream.

What’s next?

This blog post explained how to set up the connecting between a XMPP client and server. The captured stream will be used for further analysis. The next step will be the development of a simple XMPP client which will echo back received messages in reverse order. This client will not be written with Windows Communication Foundation to get some feeling how a .Net client with the TcpClient class will work.

If you have questions after reading this post, or have other feedback? Please send in a comment.

<!--client-->
<?xml version="1.0"?>
<stream:stream xmlns:stream="http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="localfire" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" >

<!--server-->
<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="localfire" id="c035b19c" xml:lang="en" version="1.0">

<stream:features>
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>DIGEST-MD5</mechanism>
<mechanism>PLAIN</mechanism>
<mechanism>ANONYMOUS</mechanism>
<mechanism>CRAM-MD5</mechanism>
</mechanisms>
<compression xmlns="http://jabber.org/features/compress">
<method>zlib</method>
</compression>
<auth xmlns="http://jabber.org/features/iq-auth"/>
<register xmlns="http://jabber.org/features/iq-register"/>
</stream:features>

<!--client-->
<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="DIGEST-MD5" />

<!--server-->
<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cmVhbG09ImxvY2FsZmlyZSIsbm9uY2U9IitaQVRkeUZNeWJjV1ZveHJHTXNIV2dxNVR1QWhNUkFoQ08wVWlp
VzciLHFvcD0iYXV0aCIsY2hhcnNldD11dGYtOCxhbGdvcml0aG09bWQ1LXNlc3M=</challenge>

<!--client-->
<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">dXNlcm5hbWU9InVzZXIxIixyZWFsbT0ibG9jYWxmaXJlIixub25jZT0iK1pBVGR5Rk15YmNXVm94ckdNc0hXZ3E1
VHVBaE1SQWhDTzBVaWlXNyIsY25vbmNlPSJhM0dMY21ra3VFUjEwaVBTMlJzQnpRZWlBY2pzSnIzNVNyTzRkTFNzM
2FnPSIsbmM9MDAwMDAwMDEsZGlnZXN0LXVyaT0ieG1wcC9sb2NhbGZpcmUiLHFvcD1hdXRoLHJlc3BvbnNlPWM1N
TlhMjBhMmFkZGVjYmJjOTFkZTZkNDYwODYyODU5LGNoYXJzZXQ9dXRmLTg=</response>

<!--server-->
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cnNwYXV0aD04NjMxMTIzNDZlM2MxOTUxMGZlNzFmNTA5N2E0MWViMg==</success>

<!--client-->
<?xml version="1.0"?>
<stream:stream xmlns:stream="http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="localfire" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" >

<!--server-->
<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="localfire" id="c035b19c" xml:lang="en" version="1.0">
<stream:features>
<compression xmlns="http://jabber.org/features/compress">
<method>zlib</method>
</compression>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>
</stream:features>

<!--client-->
<iq type="set" id="bind_1" >
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<resource>Home</resource>
</bind>
</iq>

<!--server-->
<iq xmlns="jabber:client" type="result" id="bind_1" to="localfire/c035b19c" >
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<jid>user1@localfire/Home</jid>
</bind>
</iq>

<!--client-->
<iq type="set" id="aab7a" >
<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>
</iq>

<!--server-->
<iq type="result" id="aab7a" to="user1@localfire/Home" >
<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>
</iq>

<!--client-->
<iq type="get" id="aab8a" >
<query xmlns="jabber:iq:roster"/>
</iq>

<!--server-->
<iq type="result" id="aab8a" to="user1@localfire/Home" >
<query xmlns="jabber:iq:roster">
<item subscription="both" jid="user2@localfire" />
</query>
</iq>

<!--client-->
<presence>
<priority>5</priority>
<c xmlns="http://jabber.org/protocol/caps" node="http://psi-im.org/caps" ver="0.13-dev-rev2" ext="ca cs ep-notify html" />
</presence>

<iq type="get" to="user1@localfire/Home" id="aabaa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://psi-im.org/caps#ca" />
</iq>

<iq type="get" id="aabba" >
<query xmlns="jabber:iq:private">
<storage xmlns="storage:bookmarks"/>
</query>
</iq>

<iq type="get" to="user1@localfire" id="aabca" >
<vCard xmlns="vcard-temp" version="2.0" prodid="-//HandGen//NONSGML vGen v1.0//EN" />
</iq>

<iq type="get" to="localfire" id="aabda" >
<query xmlns="http://jabber.org/protocol/disco#info"/>
</iq>

<!--server-->
<presence from="user2@localfire/Work" to="user1@localfire/Home" >
<c xmlns="http://jabber.org/protocol/caps" node="http://exodus.jabberstudio.org/caps" ext="xhtml-im" ver="0.10.0.0" />
<priority>1</priority>
</presence>

<!--client-->
<iq type="get" to="user2@localfire/Work" id="aabea" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://exodus.jabberstudio.org/caps#0.10.0.0" />
</iq>

<iq type="get" to="user2@localfire/Work" id="aabfa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://exodus.jabberstudio.org/caps#xhtml-im" />
</iq>

<!--server-->
<iq from="user1@localfire/Home" type="get" to="user1@localfire/Home" id="aabaa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://psi-im.org/caps#ca" />
</iq>

<!--client-->
<iq type="result" to="user1@localfire/Home" id="aabaa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://psi-im.org/caps#ca" >
<identity category="client" type="pc" name="Psi" />
<feature var="urn:xmpp:jingle:1" />
<feature var="urn:xmpp:jingle:transports:ice-udp:1" />
<feature var="urn:xmpp:jingle:apps:rtp:1" />
<feature var="urn:xmpp:jingle:apps:rtp:audio" />
</query>
</iq>

<!--server-->
<iq type="result" id="aabba" to="user1@localfire/Home" >
<query xmlns="jabber:iq:private">

<storage xmlns="storage:bookmarks"/>
</query>
</iq>

<iq from="user1@localfire" type="result" id="aabca" to="user1@localfire/Home" >
<vCard xmlns="vcard-temp" version="2.0" prodid="-//HandGen//NONSGML vGen v1.0//EN" >
<FN>User 1</FN>
</vCard>
</iq>

<iq from="localfire" type="result" id="aabda" to="user1@localfire/Home" >
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="server" type="im" name="Openfire Server" />
<identity category="pubsub" type="pep" />
<feature var="http://jabber.org/protocol/pubsub#manage-subscriptions" />
<feature var="http://jabber.org/protocol/pubsub#modify-affiliations" />
<feature var="http://jabber.org/protocol/pubsub#retrieve-default" />
<feature var="http://jabber.org/protocol/pubsub#collections" />
<feature var="jabber:iq:private" />
<feature var="http://jabber.org/protocol/disco#items" />
<feature var="vcard-temp" />
<feature var="http://jabber.org/protocol/pubsub#publish" />
<feature var="http://jabber.org/protocol/pubsub#subscribe" />
<feature var="http://jabber.org/protocol/pubsub#retract-items" />
<feature var="http://jabber.org/protocol/offline" />
<feature var="http://jabber.org/protocol/pubsub#meta-data" />
<feature var="jabber:iq:register" />
<feature var="http://jabber.org/protocol/pubsub#retrieve-subscriptions" />
<feature var="http://jabber.org/protocol/pubsub#default_access_model_open" />
<feature var="jabber:iq:roster" />
<feature var="http://jabber.org/protocol/pubsub#config-node" />
<feature var="http://jabber.org/protocol/address" />
<feature var="http://jabber.org/protocol/pubsub#publisher-affiliation" />
<feature var="http://jabber.org/protocol/pubsub#item-ids" />
<feature var="http://jabber.org/protocol/pubsub#instant-nodes" />
<feature var="http://jabber.org/protocol/commands" />
<feature var="http://jabber.org/protocol/pubsub#multi-subscribe" />
<feature var="http://jabber.org/protocol/pubsub#outcast-affiliation" />
<feature var="http://jabber.org/protocol/pubsub#get-pending" />
<feature var="google:jingleinfo" />
<feature var="jabber:iq:privacy" />
<feature var="http://jabber.org/protocol/pubsub#subscription-options" />
<feature var="jabber:iq:last" />
<feature var="http://jabber.org/protocol/pubsub#create-and-configure" />
<feature var="urn:xmpp:ping" />
<feature var="http://jabber.org/protocol/pubsub#retrieve-items" />
<feature var="jabber:iq:time" />
<feature var="http://jabber.org/protocol/pubsub#create-nodes" />
<feature var="http://jabber.org/protocol/pubsub#persistent-items" />
<feature var="jabber:iq:version" />
<feature var="http://jabber.org/protocol/pubsub#presence-notifications" />
<feature var="http://jabber.org/protocol/pubsub" />
<feature var="http://jabber.org/protocol/pubsub#retrieve-affiliations" />
<feature var="http://jabber.org/protocol/pubsub#delete-nodes" />
<feature var="http://jabber.org/protocol/pubsub#purge-nodes" />
<feature var="http://jabber.org/protocol/disco#info" />
<feature var="http://jabber.org/protocol/rsm" />
</query>
</iq>

<iq from="user2@localfire/Work" type="result" id="aabea" to="user1@localfire/Home" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://exodus.jabberstudio.org/caps#0.10.0.0" >
<feature var="jabber:iq:agents" />
<feature var="jabber:iq:oob" />
<feature var="jabber:iq:browse" />
<feature var="jabber:iq:time" />
<feature var="jabber:iq:version" />
<feature var="jabber:iq:last" />
<feature var="http://jabber.org/protocol/disco#items" />
<feature var="http://jabber.org/protocol/disco#info" />
<feature var="storage:bookmarks" />
<feature var="jabber:x:data" />
<feature var="jabber:x:conference" />
<feature var="jabber:x:event" />
<feature var="http://jabber.org/protocol/muc" />
<feature var="http://jabber.org/protocol/muc#user" />
<feature var="http://jabber.org/protocol/muc#owner" />
<feature var="http://jabber.org/protocol/si" />
<feature var="http://jabber.org/protocol/si/profile/file-transfer" />
<feature var="http://jabber.org/protocol/bytestreams" />
</query>
</iq>

<iq from="user2@localfire/Work" type="result" id="aabfa" to="user1@localfire/Home" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://exodus.jabberstudio.org/caps#xhtml-im" >
<feature var="http://jabber.org/protocol/xhtml-im" />
</query>
</iq>

<iq from="user1@localfire/Home" type="result" to="user1@localfire/Home" id="aabaa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://psi-im.org/caps#ca" >
<identity category="client" type="pc" name="Psi" />
<feature var="urn:xmpp:jingle:1" />
<feature var="urn:xmpp:jingle:transports:ice-udp:1" />
<feature var="urn:xmpp:jingle:apps:rtp:1" />
<feature var="urn:xmpp:jingle:apps:rtp:audio" />
</query>
</iq>

<!--client-->
<message type="chat" to="user2@localfire" id="aac0a" >
<body>Hello</body>
<active xmlns="http://jabber.org/protocol/chatstates"/>
</message>

<!--server-->
<message from="user2@localfire/Work" type="chat" id="jcl_13" to="user1@localfire/Home" >
<body>Goodbye</body>
<x xmlns="jabber:x:event">
<composing/>
</x>
<html xmlns="http://jabber.org/protocol/xhtml-im">
<body xmlns="http://www.w3.org/1999/xhtml">
<p style="font-size:small;font-family:Arial;color:#000000" >Goodbye</p>
</body>
</html>
</message>

<!--client-->
<presence type="unavailable" >
<status>Logged out</status>
</presence>

</stream:stream>

<!--server-->
</stream:stream>