- What Is a Jabber Client?
- Session Mechanics
- Protocol Mechanics
- Protocol Details
- The <iq> Element
- Summary
The <iq> Element
The login example showed a number of uses for the <iq> tag, and it acted as a container for both outgoing requests (from the client to the server) and incoming responses (server to client). Note that in contrast to certain other protocol structures, the info/query streams are "in band" with other message types and can occur asynchronously. A nice side effect of completely in-band protocols is that unlike email, where registration is a completely separate and distinct process, a new user can register with a reachable Jabber server on the fly.
Other than the asynchronous nature of requests and responses, the mechanism is like HTTP-GET or -PUT: It enables two entities to make requests and receive responses.
An info/query stanza may possess the following attributes, though not all are required or even appropriate in every query conversation, and might reasonably be ignored by either participant in the conversation:
toSpecifies the intended recipient of the query stanza. This is optional for most query types, as for example when we asked for the registration information from the server earlier or even when we sent back our registration information. In the first case, the jerry-rigged "client" just wanted some information from the physical jabberd to which we Telneted. It would have been superfluous to send a specific to entity. In both cases, the client had a really good idea of the identity of the intended recipient. Equally, when the Jabber server responded, it had a good idea of the physical entity to whom it was responding (and no idea of a jabber-id because one didn't yet exist). It might be, however, that the info/query is not destined for the jabberd itself but rather for the account setup portion of your news and sports delivery component, in which case the switch will definitely want a destination address to which the query stanza should be routed.
fromSpecifies the sender of the query stanza. This is optional for most query types, as for example when we asked for the registration information from the server earlier, or even when we sent back the registration information. Because the user didn't exist at either of those points in time, it would have been strange (actually incorrect) for the server to expect a jabber-id. The same holds true of the server's responses. Again, it might be the case that the info/query is not destined for the jabberd itself, in which case the sender's JID might be important. The from='' attribute is checked by the server to see whether it matches your user@server. If that part does not match, then it overwrites the from='' you sent from the Client with the JID that you used to authenticate (that is log in). This enables you to control your resource, but you cannot SPAM other users by saying that you are someone else.
idAn optional unique identifier for the purpose of tracking the request-response interaction. The sender of the query stanza could set this attribute, which may be returned in any replies. As you may have noticed in the previous examples, you could type in any valid string for an ID. The responder used the same ID to let the sender know to what the response referred. Writing a real client, you would probably want to use a random number generator to assure uniqueness for query ID.
-
typeThis attribute is required in all queries and specifies a distinct point of interaction. Transactions and responses generally follow the pattern shown in Figure 3.6.
Figure 3.6 Common C2S queries and responses.
The type Attribute
If you look back at the example client-to-server (C2S) interactions, you will have noticed that the dialogue involved in a query is actually pretty simple, and is defined by the type attribute in the <iq> tag. You can either "get" something (information usually) from a Jabber server, or "set" something at the switch. Normally, a "set" sends stuff to the switch, which will persist for you in the .xml data file it stores on your behalf. What you get back is either a "result" or an "error." Any query-capable client you write therefore simply has to know how to generate queries and tell the difference between good and bad results.
You saw an example of a "get"-type query in the request for registration information earlier, and a "set"-type query when registration information was supplied to create the user. You saw "result" and "error" responses when we successfully registered a new user and then tried to register again.
The value of type must be one of the following (anything else is ignored):
-
getThe stanza is a request for information. The details of the request will be found in the child element for the tag. The child element must be qualified by namespace. For example, a request for registration information uses the xml namespace jabber:iq:register. A login request uses jabber:iq:auth. The get queries shown in the C2S example in Figure 3.6 expect to see one of the results returned.
-
setThe stanza contains data intended to provide required data (most likely specified in a previous returned result, set new values, or replace existing values. The 'get' queries shown in the C2S example in Figure 3.6 expect to see one of the results returned.
- resultThe stanza is a response to a successful get or set request. The result might have a single XML child element with enumeration and/or clarification of data required in a subsequent set query. The single XML child element is a <query> tag pair. In the example that requested registration information, the server returned a <query> with several children, with each tag representing a required bit of information or a clarification (the <instructions> element) suitable for display out to a human reader:
- errorMeans that an error has occurred in the processing (or delivery, in the case of a message being routed to some other entity) of a previously-sent get or set. The returned stanza must include a single <error/> child element, which in turn must contain a numeric code attribute corresponding to one of the standard error codes. It could also contain an optional text string as element text corresponding to a description of the error. In the example where we tried to double register "Mr. Praline," the server returned an <iq> whose type was error and whose child element was <error code='409'>Username Not Available</error>. A complete list of error codes is shown in Table 3.1.
<query xmlns='jabber:iq:register'> <password/> <instructions> Choose a username and password to register with this server. </instructions> <name/> <email/> <username/> </query>
Table 3.1 Complete List of Jabber Error Codes
Code |
Description |
302 |
Redirect |
400 |
Bad Request |
401 |
Unauthorized |
402 |
Payment Required |
403 |
Forbidden |
404 |
Not Found |
405 |
Not Allowed |
406 |
Not Acceptable |
407 |
Registration Required |
408 |
Request Timeout |
409 |
Conflict |
500 |
Internal Server Error |
501 |
Not Implemented |
502 |
Remote Server Error |
503 |
Service Unavailable |
504 |
Remote Server Timeout |
510 |
Disconnected |
TIP
If you're familiar with HTTP, you'll recognize the numbers in Table 3.1 as the same as HTTP status codes.
Data and Namespaces in <iq> Elements
A key point is that the data content of both requests and responses is qualified by the namespace declaration of a single direct child element of the <iq> element. That is, to get a definition of what the data mean, you can use the namespace to clarify. The types of topics that can be included in an <iq> query are qualified by different namespaces that can be included in the tag. When we logged in with the following:
<iq id='WhateverYouWantHere' type='set'> <query xmlns='jabber:iq:auth'> <username>Praline</username> <password>Cleese</password> <resource>telnet</resource> </query> </iq>
the meaning of the children tags of the <query> tag are explained in the jabber:iq:auth namespace.
If you are an XML purist we should say that an <iq> element can have only one legitimate child, the <error/> tag. Otherwise however, whenever there's a <query> element, the children are always XML from another namespace. An info/query stanza can contain any properly-namespaced child element. Even a simple result might contain such, although it might be unusual. Implement only what is truly necessary in your client and forget the rest. A complete list of all the namespaces relevant in Jabber, including those used in info/queries, is shown in Table 3.2.
Table 3.2 Complete List of Jabber Namespaces
Mnemonic (from jabberPy library) |
Namespace |
NS_CLIENT |
jabber:client |
NS_SERVER |
jabber:server |
NS_AUTH |
jabber:iq:auth |
NS_REGISTER |
jabber:iq:register |
NS_ROSTER |
jabber:iq:roster |
NS_OFFLINE |
jabber:x:offline |
NS_AGENT |
jabber:iq:agent |
NS_AGENTS |
jabber:iq:agents |
NS_DELAY |
jabber:x:delay |
NS_VERSION |
jabber:iq:version |
NS_TIME |
jabber:iq:time |
NS_VCARD |
vcard-temp |
NS_PRIVATE |
jabber:iq:private |
NS_SEARCH |
jabber:iq:search |
NS_OOB |
jabber:iq:oob |
NS_XOOB |
jabber:x:oob |
NS_ADMIN |
jabber:iq:admin |
NS_FILTER |
jabber:iq:filter |
NS_AUTH_0K |
jabber:iq:auth:0k |
NS_BROWSE |
jabber:iq:browse |
NS_EVENT |
jabber:x:event |
NS_CONFERENCE |
jabber:iq:conference |
NS_SIGNED |
jabber:x:signed |
NS_ENCRYPTED |
jabber:x:encrypted |
NS_GATEWAY |
jabber:iq:gateway |
NS_LAST |
jabber:iq:last |
NS_ENVELOPE |
jabber:x:envelope |
NS_EXPIRE |
jabber:x:expire |
NS_XHTML |
|
NS_XDBGINSERT |
jabber:xdb:ginsert |
NS_XDBNSLIST |
jabber:xdb:nslist |
Jabber Presence
The <presence/> element has a number of attributes and subelements which we'll present in practical usage first, then document more formally.
We presented an instance of this tag earlier in the Telnet session when we suggested you try typing in a single-line stanza consisting of only a <presence/> element. Remember that the Jabber server returned a number of <presence> elements in response. The primary purpose of this element is the synchronization of presence and availability among peers who care. By definition, and in every switch implementation, this means each member of a client's roster. Normally, the switch may cache this list, but because the in-memory list is a clone copy, it may refer to the persistent stored version for reliability as needed.
Notice that you didn't need to add to or from' attributes to your initial presence message. The server's automatic reaction upon receiving an otherwise unqualified <presence/> message is to read your roster from its external persistent store and do two things. First, it gets the presence of the members of your roster list by sending them presence messages with an attribute of probe. For example:
<presence type='probe' to='jane@localhost' from='praline@localhost'/>
If the server can't connect with the probed user, then it simply drops the packet. Because virtually every GUI client initializes with client's entire roster shown as "offline," the portrayal of roster items changes only on arrival of an appropriate <presence/> message.
Second, if the server does connect to a member of your roster, the delivery of a presence-probe message to the client stimulates the client to reply with its complete presence state. What gets returned to you may vary from client to client. The server delivers that message back to your client, which then updates its GUI. You also did not have to specify a status element on your initial presence message. The server assumes a state of "available" for you.
This "whatever happens, happens" brand of interaction is illustrated in Figure 3.7. The server receives a simple <presence/> message from Client A, reads Client A's roster and expands, then sends presences messages to Client B and C. B responds with a <presence/> packet addressed to Client A, which then updates its GUI, but no response is ever received from Client C. Of course, when Client C finally does come online, it will send a <presence/> packet to Client A, to which Client A will respond; on response, Client C updates its GUI.
Figure 3.7 Presence probes and response interactions.
You've seen how the server mediates on behalf of Client A to multicast its presence to its roster. So how does Client A get updated with Client C's presence when Client C comes online, or from Client B when its presence changes? The user at Client B might, for example, explicitly set a presence using the client GUI, or Client B might just disconnect.
In both cases, the server actually does the honors, as one client doesn't normally send presence packets directly to another (although it's possible to bypass the server's Jabber session managementJSMmodule if both parties are subscribed to each other's presence). Because Praline@localhost and jane@localhost are subscribed to one another, Mr. Praline can, after login, simply generate a packet like this:
<presence to='jane@localhost/Home' from='Praline@localhost'> <status>returning dead parrot</status> <priority>0</priority> </presence>
and toss it over the wall to the server. If you have debug turned on at the server (for example, by running the server from the command line with the D option), as in
.\jabberd -D -H .
you'll first see the routing request being generated by the server, then the following:
deliver.c:257 deliver( to[jane@localhost/Home],from[Praline@localhost],type[2],packet[ <presence to='jane@localhost/Home' from='Praline@localhost'> <status> returning dead parrot </status> <priority>0</priority> </presence> ])
The server is generally quite happy to route something for you, so it wraps the request in a <route> packet and delivers it to jane@localhost. The problem is (as you'll notice if you run this experiment) that jane@localhost does not respond with its presence. You are short-circuiting the presence and availability mechanism, so this is not a wise way to write a client. Rather you should rely upon the subscription mechanisms in the Jabber server to make the necessary circuits for you.
To allow the server to manage subscriptions for a client pair, the two clients must mutually agree to subscribe to each other's presence and availability. One client can't just probe another client to whom it's not subscribed. Try it using the Telnet approach you saw earlier to generate a presence probe, in this case from Mr. Praline to bill@localhost (these two are not cross-subscribed):
<presence to='bill@localhost/Home' from='Praline@localhost' type='probe'/>
The server responds by dropping the packet, as shown in the following server trace:
Sun Feb 23 14:39:59 2003 mod_presence Praline@localhost attempted to probe by someone not qualified
You can, however, probe someone with whom you have agreed to share presence information. Here, praline@localhost and jane@localhost have both agreed to share presence, so Mr. Praline can type in (in a Telnet session):
<presence to='jane@localhost' from='Praline@localhost' type='probe'/>
And now the server allows the connection. In response, jane@localhost replies:
<presence from='jane@localhost/Home' to='Praline@localhost'> <status>available</status> <priority>0</priority> <x xmlns='jabber:x:delay' from='jane@localhost/Home' stamp='20030223T19:42:13'/> </presence>
There are some interesting subelements in the response packet. The <status>... bit you have already seen. Generally, clients return either a <show></show> pair or a <status></status> pair (but not often both). Either contains CDATA, which can be any free text, as we document later. There's also a <priority> tag, which is used to help distinguish one presence from another: It may be the case that you are logged in to a particular Jabber server on two different machines; a higher priority value in the element suggests to the server that this is the client to preferentially route packets. Finally, the <x xmlns='jabber:x:delay'...> bit is a time stamp that a client can use to determine the vintage of information. In this example, the available status was good as of about 7:42 p.m. (time-relative to the server).
Notice that the entire series of conversations takes place asynchronously and relies on each party in the conversation to do its part. What impresses us as especially clever and in keeping with the overall design philosophy is the asynchronous nature of so many things in Jabber. There is very little need for the Jabber server to keep huge amounts of state information for clients. The switch simply attempts to act as a switch or a router. If it's unable to route a message, then it allows the failure to happen. When clients are offline, their information is persisted into an XML file, including their roster contents, and (optionally) their preferences. When a client returns online, the Jabber server reads in the persisted XML file, whose format we covered in Chapter 2, "Installing and Configuring Jabber Software," using its XDB capability, and acts upon what it finds (particularly roster information) there. The switch follows to the rule, "Do only what's required and no more." Strict adherence to this rule makes for a lean and mean switch, both functionally and footprint-wise.
Notice too the division of labor among parties in Jabber. The server merely delivers the presence-probe to the clients on your roster. What they choose to do in response to the probe is not the server's concern. Clients most often respond by sending a presence packetindeed that's the expected behavior. When a client sends a responsorial presence packet, the server simply delivers it to you without having to remember that this packet is somehow connected with something that it sent to you previously.
NOTE
We aren't going to cover the intricacies of writing a complete server in this book, but understanding these concepts will help you should you decide to use the open source Jabber server as a way of getting started toward building your own or making your own contribution to the open source implementation.
Presence Attributes
In the primitive example at the beginning of the chapter, the presence message was unmodified by additional attributes, but there are a few worthy of mention that you need to put into your programming toolkit. Let's formally talk about them now.
The <presence> Element
In addition to being used as a carrier for clients' presence and availability, the <presence/> element is also used by the server as a management tool for connected clients. The <presence/> element is also used to negotiate and manage subscriptions to the presence of other entities. You have seen some practical examples of using presence already, but to formalize your understanding, you should look at some of the attributes of the <presence/> tag itself.
The <presence/> Attributes
A presence element can have several attributes, not all of which are appropriate to a given purpose.
toSpecifies the intended recipient of the presence stanza (if any). Often, this attribute is filled in by the Jabber server rather than specified by a client. A <presence/> message with no other qualification attributes is normally issued when a client session connects. This is expanded by the Jabber server into messages for each of the client's roster members. After it is expanded by the server, a <presence> message contains both to and from attributes. Examples were shown earlier in the section "Protocol Mechanics."
fromSpecifies the sender of the presence stanza. Normally this is reflected back to a sender as a part of a response to a presence multi-cast managed by the Jabber server's session manager module. Again, the from attribute is verified against your JID and overwritten if the user@server does not match the JID with which you logged in. Usually it's safest to not send it.
idA unique identifier for the purpose of tracking presence. The sender of the presence stanza sets this attribute, which may be returned in any replies. Although it is permissible to include an id attribute, it has no value for presence management.
typeDescribes the availability state, subscription request, presence request, or error. When there is no type attribute there is an implied value of available. When specified, type should have one of the values shown in Table 3.3.
Table 3.3 Complete List of Jabber <presence/> Type Attributes
Value |
Significance |
available |
This is the default availability value. To signify a presence of "available," don't set a type attribute at all in the outgoing packet. |
unavailable |
The sending client (identified in the from attribute) is no longer available for communication. This likely will have been sent by the Jabber server (on a client's behalf) when a client disconnect was detected. |
probe |
This attribute is normally issued by the Jabber server on behalf of a client and addressed to the clients on the originating client's roster. The normal form is: <presence type='probe' to='user@server' from='user@server'/> A resource may be specified too (as in to='user@server/resource', and is not ignored if present. A client may do its own probes (only to clients with which it has mutually agreed to share presence informationavoids wanton misuse of the capability). The <presence> message must be fully qualified with from, to, and type=probe attributes. |
subscribe |
The sending client (identified in the from attribute) wishes to subscribe to the recipient's presence. The recipient is identified in the to attribute. |
unsubscribe |
The sending client (identified in the from attribute) wishes to unsubscribe to the recipient's presence. The recipient is identified in the to attribute. |
subscribed |
The sending client (identified in the from attribute) agrees to allow the recipient (identified in the to attribute) to access its presence information. The recipient originated the conversational exchange via a presence message whose type was subscribe. |
unsubscribed |
The sending client (identified in the from attribute) acknowledges an unsubscribe request from the recipient (identified in the to attribute) to access its presence information. The recipient originated the conversational exchange via a presence message whose type was unsubscribe. |
error |
An error occurred in the server's processing or delivery of the presence stanza. |
The <presence> Subelements
A presence element can have four distinct stanzas as subelements, not all of which are needed in a particular situation.
<show/>This stanza describes the availability of a client in terms of specific values. Other values are ignored. A <show> element typically qualifies a <presence> element whose type attribute is available. A presence element usually contains a <show> child element or a <status> child element but not both, as shown in Table 3.4.
Table 3.4 Complete List of Jabber "Standard" Presence Codes
Value |
Significance |
away |
The client specified in the from attribute is temporarily away from the keyboard. |
chat |
The client specified in the from attribute is available for "chat." Generally superfluous, as an available client is assumed to be available for chat. |
dnd |
The client specified in the from attribute has marked its availability as "do not disturb" (dnd = "Do Not Disturb"). |
normal |
The client is available. Once again, generally superfluous. |
xa |
The client specified in the from attribute is away from the keyboard for an extended period of time. (xa == "eXtended Away"). |
<status/>An optional natural-language description of availability status. Normally a client issues this as an explicit expression of availability: "I am in a meeting," "I am writing," and so on. It is used in conjunction with the show element to provide a detailed description of an availability state (for example, "In a meeting").
-
<priority/>The primary purpose of the priority stanza is to suggest to the server which instance of a particular jabberId@jabberServer is the "best" or "first amongst equals" to which a message ought to be routed. Why is this important? Well, suppose you're logged in at your work desk as greenEggs@ham.com, but like many of us you are managing by walking around and you are also logged in on your PDA as greenEggs@ham.com. Now suppose Sam-I-Am@ham.com wants to send you a message. To which presence does the server choose to route a message? If the message is fully qualified with jabberID@jabberServer/Resource, then this might certainly be used to disambiguate. But what if Sam-I-Am is not too brilliant and just addresses a message without a resource? This is where priority comes into play. Clients can set a priority (a non-negative integer) to represent the priority level of the connected resource, with zero as the lowest priority. The higher the number, the more preferred the resource is. Note that although the specification allows a negative priority number to mean that the sender should not be used for direct or immediate contact, not all clients or servers implement this control. Almost all known GUI clients have a control panel to allow setting this value as shown in Figure 3.8.
Figure 3.8 Setting presence priority via GUI.
<error/>If the server routes a <presence/> message that includes a type="error" attribute, the presence stanza must include an <error/> child element, which in turn should contain a numeric code attribute corresponding to one of the standard error codes listed in Table 3.1 and may also contain PCDATA corresponding to a natural-language description of the error.
Finally, note that a <presence/> message might also contain a namespace-qualified subelement. You as a developer of a client can choose to either ignore these or parse the ones important to your application.
The <message> Element
The message element is the unglamourous "foot soldier" of all the message types. Its attributes and child elements are relatively easy to understand. A minimal message must contain the address elements as attributes on the message tag itself. The to attribute must appear, and a <body> subelement:
<message id='' to='jane@localhost' from='dana@localhost/Home'> <body>hi</body> </message>
Again, in a Client, the from is verified against your JID and overwritten if the user@server does not match the JID with which you logged in. It is not incorrect to set and send it, but it is usually best to not send it at all.
Assuming that the two clients were connected to localhost, jane@localhost would receive a message from dana@localhost, and this would result in a message rather than a chat session because no type field has been specified.
If a chat stream had been opened by one of the end-points specifying type=chat as an attribute on the message tag, then a chat window is opened and the message becomes slightly more complex (see Figure 3.9).
Figure 3.9 Typical message content in a GUI.
In the first message exchange, the chat session is initiated by setting the message type to chat (type='chat'). A thread is established (the thread subelement) to uniquely tag the session between the two clients. More about the <thread/> subelement briefly.
<message id='jcl_7' to='jane@localhost' type='chat' from='dana@localhost/Home'> <thread>9164572e818c2845f73b442cbc62e1ccf6c3cb48</thread> <body>how's the sailing today?</body> <x xmlns='jabber:x:event'> <composing/> </x> </message>
For completeness' sake, let's identify the attributes and subelements of <message/> formally.
The <message/> Attributes
A message element can have several attributes, not all of which are appropriate to a given purpose.
toSpecifies the message's intended recipient. This attribute is populated by the client initiating the dialogue. In general the JabberID, which is the value on the right side of the equal sign token for the to attribute, is of the form user@server/ resource. The form user@host is often seen too, though, and either resolves to user@host/resource by the Jabber server if the recipient is online, or is directed to offline storage if the user is not online. This attribute is required for all <message/> elements. We've shown several examples already in this chapter.
fromSpecifies the sender of the message stanza. Normally this is reflected back to a sender as a part of a response to a presence multi-cast managed by the Jabber server's session manager module. In general the Jabber ID for the from attribute must be of the form user@server/resource; however user@server is also acceptable. It may happen that the server might modify or replace the value of this attribute (for example to prevent certain kinds of spoofing), but it's normal practice for you to insert this element in your own client code because it is required for all <message/> elements.
idA unique identifier whose value is a string generated by the Jabber client or client library, and is used by the client to identify the message for tracking purposes (most commonly to correlate messages sent to a group chat room with messages received from the room, or to associate a previously sent message with any errors it might generate). The id attribute is optional.
typeQualifies a message and gives "hints" to a client about what sort of visual interface might be appropriate to display the message. Although the method of portraying the message view is entirely left up to the client, almost all GUI clients provide a different view based on message type. Most interfaces provide an ongoing thread view for chat and groupchat interchanges, and a more "email-like" view for the default message type. For contrast, though, see the unique Lluna client (see "Resources" in Appendix C; see http://www.jabber.org/user/clientlist.php for a comprehensive client list), which uses animated avatars on a Web page. Table 3.5 lists the valid values of the type attribute.
Table 3.5 Complete List of Jabber <message/> Type Attributes
Value |
Significance |
no value or not present |
When nothing is specified, this is a hint that suggests that the receiving client should consider the message as possibly having a "subject" and message "body." Often, clients present this in a separate non-modal window. |
chat |
This is a hint that suggests that the receiving client should display the message in a typical line-by-line, rolling log chat interface (although the exact interface may be client-specific). |
groupchat |
This is a hint that suggests that the receiving client should display the message in a "chatroom"-style interface, most famously seen in AOL/AIM or IRC implementations. |
headline |
Enables you to encapsulate a news article with a title and body sub element. Uses the <x xmlns='jabber:x:oob'> namespace tag. |
error |
Generally this indicates a response message from the Jabber server. If the value of the type attribute is error, the message should contain an <error/> subelement. This subelement is more fully described later in this chapter. |
The <body/> Subelement
The <body/> stanza is a child element of <message/> that carries the message's content. It must not carry any tag attributes. As seen in the earlier "Dead Parrot Sketch" example, notice there's something interesting going on in the actual message data itself contained in the <body></body> tag pairmore about that briefly. Finally, there is an inclusion of XML content from an external namespace (jabber:x:event). This namespace is a standard Jabber namespace that is used to request and respond to message events relating to the delivery, display, and composition of messages. We showed the Jabber namespace more fully in Table 3.2 (earlier), but basically it is one of four events, listed in Table 3.6, that either a client issues when sending a message or that the server issues on a client's behalf.
Table 3.6 jabber:x:event Attachments to a Message
Value |
Significance |
Offline |
Indicates that the server has stored the message offline because the intended recipient is not available. This event is to be raised by the Jabber server. |
Delivered |
Indicates that the message has been delivered to the recipient. This signifies that the message has reached the Jabber client, but does not necessarily mean that the message has been displayed. This event is to be raised by the Jabber client. |
Displayed |
After the Jabber client has received the message, it may be displayed to the user. This event indicates that the message has been displayed, and is to be raised by the Jabber client. Even if a message is displayed multiple times, this event should be raised only once. |
Composing |
In threaded chat conversations, this indicates that the recipient is composing a reply to a message that was just sent. The event is to be raised by the Jabber client. A Jabber client is allowed to raise this event multiple times in response to the same request, providing that a specific sequence is followed. |
Therefore, in your code, whenever you add a jabber:x:event extension to a <message/> element, your code as the message sender can track stages in the delivery of that element to its recipient. As shown in the "Dead Parrot Sketch" example, it merely says that the recipient can potentially expect more data later.
XML CDATA in the <body/> Subelement
In Figure 3.9, when jane@localhost replies to dana@localhost, she says: "I'll send you the Annapolis <weather-report-url>." However, when the client generates the message, this is what the message body looks like in raw form:
<message id="jcl_8" to="dana@localhost/Home" type="chat"> <thread>9164572e818c2845f73b442cbc62e1ccf6c3cb48</thread> <body>I 'll send you the Annapolis <weather-report-url></body> <x xmlns="jabber:x:event"> <composing/></x> </message>
What's going on here? Well, remember that Jabber is XML-based messaging. Therefore the CDATA carried in the message body must properly escape the apostrophe, the quote symbol, the ampersand, the less-than symbol, and the greater-than symbol. Thus your client code can't just send an unexamined message body to the peer on the other side of the conversation. Additionally, the receiving peer must decode the escaped XML as well. Just remember that everywhere in your code that you prepare text for transmission, you need to translate occurrences of special XML characters(&, <, >, and so on) appropriately, and whenever you want to get the textual data from an XML node (for example, the <body/> of a message), you do the opposite.
Fortunately, if you're creating an application atop a client library, the library creator already thought this through for us. For example, in the Python JabberPy package (see "Resources" in Appendix C), the XMLStream handler maintains two simple methods:
def XMLescape(txt): "Escape XML entities" txt = replace(txt, "&", "&") txt = replace(txt, "<", "<") txt = replace(txt, ">", ">") return txt def XMLunescape(txt): "Unescape XML entities" txt = replace(txt, "&", "&") txt = replace(txt, "<", "<") txt = replace(txt, ">", ">") return txt
If you're writing a client from scratch in, say, Python or Java, you should do likewise. Here's another snippet from JabberPy that shows transformation of XML to a string representation suitable for display (for example, in a debug window). Notice that it recurses on itself (line 16), works across namespaces (lines 4-7), and constructs a string representation with escapes properly handled (lines 10, 15, and 18).
01: def _xmlnode2str(self, parent=None): 02: """Returns an xml ( string ) representation of the node 03: and it children""" 04: s = "<" + self.name 05: if self.namespace: 06: if parent and parent.namespace != self.namespace: 07: s = s + " xmlns = '%s' " % self.namespace 08: for key in self.attrs.keys(): 09: val = str(self.attrs[key]) 10: s = s + " %s='%s'" % ( key, XMLescape(val) ) 11: s = s + ">" 12: cnt = 0 13: if self.kids != None: 14: for a in self.kids: 15: if (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt]) 16: s = s + a._xmlnode2str(parent=self) 17: cnt=cnt+1 18: if (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt]) 19: s = s + "</" + self.name + ">" 20: return s
Additional <message/> Subelements
In addition to the <body/> subelement, a message stanza may contain zero or one of each of the following stanzas as child elements (which may not contain mixed content):
NOTE
In actual fact, a <message/> is not even required to have a <body/> subelement; The XML specification requires zero or one <body/> subelement. However, it seems to us that it wouldn't be much of a message without saying somethinga bit like a sentence consisting of a single period.
-
<subject/>The message's subject. The <subject/> tag cannot contain attributes.
<message id='' to='bill@localhost' from='dana@localhost/Exodus'> <subject>Chapter 10</subject> <body> How is chapter 10 coming along? </body> </message>
Normally if the message's type attribute is set to chat, the client does not provide a capability to set a subject, although nothing in the protocol forbids this. It's simply an idiosyncracy of chatting versus messaging.
-
<thread/>A random string that is generated by the sender and that may be copied back in replies, although nothing in the protocol absolutely demands that it must be. It is used for tracking a conversation thread. The <thread/> element can not contain attributes.
<message id='jcl_7' to='dana@localhost' type='chat' from='bill@localhost/Exodus'> <thread>0ef291b610e9ec253f0970ebdb0477e1574a4e32</thread> <body>The chapter is going fine</body> <x xmlns='jabber:x:event'> <composing/> </x> </message>
In customary client usage, whenever a GUI chat window is closed by either client, a new chat message will generate a new thread value.
-
<error/>If the message contains an attribute where
type="error", the <message/> stanza must
include an <error/> child, which in turn must have a standard
code attribute corresponding to one of the standard error codes shown
earlier, and could also contain PCDATA corresponding to a natural-language
description of the error.
This is demonstrated in the following example, in which client code mistakenly sends a message to a non-existent user (dana@localhost sends a message to william@localhost instead of the correct bill@localhost):
<message to="dana@localhost" from=william@localhost type="error"> <body>hi</body> <error code="404">Not Found</error> </message>
- <x/> (external) stanzasA message may also contain any properly-namespaced child element (other than the common PCDATA elements, <stream:stream/> elements, or children of <stream:stream/> elements). This is one way of extending the <message/> element in an arbitrary way. An <x/> stanza can also be used as a protocol element to send commands from server to client or from one client to another. Each time the element is used, a relevant XML namespace must be specified. A single message may contain multiple instances of the <x/> subelement. The namespaces most often occurring in a message with an <x/> subelement are listed in Table 3.7.
NOTE
The <x/> tag is not just a child of <message/>. It is commonly used here, but current work and research is also moving to embedding it in <iq/>, <query/>, and often <presence/> tags.
Table 3.7 Common Extended Namespaces in a <message/>
Namespace |
Use |
jabber:x:autoupdate |
Possible use: Enable arbitrary client-to-application queries about any software updates or version changes available. |
jabber:x:delay |
Used to provide timestamp information about messages and presence information. Conveyed on presence responses and stored for later delivery when sent to an offline Jabber client. In the latter case, as the client comes back online, this namespace includes information that enables the Jabber client to display the time when the packet was originally sent. Message example: <message type='groupchat' from='bill@localhost'> <body>Looks like a good day for sailing!</body> <x xmlns='jabber:x:delay' stamp='20030313T15:35:43'>Cached</x> </message> Presence example: <presence from='dana@localhost/Exodus' to='bill@localhost'> <status>available</status> <priority>0</priority> <x xmlns='jabber:x:delay' from='dana@localhost/Exodus' stamp='20030314T12:22:14'/> </presence> |
jabber:x:encrypted |
Supports exchange of messages encrypted using the public key infrastructure (normally implemented using PGP by the client). A related namespace, jabber:x:signed, is used to support signed messages. Message example: <message from='dana@localhost/Exodus' to='bill@localhost/Exodus'> <body>This Message is Encrypted</body> <x xmlns='jabber:x:encrypted'> ... PGP encrypted message... </x> </message> |
jabber:x:oob |
Indicates "out of band" data. Enables clients to exchange a standard URI with a description for the purpose of file transfers. URIs exchanged using jabber:x:oob can be included with any message (inside an <x/> subelement) and act as an attachment in the sense familiar from email. Multiple attachments can be included in one message. Message example: <message from='dana@localhost/Exodus' to='bill@localhost/Exodus' type='chat'> <body>URL Attached.</body> <x xmlns="jabber:x:oob"> <url> http://java.sun.com/javaone </url> <desc> JavaOne Site </desc> </x> </message> Note: A client may choose to ignore OOB data, in which case nothing is seen at the client. |
jabber:x:roster |
This namespace allows a user to include roster items within a message, thus making it easy to send contact lists from one user to another. Each roster item is contained in an <item/> subelement within an <x/> element. Message example: <message to="bill@localhost" type='chat' from='dana@localhost'> <body>My contacts!</body> <x xmlns="jabber:x:roster"> <item jid="jane@localhost" name="jane"> <group>Exodus</group> </item> <item jid="Praline@localhost" name="Mr Praline"> <group>Friends</group> </item> </x> </message> |
Using <message/> to Convey Arbitrary Data
In its most familiar usage, Jabber facilitates the movement of short bits of text from one client to the next. What those textual bits mean in any true sense of the word is up to the interpretation of the humans or systems at the end points of the communication with mediation from the client software. From this perspective the Jabber server is "dumb" and clients may grow arbitrarily "smart."
From a practical standpoint, all that client end points have to ensure is that the XML stream they transmit conforms syntactically to the standards described thus far in the book. Message bodies can therefore contain pretty much any data the communicants want to put there. The only proviso is that the message body be contained within a <body/> tag and look like text. Therefore, you could, for example, embed Base64 data representing anything in a message body and have the client programs code and decode the data.
To illustrate this point let's look at an application that is an extreme use to suggest that the outer limits of intended usage are indeed pretty "outer." For this exercise we are going to create an application that shares pictures between roster members. The application works like this: Anytime a client endpoint opens and displays a picture (GIF, JPEG, and so on) from the file system, that picture is transported to the endpoint's roster members. For this exercise, we will assume all roster members are using the same client. The client code shown is a purpose-built client that handles only the intended functionality we have described. It is not a general Jabber textual chat client; as the code will show, it supports only examination of one's roster, encoding of the binary data in an image, and transmission to the roster concurrent with file opening and display.
We have a number of times said that a client need only support as much of the Jabber protocol elements as suit its needs, and this application which mixes in Jabber transport with a picture viewer application illustrates that point well. As a side effect, this example demonstrates that it's relatively easy to add Jabber functionality to general applications. One of our disappointments with many technical books is that code examples are often contrived and don't resemble the approach that you would actually take in coding a real application. Here we've taken a real Ruby language GUI application, stripped it down a bit so that it's easier to present, and then added a few simple lines that turn it into a Jabber client.
NOTE
This example comes from the FOX Ruby site (http://fxruby.sourceforge.net) .FXRuby is a Ruby language extension module that provides an interface to the FOX GUI library. In imageviewer.rb you can see an application that is a model for a typical full-featured GUI application, with a menu bar, toolbar, and so forth.
Consider a group of clients whose relationship may be discerned from Figure 3.10.
Figure 3.10 A group of clients.
Notice that peer1's roster consists of peer2 and peer3. Peer2's roster consists of peer1. Peer3's roster consists of peer1 also.
In this application, any graphic file that peer1 opens and displays will be transmitted to both peer2 and peer3, replacing whatever was on their local canvases. Any graphic file that peer2 opens and displays will be transmited to peer1, replacing whatever was on its local canvas. Likewise with peer3. Therefore, Figure 3.11 must have been generated by peer1 opening and displaying a picture of a rather wily looking coyote.
Figure 3.11 Binary messaging application in action.
How does this work? First, Listing 3.1 shows the Ruby code in its entirety, then deconstructs it. To run this code experimentally, you should install a current release of Ruby on your system, then add the jabber4r library. You also need to download the FOX Ruby library. Create some clients sharing rosters, as we showed in the previous figures. From the command line, launch an instance via
ruby bitmapper.rb 'account@host/resource' 'password'
Listing 3.1 Bitmapper.rb
1:#!/usr/bin/env ruby 2: 3: 4: 5:require 'jabber4r/jabber4r' 6: 7:(puts "bitmapper.rb 'account@host/resource' 'password'") & exit if ARGV.size < 2 8: 9: 10:require 'fox' 11: 12:include Fox 13: 14:class ImageWindow < FXMainWindow 15: 16: attr_reader :jabber_session 17: 18: include Responder 19: 20: def initialize(app) 21: # Invoke base class initialize first 22: super(app, "Jabber Image Sender (#{ARGV[0]}): - untitled", nil, nil, DECOR_ALL, 23: 0, 0, 850, 600, 0, 0) 24: 25: # Make some icons 26: uplevelicon = getIcon("tbuplevel.png") 27: 28: # Status bar 29: statusbar = FXStatusbar.new(self, 30: LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|STATUSBAR_WITH_DRAGCORNER) 31: 32: # Splitter 33: splitter = FXSplitter.new(self, (LAYOUT_SIDE_TOP|LAYOUT_FILL_X| 34: LAYOUT_FILL_Y| SPLITTER_TRACKING|SPLITTER_ VERTICAL|SPLITTER_REVERSED)) 35: 36: # Sunken border for image widget 37: imagebox = FXHorizontalFrame.new(splitter, 38: FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y, 39: 0, 0, 0, 0, 0, 0, 0, 0) 40: 41: # Make image widget 42: @imageview = FXImageView.new(imagebox, nil, nil, 0, 43: LAYOUT_FILL_X|LAYOUT_FILL_Y) 44: 45: # Sunken border for file list 46: @filebox = FXHorizontalFrame.new(splitter, LAYOUT_FILL_X|LAYOUT_FILL_Y, 47: 0, 0, 0, 0, 0, 0, 0, 0) 48: 49: # Make file list 50: fileframe = FXHorizontalFrame.new(@filebox, 51: FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y, 52: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 53: @filelist = FXFileList.new(fileframe, nil, 0, 54: LAYOUT_FILL_X|LAYOUT_FILL_Y| ICONLIST_MINI_ICONS|ICONLIST_AUTOSIZE) 55: @filelist.connect(SEL_DOUBLECLICKED, method(:onCmdFileList)) 56: FXButton.new(@filebox, "\tUp one level\tGo up to higher directory.", 57: uplevelicon, @filelist, FXFileList::ID_DIRECTORY_UP, 58: BUTTON_TOOLBAR|FRAME_RAISED|LAYOUT_FILL_Y) 59: 60: # Initialize file name and pattern for file dialog 61: @filename = "untitled" 62: @preferredFileFilter = 0 63: end 64: 65: # Convenience function to construct a PNG icon 66: def getIcon(filename) 67: FXPNGIcon.new(getApp(), File.open(filename, "rb").read) 68: end 69: 70: def hasExtension(filename, ext) 71: File.basename(filename, ext) != File.basename(filename) 72: end 73: 74: def loadImage(file) 75: file = file.gsub( /\\/ , "/" ) # if you're on WIN32.. 76: sendJabberImage(file) 77: updateImage(file) 78: end 79: 80: # Load the named image file 81: def updateImage(file) 82: img = nil 83: if hasExtension(file, ".gif") 84: img = FXGIFImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 85: elsif hasExtension(file, ".bmp") 86: img = FXBMPImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 87: elsif hasExtension(file, ".xpm") 88: img = FXXPMImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 89: elsif hasExtension(file, ".png") 90: img = FXPNGImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 91: elsif hasExtension(file, ".jpg") 92: img = FXJPGImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 93: elsif hasExtension(file, ".pcx") 94: img = FXPCXImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 95: elsif hasExtension(file, ".tif") 96: img = FXTIFImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 97: elsif hasExtension(file, ".tga") 98: img = FXTGAImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 99: elsif hasExtension(file, ".ico") 100: img = FXICOImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP) 101: end 102: 103: # Perhaps failed? 104: if !img 105: FXMessageBox.error(self, MBOX_OK, "Error loading image", 106: "Unsupported image type: #{file}") 107: return 108: end 109: 110: # Load it... 111: getApp().beginWaitCursor do 112: FXFileStream.open(file, FXStreamLoad) { |stream| img.loadPixels(stream) } 113: img.create 114: @imageview.image = img 115: end 116: end 117: 118: # Quit the application 119: def onCmdQuit(sender, sel, ptr) 120: # Quit 121: getApp().exit(0) 122: end 123: 124: # Command message from the file list 125: def onCmdFileList(sender, sel, index) 126: if index >= 0 127: if @filelist.isItemDirectory(index) 128: @filelist.directory = @filelist.getItemPathname(index) 129: elsif @filelist.isItemFile(index) 130: @filename = @filelist.getItemPathname(index) 131: loadImage(@filename) 132: end 133: end 134: return 1 135: end 136: # Create and show window 137: def create 138: puts "creating app" 139: dir = "." 140: @filebox.height = 100 141: super # i.e. FXMainWindow::create() 142: show(PLACEMENT_SCREEN) 143: end 144: 145: 146: 147: def connectToJabber(account, account_password) 148: begin 149: @jabber_session = Jabber::Session.bind_digest ("#{account}", "#{account_password}") 150: @jabber_session.add_message_listener do |message| 151: File.open("TMP"+message.subject, "wb") do |file| 152: file.write message.body.unpack("m")[0] 153: file.flush 154: end 155: updateImage("TMP"+message.subject) 156: File.delete("TMP"+message.subject) 157: 158: end 159: rescue 160: puts "Account does not exist: #{account} - #{account_password}" 161: puts $! 162: puts $!.backtrace 163: exit 164: end 165: end 166: 167: def sendJabberImage(imagefile) 168: puts imagefile 169: @jabber_session.roster.each_item do |item| 170: puts item 171: item.each_resource do |resource| 172: if resource.name == "bitmapper" 173: data = nil 174: File.open(imagefile, "rb") {|file| data = file.read} 175: data = [data].pack("m") 176: @jabber_session.new_chat_message(item.jid.to_s+"/bitmapper") .set_subject(File.basename(imagefile)).set_body(data).send 177: end 178: end 179: end 180: end 181: 182: def disconnectFromJabber 183: @jabber_session.close if @jabber_session 184: end 185:end 186: 187:if $0==__FILE__ 188: # Make application 189: application = FXApp.new("ImageViewer", "FoxTest") 190: # Make window 191: window = ImageWindow.new(application) 192: 193: # Handle interrupts to terminate program gracefully 194: application.addSignal("SIGINT", window.method(:onCmdQuit)) 195: 196: # Create it 197: application.create 198: 199: # Connect to Jabber 200: window.connectToJabber(*ARGV) 201: 202: # Run 203: application.run 204: 205: # Shutdown Jabber connection 206: window.disconnectFromJabber 207: 208:end
Let's begin looking at this example from back to front. First, a block (lines 187-208) outside the ImageWindow class definition says that if we are running this application from the command line (if $0==__FILE__ .. end), create an instance of ImageWindow into the local variable window (lines 189-191). We map an interrupt handler to a method (onCmdQuit) within the instance (line 194). Finally, we give the window its own thread of execution (line 203) and clean up the Jabber connection as the application exits.
187:if $0==__FILE__ 188: # Make application 189: application = FXApp.new("ImageViewer", "FoxTest") 190: # Make window 191: window = ImageWindow.new(application) 192: 193: # Handle interrupts to terminate program gracefully 194: application.addSignal("SIGINT", window.method(:onCmdQuit)) 195: 196: # Create it 197: application.create 198: 199: # Connect to Jabber 200: window.connectToJabber(*ARGV) 201: 202: # Run 203: application.run 204: 205: # Shutdown Jabber connection 206: window.disconnectFromJabber 207: 208:end
During the stream of execution, we import the jabber4r library and check the number of command-line arguments. Execution then passes to the block above.
1:#!/usr/bin/env ruby 2: 3: 4: 5:require 'jabber4r/jabber4r' 6: 7:(puts "bitmapper.rb 'account@host/resource' 'password'") & exit if ARGV.size < 2 8: 9:
All of the code associated with the FXRuby extension is provided by the FOX module, so we need to start by requiring this feature:
10:require 'fox' 11:
Because all the FOX Ruby classes are defined under the FOX module, you normally refer to them by their "fully qualified" names (that is, names that begin with the Fox:: prefix). To avoid extra finger typing, you add an include Fox statement so that all the names in the FOX module are "included" into the global namespace:
12:include Fox 13:
The ImageWindow class inherits from a FOX MainWindow (line 14); standard practice with this toolkit is to subclass your own main window from FOX's MainWindow class and construct its contents in the class's initialize method (lines 20-63; see http:// fxruby.sourceforge.net for toolkit details).
14:class ImageWindow < FXMainWindow 15: 16: attr_reader :jabber_session 17: 18: include Responder 19: 20 .. 63
When we load the image for display, we also invoke sendJabberImage(file):
74: def loadImage(file) 75: file = file.gsub( /\\/ , "/" ) # if you're on WIN32.. 76: sendJabberImage(file) 77: updateImage(file) 78: end
Let's look at that in more detail. We get the current session's roster and iterate over each roster entry. This is not cached in the jabber_session object, but rather fetched from the server each time to assure currency. (Technically there is some potential from a client to disconnect during this loop, but in this case, the server will simply drop the message.) The roster contains a dictionary of roster items, and each iteration yields one of these, which includes a Jabber ID (Jabber::JID) and a Jabber::Roster::RosterItem::Resource. We check the resources to assure that the client is capable of receiving and displaying images (that is, it must have a resource called bitmapper, line 172).
167: def sendJabberImage(imagefile) 169: @jabber_session.roster.each_item do |item| 170: puts item 171: item.each_resource do |resource| 172: if resource.name == "bitmapper" 173: data = nil 174: File.open(imagefile, "rb") {|file| data = file.read} 175: data = [data].pack("m") 176: @jabber_session.new_chat_message(item.jid.to_s+"/bitmapper") .set_subject(File.basename(imagefile)).set_body(data).send 177: end 178: end 179: end 180: end
We create a Base64-encoded version of the file (line 175), and send it via the jabber_session object. We set the body of the message to the Base64 data (line 176) and send it. The construction of the message, the setting of a subject, and the message send are all chained in this terse call.
Note that even though we send a chat-typed message, we set the subject to a filename. Setting subject in a chat message is unusual, but as we have pointed out previously, it is not forbidden by the protocol. You will see next how this becomes useful at the receiving instance.
On receipt of the message, an instance fires its connectToJabber method. It pulls a suggested filename from the message subject, stores the decoded Base64 data into the file (line 152), then forces the FOX application to read the file and construct and display the image (line 155).
147: def connectToJabber(account, account_password) 148: begin 149: @jabber_session = Jabber::Session.bind_digest("#{account}", "#{account_password}") 150: @jabber_session.add_message_listener do |message| 151: File.open("TMP"+message.subject, "wb") do |file| 152: file.write message.body.unpack("m")[0] 153: file.flush 154: end 155: updateImage("TMP"+message.subject) 156: File.delete("TMP"+message.subject) 157: 158: end
We catch exceptions in lines 159-165:
159: rescue 160: puts "Account does not exist: #{account} - #{account_password}" 161: puts $! 162: puts $!.backtrace 163: exit 164: end 165: end 166:
As a part of application cleanup (on exit), we disconnect the session, if one exists (line 183).
181: 182: def disconnectFromJabber 183: @jabber_session.close if @jabber_session 184: end 185:end 186:
This example uses the very complete jabber4r library, and as you can see, it offers a number of excellent high-level capabilities, including its own thread handling.
We show this example in Ruby because of the language's elegance and succinctness, but it serves to instruct for any other language implementation as well.
If you prefer a Python example, here's the similar application that uses PythonCard, a very nice GUI builder (http://pythoncard.sourceforge.net), one of several GUIs for Python.
It looks slightly differentthere are no command-line parameters and there is a separate login screen. We show all source for the three source files (pictureViewer.py, jabberLogin.py, and jabberHandler.py) composing the application for reference in Listings 3.2, 3.3, and 3.4 respectively; however, our comments apply mainly to the JabberHandler.
NOTE
To exercise this example, you need to download both the PythonCard application framework (from http://pythoncard.sourceforge.net) and the wxPython library (from http://www.wxpython.org). Just use the setup.py with each download to install them in the Python library folder.
Figure 3.12 PythonCard ImageViewer implementation.
Listing 3.2 pictureViewer.py
1:import sys, re, os, string 2:from PythonCardPrototype import clipboard, dialog, graphic, log, model, EXIF 3:from wxPython import wx 4:import os, sys 5:import jabberLogin 6:from jabberHandler import JabberHandler 7: 8:class PictureViewer(model.Background): 9: 10: def on_openBackground(self, event): 11: self.ignoreSizeEvent = 1 12: 13: self.x = 0 14: self.y = 0 15: self.filename = None 16: self.bmp = None 17: 18: bgSize = self.getSize() 19: bufSize = self.GetClientSize() 20: widthDiff = bgSize[0] - bufSize[0] 21: heightDiff = bgSize[1] - bufSize[1] 22: displayRect = wx.wxGetClientDisplayRect() 23: self.maximizePosition = (displayRect[0], displayRect[1]) 24: self.maximumSize = (displayRect[2] - widthDiff, displayRect[3] - heightDiff) 25: 26: if len(sys.argv) > 1: 27: # accept a file argument on the command-line 28: filename = os.path.abspath(sys.argv[1]) 29: log.info('pictureViewer filename: ' + filename) 30: if not os.path.exists(filename): 31: filename = os.path.abspath(os.path.join(self.stack. app.startingDirectory,sys.argv[1])) 32: if os.path.isfile(filename): 33: self.openFile(filename) 34: 35: if self.filename is None: 36: self.fitWindow() 37: self.jabberHandler = None 38: 39: self.Show(1) 40: 41: 42: def on_idle(self, event): 43: self.ignoreSizeEvent = 0 44: if self.jabberHandler is not None and self. jabberHandler.isConnected(): 45: self.jabberHandler.Process() 46: def on_size(self, event): 47: if self.bmp is not None and not self.ignoreSizeEvent: 48: oldSize = self.bmp.getSize() 49: newSize = self.GetClientSize() 50: widthScale = newSize[0] / (0.0 + oldSize[0]) 51: heightScale = newSize[1] /(0.0 + oldSize[1]) 52: self.displayFileScaled(widthScale, heightScale, 1) 53: def sizeScaled(self, size, widthScale, heightScale): 54: return ((int(size[0] * widthScale), int(size[1] * heightScale))) 55: def displayFileScaled(self, widthScale, heightScale, inUserResize=0): 56: if self.filename is not None: 57: bufOff = self.components.bufOff 58: bufOff.autoRefresh = 0 59: # figure out new size for window 60: size = self.bmp.getSize() 61: newSize = self.sizeScaled(size, widthScale, heightScale) 62: bufOff.size = newSize 63: 64: if inUserResize: 65: self.panel.SetSize(newSize) 66: else: 67: self.fitWindow() 68: bufOff.clear() 69: 70: bufOff.autoRefresh = 1 71: bufOff.drawBitmapScaled(self.bmp, 0, 0, newSize) 72: 73: # attempt to display the file full size if possible 74: # otherwise the bitmap needs to be scaled 75: def displayFile(self): 76: if self.filename is not None: 77: bufOff = self.components.bufOff 78: bufOff.autoRefresh = 0 79: # figure out new size for window 80: bufOff.size = self.bmp.getSize() 81: self.fitWindow() 82: bufOff.clear() 83: 84: bufOff.autoRefresh = 1 85: bufOff.drawBitmap(self.bmp, 0, 0) 86: def fitToScreen(self): 87: oldSize = self.bmp.getSize() 88: newSize = self.maximumSize 89: widthScale = newSize[0] / (0.0 + oldSize[0]) 90: heightScale = newSize[1] /(0.0 + oldSize[1]) 91: scale = min(widthScale, heightScale) 92: self.displayFileScaled(scale, scale) 93: self.Center() 94: def fitWindow(self): 95: self.ignoreSizeEvent = 1 96: size = self.components.bufOff.size 97: self.panel.SetSize(size) 98: #if self.ignoreSizeEvent == 1: 99: self.SetClientSize(size) 100: def openFile(self, path): 101: self.filename = path 102: f = open(path, 'rb') 103: tags=EXIF.process_file(f) 104: f.close() 105: try: 106: # the repr() is something like 107: # (0x0112) Short=8 @ 54 108: # but the str() is just 1, 8, etc. 109: orientation = int(str(tags['Image Orientation'])) 110: except: 111: orientation = 1 112: self.bmp = graphic.Bitmap(self.filename) 113: if orientation == 8: 114: # need to rotate the image 115: # defaults to clockwise, 0 means counter-clockwise 116: #print "rotating" 117: self.bmp.rotate90(0) 118: elif orientation == 6: 119: self.bmp.rotate90(1) 120: size = self.bmp.getSize() 121: title = os.path.split(self.filename) [-1] + " %d x %d" % size 122: self.SetTitle(title) 123: # if either dimension of the image is beyond our maximum 124: # then display the image fit to the screen 125: if size[0] > self.maximumSize[0] or size[1] > self.maximumSize[1]: 126: self.fitToScreen() 127: else: 128: self.displayFile() 129: def on_menuFileOpen_select(self, event): 130: result = dialog.openFileDialog() 131: if result['accepted']: 132: self.openFile(result['paths'][0]) 133: if self.jabberHandler is not None and self.jabberHandler.isConnected(): 134: # distribute the picture to all clients on my roster 135: self.jabberHandler.sendToRoster(result['paths'][0]) 136: def on_menuFileConnectJabber_select(self, event): 137: result = jabberLogin.jabberLogin(self) 138: self.jabberHandler = JabberHandler(self, jid=result['JabberIdText'], password=result['PasswordText'], server=result['JabberServerText']) 139: self.jabberHandler.ConnectToJabber() 140: def on_menuFileSaveAs_select(self, event): 141: if self.filename is None: 142: path = '' 143: filename = '' 144: else: 145: path, filename = os.path.split(self.filename) 146: wildcard = "All files (*.*)|*.*" 147: result = dialog.saveFileDialog(None, "Save As", path, filename, wildcard) 148: if result['accepted']: 149: path = result['paths'][0] 150: fileType = graphic.bitmapType(path) 151: try: 152: bmp = self.components.bufOff.getBitmap() 153: bmp.SaveFile(path, fileType) 154: return 1 155: except: 156: return 0 157: else: 158: return 0 159: def on_menuFileExit_select(self, event): 160: self.Close() 161: 162:if __name__ == '__main__': 163: # require JabberID, Password, Server 164: app = model.PythonCardApp(PictureViewer ) 165: app.MainLoop()
The pictureViewer.py file is a simple frame for displaying image files. From the command line, invoke it via > python pictureViewer.py, as shown in Listing 3.3.
Listing 3.3 jabberLogin.py
1:from PythonCardPrototype import model, res 2:import os 3: 4:class JabberLogin(model.CustomDialog): 5: def __init__(self, parent): 6: model.CustomDialog.__init__(self, parent) 7: 8: 9: def jabberLogin(parent): 10: dlg = JabberLogin(parent) 11: dlg.showModal() 12: result = {'accepted':dlg.accepted()} 13: result['JabberIdText'] = dlg.components.JabberIdText.text 14: result['PasswordText'] = dlg.components.PasswordText.text 15: result['JabberServerText'] = dlg.components.JabberServerText.text 16: 17: dlg.destroy() 18: return result
JabberLogin.py shown in Listing 3.3 is a simple dialog box for collecting the appropriate connection information from the user. It's shown in Figure 3.12. PictureViewer, the parent frame, calls JabberLogin whenever the user selects Connect to Jabber from the File menu:
(in pictureViewer.py) 136: def on_menuFileConnectJabber_select(self, event): 137: result = jabberLogin.jabberLogin(self) 138: self.jabberHandler = JabberHandler(self, jid=result['JabberIdText'], password=result['PasswordText'], server=result['JabberServerText']) 139: self.jabberHandler.ConnectToJabber()
When the GUI user logs into a Jabber connection, the JabberHandler is called. It's the class that implements and encapsulates all the Jabber details for PictureViewer (pictureViewer.py, lines 138139, and lines 4245.) Listing 3.4 shows it in its entirety and then we deconstruct its significant implementation in detail.
Listing 3.4 jabberHandler.py
1:import jabber 2:import sys 3:import sha 4:import sys, re, os, string, base64 5: 6:class JabberHandler: 7: def __init__(self, parent, jid=None, password=None, server='localhost', resource="pictureviewer"): 8: self.parent = parent 9: self.jid = jid 10: self.password = password 11: self.server = server 12: self.roster = None 13: self.resource = resource 14: self.ConnectToJabber() 15: self.counter = 0 16: 17: 18: def ConnectToJabber(self): 19: self.con = jabber.Client(host=self.server, debug=0, port=5222,log=sys.stderr) 20: try: 21: self.con.connect() 22: self.connected = True 23: except IOError, e: 24: print "Couldn't connect: %s" % e 25: sys.exit(0) 26: else: 27: print "\nConnected\n" 28: 29: self.con.setMessageHandler(self.messageCB) 30: self.con.setPresenceHandler(self.presenceCB) 31: self.con.setIqHandler(self.iqCB) 32: 33: if self.con.auth(self.jid,self.password,self.resource): 34: print "Logged in as %s to server %s" % ( self.jid,self.server) 35: else: 36: print "ERR -> ", self.con.lastErr, self.con.lastErrCode 37: sys.exit(1) 38: self.con.sendInitPresence() 39: self.roster = self.con.requestRoster() 40: summary = self.roster.getSummary() 41: print "\nRoster:\n" 42: for name in summary.keys(): 43: print "\tname>", name, 44: print self.roster.getOnline(name) 45: print "\n" 46: 47: jids = self.roster.getJIDs() 48: for jid in jids: 49: print "JID>", jid 50: print "\n" 51: print "raw =", self.roster.getRaw() 52: 53: def isConnected(self): 54: return self.connected 55: def sendToRoster(self, path): 56: print "sending to roster", path 57: ## get from file and encode... 58: f = open(path, 'rb') 59: theData = f.read() 60: base64Data = base64.encodestring(theData) 61: for jid in self.roster.getJIDs(): 62: print "send pict > JID ", jid 63: msg = jabber.Message(jid,base64Data) 64: msg.setSubject(os.path.basename(path)) 65: self.con.send(msg) 66: 67: 68: 69: def iqCB(self, iq): 70: """Called when an iq is recieved, we just let the library handle it at the moment""" 71: print "iqCB", str(iq) 72: 73: 74: def presenceCB(self, con, prs): 75: """Called when a presence is received""" 76: print "\npresenceCB\n", str(prs) 77: who = str(prs.getFrom()) 78: type = prs.getType() 79: if type == None: type = 'available' 80: 81: # subscription request: 82: # - accept their subscription 83: # - send request for subscription to their presence 84: if type == 'subscribe': 85: print "subscribe request from %s" % who 86: con.send(jabber.Presence(to=who, type='subscribed')) 87: con.send(jabber.Presence(to=who, type='subscribe')) 88: 89: # unsubscription request: 90: # - accept their unsubscription 91: # - send request for unsubscription to their presence 92: elif type == 'unsubscribe': 93: print "unsubscribe request from %s" % who 94: self.con.send(jabber.Presence(to=who, type='unsubscribed')) 95: self.con.send(jabber.Presence(to=who, type='unsubscribe')) 96: 97: elif type == 'subscribed': 98: print "we are now subscribed to %s" % who 99: 100: elif type == 'unsubscribed': 101: print "we are now unsubscribed to %s" % who 102: 103: elif type == 'available': 104: print ("%s is available (%s / %s)" % \ 105: (who, prs.getShow(), prs.getStatus())) 106: elif type == 'unavailable': 107: print ("%s is unavailable (%s / %s)" % \ 108: (who, prs.getShow(), prs.getStatus())) 109: 110: 111: def messageCB(self, con, msg): 112: #def messageCB(self, msg): 113: print "\nmessageCB\n" 114: ### 115: 116: theData = base64.decodestring(msg.getBody()) 117: suggestedFileName = "TMP"+msg.getSubject() 118: print suggestedFileName 119: f = file(suggestedFileName, "wb") 120: f.write(theData) 121: f.close() 122: self.parent.openFile(suggestedFileName) 123: ## delete the tmp file?? 124: 125: def Process(self): 126: self.con.process(0)
Here we'll deconstruct only the JabberHandler class. The strategy the main application employs is that whenever it has idle cycles, it checks to see whether it has a handle to the JabberHandler and the user has connected to Jabber (if self.jabberHandler is not None and self.jabberHandler.isConnected()). If so, then it calls the JabberHandler.Process() method:
(in PictureViewr.py) 42: def on_idle(self, event): 43: self.ignoreSizeEvent = 0 44: if self.jabberHandler is not None and self.jabberHandler.isConnected(): 45: self.jabberHandler.Process()
This in turn invokes the process() method of the underlying jabber and xmlstream classes (in jabber.py). This way the application can check regularly for incoming messages without blocking UI. A much better way to do this is to spawn a separate thread of execution for the JabberHandler, and use a Python Message Queue for the halves of the application to communicate. For simplicity, we did not do this here, but you should be aware that design strategy for intra-process communication can often have a significant impact on perceived performance.
NOTE
There is a good discussion of this particular topic at http://pythoncard.sourceforge.net/timers-threads.html.
When the GUI user logs into a Jabber connection, this is the class that implements the Jabber handling.
The application imports needed features from the Jabber library and a few others:
1:import jabber 2:import sys 3:import sha 4:import os, base64 5:
The constructor needs the parent application's handle (the GUI), a user ID, a server name, and a password. These were all garnered from the GUI and passed to the constructor.
6:class JabberHandler: 7: def __init__(self, parent, jid=None, password=None, server='localhost', resource="pictureviewer"): 8: self.parent = parent 9: self.jid = jid 10: self.password = password 11: self.server = server 12: self.roster = None 13: self.resource = resource 14: self.ConnectToJabber() 15:
The attempt to connect with the server is implemented here:
16: def ConnectToJabber(self): 17: self.con = jabber.Client(host=self.server, debug=False, port=5222,log=sys.stderr) 18: try: 19: self.con.connect() 20: self.connected = True 21: except IOError, e: 22: print "Couldn't connect: %s" % e 23: sys.exit(0) 24:
We set handlers for the various callbacks, and use our own overrides for the default methods:
25: self.con.setMessageHandler(self.messageCB) 26: self.con.setPresenceHandler(self.presenceCB) 27: self.con.setIqHandler(self.iqCB) 28:
If we fail, the application exists. A more mature application might do something such as raise an error dialog and give the user another chance to log in.
29: if not self.con.auth(self.jid,self. password,self.resource): 30: print "ERR -> ", self.con.lastErr, self.con.lastErrCode 31: sys.exit(1) 32: self.con.sendInitPresence() 33: self.roster = self.con.requestRoster() 34: summary = self.roster.getSummary()
These lines show off the Jabber library capabilities and are not needed to run the application. When lines 3641 are executed:
35:### didactic code, not needed for application 36: print "\nRoster:\n" 37: for name in summary.keys(): 38: print "\tname>", name, 39: print self.roster.getOnline(name) 40: print "\n" 41:
the returned result looks like:
Roster: name> Pix2@localhost offline name> Pix3@localhost offline
And this:
42: jids = self.roster.getJIDs() 43: for jid in jids: 44: print "JID>", jid 45: print "\n" 46: print "raw =", self.roster.getRaw()
produces:
JID> Pix2@localhost JID> Pix3@localhost
as well as the raw roster, which is a Python dictionary of dictionaries. Each JabberId (Pix2@localhost, Pix3@localhost) keys a dictionary of the relevant roster items (status, availability, and so on).
raw = { u'Pix2@localhost': {'status': None, 'name': u'Pix2', 'groups': [], 'online': 'offline', 'ask': None, 'show': None, 'sub': u'both' }, u'Pix3@localhost': {'status': None, 'name': u'Pix3', 'groups': [u'Unfiled'], 'online': 'offline', 'ask': None, 'show': None, 'sub': u'both' } }
The sendToRoster method gets a cached roster and then sends the image to roster members:
50: def sendToRoster(self, path): 51: print "sending to roster", path 52: ## get from file and encode... 53: f = open(path, 'rb') 54: theData = f.read() 55: base64Data = base64.encodestring(theData) 56: for jid in self.roster.getJIDs(): 57: 58: msg = jabber.Message(jid,base64Data) 59: msg.setSubject(os.path.basename(path)) 60: self.con.send(msg)
Why did we encode the binary bits (line 55 and also on line 175 of Listing 3.1 the earlier Ruby example), and then pass that to the message constructor?
Well, remember that the data in the body of a message must conform to the rule for ordinary PCDATA. The only reasonable choice is to turn any alien format (alien from the standpoint of XML) into Base64 ASCII data. On receipt, the destination client will simply decode the Base64 data and use it to create a binary object.
We implement a minimal presence handling here, just to keep the roster peers in sync.
69: def presenceCB(self, con, prs): 70: """Called when a presence is recieved""" 71: print "\npresenceCB\n", str(prs) 72: who = str(prs.getFrom()) 73: type = prs.getType() 74: if type == None: type = 'available' 75: 76: # subscription request: 77: # - accept their subscription 78: # - send request for subscription to their presence 79: if type == 'subscribe': 80: con.send(jabber.Presence(to=who, type='subscribed')) 81: con.send(jabber.Presence(to=who, type='subscribe')) 82: 83: # unsubscription request: 84: # - accept their unsubscription 85: # - send request for unsubscription to their presence 86: elif type == 'unsubscribe': 87: self.con.send(jabber.Presence(to=who, type='unsubscribed')) 88: self.con.send(jabber.Presence(to=who, type='unsubscribe')) 89:
We implement a message callback (messageCB) to decode the message body contents (line 92), grab the message subject, use it for a temporary filename (line 93), write the decoded contents of the message (lines 9496), and notify a method in the parent that there is a new image for display. The parent has a method (self.parent.openFile) that does just this.
90: def messageCB(self, con, msg): 91: 92: theData = base64.decodestring(msg.getBody()) 93: suggestedFileName = "TMP"+msg.getSubject() 94: f = file(suggestedFileName, "wb") 95: f.write(theData) 96: f.close() 97: self.parent.openFile(suggestedFileName)
Finally, whenever the parent process invokes its JabberHandler's Process method (pictureViewer.py, lines 4245), the underlying Jabber process() method fires.
98: 99: def Process(self): 100: self.con.process(0)
You should use the Pythocard resource editor to produce pictureViewer.rsrc.py and jabberLogin.rsrc.py files corresponding to both the pictureViewer.py and jabberLogin.py files. It would stray too far off topic to discuss this further. If you need additional help in this step (somewhat unlikely,) please email the authors.
Remember before running either the Ruby or Python example to modify your jabber.xml configuration file to allow larger file transfers (see Chapter 2). A "real" application such as this should have to break large binary data into many small messages to better conform to the Jabber model. Sending too large a single message would both overtax the server and possibly even break a client. Remember that the Jabber message model prefers short "IM-style" messages. Additionally, the Jabber server prefers many clients exchanging short messages as well.
NOTE
Also note that the server doesn't respond well to message flooding. Thus, even if you broke the message up into several pieces and tried to send them simultaneously, the server still might complain to your application for violating the karma settings.