#!/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<ACTION $response>);
$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);
}