EV-Telegram-TDLib
view release on metacpan or search on metacpan
lib/EV/Telegram/TDLib.pm view on Meta::CPAN
my ($msg) = @_;
print "message $msg->{id} in chat $msg->{chat_id}\n";
},
on_error => sub { warn "tdlib: $_[0]\n" },
);
# a die inside a callback is contained and reported, not propagated, so
# leaving the loop is what ends the program; see L</ERROR HANDLING>
my $status = 0;
$td->login(sub {
my (undef, $err) = @_;
if ($err) {
warn "login failed: $err->{message}\n";
$status = 1;
return EV::break;
}
$td->send_message($chat_id, 'hello', sub {
my ($msg, $err) = @_;
warn "send failed: $err->{message}\n" if $err;
$status = 1 if $err;
$td->close(sub { EV::break });
});
});
EV::run;
exit $status;
=head1 DESCRIPTION
EV::Telegram::TDLib binds TDLib's tdjson C interface to the L<EV>
event loop. A dedicated reader thread blocks in td_receive, copies each
JSON result, and wakes the loop through ev_async; the loop decodes,
correlates replies to pending requests by C<@extra>, drives the
authorization state machine, maintains user and chat caches, and calls
your handlers.
Asynchronous callbacks follow the family idiom: they receive
C<($result, $err)> where C<$err> is undef on success and a decoded
TDLib error object on failure. A TDLib error is never thrown; see
L</CONVENTIONS> for what does croak.
Requires a perl with 64-bit integers: Telegram ids are int64 and
message ids are shifted left by 20 bits, so they must never round-trip
through an NV. The Makefile refuses to build otherwise.
The bundled TDLib is 1.8.66, pinned by Alien::TDLib at commit
022d60202e446ad1287b9fb68e687c8a0760788b.
=head1 CONSTRUCTOR
=head2 new(%opt)
Creates a client and registers it in a process-global registry. The
client is held under a strong reference until close() completes; see
L</CAVEATS>. Options:
=over 4
=item api_id, api_hash
Telegram application credentials from https://my.telegram.org. Keep
them in the environment, not in source; see L</SECURITY>.
=item phone_number
Phone number in international format for user authorization. Used
when the state machine reaches authorizationStateWaitPhoneNumber,
unless bot_token is present. Setting on_qr as well does not override
it: a QR link is requested only when on_qr is set and no phone_number
was given.
=item bot_token
Bot token from BotFather. When present it is sent automatically at
authorizationStateWaitPhoneNumber and no further credential callbacks
are needed.
=item database_directory
Session and database directory. Default C<tdlib-db>. See L</SECURITY>.
=item files_directory
Downloaded files directory. Defaults to database_directory.
=item database_encryption_key
Encryption key for the local database. Empty by default; set it. Any
byte string will do, but it must be bytes: a character above 255 is
rejected while the first request is being built, which happens during
update dispatch, so it reaches C<on_error> rather than croaking out of
new(). It is base64-encoded on the way out, so what you pass is what
keys the database.
B<Changed in 0.04.> 0.03 sent this value raw, which TDLib refused for
most passphrases -- taking login with it -- while accepting one that
happened to look like base64 and keying the database with the decoded
bytes instead. If a 0.03 database opened with such a key, open it now
with C<MIME::Base64::decode_base64($old_key)>.
Changing it later needs
L<set_database_encryption_key()|/"set_database_encryption_key($key, $cb), session_accepts_secret_chats($session_id, $on, $cb)">,
and losing it loses the database.
=item use_test_dc
Use the Telegram test data centers instead of production. Always set
this in tests.
=item use_file_database, use_chat_info_database, use_message_database, use_secret_chats
TDLib feature switches, all defaulting to true. TDLib turns a switch
back on when a later one needs it -- the message database needs the
chat info database, which needs the file database -- so
C<< use_chat_info_database => 0 >> does nothing unless
C<< use_message_database => 0 >> too, and C<< use_file_database => 0 >>
needs both.
=item application_name
The platform identifier sent with every Mini App request, which the app
lib/EV/Telegram/TDLib.pm view on Meta::CPAN
Build and amend quick replies. Adding takes a shortcut B<name> and
creates the shortcut if it does not exist; the rest take its id.
add_quick_reply_message takes C<reply_to>, the id of an earlier message
in the same shortcut to reply to.
=head3 set_quick_reply_name($shortcut_id, $name, $cb), reorder_quick_replies(\@ids, $cb), send_quick_reply($chat_id, $shortcut_id, %opt, $cb), check_quick_reply_name($name)
Rename, order and send a shortcut. send_quick_reply takes
C<sending_id>, a non-persistent id of your own choosing that comes back
in the messages' messageSendingStatePending, which is how you match
them to the updateNewMessage updates that follow. It calls back once
Telegram accepts the messages, and refuses C<< wait => 'sent' >>.
check_quick_reply_name is synchronous: it returns the result, and
croaks if given a callback.
=head1 MINI APPS
A Mini App (Telegram also calls it a Web App) is a web page a bot
offers, opened inside a Telegram client. TDLib does not render it. It
resolves the app, returns a URL and a launch id, and relays the data
the page sends back; hosting a webview and loading the URL is the
application's job. Nothing here needs a browser if all you want is the
data channel.
The usual flow is:
$td->web_app($bot_id, 'probe', sub {
my ($found, $err) = @_;
...
});
$td->open_web_app($chat_id, $bot_id, $button_url, sub {
my ($info, $err) = @_;
# hand $info->{url} to a webview, keep $info->{launch_id}
});
$td->close_web_app($launch_id, sub { });
Data flows back either through
L</send_web_app_data($bot_user_id, $button_text, $data, $cb)>, which a
client calls on the page's behalf and the bot receives through
L</on_web_app_data($cb)>, or through
L<answer_web_app_query()|/"answer_web_app_query($query_id, \%result, $cb), web_app_request($bot_user_id, $method, $parameters, $cb), web_app_placeholder($bot_user_id, $cb)"> for the inline
variant.
=head2 The platform identifier
C<application_name> is not a free-form label. It is sent to Telegram as
the platform string and handed to the page as C<tgWebAppPlatform>.
Telegram accepts 0-64 characters from C<A-Za-z0-9_> and rejects
anything else with C<PLATFORM_INVALID>, an error that names nothing
near the real cause; a hyphen is the easy way to trip it. This module
validates the value when the client is constructed, so the failure
arrives with an explanation instead.
Any accepted value works, but the value still matters. Real clients
send a conventional identifier (C<android>, C<ios>, C<macos>,
C<tdesktop>, C<weba>, C<webk>) and Mini App pages branch on it to pick
layout, theming and available features. An invented name passes
validation and then lands in whatever an app does with an unrecognised
platform. The default is C<tdesktop>.
=head2 Launch URLs carry credentials
The URL returned by open_web_app and the web_app_*_url methods has the
signed init data in its fragment: the user's name, username, photo URL
and an authentication hash. It is a credential. Do not log it, paste it
into a bug report, or store it anywhere the page itself would not go.
=head1 AUTHORIZATION
TDLib drives authorization as a state machine reported through
updateAuthorizationState; L</auth_state()> exposes the current state.
With auto_auth on (the default), each state is answered automatically
or routed to a credential callback:
=over 4
=item authorizationStateWaitTdlibParameters
setTdlibParameters is sent automatically from the constructor options.
No callback. An error reply (bad api credentials, an unwritable
database_directory) fails login: the values come from the constructor,
so there is no interactive channel to retry through.
=item authorizationStateWaitPhoneNumber
bot_token is sent when given; otherwise requestQrCodeAuthentication
when on_qr is set and no phone_number was given; otherwise the
phone_number is sent. No callback in any branch. An error reply
(an invalid phone number or bot token) fails login, for the same
reason as above.
=item authorizationStateWaitCode
C<on_code> receives C<($info, $submit)>: $info is the decoded
authenticationCodeInfo, $submit is a code ref that sends the code.
The split exists so the code can come from anywhere (a prompt, a GUI,
a queue) without blocking the loop. A missing callback fails login.
A rejected submission does not fail login: TDLib stays in the state
after an error reply (a mistyped code, an expired one), so the
handler is called again as C<($info, $submit, $err)> with the decoded
error as the third argument, and may submit a corrected value. To
give up instead, close the client.
=item authorizationStateWaitPassword
C<on_password> receives C<($info, $submit)>; $info carries the
password_hint. A missing callback fails login. A rejected password
re-asks with the error as a third argument, as above.
=item authorizationStateWaitEmailAddress, authorizationStateWaitEmailCode
C<on_email> and C<on_email_code>, same C<($info, $submit)> shape and
the same retry-on-error behaviour.
=item authorizationStateWaitOtherDeviceConfirmation
C<on_qr> receives C<($link)> only. The signature is deliberately
asymmetric: QR confirmation has nothing to submit, the other device
confirms the login, so there is no $submit callback.
=item authorizationStateWaitRegistration
Answered automatically from the register option; without it login
fails. An error reply from registerUser fails login: like the other
automatic steps, it has no interactive channel.
=item authorizationStateWaitPremiumPurchase
Cannot be satisfied programmatically; login fails with an error.
=item authorizationStateReady
The login() callback succeeds.
=item authorizationStateClosed
Pending requests, in-flight sends and downloads are failed, close()
callbacks run, then on_close fires.
lib/EV/Telegram/TDLib.pm view on Meta::CPAN
of B<that> update, on_update included. Either way the next update is
unaffected. The two read differently in the report: C<a callback died>
for a guarded one, C<dispatch died> for the rest.
The practical consequence is that B<die is not how a callback ends the
program>. It is reported and the loop keeps running, so a script whose
only exit path was the die simply hangs. Leave the loop instead, and
let the exit status carry the failure:
$td->login(sub {
my (undef, $err) = @_;
if ($err) {
warn "login failed: $err->{message}\n";
$status = 1;
return EV::break;
}
...
});
EV::run;
exit $status;
The containment covers every asynchronous delivery, not only the ones
that arrive through the dispatch. A callback answered from a timer --
a L<send()|/"send(\%request, $cb, %opt)"> timeout, an ask timing out, a
deferred failure on a closed client -- is guarded the same way, so a
die there reaches C<on_error> rather than only EV's own stderr.
Some errors are delivered synchronously, before the method returns:
a parse_mode failure in
L</send_message($chat_id, $text, %opt, $cb)> or
L</edit_message($chat_id, $message_id, $text, %opt, $cb)>, and equally
in send_file, send_poll and answer_inline_query, invokes the callback
with the parseTextEntities error before the method returns, and
nothing is sent. A download already in progress and a mark_read with
nothing to mark report the same way.
=head1 ENVIRONMENT
=over 4
=item EV_TDLIB_SHUTDOWN_TIMEOUT
Seconds the END block waits for open clients to finish closing before
giving up, default 3. Giving up tears TDLib's statics down while it is
still closing, which can abort the process at exit -- TDLib detaches
its scheduler thread rather than joining it once exit has begun, so
the crash is a race and will not show on every run. Raise this on a
heavily loaded machine or under a sanitizer, where everything runs
several times slower.
=item TDLIB_LOG_VERBOSITY
TDLib's log verbosity level, applied once when the module is loaded.
Defaults to 1; TDLib's own default of 5 is very noisy on stderr.
=item TD_API_ID, TD_API_HASH, TD_PHONE, TD_BOT_TOKEN, TD_DATABASE_DIRECTORY
Not the module's API: the credential convention shared by the scripts
in F<eg/> and by F<xt/live_auth.t>. The module itself takes
credentials only as constructor options; see L</new(%opt)>.
=back
=head1 EXAMPLES
Runnable scripts live in F<eg/>. Once the module is installed, run one
from anywhere with C<perl eg/NAME.pl>; from an unpacked distribution
C<perl -Mblib eg/NAME.pl> works after C<make>, and not before it, since
there is no F<blib> until then. Credentials come from the environment,
see L</ENVIRONMENT>:
=over 4
=item F<eg/01-login.pl>
user login with phone, SMS code and 2FA; creates the session database
=item F<eg/02-bot-echo.pl>
bot login via token; echoes incoming text messages
=item F<eg/03-list-chats.pl>
loads the chat list and prints id and title per chat
=item F<eg/04-send-message.pl>
sends a markdown message and waits for real delivery
=item F<eg/05-download-file.pl>
downloads a file id with progress percentage
=item F<eg/06-raw-method.pl>
raw send()/execute() for methods the mixins do not wrap
=item F<eg/07-gtk4-chat.pl>
a two-pane GTK4 chat window driven by EV rather than by gtk_main
=item F<eg/08-tickit-chat.pl>
the same two panes in a terminal, on the same single loop
=item F<eg/09-mcp-server.pl>
exposes Telegram as MCP tools over JSON-RPC on stdio
=item F<eg/10-webapp-bot.pl>
offers a Mini App button and prints the data the page sends back
=item F<eg/11-command-bot.pl>
command routing, inline button routing and ask()
=back
L<EV::Telegram::TDLib::Cookbook> has task-oriented recipes.
lib/EV/Telegram/TDLib.pm view on Meta::CPAN
Clients stay registered until closed. The registry holds a strong
reference on purpose: TDLib requires every client to be closed before
process exit, so an object must not vanish when the caller drops its
last reference. L</close($cb)> is not optional; dropping your last
Perl reference does not close anything. See L</AUTHORIZATION> for the
Closed state that ends the lifecycle.
=item *
An END block closes leftover clients and pumps the loop for a bounded
interval (three seconds, or C<EV_TDLIB_SHUTDOWN_TIMEOUT>) so TDLib
flushes its database, then joins the reader thread. B<Pumping the loop
runs your callbacks>, so a program that exits early -- from a
login-failure path, say, or one that never ran the loop at all -- still
sees on_update and reply callbacks fire during global destruction,
after its own END blocks have run. Write them so they are safe to run
then, or close() before you leave. The three seconds
bound the pump only: the join has its own two-second wait, after which
it prods TDLib to wake the reader and then waits without a deadline,
and a reader sitting in td_receive can take up to ten seconds more to
notice. Shutdown is bounded, but by the sum rather than by the three.
It is a safety net, not a substitute for close().
=item *
A callback that dies is contained and reported through on_error; the
drain continues. See L</ERROR HANDLING>.
=back
=head1 SECURITY
The database directory holds the session: whoever reads it owns the
account. Treat it as exactly as sensitive as a password: restrictive
permissions, no commits, no backups to third-party storage.
TDLib creates that directory 0750 and its binlog 0600, but the sqlite
files are created 0644 masked by your umask, so 022 and 002 both leave
them world-readable and only a narrower umask changes that: 027 gives
0640, 077 gives 0600. The 0750 directory is what keeps other users
out. This module does not set a umask, which
is the caller's to choose; set one before creating a client if the
default is not what you want. Note also that the examples default to a
C<tdlib-db> in the working directory, so running one from a checkout
leaves a live session there.
Credentials stay in the object. api_id, api_hash, bot_token, the phone
number and the encryption key are held for the life of the client, so a
core dump or a process-memory read exposes them. Perl cannot reliably
erase a string, so this is a property to design around rather than
something the module can fix: disable cores where it matters.
Set database_encryption_key so the local database is encrypted at
rest, and keep the key out of source control.
api_id and api_hash identify your application to Telegram. Pass them
from your environment rather than hardcoding them; new() does not read
any environment variable itself, but the examples in F<eg/> take them
from TD_API_ID and TD_API_HASH.
The login callbacks receive credentials from wherever you choose to
read them. If that is a terminal, do not echo the 2FA password: it
lands in the scrollback, in a screen recording and in whatever the
session is logged to. F<eg/01-login.pl> shows the shape -- turn echo
off through a guard object, so an interrupt cannot leave the terminal
silent for whatever runs next.
B<TDLib's log can carry credentials.> Above the lowest levels it writes
the requests it makes, and those include the ones carrying your
api_hash and bot token; logging in with verbosity raised has been
observed to put both on stderr in clear. TDLib's own default is 5, so
this module sets 1 at load, and a C<TDLIB_LOG_VERBOSITY> it cannot read
as a level falls back to 1 rather than leaving TDLib at 5. Raise it to
debug something and whatever collects your stderr collects that too.
=head1 REQUIREMENTS
=over 4
=item *
perl 5.12 or later, built with 64-bit integers. Makefile.PL refuses to
configure when ivsize is below 8: Telegram chat and user ids are
int64, and message ids are shifted left by 20 bits, so they must
never round-trip through an NV.
=item *
L<EV> 4.11 or later.
=item *
L<Cpanel::JSON::XS> 4.00 or later.
=item *
L<Alien::TDLib>, at configure and build time. It provides TDLib
1.8.66, pinned at commit 022d60202e446ad1287b9fb68e687c8a0760788b;
TDLib itself is licensed under the Boost Software License 1.0.
=back
=head1 LIMITATIONS
Deliberately out of scope:
=over 4
=item *
No Bot API (HTTP) client; this binding speaks tdjson only.
=item *
No voice or video calls.
=item *
No group calls or video chats.
=item *
Around 418 of TDLib's roughly one thousand methods have a
hand-written wrapper. For the rest,
L<call()|/"call($function, \%args, $cb, %opt)"> validates argument names
against a shipped schema catalogue, and
L<send()|/"send(\%request, $cb, %opt)"> and L</execute(\%request)>
are the escape hatch (see L</ESCAPE HATCH>).
( run in 0.589 second using v1.01-cache-2.11-cpan-3f962061493 )