From 887d7ada897882e3e490e009206b49952c8d87cb Mon Sep 17 00:00:00 2001 From: Sergei Fedosov Date: Thu, 19 Aug 2021 19:26:42 +0300 Subject: [PATCH] [WIP] Initial commit --- .gitignore | 8 + .perlcriticrc | 95 +++++++++ LICENSE | 127 +++++++++++ TODO | 4 + bin/aleesa-irc | 197 +++++++++++++++++ bootstrap.sh | 5 + cpanfile | 17 ++ data/config_sample.json | 37 ++++ lib/Conf.pm | 37 ++++ lib/IRCApp.pm | 462 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 989 insertions(+) create mode 100644 .gitignore create mode 100644 .perlcriticrc create mode 100644 LICENSE create mode 100644 TODO create mode 100755 bin/aleesa-irc create mode 100644 bootstrap.sh create mode 100644 cpanfile create mode 100644 data/config_sample.json create mode 100644 lib/Conf.pm create mode 100644 lib/IRCApp.pm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95ccf19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +_Deparsed_XSubs.pm +.idea/** +.vscode/** +.vstags +*.pid +*.log +data/config.json +vendor_perl/** diff --git a/.perlcriticrc b/.perlcriticrc new file mode 100644 index 0000000..41f004d --- /dev/null +++ b/.perlcriticrc @@ -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 + 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/TODO b/TODO new file mode 100644 index 0000000..10764f2 --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +Исправить: +[Sun Aug 22 12:37:23 2021] (in cleanup) Can't call method "unsubscribe" on an undefined value at /root/aleesa-bot/aleesa-irc/vendor_perl/lib/perl5/Mojo/EventEmitter.pm line 34 during global destruction. + +Это происходит при завершении работы. Предположительно исправляется с помощью хэндлера SIGINT и SIGKILL. Выйти с каналов и завершить соединение. diff --git a/bin/aleesa-irc b/bin/aleesa-irc new file mode 100755 index 0000000..f213cc5 --- /dev/null +++ b/bin/aleesa-irc @@ -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 IRCApp qw (RunIRCApp); + +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 bot, at last +RunIRCApp (); + +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: 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/cpanfile b/cpanfile new file mode 100644 index 0000000..7410f6c --- /dev/null +++ b/cpanfile @@ -0,0 +1,17 @@ +requires 'Carp'; +requires 'Cwd::utf8'; +requires 'Date::Format::ISO8601'; +requires 'Encode'; +requires 'EV'; +requires 'File::Basename'; +requires 'File::Path'; +requires 'IO::Socket::SSL'; +requires 'JSON'; +requires 'JSON::XS'; +requires 'local::lib'; +requires 'Log::Any'; +requires 'Log::Any::Adapter'; +requires 'Log::Any::For::Std'; +requires 'Mojo::IRC'; +requires 'Mojo::Redis'; +requires 'POSIX'; diff --git a/data/config_sample.json b/data/config_sample.json new file mode 100644 index 0000000..0f7de09 --- /dev/null +++ b/data/config_sample.json @@ -0,0 +1,37 @@ +{ + # таймаут подключения задаётся переменной MOJO_IRC_CONNECT_TIMEOUT, в секундах, что есть форменное пидорство + "server" : "localhost", + + "port" : 6667, + + # 0 если ssl не нужен и 1 - если нужен. Проверка ssl сертификата выключена, так как нам надо работать + "ssl" : 0, + + # к нему же мы приводим и user и real name + "nick" : "aleesa", + + # каналы, на которые заходит бот. предполагается, что бот заходит на каналы в обязательном порядке + "channels": [ + "#a", + "#b" + ], + + # редиска, через которую бот общается с другини компонентами + "redisaddr" : "redis", + "redisport" : 6379, + + # 1 если запускаемся как unix daemon и 0 - если нет + "daemonize" : 0, + + # обычный пользователь, от имени которого запускается приложение + "user" : "nobody", + + # команды бота начинаются с этого символа, крайне желательно, чтобы он был одинаковым на всех компонентах + "csign": "!", + + # в этот файл пишем логи + "log" : "/var/log/aleesa-irc/aleesa-irc.log", + + # ditto + "pid" : "/var/run/aleesa-irc/aleesa-irc.pid" +} diff --git a/lib/Conf.pm b/lib/Conf.pm new file mode 100644 index 0000000..8708c3e --- /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 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: diff --git a/lib/IRCApp.pm b/lib/IRCApp.pm new file mode 100644 index 0000000..616c7c2 --- /dev/null +++ b/lib/IRCApp.pm @@ -0,0 +1,462 @@ +package IRCApp; +# TODO: настроить в логгировании loglevel, всякие [ERRO] [WARN] [INFO] итд +# общие модули - синтаксис, кодировки итд +use 5.018; +use strict; +use warnings; +use utf8; +use open qw (:std :utf8); + +# модули для работы приложения +use Date::Format::ISO8601 qw (gmtime_to_iso8601_datetime); +use Log::Any qw ($log); +use Mojo::Redis; +use Mojo::IOLoop; +use Mojo::IRC; +use Data::Dumper; + +use Conf qw (LoadConf); + +use version; our $VERSION = qw (1.0); +use Exporter qw (import); +our @EXPORT_OK = qw (RunIRCApp); + +my $c = LoadConf (); +my $identified = 0; + +# основной парсер +my $parse_redis_message = sub { + my $self = shift; + my $m = shift; + my $answer = $m; + my $send_to = $answer->{from}; + + $self->json ($send_to)->notify ( + $send_to => { + from => $answer->{from}, + userid => $answer->{userid}, + chatid => $answer->{chatid}, + plugin => $answer->{plugin}, + message => $answer->{message} + } + ); + + return; +}; + +my $on_irc_privmsg = sub { + my $self = shift; + my $m = shift; + + my $reply; + + if (substr ($text, 1, 3) eq 'rum' || substr ($text, 1, 3) eq 'ром') { + my $target; + + if ((length ($text) > 5)) { + $target = trim (substr $text, 5); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'притаскивает на подносе стопку рома для ' . $target . ', края стопки искрятся кристаллами соли.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 5) eq 'vodka' || substr ($text, 1, 5) eq 'водка') { + my $target; + + if ((length ($text) > 7)) { + $target = trim (substr $text, 7); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'подаёт шот водки с небольшим маринованным огурчиком на блюдце для ' . $target . '. Из огурчика торчит небольшая вилочка.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 4) eq 'beer' || substr ($text, 1, 4) eq 'пиво') { + my $target; + + if ((length ($text) > 6)) { + $target = trim (substr $text, 6); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'бахает об стол перед ' . $target . ' кружкой холодного пива, часть пенной шапки сползает по запотевшей стенке кружки.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 7) eq 'tequila') { + my $target; + + if ((length ($text) > 9)) { + $target = trim (substr $text, 9); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'ставит рядом с ' . $target . ' шот текилы, аккуратно на ребро стопки насаживает дольку лайма и ставит кофейное блюдце с горочкой соли.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 6) eq 'текила') { + my $target; + + if ((length ($text) > 8)) { + $target = trim (substr $text, 8); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'ставит рядом с ' . $target . ' шот текилы, аккуратно на ребро стопки насаживает дольку лайма и ставит кофейное блюдце с горочкой соли.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 6) eq 'whisky') { + my $target; + + if ((length ($text) > 8)) { + $target = trim (substr $text, 8); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'демонстративно достаёт из морозилки пару кубических камушков, бросает их в толстодонный стакан и аккуратно наливает Jack Daniels. Запускает стакан вдоль барной стойки, он останавливается около ' . $target . '.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 5) eq 'виски') { + my $target; + + if ((length ($text) > 7)) { + $target = trim (substr $text, 7); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'демонстративно достаёт из морозилки пару кубических камушков, бросает их в толстодонный стакан и аккуратно наливает Jack Daniels. Запускает стакан вдоль барной стойки, он останавливается около ' . $target . '.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 8) eq 'absinthe') { + my $target; + + if ((length ($text) > 10)) { + $target = trim (substr $text, 10); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'наливает абсент в стопку. Смочив кубик сахара в абсенте кладёт его на дырявую ложечку и пожигает. Как только пламя потухнет, ' . $c->{nick} . ' размешивает оплавившийся кубик в абсенте и подносит стопку ' . $target . '.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 6) eq 'абсент') { + my $target; + + if ((length ($text) > 8)) { + $target = trim (substr $text, 8); + } + + if (! defined $target || ($target eq '')) { + $target = $chattername; + } + + $reply = 'наливает абсент в стопку. Смочив кубик сахара в абсенте кладёт его на дырявую ложечку и пожигает. Как только пламя потухнет, ' . $c->{nick} . ' размешивает оплавившийся кубик в абсенте и подносит стопку ' . $target . '.'; + $bot->send_long_message ('utf8', 0, "PRIVMSG\001ACTION", $chatid, $reply); + return; + } elsif (substr ($text, 1, 2) eq 'w ' || substr ($text, 1, 2) eq 'п ') { + my $city = substr $text, 2; + $reply = Weather ($city) =~ tr/\n/ /r; + } elsif (substr ($text, 1) eq 'anek' || substr ($text, 1) eq 'анек' || substr ($text, 1) eq 'анекдот' ) { + $reply = Anek (); + } elsif (substr ($text, 1) eq 'coin' || substr ($text, 1) eq 'монетка') { + if (rand (101) < 0.016) { + $reply = 'ребро'; + } else { + if (irand (2) == 0) { + if (irand (2) == 0) { + $reply = 'орёл'; + } else { + $reply = 'аверс'; + } + } else { + if (irand (2) == 0) { + $reply = 'решка'; + } else { + $reply = 'реверс'; + } + } + } + } elsif (substr ($text, 1) eq 'roll' || substr ($text, 1) eq 'dice' || substr ($text, 1) eq 'кости') { + $reply = sprintf 'На первой кости выпало %d, а на второй — %d.', irand (6) + 1, irand (6) + 1; + } elsif (substr ($text, 1) eq 'version' || substr ($text, 1) eq 'ver') { + $reply = 'Версия нуль.чего-то_там.чего-то_там'; + } elsif (substr ($text, 1) eq 'lat' || substr ($text, 1) eq 'лат') { + $reply = Lat (); + } elsif (substr ($text, 1) eq 'cat' || substr ($text, 1) eq 'кис') { + $reply = Kitty (); + } elsif (substr ($text, 1) eq 'fox' || substr ($text, 1) eq 'лис') { + $reply = Fox (); + } elsif (substr ($text, 1) eq 'frog' || substr ($text, 1) eq 'лягушка') { + $reply = Frog (); + } elsif (substr ($text, 1) eq 'horse' || substr ($text, 1) eq 'лошадь' || substr ($text, 1) eq 'лошадка') { + $reply = Horse (); + } elsif (substr ($text, 1) eq 'snail' || substr ($text, 1) eq 'улитка') { + $reply = Snail (); + } elsif (substr ($text, 1) eq 'dig' || substr ($text, 1) eq 'копать') { + $reply = Dig ($chattername); + } elsif (substr ($text, 1) eq 'fish' || substr ($text, 1) eq 'fisher' || substr ($text, 1) eq 'рыба' || substr ($text, 1) eq 'рыбка' || substr ($text, 1) eq 'рыбалка') { + $reply = Fish ($chattername); + } elsif (substr ($text, 1) eq 'buni') { + $reply = Buni (); + } elsif (substr ($text, 1) eq 'xkcd') { + $reply = Xkcd (); + } elsif (substr ($text, 1) eq 'monkeyuser') { + $reply = Monkeyuser (); + } elsif (substr ($text, 1) eq 'drink' || substr ($text, 1) eq 'праздник') { + $reply = Drink (); + } elsif (substr ($text, 1) eq 'bunny' || substr ($text, 1) eq 'rabbit' || substr ($text, 1) eq 'кролик') { + $reply = Rabbit (); + } elsif (substr ($text, 1) eq 'owl' || substr ($text, 1) eq 'сова') { + $reply = Owl (); + } elsif ((length ($text) >= 6 && (substr ($text, 1, 6) eq 'karma ' || substr ($text, 1, 6) eq 'карма ')) || substr ($text, 1) eq 'karma' || substr ($text, 1) eq 'карма') { + my $mytext = ''; + + if (length ($text) > 6) { + $mytext = substr $text, 7; + chomp $mytext; + $mytext = trim $mytext; + } else { + $mytext = ''; + } + + $reply = KarmaGet ($chatid, $mytext); + } elsif (substr ($text, 1) eq 'friday' || substr ($text, 1) eq 'пятница') { + $reply = Friday (); + } elsif (substr ($text, 1) eq 'proverb' || substr ($text, 1) eq 'пословица') { + $reply = Proverb (); + } elsif (substr ($text, 1) eq 'fortune' || substr ($text, 1) eq 'фортунка' || substr ($text, 1) eq 'f' || substr ($text, 1) eq 'ф') { + my $phrase = Fortune (); + $reply = $phrase; + } elsif (substr ($text, 1) eq 'ping') { + $reply = 'Pong.'; + } elsif (substr ($text, 1) eq 'пинг') { + $reply = 'Понг.'; + } elsif (substr ($text, 1) eq 'kde' || substr ($text, 1) eq 'кде') { + my @phrases = ( + 'Нет, я не буду поднимать вам плазму.', + 'Повторяйте эту мантру по утрам не менее 5 раз: "Плазма не падает." И, возможно, она перестанет у вас падать.' + ); + + $reply = $phrases[irand ($#phrases + 1)]; + } elsif (substr ($text, 1) eq '=(' || substr ($text, 1) eq ':(' || substr ($text, 1) eq '):') { + $reply = ':)'; + } elsif (substr ($text, 1) eq '=)' || substr ($text, 1) eq ':)' || substr ($text, 1) eq '(:') { + $reply = ':D'; + } + + $log->warn (Dumper ($m)); + return; +}; + +my $on_ctcp_source = sub { + my $self = shift; + my $msg = shift; + my $response_to = (split /!/, $msg->{prefix}, 2)[0]; + $self->write (NOTICE => $response_to => $self->ctcp (SOURCE => 'https://github.com/aleesa-bot/aleesa-irc')); + return; +}; + +my $on_ctcp_finger = sub { + my $self = shift; + my $msg = shift; + my $response_to = (split /!/, $msg->{prefix}, 2)[0]; + $self->write (NOTICE => $response_to => $self->ctcp (FINGER => $c->{nick})); + return; +}; + +my $on_ctcp_ping = sub { + my $self = shift; + my $msg = shift; + my $response_to = (split /!/, $msg->{prefix}, 2)[0]; + $self->write (NOTICE => $response_to => $self->ctcp (PING => $msg->{params}->[1])); + return; +}; + +my $on_ctcp_time = sub { + my $self = shift; + my $msg = shift; + my $response_to = (split /!/, $msg->{prefix}, 2)[0]; + $self->write (NOTICE => $response_to => $self->ctcp (TIME => gmtime_to_iso8601_datetime (time ()))); + return; +}; + +my $on_ctcp_clientinfo = sub { + my $self = shift; + my $msg = shift; + my $response_to = (split /!/, $msg->{prefix}, 2)[0]; + $self->write (NOTICE => $response_to => $self->ctcp (CLIENTINFO => 'ACTION CLIENTINFO FINGER PING SOURCE TIME USERINFO VERSION')); + return; +}; + +my $on_irc_end_of_motd = sub { + my $self = shift; + + # Идентифицируемся в NickServ, если надо + if (defined ($c->{identify}) && $c->{identify} ne '') { + # TODO: invent heuristics to detect different login|identify commands depending on particular irc net or + # known ircd detected during login procedure + # Эта команда используется на фриноде (мир праху её) и libera.chat + $self->write ( qw (PRIVMSG NickServ IDENTIFY $c->{nick} $c->{identify}), \&_on_nickserv_identify); + + until ($identified) { ## no critic (ControlStructures::ProhibitUntilBlocks) + sleep 1; + } + } + + # А теперь заходим на все интересующие нас каналы + foreach my $chan (@{$c->{channels}}) { + $self->write ( ('JOIN', $chan), \&_on_join_channel); + } + return; +}; + +sub _on_nickserv_identify { + my $self = shift; + my $error = shift; + + if ($error) { + $log->error ("NickServ identify failed: $error"); + exit 1; + } else { + $log->info ('Identified with Nickserv'); + $identified = 1; + } + + return; +} + +sub _on_join_channel { + my $self = shift; + my $error = shift; + + if ($error) { + $log->error ("Unable to join: $error"); + } else { + $log->info ('Joined'); + } + + return; +} + +# зовётся в момент "после коннекта" +sub _on_connect { + my $self = shift; + my $error = shift; + + if ($error) { + $log->error ("Unable to connect: $error"); + exit 1; + } else { + $log->info ('Connected.'); + } + + return; +} + +sub _on_disconnect { + my $self = shift; + my $error = shift; + + if ($error) { + $log->error ("Unable to Disconnect: $error"); + } else { + $log->info ('Disconnected.'); + } + + return; +} + + +# main loop, он же event loop +sub RunIRCApp { + my $redis = Mojo::Redis->new ( + sprintf 'redis://%s:%s/1', $c->{server}, $c->{port} + ); + + my $pubsub = $redis->pubsub; + my $sub; + + $pubsub->listen ( + # Вот такая ебическая конструкция для авто-подписывания на все новые каналы. + # Странное ограничение, при котором на шаблон каналов можно подписаться только, подписавшись на каждый из + # каналов. То есть подписка создаётся по запросу. В AnyEvent::Redis подписаться можно сразу на * :( + # Но конкретно в моём случае этот момент неважен, т.к. подразумевается, что каналы будут добавляться, но не + # будут убавляться. + 'webapp:*' => sub { + my ($ps, $channel) = @_ ; + + unless (defined $sub->{$channel}) { + $log->info ("Subscribing to $channel"); + + $sub->{$channel} = $ps->json ($channel)->listen ( + $channel => sub { return $parse_redis_message->(@_); } + ); + } + } + ); + + my $irc = Mojo::IRC->new ( + nick => $c->{nick}, + user => $c->{nick}, + server => sprintf '%s:%s', $c->{server}, $c->{port}, + ); + + # Нахуй эту вашу проверку подлинности, нам работать надо, а не шашечки. + if ($c->{ssl}) { + $irc->tls({ 'insecure' => 1 }); + } + + # включим обработку ctcp-событий + $irc->parser(Parse::IRC->new(ctcp => 1)); + + # добавим коллбэк на момент "после подключения" + $irc->connect(\&_on_connect); + + # когда приходит это событие, сервер нас поприветствовал и мы можем уже пробовать идентифицироваться у nickserv + # (если надо) и джойниться к каналам. + $irc->on ( irc_rpl_endofmotd => sub { return $on_irc_end_of_motd->(@_); } ); + + # этот коллбэк будет зваться по мере бесед, сюда попадают все фразы и ctcp + $irc->on ( irc_privmsg => sub { return $on_irc_privmsg->(@_); } ); + + # эти коллбэки будет зваться когда приходят ctcp запросы. + $irc->on ( ctcp_source => sub { return $on_ctcp_source->(@_); } ); + $irc->on ( ctcp_finger => sub { return $on_ctcp_finger->(@_); } ); + $irc->on ( ctcp_userinfo => sub { return $on_ctcp_finger->(@_); } ); + $irc->on ( ctcp_ping => sub { return $on_ctcp_ping->(@_); } ); + $irc->on ( ctcp_time => sub { return $on_ctcp_time->(@_); } ); + $irc->on ( ctcp_clientinfo => sub { return $on_ctcp_clientinfo->(@_); } ); + + # зарегистрируем дефолтные коллбэки на всё, что не закастомизировано + $irc->register_default_event_handlers; + + do { Mojo::IOLoop->start } until Mojo::IOLoop->is_running; + return; +} + +1; + +# vim: set ft=perl noet ai ts=4 sw=4 sts=4: