#!/usr/bin/perl -w # # IRC RDF Chat Logger # # $Source: /usr/local/cvsroot/openacs-4/contrib/packages/irc-logger/perl/logger,v $ # $Id: logger,v 1.3 2013/09/12 14:38:11 victorg Exp $ # # (C) Copyright 2000-2001 Dave Beckett, ILRT, University of Bristol # http://purl.org/net/dajobe/ # # with modifications from Ralph Swick # http://www.w3.org/People/all#swick # # 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. # # See http://www.gnu.org/copyleft/gpl.html # # For documentation run perldoc on this file 'perldoc logger' # use strict; # Standard Perl modules use File::Path; use File::Basename; use Sys::Hostname; use Getopt::Long; use IO::Handle; use Encode; # From CPAN use URI; use Net::IRC; %ENV=(); $ENV{PATH}='/bin:/usr/bin:/usr/local/bin'; # Global constants $::program=basename $0; $::Host=(hostname || 'unknown'); $::Nick='logger'; # OK this can be changed if clashes $::IRC_Name='Chat Logger'; $::LogActionMsgs=1; $::LogUserHosts=0; $::OffTopic=1; # [off] at start of line is not logged @::LogTypes=qw(rdf html txt); @::DefaultLogTypes=qw(rdf txt); # ugly system dependencies # MSWin does not permit ":" in file path components $::PortSep = $^O eq "MSWin32" ? "/" : ":"; $::html_suffix="\n\n\n"; $::html_suffix_length=undef; # account for \n to \r\n expansion in stdio $::rdf_suffix=" \n \n\n\n"; $::rdf_suffix_length=undef; # account for \n to \r\n expansion in stdio #URIschemes and URIpatterns must match in order. The order is not signficant. @::URIschemes = ( 'http://', 'news:', 'ftp://', 'file://', 'gopher://', 'nntp://', 'wais://', 'telnet://', 'prospero://', 'mailto:' ); @::URIpatterns = ( qr|(http://)([^] \)>\"\'\n\[\t\\]*)(.*)$|, qr|(news:)([^] \)>"\'\n[\t\\]*)(.*)$|, qr|(ftp://)([^] \)>"\'\n[\t\\]*)(.*)$|, qr|(file://)([^] \)>"\'\n[\t\\]*)(.*)$|, qr|(gopher://)([^] \)>"\'\n[\t\\]*)(.*)$|, qr|(nntp://)([^] \)>"\'\n[\t\\]*)(.*)$|, qr|(wais://)([^] \)>"\'\n[\t\\]*)(.*)$|, qr|(telnet://)([^] \)>"\'\n[\t\\]*)(.*)$|, qr|(prospero://)([^] \)>"\'\n[\t\\]*)(.*)$|, qr|(mailto:)([^] \)>"\'\n[\t\\]*)(.*)$|, ); # Global variables # IRC object $::IRC=undef; # root dir of logs $::Log_Root=''; # place on the web this corresponds to $::Log_URI=''; # System password $::Password=''; # Print welcome message? $::Do_Welcome=0; # True if leaving (so don't reconnect) $::Departing=0; # True if connecting (don't log server notices) $::Connecting=0; # Administrative messages log $::Admin_LOG=undef; # Process ID $::PID_File=undef; # On connect /msg $user $cmd $::Connect_User=undef; $::Connect_CMD=undef; ###################################################################### package URI::irc; # http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt # irc:[ //[ [:] ]/[] [,needpass] ] require URI::_server; @URI::irc::ISA=qw(URI::_server); sub default_port { 6667 } sub channel ($) { my $path=shift->path; if($path && $path =~ m%^/([^/]+)%) { return $1; } undef; } package main; # this hideous concatenation only serves to hide this source line from CVS # The objective is to create a regex pattern that matches all CVS/RCS keywords $::CVSkeywords = '\$'.'Date\$|\$'.'Date:.*\$|\$'.'Revision\$|\$'.'Revision:.*\$|\$'.'Author\$|\$'.'Author:.*\$|\$'.'Id\$|\$'.'Id:.*\$|\$'.'Log\$|\$'.'Log:.*\$|\$'.'Header\$|\$'.'Header:.*\$|\$'.'Locker\$|\$'.'Locker:.*\$|\$'.'Name\$|\$'.'Name:.*\$|\$'.'RCSfile\$|\$'.'RCSfile:.*\$|\$'.'Source\$|\$'.'Source:.*\$|\$'.'State\$|\$'.'State:.*\$'; ###################################################################### sub main { my $usage=<<"EOT"; Usage: $::program [option...] password channel-URI channel-title log-dir log-URI where option is one or more of: -html write an XHTML log -notext Do not write a text log -log Write logs to logfile rather than the default of channel/YYYY-MM-DD (\".txt\", \".html\", and \".rdf\" will be appended) -nick set the nick -connectuser on connect /msg this user -connectcmd with this command -noaction Do not log /me messages -noofftopic Do not ignore lines starting with [off] -userhosts Record user\@host in /join messages and channel-URI is like irc://host[:port]/channel EOT my(%do_log_types)=map {$_ => 1} @::DefaultLogTypes; my $log_name=undef; die $usage unless GetOptions ('action!' => \$::LogActionMsgs, 'html!' => \$do_log_types{'html'}, 'text!' => \$do_log_types{'txt'}, 'log=s' => \$log_name, 'nick=s' => \$::Nick, 'connectuser=s' => \$::Connect_User, 'connectcmd=s' => \$::Connect_CMD, 'userhost!' => \$::LogUserHosts, 'offtopic!' => \$::OffTopic, ) && @ARGV==5; @::DefaultLogTypes=grep($do_log_types{$_}, @::LogTypes); my($password,$uri_string,$channel_title, $log_root, $log_uri_string)=@ARGV; my $uri; eval '$uri=new URI $uri_string'; die "$::program: '$uri_string' does not look like an IRC URI\n" if ($@ || !$uri); die "$::program: log dir $log_root does not exist\n" unless -d $log_root; # Set globals $::Password=$password; $::Log_Root=$log_root; $::Log_URI=$log_uri_string; $::Do_Welcome=0; $::Departing=0; $::Connecting=1; # Open the administrative log file my $admin_log_file=$::Log_Root.'/admin.log'; $::Admin_LOG=new IO::File; $::Admin_LOG->open(">>$admin_log_file") or die "$::program Failed to append to admin log file $admin_log_file - $!\n"; $::Admin_LOG->autoflush(1); umask 022; # FIXME - pid_file should not have channel in it, when logger # handles multiple channels my $channel_name=$uri->channel; $::PID_File=$::Log_Root.'/logger-'.$channel_name.'.pid'; open(PID,">$::PID_File"); print PID "$$\n"; close(PID); $::IRC = new Net::IRC; my $channel=&Channel_new($uri, $channel_title, $log_name); Channel_join($channel); # Never returns $::IRC->start; } # MAIN CODE &main; exit(0); ###################################################################### # Methods on 'logger Channel' object # package Logger::Channel; @::Channels=(); sub Channel_new ($$$) { my($uri,$title,$log_name)=@_; my $self={}; # Channel title $self->{Title}=$title; # irc:: URI $self->{URI}=$uri; # a file prefix to log to (i.e. write log_name.rdf etc.) or undef # to use default schema $self->{LogName}=$log_name; # topic of channel $self->{Topic}=''; # Last ID seen $self->{Last_ID}=''; # True if logging $self->{Listening}=0; # Track midnight rollover $self->{hour}= undef; # NET::IRC connection object $self->{CONN}=undef; $self->{LogTypes}=[@::DefaultLogTypes]; for my $type (@{$self->{LogTypes}}) { $self->{FH}->{$type}=undef; } # Prefix of log files - add ".html" etc. to give file name $self->{LogFilePrefix}=undef; # Prefix of log URIs or undef if no URI $self->{LogURIPrefix}=undef; push(@::Channels, $self); $self; } sub Channel_join($) { my($self)=@_; my $uri=$self->{URI}; my $channel_name=$uri->channel; $self->{Listening}=1; my $user_name=substr($channel_name,0,8)."-logger"; my $conn = $::IRC->newconn(Nick => $::Nick, Server => $uri->host, Port => $uri->port, Ircname => $::IRC_Name, Username => $user_name, LocalAddr => $::Host); die "$::program: Cannot create connection to $uri\n" unless $conn; $self->{CONN}=$conn; Channel_open_logs($self); # Install handlers # On 'end of MOTD' event, join ilrt channel $conn->add_global_handler(376, \&on_connect); $conn->add_global_handler('nomotd', \&on_connect); $conn->add_global_handler(353, \&on_names); $conn->add_global_handler('disconnect', \&on_disconnect); $conn->add_handler('msg', \&on_msg); $conn->add_handler('public', \&on_public); $conn->add_handler('part', \&on_part); $conn->add_handler('join', \&on_join); $conn->add_handler('kick', \&on_kick); $conn->add_handler('caction', \&on_caction); $conn->add_handler('quit', \&on_quit); $conn->add_handler('nick', \&on_nick); $conn->add_handler('nicknameinuse', \&on_nicknameinuse); $conn->add_handler('topic', \&on_topic); $conn->add_handler('notice', \&on_notice); } sub Channel_by_conn($) { my $conn=shift; for my $channel (@::Channels) { return $channel if $channel->{CONN} == $conn; } } sub Channel_get_log_dir ($) { my $self=shift; my $uri=$self->{URI}; my $channel_name=$uri->channel; $channel_name=~ s/\W//g; return $::Log_Root.'/'.$uri->host.$::PortSep.$uri->port.'/'.$channel_name.'/'; } sub Channel_get_log_lines ($) { my $self=shift; my(@file_dates); if (my $log_name=$self->{LogName}) { @file_dates=["$log_name.txt", undef]; } else { my $log_dir=Channel_get_log_dir($self); return () if !opendir(DIR, $log_dir); for my $file (reverse sort readdir(DIR)) { next unless $file =~ /^(\d\d\d\d-\d\d-\d\d).txt/; my $date=$1; push(@file_dates, ["$log_dir/$file", $date]); } closedir(DIR); } # In date order, newest lines at top my(@results)=(); # Newest files first for my $file_date (@file_dates) { my($file,$date)=@$file_date; open(LOG, $file) or next; # Append to list, reverse date sorted list - i.e. older entries at end my(@lines); while() { chomp; # Ignore logger's own output next if m%^\d\d:\d\d:\d\d <$::Nick>%; next if !length $_; $_="$date $_" if $date; push(@lines, $_); }; close(LOG); push(@results, reverse @lines); } return(@results); } sub Channel_open_logs ($) { my $self=shift; my $conn=$self->{CONN}; my $uri=$self->{URI}; my $channel_name=$uri->channel; my @tm = gmtime; $tm[5]+= 1900; $tm[4]++; my $date = sprintf("%04d-%02d-%02d", $tm[5], $tm[4], $tm[3]); my(%log_files); if (my $log_name=$self->{LogName}) { $self->{LogFilePrefix}=$log_name; $self->{LogURIPrefix}=$::Log_URI if $::Log_URI; } else { my $log_dir=Channel_get_log_dir($self); mkpath([$log_dir], 0, 0755) if ! -d $log_dir; # Failed! if (! -d $log_dir) { log_admin_event($self, undef, time, "Failed to create chat log dir $log_dir - $!"); unlink $::PID_File; exit(0); } $self->{LogFilePrefix}=$log_dir."/".$date; $self->{LogURIPrefix}=$::Log_URI.$date if $::Log_URI; } for my $type (@{$self->{LogTypes}}) { $log_files{$type}=$self->{LogFilePrefix}.".".$type; } if (grep($_ eq 'txt', @{$self->{LogTypes}})) { my $txt_log_fh=$self->{FH}->{txt}=new IO::File; my $txt_log_file=$log_files{txt}; if (!$self->{FH}->{txt}->open(">>$txt_log_file")) { log_admin_event($self, undef, time, "Failed to create text log file $txt_log_file - $!"); unlink $::PID_File; exit(0); } $txt_log_fh->autoflush(1); } if (grep($_ eq 'html', @{$self->{LogTypes}})) { my $html_log_fh; my $html_log_file=$log_files{html}; if (!-r $html_log_file) { # new file? $html_log_fh=$self->{FH}->{html}=new IO::File; if (!$html_log_fh->open(">$html_log_file")) { log_admin_event($self, undef, time, "Failed to create HTML log file $html_log_file - $!"); unlink $::PID_File; exit(0); } my $escaped_chan = xml_escape($channel_name); print $html_log_fh <<"EOT"; IRC log of $escaped_chan on $date EOT my $progID = '$Id: logger,v 1.3 2013/09/12 14:38:11 victorg Exp $'; $progID =~ s/\$/\$/g; # Hide the CVS tag from CVS in the output print $html_log_fh ''; print $html_log_fh <<"EOT";

