#!/usr/bin/env perl # iBot version 1.8, copyright Marko Vihoma. # iBot, an IRC-bot which comments on something said or informs on current # tv-shows, current weather in selected city and has a top 5 Yahoo! search. # Yahoo! search currently disabled, as the old API no more works. # Tv-shows is also currently broken and only gives an error message. # iBot now also keeps track if an HTTP URL has been seen by # it before and logs it to a file as well as gloats on old URLs. use strict; use warnings; ### Configuration variables ## Basic variables for bot creation my $server = "irc.blinkenshell.org"; my $port = "6697"; my @channels = [ "#iblink" ]; my $nick = "TestiBot"; my $username = "TestiBot"; my $name = "Sohron's iBot for testing"; my $charset = "iso-8859-15"; my $ssl = 1; ## Put the below configurations to a separate configuration file and use ## Config::Tiny when i have the strength :p ## How many Yahoo! search results my $yahoo_results = 5; ## How many messages to sleep at the first keyword my $sleep = 0; ## Shall we taint the user pasting an old url my $urlcheck_taint = 1; ## Files configuration # Log files directory my $logdir = "$ENV{'HOME'}/log"; # Configuration files directory my $etcdir = "$ENV{'HOME'}/etc/iBot"; # Where to log my $urllog = "${logdir}/iBot_urls.log"; # Production URL log #my $urllog = "${logdir}/iBotDev_urls.log"; # For testing ## Where is Yahoo! application ID my $yid_file = "${etcdir}/iBot_yahoo_app_ID.txt"; ## Operators and bot controllers # The creator ;) has total control over iBot (reload at the moment) my $creator = "Sohron"; # Creators password file my $cpw_file = "${etcdir}/creator_password.txt"; # Ops hash, who have command over channels my %ops; ## #iblink configuration # #iblink file $ops{"#iblink"}->[0] = "${etcdir}/iblink_ops.txt"; ## Flood control # Flood timeout in seconds my $flood_time = 10; # Flood count (min $flood_count and max $flood_count*2 messages allowed in # $flood_time seconds) my $flood_count = 15; ### Configuration END my $starttime = time(); # Create a new iBot object my $bot = iBot->new( server => $server, port => $port, channels => @channels, nick => $nick, username => $username, name => $name, ircname => $name, charset => $charset, ssl => $ssl, ops => \%ops, ); # Run the bot $bot->run(); # The bot class package iBot; use base qw(Bot::BasicBot::Op); use LWP::UserAgent; use Encode; use POSIX; # Version our $VERSION = "1.8"; # How many said()-events by a nick in $flood_time seconds. my $said; # Yahoo Application ID my $yid; # Creators password my $cpassword; # UserAgent string my $agent = "iBot/$VERSION"; sub init { my $self = shift; # If return value is true init is succesful, and new succeeds my $ret = 1; my $errmsg = "Couldn't open file"; # Get the Yahoo! Application ID from file my $yidfh; open $yidfh, "<", $yid_file || ($ret = 0 && print STDERR "$errmsg $yid_file: $!\n"); chomp($yid = <$yidfh>); close $yidfh; # Get $creator's password from file my $cpwfh; open $cpwfh, "<", $cpw_file || ($ret = 0 && print STDERR "$errmsg $cpw_file: $!\n"); chomp($cpassword = <$cpwfh>); close $cpwfh; # Get ops from files for my $channel (keys %ops) { # Read ops, die at failure my $chfh; my $file = $ops{$channel}->[0]; open $chfh, "<", $file || ($ret = 0 && print STDERR "$errmsg $file: $!\n"); chomp(my @array = <$chfh>); # $ops{$channel}->[1] will contain a ref to an anonymous array containing # all the ops on channel $channel $ops{$channel}->[1] = [ @array ]; close $chfh; $chfh = undef; } return $ret; } sub said { my $self = shift; my $message = shift; my $reply = undef; my $body = $message->{body}; my $channel = $message->{channel}; my $server = $self->{server}; my $who = $message->{who}; my $say = undef; my $num = int(rand(4)); # Check for flooding on local server if (($server eq "irc.blinkenshell.org" or $server eq "localhost") and $channel ne "msg") { $said->{$who}->[0] = time unless(exists $said->{$who}); $said->{$who} = $self->flood($channel, $who, $said->{$who}); } # if receiving a privmsg there may be coming something special if ($channel eq "msg") { # Reload configurations (ops at the moment) if ($body =~ /^reload .+$/) { $body =~ s/^reload (.+)/$1/; $self->reload($who, $body); } } # Check if HTTP URL has already been posted and comment accodringly $self->forkit(run => \&urlcheck, arguments => [$who, $body, $channel], body => $body, channel => $channel); # Return iBot version if ($body =~ /^!version/) { $reply = version(); } # Check for current weather in a selected city elsif ($body =~ /^\!temp/) { $body =~ s/temp (.+)/$1/; $self->forkit(run => \&temp, who => $who, channel => 'msg', arguments => [$body]); } # Search Yahoo! elsif ($body =~ /^\!yahoo/) { # Does /x hurt in a search query? $body =~ s/^\!yahoo (.+)$/$1/; $self->forkit(run => \&yahoo, who => $who, channel => 'msg', arguments => [$body]); } # Get bot uptime elsif ($body =~ /^\!uptime$/x) { $reply = uptime(); } # Get bot boxes uptime elsif ($body =~ /^\!server_uptime$/x) { $reply = server_uptime(); } # Get bot boxes `uname -a` elsif ($body =~ /^\!uname$/x) { $reply = server_version(); } # Give help :p elsif ($body =~ /^\!help$/x) { $reply = help(); } # Only comment on keywords if no !-command in first elsif ($body !~ /^!yahoo|^!temp|^!uptime|^!server_uptime|^!uname|^!help|^!version/) { # Check for some keywords to comment on if ($body =~ /tits|boobs/i) { $reply = 'Oh I love "tits" :D' if ($num == 0); $reply = "Gimme some of that tittie!" if ($num == 1); $reply = "Do You have tits?" if ($num == 2); $reply = "What do tits look like?" if ($num == 3); } elsif ($body =~ /openbsd/i) { $reply = "I'm running on OpenBSD too!" if ($num == 0); $reply = "OpenBSD is tougher than OS X :p" if ($num == 1); $reply = "Shit! OpenBSD is tough..." if ($num == 2); $reply = "http://www.openbsd.org/" if ($num == 3); } elsif ($body =~ /pussy|cunt|vagina/i) { $reply = "Show me some pussy!" if ($num == 0); $reply = "I like pussy!" if ($num == 1); $reply = "I haven't come from $creator\'s vagina :p" if ($num == 2); $reply = "Some pussy for you and me :D" if ($num == 3); } elsif ($body =~ /booze|alcohol/i) { $reply = "Drink booze and everything will get better ;)" if ($num == 0); $reply = "Booze to the machine!" if ($num == 1); $reply = "Do you have any alcohol?" if ($num == 2); $reply = "Gimme some booze!" if ($num == 3); } elsif ($body =~ /beer/i) { $reply = "Beer to the machine!" if ($num == 0); $reply = "Do you have any beer?" if ($num == 1); $reply = "Pass me that blottle!" if ($num == 2); $reply = "have you ever drunk beer?" if ($num == 3); } elsif ($body =~ /apple/i) { $reply = "My boss loves baked apples!" if ($num == 0); $reply = "My boss is allergic to fresh apples :(" if ($num == 1); $reply = "Apple is evil, but it still makes great products." if ($num == 2); $reply = "Fuck Apple and all it's friends, whoever that is!" if ($num == 3); } elsif ($body =~ /os x/i) { $reply = "My boss loves OS X :D" if ($num == 0); $reply = "OS X, the _best_ UNIX workstation OS!" if ($num == 1); $reply = "My boss runs OS X on his MBP :)" if ($num == 2); $reply = "Oh, fuck it, just run Ubuntu you geek :p" if ($num == 3); } elsif ($body =~ /fist/i) { $reply = "$who: You want me to fist fuck you?" if ($num == 0); $reply = "Fist fight!" if ($num == 1); $reply = "Fisting looks sick :p" if ($num == 2); $reply = "I don't have fists :'(" if ($num == 3); } # Check for now playing tv-shows if iBot is addressed and said the # right keyword (yte = Yle Teema, nep = Jim). # Disabled because YLE's HTML has chenged... #if (not $body =~ /^!yahoo|^!temp|^!reload/) { #$self->forkit(run => \&tvcheck, who => $who, channel => 'msg', # arguments => ["tv1"]) if ($body =~ /tv1/i); #$self->forkit(run => \&tvcheck, who => $who, channel => 'msg', # arguments => ["tv2"]) if ($body =~ /tv2/i); #$self->forkit(run => \&tvcheck, who => $who, channel => 'msg', # arguments => ["mtv"]) if ($body =~ /mtv/i); #$self->forkit(run => \&tvcheck, who => $who, channel => 'msg', # arguments =>["nel"]) if ($body =~ /nel/i); #$self->forkit(run => \&tvcheck, who => $who, channel => 'msg', # arguments => ["sub"]) if ($body =~ /sub/i); #$self->forkit(run => \&tvcheck, who => $who, channel => 'msg', # arguments => ["yte"]) if ($body =~ /yte|teema/i); #$self->forkit(run =>\&tvcheck, who => $who, channel => 'msg', # arguments => ["nep"]) if ($body =~ /jim/i); #if ($body =~ /^\!all/i) { # for my $tv ("tv1", "tv2", "yte", "mtv", "nel", "nep", "sub") { # $self->forkit(run => \&tvcheck, who => $who, channel => 'msg', # arguments => [$tv]); # } } #$reply = undef if ($self->tick() || defined $say); #$sleep = 20 if ($reply); return $reply; } sub chanjoin { my $self = shift; my $message = shift; my $reply = undef; my $who = $message->{who}; my $channel = $message->{channel}; my $server = $self->{server}; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); $mon++; $mon = "0$mon" if ($mon =~ /^\d$/x); $min = "0$min" if ($min =~ /^\d$/x); $mday = "0$mday" if ($mday =~ /^\d$/x); $year += 1900; # Give ops for my $opch (keys %ops) { if ($channel eq $opch) { my @array = @{ $ops{$opch}->[1] }; for my $op (@array) { if ($who eq $op) { $self->mode($channel, '+o', $who); } } } } # Join statistics if ($channel eq "#iblink") { $self->log("\nJoin $channel: ${year}${mon}${mday} ${hour}:${min} $who\n"); } #return 1; } sub chanpart { my $self = shift; my $message = shift; my $reply = undef; $reply = "The master is gone, everyone cry!" if ($message->{who} eq $creator); return $reply; } sub kicked { my $self = shift; my $message = shift; my $reply = "hahhaa " . $message->{who} . " got kicked :p"; return $reply; } sub help { my $reply = "Say \"!temp <city>\" then you'll get the weather of that" . " city. Say \"!uptime\", and i'll tell you my uptime, " . "\"!server_uptime\", and i'll tell you the uptime of the box i'm living " . "on or \"!uname\", and i'll tell you my boxes uname -a output. " . "Also, there's some keywords to which i'll respond which you don't " . "know ;)"; return $reply; } sub tick { if ($sleep) { return --$sleep; } return 0; } sub reload { my ($self, $sender, $password) = @_; # Only $creator can initiate reload for now if ($sender eq $creator && $password eq $cpassword) { my %changed; for my $opch (keys %ops) { my $file = $ops{$opch}[0]; my $opfh; if (open $opfh, "<", $file) { chomp(my @array = <$opfh>); $ops{$opch}->[1] = [ @array ]; close $opfh; my $count = scalar @array; $changed{$opch} = "${opch}: $count ops loaded"; } else { $changed{$opch} = "${opch}: Couldn't open file $file: $!"; } } # Return status for each channel for my $opch (keys %changed) { my $reply = $changed{$opch}; $self->say(who => $sender, channel => "msg", body => $reply); return 1; } } else { # Return that $sender is not authorized to initiate config reload my $reply = "$sender, you're not authorized to reload settings >:|"; $self->say(who => $sender, channel => "msg", body => $reply); return 0; } return 1; } sub uptime { my $time = time(); my $uptime = $time - $starttime; my $days = int($uptime / 86400); if ($days < 1) { $days = 0 }; my $hours = (($uptime - $days * 86400) / 3600) % 24; if ($hours < 1) { $hours = 0 }; my $minutes = (($uptime - $days * 86400 - $hours * 3600) / 60) % 60; if ($minutes < 1) { $minutes = 0 }; my $seconds = ($uptime - $days * 86400 - $hours * 3600 - $minutes * 60) % 60; if ($hours < 10) { $hours = "0$hours" }; if ($minutes < 10) { $minutes = "0$minutes" }; if ($seconds < 10) { $seconds = "0$seconds" }; return "up " . $days . " days, " . $hours . ":" . $minutes . ":" . $seconds; } sub server_uptime { my $uptime = qx/uptime/; return $uptime; } sub server_version { my @uname = POSIX::uname; my $uname = ""; for (@uname) { $uname .= "$_ "; } return $uname; } sub tvcheck { shift; # Temporarily not available, as Yle's HTML has changed. print "This service temporarily not available :("; return 1; ## my $ch = shift; my $content; my $err = "${ch}: couldn't get the show :(\n"; print $err and return 0 if (not $ch); # Create a new LWP useragent my $ua = LWP::UserAgent->new(); # Set UserAgent $ua->agent($agent); # Create a request my $req = HTTP::Request->new( GET => 'http://ohjelmaopas.yle.fi/mediaoppaat/tv-opas?groups=' . $ch ); # Pass request to the user agent and get a response back my $res = $ua->request($req); # Check the outcome of the response if ($res->is_success) { $content = $res->content; } else { #print "Yhteysvirhe!\n"; # DEBUG print print $err; return 0; } # Check $content for a line matching current shows div class tag if ($content =~ /onair/) { #print "Jottain on menossa!\n"; # DEBUG print $content =~ s/^.+?onair.+?class="programmelink".+?>(.+?)<\/a><\/div><br \/>.*$/$1/sx; print $err and return 0 if ($content =~ /^[\s]*$/x); } else { #print "Ei mätsänny!\n"; # DEBUG print print "${ch}: nada.\n"; return 0; } $content = Encode::encode("iso-8859-15", Encode::decode_utf8($content)); print "${ch}: $content\n"; return 1; } sub version { return "iBot/$VERSION\n"; } sub temp { shift; my $city = shift; my $content; my $int_err = "Couldn't get temperature report :(\n"; my $inf_err = "The response was malformed"; my $empty_err = "The response was empty"; my $na = "not available"; # Create a new useragent my $ua = LWP::UserAgent->new(); $ua->agent($agent); # Create a request my $req = HTTP::Request->new( POST => 'http://m.wund.com/' . 'cgi-bin/findweather/getForecast?brand=mobile&query=' . $city ); $req->content_type('application/x-www-form-urlencoded'); # Pass request to the user agent and get a response back my $res = $ua->request($req); # DEBUG print #print $res->content; # Check the outcome of the response if ($res->is_success) { $content = $res->content; } else { print $int_err; return 0; } my $cur_temp = my $windchill = my $sky = $content; $cur_temp =~ s/^.+?Temperature.+?<b>(-?\d+)<\/b>°C.+$/$1°C/ms; if ($windchill =~ /Windchill/) { $windchill =~ s/^.+?Windchill.+?<b>(-?\d+)<\/b>°C.+$/$1°C/ms; } else { $windchill = $na; } if ($sky =~ />Conditions</) { $sky =~ s/^.+?Conditions.+?<b>([\w\s]+)<\/b>.+$/$1/ms; } else { $sky = $na; } if ($cur_temp =~ /^[\s]$/) { print "${empty_err}\n"; return 0; } elsif ($cur_temp !~ /^-?\d+°C$/ || ($windchill !~ /^-?\d+°C$/ && $windchill !~ /^$na$/) || ($sky !~ /^[\w\s]+$/ && $sky !~ /^$na$/) ) { print "${inf_err}\n"; return 0; } else { print "${city}\nTemperature: ${cur_temp}\n"; print "Windchill: ${windchill}\n"; print "Sky: ${sky}\n"; print "\n"; return 1; } } #sub temp { # shift; # my $city = shift; # my $content; # my $err = "Couldn't get the weather :(\n"; # print $err and return 0 if (not $city); # # # Create a new useragent # my $ua = LWP::UserAgent->new(); # $ua->agent($agent); # # # Create a request # my $req = HTTP::Request->new( # POST => 'http://m.wund.com/' . # 'cgi-bin/findweather/getForecast?brand=mobile&query=' . $city # ); # # $req->content_type('application/x-www-form-urlencoded'); # # # Pass request to the user agent and get a response back # my $res = $ua->request($req); # # # Check the outcome of the response # if ($res->is_success) { # $content = $res->content; # } # else { # print $err; # return 0; # } # # $content =~ s/^.+?Temperature.+?<b>(-?\d+)<\/b>°C.+$/$1°C/msx; # # if ($content =~ /^[\s]$/x) { # print "The response was empty :(\n"; # return 0; # } # elsif (!($content =~ /^-?\d+°C$/x)) { # print "The response was malformed :(\n"; # return 0; # } # else { # print "${city}: $content\n"; # return 1; # } #} sub flood { my ($self, $channel, $who, $times) = @_; if (exists $times->[0]) { if (@$times >= $flood_count) { my $secs = $times->[$#$times] - $times->[0]; if ($secs < $flood_time) { # DEBUG print #print STDERR "$who flooded in $secs seconds.\n"; # DEBUG END $self->kick($channel, $who, "No flooding!"); } my @tmp = (time); my $reply = \@tmp; return $reply; } else { # DEBUG #my $n = @$times; #print STDERR "$who said something $n times.\n"; # DEBUG END push(@$times, time); return $times; } } } sub yahoo { shift; print "Service temporarily unavailable.\n" and return 0; my $query = shift; my @content; my $err = "Couldn't search :(\n"; my $noq = "Please give a search term."; print $noq and return 0 if not $query; my @res; my $reply; my $ua = LWP::UserAgent->new(); $ua->agent($agent); my $response = $ua->get( "http://search.yahooapis.com/WebSearchService/V1/webSearch?appid=" . "${yid}&query=${query}&results=${yahoo_results}" ); # DEBUG print print $response->status_line . "\n"; # DEBUG end if ($response->is_success) { my $content = $response->content; @content = split("\n", $content); } else { print $err; return 0; } my $n = 0; for my $line (@content) { print "Sorry, but the search limit has been reached, try again in " . "a while.\n" and return 0 if ($line =~ /^<Error/x); if ($line =~ /<Result><Title>/x) { $line =~ s/^.+?<Title>(.+?)<\/Title>.+?<Url>(.+?)<\/Url>.*$/$1 <$2>/x; $res[$n++] = $line; } } if (@res) { $reply = "1. $res[0]\n"; for my $n (1..4) { $reply .= $n++ . ". $res[$n]\n"; } print $reply; return 1; } else { print $err; return 0; } } sub urlcheck { shift; my $who = shift; my $arg = shift; my $nch = shift; my $now = time; my $reply = undef; my $line; my $url; my $time; my $channel; my $poster; my $date; my $is_url = 0; $is_url = 1 if ($arg =~ /http[s]?:\/\//x); if ($is_url) { $arg =~ s/^.*?(http[s]?:\/\/\S+).*$/$1/x; my $urlfh; open $urlfh, "<", $urllog or Bot::BasicBot::Op::log("Couldn't open file $urllog: $!"); while (<$urlfh>) { ($url, $time, $channel, $poster) = split(/\|\|\|/x, $_); chomp $poster; if (($arg eq $url) and ($nch eq $channel)) { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time); $mon++; $year += 1900; $date = "$mday. $mon. $year"; if ($urlcheck_taint) { print "haha, old! $arg first time pasted by " . "$poster $date :p\n"; } close $urlfh; $urlfh = undef; return 1; } } close $urlfh if (defined $urlfh); if (open $urlfh, ">>", $urllog) { print $urlfh "${arg}|||${now}|||${nch}|||${who}\n"; close $urlfh; return 1; } else { Bot::BasicBot::Op::log("Couldn't open file $urllog: $!"); return 0; } } } # Class END
iBot - simple IRC bot class (this is in fact an enhanced Lokk<iBot for BlinkenIRC's #iblink channel)
use base qw(Bot::BasicBot::Op);
use iBot;
# with all defaults
my $bot = iBot->new( channels => ["#bottest"] );
$bot->run();
# with all known options
my $bot = iBot->new(
server => "irc.example.com",
port => "6667",
channels => ["#bottest"],
nick => "basicbot",
alt_nicks => ["bbot", "simplebot"],
username => "bot",
name => "Yet Another Bot",
ignore_list => [qw(dipsy dadadodo laotse)],
charset => "utf-8", # charset the bot assumes the channel is using
ops => \%ops, # operators hash
);
$bot->run();
iBot uses a modified Bot::BasicBot module (Bot::BasicBot::Op) to import useful methods. It monitors for said events on connected channels and responds as wanted. iBot also has basic opering abilities (/mode +o and /kick).
Initiate $yid, $cpassword and $ops{$channel}[1] from files.
=cut
said($mess)Return an aswer for certain keywords or wait 20 messages if reply has just been sent on a channel (no sleep for privmsg). Now uses Bot::BasicBot::Op's forkit method for non-blocking tv-show, temperature and Yahoo! search queries.
chanjoin($mess)React if someone joins a channel.
chanpart($mess)Send a message if $creator parts a channel (disabled by default).
kicked($mess)Send a gloating message if someone has been kicked.
help()Send a help message if someone says "!help".
tick()The sleep/wait counter.
reload($sender)Reloads configuration files (only op files at the moment).
=cut
uptime()Returns bots uptime =cut
server_uptime()Returns the uptime of the box the bot is running on.
server_version()Returns the output of 'uname -a' on the box the bot is running on.
said() through $self->forkit().
Check for whats playing on a tv-channel $ch from http://ohjelmaopas.yle.fi/. Currently disabled, as YLE's HTML has changed and i'm not in the mood for fixing it now (maybe some sort of HTML-/XML-parser in the future)...
Returns the bot's name/version.
Report the current temperature, windchill and sky for city $city from http://www.wunderground.com/.
Check channel $channel if user $who is flooding by checking if array ref @times' last indexes timestamp is less than 11seconds after first indexes timestamp and if there have been at least $flood_count said() events in 10s.. Kick user $who if it is flooding. This isn't working just as i wanted, but maybe I'll learn eventually ;)
Ask Yahoo! search for 5 hits for terms $query. Requires a Yahoo! Application ID (needs registration, see http://developer.yahoo.com/faq/index.html#appid). This service is also disabled for now, the old search API has been dropped, so should start using YQL if possible.
Check if $arg contains an HTTP URL and if so, check if it has been posted earlier. If URL has been posted earlier, send a gloating message.
iBot class and iBot.pl by Marko Vihoma <lokki1977@gmail.com>
Bot::BasicBot is written by Tom Insam and the initial version by Mark Fowler. Bot::BasicBot::Op is modified from Bot::BasicBot adding basic opering methods by Marko Vihoma.
iBot requires Bot::BasicBot::Op and LWP to run. Bot::BasicBot::Op is based on Bot::BasicBot, POE and POE::Component::IRC.
tvcheck, temp and yahoo search use regular expressions to parse the HTML/XML
response so they may break if HTML/XML input changes.
tvcheck() and yahoo() are currently broken and disabled (just gives an error
message).
Bot::BasicBot::Op, Bot::BasicBot and its examples, POE, POE::Component::IRC