EV-Telegram-TDLib

 view release on metacpan or  search on metacpan

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

package EV::Telegram::TDLib::Chats;

use strict;
use warnings;
use Carp qw(croak);

our $VERSION = '0.04';

=head1 NAME

EV::Telegram::TDLib::Chats - chat 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/"Chats mixin">.

=cut

sub CLONE_SKIP { 1 }


# updates whose payload fields overwrite the same-named cached chat fields;
# two of them also carry the chat's complete position set, which replaces it
my %CHAT_FIELDS = (
    updateChatTitle                      => ['title'],
    updateChatPhoto                      => ['photo'],
    updateChatPermissions                => ['permissions'],
    updateChatLastMessage                => ['last_message'],
    updateChatDraftMessage               => ['draft_message'],
    updateChatReadInbox                  => ['last_read_inbox_message_id', 'unread_count'],
    updateChatReadOutbox                 => ['last_read_outbox_message_id'],
    updateChatUnreadMentionCount         => ['unread_mention_count'],
    updateChatNotificationSettings       => ['notification_settings'],
    updateChatIsMarkedAsUnread           => ['is_marked_as_unread'],
    updateChatBlockList                  => ['block_list'],
    updateChatHasScheduledMessages       => ['has_scheduled_messages'],
    updateChatDefaultDisableNotification => ['default_disable_notification'],
    updateChatActionBar                  => ['action_bar'],
    updateChatTheme                      => ['theme'],
    updateChatAvailableReactions         => ['available_reactions'],
    updateChatPendingJoinRequests        => ['pending_join_requests'],
    updateChatMessageAutoDeleteTime      => ['message_auto_delete_time'],
);

our %UPDATES = (
    updateNewChatJoinRequest    => \&update_join_request,
    updateNewChat      => \&update_new_chat,
    updateChatPosition => \&update_chat_position,
    updateChatAddedToList     => \&update_chat_list,
    updateChatRemovedFromList => \&update_chat_list,
    (map { $_ => \&update_chat_fields } keys %CHAT_FIELDS),
);

sub _chats { $_[0]{cache}{chats} ||= {} }

sub update_new_chat {
    my ($self, $obj) = @_;
    my $chat = $obj->{chat} or return;
    $self->_chats->{ $chat->{id} } = $chat;
    if (my $cb = $self->{on_chat}) { $cb->($chat) }
}

