Přemysl Janouch
a0e9ede3e3
I've come to the conclusion that copyright mostly just stands in the way of software development. In my jurisdiction I cannot give up my own copyright and 0BSD seems to be the closest thing to public domain. The updated mail address, also used in my author/committer lines, is shorter and looks nicer. People rarely interact anyway.
178 lines
4.7 KiB
Perl
Executable File
178 lines
4.7 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
#
|
|
# ZyklonB factoids plugin
|
|
#
|
|
# Copyright 2016 Přemysl Janouch <p@janouch.name>
|
|
# 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 <STDIN>)->{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: <name> = <definition>")
|
|
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: <name> <number>")
|
|
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: <name> [<number>]")
|
|
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 = <STDIN>) {
|
|
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});
|
|
}
|