#!/usr/bin/perl -w
#
# Enforced originality! If someone repeats something that has been already
# said in channel, silence them. Silence time increasing geometrically.
#
# Copyright (C) 2007 Dan Boger - zigdon+bot@gmail.com
#
# 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.
#
use strict;
use Net::IRC;
use Time::HiRes qw/usleep/;
use DBI;
use Date::Calc qw/Normalize_DHMS/;
use Data::Dumper;
# IRC info
my">
my $irc_server = "irc.example.com";
my">
my $irc_chan = "#xkcd-signal";
my">
my $irc_nick = "botname";
my">
my $irc_pass = "botpass";
my">
my $irc_name = "I AM MODERATOR";
my">
my $irc_nick_default = $irc_nick;
# Database info
my">
my $db_name = "schema_name";
my">
my $db_user = "database_user";
my">
my $db_pass = "sekret";
# Other settings
my">
my $logfile = "/path/to/logfile";
my">
my $maint_time = time + 20;
# Authorized users
zigdon => 'user@hostmask.example.com',
xkcd => 'other@hostmask2.example.com',
);
my">
my ( $irc, $irc_conn, $dbh ) = &setup
;
$irc_conn->names;
&event_loop;
sub logmsg {
}
sub event_loop {
#warn "event_loop (@_)n";
$irc->do_one_loop();
usleep 50;
if ( $next_unban and">
and time > $next_unban ) {
&process_unbans;
}
logmsg "Running maint";
logmsg "Clearing nick_changes for $_";
}
#$irc_conn->debug(1);
$irc_conn->privmsg( 'nickserv', "status " .
join " ",
$sql{high_score}->execute;
my">
my ( $hi_nick, $hi_score ) = $sql{high_score
}->fetchrow_array;
$sql{high_score}->finish;
logmsg "High score: $hi_nick, $hi_score";
my">
my $oldtopic = $topic;
$topic =~ s/High score:(?: S+ S+)? |/High score: $hi_nick $hi_score |/;
if ( $topic ne $oldtopic ) {
logmsg "Setting topic to $topic";
$irc_conn->topic( $irc_chan, $topic );
}
}
$maint_time = time + 300;
}
}
}
sub process_unbans {
$sql{get_unbans
}->execute(time);
while ( my">
my ( $nick, $userhost, $id ) = $sql{get_unbans
}->fetchrow_array ) {
logmsg "Restoring $userhost";
$irc_conn->mode( $irc_chan, "+v", $nick );
$sql{clear_ban}->execute($id);
#$irc_conn->privmsg( $nick, "you may now speak in $irc_chan." );
}
$sql{next_unban}->execute;
($next_unban) = $sql{next_unban}->fetchrow_array;
$sql{next_unban}->finish;
}
sub setup {
#warn "setup (@_)n";
# open our log file
open LOG
, ">>$logfile" or die "Can't write to $logfile: $!";
logmsg "Starting up";
# log into IRC
logmsg "Connecting irc://$irc_nick";
$irc = new Net::IRC;
my">
my $irc_conn = $irc->newconn(
Nick => $irc_nick,
Server => $irc_server,
Ircname => $irc_name,
);
open DEBUG
, ">>$logfile.debug"
or die "Can't write to $logfile.debug: $!";
$irc_conn->add_default_handler( &dump_event );
}
# talk events
$irc_conn->add_handler( public => &irc_on_public );
$irc_conn->add_handler( caction => &irc_on_public );
$irc_conn->add_handler( notice => &irc_on_notice );
$irc_conn->add_handler( msg => &irc_on_msg );
# user events
$irc_conn->add_handler( nick => &irc_on_nick );
$irc_conn->add_handler( join => &irc_on_joinpart
);
$irc_conn->add_handler( part => &irc_on_joinpart );
$irc_conn->add_handler( quit => &irc_on_joinpart );
# server events
$irc_conn->add_handler( 376 => &irc_on_connect ); # end of MOTD
$irc_conn->add_handler( topic => &irc_on_topic );
$irc_conn->add_handler( namreply => &irc_on_names );
$irc_conn->add_handler(
chanoprivsneeded => sub {
logmsg "Reauthing to nickserv";
$irc_conn->privmsg( "nickserv", "identify $irc_pass" );
}
);
# connect to the database
logmsg "Connecting dbi://$db_user@$db_name";
my">
my $dbh = DBI
->connect( "DBI:mysql:database=$db_name", $db_user, $db_pass )
or die "Can't connect to the database!";
logmsg "Preparing SQL statements";
$sql{lookup_line} = $dbh->prepare(
"select id from `lines`
where msg = ?
limit 1"
);
$sql{add_line} = $dbh->prepare(
"insert into `lines` (msg)
values (?)"
);
$sql{lookup_user} = $dbh->prepare(
"select timeout_power, banned_until from users
where mask = ?
limit 1"
);
$sql{lookup_mask} = $dbh->prepare(
"select mask
from users
where nick = ?
order by last_talk desc
limit 1"
);
$sql{update_user} = $dbh->prepare(
"update users
set timeout_power = timeout_power + 2,
banned_until = ?,
nick = ?,
total_bans = total_bans + 1
where mask = ?
limit 1"
);
$sql{add_user} = $dbh->prepare(
"insert into users (banned_until, nick, mask, timeout_power, lines_talked, total_bans)
values (?, ?, ?, ?, 0, 0)"
);
$sql{user_talk} = $dbh->prepare(
"update users
set lines_talked = lines_talked + 1,
word_count = word_count + ? + 1,
last_talk = null
where mask = ?
limit 1"
);
$sql{next_unban} = $dbh->prepare(
"select min(banned_until)
from users
where banned_until > 0"
);
$sql{get_unbans} = $dbh->prepare(
"select nick, mask, id
from users
where banned_until > 0
and banned_until <= ?"
);
$sql{clear_ban} = $dbh->prepare(
"update users
set banned_until = 0
where id = ?"
);
$sql{high_score} = $dbh->prepare(
"select nick, lines_talked/word_count * lines_talked/(total_bans + 1) as score
from users
order by lines_talked/word_count * lines_talked/(total_bans + 1) desc, lines_talked desc
limit 1"
);
logmsg "Setup complete";
return ( $irc, $irc_conn, $dbh );
}
# event handlers
sub irc_on_connect {
#warn "irc_on_connect (@_)n";
my">
my ( $self, $event ) = @_;
logmsg "Connected to IRC, joining $irc_chan";
$self->join($irc_chan);
logmsg "Authenticating";
$self->privmsg( "nickserv", "identify $irc_pass" );
}
sub irc_on_notice {
my">
my ( $self, $event ) = @_;
my">
my ( $nick, $msg ) = ( $event->nick, $event->args );
logmsg "Notice from $nick to " . @{ $event->to }[0];
&fail( $self, $nick, $event->userhost );
}
sub irc_on_msg {
#warn "irc_on_msg (@_)n";
my">
my ( $self, $event ) = @_;
my">
my ( $nick, $msg ) = ( $event->nick, $event->args );
( $msg, @args ) = split ' ', $msg;
logmsg "PRIVMSG $nick: $msg @args";
if ( lc $msg eq
'timeout' ) {
if ( $sql{lookup_mask
}->execute( $args[0] ) > 0 ) {
my">
my ($mask) = $sql{lookup_mask
}->fetchrow_array;
$sql{lookup_mask}->finish;
$timeout = &get_timeout($mask);
}
}
$timeout = &get_timeout( $event->userhost );
}
$timeout = &timeout_to_text( 2**( $timeout + 2 ) );
$self->privmsg( $nick, "Next timeout will be $timeout" );
}
$self->privmsg( $nick, "No timeout found" );
}
}
{
logmsg "AUTH $nick: $msg";
$self->privmsg( $nick, "Quitting" );
}
elsif ( $msg =~ /^unban (S+)/ ) {
logmsg "Unbanning $1 by command";
$self->mode( $irc_chan, "-b", $1 );
}
elsif ( $msg =~ /^kick (S+)( .*)?/ ) {
logmsg "Kicking $1 by command";
$self->kick( $irc_chan, $1, $2 ? $2 : "Kick" );
}
elsif ( $msg =~ /^fail ([^!]+)!(S+)/ ) {
logmsg "Failing $1!$2 by command";
&fail( $self, $1, $2 );
}
elsif ( $msg =~ /^fail (S+)/ ) {
if ( $sql{lookup_mask
}->execute($1) > 0 ) {
my">
my ($mask) = $sql{lookup_mask
}->fetchrow_array;
$sql{lookup_mask}->finish;
logmsg "Failing $1!$mask by command";
&fail( $self, $1, $mask );
}
logmsg "Couldn't find mask for $1";
}
}
$self->privmsg( $nick, "Huh?" );
}
}
}
# public msg - someone talking in chat
sub irc_on_public {
#warn "irc_on_public (@_)n";
my">
my ( $self, $event ) = @_;
my">
my ( $nick, $userhost ) = ( $event->nick, $event->userhost );
my">
my ($msg) = ( $event->args );
logmsg "$nick: $msg";
# process the message so that we strip them down
# remove case
# remove addressing nicks:
$msg =~ s/^S+: ?//;
# remove any nicks referred to
$msg =~ s/$nick_re/ /g;
# remove punct
$msg =~ s/[^p{L}d -]+//g;
# removing leading/trailing/multiple spaces
$msg =~ s/^s+|s+$//g;
$msg =~ s/ss+/ /g;
# check if the line was already in the DB
my">
my $res = $sql{lookup_line
}->execute($msg);
# kick!
&fail( $self, $nick, $userhost );
}
# add it as a new line
$sql{add_line}->execute($msg);
my">
my $words = ( $msg =~ tr/ / / );
if ( $sql{user_talk
}->execute( $words, $userhost ) == 0 ) {
$sql{add_user}->execute( 0, $nick, $userhost, 1 );
$sql{user_talk}->execute( $words, $userhost );
}
}
$sql{lookup_line}->finish;
}
sub get_timeout {
$sql{lookup_user}->execute($mask);
my">
my ($timeout) = $sql{lookup_user
}->fetchrow_array;
$sql{lookup_user}->finish;
}
sub timeout_to_text {
my">
my ( $dd, $dh, $dm, $ds ) = Normalize_DHMS
( 0, 0, 0, $timeout );
$delta_text .
= "$dd day" .
( $dd == 1 ? " " : "s " ) if $dd;
$delta_text .
= "$dh hour" .
( $dh == 1 ? " " : "s " ) if $dh;
$delta_text .
= "$dm minute" .
( $dm == 1 ? " " : "s " ) if $dm;
$delta_text .
= "$ds second" .
( $ds == 1 ? " " : "s " ) if $ds;
$delta_text =~ s/ $//;
}
# fail - silence for 2**2n
sub fail {
my">
my ( $self, $nick, $userhost ) = @_;
logmsg "Failing $nick ($userhost)";
# look up the last timeout value for this userhost, default is 1
my">
my $timeout = &get_timeout
($userhost);
$timeout += 2;
my">
my $delta_text = &timeout_to_text
( 2**$timeout );
$self->notice( $irc_chan, "$nick, you have been muted for $delta_text." );
$self->mode( $irc_chan, "-v", $nick );
my">
my $target = time + 2**$timeout;
if ( $sql{update_user
}->execute( $target, $nick, $userhost ) == 0 ) {
$sql{add_user}->execute( $target, $nick, $userhost, 2 );
}
logmsg "Silenced for " . ( 2**$timeout ) . " seconds";
if ( not $next_unban or $target < $next_unban ) {
$next_unban = $target;
}
}
sub kick {
#warn "kick (@_)n";
my">
my ( $self, $nick, $userhost, $msg ) = @_;
&fail( $self, $nick, $userhost );
$msg ||= "Go away";
logmsg "Kicking $nick ($userhost): $msg";
$self->kick( $irc_chan, $nick, $msg );
}
sub load_log {
# http://isomerica.net/~xkcd/#xkcd.log
# 20:50 <@zigdon> oh, right, he can't actually kick you
# 20:56 * zigdon tests
#
s/^[^>]+> //
s/^s+|s+$//g;
s/ss+/ /g;
# remove addressing nicks:
s/^S+: ?//;
# remove punct
s/[^p{L} -]+//g;
# remove case
my">
my $res = $sql{lookup_line
}->execute($msg);
$sql{add_line}->execute($msg);
}
}
sub update_nick_re {
$nick_re = "moderator";
$nick_re = qr/s*(?:$nick_re)s*/;
# logmsg "Nick_re = $nick_re";
}
sub irc_on_nick {
my">
my ( $self, $event ) = @_;
my">
my ( $oldnick, $newnick, $userhost ) =
( lc $event->nick, $event->args, $event->userhost );
$nicks{$newnick} = $nicks{$oldnick};
# if someone changes nicks too often (more than 3 times in a maint period), that's a fail
if ( exists $nick_changes{$userhost} ) {
if ( $nick_changes{$userhost}[1]++ > 2 ) {
&fail( $self, $newnick, $userhost );
}
elsif ( $nick_changes{$userhost}[1] > 5 ) {
&kick( $self, $newnick );
}
}
$nick_changes{$userhost} = [ time, 1 ];
}
logmsg
"$oldnick is now known as $newnick ($nick_changes{$userhost}[1] since ",
&update_nick_re;
}
sub irc_on_joinpart {
my">
my ( $self, $event ) = @_;
my">
my ($nick) = lc $event->nick;
if ( $event->{type
} eq
'join' ) {
$nicks{$nick} = 1;
$action = "joined";
# if this is a new user, give them voice after a minute (disabled)
# if it's an existing user, and they're not currently banned, give them
# voice immediately
if ( $sql{lookup_user
}->execute( $event->userhost ) > 0 or 1 ) {
my">
my ( $power, $ban ) = $sql{lookup_user
}->fetchrow_array;
$sql{lookup_user}->finish;
$irc_conn->mode( $irc_chan, "+v", $nick );
}
}
$sql{add_user
}->execute( time + 60, $nick, $event->userhost, 1 );
}
$irc_conn->privmsg( $nick,
"Welcome to #xkcd-signal. Give me a minute, and I'll set you up with a voice. In the meantime, you might want to read the FAQ: http://www.xkcdb.com/signalfaq"
);
}
}
$action = "left";
}
logmsg "$nick has $action the channel";
&update_nick_re;
}
sub irc_on_names {
my">
my ( $self, $event ) = @_;
my">
my ( $nick, $mynick ) = ( $event->nick, $self->nick );
my">
my ($names) = ( $event->args )[3];
print "Event: $_[1]->{type}n";
print DEBUG Dumper
[ @_[ 1 .. $
#_ ] ];
%nicks =
( %nicks, map { s/^(W)//; ( $_ => $1 ? $1 : 1 ) } split ' ', $names );