Initial commit
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
_Deparsed_XSubs.pm
|
||||||
|
.idea/**
|
||||||
|
.vscode/**
|
||||||
|
.vstags
|
||||||
|
*.pid
|
||||||
|
*.log
|
||||||
|
data/cache/**
|
||||||
|
data/config.json
|
||||||
|
vendor_perl/**
|
||||||
95
.perlcriticrc
Normal file
95
.perlcriticrc
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env perlcritic
|
||||||
|
severity = 5
|
||||||
|
verbose = 8
|
||||||
|
|
||||||
|
# minorify some of warnings that are not really critical
|
||||||
|
[-InputOutput::RequireCheckedClose]
|
||||||
|
# What can we handle in case of error?
|
||||||
|
|
||||||
|
[-Modules::ProhibitExcessMainComplexity]
|
||||||
|
# Noone cares
|
||||||
|
|
||||||
|
[-Subroutines::ProhibitExcessComplexity]
|
||||||
|
# Noone cares
|
||||||
|
|
||||||
|
[-Subroutines::ProhibitExplicitReturnUndef]
|
||||||
|
# But it's very handy sometimes!
|
||||||
|
|
||||||
|
[-ValuesAndExpressions::ProhibitMagicNumbers]
|
||||||
|
# this policy makes too much false positives
|
||||||
|
|
||||||
|
[-RegularExpressions::RequireLineBoundaryMatching]
|
||||||
|
# We use /m as needed
|
||||||
|
|
||||||
|
[-RegularExpressions::RequireExtendedFormatting]
|
||||||
|
# We do not use extended formatting of regex, they still not THAT complex
|
||||||
|
|
||||||
|
[-RegularExpressions::RequireDotMatchAnything]
|
||||||
|
# huh?
|
||||||
|
|
||||||
|
[-RegularExpressions::ProhibitEscapedMetacharacters]
|
||||||
|
# Looks like it pointless
|
||||||
|
|
||||||
|
[-ControlStructures::ProhibitCascadingIfElse]
|
||||||
|
# I like long if..else chains
|
||||||
|
|
||||||
|
[-ControlStructures::ProhibitPostfixControls]
|
||||||
|
# In short lines we can afford ourselves to use postfix controls
|
||||||
|
# but ofcorse we should avoid such constructions in long lines.
|
||||||
|
|
||||||
|
[-Subroutines::ProhibitSubroutinePrototypes]
|
||||||
|
# Contrary to Perl::Critic we consider prototypes as useful, even if
|
||||||
|
# the compile-time checks of Perl are not perfect, Perl may give some
|
||||||
|
# hints.
|
||||||
|
|
||||||
|
[-CodeLayout::RequireTidyCode]
|
||||||
|
# Impossible to complete, because of on higher linting levels "## no critic..."
|
||||||
|
# require be per-case on same line where critique is, but tidy align them
|
||||||
|
# one line down.
|
||||||
|
|
||||||
|
[-CodeLayout::ProhibitParensWithBuiltins]
|
||||||
|
# duh...
|
||||||
|
|
||||||
|
[-ValuesAndExpressions::ProhibitEmptyQuotes]
|
||||||
|
# cause we consider following correct and useful: my $var = '';
|
||||||
|
# the recomended use of q{} looks too complex: my $var = q{};
|
||||||
|
|
||||||
|
[-ValuesAndExpressions::ProhibitNoisyQuotes]
|
||||||
|
# Other people, other opinions: "," is better readable than qw(,)
|
||||||
|
|
||||||
|
[-BuiltinFunctions::ProhibitSleepViaSelect]
|
||||||
|
# We do not wish to use too much external modules.
|
||||||
|
# select undef, undef, undef, 0.25; # actually is okay if you add comment that
|
||||||
|
# it is sleep for 0.25 seconds
|
||||||
|
|
||||||
|
[-NamingConventions::Capitalization]
|
||||||
|
# duh...
|
||||||
|
# Noone care, really
|
||||||
|
|
||||||
|
[-ControlStructures::ProhibitUnlessBlocks]
|
||||||
|
# They should not be too complex - only one condition should be checked,
|
||||||
|
# that's all
|
||||||
|
|
||||||
|
[-Miscellanea::ProhibitTies]
|
||||||
|
# Just allow it. It's very handy stuff and extremely simple and safe enugh to use.
|
||||||
|
|
||||||
|
[-ValuesAndExpressions::RequireNumberSeparators]
|
||||||
|
# Not really
|
||||||
|
|
||||||
|
[-InputOutput::RequireBracedFileHandleWithPrint]
|
||||||
|
# We prefer not to use braced file handles
|
||||||
|
|
||||||
|
[-Documentation::PodSpelling]
|
||||||
|
# It doesn't help, but emits annoying errors
|
||||||
|
|
||||||
|
[Documentation::RequirePodSections]
|
||||||
|
# Considering this as "soft" requirement. For modules that is (or will be)
|
||||||
|
# stand-alone libs well formed documentation is highly desired.
|
||||||
|
lib_sections = NAME | VERSION | DESCRIPTION | AUTHOR | LICENSE AND COPYRIGHT
|
||||||
|
|
||||||
|
[-InputOutput::RequireBriefOpen]
|
||||||
|
# Yes, i do!
|
||||||
|
|
||||||
|
[-ControlStructures::ProhibitDeepNests]
|
||||||
|
# Have to use it, so fuckoff
|
||||||
|
|
||||||
127
LICENSE
Normal file
127
LICENSE
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
The "Artistic License"
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The intent of this document is to state the conditions under which a
|
||||||
|
Package may be copied, such that the Copyright Holder maintains some
|
||||||
|
semblance of artistic control over the development of the package,
|
||||||
|
while giving the users of the package the right to use and distribute
|
||||||
|
the Package in a more-or-less customary fashion, plus the right to make
|
||||||
|
reasonable modifications.
|
||||||
|
|
||||||
|
Definitions:
|
||||||
|
|
||||||
|
"Package" refers to the collection of files distributed by the
|
||||||
|
Copyright Holder, and derivatives of that collection of files
|
||||||
|
created through textual modification.
|
||||||
|
|
||||||
|
"Standard Version" refers to such a Package if it has not been
|
||||||
|
modified, or has been modified in accordance with the wishes
|
||||||
|
of the Copyright Holder as specified below.
|
||||||
|
|
||||||
|
"Copyright Holder" is whoever is named in the copyright or
|
||||||
|
copyrights for the package.
|
||||||
|
|
||||||
|
"You" is you, if you're thinking about copying or distributing
|
||||||
|
this Package.
|
||||||
|
|
||||||
|
"Reasonable copying fee" is whatever you can justify on the
|
||||||
|
basis of media cost, duplication charges, time of people involved,
|
||||||
|
and so on. (You will not be required to justify it to the
|
||||||
|
Copyright Holder, but only to the computing community at large
|
||||||
|
as a market that must bear the fee.)
|
||||||
|
|
||||||
|
"Freely Available" means that no fee is charged for the item
|
||||||
|
itself, though there may be fees involved in handling the item.
|
||||||
|
It also means that recipients of the item may redistribute it
|
||||||
|
under the same conditions they received it.
|
||||||
|
|
||||||
|
1. You may make and give away verbatim copies of the source form of the
|
||||||
|
Standard Version of this Package without restriction, provided that you
|
||||||
|
duplicate all of the original copyright notices and associated disclaimers.
|
||||||
|
|
||||||
|
2. You may apply bug fixes, portability fixes and other modifications
|
||||||
|
derived from the Public Domain or from the Copyright Holder. A Package
|
||||||
|
modified in such a way shall still be considered the Standard Version.
|
||||||
|
|
||||||
|
3. You may otherwise modify your copy of this Package in any way, provided
|
||||||
|
that you insert a prominent notice in each changed file stating how and
|
||||||
|
when you changed that file, and provided that you do at least ONE of the
|
||||||
|
following:
|
||||||
|
|
||||||
|
a) place your modifications in the Public Domain or otherwise make them
|
||||||
|
Freely Available, such as by posting said modifications to Usenet or
|
||||||
|
an equivalent medium, or placing the modifications on a major archive
|
||||||
|
site such as uunet.uu.net, or by allowing the Copyright Holder to include
|
||||||
|
your modifications in the Standard Version of the Package.
|
||||||
|
|
||||||
|
b) use the modified Package only within your corporation or organization.
|
||||||
|
|
||||||
|
c) rename any non-standard executables so the names do not conflict
|
||||||
|
with standard executables, which must also be provided, and provide
|
||||||
|
a separate manual page for each non-standard executable that clearly
|
||||||
|
documents how it differs from the Standard Version.
|
||||||
|
|
||||||
|
d) make other distribution arrangements with the Copyright Holder.
|
||||||
|
|
||||||
|
4. You may distribute the programs of this Package in object code or
|
||||||
|
executable form, provided that you do at least ONE of the following:
|
||||||
|
|
||||||
|
a) distribute a Standard Version of the executables and library files,
|
||||||
|
together with instructions (in the manual page or equivalent) on where
|
||||||
|
to get the Standard Version.
|
||||||
|
|
||||||
|
b) accompany the distribution with the machine-readable source of
|
||||||
|
the Package with your modifications.
|
||||||
|
|
||||||
|
c) give non-standard executables non-standard names, and clearly
|
||||||
|
document the differences in manual pages (or equivalent), together
|
||||||
|
with instructions on where to get the Standard Version.
|
||||||
|
|
||||||
|
d) make other distribution arrangements with the Copyright Holder.
|
||||||
|
|
||||||
|
5. You may charge a reasonable copying fee for any distribution of this
|
||||||
|
Package. You may charge any fee you choose for support of this
|
||||||
|
Package. You may not charge a fee for this Package itself. However,
|
||||||
|
you may distribute this Package in aggregate with other (possibly
|
||||||
|
commercial) programs as part of a larger (possibly commercial) software
|
||||||
|
distribution provided that you do not advertise this Package as a
|
||||||
|
product of your own. You may embed this Package's interpreter within
|
||||||
|
an executable of yours (by linking); this shall be construed as a mere
|
||||||
|
form of aggregation, provided that the complete Standard Version of the
|
||||||
|
interpreter is so embedded.
|
||||||
|
|
||||||
|
6. The scripts and library files supplied as input to or produced as
|
||||||
|
output from the programs of this Package do not automatically fall
|
||||||
|
under the copyright of this Package, but belong to whoever generated
|
||||||
|
them, and may be sold commercially, and may be aggregated with this
|
||||||
|
Package. If such scripts or library files are aggregated with this
|
||||||
|
Package via the so-called "undump" or "unexec" methods of producing a
|
||||||
|
binary executable image, then distribution of such an image shall
|
||||||
|
neither be construed as a distribution of this Package nor shall it
|
||||||
|
fall under the restrictions of Paragraphs 3 and 4, provided that you do
|
||||||
|
not represent such an executable image as a Standard Version of this
|
||||||
|
Package.
|
||||||
|
|
||||||
|
7. C subroutines (or comparably compiled subroutines in other
|
||||||
|
languages) supplied by you and linked into this Package in order to
|
||||||
|
emulate subroutines and variables of the language defined by this
|
||||||
|
Package shall not be considered part of this Package, but are the
|
||||||
|
equivalent of input as in Paragraph 6, provided these subroutines do
|
||||||
|
not change the language in any way that would cause it to fail the
|
||||||
|
regression tests for the language.
|
||||||
|
|
||||||
|
8. Aggregation of this Package with a commercial distribution is always
|
||||||
|
permitted provided that the use of this Package is embedded; that is,
|
||||||
|
when no overt attempt is made to make this Package's interfaces visible
|
||||||
|
to the end user of the commercial distribution. Such use shall not be
|
||||||
|
construed as a distribution of this Package.
|
||||||
|
|
||||||
|
9. The name of the Copyright Holder may not be used to endorse or promote
|
||||||
|
products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
|
||||||
|
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
The End
|
||||||
41
README
Normal file
41
README
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
aleesa-craniac
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Бредогенератор, основанный на цепях Маркова. Попутно сохраняет состояние цепей
|
||||||
|
в sqlite-базу. Собственно, является "мозгом" aleesa bot-а.
|
||||||
|
|
||||||
|
Механика работы
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Бредогенератор подписан на все каналы, попадающие под шаблон "craniac:*" (без
|
||||||
|
кавычек). Предполагается, что есть специальный канал "broadcast", на который
|
||||||
|
подписываются все модули и, если надо, ответ в такой канал получают все.
|
||||||
|
|
||||||
|
Формат протокола - это json с полями:
|
||||||
|
from - модуль, который решил отправить запрос.
|
||||||
|
chatid - чат, из которого сделан вызов
|
||||||
|
userid - пользователь, на чью фразу мы отвечаем
|
||||||
|
message - оригинальный текст сообщения
|
||||||
|
plugin - плагин, нужный для генерации ответа
|
||||||
|
misc->{answer} - 1 - ждём ответа на запрос, 0 - не ждём ответа (данные для
|
||||||
|
добавления в цепи Маркова)
|
||||||
|
|
||||||
|
N.B.
|
||||||
|
При запуске нового приложения, которое будет пользоваться этим
|
||||||
|
бредогенератором, оно должно "поприветствовать" craniac-а по всем каналам, для
|
||||||
|
того, чтобы сработала внутренняя логика и craniac подписался на новые каналы.
|
||||||
|
Если этого не сделать, то первое сообщение по новому каналу будет "проглочено"
|
||||||
|
и "потрачено" на подписку на этот канал.
|
||||||
|
|
||||||
|
|
||||||
|
Как это запустить?
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Понадобится perl, cpanm и local::lib, а также несколько разных devel-пакетов
|
||||||
|
для разных библиотек. Как правило, это находится методом тыка, но точно
|
||||||
|
понадобятся sqlite-devel, bdb-devel (или db-devel). Кроме того, понадобятся
|
||||||
|
development tools - gcc, gcc-g++, perl-devel, make, cmake и так далее.
|
||||||
|
|
||||||
|
В каталоге с приложением под целевым пользователем запускаем bash bootstrap.sh
|
||||||
|
Ждём с надеждой, что оно не обломится в пути.
|
||||||
|
|
||||||
|
Если всё хорошо, то после создания data/config.json из data/config_sample.json
|
||||||
|
и получения ключей и настройки модулей openweathermap и flickr, можно
|
||||||
|
запускать приложение. Оно запустится как unix daemon.
|
||||||
197
bin/aleesa-craniac
Executable file
197
bin/aleesa-craniac
Executable file
@@ -0,0 +1,197 @@
|
|||||||
|
#!/usr/bin/perl
|
||||||
|
|
||||||
|
use 5.018;
|
||||||
|
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;
|
||||||
|
|
||||||
|
# before we run, change working dir
|
||||||
|
BEGIN {
|
||||||
|
use Cwd qw (chdir abs_path);
|
||||||
|
my @CWD = split /\//xms, abs_path ($PROGRAM_NAME);
|
||||||
|
|
||||||
|
if ($#CWD > 1) {
|
||||||
|
$#CWD = $#CWD - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workdir = join '/', @CWD;
|
||||||
|
chdir $workdir;
|
||||||
|
}
|
||||||
|
|
||||||
|
use lib ("$workdir/lib", "$workdir/vendor_perl", "$workdir/vendor_perl/lib/perl5");
|
||||||
|
no Cwd;
|
||||||
|
use Cwd::utf8 qw (chdir abs_path);
|
||||||
|
use File::Basename qw (basename dirname);
|
||||||
|
use File::Path qw (make_path);
|
||||||
|
use Log::Any qw ($log);
|
||||||
|
use Log::Any::Adapter;
|
||||||
|
use Log::Any::For::Std;
|
||||||
|
use POSIX qw (setgid setuid setsid);
|
||||||
|
use Conf qw (LoadConf);
|
||||||
|
use Craniac qw (RunCraniac);
|
||||||
|
|
||||||
|
sub daemonize;
|
||||||
|
sub __fork;
|
||||||
|
sub __signal_handler;
|
||||||
|
|
||||||
|
local $OUTPUT_AUTOFLUSH = 1;
|
||||||
|
my $runAsUid = $UID;
|
||||||
|
|
||||||
|
my $piddir = '/var/run/' . basename (abs_path ($PROGRAM_NAME));
|
||||||
|
my $C = LoadConf ();
|
||||||
|
my $loglevel = $C->{'loglevel'} // 'notice';
|
||||||
|
my $logfile;
|
||||||
|
|
||||||
|
if (defined $C->{'log'}) {
|
||||||
|
$logfile = $C->{'log'};
|
||||||
|
} elsif (defined $C->{'debug_log'}) {
|
||||||
|
$logfile = $C->{'debug_log'};
|
||||||
|
}
|
||||||
|
|
||||||
|
# if we run under root, create pid dir and set correct owner for it, same for debug log if it is defined
|
||||||
|
if ($UID == 0) {
|
||||||
|
my $user = $C->{user};
|
||||||
|
|
||||||
|
unless (defined $user) {
|
||||||
|
die "Unable to run as root, please define unprivileged user in config.json\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
my ($uid, $gid) = (getpwnam $user)[2,3];
|
||||||
|
|
||||||
|
unless (-d $piddir) {
|
||||||
|
make_path (
|
||||||
|
$piddir, {
|
||||||
|
uid => $uid,
|
||||||
|
group => $gid
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defined $logfile) {
|
||||||
|
my $logdir = dirname $logfile;
|
||||||
|
|
||||||
|
unless (-d $logdir) {
|
||||||
|
make_path (
|
||||||
|
$logdir, {
|
||||||
|
uid => $uid,
|
||||||
|
group => $gid
|
||||||
|
}
|
||||||
|
) or die "Unable to create $logdir: $OS_ERROR\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# drop privs
|
||||||
|
setgid ($gid) or die "Unable to switch to group of $user($OS_ERROR)\n";
|
||||||
|
setuid ($uid) or die "Unable to switch to account $user($OS_ERROR)\n";
|
||||||
|
} else {
|
||||||
|
$piddir = $workdir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defined $logfile) {
|
||||||
|
Log::Any::Adapter->set ('File', $logfile, log_level => $loglevel);
|
||||||
|
|
||||||
|
# Some annoying CHI logging events come with category set to CHI::Driver and log_level set to debug.
|
||||||
|
# Until we use start using Log::Any native features and set whole loglevel accordingly, lets lower
|
||||||
|
# verbosity paticulary for CHI.
|
||||||
|
Log::Any::Adapter->set (
|
||||||
|
{
|
||||||
|
category => 'CHI::Driver'
|
||||||
|
},
|
||||||
|
'File',
|
||||||
|
$logfile,
|
||||||
|
log_level => 'warn'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log::Any::Adapter->set ('File', '/dev/null');
|
||||||
|
}
|
||||||
|
|
||||||
|
# по-умолчанию, демонизируемся, если явно не указано, что этого делать не надо
|
||||||
|
if (defined ($c->{daemonize}) && $c->{daemonize}) {
|
||||||
|
daemonize ();
|
||||||
|
} elsif ( ! defined ($c->{daemonize})) {
|
||||||
|
daemonize ();
|
||||||
|
}
|
||||||
|
|
||||||
|
# write pidfile
|
||||||
|
my $apath = abs_path $PROGRAM_NAME;
|
||||||
|
|
||||||
|
unless (defined $apath) {
|
||||||
|
$log->fatal ("[FATAL] Unable to get abs_path: $OS_ERROR\n");
|
||||||
|
exit 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
my $pidfile = sprintf '%s/%s.pid', $piddir, basename $apath;
|
||||||
|
$log->info ("[INFO] Creating pidfile at $pidfile");
|
||||||
|
|
||||||
|
open my $MYPID, '>', $pidfile or do {
|
||||||
|
$log->fatal ("[FATAL] Error opening $pidfile: $OS_ERROR");
|
||||||
|
exit 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
print {$MYPID} $PID or do {
|
||||||
|
$log->fatal ("[FATAL] Error writing to $pidfile: $OS_ERROR");
|
||||||
|
exit 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
close $MYPID; ## no critic (InputOutput::RequireCheckedSyscalls
|
||||||
|
|
||||||
|
local $SIG{INT} = \&__signal_handler;
|
||||||
|
local $SIG{TERM} = \&__signal_handler;
|
||||||
|
|
||||||
|
# set correct proctitle
|
||||||
|
$PROGRAM_NAME = abs_path $PROGRAM_NAME; ## no critic (Variables::RequireLocalizedPunctuationVars)
|
||||||
|
|
||||||
|
# run main app, at last
|
||||||
|
RunCraniac ();
|
||||||
|
|
||||||
|
if (-f $pidfile) {
|
||||||
|
unlink $pidfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0;
|
||||||
|
|
||||||
|
sub daemonize {
|
||||||
|
__fork ();
|
||||||
|
POSIX::setsid ();
|
||||||
|
open STDIN, '<', '/dev/null'; ## no critic (InputOutput::RequireCheckedSyscalls, InputOutput::RequireCheckedOpen)
|
||||||
|
# we handle STDOUT && STDERR via Log::Any::For::Std, so do not close them explicitly
|
||||||
|
#open STDOUT, '>', '/dev/null'; ## no critic (InputOutput::RequireCheckedSyscalls, InputOutput::RequireCheckedOpen)
|
||||||
|
#open STDERR, '>', '/dev/null'; ## no critic (InputOutput::RequireCheckedSyscalls, InputOutput::RequireCheckedOpen)
|
||||||
|
__fork ();
|
||||||
|
umask 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub __fork {
|
||||||
|
my $pid = fork;
|
||||||
|
|
||||||
|
unless (defined $pid) {
|
||||||
|
$log->fatal ("[FATAL] Can't fork: $OS_ERROR");
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pid != 0) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub __signal_handler {
|
||||||
|
$log->info ("[INFO] Caught a signal $OS_ERROR");
|
||||||
|
|
||||||
|
if (-f $pidfile) {
|
||||||
|
unlink $pidfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
__END__
|
||||||
|
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:
|
||||||
5
bootstrap.sh
Normal file
5
bootstrap.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cpanm -l vendor_perl --self-contained --force --installdeps .
|
||||||
|
|
||||||
|
# vim: set ft=sh noet ai ts=4 sw=4 sts=4:
|
||||||
18
cpanfile
Normal file
18
cpanfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
requires 'Cwd';
|
||||||
|
requires 'Cwd::utf8';
|
||||||
|
requires 'DateTime';
|
||||||
|
requires 'Digest::SHA';
|
||||||
|
requires 'English';
|
||||||
|
requires 'File::Basename';
|
||||||
|
requires 'File::Path';
|
||||||
|
requires 'Hailo';
|
||||||
|
requires 'IO::Socket::SSL';
|
||||||
|
requires 'local::lib';
|
||||||
|
requires 'Log::Any';
|
||||||
|
requires 'Log::Any::Adapter';
|
||||||
|
requires 'Log::Any::For::Std';
|
||||||
|
requires 'Math::Random::Secure';
|
||||||
|
requires 'Mojo::IOLoop';
|
||||||
|
requires 'Mojo::Redis';
|
||||||
|
requires 'POSIX';
|
||||||
|
requires 'Text::Fuzzy';
|
||||||
21
data/config_sample.json
Normal file
21
data/config_sample.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
# server address
|
||||||
|
"server" : "redis",
|
||||||
|
"port" : "6379",
|
||||||
|
|
||||||
|
# should we fork() and become unix daemon?
|
||||||
|
"daemonize": 1,
|
||||||
|
|
||||||
|
# where caches go there
|
||||||
|
"cachedir" : "data/cache",
|
||||||
|
|
||||||
|
# unprivileged system user to run bot as
|
||||||
|
"user" : "nobody",
|
||||||
|
|
||||||
|
# logs goes there
|
||||||
|
"log" : "/var/log/aleesa-craniac/aleesa-craniac.log",
|
||||||
|
"loglevel": "notice",
|
||||||
|
|
||||||
|
# ditto
|
||||||
|
"pid" : "/var/run/aleesa-craniac/aleesa-craniac.pid",
|
||||||
|
}
|
||||||
206
lib/Craniac.pm
Normal file
206
lib/Craniac.pm
Normal 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:
|
||||||
Reference in New Issue
Block a user