IRC log of $escaped_chan on $date

Timestamps are in UTC.

EOT print $html_log_fh $::html_suffix; } else { $html_log_fh=$self->{FH}->{html}=new IO::File; if (!$html_log_fh->open("+<$html_log_file")) { log_admin_event($self, undef, time, "Failed to append to HTML log file $html_log_file - $!"); unlink $::PID_File; exit(0); } } # figure out whether stdio expands NEWLINE to CRLF if (!defined $::html_suffix_length) { seek($html_log_fh, 0, 2); # find the end my $curlen = tell($html_log_fh); print $html_log_fh $::html_suffix; $::html_suffix_length = tell($html_log_fh) - $curlen; truncate ($html_log_fh, $curlen); } $html_log_fh->autoflush(1); } # Note RDF log type is not optional ;-) my $rdf_log_fh; my $rdf_log_file=$log_files{rdf}; if(!-r $rdf_log_file) { $rdf_log_fh=$self->{FH}->{rdf}=new IO::File; if (!$rdf_log_fh->open(">$rdf_log_file")) { log_admin_event($self, undef, time, "Failed to create RDF log file $rdf_log_file - $!"); unlink $::PID_File; exit(0); } my $escaped_chan_uri = xml_escape($self->{URI}); print $rdf_log_fh <<"EOT"; EOT print $rdf_log_fh $::rdf_suffix; } else { $rdf_log_fh=$self->{FH}->{rdf}=new IO::File; if (!$rdf_log_fh->open("+<$rdf_log_file")) { log_admin_event($self, undef, time, "Failed to append to RDF log file $rdf_log_file - $!"); unlink $::PID_File; exit(0); } } # figure out whether stdio expands NEWLINE to CRLF if (!defined $::rdf_suffix_length) { seek($rdf_log_fh, 0, 2); # find the end my $curlen = tell($rdf_log_fh); print $rdf_log_fh $::rdf_suffix; $::rdf_suffix_length = tell($rdf_log_fh) - $curlen; truncate ($rdf_log_fh, $curlen); } $self->{FH}->{rdf}->autoflush(1); } sub Channel_close_logs ($) { my $self=shift; for my $type (@{$self->{LogTypes}}) { $self->{FH}->{$type}->close; $self->{FH}->{$type}=undef; } } ###################################################################### # Logging - methods on Net::IRC::Connection object sub log_event ($$$;$) { my($self, $event, $t, $msg)=@_; my $nick=$event->nick; my $channel=Channel_by_conn($self); return if !$channel->{Listening}; my @tm = gmtime($t); $tm[5]+= 1900; $tm[4]++; my $date = sprintf("%04d-%02d-%02d", $tm[5], $tm[4], $tm[3]); my $time = sprintf("%02d:%02d:%02d", $tm[2], $tm[1], $tm[0]); my $date_time="$date $time"; my $hour=$tm[2]; # if we're using dated logs and we passed midnight, start new logs if (!$channel->{LogName} && defined $channel->{hour} && ($hour < $channel->{hour})) { Channel_close_logs($channel); Channel_open_logs($channel); } $channel->{hour} = $hour; $msg=ensure_utf8($msg); if (grep($_ eq 'txt', @{$channel->{LogTypes}})) { my $txt_msg=$nick ? qq{<$nick> $msg} : $msg; my $txt_log_fh=$channel->{FH}->{txt}; print $txt_log_fh "$time $txt_msg\n"; } # Make a legal XML id from the time my $ID="T$time"; $ID =~ s/:/-/g; # add a unique suffix if necessary if(length($channel->{Last_ID}) != length($ID)) { # If last ID was in same second, need to start from next # ID HH-MM-SS-X and increment X # otherwise, new ID is OK if ($channel->{Last_ID} =~ /^$ID-(\d+)$/) { $ID.="-".($1+1); } } elsif($ID eq $channel->{Last_ID}) { # Else if was same as last ID, must be first duplicate of this # ID in this time slot ie last id was HH-MM-SS so add -1 to make # it unique HH-MM-SS-1 $ID.="-1"; } $channel->{Last_ID}=$ID; my @uris = getURI($msg); if (grep($_ eq 'html', @{$channel->{LogTypes}})) { my $html_log_fh=$channel->{FH}->{html}; # seek back # @@ note that this assumes the existing data was NL-expanded identically seek($html_log_fh, -$::html_suffix_length, 2); # 2= SEEK_END my $escapedMsg = cvs_escape($msg); if ($#uris > -1) { $escapedMsg = URI2link($escapedMsg, @uris); } print $html_log_fh qq{
$time [$nick]
}.$escapedMsg.qq{
\n}; print $html_log_fh $::html_suffix; } my $rdf_log_fh=$channel->{FH}->{rdf}; # seek back # @@ note that this assumes the existing data was NL-expanded identically seek($rdf_log_fh, -$::rdf_suffix_length, 2); # 2= SEEK_END print $rdf_log_fh <<"EOT"; ${date}T${time}Z EOT print $rdf_log_fh qq{ }.cvs_escape($msg).qq{\n}; print $rdf_log_fh qq{ \n} if $nick; foreach my $uri (@uris) { $uri = xml_escape($uri); print $rdf_log_fh qq{ \n}; } print $rdf_log_fh qq{ \n}; print $rdf_log_fh qq{ \n}; print $rdf_log_fh $::rdf_suffix; } sub log_admin_event ($$$;$) { my($self, $event, $t, $msg)=@_; my $nick=$event ? $event->nick : ''; my @tm = gmtime($t); $tm[5]+= 1900; $tm[4]++; my $date = sprintf("%04d-%02d-%02d", $tm[5], $tm[4], $tm[3]); my $time = sprintf("%02d:%02d:%02d", $tm[2], $tm[1], $tm[0]); my $txt_msg=$nick ? qq{<$nick> $msg} : $msg; if($::Admin_LOG) { print $::Admin_LOG "${date}Z${time} $txt_msg\n"; } else { warn "${date}Z${time} $txt_msg\n"; } } ###################################################################### # Global events # What to do when logger successfully connects. sub on_connect { my ($self, $event) = @_; my $channel=Channel_by_conn($self); my $channel_name; if ($channel->{URI}->channel =~ m/[\#&]/) { $channel_name = $channel->{URI}->channel; # don't add a prefix if one was given } else { $channel_name ='#' . $channel->{URI}->channel; # else assume a public channel } if($::Connect_User && $::Connect_CMD) { $self->privmsg($::Connect_User, $::Connect_CMD); } log_admin_event($self, $event, time, "Connected to server"); $self->join($channel_name); $channel->{Listening}=1; $self->me($channel_name, 'is logging'); } # What to do when logger's nick is already used sub on_nicknameinuse { my ($self, $event) = @_; if ($::Nick !~ /_/) { $::Nick.="_1"; } else { my($name,$number)=split(/_/, $::Nick); $::Nick=$name."_".($number+1); } $self->nick($::Nick); } # Prints the names of people in a channel when we enter. sub on_names { my ($self, $event) = @_; my (@list, $channel_name) = ($event->args); # splice() only works on real arrays. Sigh. ($channel_name, @list) = splice @list, 2; log_event($self, $event, time, "Users on $channel_name: @list"); $::Connecting=0; } # Reconnect to the server when we die. sub on_disconnect { my ($self, $event) = @_; my $t=time; my $m="Disconnected from ". $event->from(). " (". ($event->args())[0]. ")"; log_admin_event($self, $event, $t, $m); log_event($self, $event, $t, $m); return if $::Departing; log_admin_event($self, $event, time, "Attempting to reconnect"); unlink $::PID_File; exit(0); } ###################################################################### # Per connection events # When received a private message sub on_msg { my($self,$event)=@_; my $nick = $event->nick; my $channel_name = $event->to; return if $nick eq $::Nick; my $arg = join(' ', $event->args); command_for_me($self, $event, $channel_name, $arg, 1); } # What to do when we receive channel text. sub on_public { my ($self, $event) = @_; my @to = $event->to; my ($nick, $mynick) = ($event->nick, $self->nick); my $arg = join(' ', $event->args); # Private stuff return if ($::OffTopic && $arg =~ /^\[off\]/i); log_event($self, $event, time, $arg); if ($arg =~ /^$mynick[,:]\s*(.*)$/i) { command_for_me($self, $event, $to[0], $1, 0); } } # What to do when we receive /me (and other stuff??) sub on_caction { my ($self, $event) = @_; my $nick = $event->nick; my $arg = join(' ', $event->args); # Private stuff return if !$::LogActionMsgs || ($::OffTopic && $arg =~ /^\[off\]/i); log_event($self, $event, time, "* $nick $arg"); } # What to do when someone leaves a channel logger is on. sub on_part { my ($self, $event) = @_; my ($channel_name) = ($event->to)[0]; my $nick=$event->nick; log_event($self, $event, time, "$nick has left $channel_name"); } # What to do when we receive channel notice (mostly other bots) sub on_notice { my ($self, $event) = @_; my $nick = $event->nick; my $arg = join(' ', $event->args); # Private stuff return if ($::OffTopic && $arg =~ /^\[off\]/i); return if $::Connecting; log_event($self, $event, time, $arg); } # What to do when someone leaves a channel logger is on. sub on_quit { my ($self, $event) = @_; my $nick=$event->nick; my $arg = join(' ', $event->args); my $msg="$nick has quit"; $msg.=" ($arg)" if $::LogUserHosts; log_event($self, $event, time, $msg); } # What to do when someone is kicked on a channel logger is on. sub on_kick { my ($self, $event) = @_; my $nick=$event->nick; my $whom = ($event->to)[0]; my $channel_name = ($event->args)[0]; my $msg="$nick has kicked $whom from $channel_name"; log_event($self, $event, time, $msg); } # What to do when someone does /nick sub on_nick { my ($self, $event) = @_; my $nick=$event->nick; my $arg = join(' ', $event->args); log_event($self, $event, time, "$nick is now known as $arg"); } # What to do when someone does /topic MSG sub on_topic { my ($self, $event) = @_; my $channel=Channel_by_conn($self); my $nick=$event->nick; my(@args)=$event->args; my $arg; if(@args == 3) { $arg=$args[2]; log_event($self, $event, time, "topic is: $arg"); } else { $arg=$args[0]; log_event($self, $event, time, "$nick has changed the topic to: $arg"); } $channel->{Topic}=$arg; } # What to do when someone joins a channel logger is on. sub on_join { my ($self, $event) = @_; my $channel=Channel_by_conn($self); my ($channel_name) = ($event->to)[0]; my $user_nick=$event->nick; my $format=$::LogUserHosts ? "%s (%s) has joined $channel_name" : "%s has joined $channel_name"; my $t=time; my $m=sprintf($format, $user_nick, $event->userhost); log_event($self, $event, $t, $m); if($user_nick eq $::Nick) { log_admin_event($self, $event, $t, $m); } return if !$::Do_Welcome; my $logging=($channel->{Listening}) ? "logging" : "not logging"; my(@intro)=( "Welcome to the $channel->{Title}", "I'm currently $logging to $::Log_URI", "For extensive help do: /msg $::Nick help" ); my $do_sleep=0; for my $output (@intro) { sleep(1) if $do_sleep; $self->privmsg($event->nick, $output); $do_sleep=1; } } sub command_for_me ($$$$$) { my($self, $event, $channel_name, $command, $is_private)=@_; my $channel=Channel_by_conn($self); my $from_nick=$event->nick; $command=~s/^\s+//; my $output=''; my $dest_nick=($is_private ? $from_nick : $event->to); my $valid_password=0; if($command =~ s/^password (\S+)\s*//) { if($1 eq $::Password) { $valid_password=1; } else { $self->privmsg($dest_nick, "Invalid password"); return; } } if($valid_password) { if($command=~ /^(?:quit|finish|terminate|die die die|exterminate|bye|excuse us)/i) { $::Departing=1; $self->me($event->to, 'is departing'); # Log who told me to quit log_admin_event($self, $event, time, "Logger told to quit"); $self->quit; unlink $::PID_File; exit(0); } if($command eq 'restart') { $::Departing=1; $self->me($event->to, ' is departing'); $self->quit; sleep(1); $::Connecting=1; $self->connect(); return; } if($command eq 'debug') { $self->privmsg($dest_nick, "Debugging is on"); $self->debug(1); return; } if($command eq 'nodebug') { $self->privmsg($dest_nick, "Debugging is off"); $self->debug(0); return; } } if($command=~ /^(?:be quiet|shut up|silence|sshush|stop|off|nolisten)/i) { if($channel->{Listening}) { $self->me($event->to, 'is not logging'); # Log who turned me off log_admin_event($self, $event, time, "Logging turned off"); $channel->{Listening}=0; } else { $self->me($event->to, 'is already not logging'); } return; } if($command=~ /^(?:hello|log|listen|record|start|begin|on|listen)/i) { if(!$channel->{Listening}) { $self->me($event->to, 'is logging'); # Log who turned me on log_admin_event($self, $event, time, "Logging turned on"); $channel->{Listening}=1; } else { $self->me($event->to, 'is already logging'); } return; } if($command=~ /^(?:sync)/i) { Channel_close_logs($channel); Channel_open_logs($channel); return; } if($command=~ /^(?:pointer|bookmark|here|where am i\?)/i) { my($log_uri)=$channel->{LogURIPrefix}; my $output; if($log_uri) { $log_uri.="#".$channel->{Last_ID} if $channel->{Last_ID}; $output="See $log_uri"; } else { $output="There is no log URI"; } $self->privmsg($dest_nick, $output); log_event($self, $event, time, $output) unless $is_private; return; } if($command=~ /^chump\s*(.+)$/i) { my $item=$1; my($log_uri)=$channel->{LogURIPrefix}; my $output; my $chump=undef; if($item =~ /^([A-Za-z]+):?$/) { $chump=uc $1; $chump=undef if $chump eq 'BLURB'; } if(!$chump) { $output="Invalid chump item $item"; } elsif($log_uri) { $log_uri.="#".$channel->{Last_ID} if $channel->{Last_ID}; $output="$chump:See [$log_uri|discussion]"; } else { $output="There is no log URI"; } $self->privmsg($dest_nick, $output); log_event($self, $event, time, $output) unless $is_private; return; } if(!$channel->{Listening}) { $output="I'm not logging. "; } else { $output="I'm logging. "; } # Allow question? if ($command =~ /^(.+)\?$/) { $command="grep $1"; } if($command=~ /^help/i) { my(@help)=( "The commands I know are:", " silence - Stop logging (also: stop, off, ...)", " listen - Start logging (also: start, on, ...)", " bookmark - Give the URI of the current log", " chump LETTER - Record the URI of the current log under chump LETTER", "I respond to '$::Nick, command' in public and '/msg $::Nick command' in private", "Logging Policy: All public output is logged if I am listening except for" ); if (! $::LogActionMsgs) { @help=(@help, "\"action\" messages (messages sent with the '/me' command) and" ); } @help=(@help, "any lines starting [off]. All commands to me are logged.", "My public output is logged but these lines are not searchable.", "The logs are at $::Log_URI", "Do $::Nick, adminhelp for help on administrative commands", ); my $do_sleep=0; for my $output (@help) { sleep(1) if $do_sleep; $self->privmsg($dest_nick, $output); $do_sleep=1; } return; } if($command =~ /^adminhelp/i) { my(@help)=( "Administrative commands are as follows:", " quit - I will depart", " restart - I will leave and rejoin channel", " debug - Turn on debugging", " nodebug - Turn off debugging", "These commands work only with the admin PASSWORD like this:", "/msg $::Nick password PASSWORD command'", ); my $do_sleep=0; for my $output (@help) { sleep(1) if $do_sleep; $self->privmsg($dest_nick, $output); $do_sleep=1; } return; } if ($command =~ /^(?:grep|search for|find)\s+(.+)$/) { $output.=qq{Sorry, searching removed.}; } else { $output.="I don't understand '$command', $from_nick. Try /msg $::Nick help"; } $self->privmsg($dest_nick, $output); log_event($self, $event, time, $output) unless $is_private; } ###################################################################### # Utility subroutines sub ensure_utf8 ($) { my $text = shift; # If it cannot be decoded as UTF-8... my $t="".$text.""; eval { decode("utf8", $t, Encode::FB_CROAK); }; if($@) { # Assume it is latin-1 (there is no real IRC encoding), encode as UTF-8 $text=encode("utf8", decode("iso-8859-1", $text), Encode::FB_QUIET); } $text; } # Escape any special characters that are significant to XML # Then hide any CVS/RCS tags from future invocations of CVS/RCS sub cvs_escape ($) { my ($text) = @_; $text = xml_escape($text); return $text if ($text !~ /\$/o); # nothing to hide if ($text =~ /$::CVSkeywords/o) { $text =~ s/\$/$/g; } return $text; } sub xml_escape ($) { my $string=shift; $string =~ s/\&/\&/g; $string =~ s//\>/g; $string =~ s/[\x00-\x1F]//g; # remove ASCII 0-31 # Output is UTF-8, so don't throw away high characters # $string =~ s/([\x80-\xFF])/"\&#".ord($1).";"/ge; # escape ASCII 128-255 $string; } # URI recognition # The following URI recognition algorithm was translated from # the Hypermail implementation; see parseurl() in # http://dev.w3.org/cvsweb/~checkout~/hypermess/hypermail/src/string.c?rev=1.4 #returns a list of any URIs found in the input string sub getURI ($) { my ($l) = @_; my @ret = (); if ($l !~ /:/o) { # give up if input can't have any schemes return (); } chomp $l; while (my $leftmost = length ($l)) { # while string is not empty my $i = 0; my $match = -1; foreach my $u (@::URIschemes) { # search for first matching URI scheme my $p = index( $l, $u ); if ($p >= 0 && $p < $leftmost) { $leftmost = $p; $match = $i; } $i++; } if ($match != -1) { # if a scheme was found, extract the URI $l = substr($l, $leftmost); my $u = $l; $u =~ s/$::URIpatterns[$match]/$1$2/; $l = $3; # rest of string after extracting the pattern @ret=(@ret, $u); } else { $l = ""; } } return @ret; } #Return a string with any URIs specified by the second argument (a list) #found in the first argument expanded to HTML anchors sub URI2link ($@) { my ($l, @uri) = @_; chomp $l; foreach my $u (@uri) { my $p = quotemeta $u; $l =~ s/($p)/$1<\/a>/; } return $l; } __END__ =pod =head1 NAME logger - RDF IRC Chat Logger =head1 SYNOPSIS logger [options...] PASSWORD CHANNEL-URI CHANNEL-TITLE LOG-DIR LOG-URI > logger.log An irc logger bot that automatically generated logs for various IRC chat channels. Call it with parameters above where PASSWORD Administrator password for some commands CHANNEL-URI IRC channel URI like irc://host[:port]/channel CHANNEL-TITLE A title to use in welcome messages LOG-DIR Root directory to start writing logs LOG-URI URI of where the logs appear on the web and options are: -html Write an XHTML log as well as text and RDF. -log LOGFILE Write logs to LOGFILE rather than the default of CHANNEL/YYYY-MM-DD (".txt", ".html", and ".rdf" will be appended) -nick NICK Use IRC nick NICK -noaction Do not log /me messages -noofftopic Do not ignore lines starting with [off] -userhosts Record user@host from /join messages =head1 DESCRIPTION The logger bot listens to the chat channel and records it in public logs which are written live in three formats - RDF, plain text and HTML (usually created from RDF via XSLT by some other program). Logger accepts a few commands that can be done publically with logger, COMMAND or privately with /msg logger COMMAND The full list of commands can be found by the help command /msg logger help Logger can be told to stop listening/recording to the chat, if for some reason this is required. For individual messages this can be done by putting the phrase '[off]' at the start of a line of text and for longer conversations, tell logger to stop listening with: logger, off and recording again with logger, on (there are other synonyms). The on/off messages are logged, as well as all public commands to logger. This feature can be turned off with -noofftopic The current log URI, and the position in it can be queried at any time with logger, pointer or using one of the other aliases: here, bookmark, where am i? The current log URI can be recorded in a particular chump bot item using: logger, chump D to record the discussion below item D. Logger has some administrative commands that can be found from: /msg logger adminhelp These require the startup password in order to use, for example this will make logger terminate: /msg logger password PASSWORD quit logger will attempt to reconnect when disconnected but doesn't yet handle all network problems very gracefully. =head2 LICENSE GNU GPL version 2 or later - see http://www.gnu.org/copyleft/gpl.html =head2 AUTHOR Dave Beckett - http://purl.org/net/dajobe/, Institute for Learning and Research Technology, http://ilrt.org/, University of Bristol. with lots of changes from Ralph Swick - http://www.w3.org/People/all#swick, W3C/MIT =cut