Now Telegram::Bot is Teapot::Bot
This commit is contained in:
395
lib/Teapot/Bot/Brain.pm
Normal file
395
lib/Teapot/Bot/Brain.pm
Normal file
@@ -0,0 +1,395 @@
|
||||
package Teapot::Bot::Brain;
|
||||
$Teapot::Bot::Brain::VERSION = '0.021';
|
||||
# ABSTRACT: A base class to make your very own Telegram bot
|
||||
|
||||
|
||||
use Mojo::Base -base;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Mojo::IOLoop;
|
||||
use Mojo::UserAgent;
|
||||
use Carp qw/croak/;
|
||||
use Log::Any;
|
||||
use Data::Dumper;
|
||||
|
||||
use Teapot::Bot::Object::Message;
|
||||
|
||||
# base class for building telegram robots with Mojolicious
|
||||
has longpoll_time => 60;
|
||||
has ua => sub { Mojo::UserAgent->new->inactivity_timeout(shift->longpoll_time + 15) };
|
||||
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 unless ($now - $last_check) >= $seconds;
|
||||
$last_check = $now;
|
||||
$task->($self);
|
||||
});
|
||||
};
|
||||
|
||||
# keep a copy
|
||||
push @{ $self->tasks }, $repeater;
|
||||
|
||||
# kick it off
|
||||
$repeater->();
|
||||
}
|
||||
|
||||
|
||||
sub add_listener {
|
||||
my $self = shift;
|
||||
my $coderef = shift;
|
||||
|
||||
push @{ $self->listeners }, $coderef;
|
||||
}
|
||||
|
||||
sub init {
|
||||
die "init was not overridden!";
|
||||
}
|
||||
|
||||
|
||||
sub think {
|
||||
my $self = shift;
|
||||
$self->init();
|
||||
|
||||
$self->_add_getUpdates_handler;
|
||||
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub getMe {
|
||||
my $self = shift;
|
||||
my $token = $self->token || croak "no token?";
|
||||
|
||||
my $url = "https://api.telegram.org/bot${token}/getMe";
|
||||
my $api_response = $self->_post_request($url);
|
||||
|
||||
return Teapot::Bot::Object::User->create_from_hash($api_response, $self);
|
||||
}
|
||||
|
||||
|
||||
sub sendMessage {
|
||||
my $self = shift;
|
||||
my $args = shift || {};
|
||||
|
||||
my $send_args = {};
|
||||
croak "no chat_id supplied" unless $args->{chat_id};
|
||||
$send_args->{chat_id} = $args->{chat_id};
|
||||
|
||||
croak "no text supplied" unless $args->{text};
|
||||
$send_args->{text} = $args->{text};
|
||||
|
||||
# these are optional, send if they are supplied
|
||||
$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};
|
||||
die "bad reply_markup supplied"
|
||||
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' );
|
||||
$send_args->{reply_markup} = $reply_markup;
|
||||
}
|
||||
|
||||
my $token = $self->token || croak "no token?";
|
||||
my $url = "https://api.telegram.org/bot${token}/sendMessage";
|
||||
my $api_response = $self->_post_request($url, $send_args);
|
||||
|
||||
return Teapot::Bot::Object::Message->create_from_hash($api_response, $self);
|
||||
}
|
||||
|
||||
|
||||
sub forwardMessage {
|
||||
my $self = shift;
|
||||
my $args = shift || {};
|
||||
my $send_args = {};
|
||||
croak "no chat_id supplied" unless $args->{chat_id};
|
||||
$send_args->{chat_id} = $args->{chat_id};
|
||||
|
||||
croak "no from_chat_id supplied" unless $args->{from_chat_id};
|
||||
$send_args->{from_chat_id} = $args->{from_chat_id};
|
||||
|
||||
croak "no message_id supplied" unless $args->{message_id};
|
||||
$send_args->{message_id} = $args->{message_id};
|
||||
|
||||
# these are optional, send if they are supplied
|
||||
$send_args->{disable_notification} = $args->{disable_notification} if exists $args->{disable_notification};
|
||||
|
||||
my $token = $self->token || croak "no token?";
|
||||
my $url = "https://api.telegram.org/bot${token}/forwardMessage";
|
||||
my $api_response = $self->_post_request($url, $send_args);
|
||||
|
||||
return Teapot::Bot::Object::Message->create_from_hash($api_response, $self);
|
||||
}
|
||||
|
||||
|
||||
sub sendPhoto {
|
||||
my $self = shift;
|
||||
my $args = shift || {};
|
||||
my $send_args = {};
|
||||
|
||||
croak "no chat_id supplied" unless $args->{chat_id};
|
||||
$send_args->{chat_id} = $args->{chat_id};
|
||||
|
||||
# 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
|
||||
croak "no photo supplied" unless $args->{photo};
|
||||
if (-e $args->{photo}) {
|
||||
$send_args->{photo} = { photo => { file => $args->{photo} } };
|
||||
}
|
||||
else {
|
||||
$send_args->{photo} = $args->{photo};
|
||||
}
|
||||
|
||||
my $token = $self->token || croak "no token?";
|
||||
my $url = "https://api.telegram.org/bot${token}/sendPhoto";
|
||||
my $api_response = $self->_post_request($url, $send_args);
|
||||
|
||||
return Teapot::Bot::Object::Message->create_from_hash($api_response, $self);
|
||||
}
|
||||
|
||||
|
||||
sub _add_getUpdates_handler {
|
||||
my $self = shift;
|
||||
|
||||
my $http_active = 0;
|
||||
my $last_update_id = -1;
|
||||
my $token = $self->token;
|
||||
|
||||
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}&timeout=60";
|
||||
$http_active = 1;
|
||||
|
||||
$self->ua->get($updateURL => sub {
|
||||
my ($ua, $tx) = @_;
|
||||
my $res = $tx->res->json;
|
||||
my $items = $res->{result};
|
||||
foreach my $item (@$items) {
|
||||
$last_update_id = $item->{update_id};
|
||||
$self->_process_message($item);
|
||||
}
|
||||
|
||||
$http_active = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
# 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};
|
||||
|
||||
# if we got to this point without creating a response, it must be a type we
|
||||
# don't handle yet
|
||||
if (! $update) {
|
||||
die "Do not know how to handle this update: " . Dumper($item);
|
||||
}
|
||||
|
||||
foreach my $listener (@{ $self->listeners }) {
|
||||
# call the listener code, supplying ourself and the update
|
||||
$listener->($self, $update);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub _post_request {
|
||||
my $self = shift;
|
||||
my $url = shift;
|
||||
my $form_args = shift || {};
|
||||
|
||||
my $res = $self->ua->post($url, form => $form_args)->result;
|
||||
if ($res->is_success) { return $res->json->{result}; }
|
||||
elsif ($res->is_error) { die "Failed to post: " . $res->message; }
|
||||
else { die "Not sure what went wrong"; }
|
||||
}
|
||||
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=pod
|
||||
|
||||
=encoding UTF-8
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Teapot::Bot::Brain - A base class to make your very own Telegram bot
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
version 0.021
|
||||
|
||||
=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 L<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.
|
||||
|
||||
=head2 sendMessage
|
||||
|
||||
See L<https://core.telegram.org/bots/api#sendmessage>.
|
||||
|
||||
Returns a L<Teapot::Bot::Object::Message> object.
|
||||
|
||||
=head2 forwardMessage
|
||||
|
||||
See L<https://core.telegram.org/bots/api#forwardmessage>.
|
||||
|
||||
Returns a L<Teapot::Bot::Object::Message> object.
|
||||
|
||||
=head2 sendPhoto
|
||||
|
||||
See L<https://core.telegram.org/bots/api#sendphoto>.
|
||||
|
||||
Returns a L<Teapot::Bot::Object::Message> object.
|
||||
|
||||
=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 COPYRIGHT AND LICENSE
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user