#!/usr/bin/perl ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # # datasink - a message archiving component # ======================================== # # This is an implementation of JEP-0136 (Message Archiving). # See http://www.jabber.org/jeps/jep-0136.html for details. # # I implemented this for my personal use with jwchat # (http://jwchat.sourceforge.net) a web based jabber client. # # Supported features: # * uses mysql to store data # * built in support for Service Discovery # # For better useability there is a slight difference with the protocol # defined in JEP 0136. Retrieving collections honor an additional # attribute 'jid' to get only those collections matching conversation # with 'jid', e.g. # # # # # # would return only those collections for zz@boyzzbrigade.org # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # # Installation instructions # ========================= # # * create a database for use with datasink # * import sql schema from datasink.sql # * set timezone to UTC for mysql to make timestamps work correctly # * install required perl modules # - Net::Jabber # - DBI # - Data::Dumper # - Digest::MD5 # - Getopt::Long # * edit your jabber server to let datasink connect as a component # * edit config.xml to suit your needs # * restart your jabber server # * start datasink # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # my $VERSION = "0.2.6"; # BEGIN LICENSE BLOCK ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # # Copyright (C) 2004 Stefan Strigler # # many ideas and code taken from users-agent and ChatBot # (thanks to Ryan Eatmon ) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # END LICENSE BLOCK use strict; # har har use Net::Jabber qw ( Component ) ; use Digest::MD5 qw(md5_hex); use Data::Dumper; use Getopt::Long; use DBI; my %optctl = (); $optctl{debug} = 0; $optctl{config} = "config.xml"; &GetOptions(\%optctl, "debug=i","config=s"); if (!(-f $optctl{config})) { print STDERR "ERROR: Config file cannot be found:\n"; print STDERR " $optctl{config}\n"; exit(1); } # this one's unused atm as my own debugger is better (imo) ;-) my $Debug = new Net::Jabber::Debug(level=>$optctl{debug}, header=>"Users-Agent"); ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # # global vars # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### my %config; # holds the config ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # # Setup the signals so that we can ctrl-c datasink and have it shut down # gracefully. # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### $SIG{HUP} = \&Stop; $SIG{KILL} = \&Stop; $SIG{TERM} = \&Stop; $SIG{INT} = \&Stop; ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # some little helpers ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### sub debug() { return unless ($config{debug}); my $msg = shift; my ($package,$filename,$line,$sub) = caller(1); ($package,$filename,$line) = caller(0); print STDERR "$sub\[l.$line\]: $msg\n"; } sub cutResource() { # cut's off resource from jids my $jid = shift; if ($jid =~ /\//) { $jid = substr($jid,0,index($jid,'/')); } return $jid; } sub createCID() { # creates a new cid my $jid = shift; my $digest; srand(); my $stamp = rand(); $digest = md5_hex($stamp,$jid); &debug("digest: " . $digest); return $digest; } sub dbSQL { my ($dbh, $sql_statement) = @_; &debug("sql_statement: $sql_statement"); #Prepare and error checking. my $sth = $dbh->prepare($sql_statement); $sth->execute; if ($sth->err) { # try to reconnect and send the query again &debug("trying to reconnect to DB"); $dbh = DBI->connect("DBI:mysql:database=".$config{mysql}->{dbname},$config{mysql}->{username},$config{mysql}->{password},{AutoCommit => 1, PrintError => 1}) or die "Can't connect to $config{mysql}->{dbname}: $DBI::errstr"; $sth = $dbh->prepare($sql_statement); $sth->execute; die $sth->errstr if $sth->err; } return $sth; } ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # main() # # let's get it on ... ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # create a parser my $xml_parser = XML::Stream::Parser->new(Style => 'Node'); # get config my $tree = $xml_parser->parsefile($optctl{config}); %config = %{&XML::Stream::XML2Config($tree)}; print "Message Archiving Component \'$config{component}->{name}\' v$VERSION starting...\n"; # get db handle my $dbh = DBI->connect("DBI:mysql:database=".$config{mysql}->{dbname},$config{mysql}->{username},$config{mysql}->{password},{AutoCommit => 1, PrintError => 1}) or die "Can't connect to $config{mysql}->{dbname}: $DBI::errstr"; $dbh->{mysql_auto_reconnect} = 1; # create component my $cmp = new Net::Jabber::Component(); $cmp->Info(name=>'Message Archive',version=>$VERSION); # register callbacks $cmp->SetCallBacks(iq=>\&handleIQ); # start component $cmp->Execute( hostname=>$config{server}->{hostname}, port=>$config{server}->{port}, secret=>$config{server}->{secret}, # connectiontype=>$config{server}->{connectiontype}, # unused anyway componentname=>$config{component}->{name}, connectattempts=>$config{component}->{connectattempts}, connectsleep=>$config{component}->{connectsleep} ); # component died unexpectedly print "Giving up and Exiting ...\n"; exit(0); ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # Stop # # do a clean shutdown ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### sub Stop() { print "\n\n***\n\nShutting down $config{component}->{name}: "; $cmp->Disconnect(); $dbh->disconnect(); print "done.\n"; exit(0); } ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # sendError - guess what it does ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### sub sendError() { my $IQ = shift; my $to = $IQ->GetFrom(); my $iqReply = XML::Stream::Node->new('iq'); $iqReply->put_attrib('type'=>'error'); $iqReply->put_attrib('to'=>$to); $iqReply->put_attrib('from'=>$config{component}->{name}); $iqReply->put_attrib('id'=>$IQ->GetID()) if($IQ->GetID() ne ''); my $rnode = XML::Stream::Node->new('error'); $rnode->put_attrib('type' => 'auth'); $rnode->put_attrib('code' => '406'); my $nnode = XML::Stream::Node->new('not-acceptable'); $nnode->put_attrib('xmlns'=>'urn:ietf:params:xml:ns:xmpp-stanzas'); $rnode->add_child($nnode); $iqReply->add_child($rnode); &debug($iqReply->GetXML()); $cmp->Send($iqReply); } ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # handleDiscoInfo # # replies to disco request ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### sub handleDiscoInfo() { my $IQ = shift; my $iqReply = $IQ->Reply(type=>'result'); my $iqQuery = $iqReply->NewQuery("http://jabber.org/protocol/disco#info"); $iqQuery->AddIdentity( 'category' => 'store', 'type' => 'file', 'name' => 'Message Archive' ); $iqQuery->AddFeature('var'=>'http://jabber.org/protocol/archive'); &debug($iqReply->GetXML()); $cmp->Send($iqReply); } ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # handleIQSet # # handles i.e.: # # * storing a message. if cid supplied take that if not create a new # one # or # * rename a collection # or # * close a collection ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### sub handleIQSet() { my $IQ = shift; my $from = $IQ->GetFrom(); my $child; foreach $child ($IQ->GetTree()->children()) { my $cid = $child->get_attrib('cid'); my $new_cid = 0; if ($child->get_tag() eq 'archive') { &debug("archive: " . $child->GetXML()); # name given? my $colname = $child->get_attrib('name'); if (defined($colname)) { &debug("name: ".$colname); } unless (defined($cid) && $cid ne '') { $cid = &createCID($from); $new_cid = 1; } # get message my $message = $child->XPath('message'); if (defined($message) && $message ne '') { &debug($message->GetXML()); } else { &debug("no message"); next; } my ($jid,$dir); if (($message->get_attrib('to') && &cutResource($message->get_attrib('to')) eq &cutResource($from)) || !$message->get_attrib('to')) { $jid = &cutResource($message->get_attrib('from')); $dir = 'from'; } else { $jid = &cutResource($message->get_attrib('to')); $dir = 'to'; } my $body = $message->XPath('body'); next unless (defined($body) && $body ne ''); &debug("body: ".$body->get_cdata()); # add message to tree if ($new_cid) { # start a new collection if (defined($colname) && $colname ne '') { &dbSQL($dbh,"INSERT INTO item VALUES(".$dbh->quote($cid).",".$dbh->quote(&cutResource($from)).",".$dbh->quote($jid).",".$dbh->quote($colname).",NOW(),NULL)"); } else { &dbSQL($dbh,"INSERT INTO item VALUES(".$dbh->quote($cid).",".$dbh->quote(&cutResource($from)).",".$dbh->quote($jid).",NULL,NOW(),NULL)"); } } else { # lookup collection my $sth = &dbSQL($dbh,"SELECT cid FROM item WHERE cid=".$dbh->quote($cid)); unless (defined($sth->fetchrow_array())) { &debug("no collection found for $cid"); # send error &sendError($IQ); return; } if (defined($colname) && $colname ne '') { # rename this # collection &dbSQL("UPDATE item SET name=$dbh->quote($colname) WHERE cid=$dbh->quote($cid)"); } } # insert message into collection &dbSQL($dbh,"INSERT INTO message(item_cid,dir,body) VALUES(".$dbh->quote($cid).",'$dir',".$dbh->quote($body->get_cdata()).")"); } elsif ($child->get_tag() eq 'done') { # close collection unless (defined($cid) && $cid ne '') { # cid missing - error out &sendError($IQ); return; } my $sth = &dbSQL($dbh,"SELECT cid FROM item WHERE cid=".$dbh->quote($cid)); unless (defined($sth->fetchrow_array())) { &debug("no collection found for $cid"); # send error &sendError($IQ); return; } &dbSQL($dbh,"UPDATE item SET end=NOW() WHERE cid=".$dbh->quote($cid)); } else { # unknown tag - send error &sendError($IQ); return; } # send result my $iqReply = XML::Stream::Node->new('iq'); $iqReply->put_attrib('type'=>'result'); $iqReply->put_attrib('to'=>$from); $iqReply->put_attrib('from'=>$config{component}->{name}); $iqReply->put_attrib('id'=>$IQ->GetID()) if($IQ->GetID() ne ''); my $rnode = XML::Stream::Node->new('archive'); $rnode->put_attrib('xmlns' => 'http://jabber.org/protocol/archive'); $rnode->put_attrib('cid' => $cid) if ($new_cid); $iqReply->add_child($rnode); &debug($iqReply->GetXML()); $cmp->Send($iqReply); } } ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # handleIQGet # # handles # * retrieve a list of collections # or # * return a specific collection ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### sub handleIQGet() { my $IQ = shift; my $from = $IQ->GetFrom(); my $archive = ($IQ->GetTree()->children())[0]; if (defined($archive->get_attrib('cid'))) { # send collection my $iqReply = XML::Stream::Node->new('iq'); $iqReply->put_attrib('type'=>'result'); $iqReply->put_attrib('to'=>$from); $iqReply->put_attrib('from'=>$config{component}->{name}); $iqReply->put_attrib('id'=>$IQ->GetID()) if($IQ->GetID() ne ''); my $rnode = XML::Stream::Node->new('archive'); $rnode->put_attrib('xmlns' => 'http://jabber.org/protocol/archive'); $iqReply->add_child($rnode); my $sth = &dbSQL($dbh,"SELECT jid FROM item WHERE cid=".$dbh->quote($archive->get_attrib('cid'))); my ($jid) = $sth->fetchrow_array; unless (defined($jid)) { # collection not found &sendError($IQ); return } $sth = &dbSQL($dbh,"SELECT dir,body FROM message WHERE item_cid=".$dbh->quote($archive->get_attrib('cid'))." ORDER BY cDate"); my ($dir,$body); while (($dir,$body) = $sth->fetchrow_array) { my $message = XML::Stream::Node->new('message'); if ($dir eq 'to') { $message->put_attrib('to'=>$jid); } else { $message->put_attrib('from'=>$jid); } $message->add_child('body',$body); $rnode->add_child($message); } &debug($iqReply->GetXML()); $cmp->Send($iqReply); } else { # list available collections my $iqReply = XML::Stream::Node->new('iq'); $iqReply->put_attrib('type'=>'result'); $iqReply->put_attrib('to'=>$from); $iqReply->put_attrib('from'=>$config{component}->{name}); $iqReply->put_attrib('id'=>$IQ->GetID()) if($IQ->GetID() ne ''); my $rnode = XML::Stream::Node->new('archive'); $rnode->put_attrib('xmlns' => 'http://jabber.org/protocol/archive'); $iqReply->add_child($rnode); my $sql = "SELECT cid,jid,name,DATE_FORMAT(start,'%Y%m%dT%H:%i:%S'),DATE_FORMAT(end,'%Y%m%dT%H:%i:%S') FROM item WHERE owner=".$dbh->quote(&cutResource($from)); if ($archive->get_attrib('jid')) { $sql .= " AND jid=".$dbh->quote($archive->get_attrib('jid')); } $sql .= " ORDER BY start DESC"; my $sth = &dbSQL($dbh,$sql); my ($cid,$jid,$name,$start,$end); while (($cid,$jid,$name,$start,$end) = $sth->fetchrow_array) { my $item = XML::Stream::Node->new('item'); $item->put_attrib('cid'=>$cid); $item->put_attrib('jid'=>$jid); $item->put_attrib('name'=>$name) if (defined($name) && $name ne ''); $item->put_attrib('start'=>$start); $item->put_attrib('end'=>$end) if (defined($end) && $end ne ''); next if (defined($archive->get_attrib('start')) && str2time($item->get_attrib("start")) <= str2time($archive->get_attrib('start'))); # skip older collections next if (defined($archive->get_attrib('end')) && defined($item->get_attrib('end')) && str2time($item->get_attrib("end")) >= str2time($archive->get_attrib('end'))); # skip older collections $rnode->add_child($item); } &debug($iqReply->GetXML()); $cmp->Send($iqReply); } } ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # handleIQ # # my own IQ router as lib functions don't apply to iq's without query # child tag ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### sub handleIQ() { my ($sid,$IQ) = @_; &debug($IQ->GetXML()); my $type = $IQ->GetType(); if ($type eq 'set') { &handleIQSet($IQ); } elsif ($type eq 'get') { my $xmlns = $IQ->GetQueryXMLNS(); if (defined($xmlns) && $xmlns eq 'http://jabber.org/protocol/disco#info') { &handleDiscoInfo($IQ); } else { &handleIQGet($IQ); } } }