mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-26 19:33:45 +03:00
734 lines
26 KiB
Python
734 lines
26 KiB
Python
import inspect
|
|
import itertools
|
|
import typing
|
|
import warnings
|
|
|
|
from .. import errors, hints, _tl
|
|
from .._misc import helpers, utils, requestiter
|
|
from ..types import _custom
|
|
|
|
_MAX_CHUNK_SIZE = 100
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from .telegramclient import TelegramClient
|
|
|
|
|
|
class _MessagesIter(requestiter.RequestIter):
|
|
"""
|
|
Common factor for all requests that need to iterate over messages.
|
|
"""
|
|
async def _init(
|
|
self, entity, offset_id, min_id, max_id,
|
|
from_user, offset_date, add_offset, filter, search, reply_to,
|
|
scheduled
|
|
):
|
|
# Note that entity being `None` will perform a global search.
|
|
if entity:
|
|
self.entity = await self.client.get_input_entity(entity)
|
|
else:
|
|
self.entity = None
|
|
if self.reverse:
|
|
raise ValueError('Cannot reverse global search')
|
|
|
|
# Telegram doesn't like min_id/max_id. If these IDs are low enough
|
|
# (starting from last_id - 100), the request will return nothing.
|
|
#
|
|
# We can emulate their behaviour locally by setting offset = max_id
|
|
# and simply stopping once we hit a message with ID <= min_id.
|
|
if self.reverse:
|
|
offset_id = max(offset_id, min_id)
|
|
if offset_id and max_id:
|
|
if max_id - offset_id <= 1:
|
|
raise StopAsyncIteration
|
|
|
|
if not max_id:
|
|
max_id = float('inf')
|
|
else:
|
|
offset_id = max(offset_id, max_id)
|
|
if offset_id and min_id:
|
|
if offset_id - min_id <= 1:
|
|
raise StopAsyncIteration
|
|
|
|
if self.reverse:
|
|
if offset_id:
|
|
offset_id += 1
|
|
elif not offset_date:
|
|
# offset_id has priority over offset_date, so don't
|
|
# set offset_id to 1 if we want to offset by date.
|
|
offset_id = 1
|
|
|
|
if from_user:
|
|
from_user = await self.client.get_input_entity(from_user)
|
|
self.from_id = await self.client.get_peer_id(from_user)
|
|
else:
|
|
self.from_id = None
|
|
|
|
# `messages.searchGlobal` only works with text `search` or `filter` queries.
|
|
# If we want to perform global a search with `from_user` we have to perform
|
|
# a normal `messages.search`, *but* we can make the entity be `inputPeerEmpty`.
|
|
if not self.entity and from_user:
|
|
self.entity = _tl.InputPeerEmpty()
|
|
|
|
if filter is None:
|
|
filter = _tl.InputMessagesFilterEmpty()
|
|
else:
|
|
filter = filter() if isinstance(filter, type) else filter
|
|
|
|
if not self.entity:
|
|
self.request = _tl.fn.messages.SearchGlobal(
|
|
q=search or '',
|
|
filter=filter,
|
|
min_date=None,
|
|
max_date=offset_date,
|
|
offset_rate=0,
|
|
offset_peer=_tl.InputPeerEmpty(),
|
|
offset_id=offset_id,
|
|
limit=1
|
|
)
|
|
elif scheduled:
|
|
self.request = _tl.fn.messages.GetScheduledHistory(
|
|
peer=entity,
|
|
hash=0
|
|
)
|
|
elif reply_to is not None:
|
|
self.request = _tl.fn.messages.GetReplies(
|
|
peer=self.entity,
|
|
msg_id=reply_to,
|
|
offset_id=offset_id,
|
|
offset_date=offset_date,
|
|
add_offset=add_offset,
|
|
limit=1,
|
|
max_id=0,
|
|
min_id=0,
|
|
hash=0
|
|
)
|
|
elif search is not None or not isinstance(filter, _tl.InputMessagesFilterEmpty) or from_user:
|
|
# Telegram completely ignores `from_id` in private chats
|
|
ty = helpers._entity_type(self.entity)
|
|
if ty == helpers._EntityType.USER:
|
|
# Don't bother sending `from_user` (it's ignored anyway),
|
|
# but keep `from_id` defined above to check it locally.
|
|
from_user = None
|
|
else:
|
|
# Do send `from_user` to do the filtering server-side,
|
|
# and set `from_id` to None to avoid checking it locally.
|
|
self.from_id = None
|
|
|
|
self.request = _tl.fn.messages.Search(
|
|
peer=self.entity,
|
|
q=search or '',
|
|
filter=filter,
|
|
min_date=None,
|
|
max_date=offset_date,
|
|
offset_id=offset_id,
|
|
add_offset=add_offset,
|
|
limit=0, # Search actually returns 0 items if we ask it to
|
|
max_id=0,
|
|
min_id=0,
|
|
hash=0,
|
|
from_id=from_user
|
|
)
|
|
|
|
# Workaround issue #1124 until a better solution is found.
|
|
# Telegram seemingly ignores `max_date` if `filter` (and
|
|
# nothing else) is specified, so we have to rely on doing
|
|
# a first request to offset from the ID instead.
|
|
#
|
|
# Even better, using `filter` and `from_id` seems to always
|
|
# trigger `RPC_CALL_FAIL` which is "internal issues"...
|
|
if not isinstance(filter, _tl.InputMessagesFilterEmpty) \
|
|
and offset_date and not search and not offset_id:
|
|
async for m in self.client.iter_messages(
|
|
self.entity, 1, offset_date=offset_date):
|
|
self.request.offset_id = m.id + 1
|
|
else:
|
|
self.request = _tl.fn.messages.GetHistory(
|
|
peer=self.entity,
|
|
limit=1,
|
|
offset_date=offset_date,
|
|
offset_id=offset_id,
|
|
min_id=0,
|
|
max_id=0,
|
|
add_offset=add_offset,
|
|
hash=0
|
|
)
|
|
|
|
if self.limit <= 0:
|
|
# No messages, but we still need to know the total message count
|
|
result = await self.client(self.request)
|
|
if isinstance(result, _tl.messages.MessagesNotModified):
|
|
self.total = result.count
|
|
else:
|
|
self.total = getattr(result, 'count', len(result.messages))
|
|
raise StopAsyncIteration
|
|
|
|
if self.wait_time is None:
|
|
self.wait_time = 1 if self.limit > 3000 else 0
|
|
|
|
# When going in reverse we need an offset of `-limit`, but we
|
|
# also want to respect what the user passed, so add them together.
|
|
if self.reverse:
|
|
self.request.add_offset -= _MAX_CHUNK_SIZE
|
|
|
|
self.add_offset = add_offset
|
|
self.max_id = max_id
|
|
self.min_id = min_id
|
|
self.last_id = 0 if self.reverse else float('inf')
|
|
|
|
async def _load_next_chunk(self):
|
|
self.request.limit = min(self.left, _MAX_CHUNK_SIZE)
|
|
if self.reverse and self.request.limit != _MAX_CHUNK_SIZE:
|
|
# Remember that we need -limit when going in reverse
|
|
self.request.add_offset = self.add_offset - self.request.limit
|
|
|
|
r = await self.client(self.request)
|
|
self.total = getattr(r, 'count', len(r.messages))
|
|
|
|
entities = {utils.get_peer_id(x): x
|
|
for x in itertools.chain(r.users, r.chats)}
|
|
|
|
messages = reversed(r.messages) if self.reverse else r.messages
|
|
for message in messages:
|
|
if (isinstance(message, _tl.MessageEmpty)
|
|
or self.from_id and message.sender_id != self.from_id):
|
|
continue
|
|
|
|
if not self._message_in_range(message):
|
|
return True
|
|
|
|
# There has been reports that on bad connections this method
|
|
# was returning duplicated IDs sometimes. Using ``last_id``
|
|
# is an attempt to avoid these duplicates, since the message
|
|
# IDs are returned in descending order (or asc if reverse).
|
|
self.last_id = message.id
|
|
self.buffer.append(_custom.Message._new(self.client, message, entities, self.entity))
|
|
|
|
if len(r.messages) < self.request.limit:
|
|
return True
|
|
|
|
# Get the last message that's not empty (in some rare cases
|
|
# it can happen that the last message is :tl:`MessageEmpty`)
|
|
if self.buffer:
|
|
self._update_offset(self.buffer[-1], r)
|
|
else:
|
|
# There are some cases where all the messages we get start
|
|
# being empty. This can happen on migrated mega-groups if
|
|
# the history was cleared, and we're using search. Telegram
|
|
# acts incredibly weird sometimes. Messages are returned but
|
|
# only "empty", not their contents. If this is the case we
|
|
# should just give up since there won't be any new Message.
|
|
return True
|
|
|
|
def _message_in_range(self, message):
|
|
"""
|
|
Determine whether the given message is in the range or
|
|
it should be ignored (and avoid loading more chunks).
|
|
"""
|
|
# No entity means message IDs between chats may vary
|
|
if self.entity:
|
|
if self.reverse:
|
|
if message.id <= self.last_id or message.id >= self.max_id:
|
|
return False
|
|
else:
|
|
if message.id >= self.last_id or message.id <= self.min_id:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _update_offset(self, last_message, response):
|
|
"""
|
|
After making the request, update its offset with the last message.
|
|
"""
|
|
self.request.offset_id = last_message.id
|
|
if self.reverse:
|
|
# We want to skip the one we already have
|
|
self.request.offset_id += 1
|
|
|
|
if isinstance(self.request, _tl.fn.messages.Search):
|
|
# Unlike getHistory and searchGlobal that use *offset* date,
|
|
# this is *max* date. This means that doing a search in reverse
|
|
# will break it. Since it's not really needed once we're going
|
|
# (only for the first request), it's safe to just clear it off.
|
|
self.request.max_date = None
|
|
else:
|
|
# getHistory, searchGlobal and getReplies call it offset_date
|
|
self.request.offset_date = last_message.date
|
|
|
|
if isinstance(self.request, _tl.fn.messages.SearchGlobal):
|
|
if last_message.input_chat:
|
|
self.request.offset_peer = last_message.input_chat
|
|
else:
|
|
self.request.offset_peer = _tl.InputPeerEmpty()
|
|
|
|
self.request.offset_rate = getattr(response, 'next_rate', 0)
|
|
|
|
|
|
class _IDsIter(requestiter.RequestIter):
|
|
async def _init(self, entity, ids):
|
|
self.total = len(ids)
|
|
self._ids = list(reversed(ids)) if self.reverse else ids
|
|
self._offset = 0
|
|
self._entity = (await self.client.get_input_entity(entity)) if entity else None
|
|
self._ty = helpers._entity_type(self._entity) if self._entity else None
|
|
|
|
# 30s flood wait every 300 messages (3 requests of 100 each, 30 of 10, etc.)
|
|
if self.wait_time is None:
|
|
self.wait_time = 10 if self.limit > 300 else 0
|
|
|
|
async def _load_next_chunk(self):
|
|
ids = self._ids[self._offset:self._offset + _MAX_CHUNK_SIZE]
|
|
if not ids:
|
|
raise StopAsyncIteration
|
|
|
|
self._offset += _MAX_CHUNK_SIZE
|
|
|
|
from_id = None # By default, no need to validate from_id
|
|
if self._ty == helpers._EntityType.CHANNEL:
|
|
try:
|
|
r = await self.client(
|
|
_tl.fn.channels.GetMessages(self._entity, ids))
|
|
except errors.MessageIdsEmptyError:
|
|
# All IDs were invalid, use a dummy result
|
|
r = _tl.messages.MessagesNotModified(len(ids))
|
|
else:
|
|
r = await self.client(_tl.fn.messages.GetMessages(ids))
|
|
if self._entity:
|
|
from_id = await _get_peer(self.client, self._entity)
|
|
|
|
if isinstance(r, _tl.messages.MessagesNotModified):
|
|
self.buffer.extend(None for _ in ids)
|
|
return
|
|
|
|
entities = {utils.get_peer_id(x): x
|
|
for x in itertools.chain(r.users, r.chats)}
|
|
|
|
# Telegram seems to return the messages in the order in which
|
|
# we asked them for, so we don't need to check it ourselves,
|
|
# unless some messages were invalid in which case Telegram
|
|
# may decide to not send them at all.
|
|
#
|
|
# The passed message IDs may not belong to the desired entity
|
|
# since the user can enter arbitrary numbers which can belong to
|
|
# arbitrary chats. Validate these unless ``from_id is None``.
|
|
for message in r.messages:
|
|
if isinstance(message, _tl.MessageEmpty) or (
|
|
from_id and message.peer_id != from_id):
|
|
self.buffer.append(None)
|
|
else:
|
|
self.buffer.append(_custom.Message._new(self.client, message, entities, self._entity))
|
|
|
|
|
|
async def _get_peer(self: 'TelegramClient', input_peer: 'hints.EntityLike'):
|
|
try:
|
|
return utils.get_peer(input_peer)
|
|
except TypeError:
|
|
# Can only be self by now
|
|
return _tl.PeerUser(await self.get_peer_id(input_peer))
|
|
|
|
|
|
def get_messages(
|
|
self: 'TelegramClient',
|
|
entity: 'hints.EntityLike',
|
|
limit: float = None,
|
|
*,
|
|
offset_date: 'hints.DateLike' = None,
|
|
offset_id: int = 0,
|
|
max_id: int = 0,
|
|
min_id: int = 0,
|
|
add_offset: int = 0,
|
|
search: str = None,
|
|
filter: 'typing.Union[_tl.TypeMessagesFilter, typing.Type[_tl.TypeMessagesFilter]]' = None,
|
|
from_user: 'hints.EntityLike' = None,
|
|
wait_time: float = None,
|
|
ids: 'typing.Union[int, typing.Sequence[int]]' = None,
|
|
reverse: bool = False,
|
|
reply_to: int = None,
|
|
scheduled: bool = False
|
|
) -> 'typing.Union[_MessagesIter, _IDsIter]':
|
|
if ids is not None:
|
|
if not utils.is_list_like(ids):
|
|
ids = [ids]
|
|
|
|
return _IDsIter(
|
|
client=self,
|
|
reverse=reverse,
|
|
wait_time=wait_time,
|
|
limit=len(ids),
|
|
entity=entity,
|
|
ids=ids
|
|
)
|
|
|
|
return _MessagesIter(
|
|
client=self,
|
|
reverse=reverse,
|
|
wait_time=wait_time,
|
|
limit=limit,
|
|
entity=entity,
|
|
offset_id=offset_id,
|
|
min_id=min_id,
|
|
max_id=max_id,
|
|
from_user=from_user,
|
|
offset_date=offset_date,
|
|
add_offset=add_offset,
|
|
filter=filter,
|
|
search=search,
|
|
reply_to=reply_to,
|
|
scheduled=scheduled
|
|
)
|
|
|
|
|
|
async def _get_comment_data(
|
|
self: 'TelegramClient',
|
|
entity: 'hints.EntityLike',
|
|
message: 'typing.Union[int, _tl.Message]'
|
|
):
|
|
r = await self(_tl.fn.messages.GetDiscussionMessage(
|
|
peer=entity,
|
|
msg_id=utils.get_message_id(message)
|
|
))
|
|
m = r.messages[0]
|
|
chat = next(c for c in r.chats if c.id == m.peer_id.channel_id)
|
|
return utils.get_input_peer(chat), m.id
|
|
|
|
async def send_message(
|
|
self: 'TelegramClient',
|
|
entity: 'hints.EntityLike',
|
|
message: 'hints.MessageLike' = '',
|
|
*,
|
|
reply_to: 'typing.Union[int, _tl.Message]' = None,
|
|
attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None,
|
|
parse_mode: typing.Optional[str] = (),
|
|
formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None,
|
|
link_preview: bool = True,
|
|
file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None,
|
|
thumb: 'hints.FileLike' = None,
|
|
force_document: bool = False,
|
|
clear_draft: bool = False,
|
|
buttons: 'hints.MarkupLike' = None,
|
|
silent: bool = None,
|
|
background: bool = None,
|
|
supports_streaming: bool = False,
|
|
schedule: 'hints.DateLike' = None,
|
|
comment_to: 'typing.Union[int, _tl.Message]' = None
|
|
) -> '_tl.Message':
|
|
if file is not None:
|
|
return await self.send_file(
|
|
entity, file, caption=message, reply_to=reply_to,
|
|
attributes=attributes, parse_mode=parse_mode,
|
|
force_document=force_document, thumb=thumb,
|
|
buttons=buttons, clear_draft=clear_draft, silent=silent,
|
|
schedule=schedule, supports_streaming=supports_streaming,
|
|
formatting_entities=formatting_entities,
|
|
comment_to=comment_to, background=background
|
|
)
|
|
|
|
entity = await self.get_input_entity(entity)
|
|
if comment_to is not None:
|
|
entity, reply_to = await _get_comment_data(self, entity, comment_to)
|
|
|
|
if isinstance(message, _tl.Message):
|
|
if buttons is None:
|
|
markup = message.reply_markup
|
|
else:
|
|
markup = self.build_reply_markup(buttons)
|
|
|
|
if silent is None:
|
|
silent = message.silent
|
|
|
|
if (message.media and not isinstance(
|
|
message.media, _tl.MessageMediaWebPage)):
|
|
return await self.send_file(
|
|
entity,
|
|
message.media,
|
|
caption=message.message,
|
|
silent=silent,
|
|
background=background,
|
|
reply_to=reply_to,
|
|
buttons=markup,
|
|
formatting_entities=message.entities,
|
|
schedule=schedule
|
|
)
|
|
|
|
request = _tl.fn.messages.SendMessage(
|
|
peer=entity,
|
|
message=message.message or '',
|
|
silent=silent,
|
|
background=background,
|
|
reply_to_msg_id=utils.get_message_id(reply_to),
|
|
reply_markup=markup,
|
|
entities=message.entities,
|
|
clear_draft=clear_draft,
|
|
no_webpage=not isinstance(
|
|
message.media, _tl.MessageMediaWebPage),
|
|
schedule_date=schedule
|
|
)
|
|
message = message.message
|
|
else:
|
|
if formatting_entities is None:
|
|
message, formatting_entities = await self._parse_message_text(message, parse_mode)
|
|
if not message:
|
|
raise ValueError(
|
|
'The message cannot be empty unless a file is provided'
|
|
)
|
|
|
|
request = _tl.fn.messages.SendMessage(
|
|
peer=entity,
|
|
message=message,
|
|
entities=formatting_entities,
|
|
no_webpage=not link_preview,
|
|
reply_to_msg_id=utils.get_message_id(reply_to),
|
|
clear_draft=clear_draft,
|
|
silent=silent,
|
|
background=background,
|
|
reply_markup=self.build_reply_markup(buttons),
|
|
schedule_date=schedule
|
|
)
|
|
|
|
result = await self(request)
|
|
if isinstance(result, _tl.UpdateShortSentMessage):
|
|
return _custom.Message._new(self, _tl.Message(
|
|
id=result.id,
|
|
peer_id=await _get_peer(self, entity),
|
|
message=message,
|
|
date=result.date,
|
|
out=result.out,
|
|
media=result.media,
|
|
entities=result.entities,
|
|
reply_markup=request.reply_markup,
|
|
ttl_period=result.ttl_period
|
|
), {}, entity)
|
|
|
|
return self._get_response_message(request, result, entity)
|
|
|
|
async def forward_messages(
|
|
self: 'TelegramClient',
|
|
entity: 'hints.EntityLike',
|
|
messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]',
|
|
from_peer: 'hints.EntityLike' = None,
|
|
*,
|
|
background: bool = None,
|
|
with_my_score: bool = None,
|
|
silent: bool = None,
|
|
as_album: bool = None,
|
|
schedule: 'hints.DateLike' = None
|
|
) -> 'typing.Sequence[_tl.Message]':
|
|
if as_album is not None:
|
|
warnings.warn('the as_album argument is deprecated and no longer has any effect')
|
|
|
|
single = not utils.is_list_like(messages)
|
|
if single:
|
|
messages = (messages,)
|
|
|
|
entity = await self.get_input_entity(entity)
|
|
|
|
if from_peer:
|
|
from_peer = await self.get_input_entity(from_peer)
|
|
from_peer_id = await self.get_peer_id(from_peer)
|
|
else:
|
|
from_peer_id = None
|
|
|
|
def get_key(m):
|
|
if isinstance(m, int):
|
|
if from_peer_id is not None:
|
|
return from_peer_id
|
|
|
|
raise ValueError('from_peer must be given if integer IDs are used')
|
|
elif isinstance(m, _tl.Message):
|
|
return m.chat_id
|
|
else:
|
|
raise TypeError('Cannot forward messages of type {}'.format(type(m)))
|
|
|
|
sent = []
|
|
for _chat_id, chunk in itertools.groupby(messages, key=get_key):
|
|
chunk = list(chunk)
|
|
if isinstance(chunk[0], int):
|
|
chat = from_peer
|
|
else:
|
|
chat = await chunk[0].get_input_chat()
|
|
chunk = [m.id for m in chunk]
|
|
|
|
req = _tl.fn.messages.ForwardMessages(
|
|
from_peer=chat,
|
|
id=chunk,
|
|
to_peer=entity,
|
|
silent=silent,
|
|
background=background,
|
|
with_my_score=with_my_score,
|
|
schedule_date=schedule
|
|
)
|
|
result = await self(req)
|
|
sent.extend(self._get_response_message(req, result, entity))
|
|
|
|
return sent[0] if single else sent
|
|
|
|
async def edit_message(
|
|
self: 'TelegramClient',
|
|
entity: 'typing.Union[hints.EntityLike, _tl.Message]',
|
|
message: 'hints.MessageLike' = None,
|
|
text: str = None,
|
|
*,
|
|
parse_mode: str = (),
|
|
attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None,
|
|
formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None,
|
|
link_preview: bool = True,
|
|
file: 'hints.FileLike' = None,
|
|
thumb: 'hints.FileLike' = None,
|
|
force_document: bool = False,
|
|
buttons: 'hints.MarkupLike' = None,
|
|
supports_streaming: bool = False,
|
|
schedule: 'hints.DateLike' = None
|
|
) -> '_tl.Message':
|
|
if formatting_entities is None:
|
|
text, formatting_entities = await self._parse_message_text(text, parse_mode)
|
|
file_handle, media, image = await self._file_to_media(file,
|
|
supports_streaming=supports_streaming,
|
|
thumb=thumb,
|
|
attributes=attributes,
|
|
force_document=force_document)
|
|
|
|
if isinstance(message, _tl.InputBotInlineMessageID):
|
|
request = _tl.fn.messages.EditInlineBotMessage(
|
|
id=message,
|
|
message=text,
|
|
no_webpage=not link_preview,
|
|
entities=formatting_entities,
|
|
media=media,
|
|
reply_markup=self.build_reply_markup(buttons)
|
|
)
|
|
# Invoke `messages.editInlineBotMessage` from the right datacenter.
|
|
# Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing.
|
|
# TODO should cache current session state
|
|
state = await self.session.get_state()
|
|
|
|
exported = state.dc_id != entity.dc_id
|
|
if exported:
|
|
try:
|
|
sender = await self._borrow_exported_sender(entity.dc_id)
|
|
return await self._call(sender, request)
|
|
finally:
|
|
await self._return_exported_sender(sender)
|
|
else:
|
|
return await self(request)
|
|
|
|
entity = await self.get_input_entity(entity)
|
|
request = _tl.fn.messages.EditMessage(
|
|
peer=entity,
|
|
id=utils.get_message_id(message),
|
|
message=text,
|
|
no_webpage=not link_preview,
|
|
entities=formatting_entities,
|
|
media=media,
|
|
reply_markup=self.build_reply_markup(buttons),
|
|
schedule_date=schedule
|
|
)
|
|
msg = self._get_response_message(request, await self(request), entity)
|
|
return msg
|
|
|
|
async def delete_messages(
|
|
self: 'TelegramClient',
|
|
entity: 'hints.EntityLike',
|
|
message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]',
|
|
*,
|
|
revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]':
|
|
if not utils.is_list_like(message_ids):
|
|
message_ids = (message_ids,)
|
|
|
|
message_ids = (
|
|
m.id if isinstance(m, (
|
|
_tl.Message, _tl.MessageService, _tl.MessageEmpty))
|
|
else int(m) for m in message_ids
|
|
)
|
|
|
|
if entity:
|
|
entity = await self.get_input_entity(entity)
|
|
ty = helpers._entity_type(entity)
|
|
else:
|
|
# no entity (None), set a value that's not a channel for private delete
|
|
ty = helpers._EntityType.USER
|
|
|
|
if ty == helpers._EntityType.CHANNEL:
|
|
res = await self([_tl.fn.channels.DeleteMessages(
|
|
entity, list(c)) for c in utils.chunks(message_ids)])
|
|
else:
|
|
res = await self([_tl.fn.messages.DeleteMessages(
|
|
list(c), revoke) for c in utils.chunks(message_ids)])
|
|
|
|
return sum(r.pts_count for r in res)
|
|
|
|
async def send_read_acknowledge(
|
|
self: 'TelegramClient',
|
|
entity: 'hints.EntityLike',
|
|
message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None,
|
|
*,
|
|
max_id: int = None,
|
|
clear_mentions: bool = False) -> bool:
|
|
if max_id is None:
|
|
if not message:
|
|
max_id = 0
|
|
else:
|
|
if utils.is_list_like(message):
|
|
max_id = max(msg.id for msg in message)
|
|
else:
|
|
max_id = message.id
|
|
|
|
entity = await self.get_input_entity(entity)
|
|
if clear_mentions:
|
|
await self(_tl.fn.messages.ReadMentions(entity))
|
|
if max_id is None:
|
|
return True
|
|
|
|
if max_id is not None:
|
|
if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:
|
|
return await self(_tl.fn.channels.ReadHistory(
|
|
utils.get_input_channel(entity), max_id=max_id))
|
|
else:
|
|
return await self(_tl.fn.messages.ReadHistory(
|
|
entity, max_id=max_id))
|
|
|
|
return False
|
|
|
|
async def pin_message(
|
|
self: 'TelegramClient',
|
|
entity: 'hints.EntityLike',
|
|
message: 'typing.Optional[hints.MessageIDLike]',
|
|
*,
|
|
notify: bool = False,
|
|
pm_oneside: bool = False
|
|
):
|
|
return await _pin(self, entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside)
|
|
|
|
async def unpin_message(
|
|
self: 'TelegramClient',
|
|
entity: 'hints.EntityLike',
|
|
message: 'typing.Optional[hints.MessageIDLike]' = None,
|
|
*,
|
|
notify: bool = False
|
|
):
|
|
return await _pin(self, entity, message, unpin=True, notify=notify)
|
|
|
|
async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False):
|
|
message = utils.get_message_id(message) or 0
|
|
entity = await self.get_input_entity(entity)
|
|
if message <= 0: # old behaviour accepted negative IDs to unpin
|
|
await self(_tl.fn.messages.UnpinAllMessages(entity))
|
|
return
|
|
|
|
request = _tl.fn.messages.UpdatePinnedMessage(
|
|
peer=entity,
|
|
id=message,
|
|
silent=not notify,
|
|
unpin=unpin,
|
|
pm_oneside=pm_oneside
|
|
)
|
|
result = await self(request)
|
|
|
|
# Unpinning does not produce a service message.
|
|
# Pinning a message that was already pinned also produces no service message.
|
|
# Pinning a message in your own chat does not produce a service message,
|
|
# but pinning on a private conversation with someone else does.
|
|
if unpin or not result.updates:
|
|
return
|
|
|
|
# Pinning a message that doesn't exist would RPC-error earlier
|
|
return self._get_response_message(request, result, entity)
|