Executive summary
It’s perfectly possible! Jump to the HTML demo!
Longer version
This started with a very simple need: wanting to improve the notifications I’m receiving from various sources. Those include:
- changes or failures reported during Puppet runs on my own infrastructure, and on at a customer’s;
- build failures for the Debian Installer;
- changes in banking amounts;
- and lately: build status for jobs in a customer’s Jenkins instance.
I’ve been using plaintext notifications for a number of years but I decided to try and pimp them a little by adding some colors.
While the XMPP-sending details are usually hidden in a local module,
here’s a small self-contained example: connecting to a server, sending
credentials, and then sending a message to someone else. Of course,
one might want to tweak the Configuration
section before trying to run
this script…
#!/usr/bin/perl
use strict;
use warnings;
use Net::XMPP;
# Configuration:
my $hostname = 'example.org';
my $username = 'bot';
my $password = 'call-me-alan';
my $resource = 'demo';
my $recipient = 'human@example.org';
# Open connection:
my $con = Net::XMPP::Client->new();
my $status = $con->Connect(
hostname => $hostname,
connectiontype => 'tcpip',
tls => 1,
ssl_ca_path => '/etc/ssl/certs',
);
die 'XMPP connection failed'
if ! defined($status);
# Log in:
my @result = $con->AuthSend(
hostname => $hostname,
username => $username,
password => $password,
resource => $resource,
);
die 'XMPP authentication failed'
if $result[0] ne 'ok';
# Send plaintext message:
my $msg = 'Hello, World!';
my $res = $con->MessageSend(
to => $recipient,
body => $msg,
type => 'chat',
);
die('ERROR: XMPP message failed')
if $res != 0;
For reference, here’s what the XML message looks like in Gajim’s XML console (on the receiving end):
<message type='chat' to='human@example.org' from='bot@example.org/demo'>
<body>Hello, World!</body>
</message>
Issues start when one tries to send some HTML message, e.g. with the last part changed to:
# Send plaintext message:
my $msg = 'This is a <b>failing</b> test';
my $res = $con->MessageSend(
to => $recipient,
body => $msg,
type => 'chat',
);
as that leads to the following message:
<message type='chat' to='human@example.org' from='bot@example.org/demo'>
<body>This is a <b>failing</b> test</body>
</message>
So tags are getting encoded and one gets to see the uninterpreted “HTML code”.
Trying various things to embed that inside <body>
and <html>
tags,
with or without namespaces, led nowhere.
Looking at a message sent from Gajim to Gajim (so that I could craft an HTML message myself and inspect it), I’ve noticed it goes this way (edited to concentrate on important parts):
<message xmlns="jabber:client" to="human@example.org/Gajim" type="chat">
<body>Hello, World!</body>
<html xmlns="http://jabber.org/protocol/xhtml-im">
<body xmlns="http://www.w3.org/1999/xhtml">
<p>Hello, <strong>World</strong>!</p>
</body>
</html>
</message>
Two takeaways here:
The message is send both in plaintext and in HTML. It seems Gajim archives the plaintext version, as opening the history/logs only shows the textual version.
The fact that the HTML message is under a different path (
/message/html
as opposed to/message/body
) means that one cannot use theMessageSend
method to send HTML messages…
This was verified by checking the documentation and code of the
Net::XMPP::Message
module. It comes with various getters and
setters for attributes. Those are then automatically collected when
the message is serialized into XML (through the GetXML()
method). Trying to add handling for a new HTML attribute would mean being
extra careful as that would need to be treated with $type = 'raw'
…
Oh, wait a minute! While using git grep
in the sources, looking for
that raw
type thing, I’ve discovered what sounded promising: an
InsertRawXML()
method, that doesn’t appear anywhere in either the
code or the documentation of the Net::XMPP::Message
module.
It’s available, though! Because Net::XMPP::Message
is derived from
Net::XMPP::Stanza
:
use Net::XMPP::Stanza;
use base qw( Net::XMPP::Stanza );
which then in turn comes with this function:
##############################################################################
#
# InsertRawXML - puts the specified string onto the list for raw XML to be
# included in the packet.
#
##############################################################################
Let’s put that aside for a moment and get back to the MessageSend()
method. It wants parameters that can be passed to the
Net::XMPP::Message
SetMessage()
method, and here is its entire
code:
###############################################################################
#
# MessageSend - Takes the same hash that Net::XMPP::Message->SetMessage
# takes and sends the message to the server.
#
###############################################################################
sub MessageSend
{
my $self = shift;
my $mess = $self->_message();
$mess->SetMessage(@_);
$self->Send($mess);
}
The first assignment is basically equivalent to my $mess = Net::XMPP::Message->new();
, so
what this function does is: creating a Net::XMPP::Message
for us, passing all
parameters there, and handing the resulting object over to the Send()
method. All in all, that’s merely a proxy.
HTML demo
The question becomes: what if we were to create that object ourselves,
then tweaking it a little, and then passing it directly to Send()
,
instead of using the slightly limited MessageSend()
? Let’s see what
the rewritten sending part would look like:
# Send HTML message:
my $text = 'This is a working test';
my $html = 'This is a <b>working</b> test';
my $message = Net::XMPP::Message->new();
$message->SetMessage(
to => $recipient,
body => $text,
type => 'chat',
);
$message->InsertRawXML("<html><body>$html</body></html>");
my $res = $con->Send($message);
And tada!
<message type='chat' to='human@example.org' from='bot@example.org/demo'>
<body>This is a working test</body>
<html>
<body>This is a <b>working</b> test</body>
</html>
</message>
I’m absolutely no expert when it comes to XMPP standards, and one
might need/want to set some more metadata like xmlns
but I’m happy
enough with this solution that I thought I’d share it as is. ;)