From 6956655fb3f77937690bdb2c40df5fb066785081 Mon Sep 17 00:00:00 2001 From: eleksir Date: Wed, 25 Aug 2021 17:48:44 +0300 Subject: [PATCH] Initial commit --- .gitignore | 9 ++ .perlcriticrc | 97 ++++++++++++++++++ LICENSE | 127 +++++++++++++++++++++++ README | 67 ++++++++++++ TODO | 28 +++++ bin/aleesa-misc | 184 +++++++++++++++++++++++++++++++++ bootstrap.sh | 5 + contrib/aleesa-misc.openrc | 11 ++ cpanfile | 24 +++++ data/config_sample.json | 27 +++++ lib/Conf.pm | 37 +++++++ lib/Misc.pm | 205 +++++++++++++++++++++++++++++++++++++ 12 files changed, 821 insertions(+) create mode 100644 .gitignore create mode 100644 .perlcriticrc create mode 100644 LICENSE create mode 100644 README create mode 100644 TODO create mode 100755 bin/aleesa-misc create mode 100644 bootstrap.sh create mode 100644 contrib/aleesa-misc.openrc create mode 100644 cpanfile create mode 100644 data/config_sample.json create mode 100644 lib/Conf.pm create mode 100644 lib/Misc.pm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20e62d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +_Deparsed_XSubs.pm +.idea/** +.vscode/** +.vstags +*.pid +*.log +data/cache/** +data/config.json +vendor_perl/** diff --git a/.perlcriticrc b/.perlcriticrc new file mode 100644 index 0000000..4045ea6 --- /dev/null +++ b/.perlcriticrc @@ -0,0 +1,97 @@ +#!/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 + +[-Variables::ProhibitPackageVars] +# fuck off \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ff3f42 --- /dev/null +++ b/LICENSE @@ -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 diff --git a/README b/README new file mode 100644 index 0000000..01cb5e8 --- /dev/null +++ b/README @@ -0,0 +1,67 @@ +aleesa-misc +----------------------------------------------------------------------------------------------------------------------- +Основной роутер команд. Все протокольные плагины шлют команды сюда, а отсюда команды разлетаются в модули-исполнители и +ответ от исполнителей идёт обратно заказчику. +Работает по протоколу Redis pubsub. + + +Механика работы +----------------------------------------------------------------------------------------------------------------------- +Роутер подписан на канал misc. + +Модули, реализующие "клиентские протоколы" - telegram, irc, jabber и проч, подписаны на канал своего протокола. Входящие +сообщения они получают только через этот канал. + +Модули, реализующие иную функциональность также подписаны каждый на свой канал. Кроме того, для реализации +дополнительных синхронных возможностей, они могут слушать например http запросы на отдельных - ip:портах. Но это уже +модуль-специфичная настройка. + +Формат протокола - это json с полями: +from - модуль (по сути, redis-канал), в который надо ответить +chatid - чат, из которого прилетело сообщение +userid - пользователь, на чью фразу мы отвечаем, или пользователь, для которого предназначается ответ +message - текст сообщения +plugin - первоначальный источник (по сути, redis-канал) сообщения. +mode - public или private + +misc - объект с дополнительными полями: + +misc->answer - 0 если ответ не требуется и 1 если ответ требуется (использован в модуле craniac), значение + по-умолчанию 1, если поля нет. + +misc->bot_nick - ник бота. Типа как, для разных чятов он может быть разный. (Использовано в модуле craniac.) Если + поле отсутствует, проверка на обращение к боту по нику не происходит. + +misc->csign - command sign - значок, которым предваряется команда в чятике.Исторически сложилось так, что для + разных ботов этот символ был разный, например, eggdrop использовал точку, а sulci - восклицательный + знак. По-умолчанию, aleesa-bot вездепроставляет восклицательный знак, если во входящем сообщении + такой настройки нету. Изначально настройка задаётся в конфиге чат-модуля (aleesa-telegram, + например). + +misc->fwd_cnt - количество пересылок сообщения. Значение по-умолчанию 5. Предполагается, что при пересылке + сообщения,модуль будет инкрементировать это значение и заодно проверять, что оно не превышает + настройку forwards_max в конфиге или 5, если в конфиге такой настройки нету. + +misc->msg_format - Использовать форматирование в сообщении. На текущий момент этот флажок выставляется в модуле + telegram и благополучно проходит через модуль misc, далее попадает в модуль phrases, и, наконец, + возвращается в модуль telegram, чтобы выходное сообщение было отослано как отформатированное. + +Как это запустить? +----------------------------------------------------------------------------------------------------------------------- +Понадобится perl, cpanm и local::lib, а также несколько разных devel-пакетов для разных библиотек. Как правило, это +находится методом тыка, но точно понадобятся zlib-devel, openssl-devel, hiredis-devel. Кроме того, понадобятся +development tools - gcc, gcc-g++, perl-devel, make, cmake и так далее. + +Приложение тестировалось на alpine linux x86_64 3.12, со встроенным perl-5.32.1 + +В каталоге с приложением под целевым пользователем запускаем bash bootstrap.sh Ждём с надеждой, что оно не обломится в +пути. + +Если всё хорошо, то после создания data/config.json из data/config_sample.json и можно запускать приложение. Оно +запустится как unix daemon. + +В каталоге contrib находится скрипт для alpine linux, системы инициализации openrc. Этот скрипт достаточно положить В +/etc/init.d/aleesa-misc сконфигурить сервис по инструкции выше и после этого сервис запускается командой: +service aleesa-misc start +в автозагрузку его можно прописать через команду +rc-update add aleesa-misc default diff --git a/TODO b/TODO new file mode 100644 index 0000000..5e289cb --- /dev/null +++ b/TODO @@ -0,0 +1,28 @@ +Реализовать парсер для команды !admin : +* Вероятно, попротокольно, так как эта команда есть только в telegram, но формально должна появиться в jabber и irc. +* Понадобится отдельный способ запросить "протокольный" плагин на тему админ ли чел, который хочет использовать + команду !admin. +* Для телеги у нас есть возможность автоматически определять админов. +* Для IRC - есть возможность определить op-ов. (+ к тому нужно следить за сменой статуса - парсить соответствующие + ивенты). +* Для jabber - пока непонятно. + +Брать каналы из конфига. Те самые redis-каналы, в которые надо отсылать сообщения. Продумать вопрос с оптимальным форматом конфига. + Пока что он видится в фомате хэша + { + channels : { + telegram : telegram, + craniac : craniac, + irc : irc, + webapp : webapp, + ... + } + } +но не на всех этих каналах надо слушать, в какие-то достаточно только писать. + +Реализовать возможность релоада конфига. Но для начала понять, нужно ли оно нам? + +Решить вопрос с Cron-ом. Будет ли это отдельный демонок либо эти функции будут в роутере (чего наверно не хотелось бы). +В Cron у нас войдёт как минимум утренняя фортунка и скорее всего rss, когда появится. + +Проверить момент, когда при старте сервиса redis ещё не запущен. diff --git a/bin/aleesa-misc b/bin/aleesa-misc new file mode 100755 index 0000000..decf2ec --- /dev/null +++ b/bin/aleesa-misc @@ -0,0 +1,184 @@ +#!/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; + +# До фактического запуска логики программы, сменим рабочий каталог, чтобы подцепить либы +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 Mojo::Log; +use POSIX qw (setgid setuid setsid); +use Conf qw (LoadConf); +use Misc qw (RunMisc); + +sub daemonize; +sub __fork; +sub __signal_handler; + +local $OUTPUT_AUTOFLUSH = 1; +my $runAsUid = $UID; + +my $piddir = '/var/run/' . basename (abs_path ($PROGRAM_NAME)); +my $CONF = LoadConf (); +my $loglevel = $CONF->{'loglevel'} // 'info'; +my $logfile; +my $log; + +if (defined $CONF->{'log'}) { + $logfile = $CONF->{'log'}; +} elsif (defined $CONF->{'debug_log'}) { + $logfile = $CONF->{'debug_log'}; +} + +# Если мы запускаемся под рутом, то попробуем создать необходимые служебные каталоги и простать на них корректных +# владельца и права +if ($UID == 0) { + my $user = $CONF->{'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"; + } + } + + # Переключаемся на "рабочего" пользователя и сбрасываем привилегии. + 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 = Mojo::Log->new (path => $logfile, level => $loglevel); +} else { + $log = Mojo::Log->new (path => '/dev/null', level => 'fatal'); +} + +# По-умолчанию, демонизируемся, если явно не указано, что этого делать не надо +if (defined ($CONF->{'daemonize'}) && $CONF->{'daemonize'}) { + daemonize (); +} elsif ( ! defined ($CONF->{'daemonize'})) { + daemonize (); +} + +# Запишем pid-файл +my $apath = abs_path $PROGRAM_NAME; + +unless (defined $apath) { + $log->fatal ("[FATAL] Unable to get abs_path: $OS_ERROR\n"); + exit 1; +}; + +our $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) + +# При наличии хака в лице Mojo::IOLoop::Signal в Misc.pm, установка хэндлера в этом месте - это фактически +# подстраховка на случай ядерной войны. +local $SIG{INT} = \&__signal_handler; +local $SIG{TERM} = \&__signal_handler; + +# Поставим правильное имя процесса в proctitle +$PROGRAM_NAME = abs_path $PROGRAM_NAME; ## no critic (Variables::RequireLocalizedPunctuationVars) + +# И запустим уже наш сервис +RunMisc (); + +if (-f $pidfile) { + unlink $pidfile; +} + +exit 0; + +sub daemonize { + __fork (); + POSIX::setsid (); + open STDIN, '<', '/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: diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..eb5740a --- /dev/null +++ b/bootstrap.sh @@ -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: diff --git a/contrib/aleesa-misc.openrc b/contrib/aleesa-misc.openrc new file mode 100644 index 0000000..751365a --- /dev/null +++ b/contrib/aleesa-misc.openrc @@ -0,0 +1,11 @@ +#!/sbin/openrc-run + +name="$SVCNAME" +command="/var/lib/$SVCNAME/bin/$SVCNAME" +pidfile="/var/run/$SVCNAME/$SVCNAME.pid" + +depend() { + need net redis + use dns + after redis +} diff --git a/cpanfile b/cpanfile new file mode 100644 index 0000000..157dfd6 --- /dev/null +++ b/cpanfile @@ -0,0 +1,24 @@ +requires 'Carp', '0'; +requires 'Cwd', '0'; +requires 'Cwd::utf8', '==0.011'; +requires 'Data::Dumper', '==2.183'; +requires 'Encode', '==3.16'; +requires 'English', '==1.11'; +requires 'EV', '==4.33'; +requires 'Exporter', '==5.74'; +requires 'File::Basename', '0'; +requires 'File::Path', '==2.18'; +requires 'IO::Socket::SSL', '==2.072'; +requires 'JSON', '==4.03'; +requires 'JSON::XS', '==4.03'; +requires 'local::lib', '==2.000028'; +requires 'Math::Random::Secure', '==0.080001'; +requires 'Mojolicious', '==9.22'; +requires 'Mojo::Redis', '==3.27'; +requires 'Mojo::IOLoop::Signal', '==0.003'; +requires 'Mojo::Log', '0'; +requires 'open', '0'; +requires 'POSIX', '0'; +requires 'Protocol::Redis::XS', '0'; +requires 'utf8', '0'; +requires 'version', '0'; \ No newline at end of file diff --git a/data/config_sample.json b/data/config_sample.json new file mode 100644 index 0000000..12ff99e --- /dev/null +++ b/data/config_sample.json @@ -0,0 +1,27 @@ +{ + # redis address + "server" : "redis", + "port" : "6379", + "channels": [ + "misc" + ], + + # should we fork() and become unix daemon? + "daemonize": 1, + + # unprivileged system user to run bot as + "user" : "nobody", + + # messages with commands will begin with this symbol + "csign": "!", + + # logs goes there + "log" : "/var/log/aleesa-misc/aleesa-misc.log", + "loglevel": "info", + + # ditto + "pid" : "/var/run/aleesa-misc/aleesa-misc.pid", + + # maximal amount of message forwardings, to avoid endless loops + "forwards_max" : 5 +} diff --git a/lib/Conf.pm b/lib/Conf.pm new file mode 100644 index 0000000..9f6928c --- /dev/null +++ b/lib/Conf.pm @@ -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); +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: diff --git a/lib/Misc.pm b/lib/Misc.pm new file mode 100644 index 0000000..8a87311 --- /dev/null +++ b/lib/Misc.pm @@ -0,0 +1,205 @@ +package Misc; + +# Общие модули - синтаксис, кодировки итд +use 5.018; +use strict; +use warnings; +use utf8; +use open qw (:std :utf8); + +# Модули для работы приложения +use Math::Random::Secure qw (irand); +# Чтобы "уж точно" использовать hiredis-биндинги, загрузим этот модуль перед Mojo::Redis +use Protocol::Redis::XS; +use Mojo::Redis; +use Mojo::IOLoop; +use Mojo::IOLoop::Signal; +use Mojo::Log; +use Data::Dumper; + +use Conf qw (LoadConf); + +use version; our $VERSION = qw (1.0); +use Exporter qw (import); +our @EXPORT_OK = qw (RunMisc); + +my $c = LoadConf (); + +my $loglevel = $c->{'loglevel'} // 'info'; +my $fwd_cnt = $c->{'forward_max'} // 5; +my $logfile; +my $log; + +if (defined $c->{'log'}) { + $logfile = $c->{'log'}; +} elsif (defined $c->{'debug_log'}) { + $logfile = $c->{'debug_log'}; +} + +if (defined $logfile) { + $log = Mojo::Log->new (path => $logfile, level => $loglevel); +} else { + $log = Mojo::Log->new (path => '/dev/null', level => 'fatal'); +} + +# Основной парсер +my $parse_message = sub { + my $self = shift; + my $m = shift; + my $answer = $m; + $answer->{from} = 'misc'; + + if (defined $answer->{misc}) { + unless (defined $answer->{misc}->{fwd_cnt}) { + $answer->{misc}->{fwd_cnt} = 1; + } else { + if ($answer->{misc}->{fwd_cnt} > $fwd_cnt) { + $log->error ('Forward loop detected, discarding message.'); + $log->debug (Dumper $m); + return; + } else { + $answer->{misc}->{fwd_cnt}++; + } + } + + unless (defined $answer->{misc}->{answer}) { + $answer->{misc}->{answer} = 1; + } + + unless (defined $answer->{misc}->{csign}) { + $answer->{misc}->{csign} = '!'; + } + } else { + $answer->{misc}->{answer} = 1; + $answer->{misc}->{csign} = '!'; + $answer->{misc}->{msg_format} = 0; + } + + # Если сообщение не попало ни под один шаблон, оно отправляется к craniac-у + my $send_to = 'craniac'; + + $log->debug ('[DEBUG] Incoming message ' . Dumper($m)); + + # Пробуем найти команды. Сообщения, начинающиеся с $answer->{misc}->{csign} + if (substr ($m->{message}, 0, 1) eq $answer->{misc}->{csign}) { + my $done = 0; + my @phrases_cmd = qw (friday пятница proverb пословица fortune фортунка f ф karma карма); + + while (my $cmd = pop @phrases_cmd) { + if (substr ($m->{message}, 1) eq $cmd) { + $send_to = 'phrases'; + $done = 1; + last; + } + } + + unless ($done) { + my @webapp_cmd = qw (buni anek анек анекдот cat кис drink праздник fox лис frog лягушка horse лошадь лошадка + monkeyuser rabbit bunny кролик snail улитка owl сова сыч xkcd tits boobs tities boobies + сиси сисечки butt booty ass попа попка); + + while (my $cmd = pop @webapp_cmd) { + if (substr ($m->{message}, 1) eq $cmd) { + $send_to = 'webapp'; + $done = 1; + last; + } + } + } + + unless ($done) { + if (substr ($m->{message}, 1, 2) eq 'w ' || substr ($m->{message}, 1, 2) eq 'п ') { + $send_to = 'webapp'; + $done = 1; + } elsif (substr ($m->{message}, 1, 8) eq 'weather ' || substr ($m->{message}, 1, 8) eq 'погодка ') { + $send_to = 'webapp'; + $done = 1; + } elsif (substr ($m->{message}, 1, 7) eq 'погода ' || substr ($m->{message}, 1, 8) eq 'погадка ') { + $send_to = 'webapp'; + $done = 1; + } + } + # Попробуем найти изменение кармы + } elsif (substr ($m->{message}, -2) eq '++' || substr ($m->{message}, -2) eq '--') { + my @arr = split /\n/, $m->{message}; + + # Предполагается, что фраза - это только одна строка + if ($#arr < 1) { + $send_to = 'phrases'; + } + } + + $log->debug ("[DEBUG] Sending message to channel $send_to " . Dumper ($answer)); + + $self->json ($send_to)->notify ( + $send_to => $answer + ); + + return; +}; + +my $__signal_handler = sub { + my ($self, $name) = @_; + $log->info ("[INFO] Caught a signal $name"); + + if (defined $main::pidfile && -f $main::pidfile) { + unlink $main::pidfile; + } + + exit 0; +}; + + +# main loop, он же event loop +sub RunMisc { + $log->info ("[INFO] Connecting to $c->{server}, $c->{port}"); + + my $redis = Mojo::Redis->new ( + sprintf 'redis://%s:%s/1', $c->{server}, $c->{port} + ); + + $log->info ('[INFO] Registering connection-event callback'); + + $redis->on ( + connection => sub { + my ($r, $connection) = @_; + + $log->info ('[INFO] Triggering callback on new client connection'); + + # Залоггируем ошибку, если соединение внезапно порвалось. + $connection->on ( + error => sub { + my ($conn, $error) = @_; + $log->error ("[ERROR] Redis connection error: $error"); + return; + } + ); + + return; + } + ); + + my $pubsub = $redis->pubsub; + my $sub; + $log->info ('[INFO] Subscribing to redis channels'); + + foreach my $channel (@{$c->{channels}}) { + $log->debug ("[DEBUG] Subscribing to $channel"); + + $sub->{$channel} = $pubsub->json ($channel)->listen ( + $channel => sub { $parse_message->(@_); } + ); + } + + Mojo::IOLoop::Signal->on ( + TERM => $__signal_handler, + INT => $__signal_handler + ); + + do { Mojo::IOLoop->start } until Mojo::IOLoop->is_running; + return; +} + +1; + +# vim: set ft=perl noet ai ts=4 sw=4 sts=4: