Files
aleesa-cli/aleesa-cli
2022-04-17 15:46:28 +03:00

467 lines
12 KiB
Perl
Executable File

#!/usr/bin/perl
use 5.018; ## no critic (ProhibitImplicitImport)
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use version; our $VERSION = qw (1.0);
my $workdir;
# До фактического запуска логики программы, сменим рабочий каталог, чтобы подцепить либы
BEGIN {
use Cwd qw (chdir abs_path);
my @CWD = split /\//xms, abs_path ($PROGRAM_NAME);
if ($#CWD > 1) {
$#CWD = $#CWD - 1;
}
$workdir = join '/', @CWD;
chdir $workdir;
}
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
use Clone qw (clone);
use JSON::XS ();
use Mojo::IOLoop::LineReader ();
use Mojo::Redis ();
use Mojo::IOLoop::Signal ();
my $c = LoadConf ();
my $redis;
my $pubsub;
my %subchannels;
my @monchannels;
my $reader;
my $mydebug = 0;
my $myverbose = 0;
my $public_mode = 0;
my $monitor = 0;
if (defined $c->{debug}) {
$mydebug = $c->{debug};
}
if (defined $c->{verbose}) {
$myverbose = $c->{verbose};
}
my $help = << "EOH";
debug - display debug mode status
debug 1 - enable debug (you probably interest in verbose more)
debug 0 - disable debug (default)
mode - show conversation mode
mode private - conversation in private mode (default)
mode public - conversation in public mode
mode monitor - sniffs subscribed channels
mon add channel - add channel to bunch of monitored ones
mon remove channel - remove channel from list of monitored ones, $c->{channel} is always monitored
mon delete channel - remove channel from list of monitored ones, $c->{channel} is always monitored
mon del channel - remove channel from list of monitored ones, $c->{channel} is always monitored
verbose - show vebose mode status
verbose 1 - show tx/rx messages also as json
verbose 0 - disable verbose mode (default)
exit - quit cli
quit - quit cli
EOH
my $redismsg->{from} = 'cli';
$redismsg->{plugin} = 'cli';
$redismsg->{userid} = 'cli';
$redismsg->{chatid} = 'cli';
$redismsg->{mode} = 'private';
$redismsg->{misc}->{answer} = 1;
if (defined $c->{bot_nick}) {
$redismsg->{misc}->{bot_nick} = "$c->{bot_nick}";
} else {
$redismsg->{misc}->{bot_nick} = 'aleesa';
}
if (defined $c->{csign}) {
$redismsg->{misc}->{csign} = "$c->{csign}";
} else {
$redismsg->{misc}->{csign} = '!';
}
$redismsg->{misc}->{msg_format} = 0;
$redismsg->{misc}->{fwd_cnt} = 1;
$redismsg->{misc}->{good_morning} = 0;
$redismsg->{misc}->{username} = 'user';
# Процедура обработки сигнала
my $__signal_handler = sub {
my ($self, $name) = @_;
exit 0;
};
# Процедура, реагирующая на строку, пойманную с клавиатуры
my $__input_line_handler = sub {
my ($self, $line) = @_;
chomp $line;
if ($line =~ /^(exit|quit)$/) { ## no critic (RegularExpressions::ProhibitFixedStringMatches)
print "Quit\n";
$__signal_handler->($self, 'Quit');
} elsif ($line eq 'help') {
print "$help\n";
} elsif ($line =~ /^mode\s+(.*)$/) {
if ($1 eq 'public') {
$public_mode = 1;
if ($monitor) {
unsubscribe_channels (keys (%subchannels));
subscribe_channels ($c->{channel});
$monitor = 0;
}
print "[info]: continue conversation in public mode\n";
} elsif ($1 eq 'private') {
$public_mode = 0;
if ($monitor) {
unsubscribe_channels (keys (%subchannels));
subscribe_channels ($c->{channel});
$monitor = 0;
}
print "[info]: continue conversation in private mode\n";
} elsif ($1 eq 'monitor') {
$monitor = 1;
my @channels = keys %subchannels;
my @sniffchannels = @monchannels;
unsubscribe_channels (@channels);
subscribe_channels (@sniffchannels);
printf "[info]: switched to redis channel sniffing mode, monitoring theese pubsub channels: %s\n", join (', ',@monchannels);
} else {
my $message = '[info]: conversation now in ';
if ($monitor) {
$message = "[info]: monitor mode is on and coversation with bot disabled\n";
$message .= sprintf "[info]: monitoring these pubsub channels: %s\n", join (', ',@monchannels);
} else {
if ($public_mode) {
$message .= "public mode\n";
} else {
$message .= "private mode\n";
}
}
print $message;
}
} elsif ($line eq 'mode') {
my $message = '[info]: conversation now in ';
if ($monitor) {
$message = "[info]: monitor mode is on and coversation with bot disabled\n";
} else {
if ($public_mode) {
$message .= "public mode\n";
} else {
$message .= "private mode\n";
}
}
print $message;
} elsif ($line =~ /^mon\s+(add|remove|del|delete)\s+(.+)/) {
# Список каналов
my $command = $1;
my $list = $2;
$list =~ s/^\s+//u;
$list =~ s/\s+$//u;
if ($list eq '') {
print "[info]: looks like list of channels is empty\n";
} else {
my @channels = split /(\s*)?\,?\s+/, $list;
# Команда была "добавить"
if ($command eq 'add') {
if ($monitor) {
my @newchannels;
foreach my $channel (@channels) {
my $subscribe = 1;
foreach my $subchannel (keys %subchannels) {
if ($subchannel eq $channel) {
$subscribe = 0;
last;
}
}
push @newchannels, $channel if ($subscribe);
}
if ($#newchannels >= 0) {
subscribe_channels (@newchannels);
}
printf "[info]: now subscribed to these channels: %s\n", join (', ', keys %subchannels);
} else {
my %list;
foreach (@monchannels) {$list{$_} = 1;}
foreach (@channels) {$list{$_} = 1;}
$#monchannels = -1;
@monchannels = keys %list;
printf "[info]: list of monitored channels: %s\n", join (', ', @monchannels);
}
# Команда была "убрать"
} else {
if ($monitor) {
my @unsub_channels;
foreach my $channel (@channels) {
my $unsubscribe = 0;
next if $channel eq $c->{channel};
foreach my $subchannel (keys %subchannels) {
if ($subchannel eq $channel) {
$unsubscribe = 1;
last;
}
}
push @unsub_channels, $channel if ($unsubscribe);
}
if ($#unsub_channels >= 0) {
unsubscribe_channels (@unsub_channels);
}
printf "[info]: now subscribed to these channels: %s\n", join (', ', keys %subchannels);
} else {
my %list;
foreach (@monchannels) {$list{$_} = 1;}
foreach (@channels) {
next if $_ eq $c->{channel};
delete $list{$_} if ($list{$_});
}
$#monchannels = -1;
@monchannels = keys %list;
printf "[info]: list of monitored channels: %s\n", join (', ', @monchannels);
}
}
}
} elsif ($line eq 'mon') {
printf "[info]: list of monitored channels: %s\n", join (', ', @monchannels);
} elsif ($line =~ /^debug\s+(\d)$/) {
if ($1 eq '1') {
$mydebug = 1;
print "[info]: debug set to 1\n";
} elsif ($1 eq '0') {
$mydebug = 0;
print "[info]: debug set to 0\n";
}
} elsif ($line eq 'debug') {
print "[info]: debug is $mydebug\n";
} elsif ($line =~ /^verbose\s+(\d)$/) {
if ($1 eq '1') {
$myverbose = 1;
print "[info]: verbose set to 1\n";
} elsif ($1 eq '0') {
$myverbose = 0;
print "[info]: verbose set to 0\n";
}
} elsif ($line eq 'verbose') {
print "[info]: verbose is $myverbose\n";
} else {
# Будем дрыхнуть до того момента, пока не установится соединение к redis
until (defined $pubsub) {
sleep 1;
}
# В режиме монитора у нас есть чем занять консоль, поэтому общаться с потрохами бота в этом инстансе мы
# прекращаем до выхода в обычный режим - private или public
unless ($monitor) {
my $msg = clone $redismsg;
$msg->{message} = $line;
if ($public_mode) {
$msg->{mode} = 'public';
} else {
$msg->{mode} = 'private';
}
if ($line ne '') {
if ($mydebug || $myverbose) {
my $json = JSON::XS->new ()->pretty (1)->canonical (1)->encode ($msg);
warn "[sent]: $json\n";
}
$pubsub->json ($c->{router_channel})->notify (
$c->{router_channel} => $msg,
);
}
}
}
};
# Процедура обработки входящего сообщения из redis pubsub канала
my $parse_message = sub {
my $self = shift;
my $msg = shift;
# Что-то от модулей надо парсить, например, сообщения бармэна.
if ($mydebug || $myverbose || $monitor) {
my $json = JSON::XS->new ()->pretty (1)->canonical (1)->encode ($msg);
warn "[recieved]: $json\n";
}
printf "[%s]: %s\n", $c->{bot_nick}, $msg->{message} unless ($monitor);
return;
};
# Создадим подключение к Redis-ке
warn "[debug]: Connecting to $c->{server}, $c->{port}\n" if $mydebug;
$redis = Mojo::Redis->new (
sprintf 'redis://%s:%s/1', $c->{server}, $c->{port},
);
warn "[debug]: Registering redis connection-event callback\n" if $mydebug;
$redis->on (
connection => sub {
my ($r, $connection) = @_;
# Залоггируем ошибку, если соединение внезапно порвалось.
# FIXME: Похоже, что $mydebug в данном случае становится локальной, а не остаётся глобальной переменной.
warn "[debug]: Registering redis events callbacks\n" if $mydebug;
$connection->on (
close => sub {
die "[error]: Connection to redis is closed\n";
},
);
# FIXME: Похоже, что $mydebug в данном случае становится локальной, а не остаётся глобальной переменной.
$connection->on (
connect => sub {
warn "[debug]: Connection to redis established\n" if $mydebug;
},
);
$connection->on (
error => sub {
my ($conn, $error) = @_;
die "[error]: Redis connection error: $error\n";
},
);
return;
},
);
warn "[debug]: Registering subscribtion callback\n" if $mydebug;
$pubsub = $redis->pubsub;
subscribe_channels ($c->{channel});
# Повесим коллбэки на сигналы TERM, QUIT, HUP, INT
warn "[debug]: Registering signal handler callbacks\n" if $mydebug;
Mojo::IOLoop::Signal->on (TERM => $__signal_handler);
Mojo::IOLoop::Signal->on (QUIT => $__signal_handler);
Mojo::IOLoop::Signal->on (HUP => $__signal_handler);
Mojo::IOLoop::Signal->on (INT => $__signal_handler);
# Установим читателя с терминала
register_input_callback ();
# Запустим Event Loop
warn "[debug]: Starting event loop\n" if $mydebug;
print "\nTo show help just enter help command\n";
print "Ready to begin conversation with $c->{bot_nick}\n\n";
do { Mojo::IOLoop->start } until Mojo::IOLoop->is_running; ## no critic (Modules::RequireExplicitInclusion)
exit 0;
sub register_input_callback {
warn "[debug]: Registering terminal input callback\n" if $mydebug;
my $stdin = \*STDIN;
binmode $stdin;
$reader = Mojo::IOLoop::LineReader->new ($stdin);
$reader->timeout (0);
# Повесим коллбэк на чтение строки текста с stdin
$reader->on (
readln => $__input_line_handler,
);
$reader->start;
return;
}
sub subscribe_channels {
my @channels = @_;
foreach my $channel (@channels) {
warn "[debug]: Subscribing to redis pubsub channel $channel\n" if $mydebug;
my $cb = $pubsub->json ($channel)->listen (
$channel => sub { $parse_message->(@_); },
);
$subchannels{$channel} = $cb;
}
$#monchannels = -1;
@monchannels = keys %subchannels;
return;
}
sub unsubscribe_channels {
my @channels = @_;
foreach my $channel (@channels) {
warn "[debug]: Unsubscribing to redis pubsub channel $channel\n" if $mydebug;
$pubsub = $pubsub->unlisten ($channel, $subchannels{$channel});
delete $subchannels{$channel};
}
$#monchannels = -1;
@monchannels = keys %subchannels;
return;
}
sub LoadConf {
my $confFile = 'config.json';
my $CH;
unless (open $CH, '<', $confFile) {
die "[error]: No conf at $confFile: $OS_ERROR\n";
}
my $len = (stat $confFile) [7];
my $json;
use bytes;
my $readlen = read $CH, $json, $len;
no bytes;
unless ($readlen) {
close $CH;
die "[error]: Unable to read $confFile: $OS_ERROR\n";
}
if ($readlen != $len) {
close $CH;
die "[error]: File $confFile is $len bytes on disk, but we read only $readlen bytes\n";
}
close $CH;
my $j = JSON::XS->new->utf8->relaxed;
return $j->decode ($json);
}
# vim: set ft=perl noet ai ts=4 sw=4 sts=4: