Bot-Telegram
view release on metacpan or search on metacpan
lib/Bot/Telegram.pm view on Meta::CPAN
package Bot::Telegram;
# ABSTRACT: a micro^W nano framework for creating Telegram bots based on L<WWW::Telegram::BotAPI>
our $VERSION = '1.10'; # VERSION
use v5.16.3;
use Mojo::Base 'Mojo::EventEmitter';
use WWW::Telegram::BotAPI;
use Mojo::Promise;
use Mojo::Log;
use Mojo::JSON 'encode_json';
use Mojo::Transaction::HTTP;
use Mojo::Message::Response;
use Bot::Telegram::X::InvalidArgumentsError;
use Bot::Telegram::X::InvalidStateError;
use constant ERR_NODETAILS => 'no details available';
use constant DEFAULT_POLLING_TIMEOUT => 20;
use constant DEFAULT_POLLING_INTERVAL => 0.3;
use constant DEFAULT_POLLING_ERROR_CB => sub {
my ($self, $tx, $type) = @_;
my $message = sub {
return $tx -> {error}{msg}
unless $self -> is_async;
for ($type) {
/agent/ and return $tx -> error -> {message};
/api/ and return ($tx -> res -> json // {}) -> {description};
}
} -> () || ERR_NODETAILS;
$self -> log -> warn("Polling failed (error type: $type): $message");
};
use constant DEFAULT_CALLBACK_ERROR_CB => sub {
my ($self, undef, $err) = @_;
$self -> log -> warn("Update processing failed: $err");
};
has [qw/api current_update polling_config/];
has [qw/_polling
_polling_timer
_polling_interval
_polling_request_id/
];
has callbacks => sub { {} };
has ioloop => sub { Mojo::IOLoop -> new };
has log => sub { Mojo::Log -> new -> level('info') };
sub new {
my $self = shift -> SUPER::new(@_);
$self -> on(polling_error => DEFAULT_POLLING_ERROR_CB);
$self -> on(callback_error => DEFAULT_CALLBACK_ERROR_CB);
$self
}
################################################################################
# General
################################################################################
sub init_api {
my ($self, %args) = @_;
Bot::Telegram::X::InvalidArgumentsError
-> throw('No token provided')
unless exists $args{token};
$args{async} //= 1;
$self -> api(WWW::Telegram::BotAPI -> new(%args));
return $self;
}
sub is_async {
my $self = shift;
Bot::Telegram::X::InvalidStateError
-> throw('API is not initialized')
unless $self -> api;
$self -> api -> {async};
}
sub api_request { shift -> api -> api_request(@_) }
sub api_request_p {
my ($self, @args) = @_;
Mojo::Promise -> new(sub {
my ($resolve, $reject) = @_;
$self -> api -> api_request(@args, sub {
my ($ua, $tx) = @_;
my $response = $tx -> res -> json;
((!$tx -> error && ref $response && $$response{ok})
? $resolve
: $reject) -> ($ua, $tx);
})
lib/Bot/Telegram.pm view on Meta::CPAN
sub set_webhook {
my ($self, $config, $cb) = @_;
Bot::Telegram::X::InvalidStateError
-> throw('Disable long polling first')
if $self -> is_polling;
Bot::Telegram::X::InvalidArgumentsError
-> throw('No config provided')
unless ref $config;
$self -> api -> api_request(
setWebhook => $config,
ref $cb eq 'CODE' ? $cb : undef);
return $self;
}
################################################################################
# Long polling
################################################################################
sub start_polling {
my $self = shift;
my $config = $self -> polling_config;
if (ref $_[0] eq 'HASH') {
$config = shift;
$config -> {offset} //= 0; # make sure we won't get any uninitiailzed warnings in shift_offset
}
my (%opts) = @_;
if ($opts{restart}) {
$self -> stop_polling;
} else {
Bot::Telegram::X::InvalidStateError
-> throw('Already running')
if $self -> is_polling;
}
$self -> polling_config($config // { timeout => DEFAULT_POLLING_TIMEOUT, offset => 0 });
$self -> _polling_interval($opts{interval} // DEFAULT_POLLING_INTERVAL);
$self -> _polling(1);
$self -> _poll;
}
sub stop_polling {
my $self = shift;
return $self unless $self -> is_polling;
for (my $agent = $self -> api -> agent) {
$self -> _polling(undef);
# In synchronous mode, it's enough to simply clear state
return $self -> _polling_interval(undef)
unless $agent -> isa('Mojo::UserAgent')
and $self -> is_async;
# In asynchronous mode, we also need to cancel existing timers
for (my $loop = Mojo::IOLoop -> singleton) {
$loop -> remove($self -> _polling_request_id);
$loop -> remove($self -> _polling_timer)
if $self -> _polling_timer; # if another request is scheduled, cancel it
}
# Reset state
$self -> _polling_request_id(undef)
-> _polling_interval(undef)
-> _polling_timer(undef);
}
$self
}
sub is_polling { !! shift -> _polling }
# In asynchronous mode: process getUpdates response or handle errors, if any
# In synchronous mode, WWW::Telegram::BotAPI::parse_error takes care of error handling for us.
sub _process_getUpdates_results {
my $self = shift;
my $async = $self -> is_async;
my ($response, $error);
$self -> log -> trace('processing getUpdates results');
my $retry_after;
if ($async) {
my ($ua, $tx) = @_;
$response = $tx -> res -> json // {};
# Error
if ($error = ($tx -> error or not $$response{ok})) {
my $type = eval {
return 'api' if $$response{error_code};
return 'agent' if $tx -> error;
} // 'unknown';
$self -> emit(polling_error => $tx, $type);
}
} else {
($response, $error) = @_;
# NOTE: $response and $error are mutually exclusive - only one is `defined` at a time
if ($error) {
$error = $self -> api -> parse_error;
# no way to access the original $tx in synchronous mode
# https://metacpan.org/dist/WWW-Telegram-BotAPI/source/lib/WWW/Telegram/BotAPI.pm#L228
$self -> emit(polling_error => { error => $error }, $$error{type});
}
}
# Handle rate limits
if (exists $response -> {parameters}{retry_after}) {
$retry_after = $response -> {parameters}{retry_after};
$self -> log -> info("Rate limit exceeded, waiting ${retry_after}s before polling again");
}
# Process the updates we have retrieved (if any) and poll for more
# (unless someone or something has disabled the polling loop in the meantime)
unless ($error) {
for my $result ($$response{result}) {
# last unless ref $result eq 'ARRAY'; # nothing to process
$self -> process_update($_)
-> shift_offset
for @$result;
}
}
return unless $self -> is_polling;
$self -> log -> trace('still polling, scheduling another iteration...');
if ($async) {
my $tid = Mojo::IOLoop -> timer(
$retry_after // $self -> _polling_interval,
sub { $self -> tap(sub { $self -> log -> trace("it's polling time!") })
-> _poll });
$self -> _polling_timer($tid);
} else {
my $d = $retry_after // $self -> _polling_interval;
# Sleep
$self -> ioloop -> timer($d, sub { $self -> ioloop -> stop });
$self -> ioloop -> start;
$self -> log -> trace("it's polling time!");
$self -> _poll;
}
}
sub _poll {
my $self = shift;
$self -> log -> trace('polling');
if ($self -> is_async) {
my $id = $self -> api -> api_request(
getUpdates => $self -> polling_config,
sub { $self -> _process_getUpdates_results(@_) }
);
# Assuming api_request always returns a valid ioloop connection ID when in asynchronous mode...
$self -> _polling_request_id($id);
} else {
my $response = eval {
$self -> api -> api_request(
getUpdates => $self -> polling_config)
};
$self -> _process_getUpdates_results($response, $@);
}
}
1
__END__
=pod
=encoding UTF-8
=head1 NAME
Bot::Telegram - a micro^W nano framework for creating Telegram bots based on L<WWW::Telegram::BotAPI>
=head1 VERSION
version 1.10
=head1 SYNOPSIS
#!/usr/bin/env perl
use Mojo::Base -strict;
use Bot::Telegram;
my $bot = Bot::Telegram
-> new
-> init_api(token => YOUR_TOKEN_HERE);
$bot -> set_callbacks(
message => sub {
my ($bot, $update) = @_;
lib/Bot/Telegram.pm view on Meta::CPAN
=back
=head2 shift_offset
$bot = $bot -> shift_offset;
Recalculate the current C<offset> for long polling.
Set it to the ID of L</"current_update"> plus one, if current update ID is greater than or equal to the current value.
This is done automatically inside the polling loop (L</"start_polling">),
but the method is made public, if you want to roll your own custom polling loop for some reason.
=head2 start_polling
$bot = $bot -> start_polling;
$bot = $bot -> start_polling($cfg);
$bot = $bot -> start_polling(restart => 1, interval => 1);
$bot = $bot -> start_polling($cfg, restart => 1, interval => 1);
Start long polling.
This method will block in synchronous mode.
Set L</"log"> level to C<trace> to see additional debugging information.
=head3 Arguments
=over 4
=item C<$cfg>
A hash ref containing L<getUpdates|https://core.telegram.org/bots/api#getupdates> options.
Note that the offset parameter is automatically incremented - whenever an update is processed (whether successfully or not),
the internally stored C<offset> value becomes update ID plus one,
IF update ID is greater than or equal to it.
The initial offset is zero by default.
The config is persistent between polling restarts and is available as L</"polling_config">.
$bot -> start_polling($cfg);
# ...
$bot -> stop_polling;
# ...
$bot -> start_polling; # will reuse the previous config, offset preserved
# ...
say $bot -> polling_config eq $cfg; # 1
If none is provided and L</"polling_config"> is empty, a default config will be generated:
{ timeout => 20, offset => 0 }
=item restart
Set to true if the loop is already running, otherwise an exception will be thrown.
=item interval
Interval in seconds between polling requests.
Floating point values are accepted (timers are set using L<Mojo::IOLoop/"timer">).
Default value is 0.3 (300ms).
=back
=head3 Exceptions
=over 4
=item C<Bot::Telegram::X::InvalidStateError>
Already running
=back
=head2 stop_polling
$bot = $bot -> stop_polling;
Stop long polling.
=head1 SEE ALSO
L<Bot::ChatBots::Telegram> - another library built on top of L<WWW::Telegram::BotAPI>
L<Telegram::Bot> - another, apparently incomplete, Telegram Bot API interface
L<Telegram::BotKit> - provides utilities for building reply keyboards and stuff, also uses L<WWW::Telegram::BotAPI>
L<WWW::Telegram::BotAPI> - lower level Telegram Bot API library used here
=head1 AUTHOR
Vasyan <somerandomtext111@gmail.com>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2024 by Vasyan.
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
( run in 1.011 second using v1.01-cache-2.11-cpan-9581c071862 )