EV-Telegram-TDLib

 view release on metacpan or  search on metacpan

lib/EV/Telegram/TDLib.pm  view on Meta::CPAN

    my $cb = $self->{$handler};
    if (!$cb) {
        $self->_fail_login("$stype reached but no $handler callback was given");
        return;
    }
    # $submit has to refer to itself so a rejected credential can be asked
    # for again, and that cycle captures $self. Nothing collects it, so the
    # client would outlive its own close: keep a handle and clear it there.
    my $submit;
    $submit = sub {
        my ($value) = @_;
        # TDLib stays in the state on rejection: ask again, with the error
        $self->send($make_request->($value), $self->_auth_reply_cb($stype, sub {
            my ($err) = @_;
            $cb->($info, $submit, $err) if $err;
        }));
    };
    push @{ $self->{auth_submits} }, \$submit;
    $cb->($info, $submit);
}

sub _auth_parameters {
    my ($self) = @_;
    my $opt = $self->{opt};
    my $dbdir = $opt->{database_directory} // 'tdlib-db';
    $self->send({
        '@type' => 'setTdlibParameters',
        use_test_dc             => _json_bool($opt->{use_test_dc}),
        database_directory      => $dbdir,
        files_directory         => $opt->{files_directory} // $dbdir,
        database_encryption_key => $opt->{database_encryption_key} // '',
        use_file_database       => _json_bool($opt->{use_file_database} // 1),
        use_chat_info_database  => _json_bool($opt->{use_chat_info_database} // 1),
        use_message_database    => _json_bool($opt->{use_message_database} // 1),
        use_secret_chats        => _json_bool($opt->{use_secret_chats} // 1),
        api_id                  => 0 + ($opt->{api_id} // 0),
        api_hash                => $opt->{api_hash} // '',
        system_language_code    => $opt->{system_language_code} // 'en',
        device_model            => $opt->{device_model} // 'EV::Telegram::TDLib',
        system_version          => $opt->{system_version} // $^O,
        application_version     => $opt->{application_version} // $VERSION,
    }, $self->_auth_fail_cb('authorizationStateWaitTdlibParameters',
                            'setTdlibParameters'));
}

sub new {
    my ($class, %opt) = @_;
    my $self = bless {
        json      => Cpanel::JSON::XS->new->utf8->allow_nonref,
        seq       => 0,
        pending   => {},
        abandoned => {},
        cache     => {},
        opt       => \%opt,
        auto_auth => exists $opt{auto_auth} ? $opt{auto_auth} : 1,
        state     => 'created',
    }, $class;

    $self->{$_} = $opt{$_} for grep { /^on_/ } keys %opt;
    $self->{application_name} = EV::Telegram::TDLib::WebApps::_check_application_name(
        $opt{application_name} // 'tdesktop');
    $self->{client_id} = _create_client_id();
    $CLIENTS{ $self->{client_id} } = $self;
    _pump_ref();
    return $self;
}

sub on_update {
    my ($self, $cb) = @_;
    $self->{on_update} = $cb if $cb;
    return $self->{on_update};
}

sub on_error {
    my ($self, $cb) = @_;
    $self->{on_error} = $cb if $cb;
    return $self->{on_error};
}

sub _emit_error {
    my ($self, $message) = @_;
    if (my $cb = $self->{on_error}) {
        $cb->($message);
    } else {
        warn "EV::Telegram::TDLib: $message\n";
    }
}

# must never die: the C drain has no frame to unwind into, and would leak the batch
sub _drain_error {
    my ($client_id, $message) = @_;
    my $self = $CLIENTS{$client_id};
    return if $self && eval { $self->_emit_error($message); 1 };
    eval { warn "EV::Telegram::TDLib: $message\n" };
}

sub _dispatch_raw {
    my ($client_id, $json) = @_;
    my $self = $CLIENTS{$client_id} or return;
    my $obj  = eval { $self->{json}->decode($json) };
    if (!defined $obj) {
        $self->_emit_error("cannot decode TDLib response: $@");
        return;
    }
    # allow_nonref means a bare scalar or an array decodes fine; every real
    # TDLib payload is an object, and treating anything else as one dies
    if (ref $obj ne 'HASH') {
        $self->_emit_error('TDLib response is not an object: '
            . (ref $obj ? lc ref $obj : 'scalar'));
        return;
    }
    my $extra = delete $obj->{'@extra'};
    delete $obj->{'@client_id'};

    if (defined $extra) {
        my $p = delete $self->{pending}{$extra};
        if ($p) {
            $p->{timer}->stop if $p->{timer};
            my $is_err = ($obj->{'@type'} // '') eq 'error';
            $p->{cb}->($is_err ? (undef, $obj) : ($obj, undef));
            return;

lib/EV/Telegram/TDLib.pm  view on Meta::CPAN

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.

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

=item application_name

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

=item system_language_code, device_model, system_version, application_version

Client identification sent with setTdlibParameters. Defaults: C<en>,
C<EV::Telegram::TDLib>, C<$^O>, this distribution's version.

=item 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().

=item register

Hashref C<{ first_name =E<gt> ..., last_name =E<gt> ... }>. When set,
authorizationStateWaitRegistration is answered with registerUser;
without it the state fails login.

=item on_update, on_error, on_close, on_user, on_chat, on_message, on_connection_state

Update handlers; see L</UPDATES> and the mixin methods below.

=item on_code, on_password, on_email, on_email_code, on_qr

Authorization credential callbacks; see L</AUTHORIZATION>.

=back

=head1 CONVENTIONS

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

=head2 Cache readers against remote getters

L</chat($chat_id)> and L</user($user_id)> are B<synchronous cache reads>:
they take no callback, do no I/O, and return undef for something the
client has not seen. Everything else named after a noun -- C<folder>,
C<topic>, C<secret_chat>, C<supergroup>, C<basic_group>, C<message>,
C<file> -- is an B<asynchronous getter> that takes a callback and asks
TDLib.

The two cache readers predate the rest and are kept as they are because
renaming a method already published on CPAN would break working code.
When you want the server's answer for a chat rather than the cached one,
use L</fetch_chat($chat_id, $cb)>.

=head2 Booleans

A method that turns something on or off takes the flag as its last
positional argument and B<defaults to true>, so C<pin_topic($chat, $id)>
pins and C<pin_topic($chat, $id, 0)> unpins. That covers close_topic,
pin_topic, pin_chat, mark_unread, hide_general_topic, folder_tags,
pause_download, protect_content, the supergroup switches, and the
process_join_request pair.

Two older methods invert through an option instead
(C<< block_user($id, unblock => 1) >>, C<< react(..., remove => 1) >>),
and a few pairs are separate methods where the two directions differ in

lib/EV/Telegram/TDLib.pm  view on Meta::CPAN

the database.

=head3 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.

=head3 search_secret_messages($query, %opt, $cb)

Searches the local database, since secret messages exist nowhere else.
Options: C<chat_id> to scope to one chat, C<filter> (a
searchMessagesFilter name, with or without the prefix), C<offset>,
C<limit>.

=head3 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.

=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($query_id, \%result, $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



( run in 0.719 second using v1.01-cache-2.11-cpan-b16cb0d3907 )