Files
aleesa-telegram/lib/Teapot/Bot/Brain.pm
2025-05-08 14:56:25 +03:00

1351 lines
39 KiB
Perl

package Teapot::Bot::Brain;
# ABSTRACT: A base class to make your very own Telegram bot
use Mojo::Base -base;
use strict;
use warnings;
use 5.018; ## no critic (ProhibitImplicitImport)
use utf8;
use English qw ( -no_match_vars );
use Mojo::IOLoop ();
use Mojo::JSON qw (true false encode_json);
use Mojo::UserAgent ();
use Mojo::Parameters ();
use Carp qw (carp croak cluck confess);
# use croak where we return error up to app that supply something wrong
# use cluck where we want to say that something bad but non-critical happen in
# lower layer f.ex. Mojo loop)
# use confess where we want to say that fatal error happen in lower layer
# f.ex. in Mojo loop
use Log::Any ();
use Data::Dumper qw (Dumper);
# TODO: import subs that are "used here directly by peeking into apropriate ns-es"
use Teapot::Bot::Object::Message ();
use Teapot::Bot::Object::Poll ();
use Teapot::Bot::Object::PollAnswer ();
use Teapot::Bot::Object::ChatMemberUpdated ();
use Teapot::Bot::Object::ChatPermissions ();
use Teapot::Bot::Object::User ();
use Teapot::Bot::Object::Chat ();
use Teapot::Bot::Object::ChatMember ();
use Teapot::Bot::Object::ChatInviteLink ();
use Teapot::Bot::Object::MessageReaction ();
$Teapot::Bot::Brain::VERSION = '0.026';
# base class for building telegram robots with Mojolicious
has longpoll_time => 60;
has ua => sub {
my $self = shift;
my $ua = Mojo::UserAgent->new;
$ua = $ua->connect_timeout(5); # defaults to 10
$ua = $ua->inactivity_timeout($self->longpoll_time + 15); # defaults to 40
$ua = $ua->max_connections(0); # no keepalive connections (defaults to 5)
$ua = $ua->request_timeout($self->longpoll_time + 15); # whole request should take no more than this time
# including connection establishing time, request time and
# response time
return $ua;
};
has token => sub { croak 'You need to supply your own token'; };
has tasks => sub { [] };
has listeners => sub { [] };
has log => sub { Log::Any->get_logger };
sub add_repeating_task {
my $self = shift;
my $seconds = shift;
my $task = shift;
my $repeater = sub {
# Perform operation every $seconds seconds
my $last_check = time();
Mojo::IOLoop->recurring(
0.1 => sub {
my $loop = shift;
my $now = time();
return if ($now - $last_check) < $seconds;
$last_check = $now;
$task->($self);
}
);
};
# keep a copy
push @{ $self->tasks }, $repeater;
# kick it off
$repeater->();
return;
}
sub add_listener {
my $self = shift;
my $coderef = shift;
push @{ $self->listeners }, $coderef;
return;
}
sub init {
croak 'init() was not overridden!';
}
sub think {
my $self = shift;
$self->init();
$self->_add_getUpdates_handler;
do {
Mojo::IOLoop->start
} until Mojo::IOLoop->is_running;
return;
}
sub getMe {
my $self;
# There is some magic happen if we call function as object, as we usually do here
# But if we access function from usual-style wrapper that calls it directly from namespace it supply noy $self object
# but string with its name, which is a bummer.
while (my $o = shift @_) {
if (ref ($o) ne '') {
$self = $o;
last;
}
}
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to getChatMember()';
return $ret;
}
my $url = sprintf 'https://api.telegram.org/bot%s/getMe', $self->token;
my $api_response = $self->_post_request($url);
unless ($api_response->{error}) {
return Teapot::Bot::Object::User->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
sub getChatMember {
my $self;
# There is some magic happen if we call function as object, as we usually do here
# But if we access function from usual-style wrapper that calls it directly from namespace it supply noy $self object
# but string with its name, which is a bummer.
while (my $o = shift @_) {
if (ref ($o) ne '') {
$self = $o;
last;
}
}
my $args = shift || {};
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to getChatMember()';
return $ret;
}
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied to getChatMember()';
return $ret;
}
unless ($args->{user_id}) {
$ret->{error} = 1;
$ret->{message} = 'No user_id supplied to getChatMember()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{user_id} = $args->{user_id};
my $url = sprintf 'https://api.telegram.org/bot%s/getChatMember', $self->token;
my $api_response = $self->_post_request($url, $send_args);
unless ($api_response->{error}) {
return Teapot::Bot::Object::ChatMember->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
sub banChatMember {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to banChatMember()';
return $ret;
}
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied to banChatMember()';
return $ret;
}
unless ($args->{user_id}) {
$ret->{error} = 1;
$ret->{message} = 'No user_id supplied to banChatMember()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{user_id} = $args->{user_id};
# Acording https://core.telegram.org/bots/api#banchatmember Applied for supergroups and channels only.
$send_args->{until_date} = $args->{until_date} if exists $args->{until_date};
# Acording https://core.telegram.org/bots/api#banchatmember Always True for supergroups and channels.
$send_args->{revoke_messages} = $args->{revoke_messages} if exists $args->{revoke_messages};
my $url = sprintf 'https://api.telegram.org/bot%s/banChatMember', $self->token;
return $self->_post_request($url, $send_args);
}
sub unbanChatMember {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to unbanChatMember()';
return $ret;
}
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied to unbanChatMember()';
return $ret;
}
unless ($args->{user_id}) {
$ret->{error} = 1;
$ret->{message} = 'No user_id supplied to unbanChatMember()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{user_id} = $args->{user_id};
$send_args->{only_if_banned} = $args->{only_if_banned} if exists $args->{only_if_banned};
my $url = sprintf 'https://api.telegram.org/bot%s/unbanChatMember', $self->token;
return $self->_post_request($url, $send_args);
}
sub muteChatMember {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to muteChatMember()';
return $ret;
}
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied to muteChatMember()';
return $ret;
}
unless ($args->{user_id}) {
$ret->{error} = 1;
$ret->{message} = 'No user_id supplied to muteChatMember()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{user_id} = $args->{user_id};
$send_args->{until_date} = $args->{until_date} if exists $args->{until_date};
# Revoke all known for API v6.4 permissions
$send_args->{permissions}->{can_send_messages} = false;
$send_args->{permissions}->{use_independent_chat_permissions} = false; # https://core.telegram.org/bots/api#restrictchatmember
$send_args->{permissions}->{can_send_polls} = false;
$send_args->{permissions}->{can_send_other_messages} = false;
$send_args->{permissions}->{can_add_web_page_previews} = false;
$send_args->{permissions}->{can_change_info} = false;
$send_args->{permissions}->{can_invite_users} = false;
$send_args->{permissions}->{can_pin_messages} = false;
my $url = sprintf 'https://api.telegram.org/bot%s/restrictChatMember', $self->token;
return $self->_post_request($url, $send_args);
}
sub getChat {
my $self;
# There is some magic happen if we call function as object, as we usually do here
# But if we access function from usual-style wrapper that calls it directly from namespace it supply noy $self object
# but string with its name, which is a bummer.
while (my $o = shift @_) {
if (ref ($o) ne '') {
$self = $o;
last;
}
}
my $args = shift || {};
my $send_args = {};
my $ret;
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied to getChat()';
return $ret;
}
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to getChat()';
return $ret;
}
my $url = sprintf 'https://api.telegram.org/bot%s/getChat', $self->token;
$send_args->{chat_id} = $args->{chat_id};
my $api_response = $self->_post_request($url, $send_args);
unless ($api_response->{error}) {
return Teapot::Bot::Object::Chat->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
sub leaveChat {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied to leaveChat()';
return $ret;
}
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to leaveChat()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
my $url = sprintf 'https://api.telegram.org/bot%s/leaveChat', $self->token;
return $self->_post_request($url, $send_args);
}
sub can_talk {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in can_talk()';
return $ret;
}
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to can_talk()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
return Teapot::Bot::Object::ChatPermissions->canTalk ($self, $send_args);
}
sub sendMessage {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in sendMessage()';
return $ret;
}
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to sendMessage()';
return $ret;
}
unless ($args->{text}) {
$ret->{error} = 1;
$ret->{message} = 'No text supplied in sendMessage()';
return $ret;
}
$send_args->{text} = $args->{text};
$send_args->{chat_id} = $args->{chat_id};
$send_args->{message_thread_id} = $args->{message_thread_id} if exists $args->{message_thread_id};
$send_args->{parse_mode} = $args->{parse_mode} if exists $args->{parse_mode};
$send_args->{disable_web_page_preview} = $args->{disable_web_page_preview} if exists $args->{disable_web_page_preview};
$send_args->{disable_notification} = $args->{disable_notification} if exists $args->{disable_notification};
$send_args->{reply_to_message_id} = $args->{reply_to_message_id} if exists $args->{reply_to_message_id};
# check reply_markup is the right kind
if (exists $args->{reply_markup}) {
my $reply_markup = $args->{reply_markup};
if ( ref($reply_markup) ne 'Teapot::Bot::Object::InlineKeyboardMarkup' ||
ref($reply_markup) ne 'Teapot::Bot::Object::ReplyKeyboardMarkup' ||
ref($reply_markup) ne 'Teapot::Bot::Object::ReplyKeyboardRemove' ||
ref($reply_markup) ne 'Teapot::Bot::Object::ForceReply' ) {
$ret->{error} = 1;
$ret->{message} = 'Incorrect reply_markup in sendMessage()';
return $ret;
}
$send_args->{reply_markup} = $reply_markup;
}
my $url = sprintf 'https://api.telegram.org/bot%s/sendMessage', $self->token;
my $api_response = $self->_post_request($url, $send_args);
unless ($api_response->{error}) {
return Teapot::Bot::Object::Message->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
sub forwardMessage {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in forwardMessage()';
return $ret;
}
unless ($args->{message_id}) {
$ret->{error} = 1;
$ret->{message} = 'No message_id supplied in forwardMessage()';
return $ret;
}
unless ($args->{from_chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No from_chat_id supplied in forwardMessage()';
return $ret;
}
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to forwardMessage()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{from_chat_id} = $args->{from_chat_id};
$send_args->{message_id} = $args->{message_id};
$send_args->{message_thread_id} = $args->{message_thread_id} if exists $args->{message_thread_id};
$send_args->{disable_notification} = $args->{disable_notification} if exists $args->{disable_notification};
my $url = sprintf 'https://api.telegram.org/bot%s/forwardMessage', $self->token;
my $api_response = $self->_post_request($url, $send_args);
unless ($api_response->{error}) {
return Teapot::Bot::Object::Message->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
# TODO: Check that message is no older than 48 hrs
sub deleteMessage {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in deleteMessage()';
return $ret;
}
unless ($args->{message_id}) {
$ret->{error} = 1;
$ret->{message} = 'No message_id supplied in deleteMessage()';
return $ret;
}
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to deleteMessage()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{message_id} = $args->{message_id};
$send_args->{message_thread_id} = $args->{message_thread_id} if exists $args->{message_thread_id};
# TODO: fix canDeleteMessage() && export it in this fle
if (Teapot::Bot::Object::ChatMember->canDeleteMessage($self, $args->{chat_id})) {
my $url = sprintf 'https://api.telegram.org/bot%s/deleteMessage', $self->token;
return $self->_post_request($url, $send_args);
}
else {
$ret->{message} = 'Unable to delete message: Not allowed';
$ret->{error} = 1;
return $ret;
}
}
sub sendPhoto {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in sendPhoto()';
return $ret;
}
unless ($args->{message_id}) {
$ret->{error} = 1;
$ret->{message} = 'No message_id supplied in sendPhoto()';
return $ret;
}
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to sendPhoto()';
return $ret;
}
# photo can be a string (which might be either a URL for telegram servers
# to fetch, or a file_id string) or a file on disk to upload - we need
# to handle that last case here as it changes the way we create the HTTP
# request
unless ($args->{photo}) {
$ret->{error} = 1;
$ret->{message} = 'No photo supplied in sendPhoto()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{message_thread_id} = $args->{message_thread_id} if exists $args->{message_thread_id};
if (-e $args->{photo}) {
$send_args->{photo} = { file => $args->{photo} };
}
else {
$send_args->{photo} = $args->{photo};
}
my $url = sprintf 'https://api.telegram.org/bot%s/sendPhoto', $self->token;
my $api_response = $self->_post_request($url, $send_args);
unless ($api_response->{error}) {
return Teapot::Bot::Object::Message->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
sub sendChatAction {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in sendChatAction()';
return $ret;
}
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to sendChatAction()';
return $ret;
}
$args->{action} = 'typing' unless (defined $args->{action});
$send_args->{message_thread_id} = $args->{message_thread_id} if exists $args->{message_thread_id};
$send_args->{chat_id} = $args->{chat_id};
$send_args->{action} = 'typing';
$send_args->{action} = 'upload_photo' if ($args->{action} eq 'upload_photo');
$send_args->{action} = 'record_video' if ($args->{action} eq 'record_video');
$send_args->{action} = 'upload_video' if ($args->{action} eq 'upload_video');
$send_args->{action} = 'record_voice' if ($args->{action} eq 'record_voice');
$send_args->{action} = 'upload_voice' if ($args->{action} eq 'upload_voice');
$send_args->{action} = 'upload_document' if ($args->{action} eq 'upload_document');
$send_args->{action} = 'find_location' if ($args->{action} eq 'find_location');
$send_args->{action} = 'record_video_note' if ($args->{action} eq 'record_video_note');
$send_args->{action} = 'upload_video_note' if ($args->{action} eq 'upload_video_note');
$send_args->{action} = 'choose_sticker' if ($args->{action} eq 'choose_sticker');
my $url = sprintf 'https://api.telegram.org/bot%s/sendChatAction', $self->token;
return $self->_post_request($url, $send_args);
}
sub exportChatInviteLink {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to exportChatInviteLink()';
return $ret;
}
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in exportChatInviteLink()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
my $url = sprintf 'https://api.telegram.org/bot%s/exportChatInviteLink', $self->token;
return $self->_post_request($url, $send_args);
}
sub createChatInviteLink {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to createChatInviteLink()';
return $ret;
}
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in createChatInviteLink()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{expire_date} = $args->{expire_date} if exists $args->{expire_date};
$send_args->{member_limit} = $args->{member_limit} if exists $args->{member_limit};
my $url = sprintf 'https://api.telegram.org/bot%s/createChatInviteLink', $self->token;
my $api_response = $self->_post_request($url, $send_args);
unless ($api_response->{error}) {
return Teapot::Bot::Object::ChatInviteLink->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
sub editChatInviteLink {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to editChatInviteLink()';
return $ret;
}
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in editChatInviteLink()';
return $ret;
}
unless ($args->{invite_link}) {
$ret->{error} = 1;
$ret->{message} = 'No invite_link supplied in editChatInviteLink()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{invite_link} = $args->{invite_link};
$send_args->{expire_date} = $args->{expire_date} if exists $args->{expire_date};
$send_args->{member_limit} = $args->{member_limit} if exists $args->{member_limit};
my $url = sprintf 'https://api.telegram.org/bot%s/editChatInviteLink', $self->token;
my $api_response = $self->_post_request($url, $send_args);
unless ($api_response->{error}) {
return Teapot::Bot::Object::ChatInviteLink->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
sub revokeChatInviteLink {
my $self = shift;
my $args = shift || {};
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to revokeChatInviteLink()';
return $ret;
}
unless ($args->{chat_id}) {
$ret->{error} = 1;
$ret->{message} = 'No chat_id supplied in revokeChatInviteLink()';
return $ret;
}
unless ($args->{invite_link}) {
$ret->{error} = 1;
$ret->{message} = 'No invite_link supplied in revokeChatInviteLink()';
return $ret;
}
$send_args->{chat_id} = $args->{chat_id};
$send_args->{invite_link} = $args->{invite_link};
my $url = sprintf 'https://api.telegram.org/bot%s/revokeChatInviteLink', $self->token;
my $api_response = $self->_post_request($url, $send_args);
unless ($api_response->{error}) {
return Teapot::Bot::Object::ChatInviteLink->create_from_hash($api_response, $self);
}
else {
return $api_response;
}
}
sub _add_getUpdates_handler {
my $self = shift;
my $http_active = 0;
my $last_update_id = -1;
my $token = $self->token;
my $timeout = 60;
my $limit = 10;
my $au->[0] = 'message';
push @{$au}, 'edited_message';
push @{$au}, 'channel_post';
push @{$au}, 'edited_channel_post';
push @{$au}, 'message_reaction';
push @{$au}, 'message_reaction_count';
push @{$au}, 'inline_query';
push @{$au}, 'chosen_inline_result';
push @{$au}, 'callback_query';
push @{$au}, 'poll';
push @{$au}, 'poll_answer';
push @{$au}, 'my_chat_member';
push @{$au}, 'chat_member';
push @{$au}, 'chat_join_request';
push @{$au}, 'chat_boost';
push @{$au}, 'removed_chat_boost';
my $allowed_updates = encode_json ($au);
my $params = Mojo::Parameters->new();
$params = $params->append(limit => $limit);
$params = $params->append(timeout => $timeout);
$params = $params->append(allowed_updates => $allowed_updates);
Mojo::IOLoop->recurring(
0.1 => sub {
# do nothing if our previous longpoll is still going
return if $http_active;
my $offset = $last_update_id + 1;
my $updateURL = "https://api.telegram.org/bot${token}/getUpdates?offset=$offset&$params";
$http_active = 1;
$self->ua->get(
$updateURL => sub {
my ($ua, $tx) = @_;
# it looks like we can catch an error here if timeout or something bad occurs,
# try not to die in this case or we risk to stuck with $http_active=1 forever
my $res = eval { $tx->res->json; };
if ((defined $res) && $res ne '') {
my $items = $res->{result};
foreach my $item (@{$items}) {
$last_update_id = $item->{update_id};
$self->_process_message($item);
}
} else {
cluck "Unable to get update from API: $EVAL_ERROR";
}
$http_active = 0;
}
);
}
);
return;
}
# process a message which arrived via getUpdates
sub _process_message {
my $self = shift;
my $item = shift;
my $update_id = $item->{update_id};
# There can be several types of responses. But only one response.
# https://core.telegram.org/bots/api#update
my $update;
$update = Teapot::Bot::Object::Message->create_from_hash($item->{message}, $self) if $item->{message};
$update = Teapot::Bot::Object::Message->create_from_hash($item->{edited_message}, $self) if $item->{edited_message};
$update = Teapot::Bot::Object::Message->create_from_hash($item->{channel_post}, $self) if $item->{channel_post};
$update = Teapot::Bot::Object::Message->create_from_hash($item->{edited_channel_post}, $self) if $item->{edited_channel_post};
# Not yet implemented, we have no InlineQuery object yet
#$update = Teapot::Bot::Object::InlineQuery->create_from_hash($tem->{inline_query}, $self) if $item->{inline_query};
# Not yet implemented, we have no ChosenInlineResult object yet
#$update = Teapot::Bot::Object::ChosenInlineResult->create_from_hash($tem->{chosen_inline_result}, $self) if $item->{chosen_inline_result};
# Not yet implemented, we have no callback_query object yet, because we do not use this mechanism, s API should not gi us that shit
#$update = Teapot::Bot::Object::CallbackQuery->create_from_hash($tem->{callback_query}, $self) if $item->{callback_query};
# Not yet implemented, we have no ShippingQuery object yet
#$update = Teapot::Bot::Object::ShippingQuery->create_from_hash($tem->{shipping_query}, $self) if $item->{shipping_query};
# Not yet implemented, we have no PreCheckoutQuery object yet
#$update = Teapot::Bot::Object::PreCheckoutQuery->create_from_hash($tem->{pre_checkout_query}, $self) if $item->{pre_checkout_query};
$update = Teapot::Bot::Object::Poll->create_from_hash($item->{poll}, $self) if $item->{poll};
$update = Teapot::Bot::Object::PollAnswer->create_from_hash($item->{poll_answer}, $self) if $item->{poll_answer};
$update = Teapot::Bot::Object::ChatMemberUpdated->create_from_hash($item->{my_chat_member}, $self) if $item->{my_chat_member};
$update = Teapot::Bot::Object::ChatMemberUpdated->create_from_hash($item->{chat_member}, $self) if $item->{chat_member};
$update = Teapot::Bot::Object::MessageReaction->create_from_hash($item->{message_reaction}, self) if $item->{message_reaction};
# if we got to this point without creating a response, it must be a type we
# don't handle yet
if (! $update) {
cluck 'Do not know how to handle this update: ' . Dumper($item);
return;
}
foreach my $listener (@{ $self->listeners }) {
# call the listener code, supplying ourself and the update
my $evalRet = eval {
$listener->($self, $update);
return 1;
};
unless ($evalRet) {
carp "An error occured during update processing: $EVAL_ERROR";
return;
}
}
return;
}
sub _post_request {
my $self = shift;
my $url = shift;
my $form_args = shift || {};
my $result;
my $res = $self->ua->post($url, form => $form_args)->result;
if ($res->is_success) {
$result = $res->json->{result};
}
elsif ($res->is_error) {
$result->{error} = 1;
$result->{debug} = $res;
$result->{param} = $form_args;
$result->{url} = $url;
}
else {
# This must be something fatal for sure, because either is_success or is_error must be set by Mojo
confess 'Not sure what went wrong';
}
return $result;
}
sub logOut {
my $self = shift;
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to logOut()';
return $ret;
}
my $url = sprintf 'https://api.telegram.org/bot%s/logOut', $self->token;
return $self->_post_request($url, $send_args);
}
sub Close {
my $self = shift;
my $send_args = {};
my $ret;
if (! defined($self->token) || $self->token eq '') {
$ret->{error} = 1;
$ret->{message} = 'No token supplied to Close()';
return $ret;
}
my $url = sprintf 'https://api.telegram.org/bot%s/close', $self->token;
return $self->_post_request($url, $send_args);
}
1;
__END__
=pod
=encoding utf8
=head1 NAME
Teapot::Bot::Brain - A base class to make your very own Telegram bot
=head1 VERSION
version 0.026
=head1 SYNOPSIS
package MyApp::Coolbot;
use Mojo::Base 'Teapot::Bot::Brain';
has token => 'token-you-got-from-@botfather';
sub init {
my $self = shift;
$self->add_repeating_task(600, \&timed_task);
$self->add_listener(\&respond_to_messages);
}
Elsewhere....
my $bot = MyApp::Coolbot->new();
$bot->think; # this will block unless there is already an event
# loop running
=head1 DESCRIPTION
This base class makes it easy to create your own Bot classes that
interface with the Telegram Bot API.
Internally it uses the Mojo::IOLoop event loop to provide non-blocking
access to the Bot API, allowing your bot to listen for events via the
longpoll getUpdates API method and also trigger timed events that can
run without blocking.
As with any bot framework, the principle is that the framework allows you
to interact with other users on Telegram. The Telegram API provides a rich
set of typed objects. This framework will allow you to create those objects
to send into the API (for instance, sending text messages, sending photos,
and more) as well as call your code (via L<add_listener> when your bot
receives messages (which might be text, or images, and so on).
How bots work with Telegram is out of scope for this document, a good
starting place is L<https://core.telegram.org/bots>.
=head1 METHODS
=head2 add_repeating_task
This method will add a sub to run every C<$seconds> seconds. Pass this method
two parameters, the number of seconds between executions, and the coderef to
execute.
Your coderef will be passed the C<Teapot::Bot::Brain> object when it is
executed.
=head2 add_listener
Respond to messages we receive. It takes a single argument, a coderef to execute
for each update that is sent to us. These are *typically* C<Teapot::Bot:Object::Message>
objects, though that is not the only type of update that may be sent (see
L<https://core.telegram.org/bots/api#update>).
Multiple listeners can be added, they will receive the incoming update in the order
that they are registered.
Any or all listeners can choose to ignore or take action on any particular update.
=head2 think
Start this bot thinking.
Calls your init method and then enters a blocking loop (unless a Mojo::IOLoop
is already running).
=head2 getMe
This is the wrapper around the C<getMe> API method. See
L<https://core.telegram.org/bots/api#getme>.
Takes no arguments, and returns the L<Teapot::Bot::Object::User> that
represents this bot.
On error returns hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json)
=head2 getChatMember
This is the wrapper around the C<getChatMember> API method. See
L<https://core.telegram.org/bots/api#getchatmember>.
Takes chat_id, and user_id as arguments.
Returns the L<Teapot::Bot::Object::ChatMember> that represents properties
of Chat User.
On error returns hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json)
=head2 banChatMember
This is the wrapper around the C<banChatMember> API method. See
L<https://core.telegram.org/bots/api#banchatmember>.
Takes chat_id, and user_id as arguments. And optionally until_date and
revoke_messages.
Returns Telegram API answer that is basically returns JSON::PP::true on success.
On error returns hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json)
=head2 unbanChatMember
This is the wrapper around the C<unbanChatMember> API method. See
L<https://core.telegram.org/bots/api#unbanchatmember>.
Takes chat_id, and user_id as arguments. And optionally only_if_banned.
Returns Telegram API answer that is basically returns JSON::PP::true on success.
On error returns hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json)
=head2 muteChatMember
This is the wrapper around the C<restrictChatMember> API method. See
L<https://core.telegram.org/bots/api#restrictchatmember>.
Takes chat_id, and user_id as arguments. And optionally until_date.
Shorthand for revoking all permissions for member being muted.
Returns Telegram API answer that is basically returns JSON::PP::true on success.
On error returns hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json)
=head2 getChat
This is the wrapper around the C<getChat> API method. See
L<https://core.telegram.org/bots/api#getchat>.
Takes chat_id as argument.
Returns the L<Teapot::Bot::Object::Chat> that represents properties of Chat.
On error returns hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json)
=head2 leaveChat
This is the wrapper around the C<leaveChat> API method. See
L<https://core.telegram.org/bots/api#leavechat>.
Takes chat_id as argument.
Returns Telegram API answer that is basically returns true on success.
On error returns hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json)
=head2 can_talk
Returns hash reference with error set to 0 and can_talk set either to 0 if bot
is unable to sendMessage() to chat or 1 if bot can sendMessage() to chat.
On error returns hash reference with error set to 1 and can_talk set to 0
Also can return hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json) if
error caught during calling BotAPI.
=head2 sendMessage
See L<https://core.telegram.org/bots/api#sendmessage>.
Returns a L<Teapot::Bot::Object::Message> object.
On error returns hash reference with error set to 1.
Also can return hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json) if
error caught during calling BotAPI.
=head2 forwardMessage
See L<https://core.telegram.org/bots/api#forwardmessage>.
Returns a L<Teapot::Bot::Object::Message> object.
On error returns hash reference with error set to 1
=head2 deleteMessage
See L<https://core.telegram.org/bots/api#deletemessage>.
Returns a L<Teapot::Bot::Object::Message> object.
On error returns hash reference with error set to 1.
Also can return hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json) if
error caught during calling BotAPI.
=head2 sendPhoto
See L<https://core.telegram.org/bots/api#sendphoto>.
Returns a L<Teapot::Bot::Object::Message> object.
On error returns hash reference with error set to 1
=head2 sendChatAction
See L<https://core.telegram.org/bots/api#sendchataction>.
Returns nothing, it's send-only method (Telegram API returns true on success)
On error returns hash reference with error set to 1.
Also can return hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json) if
error caught during calling BotAPI.
=head2 exportChatInviteLink
See L<https://core.telegram.org/bots/api#exportchatinvitelink>
Takes chat_id as argument.
Returns a raw api answer.
On error returns hash reference with error set to 1.
Also can return hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json) if
error caught during calling BotAPI.
=head2 createChatInviteLink
See L<https://core.telegram.org/bots/api#createchatinvitelink>
Takes chat_id as argument. And optionally expire_date and member_limit.
Returns a L<Teapot::Bot::Object::ChatInviteLink> object.
On error returns hash reference with error set to 1.
Also can return hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json) if
error caught during calling BotAPI.
=head2 editChatInviteLink
See L<https://core.telegram.org/bots/api#editchatinvitelink>
Takes chat_id and invite_link as arguments. And optionally expire_date
and member_limit.
Returns a L<Teapot::Bot::Object::ChatInviteLink> object.
On error returns hash reference with error set to 1.
Also can return hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json) if
error caught during calling BotAPI.
=head2 revokeChatInviteLink
See L<https://core.telegram.org/bots/api#revokechatinvitelink>
Takes chat_id and invite_link as arguments.
Returns a L<Teapot::Bot::Object::ChatInviteLink> object.
On error returns hash reference with error set to 1.
Also can return hash reference with error field set to 1, also set fields
param, url and debug (which is result of Mojo::UserAgent->post->result->json) if
error caught during calling BotAPI.
=head1 SEE ALSO
=over 4
=item *
L<Mojolicious>
=item *
L<https://core.telegram.org/bots>
=back
=head1 Telegram Bot API methods
The following methods are relatively thin wrappers around the various
methods available in the Telgram Bot API to send messages and perform other
updates.
L<https://core.telegram.org/bots/api#available-methods>
They all return immediately with the corresponding Teapot::Bot::Object
subclass - consult the documenation for each below to see what to expect.
Note that not all methods have yet been implemented.
=head1 AUTHOR
Justin Hawkins <justin@eatmorecode.com>
=head1 LICENSE AND COPYRIGHT
This software is copyright (c) 2019 by Justin Hawkins.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut
# vim: set ft=perl noet ai ts=2 sw=2 sts=2: