Friday, June 13, 2008

Setting an Avatar with Ruby's XMPP4R API

I've been using XMPP4R recently to create Jabber bots. These bots perform a variety of services from posting to micro blogging services, to providing status updates on the condition of servers and their processes. I was recently asked to make it so that one of these bots would have a nice little buddy icon(avatar) and also provide status messages.

Jabber does this using what they call "vcards." There isn't a ton of info online about using the vcard support in XMPP4R short of the RDOC documentation. It took a little trial and error( and a little RFC reading) but I was able to get it to work. Here is what I did.

To give a little context, all of my bots start with kind of a basic skeleton:


require 'rubygems'
require 'xmpp4r'

class JabberBot
# user : Jabber ID
# pass : Password
def initialize(user, pass)
@end = false
Jabber::debug = false

# connect Jabber client to the Server
@client = Jabber::Client.new(Jabber::JID.new(user))
@client.connect
@client.auth(pass)
# send initial presence to let the server know you are ready for messages
@client.send(Jabber::Presence::new)

# just a thread to keep the jabber server in touch
@keepalive = Thread.new do
while not @end
# saying "I'm alive!" to the Jabber server
pres = Jabber::Presence::new
@client.send(pres)
sleep 30
end # keepalive
end # initialize
end


This skeleton pretty much only connects to a Jabber server. It can be run(most simply) like this:


bot = JabberBot.new('username@jabber.server', 'password')

while true do
end


To take advantage of the vcard support in XMPP4R you need to include the vcard portion of the XMPP4R library.


require 'xmpp4r/vcard'


This line allows you to use the functionality of XMPP4R that can manipulate vcards. Vcards are how Jabber handles user information(in this case our bot is the user) and also avatar/buddy icon information. Here is the code I use to set my vcard information.


# set vcard info
# this is in a thread because it waits on a server response
# the XMPP4R docs suggest placing this code inside a thread
@avatar_sha1 = nil # this gets used later on
Thread.new do
vcard = Jabber::Vcard::IqVcard.new
vcard["FN"] = "My Bot" # full name
vcard["NICKNAME"] = "mybot" # nickname
# buddy icon stuff
vcard["PHOTO/TYPE"] = "image/png"
# open buddy icon/avatar image file
image_file = File.new("buddy.png", "r")
# Base64 encode the file contents
image_b64 = Base64.b64encode(image_file.read())
# process sha1 hash of photo contents
# this is used by the presence setting
image_file.rewind # must rewind the file to the beginning
@avatar_sha1 = Digest::SHA1.hexdigest(image_file.read())
vcard["PHOTO/BINVAL"] = image_b64 # set the BINVAL to the Base64 encoded contents of our image
begin
# create a vcard helper and immediately set the vcard info
vcard_helper = Jabber::Vcard::Helper.new(@client).set(vcard)
rescue
puts "#{Time.now} vcard operation timed out."
end
end


What that code does is it first creates a vcard object. This object holds all the info for the jabber user(in this case a bot. See XEP-0153 XMPP extensions docs for more details.). It then sets the full name and the nickname.

Next is where the buddy icon stuff happens. First the code creates a file object pointing to the file that contains the buddy icon, buddy.png, and Base64 encodes it contents. The Base64 representation of the file is what the PHOTO/BINVAL part of the vcard is expecting. It also creates a SHA1 hash of the file contents more on this later.

Once it sets the file info in the vcard object, it creates a vcard helper and then immediately sets the vcard information using the helper's "set" method and passing the vcard object to that. At this point the vcard and the buddy icon/avatar are set.

Why the SHA1 hash of the file content's then? Well, according to the XEP-0153 XMPP extensions docs once you set a buddy icon/avatar you are supposed to include an SHA1 hash of the icon's non-Base64 encoded contents in subsequent presence messges sent to the server. I believe this is so that when other people cache your icon, they can be sure they have the most up to date one.

The spec wants this information in the following format:

<presence from='username@jabber.server/ResourceName'>
<x xmlns='vcard-temp:x:update'>
<photo>sha1-hash-of-image</photo>
</x>
</presence>


In order to accomplish this in my code, I add the following to my "keepalive" thread in my code skeleton at the top:

# append buddy icon/avatar info
if not @avatar_sha1.nil?
# send the sha1 hash of the avatar to the server
# as per the RFC http://www.xmpp.org/extensions/xep-0153.html
x = REXML::Element::new("x")
x.add_namespace('vcard-temp:x:update')
photo = REXML::Element::new("photo")
avatar_hash = REXML::Text.new(@avatar_sha1)
# add text to photo
photo.add(avatar_hash)
# add photo to x
x.add(photo)
# add x to presence
pres.add_element(x)
end


What this code does is construct a snippet of XML as described above, and attaches it to my presence messages. Now when I run my bot I get a neat little buddy icon.

I would like to write more of these blog entries about coding some of these different kinds of service bots. If anyone out there on the internet reads this, please leave a comment and let me know what you think. I am new to Ruby programming and would love to learn how to do this better.