EV-Telegram-TDLib
view release on metacpan or search on metacpan
lib/EV/Telegram/TDLib/Cookbook.pod view on Meta::CPAN
=head1 NAME
EV::Telegram::TDLib::Cookbook - task-oriented recipes for EV::Telegram::TDLib
=head1 DESCRIPTION
Recipes for common tasks, showing how the pieces of
L<EV::Telegram::TDLib> combine. This is not the reference; the main
L<EV::Telegram::TDLib> manual is, and it documents every option and
callback named here.
Conventions in every recipe:
=over 4
=item *
Callbacks receive C<($result, $err)>; C<$err> is undef on success.
Errors are never thrown.
=item *
C<use EV; use EV::Telegram::TDLib;> is assumed, as is a constructed
C<$td> client. Credentials come from the environment
(TD_API_ID, TD_API_HASH, TD_PHONE, TD_BOT_TOKEN) -- a convention of
these recipes and F<eg/>, not the module's API.
=item *
Every snippet ends up inside a running EV loop; stand-alone recipes
finish with C<EV::run>.
=back
The recipes that need no credentials (formatted text, raw methods,
multiple clients) were run for real against TDLib 1.8.66. The rest
were checked field by field against the pinned TDLib scheme
(td_api.tl at commit 022d60202e446ad1287b9fb68e687c8a0760788b).
=head1 RECIPES
=head2 Logging in as a user, and reusing the session
Problem: authorize a user account with phone number, SMS code and
optional 2FA password -- and do it only once.
The database directory IS the session. The first run asks for the
code; every later run with the same C<database_directory> goes
straight to authorizationStateReady and C<on_code> never fires. Keep
the callbacks in place anyway: sessions can be revoked server-side.
A mistyped code or password does not fail login: the callback fires
again with the TDLib error as a third argument, so prompt again and
resubmit. What fails login outright is a rejected automatic step --
an invalid phone number or bot token, or parameters Telegram
refuses.
my $td = EV::Telegram::TDLib->new(
api_id => $ENV{TD_API_ID},
api_hash => $ENV{TD_API_HASH},
phone_number => $ENV{TD_PHONE},
database_directory => 'tdlib-db', # the session; keep it secret
on_code => sub {
my ($info, $submit) = @_;
print "code from Telegram: ";
chomp(my $code = <STDIN>);
$submit->($code);
},
on_password => sub { # only fires when 2FA is enabled
my ($info, $submit) = @_;
print "2FA password: ";
chomp(my $password = <STDIN>);
$submit->($password);
},
on_error => sub { warn "tdlib: $_[0]\n" },
);
$td->login(sub {
my (undef, $err) = @_;
die "login failed: $err->{message}\n" if $err;
$td->me(sub {
my ($me, $err) = @_;
die "getMe failed: $err->{message}\n" if $err;
# usernames is a plural object in this TDLib; there is
# no user->{username} field
my $names = $me->{usernames};
my $username = $names ? $names->{active_usernames}[0] : undef;
print "logged in as $me->{first_name}",
(defined $username ? " (\@$username)" : ''), "\n";
$td->close(sub { EV::break });
});
});
EV::run;
lib/EV/Telegram/TDLib/Cookbook.pod view on Meta::CPAN
print "$text\n"; # already characters
printf "%d characters\n", length $text;
});
The one way to get this wrong is to encode it yourself first. Doing so
sends each byte as its own character, and nothing complains:
use Encode ();
$td->send_message($chat_id, Encode::encode('UTF-8', $text), sub { });
# arrives as mojibake, with no error anywhere
C<use utf8> matters only for literals in your own source. Text read
from a socket, a file or a database is bytes until you decode it, so
decode once where it enters and pass characters from there on. See
L<EV::Telegram::TDLib/UNICODE> for the details.
=head2 Calling a TDLib method with no wrapper
Problem: use one of the roughly one thousand TDLib methods the mixins
do not cover.
C<call> is the usual way in. It fills in the C<@type> from the method
name and checks the argument names against a catalogue of every TDLib
function, so a typo fails in Perl with the valid names listed instead
of coming back as an opaque server error:
$td->call(getChatMember => {
chat_id => $chat_id,
member_id => { '@type' => 'messageSenderUser', user_id => $user_id },
}, sub {
my ($member, $err) = @_;
die "getChatMember: $err->{message}\n" if $err;
print "status: $member->{status}{'\@type'}\n";
});
A method the catalogue does not know is passed straight through, so a
TDLib newer than the shipped catalogue still works; a missing argument
is not an error, since TDLib supplies its own defaults.
C<send> is the same thing without the check, and is what C<call> uses
underneath. Reach for it when you want no validation at all. It takes
any raw request hashref and correlates the reply by
C<@extra>, which it assigns itself and returns. Never set C<@extra>
by hand: a caller-supplied value is overwritten on purpose, because a
collision would misroute a reply to the wrong callback. C<execute> is
the synchronous twin for the few methods TDLib documents as
synchronous; it needs no network and no authorization.
my $version = EV::Telegram::TDLib->execute(
{ '@type' => 'getOption', name => 'version' });
print "TDLib $version->{value}\n"; # an optionValueString reply
my $extra;
$extra = $td->send({ '@type' => 'getOption', name => 'version' }, sub {
my ($res, $err) = @_;
die "getOption failed: $err->{message}\n" if $err;
print "async reply: $res->{value} (\@extra $extra)\n";
$td->close(sub { EV::break });
});
This recipe runs with C<auto_auth =E<gt> 0> and no credentials at
all; F<eg/06-raw-method.pl> is exactly that.
=head2 Timing out a request
Problem: do not wait forever for a reply that may never come.
C<send> accepts a C<timeout> in seconds. On expiry the callback
fails with a synthetic error -- code -1, message C<timeout>:
$td->send({ '@type' => 'getMe' }, sub {
my ($user, $err) = @_;
if ($err) {
if (($err->{code} // 0) == -1 && $err->{message} eq 'timeout') {
warn "getMe gave up after 5s\n";
} else {
warn "getMe failed: $err->{message}\n";
}
return;
}
print "hello $user->{first_name}\n";
}, timeout => 5);
If TDLib's reply arrives after the timeout, it is dropped with a
warning on stderr. It is never delivered to your callback a second
time, and never to a later request that reused the C<@extra> sequence.
=head2 Handling rate limits
Problem: Telegram answered error code 429, "Too Many Requests:
retry after N" -- and a naive immediate-retry loop is how accounts
get limited.
Code 429 means slow down. In this TDLib the generic error type is
just C<code> and C<message>, so the delay lives in the message text;
L<EV::Telegram::TDLib/"retry_after($err)"> digs it out, returning
undef for anything that is not a 429 carrying a delay. The retry is
then scheduled with an EV::timer, never a sleep and never an
immediate retry:
sub send_with_backoff {
my ($req, $cb) = @_;
$td->send($req, sub {
my ($res, $err) = @_;
if (defined(my $wait = $td->retry_after($err))) {
$wait += 1; # a little margin
warn "rate limited, retrying in ${wait}s\n";
my $t; $t = EV::timer $wait, 0, sub {
undef $t;
send_with_backoff($req, $cb);
};
return;
}
$cb->($res, $err);
});
}
send_with_backoff({ '@type' => 'getMe' }, sub {
my ($user, $err) = @_;
warn "getMe failed: $err->{message}\n" if $err;
});
( run in 1.285 second using v1.01-cache-2.11-cpan-007c89162af )