1
Files
aleesa-telegram-bot/lib/telegrambot.pm
2020-11-12 00:45:16 +03:00

466 lines
16 KiB
Perl

package telegrambot;
# main bot gears are here
use 5.018;
use strict;
use warnings;
use vars qw/$VERSION/;
use utf8;
use Data::Dumper;
use open qw(:std :utf8);
use File::Path qw( mkpath );
use Hailo;
use Mojo::Base 'Teapot::Bot::Brain';
use conf qw(loadConf);
use botlib qw(weather logger trim randomCommonPhrase);
use telegramlib qw(visavi);
use lat qw(latAnswer);
use Exporter qw(import);
our @EXPORT_OK = qw(run_telegrambot);
$VERSION = '1.0';
my $c = loadConf();
my $hailo;
my $myid;
my $myusername;
my $myfirst_name;
my $mylast_name;
my $myfullname;
# guess it later
my $can_talk = 0;
has token => $c->{telegrambot}->{token};
# i promise, i'll make use of this sub
sub __cron { ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
my $self = shift;
# noop
return;
}
sub __on_msg {
my ($self, $msg) = @_;
# chat info
my $chatid;
my $chatname = 'Noname chat';
# user sending message info
my $userid;
my $username;
my $fullname;
my $vis_a_vi = 'unknown';
$can_talk = 0;
unless ($myid) {
my $myObj = Teapot::Bot::Brain::getMe ($self);
unless ($myObj) {
sleep 3;
$myObj = Teapot::Bot::Brain::getMe ($self);
}
$myid = $myObj->id;
# TODO: use these values instead of pre-defined in config!
$myusername = $myObj->username;
$myfirst_name = $myObj->first_name;
$mylast_name = $myObj->last_name;
if (defined ($myfirst_name) && ($myfirst_name ne '') && defined ($mylast_name) && ($mylast_name ne '')) {
$myfullname = $myfirst_name . ' ' . $mylast_name;
} elsif (defined ($myfirst_name) && ($myfirst_name ne '')) {
$myfullname = $myfirst_name;
} elsif (defined ($mylast_name) && ($mylast_name ne '')) {
$myfullname = $mylast_name;
} else {
$myfullname = $myusername;
}
}
if ($msg->chat->can ('id') && defined ($msg->chat->id)) {
$chatid = $msg->chat->id;
my $group_talk = 0;
if ($msg->chat->can ('username') && defined ($msg->chat->username)) {
$chatname = $msg->chat->username ;
} else {
if ($msg->chat->can ('title') && defined ($msg->chat->title)) {
$chatname = $msg->chat->title;
} else {
$chatname = 'Noname chat';
}
}
# because of bot api restriction there is no events about changing permissions
# in chat sent to bot so before sending any answer, we should check if we can do so
# N.B. there is situation when we do not recieve any messages from chat: if we
# are in exception list, even if it allows any kind of events for bot
# If we are just member of chat, it looks like we obey general chat restrictions,
# so we need only to query getChat in this case
# If we admin, we can get|send messages even if group by itself disallows it.
# works only for public chats, so id shoud be < 0.
if ($chatid < 0) {
my $chatobj = Teapot::Bot::Brain::getChat ($self, { 'chat_id' => $chatid });
unless ($chatobj) {
sleep 3;
$chatobj = Teapot::Bot::Brain::getChat ($self, { 'chat_id' => $chatid });
}
# actually evaluates to 1 for true and to 0 for false
$group_talk = int ($chatobj->{permissions}->{can_send_messages});
my $me = Teapot::Bot::Brain::getChatMember ($self, { 'chat_id' => $chatid, 'user_id' => $myid });
unless ($me) {
sleep 3;
$me = Teapot::Bot::Brain::getChatMember ($self, { 'chat_id' => $chatid, 'user_id' => $myid });
}
if ($me->{'status'} eq 'administrator') {
logger "I can talk in group chat $chatname ($chatid)";
$can_talk = 1;
} else {
logger "I can talk in group chat $chatname ($chatid)" if ($group_talk);
logger "I can not talk in group chat $chatname ($chatid)" unless ($group_talk);
$can_talk = $group_talk;
}
} else {
# private chat, so definely bot can talk
logger "I can talk in private chat $chatname ($chatid)";
$can_talk = 1;
}
} else {
logger 'Unable to get chatid';
}
# according api user must have username and/or first_name and/or last_name at least one of these fields
if ($msg->can ('from') && defined ($msg->from)) {
$userid = $msg->from->id; # it must be filled, so do not check it
$username = $msg->from->username if ($msg->from->can ('username') && defined($msg->from->username));
if ($msg->from->can ('first_name') && defined ($msg->from->first_name)) {
$fullname = $msg->from->first_name;
if ($msg->from->can('last_name') && defined ($msg->from->last_name)) {
$fullname .= ' ' . $msg->from->last_name;
}
} elsif ($msg->from->can ('last_name') && defined ($msg->from->last_name)) {
$fullname .= $msg->from->last_name;
}
$vis_a_vi = visavi ($userid, $username, $fullname);
}
my $phrase = '';
# Newcommer event, greet our new member and suggest to introduce themself.
if ($msg->can ('new_chat_members') && defined ($msg->new_chat_members)) {
my $send_args;
$send_args->{parse_mode} = 'Markdown';
$send_args->{chat_id} = $chatid;
if (defined ($username)) {
if (defined ($fullname)) {
logger "Newcommer in $chatname ($chatid): \@$username, $fullname ($userid)";
$send_args->{text} = "Дратути, [$fullname](tg://user?id=$userid). Представьтес, пожалуйста, и расскажите, что вас сюда привело.";
} else {
logger "Newcommer in $chatname ($chatid): \@$username ($userid)";
$send_args->{text} = "Дратути, [$username](tg://user?id=$userid). Представьтес, пожалуйста, и расскажите, что вас сюда привело.";
}
} else {
logger "Newcommer in $chatname ($chatid): $fullname ($userid)";
$send_args->{text} = "Дратути, [$fullname](tg://user?id=$userid). Представьтес, пожалуйста, и расскажите, что вас сюда привело.";
}
# let's emulate real human and delay answer
sleep (int ( rand (2) + 1));
for (0..(4 + int (rand (3)))) {
$msg->typing();
sleep(3);
sleep 3 unless ($_);
}
sleep ( 3 + int ( rand (2)));
Teapot::Bot::Brain::sendMessage ($self, $send_args);
return;
}
# lazy init chat-bot brains
unless (defined ($hailo->{$chatid})) {
my $brainname = sprintf ('%s/%s.brain.sqlite', $c->{telegrambot}->{braindir}, $chatid);
$hailo->{$chatid} = Hailo->new (
# we'll got file like this: data/telegrambot-brains/-1001332512695.brain.sqlite
brain => $brainname,
order => 3
);
if ($chatid < 0) {
logger "Initialized brain for public chat $chatname ($chatid): $brainname";
} else {
logger "Initialized for private chat $chatname ($chatid): $brainname";
}
}
# is this a 1-on-1 ?
if ($msg->chat->type eq 'private') {
# TODO: what about stickers, photos, documents, audio, video, etc... We should log em at least.
return unless ($msg->can ('text') && defined ($msg->text));
my $text = $msg->text;
logger sprintf ('Private chat %s say to bot: %s', $vis_a_vi, $text);
my $csign = quotemeta ($c->{telegrambot}->{csign});
my $reply = 'Давайте ещё пообщаемся, а то я ещё не научилась от вас плохому.';
# TODO: Process commands in separate sub, they are same for public and private chats.
if (substr ($text, 0, 1) eq $c->{telegrambot}->{csign}) {
if (substr ($text, 1) eq 'ping') {
$reply = 'Pong.';
} elsif (substr ($text, 1) eq 'пинг') {
$reply = 'Понг.';
} elsif (substr ($text, 1, 2) eq 'w ' || substr ($text, 1, 2) eq 'п ') {
my $city = substr ($text, 2);
$reply = weather ($city);
} elsif ((length ($text) == 4) && (substr ($text, 1, 3) eq 'lat' || substr ($text, 1, 2) eq 'лат')) {
$reply = latAnswer();
} else {
$reply = 'Чего?';
}
} else {
my $str = $hailo->{$msg->chat->id}->learn_reply ($text);
if (defined ($str) && $str ne '') {
$reply = $str;
# TODO: move it to telegramlib.pm/botlib.pm!
$phrase = trim ($text);
while ($phrase =~ /[\.|\,|\?|\!]$/) {
chop $phrase;
}
$phrase = lc ($phrase);
if (lc ($reply) eq $phrase) {
$reply = randomCommonPhrase();
} elsif (lc ($reply) eq substr ($phrase, 0, -1)) {
# in case of trailing dot
$reply = randomCommonPhrase();
} elsif (substr (lc ($reply), 0, -1) eq $phrase) {
$reply = randomCommonPhrase();
}
}
}
$msg->typing();
sleep 1;
logger sprintf ("Private chat bot reply to $vis_a_vi: %s", $reply);
$msg->reply ($reply);
# group chat
} elsif (($msg->chat->type eq 'supergroup') or ($msg->chat->type eq 'group')) {
my $reply;
# detect and log messages without text, noop here
unless (defined ($msg->text)) {
logger sprintf('No text in message from %s', $vis_a_vi);
if ($msg->can ('document') && defined ($msg->document)) {
if (defined ($msg->document->{'file_name'})) {
my $docsize = 'unknown';
$docsize = $msg->document->{'file_size'} if (defined ($msg->document->{'file_size'}));
my $type = 'unknown';
$type = $msg->document->{'mime_type'} if (defined ($msg->document->{'mime_type'}));
logger sprintf ('In public chat %s (%s) %s send document type %s named %s, size %s bytes', $chatname, $chatid, $vis_a_vi, $type, $msg->document->{'file_name'}, $docsize);
} else {
logger sprintf ('In public chat %s (%s) %s send unknown document', $chatname, $chatid, $vis_a_vi);
}
} elsif ($msg->can ('sticker') && defined ($msg->sticker)) {
my $set_name = 'unknown';
$set_name = $msg->sticker->set_name if ($msg->sticker->can ('set_name') && defined ($msg->sticker->set_name));
my $emoji = 'unknown';
$emoji = $msg->sticker->emoji if ($msg->sticker->can ('emoji') && defined ($msg->sticker->emoji));
logger sprintf ('In public chat %s (%s) %s reacted with sticker %s from pack %s', $chatname, $chatid, $vis_a_vi, $emoji, $set_name);
} elsif ($msg->can ('photo') && defined ($msg->photo)) {
# actually it is an array! duh, hate arrays!
logger sprintf ('In public chat %s (%s) %s send photo', $chatname, $chatid, $vis_a_vi);
} else {
logger Dumper ($msg);
}
return;
}
# we have text here! so potentially we can chit-chat
my $text = $msg->text;
logger sprintf ('In public chat %s (%s) %s say: %s', $chatname, $chatid, $vis_a_vi, $text);
my $qname = quotemeta ($c->{telegrambot}->{name});
my $qtname = quotemeta ($c->{telegrambot}->{tname});
my $csign = quotemeta ($c->{telegrambot}->{csign});
# are they quote something, maybe, us?
if (defined ($msg->reply_to_message) &&
defined ($msg->reply_to_message->from) &&
defined ($msg->reply_to_message->from->username) &&
($msg->reply_to_message->from->username eq $myusername)) {
logger sprintf ('In public chat %s (%s) %s quote us!', $chatname, $chatid, $vis_a_vi);
# remove our name from users reply, just in case
my $pat1 = quotemeta ('@' . $myusername);
my $pat2 = quotemeta ($myfullname);
$phrase = $text;
$phrase =~ s/$pat1//g;
$phrase =~ s/$pat2//g;
# figure out reply :)
$reply = $hailo->{$msg->chat->id}->learn_reply ($phrase);
# simple commands
# TODO: Process commands in separate sub, they are same for public and private chats.
# TODO: Log commands and answers
} elsif (substr ($text, 0, 1) eq $c->{telegrambot}->{csign}) {
if (substr ($text, 1) eq 'help') {
unless ($can_talk) {
return ;
}
my $send_args;
$send_args->{text} = << 'MYHELP';
```
!help | !помощь - список команд
!w город | !п город - погода в указанном городе
!ping | !пинг - попинговать бота
!lat | !лат - сгенерировать фразу из крылатых латинских выражений
!karma | !карма фраза - посмотреть карму фразы
фраза-- | фраза++ - убавить или добавить карму фразе
```
Но на самом деле я бот больше для общения, чем для исполнения команд.
Поговоришь со мной?
MYHELP
$send_args->{parse_mode} = 'Markdown';
$send_args->{chat_id} = $chatid;
Teapot::Bot::Brain::sendMessage ($self, $send_args);
return;
} elsif (substr ($text, 1) eq 'ping') {
$reply = 'Pong.';
} elsif (substr ($text, 1) eq 'пинг') {
$reply = 'Понг.';
} elsif (substr ($text, 1) eq 'pong') {
$reply = 'Wat?';
} elsif (substr ($text, 1) eq 'понг') {
$reply = 'Шта?';
} elsif (substr ($text, 1, 2) eq 'w ' || substr ($text, 1, 2) eq 'п ') {
my $city = substr ($text, 3);
$reply = weather ($city);
} elsif (length ($text) == 4 && (substr ($text, 1, 3) eq 'lat' || substr ($text, 1, 3) eq 'лат')) {
$reply = latAnswer();
} elsif ((length($text) > 8) || (substr ($text, 1, 6) eq 'karma ') || (substr ($text, 1, 6) eq 'карма ')) {
my $mytext = substr ($text, 7);
$mytext = trim ($mytext);
$reply = karmaGet ($chatid, $mytext);
} elsif ((length($text) > 8) || (substr ($text, 1, 6) eq 'karma ') || (substr ($text, 1, 6) eq 'карма ')) {
$reply = 'Простите, что? Карма пустоты?';
}
} elsif (
($text eq $qname) or
($text eq sprintf ('%s', $qtname)) or
($text eq sprintf ("@%s_bot", $qname)) or # :(
($text eq sprintf ('%s ', $qtname))
) {
$reply = 'Чего?';
} else {
# phrase directed to bot
if ((lc ($text) =~ /^${qname}[\,|\:]? (.+)/) or (lc ($text) =~ /^${qtname}[\,|\:]? (.+)/)){
$phrase = $1;
$reply = $hailo->{$msg->chat->id}->learn_reply ($phrase);
# bot mention by name
} elsif ((lc ($text) =~ /.+ ${qname}[\,|\!|\?|\.| ]/) or (lc ($text) =~ / $qname$/)) {
$phrase = $text;
$reply = $hailo->{$msg->chat->id}->reply($phrase);
# bot mention by telegram name
} elsif ((lc ($text) =~ /.+ ${qtname}[\,|\!|\?|\.| ]/) or (lc ($text) =~ / $qtname$/)) {
$phrase = $text;
$reply = $hailo->{$msg->chat->id}->reply ($phrase);
} elsif ((substr ($text, -2) eq '++') && (substr ($text, -2) eq '--')) {
my @arr = split(/\n/, $text);
if ($#arr < 1) {
$reply = kermaSet ($chatid, $phrase, substr ($text, -2));
} else {
# just message in chat
$hailo->{$msg->chat->id}->learn ($text);
}
# just message in chat
} else {
$hailo->{$msg->chat->id}->learn ($text);
}
}
if (defined ($reply) && $reply ne '' && $can_talk) {
# work a bit more on input phrase
$phrase = trim ($phrase);
while ($phrase =~ /[\.|\,|\?|\!]$/) {
chop $phrase;
}
$phrase = lc ($phrase);
if (lc ($reply) eq $phrase) {
$reply = randomCommonPhrase();
} elsif (lc ($reply) eq substr ($phrase, 0, -1)) {
# in case of trailing dot
$reply = randomCommonPhrase();
} elsif (substr(lc ($reply), 0, -1) eq $phrase) {
$reply = randomCommonPhrase();
}
$msg->typing();
sleep (int (rand (2)));
logger sprintf ('In public chat %s (%s) bot reply to %s: %s', $chatname, $chatid, $vis_a_vi, $reply);
$msg->reply ($reply);
} else {
if ($can_talk) {
logger sprintf ('In public chat %s (%s) bot is not required to reply to %s', $chatname, $chatid, $vis_a_vi);
} else {
logger sprintf ('In public chat %s (%s) bot can\'t talk, but reply to %s is: %s', $chatname, $chatid, $vis_a_vi, $reply);
}
}
# should be channel, so we can't talk
} else {
return;
}
return;
}
# setup our bot
sub init {
unless (-d $c->{telegrambot}->{braindir}) {
mkpath ($c->{telegrambot}->{braindir}, 0, 0755); ## no critic (ValuesAndExpressions::ProhibitLeadingZeros)
}
my $self = shift;
$self->add_listener (\&__on_msg);
# $self->add_repeating_task(900, \&__cron);
return;
}
sub run_telegrambot {
while (sleep 3) {
eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
telegrambot->new->think;
}
}
return;
}
1;
# vim: ft=perl noet ai ts=4 sw=4 sts=4: