#!/usr/bin/perl -w use strict; use feature "state"; use Data::Dumper; use Storable; use DateTime; use Switch; use Time::Format; use WebService::GData::YouTube; use LWP::Simple; use Data::Validate::URI qw(is_web_uri); use POE; use POE::Component::IRC; use POE::Component::IRC::Plugin::Console; use POE::Component::IRC::Plugin::NickServID; use POE::Component::IRC::Plugin::BotAddressed; use POE::Component::IRC::Plugin::Logger; use WWW::Wikipedia; use WWW::YouTube::Info; use Google::Search; use DBI; my $dbh = DBI->connect("dbi:SQLite:dbname=raven.db","","") or die $DBI::errstr; my @tables = $dbh->tables(); my @help = ( ['.8ball','Predict the future with yes/no questions. - ex. .8ball Is Raven awesome?'], ['.tell','Leave a message for a user - ex. .tell user The server is down again.'], ['.google','Search Google and return the first result'], ['.youtube','Search YouTube and return the first result'], ['.wiki','Search for a wiki on the selected topic'], ['.down','Check if a website is down.'], ['.quote','Record or play back a quote. ".quote" random quote from any user, ".quote user" random quote for the user, ".quote user Message to be quoted" save a new quote'], ['.info','Record or play back stored info'], ['.define','Look up a definition of a word'], ['.urban','Search the Urban Dictionary'], ['.devbukkit','Search dev.bukkit.org for plugins'], ['.seen','Show last activity for a user'], ); my @ophelp = ( ['join','Join a channel'], ['part','Leave a channel'], ['op','Add an op'], ['deop','Remove an op'], ['speakas','Make the bot say something in a channel or to a user - ex. /msg RavenBot #MyChannel What are you doing, Dave?'], ['emoteas','Make the bot perform a CTCP ACTION [/me] in a channel or to a user chat'], ['shutdown','Terminate the bot'], ); my $URLValidator = Data::Validate::URI->new(); # Create tables (if they don't exist) $dbh->do("CREATE TABLE IF NOT EXISTS Channels(name)"); $dbh->do("CREATE TABLE IF NOT EXISTS Ops(name)"); $dbh->do("CREATE TABLE IF NOT EXISTS Tells(sender, rcpt, channel, message)"); $dbh->do("CREATE TABLE IF NOT EXISTS Info(key PRIMARY KEY, data)"); $dbh->do("CREATE TABLE IF NOT EXISTS Quotes(user, data)"); $dbh->do("CREATE TABLE IF NOT EXISTS LastSeen(user PRIMARY KEY, timestamp)"); # Prepare Database statements my $addQuote = $dbh->prepare("INSERT INTO Quotes(user, data) VALUES (?, ?);"); my $getUserQuote = $dbh->prepare("SELECT data FROM Quotes WHERE user = ? ORDER BY RANDOM () LIMIT 1;"); my $getAnyQuote = $dbh->prepare("SELECT user, data FROM Quotes ORDER BY RANDOM () LIMIT 1;"); my $updateLastSeen = $dbh->prepare("REPLACE INTO LastSeen(user, timestamp) VALUES (?, ?);"); my $getLastSeen = $dbh->prepare("SELECT timestamp FROM LastSeen WHERE user = ?;"); my $updateInfo = $dbh->prepare("REPLACE INTO Info(key, data) VALUES (?, ?);"); my $getInfo = $dbh->prepare("SELECT data FROM Info WHERE key = ?;"); my $addTell = $dbh->prepare("INSERT INTO Tells(sender, rcpt, channel, message) VALUES (?, ?, ?, ?);"); my $getTells = $dbh->prepare("SELECT rowid, sender, channel, message FROM Tells WHERE rcpt = ?;"); my $removeTells = $dbh->prepare("DELETE FROM Tells WHERE rcpt = ?;"); my $addChannel = $dbh->prepare("REPLACE INTO Channels (name) VALUES (?);"); my $removeChannel = $dbh->prepare("DELETE FROM Channels WHERE name = ?;"); my $addOp = $dbh->prepare("REPLACE INTO Ops (name) VALUES (?);"); my $removeOp = $dbh->prepare("DELETE FROM Ops WHERE name = ?;"); # Finished setup # Color contants my $WHITE = "\x0300"; my $BLACK = "\x0301"; my $BLUE = "\x0302"; my $GREEN = "\x0303"; my $RED = "\x0304"; my $BROWN = "\x0305"; my $PURPLE = "\x0306"; my $ORANGE = "\x0307"; my $YELLOW = "\x0308"; my $LIGHT_GREEN = "\x0309"; my $TEAL = "\x0310"; my $LIGHT_CYAN = "\x0311"; my $LIGHT_BLUE = "\x0312"; my $PINK = "\x0313"; my $GREY = "\x0314"; my $LIGHT_GREY = "\x0315"; my $NORMAL = "\x0f"; my @magic8ball = ( qq<$GREEN Signs point to yes.>, qq<$GREEN Yes.>, qq<$YELLOW Reply hazy, try again.>, qq<$GREEN Without a doubt.>, qq<$RED My sources say no.>, qq<$GREEN As I see it, yes.>, qq<$GREEN You may rely on it.>, qq<$YELLOW Concentrate and ask again.>, qq<$RED Outlook not so good.>, qq<$GREEN It is decidedly so.>, qq<$YELLOW Better not tell you now.>, qq<$RED Very doubtful.>, qq<$GREEN Yes - definitely.>, qq<$GREEN It is certain.>, qq<$YELLOW Cannot predict now.>, qq<$GREEN Most likely.>, qq<$YELLOW Ask again later.>, qq<$RED My reply is no.>, qq<$GREEN Outlook good.>, qq<$RED Do not count on it.>, ); my ($irc) = POE::Component::IRC->spawn(); my @channels = &LoadChannels; my @ops = &LoadOps; POE::Session->create( inline_states => { _start => \&bot_start, irc_001 => \&on_connect, irc_public => \&on_public, irc_msg => \&on_private, irc_join => \&on_join, irc_bot_mentioned => \&on_mention, irc_kick => \&on_kick, irc_disconnected => \&on_disconnect, }, ); sub bot_start { $irc->yield(register => "all"); my $nick = "RavenBot"; $irc->yield(connect => { Nick => $nick, Username => 'ravenbot', Ircname => q{MCPortCentral's RavenBot}, Server => 'irc.esper.net', Port => '6667', Flood => 'true', } ); #$irc->plugin_add( 'Console', POE::Component::IRC::Plugin::Console->new( bindport => 1234, password => 'test' )); $irc->plugin_add( 'NickServID', POE::Component::IRC::Plugin::NickServID->new( Password => 'Raven!@)(' ) ); $irc->plugin_add( 'BotAddressed', POE::Component::IRC::Plugin::BotAddressed->new() ); $irc->plugin_add( 'Logger', POE::Component::IRC::Plugin::Logger->new( Path => '/opt/RavenBot/ravenbot/logs', DCC => 0, Private => 1, Public => 1, ) ); } sub on_connect { $irc->yield(join => $_) for @channels; return; } sub on_disconnect { &bot_start; return; } sub on_public { my ($kernel, $who, $where, $msg) = @_[KERNEL, ARG0, ARG1, ARG2]; my $nick = (split /!/, $who)[0]; my $channel = $where->[0]; my $dt = DateTime->now( time_zone=> 'UTC' ); my $ts = $dt->epoch; my $isOp = 0; if ($msg =~ /youtube\.com/) { my @matches = ($msg =~ m/v\=.*/g); my $videoid = substr($matches[0],2,11); my $yt = new WebService::GData::YouTube(); $yt->query->q($videoid)->limit(1,0); my $videos = $yt->search_video(); if (!(defined $videos)) { return } my $length = $$videos[0]->{_feed}{'media$group'}{'yt$duration'}{'seconds'}; my $time = &ConvertFromSeconds($length); my $url = $$videos[0]->{_feed}{link}[0]{href}; $url =~ s/\&feature=youtube_gdata//g; my $uploader = $$videos[0]->{_feed}{'media$group'}{'media$credit'}[0]{'yt$display'}; my $uploadedon = substr($$videos[0]->{_feed}{'media$group'}{'yt$uploaded'}{text}, 0, 10); my $views = &AddCommas($$videos[0]->{_feed}{'yt$statistics'}{viewCount}); eval{ $irc->yield(privmsg => $channel, $nick . qq<: \x02> . $$videos[0]->title . qq<\x02 - length: \x02> . $time . qq<\x02 - > . $url . qq< - \x02> . $views . qq<\x02 views - uploaded by: \x02> . $uploader . qq<\x02 on \x02> . $uploadedon . qq<\x02> ); }; $irc->yield(privmsg => $channel, $nick . qq<: Error while finding information>) if $@; } foreach (@ops) { if ( $nick eq $_ ) { $isOp = 1; } } $getTells->execute($nick); while (my $row = $getTells->fetchrow_hashref){ $irc->yield(notice => $nick, $$row{'sender'} . ' in ' . $$row{'channel'} . '> ' . $$row{'message'}); $dbh->do("DELETE FROM Tells where rowid = " . $$row{'rowid'} . ";"); } $updateLastSeen->execute($nick, $ts); if ($msg =~ /^\./) { my ($command, @cmdargs) = (split / /, $msg); switch ($command) { case '.8ball' { my $response = $magic8ball[rand @magic8ball]; my $question = ""; for (my $i = 0; $i <= $#cmdargs; $i++) { $question .= $cmdargs[$i] . " "; } chop($question); $irc->yield(privmsg => $channel, $nick . q< asked "> . $question . q<" - > . $response); } case /\.t($|ell$)/ { my $numArgs = @cmdargs; if ($numArgs == 0) { $irc->yield(privmsg => $channel, qq<$nick: Who did you want to tell and what?>); return; } if ($numArgs == 1) { $irc->yield(privmsg => $channel, qq<$nick: What did you want to tell that user?>); } else { my $rcpt = $cmdargs[0]; my $msg = ""; for (my $i =1; $i <= $#cmdargs; $i++) { $msg .= $cmdargs[$i] . " "; } chop($msg); $addTell->execute($nick, $rcpt, $channel, $msg); $irc->yield(privmsg => $channel, qq<$nick: \x02$rcpt\x02 will be notified on their next activity.>); } } case /\.g($|oogle$)/ { my $query = ""; for (my $i = 0; $i <= $#cmdargs; $i++) { $query .= $cmdargs[$i] . " "; } chop($query); my $search = Google::Search->Web( query => $query ); eval{ my $result = $search->first; my $content; if (defined $result) { $content = $result->content; } else { $content = "No results found"; } $content =~ s|<.+?>||g; $irc->yield(privmsg => $channel, $nick . q<: > . $result->unescapedUrl . qq< - \x02> . $result->titleNoFormatting . qq<\x02: "> . $content . q<">); }; $irc->yield(privmsg => $channel, $nick . qq<: Error while finding information>) if $@; } case /\.(yt$|youtube$)/ { my $yt = new WebService::GData::YouTube(); my $query = ""; for (my $i = 0; $i <= $#cmdargs; $i++) { $query .= $cmdargs[$i] . " "; } chop($query); eval{ $yt->query->q($query)->limit(1,0); my $videos = $yt->search_video(); my $length = $$videos[0]->{_feed}{'media$group'}{'yt$duration'}{'seconds'}; my $time = &ConvertFromSeconds($length); my $url = $$videos[0]->{_feed}{link}[0]{href}; $url =~ s/\&feature=youtube_gdata//g; my $uploader = $$videos[0]->{_feed}{'media$group'}{'media$credit'}[0]{'yt$display'}; my $uploadedon = substr($$videos[0]->{_feed}{'media$group'}{'yt$uploaded'}{text}, 0, 10); my $views = &AddCommas($$videos[0]->{_feed}{'yt$statistics'}{viewCount}); $irc->yield(privmsg => $channel, $nick . qq<: \x02> . $$videos[0]->title . qq<\x02 - length: \x02> . $time . qq<\x02 - > . $url . qq< - \x02> . $views . qq<\x02 views - uploaded by: \x02> . $uploader . qq<\x02 on \x02> . $uploadedon . qq<\x02> ); }; $irc->yield(privmsg => $channel, $nick . qq<: Error while finding information>) if $@; } case ".wiki" { my $query = "wiki "; for (my $i = 0; $i <= $#cmdargs; $i++) { $query .= $cmdargs[$i] . " "; } chop($query); eval{ my $search = Google::Search->Web( query => $query ); my $result = $search->first; my $content = $result->content; $content =~ s|<.+?>||g; $irc->yield(privmsg => $channel, $nick . q<: > . $result->unescapedUrl . qq< - \x02> . $result->titleNoFormatting . qq<\x02: "> . $content . q<">); }; $irc->yield(privmsg => $channel, $nick . qq<: Error while finding information>) if $@; } case ".down" { my $url = $cmdargs[0]; if ($URLValidator->is_web_uri( $url )) { my $status = ""; if (! head($url) ){ $status = "$url appears to be " . $RED . "down" . $NORMAL . "!"; } else { $status = "$url appears to be " . $GREEN . "up" . $NORMAL . "!"; } $irc->yield(privmsg => $channel, $nick . qq<: $status>); } else { $irc->yield(privmsg => $channel, $nick . qq<: $url does not appear to be a valid URL.>); } } case /\.q($|uote$)/ { my $numArgs = @cmdargs; if ($numArgs == 0) { $getAnyQuote->execute; my $result = $getAnyQuote->fetchrow_hashref; $irc->yield(privmsg => $channel, '<' . $$result{'user'} . '> ' . $$result{'data'}); return; } if ($numArgs == 1) { $getUserQuote->execute($cmdargs[0]); my $result = $getUserQuote->fetch; if (!(defined $result)) { $irc->yield(privmsg => $channel, 'No quotes found for ' . $cmdargs[0] . '.'); return; } $irc->yield(privmsg => $channel, '<' . $cmdargs[0] . '> ' . $$result[0]); } else { my $user = $cmdargs[0]; my $quote = ""; for (my $i =1; $i <= $#cmdargs; $i++) { $quote .= $cmdargs[$i] . " "; } chop($quote); $addQuote->execute($user, $quote); $irc->yield(privmsg => $channel, qq<$nick: Quote added to database.>); } } case /\.i($|nfo$)/ { my $numArgs = @cmdargs; if ($numArgs == 0) { $irc->yield(privmsg => $channel, qq<$nick: No key specified.>); return; } if ($numArgs == 1) { $getInfo->execute($cmdargs[0]); my $result = $getInfo->fetch; $irc->yield(privmsg => $channel, qq<\x02> . $cmdargs[0] . qq<\x02 - > . $$result[0]); } else { my $key = $cmdargs[0]; my $data = ""; for (my $i = 1; $i <= $#cmdargs; $i++) { $data .= $cmdargs[$i] . " "; } chop($data); $updateInfo->execute($key, $data); $irc->yield(privmsg => $channel, qq<$nick: Value set.>); } } case ".define" { my $query = "define:"; for (my $i = 0; $i <= $#cmdargs; $i++) { $query .= $cmdargs[$i] . " "; } chop($query); eval{ my $search = Google::Search->Web( query => $query ); my $result = $search->first; #print Dumper($result); my $content = $result->content; $content =~ s|<.+?>||g; $irc->yield(privmsg => $channel, $nick . q<: > . $result->unescapedUrl . qq< - \x02> . $result->titleNoFormatting . qq<\x02: "> . $content . q<">); }; $irc->yield(privmsg => $channel, $nick . qq<: Error while finding information>) if $@; } case ".urban" { my $query = "site:urbandictionary.com "; for (my $i = 0; $i <= $#cmdargs; $i++) { $query .= $cmdargs[$i] . " "; } chop($query); eval{ my $search = Google::Search->Web( query => $query ); my $result = $search->first; my $content = $result->content; $content =~ s|<.+?>||g; $irc->yield(privmsg => $channel, $nick . q<: > . $result->unescapedUrl . qq< - \x02> . $result->titleNoFormatting . qq<\x02: "> . $content . q<">); }; $irc->yield(privmsg => $channel, $nick . qq<: Error while finding information>) if $@; } case /.(db$|devbukkit$)/ { my $query = "site:dev.bukkit.org "; for (my $i = 0; $i <= $#cmdargs; $i++) { $query .= $cmdargs[$i] . " "; } chop($query); my $search = Google::Search->Web( query => $query ); eval{ my $result = $search->first; my $content; if (defined $result) { $content = $result->content; } else { $content = "No results found"; } $content =~ s|<.+?>||g; $irc->yield(privmsg => $channel, $nick . q<: > . $result->unescapedUrl . qq< - \x02> . $result->titleNoFormatting . qq<\x02: "> . $content . q<">); }; $irc->yield(privmsg => $channel, $nick . qq<: Error while finding information>) if $@; } case ".seen" { my $numArgs = @cmdargs; if ($numArgs >= 1) { my $user = $cmdargs[0]; $getLastSeen->execute($user); my $result = $getLastSeen->fetch; if (!(defined $result)){ $irc->yield(privmsg => $channel, qq<\x02$user\x02 does not have any known activity.>); return; } my $time = $$result[0]; my $lastseen = DateTime->from_epoch ( epoch => $time ); my $lsmsg = $lastseen->day . '-' . $lastseen->month_abbr . '-' . $lastseen->year . ' ' . $lastseen->hms . ' UTC'; my $duration = &ConvertFromSeconds(($ts - $time)); $irc->yield(privmsg => $channel, qq<\x02$user\x02 was last seen $lsmsg ($duration ago).>); } } case /.(brainfuck$|bf$)/ { $irc->yield(privmsg => $channel, qq<\x02$nick\x02 No esoteric programming languages allowed.>); } case /.help$/ { for (my $i = 0; $i <= $#help; $i++) { $irc->yield(notice => $nick, $help[$i][0] . ' - ' . $help[$i][1]); } } } } return; } sub on_private { my ($kernel, $from, $to, $msg, $identified) = @_[KERNEL, ARG0, ARG1, ARG2, ARG3]; my $nick = (split /!/, $from)[0]; my ($command, @cmdargs) = (split / /, $msg); my $isOp = 0; foreach (@ops) { if ( $nick eq $_ ) { $isOp = 1; } } switch ($command) { case 'tables' { if ($isOp == 1) { $irc->yield(privmsg => $nick, @tables); } else { $irc->yield(privmsg => $nick, "You cannot do that."); } } case 'emoteas' { if ($isOp == 1) { my $response; for (my $i = 1; $i <= $#cmdargs; $i++) { $response .= $cmdargs[$i] . " "; } $irc->yield(ctcp => $cmdargs[0], qq); $irc->yield(privmsg => $nick, "Sent!"); } else { $irc->yield(privmsg => $nick, "You cannot do that."); } } case 'speakas' { if ($isOp == 1) { my $response; for (my $i = 1; $i <= $#cmdargs; $i++) { $response .= $cmdargs[$i] . " "; } $irc->yield(privmsg => $cmdargs[0], $response); $irc->yield(privmsg => $nick, "Sent!"); } else { $irc->yield(privmsg => $nick, "You cannot do that."); } } case 'join' { if ($isOp == 1) { $irc->yield(join => $cmdargs[0]); $addChannel->execute($cmdargs[0]); } else { $irc->yield(privmsg => $nick, "You cannot do that."); } } case 'part' { if ($isOp == 1) { $irc->yield(part => $cmdargs[0]); $removeChannel->execute($cmdargs[0]); } else { $irc->yield(privmsg => $nick, "You cannot do that."); } } case 'op' { if ($isOp == 1) { push(@ops, $cmdargs[0]); $addOp->execute($cmdargs[0]); $irc->yield(privmsg => $nick, "You have granted op to $cmdargs[0]."); } else { $irc->yield(privmsg => $nick, "You cannot do that."); } } case 'deop' { if ($isOp == 1) { my $index = &Find($cmdargs[0], @ops); delete $ops[$index]; $removeOp->execute($cmdargs[0]); $irc->yield(privmsg => $nick, "You have removed op from $cmdargs[0]."); } else { $irc->yield(privmsg => $nick, "You cannot do that."); } } case 'shutdown' { if ($isOp == 1) { $irc->yield(quit => "Nevermore!"); $irc->yield(shutdown => ""); } else { $irc->yield(privmsg => $nick, "You cannot do that."); } } case 'help' { for (my $i = 0; $i <= $#ophelp; $i++) { $irc->yield(privmsg => $nick, $ophelp[$i][0] . ' - ' . $ophelp[$i][1]); } } else { $irc->yield(privmsg => $nick, "I don't recognize that."); } } return; } sub on_join { return; } sub on_mention { state $lastCall = 0; my ($kernel, $who, $where, $msg) = @_[KERNEL, ARG0, ARG1, ARG2]; my $nick = (split /!/, $who)[0]; my $channel = $where->[0]; my $ts = DateTime->now( time_zone => 'UTC' ); if ($ts->epoch >= ($lastCall + 60) && !($msg =~ /^[\.<]/)) { $irc->yield(ctcp => $channel, "ACTION senses someone talking about it."); $lastCall = $ts->epoch; } return; } sub on_kick { my ($kernel, $kicker, $where, $kickee, $why) = @_[KERNEL, ARG0, ARG1, ARG2, ARG3]; my $nick = (split /!/, $kickee)[0]; my $channel = $where; $irc->yield(privmsg => $channel, "Meh... I didn't like them anyway. :P"); return; } sub irc_console_connect { my ($peeradr, $peerport, $wheel_id) = @_[ARG0 .. ARG2]; return; } sub irc_console_authed { my $wheel_id = $_[ARG0]; return; } sub irc_console_close { my $wheel_id = $_[ARG0]; return; } sub irc_console_rw_fail { my ($peeradr, $peerport) = @_[ARG0, ARG1]; return; } $poe_kernel->run(); exit 0; sub LoadOps { my $results = $dbh->selectall_arrayref("SELECT name FROM Ops;", { Slice => {} }); my @ops; foreach my $entry ( @$results ) { push(@ops, $entry->{name}); } if (!(@ops)) { print "No ops defined. Please enter name of first op: "; my $firstop = <>; chomp($firstop); push(@ops, $firstop); } return @ops; } sub LoadChannels { my $results = $dbh->selectall_arrayref("SELECT name FROM Channels;", { Slice => {} }); my @channels; foreach my $entry ( @$results ) { push (@channels, $entry->{name}); } return @channels; } sub Find($@) { my $s = shift; $_ eq $s && return @_ while $_=pop; -1; } sub AddCommas { local $_ = shift; 1 while s/^([-+]?\d+)(\d{3})/$1,$2/; return $_; } sub ConvertFromSeconds { my $seconds = $_[0]; my $days = ""; my $hours = ""; my $minutes = ""; if ($seconds > 86400) { $days = int($seconds / 86400) . "d "; $seconds = $seconds % 86400; } else { $days = ""; } if ($seconds > 3600) { $hours = int($seconds / 3600) . "h "; $seconds = $seconds % 3600; } else { $hours = ""; } if ($seconds > 60) { $minutes = int($seconds / 60) . "m "; $seconds = $seconds % 60; } else { $minutes = ""; } if ($seconds > 0) { $seconds .= "s" } else { $seconds = "0s"; } return ($days . $hours . $minutes . $seconds); }