#!/usr/bin/env perl
#
# xB factoids plugin
#
# Copyright 2016 Přemysl Eric Janouch
# See the file LICENSE for licensing information.
#
use strict;
use warnings;
use Text::Wrap;
# --- IRC protocol -------------------------------------------------------------
binmode STDIN; select STDIN; $| = 1; $/ = "\r\n";
binmode STDOUT; select STDOUT; $| = 1; $\ = "\r\n";
sub parse ($) {
chomp (my $line = shift);
return undef unless my ($nick, $user, $host, $command, $args) = ($line =~
qr/^(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/o);
return {nick => $nick, user => $user, host => $host, command => $command,
args => defined $args ? [$args =~ /:?((?<=:).*|[^ ]+) */og] : []};
}
sub bot_print {
print "ZYKLONB print :${\shift}";
}
# --- Initialization -----------------------------------------------------------
my %config;
for my $name (qw(prefix)) {
print "ZYKLONB get_config :$name";
$config{$name} = (parse )->{args}->[0];
}
print "ZYKLONB register";
# --- Database -----------------------------------------------------------------
# Simple map of (factoid_name => [definitions]); all factoids are separated
# by newlines and definitions by carriage returns. Both disallowed in IRC.
sub db_load {
local $/ = "\n";
my ($path) = @_;
open my $db, "<", $path or return {};
my %entries;
while (<$db>) {
chomp;
my @defs = split "\r";
$entries{shift @defs} = \@defs;
}
\%entries
}
sub db_save {
local $\ = "\n";
my ($path, $ref) = @_;
my $path_new = "$path.new";
open my $db, ">", $path_new or die "db save failed: $!";
my %entries = %$ref;
print $db join "\r", ($_, @{$entries{$_}}) for keys %entries;
close $db;
rename $path_new, $path or die "db save failed: $!";
}
# --- Factoids -----------------------------------------------------------------
my $db_path = 'factoids.db';
my %db = %{db_load $db_path};
sub learn {
my ($respond, $input) = @_;
return &$respond("usage: = ")
unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*=\s*(.+?)\s*$/;
my ($name, $number, $definition) = ($1, $2, $3);
return &$respond("trailing numbers in names are disallowed")
if defined $2;
$db{$name} = [] unless exists $db{$name};
my $entries = $db{$name};
return &$respond("duplicate definition")
if grep { lc $_ eq lc $definition } @$entries;
push @$entries, $definition;
&$respond("saved as #${\scalar @$entries}");
db_save $db_path, \%db;
}
sub check_number {
my ($respond, $name, $number) = @_;
my $entries = $db{$name};
if ($number > @$entries) {
&$respond(qq/"$name" has only ${\scalar @$entries} definitions/);
} elsif (not $number) {
&$respond("number must not be zero");
} else {
return 1;
}
return 0;
}
sub forget {
my ($respond, $input) = @_;
return &$respond("usage: ")
unless $input =~ /^([^=]+?)\s+(\d+)\s*$/;
my ($name, $number) = ($1, int($2));
return &$respond(qq/"$name" is undefined/)
unless exists $db{$name};
my $entries = $db{$name};
return unless check_number $respond, $name, $number;
splice @$entries, --$number, 1;
&$respond("forgotten");
db_save $db_path, \%db;
}
sub whatis {
my ($respond, $input) = @_;
return &$respond("usage: []")
unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*$/;
my ($name, $number) = ($1, $2);
return &$respond(qq/"$name" is undefined/)
unless exists $db{$name};
my $entries = $db{$name};
if (defined $number) {
return unless check_number $respond, $name, $number;
&$respond(qq/"$name" is #$number $entries->[$number - 1]/);
} else {
my $i = 1;
my $definition = join ", ", map { "#${\$i++} $_" } @{$entries};
&$respond(qq/"$name" is $definition/);
}
}
sub wildcard {
my ($respond, $input) = @_;
$input =~ /=/ ? learn(@_) : whatis(@_);
}
my %commands = (
'learn' => \&learn,
'forget' => \&forget,
'whatis' => \&whatis,
'??' => \&wildcard,
);
# --- Input loop ---------------------------------------------------------------
while (my $line = ) {
my %msg = %{parse $line};
my @args = @{$msg{args}};
# This plugin only bothers to respond to PRIVMSG messages
next unless $msg{command} eq 'PRIVMSG' and @args >= 2
and my ($cmd, $input) = $args[1] =~ /^$config{prefix}(\S+)\s*(.*)/;
# So far the only reaction is a PRIVMSG back to the sender, so all the
# handlers need is a response callback and all arguments to the command
my ($target => $quote) = ($args[0] =~ /^[#+&!]/)
? ($args[0] => "$msg{nick}: ") : ($msg{nick} => '');
# Wrap all responses so that there's space for our prefix in the message
my $respond = sub {
local ($Text::Wrap::columns, $Text::Wrap::unexpand) = 400, 0;
my $start = "PRIVMSG $target :$quote";
print for split "\n", wrap $start, $start, shift;
};
&{$commands{$cmd}}($respond, $input) if exists($commands{$cmd});
}