-----------------------------------------------
Needs some updates after my patches.
-- Kagami
-----------------------------------------------
Report: A Monad for Writing XMPP Clients
Magnus Henoch
The original goal of the project was to write a library for writing
XMPP IM clients, with the flow of the program codified in an XMPP
monad. My idea was to completely separate the application logic from
the actual sending and receiving of stanzas (the equivalent of
messages or packets in other protocols), but I found that this way of
doing it made the bind function and the run function quite
cumbersome. Instead I embedded the IO monad into the XMPP monad.
Continuations (which in this context means waiting for a certain
packet to arrive without blocking the rest of the program) were only a
tentative part of the original plan, but turned out to be easier than
I thought, once I succeeded in wrapping my head around it.
Though my experience with the library is so far limited, it seems to
be a nice fit for the request-response model of XMPP programming.
Sending a request and waiting for a response in the same function is
quite convenient, as the "polling" and response-matching is taken care
of behind the back of the programmer.
* What is XMPP?
XMPP is a protocol for streaming XML defined in RFCs 3920 and 3921, so
far mostly used for instant messaging. Its main transport method is a
TCP connection starting with a opening tag, and ending (after
the session is over) with a closing tag. Between these come
payload elements, called stanzas. There are three kinds of stanzas,
, and . is used for sending
information to another entity, not necessarily with any previous
contact. is used for performing a specific request; it always
expects a response. is used to signal the availability of
an entity.
* XMPPConnection.hs
This module contains the XMPPConnection class. There are different
ways to make an XMPP connection, but all of these should be able to
implement the three methods: getStanzas (i.e. get stanzas in receive
buffer), sendStanza, and closeConnection.
* TCPConnection.hs
TCPConnection is an instance of XMPPConnection, so far the only one.
It uses the XMLParse module to be able to parse partial input.
getStanzas is ultimately implemented in terms of getString. I wanted
getString to sleep until there is any input, but I found no way of
doing that. Instead, it checks for input once every second.
* XMLParse.hs
This module implements a simple XML parser. Unlike most other XML
parsing libraries, it doesn't require that the XML document is
complete when starting to parse it. In theory a SAX parser could
satisfy this constraint, but in practice the ones I found didn't and
are therefore unsuitable for an XMPP application.
The interesting part of the parser is the getRest function. It takes
a parser function as an argument, attempts to parse the input with it,
and returns the result along with the part of the input that couldn't
be parsed. This means that the caller doesn't have to ensure that a
complete stanza has arrived over the network before calling the
parser. Unfortunately, it also means that invalid XML will not be
reported as an error, since it is indistinguishable from incomplete
input. Using a real SAX parser would fix this problem; however, this
isn't very important for an XMPP client, as it only receives data that
has already been parsed by the server.
The XML data is parsed into the XMLElem data type, which has two
alternatives, one for XML elements and one for character data. The
functions getAttr and getCdata provide convenient access to various
parts of an element. The xmlPath function takes a list of strings and
an element, and for each string descends to the subelement of the
current element with that name.
* XMPP.hs
This module contains the XMPP monad. The XMPP monad has two purposes:
it hides the XMPPConnection from the application code, as it is passed
along behind the scenes; and it lets the application code "stop and
wait" for a stanza fulfilling certain criteria.
The latter feature is what sets my XMPP library apart from others.
Most libraries (including mine) allow the programmer to define
callbacks for certain events, e.g. a service discovery request. This
is fine for requests which only require a single response, but in the
case of a more elaborate conversation, keeping state becomes
necessary. Using closures is a convenient way to keep that state.
Specifically, the bind function of the XMPP monad can, depending on
the return value of the first function, either just execute the next
function (keeping the list of stanza handlers as a state variable), or
package the next function as a continuation to be executed when a
stanza fulfilling a certain criterion arrives.
The actual execution of functions in the XMPP monad is done by the
runXMPP function, which queries the given connection for new stanzas
and calls the corresponding handlers.
* Stanzas.hs
This module contains helper functions that isolate common idioms.
These are of three kinds: functions for sending stanzas, stanza
predicates, and entire handlers for simple XMPP functions.
sendMessage takes two arguments, a recipient and a message text, and
sends the text as a chat message to the recipient. sendPresence sends
the simplest possible presence stanza, which tells the server that the
client is "available". sendIq sends an infoquery request with a
randomly generated ID and returns that ID; in most cases sendIqWait
would be used, as it waits for a response to the request and returns
that response. Finally, sendIqResponse sends a response to a received
request.
The stanza predicates are simply functions of type XMLElem -> Bool;
some of them take additional arguments. They can be combined with the
conj function, which takes two predicates and returns a predicate that
returns true if both composing predicates return true. Most of them
are quite self-explaining. One combination that I've found
particularly useful is "hasBody `conj` isFrom sender" - in a
conversation, this gets the next message from the contact that
actually has text in it, ignoring various meta messages.
The function handleVersion implements the Software Version extension,
through which an entity can tell the name and version of its
implementation, and the operating system it is running on. The
handleVersion function thus takes these three values as arguments, and
installs a handler for requests in the "jabber:iq:version" namespace.
Some XMPP functions could be implemented as simply as this one, while
others would need more integration with the rest of the program.
* Auth.hs
This module contains the startAuth function, which tries to
authenticate given a username, a server and a password. If it fails,
it calls 'error'.
This code currently sends passwords in plain text over the network.
It should be replaced with code that authenticates using SASL.
* JID.hs
A JID (Jabber ID) looks like:
[username@]server[/resource]
The functions getUsername and getResource return the respective parts
of the given JID, or an empty string if that part is absent.
A "bare JID" is a JID without a resource. getBareJid converts any JID
to a bare JID.
* MUC.hs
MUC stands for "Multi-User Chat", an XMPP extension for IRC-like chat
rooms. The functions in this module provide a somewhat unpolished
interface to such rooms.
The joinGroupchat function takes a nickname and a room JID, and tries
to join it. If it succeeds, it returns a function that at any time
can list the room participants; if it fails, it returns Nothing. The
functions isGroupchatMessage, isGroupchatPrivmsg, sendGroupchatMessage
and sendGroupchatPrivateMessage do what their names say.
This module illustrates a problem with the event model I have chosen.
The joinGroupchat function installs a handler to register when people
enter and leave the chatroom, so that this information is always
available to the program, but this precludes the program from adding
its own handler to those events. It would be nice if the MUC module
could somehow send its own high-level events, independent of the
underlying stanza events.