Initial commit

This commit is contained in:
eleksir
2021-08-18 23:59:17 +03:00
commit 23f7b5c602
9 changed files with 719 additions and 0 deletions

206
lib/Craniac.pm Normal file
View File

@@ -0,0 +1,206 @@
package Craniac;
# общие модули - синтаксис, кодировки итд
use 5.018;
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
# модули для работы приложения
use Digest::SHA qw (sha1_base64);
use JSON::XS;
use Hailo;
use Log::Any qw ($log);
use Mojo::Redis;
use Mojo::IOLoop;
use Math::Random::Secure qw (irand);
use Text::Fuzzy qw (distance_edits);
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 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 = (
'Так, блядь...',
'*Закатывает рукава* И ради этого ты меня позвал?',
'Ну чего ты начинаешь, нормально же общались',
'Повтори свой вопрос, не поняла',
'Выйди и зайди нормально',
'Я подумаю',
'Даже не знаю что на это ответить',
'Ты упал такие вопросы девочке задавать?',
'Можно и так, но не уверена',
'А как ты думаешь?',
);
return $myphrase[irand ($#myphrase + 1)];
}
sub utf2sha1 () {
my $string = shift;
if ($string eq '') {
return sha1_base64 '';
}
my $bytes = encode_utf8 $string;
return sha1_base64 $bytes;
}
sub fmatch (@) {
my $srcphrase = shift;
my $answer = shift;
my ($distance, undef) = distance_edits ($srcphrase, $answer);
my $srcphraselen = length $srcphrase;
my $distance_max = int ($srcphraselen - ($srcphraselen * (100 - (90 / ($srcphraselen ** 0.5))) * 0.01));
if ($distance >= $distance_max) {
return 0;
} else {
return 1;
}
}
# основной парсер
my $parse_message = sub {
my $self = shift;
my $m = shift;
my $answer = $m;
my $send_to = $answer->{from};
my $chatid = $m->{chatid};
unless (defined $hailo->{$chatid}) {
my $cid = $chatid;
# костыль, чтобы не портировать данные из телеграммных мозгов
unless ($m->{from} eq 'telegram') {
$cid = utf2sha1 ($chatid);
$cid =~ s/\//-/xmsg;
}
my $brainname = sprintf '%s/%s.sqlite', $c->{braindir}, $cid;
$hailo->{$chatid} = Hailo->new (
brain => $brainname,
order => 3
);
unless (defined $hailo->{$chatid}) {
$log->warn ($EVAL_ERROR);
return;
}
$log->info (sprintf 'Lazy init brain: %s', $brainname);
}
if ($m->{misc}->{answer}) {
# пытаемся что-то генерировать, если фраза длиннее 3-х букв
if (length ($m->{message}) > 3) {
$answer->{message} = $hailo->{$chatid}->learn_reply ($m->{message});
# если из реактора вернулся 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}
}
);
}
} else {
# пытаемся что-то запоминать, если фраза длиннее 3-х букв
if (length ($m->{message}) > 3) {
$hailo->{$chatid}->learn ($m->{message});
}
}
return;
};
# main loop, он же event loop
sub RunCraniac {
my $redis = Mojo::Redis->new (
sprintf 'redis://%s:%s/1', $c->{server}, $c->{port}
);
my $pubsub = $redis->pubsub;
my $sub;
$pubsub->listen (
# Вот такая ебическая конструкция для авто-подписывания на все новые каналы.
# Странное ограничение, при котором на шаблон каналов можно подписаться только, подписавшись на каждый из
# каналов. То есть подписка создаётся по запросу. В AnyEvent::Redis подписаться можно сразу на * :(
# Но конкретно в моём случае этот момент неважен, т.к. подразумевается, что каналы будут добавляться, но не
# будут убавляться.
'craniac:*' => sub {
my ($ps, $channel) = @_ ;
unless (defined $sub->{$channel}) {
$log->info ("Subscribing to $channel");
$sub->{$channel} = $ps->json ($channel)->listen (
$channel => sub { $parse_message->(@_); }
);
}
}
);
do { Mojo::IOLoop->start } until Mojo::IOLoop->is_running;
return;
}
1;
# vim: set ft=perl noet ai ts=4 sw=4 sts=4: