EV-Telegram-TDLib

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN


        my $chat_id = $ENV{TD_CHAT_ID};

        my $td = EV::Telegram::TDLib->new(
            api_id             => $ENV{TD_API_ID},
            api_hash           => $ENV{TD_API_HASH},
            phone_number       => '+10000000000',
            database_directory => 'tdlib-db',
            on_code    => sub {
                my ($info, $submit) = @_;
                print "code from Telegram: ";
                chomp(my $code = <STDIN>);
                $submit->($code);
            },
            on_message => sub {
                my ($msg) = @_;
                print "message $msg->{id} in chat $msg->{chat_id}\n";
            },
            on_error   => sub { warn "tdlib: $_[0]\n" },
        );

        $td->login(sub {
            my (undef, $err) = @_;
            die "login failed: $err->{message}\n" if $err;
            $td->send_message($chat_id, 'hello', sub {
                my ($msg, $err) = @_;
                die "send failed: $err->{message}\n" if $err;
                $td->close(sub { EV::break });
            });
        });

        EV::run;

DESCRIPTION
    EV::Telegram::TDLib binds TDLib's tdjson C interface to the 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 @extra, drives the
    authorization state machine, maintains user and chat caches, and calls
    your handlers.

    Asynchronous callbacks follow the family idiom: they receive "($result,
    $err)" where $err is undef on success and a decoded TDLib error object
    on failure. A TDLib error is never thrown; see "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.

CONSTRUCTOR
  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
    "CAVEATS". Options:

    api_id, api_hash
        Telegram application credentials from https://my.telegram.org. Keep
        them in the environment, not in source; see "SECURITY".

    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.

    bot_token
        Bot token from BotFather. When present it is sent automatically at
        authorizationStateWaitPhoneNumber and no further credential
        callbacks are needed.

    database_directory
        Session and database directory. Default "tdlib-db". See "SECURITY".

    files_directory
        Downloaded files directory. Defaults to database_directory.

    database_encryption_key
        Encryption key for the local database. Empty by default; set it.

    use_test_dc
        Use the Telegram test data centers instead of production. Always set
        this in tests.

    use_file_database, use_chat_info_database, use_message_database,
    use_secret_chats
        TDLib feature switches, all defaulting to true.

    application_name
        The platform identifier sent with every Mini App request, which the
        app receives as "tgWebAppPlatform". Defaults to "tdesktop". See
        "MINI APPS" for why the value matters and what it may contain.

    system_language_code, device_model, system_version, application_version
        Client identification sent with setTdlibParameters. Defaults: "en",
        "EV::Telegram::TDLib", $^O, this distribution's version.

    auto_auth
        Drive the authorization state machine automatically (default true).
        With auto_auth false, only the login and close lifecycle
        continuations run; every credential step is left to you via send().

    register
        Hashref "{ first_name => ..., last_name => ... }". When set,
        authorizationStateWaitRegistration is answered with registerUser;
        without it the state fails login.

    on_update, on_error, on_close, on_user, on_chat, on_message,
    on_connection_state
        Update handlers; see "UPDATES" and the mixin methods below.

    on_code, on_password, on_email, on_email_code, on_qr
        Authorization credential callbacks; see "AUTHORIZATION".

CONVENTIONS
    Three patterns run through the whole interface. Knowing them saves
    reading 300 method signatures.

README  view on Meta::CPAN

    Secret chats live only in the local database. They are not on the
    server, cannot be read from another device, and do not survive losing
    the database.

   new_secret_chat($user_id, $cb), open_secret_chat($secret_chat_id, $cb), secret_chat($secret_chat_id, $cb), close_secret_chat($secret_chat_id, $cb)
    Start a secret chat with a user, reopen a known one, read its state, and
    close it.

   search_secret_messages($query, %opt, $cb)
    Searches the local database, since secret messages exist nowhere else.
    Options: "chat_id" to scope to one chat, "filter" (a
    searchMessagesFilter name, with or without the prefix), "offset",
    "limit".

   set_database_encryption_key($key, $cb), session_accepts_secret_chats($session_id, $on, $cb)
    Change the key the local database is encrypted with, and choose whether
    a logged-in session may accept secret chats at all. Losing the key loses
    every secret chat with it, as nothing on the server can restore them.

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 "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 "on_web_app_data($cb)", or through
    "answer_web_app_query($query_id, \%result, $cb)" for the inline variant.

  The platform identifier
    "application_name" is not a free-form label. It is sent to Telegram as
    the platform string and handed to the page as "tgWebAppPlatform".
    Telegram accepts 0-64 characters from "A-Za-z0-9_" and rejects anything
    else with "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 ("android", "ios", "macos", "tdesktop",
    "weba", "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 "tdesktop".

  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.

