NAME

Bot::Telegram - a micro^W nano framework for creating Telegram bots based on WWW::Telegram::BotAPI

VERSION

version 1.10

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) = @_;
    my $chat = $$update{message}{chat}{id};
    my $user = $$update{message}{from}{username};
    my $text = $$update{message}{text};

    say "> User $user says: $text";

    $bot -> api -> sendMessage(
      { chat_id => $chat, text => "Hello there, $user!" },
      sub {
        my ($ua, $tx) = @_;
        if ($tx -> res -> json -> {ok}) {
          say "> Greeted user $user";
        }
      }
    );
  },

  edited_message => sub { 
    my ($bot, $update) = @_;

    my $user = $$update{edited_message}{from}{username};
    say "> User $user just edited their message";
  },
);

# You might want to increase/disable inactivity timeouts for long polling
$bot
  -> api
  -> agent
  -> inactivity_timeout(0);

# Maybe remove some default subscribers...
$bot -> unsubscribe('callback_error');

# Or replace them with custom ones...
$bot -> on(callback_error => sub {
  my $error = pop;
  $bot -> log -> fatal("update processing failed: $error");
  exit 255;
});

# Start long polling
$bot -> start_polling;
Mojo::IOLoop -> start;

DESCRIPTION

This package provides a tiny wrapper around WWW::Telegram::BotAPI that takes care of the most annoying boilerplate, especially for the long polling scenario.

Supports both synchronous and asynchronous modes of WWW::Telegram::BotAPI.

Just like the aforementioned WWW::Telegram::BotAPI, it doesn't rely too much on current state of the API - only a few fields and assumptions are used for decision making (namely, ok, result, description, error_code [presence], getUpdates POST body format and the assumption that getUpdates response would be an array of update objects, each consisting of two fields - update_id and the other one, named after the update it represents and holding the actual update contents), meaning we don't have to update the code every week just to keep it usable.

RATIONALE

WWW::Telegram::BotAPI (which this module heavily depends on) is a low-level thing not responsible for sorting updates by their types, setting up a long polling loop, etc, and using it alone might not be sufficient for complex applications. Even the simple "SYNOPSIS" example will quickly become an if-for-eval mess, should we rewrite it in pure WWW::Telegram::BotAPI, and maintaining/extending such a codebase would be a disaster.

All other similar libraries available on CPAN are either outdated, or incomplete, or... not very straightforward (imo), so I made my own!

EVENTS

Bot::Telegram inherits all events from Mojo::EventEmitter and can emit the following new ones.

callback_error

$bot -> on(callback_error => sub {
  my ($bot, $update, $error) = @_;
  warn "Update processing failed: $error";
});

Emitted when a callback dies.

Default subscriber will log the error message using "log" with the warn log level:

[1970-01-01 00:00:00.00000] [12345] [warn] Update processing failed: error details here

polling_error

$bot -> on(polling_error => sub {
  my ($bot, $tx, $type) = @_;
});

Emitted when a getUpdates request fails inside the polling loop.

Keep in mind that the loop will keep working despite the error. To stop it, you will have to call "stop_polling" explicitly:

$bot -> on(polling_error => sub { $bot -> stop_polling });

In synchronous mode, $tx will be a plain hash ref. The actual result of "parse_error" in WWW::Telegram::BotAPI is available as the error field of that hash.