# a chat list is its type plus, for a folder, the folder's id: every folder is
# a chatListFolder, so keying on the type alone let folders overwrite each other
sub position_key {
    my ($list) = @_;
    my $type = $list->{'@type'} // '';
    return $type eq 'chatListFolder'
        ? "$type:" . ($list->{chat_folder_id} // '') : $type;
}

# updateChatPosition carries the one position that changed; order 0 means the
# chat left that list
sub merge_position {
    my ($chat, $pos) = @_;
    my $key = position_key($pos->{list});
    my $positions = $chat->{positions} ||= [];
    @$positions = grep { position_key($_->{list}) ne $key } @$positions;
    push @$positions, $pos unless ($pos->{order} // 0) eq '0';
}

# payload fields the schema marks nullable: TDLib omits a null object
# field from the JSON entirely, so for these an absent key means
# "now null", not "unchanged"
my %NULLABLE = map { $_ => 1 } qw(
    last_message draft_message photo action_bar theme block_list
    pending_join_requests
);

sub update_chat_fields {
    my ($self, $obj) = @_;
    my $fields = $CHAT_FIELDS{ $obj->{'@type'} } or return;
    return unless defined $obj->{chat_id};
    my $chat = $self->_chats->{ $obj->{chat_id} } or return;
    for my $f (@$fields) {
        $chat->{$f} = $obj->{$f} if $NULLABLE{$f} || exists $obj->{$f};
    }
    # updateChatLastMessage and updateChatDraftMessage carry the chat's
    # complete set of positions and are sent instead of updateChatPosition,
    # so a list the chat has left is signalled only by its absence: replace
    $chat->{positions} = [ grep { ($_->{order} // 0) ne '0' } @{ $obj->{positions} } ]
        if ref $obj->{positions} eq 'ARRAY';
}

sub update_chat_position {
    my ($self, $obj) = @_;
    return unless defined $obj->{chat_id};
    my $chat = $self->_chats->{ $obj->{chat_id} } or return;
    merge_position($chat, $obj->{position}) if $obj->{position};
}

# chat_lists is membership, which the schema keeps apart from positions:
# updateNewChat carries it as it stood, usually empty, and these two keep it
# current
sub update_chat_list {
    my ($self, $obj) = @_;
    return unless defined $obj->{chat_id} && $obj->{chat_list};
    my $chat = $self->_chats->{ $obj->{chat_id} } or return;
    my $key = position_key($obj->{chat_list});
    my $lists = $chat->{chat_lists} ||= [];
    @$lists = grep { position_key($_) ne $key } @$lists;
    push @$lists, $obj->{chat_list} if $obj->{'@type'} eq 'updateChatAddedToList';
}

sub chat {
    my ($self, $id) = @_;
    # documented to answer undef for a chat this client has not seen, and
    # `my $c = $td->chat($id) or return` is the idiomatic call, so an undef id
    # answers the same way rather than croaking. Returning early only to keep
    # the lookup from warning from inside the module.
    return undef unless defined $id;
    # keyed as TDLib sends ids, so a padded or signed one -- accepted
    # everywhere else -- is normalised to find the same entry
    $id = 0 + $id if !ref $id && $id =~ /\A\s*[+-]?[0-9]+\s*\z/;
    return $self->_chats->{$id};
}

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

sub load_chats {
    my ($self, @args) = @_;
    my $cb = ref $args[-1] eq 'CODE' ? pop @args : sub {};
    my ($limit, @rest) = @args;
    my %opt = opts(@rest);
    need('limit', $limit);
    # loadChats is what makes TDLib fetch more of a list, so without this the
    # archive and the folders could never be paged past whatever was cached
    $self->send({ '@type' => 'loadChats', limit => 0 + $limit,
                  chat_list => chat_list($opt{list}) }, sub {
        my ($res, $err) = @_;
        # TDLib answers 404 once the chat list is exhausted; that is the
        # normal end of a load, not a failure
        $err = undef if $err && ($err->{code} // 0) == 404;
        $cb->($res, $err);
    });
    return;
}

sub chat_by_username {
    my ($self, @args) = @_;
    my $cb = ref $args[-1] eq 'CODE' ? pop @args : sub {};
    no_extra('chat_by_username', 1, \@args);
    my ($name) = @args;
    need('username', $name);
    $name = plain_text('a username', $name);
    $name =~ s/^\@//;
    $self->send({ '@type' => 'searchPublicChat', username => $name }, sub {
        my ($chat, $err) = @_;
        $self->_chats->{ $chat->{id} } = $chat if $chat;
        $cb->($chat, $err);
    });

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

    my %opt = opts(@rest);
    $self->send({
        '@type'      => 'getBlockedMessageSenders',
        block_list   => { '@type' => ($opt{stories} ? 'blockListStories'
                                                    : 'blockListMain') },
        offset       => num('offset', $opt{offset} // 0),
        limit        => num('limit', $opt{limit} // 100),
    }, $cb);
    return;
}

sub join_requests {
    my ($self, @args) = @_;
    my $cb = ref $args[-1] eq 'CODE' ? pop @args : sub {};
    my ($chat_id, @rest) = @args;
    my %opt = opts(@rest);
    need('chat_id', $chat_id);
    $self->send({
        '@type'          => 'getChatJoinRequests',
        chat_id          => 0 + $chat_id,
        invite_link      => plain_text('an invite link', $opt{link}),
        query            => plain_text('a query', $opt{query}),
        (defined $opt{offset_request}
            ? (offset_request => $opt{offset_request}) : ()),
        limit            => num('limit', $opt{limit} // 100),
    }, $cb);
    return;
}

sub process_join_request {
    my ($self, @args) = @_;
    my $cb = ref $args[-1] eq 'CODE' ? pop @args : sub {};
    my ($chat_id, $user_id, $approve, @rest) = @args;
    no_opts('process_join_request', @rest);
    need('chat_id, user_id', $chat_id, $user_id);
    $self->send({ '@type' => 'processChatJoinRequest', chat_id => 0 + $chat_id,
                  user_id => 0 + $user_id,
                  approve => json_bool(defined $approve ? $approve : 1) }, $cb);
    return;
}

# approves or declines everyone at once, optionally only those from one link
sub process_join_requests {
    my ($self, @args) = @_;
    my $cb = ref $args[-1] eq 'CODE' ? pop @args : sub {};
    my ($chat_id, $approve, @rest) = @args;
    my %opt = opts(@rest);
    need('chat_id', $chat_id);
    $self->send({ '@type' => 'processChatJoinRequests', chat_id => 0 + $chat_id,
                  invite_link => plain_text('an invite link', $opt{link}),
                  approve => json_bool(defined $approve ? $approve : 1) }, $cb);
    return;
}

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

sub update_join_request {
    my ($self, $obj) = @_;
    my $cb = $self->{on_join_request} or return;
    my $r = $obj->{request} || {};
    $cb->({
        chat_id       => $obj->{chat_id},
        user_id       => $r->{user_id},
        date          => $r->{date},
        bio           => $r->{bio},
        invite_link   => $obj->{invite_link},
        user_chat_id  => $obj->{user_chat_id},
    });
}

# A supergroup's chat id is -1000000000000 minus its supergroup id, and the
# supergroup_* methods want the latter. Everything else in this module takes a
# chat id, so accept either and convert rather than let the mismatch surface as
# an unhelpful server error.
sub supergroup_id {
    my ($id) = @_;
    return 0 + $id unless $id < 0;
    return -1000000000000 - $id;
}

# A basic group's chat id is its group id negated, with no offset: DialogId
# builds one as -chat_id and reads it back as ChatId(-id). Same reasoning as
# supergroup_id -- take either rather than send a negative id TDLib rejects.
sub basic_group_id {
    my ($id) = @_;
    return $id < 0 ? -$id : 0 + $id;
}

sub add_members {
    my ($self, @args) = @_;
    my $cb = ref $args[-1] eq 'CODE' ? pop @args : sub {};
    no_extra('add_members', 2, \@args);
    my ($chat_id, $user_ids) = @args;
    need('chat_id, user_ids', $chat_id, $user_ids);
    croak 'add_members needs an arrayref of user ids' unless ref $user_ids eq 'ARRAY';
    $self->send({ '@type' => 'addChatMembers', chat_id => 0 + $chat_id,
                  user_ids => num_list('user_ids', $user_ids) }, $cb);
    return;
}

# revoke also deletes what they already sent, which set_member_status does
# not; TDLib passes it on for a basic group only
sub ban_member {
    my ($self, @args) = @_;
    my $cb = ref $args[-1] eq 'CODE' ? pop @args : sub {};
    my ($chat_id, $user_id, @rest) = @args;
    my %opt = opts(@rest);
    need('chat_id, user_id', $chat_id, $user_id);
    $self->send({ '@type' => 'banChatMember', chat_id => 0 + $chat_id,
                  member_id => sender($user_id),
                  banned_until_date => unix_time('until', $opt{until} // 0),
                  revoke_messages => json_bool($opt{revoke}) }, $cb);
    return;
}

sub transfer_ownership {
    my ($self, @args) = @_;



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