AUTHORIZATION
    TDLib drives authorization as a state machine reported through
    updateAuthorizationState; "auth_state()" exposes the current state. With
    auto_auth on (the default), each state is answered automatically or
    routed to a credential callback:

    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.

    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.

    authorizationStateWaitCode
        "on_code" receives "($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 "($info, $submit, $err)" with the decoded
        error as the third argument, and may submit a corrected value. To
        give up instead, close the client.

    authorizationStateWaitPassword
        "on_password" receives "($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.

    authorizationStateWaitEmailAddress, authorizationStateWaitEmailCode
        "on_email" and "on_email_code", same "($info, $submit)" shape and
        the same retry-on-error behaviour.

    authorizationStateWaitOtherDeviceConfirmation
        "on_qr" receives "($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.

    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.

    authorizationStateWaitPremiumPurchase
        Cannot be satisfied programmatically; login fails with an error.

    authorizationStateReady
        The login() callback succeeds.

    authorizationStateClosed
        Pending requests, in-flight sends and downloads are failed, close()
        callbacks run, then on_close fires.

    A login failure that arrives when no login() is pending is reported to
    the on_error handler instead (or warn, when none is set), and recorded:
    a login() called after the failure fails deferred with the same error
    rather than waiting for a state that never comes.

UPDATES
    Anything arriving without a pending @extra is an update -- with one
    exception: a reply whose @extra matches no pending and no recently

README  view on Meta::CPAN

    "code" and "message", so the delay exists only in the message text and
    must be parsed from it. Do not retry immediately, and never retry in a
    tight loop: that is the pattern that gets an account limited. Back off
    for at least the stated delay, with a timer rather than a blocking
    sleep. TDLib performs its own internal rate limiting for many operations
    -- it queues and paces requests on its own -- so a 429 that reaches you
    is a hard signal, not routine operation.

    The module deliberately implements no automatic retry: a wrong retry
    policy inside a binding hides the signal and can make limiting worse.
    The back-off policy belongs to the caller; see "Handling rate limits" in
    EV::Telegram::TDLib::Cookbook.

    One structured exception: a failed message send surfaces through
    updateMessageSendFailed, whose message carries a
    messageSendingStateFailed with a numeric "retry_after" field in seconds,
    next to "can_retry". The "send_message($chat_id, $text, %opt, $cb)"
    callback receives only the error object; watch updateMessageSendFailed
    via "on_update($cb), on_error($cb)" when you need the structured field.

  Internal failures
    Internal failures that own no request -- a TDLib frame that fails JSON
    decoding, a user callback that dies -- are reported to the "on_error"
    handler, or to warn when none is set. A dying callback is contained by
    the dispatch (wrapped in G_EVAL): it is reported, and the remaining
    updates in the same batch still run; the drain does not abort. The close
    chain is contained per callback as well: one dying callback during close
    cannot skip the remaining pending failures, the close() callbacks or
    on_close.

    The containment covers dispatch context. An exception in a send()
    timeout callback runs in an EV timer, not in the dispatch: it is not
    contained and propagates out of EV::run like any other EV watcher
    callback.

    Some errors are delivered synchronously, before the method returns: a
    parse_mode failure in "send_message($chat_id, $text, %opt, $cb)" or
    "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.

ENVIRONMENT
    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.

    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.

    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 eg/ and by xt/live_auth.t. The module itself takes
        credentials only as constructor options; see "new(%opt)".

EXAMPLES
    Runnable scripts live in eg/ (from the distribution root: "perl -Mblib
    eg/NAME.pl"; credentials come from the environment, see "ENVIRONMENT"):

    eg/01-login.pl
        user login with phone, SMS code and 2FA; creates the session
        database

    eg/02-bot-echo.pl
        bot login via token; echoes incoming text messages

    eg/03-list-chats.pl
        loads the chat list and prints id and title per chat

    eg/04-send-message.pl
        sends a markdown message and waits for real delivery

    eg/05-download-file.pl
        downloads a file id with progress percentage

    eg/06-raw-method.pl
        raw send()/execute() for methods the mixins do not wrap

    eg/07-gtk4-chat.pl
        a two-pane GTK4 chat window driven by EV rather than by gtk_main

    eg/08-tickit-chat.pl
        the same two panes in a terminal, on the same single loop

    eg/09-mcp-server.pl
        exposes Telegram as MCP tools over JSON-RPC on stdio

    eg/10-webapp-bot.pl
        offers a Mini App button and prints the data the page sends back

    EV::Telegram::TDLib::Cookbook has task-oriented recipes.

CAVEATS
    *   Not fork-safe. TDLib itself is not fork-safe, so every method croaks
        after fork. Do not fork with an open client. Forking before this
        process has ever made one is allowed, and is how a preforking worker
        pool should be built: the child inherits a pump that was never used.

    *   One reader thread per process, shared by all clients. It starts with
        the first client and runs until the process ends: closing every
        client releases the loop reference, so EV::run can return, but the
        reader itself is only joined by the END-block shutdown.

    *   The default EV loop only. Requests are delivered on EV_DEFAULT; a
        non-default loop cannot receive them. Destroying the default loop
        ("default_destroy" in EV) while a client is open is out of contract:
        the reader thread would keep signalling the freed loop through
        ev_async_send. Close every client first.

    *   Not safe with Perl ithreads. The pump initialises once per process,
        so on a threaded perl any "threads->create" after this module is
        loaded dies with "the pump cannot serve a second interpreter" --
        even for a thread that never touches Telegram. Fork instead, subject
        to the fork rules above.

        Pinned assumption: TDLib's receive/execute buffer is thread-local.
        The no-lock design -- the reader thread copies every td_receive
        result before anything else runs, and execute() needs no lock



( run in 1.142 second using v1.01-cache-2.11-cpan-007c89162af )