pbot/Plugins/ActionTrigger.pm

436 lines
12 KiB
Perl

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package Plugins::ActionTrigger;
use warnings;
use strict;
use feature 'unicode_strings';
use feature 'switch';
no if $] >= 5.018, warnings => "experimental::smartmatch";
use Carp ();
use DBI;
use Time::Duration qw/duration/;
use Time::HiRes qw/gettimeofday/;
sub new {
Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref $_[1] eq 'HASH';
my ($class, %conf) = @_;
my $self = bless {}, $class;
$self->initialize(%conf);
return $self;
}
sub initialize {
my ($self, %conf) = @_;
$self->{pbot} = delete $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__);
$self->{pbot}->{commands}->register(sub { $self->actiontrigger(@_) }, 'actiontrigger', 10);
$self->{pbot}->{event_dispatcher}->register_handler('irc.public', sub { $self->on_public(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.caction', sub { $self->on_action(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.join', sub { $self->on_join(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.part', sub { $self->on_departure(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.quit', sub { $self->on_departure(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.kick', sub { $self->on_kick(@_) });
$self->{filename} = $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/triggers.sqlite3';
$self->dbi_begin;
$self->create_database;
}
sub unload {
my $self = shift;
$self->dbi_end;
$self->{pbot}->{commands}->unregister('actiontrigger');
}
sub create_database {
my $self = shift;
return if not $self->{dbh};
eval {
$self->{dbh}->do(<<SQL);
CREATE TABLE IF NOT EXISTS Triggers (
channel TEXT,
trigger TEXT,
action TEXT,
owner TEXT,
level INTEGER,
repeatdelay INTEGER,
lastused NUMERIC
)
SQL
};
$self->{pbot}->{logger}->log("ActionTrigger create database failed: $@") if $@;
}
sub dbi_begin {
my ($self) = @_;
eval {
$self->{dbh} = DBI->connect("dbi:SQLite:dbname=$self->{filename}", "", "", { RaiseError => 1, PrintError => 0, AutoInactiveDestroy => 1, sqlite_unicode => 1 }) or die $DBI::errstr;
};
if ($@) {
$self->{pbot}->{logger}->log("Error opening ActionTrigger database: $@");
return 0;
} else {
return 1;
}
}
sub dbi_end {
my ($self) = @_;
return if not $self->{dbh};
$self->{dbh}->disconnect;
delete $self->{dbh};
}
sub add_trigger {
my ($self, $channel, $trigger, $action, $owner, $level, $repeatdelay) = @_;
return 0 if $self->get_trigger($channel, $trigger);
eval {
my $sth = $self->{dbh}->prepare('INSERT INTO Triggers (channel, trigger, action, owner, level, repeatdelay, lastused) VALUES (?, ?, ?, ?, ?, ?, 0)');
$sth->bind_param(1, lc $channel);
$sth->bind_param(2, $trigger);
$sth->bind_param(3, $action);
$sth->bind_param(4, $owner);
$sth->bind_param(5, $level);
$sth->bind_param(6, $repeatdelay);
$sth->execute();
};
if ($@) {
$self->{pbot}->{logger}->log("Add trigger failed: $@");
return 0;
}
return 1;
}
sub delete_trigger {
my ($self, $channel, $trigger) = @_;
return 0 if not $self->get_trigger($channel, $trigger);
my $sth = $self->{dbh}->prepare('DELETE FROM Triggers WHERE channel = ? AND trigger = ?');
$sth->bind_param(1, lc $channel);
$sth->bind_param(2, $trigger);
$sth->execute();
return 1;
}
sub list_triggers {
my ($self, $channel) = @_;
my $triggers = eval {
my $sth;
if ($channel eq '*') {
$sth = $self->{dbh}->prepare('SELECT * FROM Triggers WHERE channel != ?');
$channel = 'global';
} else {
$sth = $self->{dbh}->prepare('SELECT * FROM Triggers WHERE channel = ?');
}
$sth->bind_param(1, lc $channel);
$sth->execute();
return $sth->fetchall_arrayref({});
};
if ($@) {
$self->{pbot}->{logger}->log("List triggers failed: $@");
}
$triggers = [] if not defined $triggers;
return @$triggers;
}
sub update_trigger {
my ($self, $channel, $trigger, $data) = @_;
eval {
my $sql = 'UPDATE Triggers SET ';
my $comma = '';
foreach my $key (keys %$data) {
$sql .= "$comma$key = ?";
$comma = ", ";
}
$sql .= "WHERE trigger = ? AND channel = ?";
my $sth = $self->{dbh}->prepare($sql);
my $param = 1;
foreach my $key (keys %$data) {
$sth->bind_param($param++, $data->{$key});
}
$sth->bind_param($param++, $trigger);
$sth->bind_param($param, $channel);
$sth->execute();
};
$self->{pbot}->{logger}->log("Update trigger $channel/$trigger failed: $@\n") if $@;
}
sub get_trigger {
my ($self, $channel, $trigger) = @_;
my $row = eval {
my $sth = $self->{dbh}->prepare('SELECT * FROM Triggers WHERE channel = ? AND trigger = ?');
$sth->bind_param(1, lc $channel);
$sth->bind_param(2, $trigger);
$sth->execute();
my $row = $sth->fetchrow_hashref();
return $row;
};
if ($@) {
$self->{pbot}->{logger}->log("Get trigger failed: $@");
return undef;
}
return $row;
}
sub actiontrigger {
my ($self, $from, $nick, $user, $host, $arguments) = @_;
if (not $self->{dbh}) {
return "Internal error.";
}
my $command;
($command, $arguments) = split / /, $arguments, 2;
my ($channel, $result);
given ($command) {
when ('list') {
($channel) = split / /, $arguments, 1;
if (not defined $channel) {
$channel = $from;
} elsif ($channel !~ m/^#/ and $channel ne 'global') {
return "Usage: actiontrigger list [channel]";
}
my @triggers = $self->list_triggers($channel);
if (not @triggers) {
$result = "No action triggers set for $channel.";
} else {
$result = "Triggers for $channel:\n";
my $comma = '';
foreach my $trigger (@triggers) {
$result .= "$comma$trigger->{trigger} -> $trigger->{action}";
$result .= " (level=$trigger->{level})" if $trigger->{level} != 0;
$result .= " (repeatdelay=$trigger->{repeatdelay})" if $trigger->{repeatdelay} != 0;
$comma = ",\n";
}
}
}
when ('add') {
if ($from =~ m/^#/) {
$channel = $from;
} else {
($channel, $arguments) = split / /, $arguments, 2;
if ($channel !~ m/^#/ and $channel ne 'global') {
return "Usage from private message: actiontrigger add <channel> <level> <repeat delay> <regex> <action>";
}
}
my ($level, $repeatdelay, $trigger, $action) = split / /, $arguments, 4;
if (not defined $trigger or not defined $action) {
if ($from !~ m/^#/) {
$result = "Usage from private message: actiontrigger add <channel> <level> <repeat delay> <regex> <action>";
} else {
$result = "Usage: actiontrigger add <level> <repeat delay> <regex> <action>";
}
return $result;
}
my $exists = $self->get_trigger($channel, $trigger);
if (defined $exists) {
return "Trigger already exists.";
}
if ($level !~ m/^\d+$/) {
return "$nick: Missing level argument?\n";
}
if ($repeatdelay !~ m/^\d+$/) {
return "$nick: Missing repeat delay argument?\n";
}
if ($level > 0) {
my $admin = $self->{pbot}->{admins}->find_admin($channel, "$nick!$user\@$host");
if (not defined $admin or $level > $admin->{level}) {
return "You may not set a level higher than your own.";
}
}
if ($self->add_trigger($channel, $trigger, $action, "$nick!$user\@$host", $level, $repeatdelay)) {
$result = "Trigger added.";
} else {
$result = "Failed to add trigger.";
}
}
when ('delete') {
if ($from =~ m/^#/) {
$channel = $from;
} else {
($channel, $arguments) = split / /, $arguments, 2;
if ($channel !~ m/^#/ and $channel ne 'global') {
return "Usage from private message: actiontrigger delete <channel> <regex>";
}
}
my ($trigger) = split / /, $arguments, 1;
if (not defined $trigger) {
if ($from !~ m/^#/) {
$result = "Usage from private message: actiontrigger delete <channel> <regex>";
} else {
$result = "Usage: actiontrigger delete <regex>";
}
return $result;
}
my $exists = $self->get_trigger($channel, $trigger);
if (not defined $exists) {
$result = "No such trigger.";
} else {
$self->delete_trigger($channel, $trigger);
$result = "Trigger deleted.";
}
}
default {
if ($from !~ m/^#/) {
$result = "Usage from private message: actiontrigger list [channel] | actiontrigger add <channel> <level> <repeat delay> <regex> <trigger> | actiontrigger delete <channel> <regex>";
} else {
$result = "Usage: actiontrigger list [channel] | actiontrigger add <level> <repeat delay> <regex> <trigger> | actiontrigger delete <regex>";
}
}
}
return $result;
}
sub on_kick {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host);
my ($victim, $reason) = ($event->{event}->to, $event->{event}->{args}[1]);
my $channel = $event->{event}->{args}[0];
return 0 if $event->{interpreted};
$self->check_trigger($nick, $user, $host, $channel, "KICK $victim $reason");
return 0;
}
sub on_action {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $msg) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->args);
my $channel = $event->{event}->{to}[0];
return 0 if $event->{interpreted};
$msg =~ s/^\/me\s+//;
$self->check_trigger($nick, $user, $host, $channel, "ACTION $msg");
return 0;
}
sub on_public {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $msg) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->args);
my $channel = $event->{event}->{to}[0];
return 0 if $event->{interpreted};
$self->check_trigger($nick, $user, $host, $channel, "PRIVMSG $msg");
return 0;
}
sub on_join {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $channel, $args) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to, $event->{event}->args);
$channel = lc $channel;
$self->check_trigger($nick, $user, $host, $channel, "JOIN");
return 0;
}
sub on_departure {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $channel, $args) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to, $event->{event}->args);
$channel = lc $channel;
$self->check_trigger($nick, $user, $host, $channel, (uc $event->{event}->type) . " $args");
return 0;
}
sub check_trigger {
my ($self, $nick, $user, $host, $channel, $text) = @_;
if (not $self->{dbh}) {
return 0;
}
my @triggers = $self->list_triggers($channel);
my @globals = $self->list_triggers('global');
push @triggers, @globals;
$text = "$nick!$user\@$host $text";
# $self->{pbot}->{logger}->log("Checking action trigger: [$text]\n");
my $now = gettimeofday;
foreach my $trigger (@triggers) {
eval {
$trigger->{lastused} = 0 if not defined $trigger->{lastused};
$trigger->{repeatdelay} = 0 if not defined $trigger->{repeatdelay};
if ($now - $trigger->{lastused} >= $trigger->{repeatdelay} and $text =~ m/$trigger->{trigger}/) {
$trigger->{lastused} = $now;
my $data = { lastused => $now };
$self->update_trigger($trigger->{channel}, $trigger->{trigger}, $data);
my $action = $trigger->{action};
my @stuff = ($1, $2, $3, $4, $5, $6, $7, $8, $9);
my $i;
map { ++$i; $action =~ s/\$$i/$_/g; } @stuff;
my $delay = 0;
my ($n, $u, $h) = $trigger->{owner} =~ /^([^!]+)!([^@]+)\@(.*)$/;
my $command = {
nick => $n,
user => $u,
host => $h,
command => $action,
level => $trigger->{level} // 0
};
$self->{pbot}->{logger}->log("ActionTrigger: ($channel) $trigger->{trigger} -> $action [$command->{level}]\n");
$self->{pbot}->{interpreter}->add_to_command_queue($channel, $command, $delay);
}
};
if ($@) {
$self->{pbot}->{logger}->log("Skipping bad trigger $trigger->{trigger}: $@");
}
}
return 0;
}
1;