First adoption step for production use with telegram frontend

This commit is contained in:
eleksir
2021-12-26 14:57:38 +03:00
parent 23f7b5c602
commit 1c82a2ec0e
6 changed files with 234 additions and 114 deletions

37
lib/Conf.pm Normal file
View File

@@ -0,0 +1,37 @@
package Conf;
# loads config
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
use Encode qw (encode decode);
use JSON::XS;
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (LoadConf);
sub LoadConf {
my $c = 'data/config.json';
open my $CH, '<', $c or die "[FATAL] No conf at $c: $OS_ERROR\n";
my $json;
my $len = (stat $c) [7];
my $readlen = read $CH, $json, $len;
$json = encode 'UTF-8', $json;
unless ($readlen) {
close $CH; ## no critic (InputOutput::RequireCheckedSyscalls
die "[FATAL] Unable to read $c: $OS_ERROR\n";
}
close $CH; ## no critic (InputOutput::RequireCheckedSyscalls
my $j = JSON::XS->new->utf8->relaxed;
return $j->decode ($json);
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:

View File

@@ -1,57 +1,46 @@
package Craniac;
# Предполагается, что craniac - это "маршрут по-умолчанию". То есть сюда попадают все фразы, которые не были распознаны
# aleesa-misc как команды.
# общие модули - синтаксис, кодировки итд
# Основная задача мозгов - распознать, требуется ли ответ и обработать фразу.
# Соответсвенно, мы предполагаем, что фразы делятся на 2 типа:
# * ключевая фраза, на которую у нас есть один или несколько готовых ответов.
# * просто фраза в чате, на которую надо напрячь мозги и (в зависимости от необходимости) ответить на неё или
# проигнорировать.
# Общие модули - синтаксис, кодировки итд
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use English qw ( -no_match_vars );
# модули для работы приложения
# Модули для работы приложения
use Digest::SHA qw (sha1_base64);
use JSON::XS;
use Hailo;
use Log::Any qw ($log);
# Чтобы "уж точно" использовать hiredis-биндинги, загрузим этот модуль перед Mojo::Redis
use Protocol::Redis::XS;
use Mojo::Redis;
use Mojo::IOLoop;
use Math::Random::Secure qw (irand);
use Text::Fuzzy qw (distance_edits);
use Conf qw (LoadConf);
use version; our $VERSION = qw (1.0);
use Exporter qw (import);
our @EXPORT_OK = qw (RunCraniac);
sub LoadConf ();
sub RandomCommonPhrase ();
sub utf2sha1 ($);
sub fmatch (@);
sub brains (@);
sub RunCraniac ();
my $c = LoadConf ();
my $hailo;
sub LoadConf {
my $cfg = 'data/config.json';
open my $CH, '<', $c or die "[FATAL] No conf at $c: $OS_ERROR\n";
my $len = (stat $cfg) [7];
my $json;
my $readlen = read $CH, $json, $len;
unless ($readlen) {
close $CH; ## no critic (InputOutput::RequireCheckedSyscalls
die "[FATAL] Unable to read $c: $OS_ERROR\n";
}
if ($readlen != $len) {
close $CH; ## no critic (InputOutput::RequireCheckedSyscalls
die "[FATAL] File $c is $len bytes on disk, but we read only $readlen bytes\n";
}
close $CH; ## no critic (InputOutput::RequireCheckedSyscalls
my $j = JSON::XS->new->utf8->relaxed;
return $j->decode ($json);
}
sub RandomCommonPhrase () {
my @myphrase = (
'Так, блядь...',
@@ -69,7 +58,7 @@ sub RandomCommonPhrase () {
return $myphrase[irand ($#myphrase + 1)];
}
sub utf2sha1 () {
sub utf2sha1 ($) {
my $string = shift;
if ($string eq '') {
@@ -95,19 +84,15 @@ sub fmatch (@) {
}
}
# основной парсер
my $parse_message = sub {
my $self = shift;
my $m = shift;
my $answer = $m;
my $send_to = $answer->{from};
my $chatid = $m->{chatid};
sub brains (@) {
my $chatid = shift;
my $telegram = shift // 0;
unless (defined $hailo->{$chatid}) {
my $cid = $chatid;
# костыль, чтобы не портировать данные из телеграммных мозгов
unless ($m->{from} eq 'telegram') {
# Костыль, чтобы не портировать данные из телеграммных мозгов
unless ($telegram) {
$cid = utf2sha1 ($chatid);
$cid =~ s/\//-/xmsg;
}
@@ -121,82 +106,180 @@ my $parse_message = sub {
unless (defined $hailo->{$chatid}) {
$log->warn ($EVAL_ERROR);
return;
return 0;
}
$log->info (sprintf 'Lazy init brain: %s', $brainname);
}
if ($m->{misc}->{answer}) {
return 1;
}
# пытаемся что-то генерировать, если фраза длиннее 3-х букв
if (length ($m->{message}) > 3) {
$answer->{message} = $hailo->{$chatid}->learn_reply ($m->{message});
# Основной парсер
my $parse_message = sub {
my $self = shift;
my $m = shift;
my $send_to = $m->{plugin};
my $chatid = $m->{chatid};
my $userid = $m->{userid};
my $phrase = $m->{message};
my $reply;
# если из реактора вернулся undef выдаём "общую" фразу из словаря
unless (defined $answer->{message}) {
$answer->{message} = RandomCommonPhrase ();
}
# если из реактора вернулась пустота выдаём "общую" фразу из словаря
if ($answer->{message} eq '') {
$answer->{message} = RandomCommonPhrase ();
}
# если сгенерированная фраза слишком похожа на начальную - выдаём "общую" фразу из словаря
if (fmatch (lc ($m->{message}), lc($answer->{message}))) {
$answer->{message} = RandomCommonPhrase ();
}
$self->json ($send_to)->notify (
$send_to => {
from => $answer->{from},
userid => $answer->{userid},
chatid => $answer->{chatid},
plugin => $answer->{plugin},
message => $answer->{message}
}
);
# Если $m->{misc}->{answer} не существует, то проставим его как 1, предполагаем, что по-умолчанию ответ от нас
# всё-таки ожидают. Уточним ниже, по содержимому фразы и по режиму беседы (private или public).
if (defined $m->{misc}) {
unless (defined $m->{misc}->{answer}) {
$m->{misc}->{answer} = 1;
}
} else {
$m->{misc}->{answer} = 1;
}
if (defined $m->{misc}->{bot_nick}) {
# Заменим обращение к боту по нику с окружающими запятыми и пробелами на пробел
$phrase =~ s/\s*\,?\s*($m->{misc}->{bot_nick})\s*\,?\s*/ /gui;
}
if ($m->{mode} eq 'private') {
$phrase =~ s/^\s*(вот\s+)?скажи(\s*мне)?\s*//gui;
$phrase =~ s/\s*(,)?\s*а\s*\?$/?/gui;
if ($phrase =~ /^ *кто +все +эти +люди *\?$/ui) {
$reply = 'Какие "люди"? Мы здесь вдвоём, только ты и я.';
} elsif ($phrase =~ /^ *кто +я *\?$/ui) {
$reply = 'Где?';
} elsif ($phrase =~ /^ *кто +(здесь|тут) *\?$/ui) {
$reply = 'Ты да я, да мы с тобой.';
}
# TODO: дописать запрос в webapp, для easter egg-ов, либо делать это на уровне aleesa-misc
if (defined ($reply) && $m->{misc}->{answer}) {
$self->json ($send_to)->notify (
$send_to => {
from => 'craniac',
userid => $userid,
chatid => $chatid,
plugin => $m->{plugin},
message => $reply
}
);
return;
}
} elsif ($m->{mode} eq 'public') {
$phrase =~ s/^\s*(вот\s+)?скажи(\s*мне)?\s*//gui;
$phrase =~ s/\s*(,)?\s*а\s*\?$/?/gui;
if ($phrase =~ /^ *кто +все +эти +люди *\?$/ui) {
$reply = 'Кто здесь?';
} elsif ($phrase =~ /^ *кто +я *?$/ui){
$reply = 'Где?';
} elsif ($phrase =~ /^ *кто +(здесь|тут) *\?$/ui) {
$reply = 'Здесь все: Никита, Стас, Гена, Турбо и Дюша Метёлкин.';
}
if (defined ($reply) && $m->{misc}->{answer}) {
$self->json ($send_to)->notify (
$send_to => {
from => 'craniac',
userid => $userid,
chatid => $chatid,
plugin => $m->{plugin},
message => $reply
}
);
return;
}
}
# Попадаем сюда только если входящая фраза не распознана ранее как ключевая.
if ($m->{misc}->{answer}) {
# Пытаемся что-то генерировать, если фраза длиннее 3-х букв
if (length ($m->{message}) > 3) {
if (brains ($chatid, 1)) {
$reply = $hailo->{$chatid}->learn_reply ($m->{message});
# Если из реактора вернулся undef выдаём "общую" фразу из словаря
unless (defined $reply) {
$reply = RandomCommonPhrase ();
} else {
# Если из реактора вернулась пустота выдаём "общую" фразу из словаря
if ($reply eq '') {
$reply = RandomCommonPhrase ();
}
# Если сгенерированная фраза слишком похожа на начальную - выдаём "общую" фразу из словаря
if (fmatch (lc ($m->{message}), lc ($reply))) {
$reply = RandomCommonPhrase ();
}
}
$self->json ($send_to)->notify (
$send_to => {
from => 'craniac',
userid => $userid,
chatid => $chatid,
plugin => $m->{plugin},
message => $reply
}
);
}
}
} else {
# пытаемся что-то запоминать, если фраза длиннее 3-х букв
if (length ($m->{message}) > 3) {
$hailo->{$chatid}->learn ($m->{message});
if (brains ($chatid, 1)) {
$hailo->{$chatid}->learn ($m->{message});
}
}
}
return;
};
# main loop, он же event loop
sub RunCraniac {
# Main loop, он же event loop
sub RunCraniac () {
$log->notice ("[NOTICE] Connecting to $c->{server}, $c->{port}");
my $redis = Mojo::Redis->new (
sprintf 'redis://%s:%s/1', $c->{server}, $c->{port}
);
my $pubsub = $redis->pubsub;
my $sub;
$log->notice ('[NOTICE] Registering connection-event callback');
$pubsub->listen (
# Вот такая ебическая конструкция для авто-подписывания на все новые каналы.
# Странное ограничение, при котором на шаблон каналов можно подписаться только, подписавшись на каждый из
# каналов. То есть подписка создаётся по запросу. В AnyEvent::Redis подписаться можно сразу на * :(
# Но конкретно в моём случае этот момент неважен, т.к. подразумевается, что каналы будут добавляться, но не
# будут убавляться.
'craniac:*' => sub {
my ($ps, $channel) = @_ ;
$redis->on (
connection => sub {
my ($r, $connection) = @_;
unless (defined $sub->{$channel}) {
$log->info ("Subscribing to $channel");
$log->info ('[INFO] Triggering callback on new client connection');
$sub->{$channel} = $ps->json ($channel)->listen (
$channel => sub { $parse_message->(@_); }
);
}
# Пишем ошибку в лог, если соединение с редиской внезапно порвалось.
$connection->on (
error => sub {
my ($conn, $error) = @_;
$log->error ("[ERROR] Redis connection error: $error");
return;
}
);
return;
}
);
my $pubsub = $redis->pubsub;
my $sub;
$log->notice ('[NOTICE] Subscribing to redis channels');
foreach my $channel (@{$c->{channels}}) {
$log->info ("[INFO] Subscribing to $channel");
$sub->{$channel} = $pubsub->json ($channel)->listen (
$channel => sub { $parse_message->(@_); }
);
}
do { Mojo::IOLoop->start } until Mojo::IOLoop->is_running;
return;
}