$bot -> on(polling_error => sub {
  my ($bot, $tx, $type) = @_;

  for ($type) {
    if (/api/) {
      my $error = ($tx -> res -> json // {}) -> {description};
    }

    elsif (/agent/) {
      if ($bot -> is_async) { # or `$tx -> isa('Mojo::Transaction::HTTP')`, if you prefer
        my $error = $tx -> error -> {message};
      } else {
        my $error = $tx -> {error}{msg};
      }
    }
  }
});

In asynchronous mode, the logic responsible for making the "error type" decision is modelled after "parse_error" in WWW::Telegram::BotAPI, meaning you will always receive same $type values for same errors in both synchronous and asynchronous modes.

See "parse_error" in WWW::Telegram::BotAPI for the list of error types and their meanings.

Default subscriber will log the error message using "log" with the warn log level:

[1970-01-01 00:00:00.00000] [12345] [warn] Polling failed (error type: $type): error details here

unknown_update

$bot -> on(unknown_update => sub {
  my ($bot, $update) = @_;
  say "> No callback defined for this kind of updates. Anyway, here's the update object:";

  require Data::Dump;
  Data::Dump::dd($update);
});

Emitted when an update of an unregistered type is received.

The type is considered "unregistered" if there is no matching callback configured (i.e. $self -> callbacks -> {$update_type} is not a coderef).

Exists mostly for debugging purposes.

There are no default subscribers to this event.

PROPERTIES

Bot::Telegram inherits all properties from Mojo::EventEmitter and implements the following new ones.

api

my $api = $bot -> api;
$bot -> api($api);

WWW::Telegram::BotAPI instance used by the bot. Can be initialized via the "init_api" method, or set directly.

callbacks

my $callbacks = $bot -> callbacks;
$bot -> callbacks($callbacks);

Hash reference containing callbacks for different update types.

While you can manipulate it directly, "set_callbacks" and "remove_callbacks" methods provide a more convinient interface.

current_update

my $update = $bot -> current_update;
say "User $$update{message}{from}{username} says: $$update{message}{text}";

Update that is currently being processed.

ioloop

$loop = $bot -> ioloop;
$bot -> ioloop($loop);

A Mojo::IOLoop object used to delay execution in synchronous mode, defaults to a new Mojo::IOLoop object.

log

$log = $bot -> log;
$bot -> log($log);

A Mojo::Log instance used for logging, defaults to a new Mojo::Log object with log level set to info.

polling_config

$bot -> polling_config($cfg);
$cfg = $bot -> polling_config;

See $cfg in "start_polling".

METHODS

Bot::Telegram inherits all methods from Mojo::EventEmitter and implements the following new ones.

api_request

$bot -> api_request('getMe');

Just a proxy function for the underlying "api_request" in WWW::Telegram::BotAPI.

The above statement is basically equivalent to:

$bot -> api -> api_request('getMe');

except that it's shorter and adds another entry to your call stack.

api_request_p

$p = $bot -> api_request_p('getMe');
$p -> then(sub {
  my ($ua, $tx) = @_;
  say 1 if $res -> json -> {ok}; # always true
}) -> catch(sub {
  my ($ua, $tx) = @_;

  if (my $err = $tx -> error) {
    die "$$err{code} response: $$err{message}"
      if $$err{code};

    die "Connection error: $$err{message}";
  } else {
    warn 'Action failed!';
    say {STDERR} $tx -> res -> json -> {description};
  }
});

A promisified wrapper for the underlying "api_request" in WWW::Telegram::BotAPI. The promise is rejected if there is an error in $tx or response is not ok. For both resolve and reject scenarios, the callback receives ($ua, $tx) from normal "api_request" in WWW::Telegram::BotAPI.

init_api

$bot = $bot -> init_api(%args);

Automatically creates a WWW::Telegram::BotAPI instance.

%args will be proxied to "new" in WWW::Telegram::BotAPI.

For most use cases you only want to set $args{token} to your bot's API token and leave everything else default.

NOTE: the WWW::Telegram::BotAPI instance created by "init_api" defaults to the asynchronous mode.

Exceptions

Bot::Telegram::X::InvalidArgumentsError

No token provided

is_async

my $is_async = $bot -> is_async;

Returns true if the underlying WWW::Telegram::BotAPI instance is in asynchronous mode.

Exceptions

Bot::Telegram::X::InvalidStateError

API is not initialized

is_polling

my $is_polling = $bot -> is_polling;

Returns true if the bot is currently in the long polling state.

process_update

$bot = $bot -> process_update($update);

Process a single update and store it in "current_update".

This function will not die regardless of the operation success. Instead, the "callback_error" event is emitted if things go bad.

remove_callbacks

$bot = $bot -> remove_callbacks(qw/message edited_message/);
# From now on, bot considers 'message' and 'edited_message' unknown updates

Remove callbacks for given update types, if set.

set_callbacks

$bot -> set_callbacks(
  message => sub {
    my ($bot, $update) = @_;
    handle_message $update;
  },

  edited_message => sub {
    my ($bot, $update) = @_;
    handle_edited_message $update;
  }
);

Set callbacks to match specified update types.

set_webhook

$bot = $bot -> set_webhook($config);
$bot = $bot -> set_webhook($config, $cb);

Set a webhook. All arguments will be proxied to "api_request" in WWW::Telegram::BotAPI.

This function ensures that actual setWebhook request will not be made as long as the polling loop is active:

eval { $bot -> set_webhook($config) };

if ($@ -> isa('Bot::Telegram::X::InvalidStateError')) {
  $bot -> stop_polling;
  $bot -> set_webhook($config);
}

For deleting the webhook, just use plain API calls:

$bot -> api_request(deleteWebhook => { drop_pending_updates => $bool }, sub { ... });

Exceptions

Bot::Telegram::X::InvalidArgumentsError

No config provided

Bot::Telegram::X::InvalidStateError

Disable long polling first

shift_offset

$bot = $bot -> shift_offset;

Recalculate the current offset for long polling.

Set it to the ID of "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 ("start_polling"), but the method is made public, if you want to roll your own custom polling loop for some reason.

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 "log" level to trace to see additional debugging information.

Arguments

$cfg

A hash ref containing getUpdates options. Note that the offset parameter is automatically incremented - whenever an update is processed (whether successfully or not), the internally stored 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 "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 "polling_config" is empty, a default config will be generated:

{ timeout => 20, offset => 0 }
restart

Set to true if the loop is already running, otherwise an exception will be thrown.

interval

Interval in seconds between polling requests.

Floating point values are accepted (timers are set using "timer" in Mojo::IOLoop).

Default value is 0.3 (300ms).

Exceptions

Bot::Telegram::X::InvalidStateError

Already running

stop_polling

$bot = $bot -> stop_polling;

Stop long polling.

SEE ALSO

Bot::ChatBots::Telegram - another library built on top of WWW::Telegram::BotAPI

Telegram::Bot - another, apparently incomplete, Telegram Bot API interface

Telegram::BotKit - provides utilities for building reply keyboards and stuff, also uses WWW::Telegram::BotAPI

WWW::Telegram::BotAPI - lower level Telegram Bot API library used here

AUTHOR

Vasyan <somerandomtext111@gmail.com>

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.