EV-Telegram-TDLib

 view release on metacpan or  search on metacpan

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

package EV::Telegram::TDLib::Messages;

use strict;
use warnings;
use Carp qw(croak);
use EV;
use Encode ();
use overload ();

our $VERSION = '0.04';

=head1 NAME

EV::Telegram::TDLib::Messages - message methods for EV::Telegram::TDLib

=head1 DESCRIPTION

One of the mixins L<EV::Telegram::TDLib> inherits from. It has no
interface of its own and is not meant to be used directly: loading the
main module loads this one, and its methods are called on a client.

They are documented together with the rest of the API, under
L<EV::Telegram::TDLib/"Messages mixin">.

=cut

sub CLONE_SKIP { 1 }

my %PARSE_MODE = (
    markdown => { '@type' => 'textParseModeMarkdown', version => 2 },
    html     => { '@type' => 'textParseModeHTML' },
);

our %UPDATES = (
    updateNewMessage            => \&update_new_message,
    updateMessageSendSucceeded  => \&update_send_succeeded,
    updateMessageSendFailed     => \&update_send_failed,
    updateDeleteMessages        => \&update_delete_messages,
);

sub sending { $_[0]{cache}{sending} ||= {} }


sub update_new_message {
    my ($self, $obj) = @_;
    my $msg = $obj->{message} or return;
    # an answer to a pending ask is not offered to the command router: a reply
    # that happens to start with a slash would otherwise run two code paths
    my $answered = $self->answer_ask($msg);
    $self->route_command($msg) unless $answered;
    if (my $cb = $self->{on_message}) { $cb->($msg) }
    # Mini App data arrives as a message content, not as its own update
    my $c = $msg->{content};
    return unless ref $c eq 'HASH'
        && ($c->{'@type'} // '') eq 'messageWebAppDataReceived';
    if (my $cb = $self->{on_web_app_data}) {
        $cb->($msg, $c->{data}, $c->{button_text});
    }
}

# A yet-unsent message id is assigned from a per-chat counter, so the Nth send
# to one chat and the Nth to another share it. Keying on the id alone dropped
# one callback and handed the other the wrong chat's message.
sub sending_key {
    my ($chat_id, $message_id) = @_;
    return unless defined $chat_id && defined $message_id;
    return (0 + $chat_id) . ':' . (0 + $message_id);
}

sub update_send_succeeded {
    my ($self, $obj) = @_;
    my $key = sending_key($obj->{message}{chat_id}, $obj->{old_message_id})
        or return;
    my $cb = delete $self->sending->{$key} or return;
    $cb->($obj->{message}, undef);
}

sub update_send_failed {
    my ($self, $obj) = @_;
    my $key = sending_key($obj->{message}{chat_id}, $obj->{old_message_id})
        or return;
    my $cb = delete $self->sending->{$key} or return;
    $cb->(undef, $obj->{error} // { '@type' => 'error', code => -1,
                                    message => 'message send failed' });
}

# The schema says it outright: a message being sent that is irrecoverably
# deleted yields this instead of updateMessageSendFailed. Without it a caller
# waiting for delivery is owed an answer that never comes, and only close()
# ever resolves it.
sub update_delete_messages {
    my ($self, $obj) = @_;
    # a cache eviction is not a deletion: the schema says such a message can
    # be retrieved again, so the send is still outstanding
    return if $obj->{from_cache};
    my $sending = $self->sending;
    return unless %$sending;
    for my $id (@{ $obj->{message_ids} || [] }) {
        my $key = sending_key($obj->{chat_id}, $id) or next;
        my $cb = delete $sending->{$key} or next;
        # guarded: one update can carry many ids, and a die in an earlier
        # callback would strand the rest
        $self->guarded($cb, undef,
                        { '@type' => 'error', code => -1,
                          message => 'message deleted before it was sent' });
    }
}

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

# Stringified here rather than at each caller: an SV that has ever been used
# as a number carries IOK, the encoder then emits a JSON Number, and TDLib
# refuses a Number in a text field. A bare numeric comparison is enough to
# set it, so the same string can send once and fail afterwards.
sub format_text {
    my ($self, $text, $parse_mode) = @_;
    # translate() and parse_markdown() answer with a formattedText, and
    # stringifying one sent HASH(0x...) to the chat: already-formatted text
    # passes straight through, and parse_mode has nothing left to do. An
    # object that stringifies, a URI say, is text like any other
    if (ref $text && !overload::Method($text, '""')) {
        return $text if ref $text eq 'HASH'
                     && ($text->{'@type'} // '') eq 'formattedText';
        croak 'text must be a string or a formattedText';
    }
    $text = defined $text ? "$text" : '';
    return { '@type' => 'formattedText', text => $text, entities => [] }
        unless $parse_mode;
    my $mode = $PARSE_MODE{$parse_mode}
        or croak "unknown parse_mode '$parse_mode'";
    return $self->execute({
        '@type' => 'parseTextEntities', text => $text, parse_mode => $mode,
    });
}

sub input_text {
    my ($self, $text, $opt, $cb) = @_;
    my $formatted = $self->format_text($text, $opt->{parse_mode});
    if (($formatted->{'@type'} // '') eq 'error') {
        $cb->(undef, $formatted);
        return;
    }
    my %content = ('@type' => 'inputMessageText', text => $formatted);
    $content{link_preview_options} = { '@type' => 'linkPreviewOptions',
                                       is_disabled => json_bool(1) }
        if $opt->{disable_preview};
    return \%content;



( run in 0.495 second using v1.01-cache-2.11-cpan-57ff5998125 )