EV-Telegram-TDLib

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

                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" },
        );

        # 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;

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

        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 MIME::Base64::decode_base64($old_key).

        Changing it later needs set_database_encryption_key(), and losing it
        loses the database.

    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. 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
        "use_chat_info_database => 0" does nothing unless
        "use_message_database => 0" too, and "use_file_database => 0" needs
        both.

    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

README  view on Meta::CPAN

    "on_update($cb), on_error($cb)" for updateQuickReplyShortcut and
    updateQuickReplyShortcuts, and for a shortcut's own messages
    updateQuickReplyShortcutMessages.

   add_quick_reply_message($shortcut_name, $text, %opt, $cb), edit_quick_reply_message($shortcut_id, $message_id, $text, %opt, $cb), delete_quick_reply($shortcut_id, $cb), delete_quick_reply_messages($shortcut_id, \@ids, $cb)
    Build and amend quick replies. Adding takes a shortcut name and creates
    the shortcut if it does not exist; the rest take its id.
    add_quick_reply_message takes "reply_to", the id of an earlier message
    in the same shortcut to reply to.

   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 "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 "wait => 'sent'".
    check_quick_reply_name is synchronous: it returns the result, and croaks
    if given a callback.

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() 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

    carries Mini App data. How far a die travels depends on which callback
    it is. The module guards each one it dispatches itself -- command
    handlers, ask answers, callback data routes, on_callback_query,
    on_inline_query and the file watchers -- so a die there is reported and
    the rest of the update still runs; you cannot use it to stop on_message
    from seeing the message too. A die in one of the plain handlers
    (on_message, on_user, on_chat and the rest) skips what is left of that
    update, on_update included. Either way the next update is unaffected.
    The two read differently in the report: "a callback died" for a guarded
    one, "dispatch died" for the rest.

    The practical consequence is that 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
    send() timeout, an ask timing out, a deferred failure on a closed client
    -- is guarded the same way, so a die there reaches "on_error" rather
    than only EV's own stderr.

    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/. Once the module is installed, run one from
    anywhere with "perl eg/NAME.pl"; from an unpacked distribution "perl
    -Mblib eg/NAME.pl" works after "make", and not before it, since there is
    no blib until then. 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

    eg/11-command-bot.pl
        command routing, inline button routing and ask()

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

CAVEATS
    *   Not fork-safe. TDLib itself is not fork-safe, so every method that
        reaches it croaks after fork: new, send, call, close, execute and
        every convenience method built on them. What does not croak is
        everything answered from this process's own memory -- "chat($id)",
        "user($id)", option() and the "on_*" accessors -- so a forked child
        can read a plausible-looking cache and only fails when it tries to
        do something with it. 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.

README  view on Meta::CPAN


    *   Not designed for subclassing. Hold a client in your own object
        instead: the class is assembled from mixins, keeps its state in the
        object hash, and calls many of its own methods internally, so a
        subclass method of the same name replaces part of the machinery. The
        methods documented here are the interface; anything else is internal
        and may change.

    *   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. "close($cb)" is not optional; dropping
        your last Perl reference does not close anything. See
        "AUTHORIZATION" for the Closed state that ends the lifecycle.

    *   An END block closes leftover clients and pumps the loop for a
        bounded interval (three seconds, or "EV_TDLIB_SHUTDOWN_TIMEOUT") so
        TDLib flushes its database, then joins the reader thread. 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().

    *   A callback that dies is contained and reported through on_error; the
        drain continues. See "ERROR HANDLING".

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

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

REQUIREMENTS
    *   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.

    *   EV 4.11 or later.

    *   Cpanel::JSON::XS 4.00 or later.

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

LIMITATIONS
    Deliberately out of scope:

    *   No Bot API (HTTP) client; this binding speaks tdjson only.

    *   No voice or video calls.

    *   No group calls or video chats.

    *   Around 418 of TDLib's roughly one thousand methods have a
        hand-written wrapper. For the rest, call() validates argument names
        against a shipped schema catalogue, and send() and
        "execute(\%request)" are the escape hatch (see "ESCAPE HATCH").

    *   The Business plane is unverified against a live server: exercising
        it needs a Telegram Business subscription, which the author does not
        have. Its methods are covered by offline tests only. Note that the
        requirement differs across the plane. The bot-side calls
        (business_connection, send_business_message, read_business_message,
        delete_business_messages, business_account_star_amount and the
        set_business_account_* setters) are documented "for bots only" and
        need no subscription on the bot itself, since the bot acts on a
        connected business account. The TL marks eight as needing the
        current account to hold a Business subscription: set_away_message,
        set_greeting_message, send_quick_reply, set_business_location,
        set_opening_hours, set_start_page, create_business_chat_link and
        edit_business_chat_link. business_features, business_chat_link_info,
        check_quick_reply_name, connected_bot and load_quick_replies are
        callable by anyone.

    *   Gift auctions, gift crafting and live-story streaming are not
        wrapped.

    *   No log message callback (TDLib's setLogMessageCallback is not
        bound); TDLIB_LOG_VERBOSITY (see "ENVIRONMENT") is the only log
        control.

    *   Linux and macOS are CI-tested. The BSDs are unsupported:
        Alien::TDLib ships no prebuilt TDLib for them, so a build there



( run in 0.989 second using v1.01-cache-2.11-cpan-3f962061493 )