1497 lines
61 KiB
Python
1497 lines
61 KiB
Python
|
import inspect
|
||
|
import itertools
|
||
|
import typing
|
||
|
import warnings
|
||
|
|
||
|
from .. import helpers, utils, errors, hints
|
||
|
from ..requestiter import RequestIter
|
||
|
from ..tl import types, functions
|
||
|
|
||
|
_MAX_CHUNK_SIZE = 100
|
||
|
|
||
|
if typing.TYPE_CHECKING:
|
||
|
from .telegramclient import TelegramClient
|
||
|
|
||
|
|
||
|
class _MessagesIter(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 = types.InputPeerEmpty()
|
||
|
|
||
|
if filter is None:
|
||
|
filter = types.InputMessagesFilterEmpty()
|
||
|
else:
|
||
|
filter = filter() if isinstance(filter, type) else filter
|
||
|
|
||
|
if not self.entity:
|
||
|
self.request = functions.messages.SearchGlobalRequest(
|
||
|
q=search or '',
|
||
|
filter=filter,
|
||
|
min_date=None,
|
||
|
max_date=offset_date,
|
||
|
offset_rate=0,
|
||
|
offset_peer=types.InputPeerEmpty(),
|
||
|
offset_id=offset_id,
|
||
|
limit=1
|
||
|
)
|
||
|
elif scheduled:
|
||
|
self.request = functions.messages.GetScheduledHistoryRequest(
|
||
|
peer=entity,
|
||
|
hash=0
|
||
|
)
|
||
|
elif reply_to is not None:
|
||
|
self.request = functions.messages.GetRepliesRequest(
|
||
|
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, types.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 = functions.messages.SearchRequest(
|
||
|
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, types.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 = functions.messages.GetHistoryRequest(
|
||
|
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, types.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, types.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
|
||
|
message._finish_init(self.client, entities, self.entity)
|
||
|
self.buffer.append(message)
|
||
|
|
||
|
# Not a slice (using offset would return the same, with e.g. SearchGlobal).
|
||
|
if isinstance(r, types.messages.Messages):
|
||
|
return True
|
||
|
|
||
|
# Some channels are "buggy" and may return less messages than
|
||
|
# requested (apparently, the messages excluded are, for example,
|
||
|
# "not displayable due to local laws").
|
||
|
#
|
||
|
# This means it's not safe to rely on `len(r.messages) < req.limit` as
|
||
|
# the stop condition. Unfortunately more requests must be made.
|
||
|
#
|
||
|
# However we can still check if the highest ID is equal to or lower
|
||
|
# than the limit, in which case there won't be any more messages
|
||
|
# because the lowest message ID is 1.
|
||
|
#
|
||
|
# We also assume the API will always return, at least, one message if
|
||
|
# there is more to fetch.
|
||
|
if not r.messages or r.messages[0].id <= 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, functions.messages.SearchRequest):
|
||
|
# 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, functions.messages.SearchGlobalRequest):
|
||
|
if last_message.input_chat:
|
||
|
self.request.offset_peer = last_message.input_chat
|
||
|
else:
|
||
|
self.request.offset_peer = types.InputPeerEmpty()
|
||
|
|
||
|
self.request.offset_rate = getattr(response, 'next_rate', 0)
|
||
|
|
||
|
|
||
|
class _IDsIter(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(
|
||
|
functions.channels.GetMessagesRequest(self._entity, ids))
|
||
|
except errors.MessageIdsEmptyError:
|
||
|
# All IDs were invalid, use a dummy result
|
||
|
r = types.messages.MessagesNotModified(len(ids))
|
||
|
else:
|
||
|
r = await self.client(functions.messages.GetMessagesRequest(ids))
|
||
|
if self._entity:
|
||
|
from_id = await self.client._get_peer(self._entity)
|
||
|
|
||
|
if isinstance(r, types.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, types.MessageEmpty) or (
|
||
|
from_id and message.peer_id != from_id):
|
||
|
self.buffer.append(None)
|
||
|
else:
|
||
|
message._finish_init(self.client, entities, self._entity)
|
||
|
self.buffer.append(message)
|
||
|
|
||
|
|
||
|
class MessageMethods:
|
||
|
|
||
|
# region Public methods
|
||
|
|
||
|
# region Message retrieval
|
||
|
|
||
|
def iter_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[types.TypeMessagesFilter, typing.Type[types.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]':
|
||
|
"""
|
||
|
Iterator over the messages for the given chat.
|
||
|
|
||
|
The default order is from newest to oldest, but this
|
||
|
behaviour can be changed with the `reverse` parameter.
|
||
|
|
||
|
If either `search`, `filter` or `from_user` are provided,
|
||
|
:tl:`messages.Search` will be used instead of :tl:`messages.getHistory`.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to
|
||
|
be around 30 seconds per 10 requests, therefore a sleep of 1
|
||
|
second is the default for this limit (or above).
|
||
|
|
||
|
Arguments
|
||
|
entity (`entity`):
|
||
|
The entity from whom to retrieve the message history.
|
||
|
|
||
|
It may be `None` to perform a global search, or
|
||
|
to get messages by their ID from no particular chat.
|
||
|
Note that some of the offsets will not work if this
|
||
|
is the case.
|
||
|
|
||
|
Note that if you want to perform a global search,
|
||
|
you **must** set a non-empty `search` string, a `filter`.
|
||
|
or `from_user`.
|
||
|
|
||
|
limit (`int` | `None`, optional):
|
||
|
Number of messages to be retrieved. Due to limitations with
|
||
|
the API retrieving more than 3000 messages will take longer
|
||
|
than half a minute (or even more based on previous calls).
|
||
|
|
||
|
The limit may also be `None`, which would eventually return
|
||
|
the whole history.
|
||
|
|
||
|
offset_date (`datetime`):
|
||
|
Offset date (messages *previous* to this date will be
|
||
|
retrieved). Exclusive.
|
||
|
|
||
|
offset_id (`int`):
|
||
|
Offset message ID (only messages *previous* to the given
|
||
|
ID will be retrieved). Exclusive.
|
||
|
|
||
|
max_id (`int`):
|
||
|
All the messages with a higher (newer) ID or equal to this will
|
||
|
be excluded.
|
||
|
|
||
|
min_id (`int`):
|
||
|
All the messages with a lower (older) ID or equal to this will
|
||
|
be excluded.
|
||
|
|
||
|
add_offset (`int`):
|
||
|
Additional message offset (all of the specified offsets +
|
||
|
this offset = older messages).
|
||
|
|
||
|
search (`str`):
|
||
|
The string to be used as a search query.
|
||
|
|
||
|
filter (:tl:`MessagesFilter` | `type`):
|
||
|
The filter to use when returning messages. For instance,
|
||
|
:tl:`InputMessagesFilterPhotos` would yield only messages
|
||
|
containing photos.
|
||
|
|
||
|
from_user (`entity`):
|
||
|
Only messages from this entity will be returned.
|
||
|
|
||
|
wait_time (`int`):
|
||
|
Wait time (in seconds) between different
|
||
|
:tl:`GetHistoryRequest`. Use this parameter to avoid hitting
|
||
|
the ``FloodWaitError`` as needed. If left to `None`, it will
|
||
|
default to 1 second only if the limit is higher than 3000.
|
||
|
|
||
|
If the ``ids`` parameter is used, this time will default
|
||
|
to 10 seconds only if the amount of IDs is higher than 300.
|
||
|
|
||
|
ids (`int`, `list`):
|
||
|
A single integer ID (or several IDs) for the message that
|
||
|
should be returned. This parameter takes precedence over
|
||
|
the rest (which will be ignored if this is set). This can
|
||
|
for instance be used to get the message with ID 123 from
|
||
|
a channel. Note that if the message doesn't exist, `None`
|
||
|
will appear in its place, so that zipping the list of IDs
|
||
|
with the messages can match one-to-one.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
At the time of writing, Telegram will **not** return
|
||
|
:tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that
|
||
|
failed (i.e. the message is not replying to any, or is
|
||
|
replying to a deleted message). This means that it is
|
||
|
**not** possible to match messages one-by-one, so be
|
||
|
careful if you use non-integers in this parameter.
|
||
|
|
||
|
reverse (`bool`, optional):
|
||
|
If set to `True`, the messages will be returned in reverse
|
||
|
order (from oldest to newest, instead of the default newest
|
||
|
to oldest). This also means that the meaning of `offset_id`
|
||
|
and `offset_date` parameters is reversed, although they will
|
||
|
still be exclusive. `min_id` becomes equivalent to `offset_id`
|
||
|
instead of being `max_id` as well since messages are returned
|
||
|
in ascending order.
|
||
|
|
||
|
You cannot use this if both `entity` and `ids` are `None`.
|
||
|
|
||
|
reply_to (`int`, optional):
|
||
|
If set to a message ID, the messages that reply to this ID
|
||
|
will be returned. This feature is also known as comments in
|
||
|
posts of broadcast channels, or viewing threads in groups.
|
||
|
|
||
|
This feature can only be used in broadcast channels and their
|
||
|
linked megagroups. Using it in a chat or private conversation
|
||
|
will result in ``telethon.errors.PeerIdInvalidError`` to occur.
|
||
|
|
||
|
When using this parameter, the ``filter`` and ``search``
|
||
|
parameters have no effect, since Telegram's API doesn't
|
||
|
support searching messages in replies.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
This feature is used to get replies to a message in the
|
||
|
*discussion* group. If the same broadcast channel sends
|
||
|
a message and replies to it itself, that reply will not
|
||
|
be included in the results.
|
||
|
|
||
|
scheduled (`bool`, optional):
|
||
|
If set to `True`, messages which are scheduled will be returned.
|
||
|
All other parameter will be ignored for this, except `entity`.
|
||
|
|
||
|
Yields
|
||
|
Instances of `Message <telethon.tl.custom.message.Message>`.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
# From most-recent to oldest
|
||
|
async for message in client.iter_messages(chat):
|
||
|
print(message.id, message.text)
|
||
|
|
||
|
# From oldest to most-recent
|
||
|
async for message in client.iter_messages(chat, reverse=True):
|
||
|
print(message.id, message.text)
|
||
|
|
||
|
# Filter by sender
|
||
|
async for message in client.iter_messages(chat, from_user='me'):
|
||
|
print(message.text)
|
||
|
|
||
|
# Server-side search with fuzzy text
|
||
|
async for message in client.iter_messages(chat, search='hello'):
|
||
|
print(message.id)
|
||
|
|
||
|
# Filter by message type:
|
||
|
from telethon.tl.types import InputMessagesFilterPhotos
|
||
|
async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos):
|
||
|
print(message.photo)
|
||
|
|
||
|
# Getting comments from a post in a channel:
|
||
|
async for message in client.iter_messages(channel, reply_to=123):
|
||
|
print(message.chat.title, message.text)
|
||
|
"""
|
||
|
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_messages(
|
||
|
self: 'TelegramClient', *args, **kwargs
|
||
|
) -> typing.Union['hints.TotalList', typing.Optional['types.Message']]:
|
||
|
"""
|
||
|
Same as `iter_messages()`, but returns a
|
||
|
`TotalList <telethon.helpers.TotalList>` instead.
|
||
|
|
||
|
If the `limit` is not set, it will be 1 by default unless both
|
||
|
`min_id` **and** `max_id` are set (as *named* arguments), in
|
||
|
which case the entire range will be returned.
|
||
|
|
||
|
This is so because any integer limit would be rather arbitrary and
|
||
|
it's common to only want to fetch one message, but if a range is
|
||
|
specified it makes sense that it should return the entirety of it.
|
||
|
|
||
|
If `ids` is present in the *named* arguments and is not a list,
|
||
|
a single `Message <telethon.tl.custom.message.Message>` will be
|
||
|
returned for convenience instead of a list.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
# Get 0 photos and print the total to show how many photos there are
|
||
|
from telethon.tl.types import InputMessagesFilterPhotos
|
||
|
photos = await client.get_messages(chat, 0, filter=InputMessagesFilterPhotos)
|
||
|
print(photos.total)
|
||
|
|
||
|
# Get all the photos
|
||
|
photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos)
|
||
|
|
||
|
# Get messages by ID:
|
||
|
message_1337 = await client.get_messages(chat, ids=1337)
|
||
|
"""
|
||
|
if len(args) == 1 and 'limit' not in kwargs:
|
||
|
if 'min_id' in kwargs and 'max_id' in kwargs:
|
||
|
kwargs['limit'] = None
|
||
|
else:
|
||
|
kwargs['limit'] = 1
|
||
|
|
||
|
it = self.iter_messages(*args, **kwargs)
|
||
|
|
||
|
ids = kwargs.get('ids')
|
||
|
if ids and not utils.is_list_like(ids):
|
||
|
async for message in it:
|
||
|
return message
|
||
|
else:
|
||
|
# Iterator exhausted = empty, to handle InputMessageReplyTo
|
||
|
return None
|
||
|
|
||
|
return await it.collect()
|
||
|
|
||
|
get_messages.__signature__ = inspect.signature(iter_messages)
|
||
|
|
||
|
# endregion
|
||
|
|
||
|
# region Message sending/editing/deleting
|
||
|
|
||
|
async def _get_comment_data(
|
||
|
self: 'TelegramClient',
|
||
|
entity: 'hints.EntityLike',
|
||
|
message: 'typing.Union[int, types.Message]'
|
||
|
):
|
||
|
r = await self(functions.messages.GetDiscussionMessageRequest(
|
||
|
peer=entity,
|
||
|
msg_id=utils.get_message_id(message)
|
||
|
))
|
||
|
m = min(r.messages, key=lambda msg: msg.id)
|
||
|
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, types.Message]' = None,
|
||
|
attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
|
||
|
parse_mode: typing.Optional[str] = (),
|
||
|
formatting_entities: typing.Optional[typing.List[types.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: typing.Optional['hints.MarkupLike'] = None,
|
||
|
silent: bool = None,
|
||
|
background: bool = None,
|
||
|
supports_streaming: bool = False,
|
||
|
schedule: 'hints.DateLike' = None,
|
||
|
comment_to: 'typing.Union[int, types.Message]' = None,
|
||
|
nosound_video: bool = None,
|
||
|
) -> 'types.Message':
|
||
|
"""
|
||
|
Sends a message to the specified user, chat or channel.
|
||
|
|
||
|
The default parse mode is the same as the official applications
|
||
|
(a custom flavour of markdown). ``**bold**, `code` or __italic__``
|
||
|
are available. In addition you can send ``[links](https://example.com)``
|
||
|
and ``[mentions](@username)`` (or using IDs like in the Bot API:
|
||
|
``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three
|
||
|
backticks.
|
||
|
|
||
|
Sending a ``/start`` command with a parameter (like ``?start=data``)
|
||
|
is also done through this method. Simply send ``'/start data'`` to
|
||
|
the bot.
|
||
|
|
||
|
See also `Message.respond() <telethon.tl.custom.message.Message.respond>`
|
||
|
and `Message.reply() <telethon.tl.custom.message.Message.reply>`.
|
||
|
|
||
|
Arguments
|
||
|
entity (`entity`):
|
||
|
To who will it be sent.
|
||
|
|
||
|
message (`str` | `Message <telethon.tl.custom.message.Message>`):
|
||
|
The message to be sent, or another message object to resend.
|
||
|
|
||
|
The maximum length for a message is 35,000 bytes or 4,096
|
||
|
characters. Longer messages will not be sliced automatically,
|
||
|
and you should slice them manually if the text to send is
|
||
|
longer than said length.
|
||
|
|
||
|
reply_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):
|
||
|
Whether to reply to a message or not. If an integer is provided,
|
||
|
it should be the ID of the message that it should reply to.
|
||
|
|
||
|
attributes (`list`, optional):
|
||
|
Optional attributes that override the inferred ones, like
|
||
|
:tl:`DocumentAttributeFilename` and so on.
|
||
|
|
||
|
parse_mode (`object`, optional):
|
||
|
See the `TelegramClient.parse_mode
|
||
|
<telethon.client.messageparse.MessageParseMethods.parse_mode>`
|
||
|
property for allowed values. Markdown parsing will be used by
|
||
|
default.
|
||
|
|
||
|
formatting_entities (`list`, optional):
|
||
|
A list of message formatting entities. When provided, the ``parse_mode`` is ignored.
|
||
|
|
||
|
link_preview (`bool`, optional):
|
||
|
Should the link preview be shown?
|
||
|
|
||
|
file (`file`, optional):
|
||
|
Sends a message with a file attached (e.g. a photo,
|
||
|
video, audio or document). The ``message`` may be empty.
|
||
|
|
||
|
thumb (`str` | `bytes` | `file`, optional):
|
||
|
Optional JPEG thumbnail (for documents). **Telegram will
|
||
|
ignore this parameter** unless you pass a ``.jpg`` file!
|
||
|
The file must also be small in dimensions and in disk size.
|
||
|
Successful thumbnails were files below 20kB and 320x320px.
|
||
|
Width/height and dimensions/size ratios may be important.
|
||
|
For Telegram to accept a thumbnail, you must provide the
|
||
|
dimensions of the underlying media through ``attributes=``
|
||
|
with :tl:`DocumentAttributesVideo` or by installing the
|
||
|
optional ``hachoir`` dependency.
|
||
|
|
||
|
force_document (`bool`, optional):
|
||
|
Whether to send the given file as a document or not.
|
||
|
|
||
|
clear_draft (`bool`, optional):
|
||
|
Whether the existing draft should be cleared or not.
|
||
|
|
||
|
buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`):
|
||
|
The matrix (list of lists), row list or button to be shown
|
||
|
after sending the message. This parameter will only work if
|
||
|
you have signed in as a bot. You can also pass your own
|
||
|
:tl:`ReplyMarkup` here.
|
||
|
|
||
|
All the following limits apply together:
|
||
|
|
||
|
* There can be 100 buttons at most (any more are ignored).
|
||
|
* There can be 8 buttons per row at most (more are ignored).
|
||
|
* The maximum callback data per button is 64 bytes.
|
||
|
* The maximum data that can be embedded in total is just
|
||
|
over 4KB, shared between inline callback data and text.
|
||
|
|
||
|
silent (`bool`, optional):
|
||
|
Whether the message should notify people in a broadcast
|
||
|
channel or not. Defaults to `False`, which means it will
|
||
|
notify them. Set it to `True` to alter this behaviour.
|
||
|
|
||
|
background (`bool`, optional):
|
||
|
Whether the message should be send in background.
|
||
|
|
||
|
supports_streaming (`bool`, optional):
|
||
|
Whether the sent video supports streaming or not. Note that
|
||
|
Telegram only recognizes as streamable some formats like MP4,
|
||
|
and others like AVI or MKV will not work. You should convert
|
||
|
these to MP4 before sending if you want them to be streamable.
|
||
|
Unsupported formats will result in ``VideoContentTypeError``.
|
||
|
|
||
|
schedule (`hints.DateLike`, optional):
|
||
|
If set, the message won't send immediately, and instead
|
||
|
it will be scheduled to be automatically sent at a later
|
||
|
time.
|
||
|
|
||
|
comment_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):
|
||
|
Similar to ``reply_to``, but replies in the linked group of a
|
||
|
broadcast channel instead (effectively leaving a "comment to"
|
||
|
the specified message).
|
||
|
|
||
|
This parameter takes precedence over ``reply_to``. If there is
|
||
|
no linked chat, `telethon.errors.sgIdInvalidError` is raised.
|
||
|
|
||
|
nosound_video (`bool`, optional):
|
||
|
Only applicable when sending a video file without an audio
|
||
|
track. If set to ``True``, the video will be displayed in
|
||
|
Telegram as a video. If set to ``False``, Telegram will attempt
|
||
|
to display the video as an animated gif. (It may still display
|
||
|
as a video due to other factors.) The value is ignored if set
|
||
|
on non-video files. This is set to ``True`` for albums, as gifs
|
||
|
cannot be sent in albums.
|
||
|
|
||
|
Returns
|
||
|
The sent `custom.Message <telethon.tl.custom.message.Message>`.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
# Markdown is the default
|
||
|
await client.send_message('me', 'Hello **world**!')
|
||
|
|
||
|
# Default to another parse mode
|
||
|
client.parse_mode = 'html'
|
||
|
|
||
|
await client.send_message('me', 'Some <b>bold</b> and <i>italic</i> text')
|
||
|
await client.send_message('me', 'An <a href="https://example.com">URL</a>')
|
||
|
# code and pre tags also work, but those break the documentation :)
|
||
|
await client.send_message('me', '<a href="tg://user?id=me">Mentions</a>')
|
||
|
|
||
|
# Explicit parse mode
|
||
|
# No parse mode by default
|
||
|
client.parse_mode = None
|
||
|
|
||
|
# ...but here I want markdown
|
||
|
await client.send_message('me', 'Hello, **world**!', parse_mode='md')
|
||
|
|
||
|
# ...and here I need HTML
|
||
|
await client.send_message('me', 'Hello, <i>world</i>!', parse_mode='html')
|
||
|
|
||
|
# If you logged in as a bot account, you can send buttons
|
||
|
from telethon import events, Button
|
||
|
|
||
|
@client.on(events.CallbackQuery)
|
||
|
async def callback(event):
|
||
|
await event.edit('Thank you for clicking {}!'.format(event.data))
|
||
|
|
||
|
# Single inline button
|
||
|
await client.send_message(chat, 'A single button, with "clk1" as data',
|
||
|
buttons=Button.inline('Click me', b'clk1'))
|
||
|
|
||
|
# Matrix of inline buttons
|
||
|
await client.send_message(chat, 'Pick one from this grid', buttons=[
|
||
|
[Button.inline('Left'), Button.inline('Right')],
|
||
|
[Button.url('Check this site!', 'https://example.com')]
|
||
|
])
|
||
|
|
||
|
# Reply keyboard
|
||
|
await client.send_message(chat, 'Welcome', buttons=[
|
||
|
Button.text('Thanks!', resize=True, single_use=True),
|
||
|
Button.request_phone('Send phone'),
|
||
|
Button.request_location('Send location')
|
||
|
])
|
||
|
|
||
|
# Forcing replies or clearing buttons.
|
||
|
await client.send_message(chat, 'Reply to me', buttons=Button.force_reply())
|
||
|
await client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear())
|
||
|
|
||
|
# Scheduling a message to be sent after 5 minutes
|
||
|
from datetime import timedelta
|
||
|
await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5))
|
||
|
"""
|
||
|
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,
|
||
|
nosound_video=nosound_video,
|
||
|
)
|
||
|
|
||
|
entity = await self.get_input_entity(entity)
|
||
|
if comment_to is not None:
|
||
|
entity, reply_to = await self._get_comment_data(entity, comment_to)
|
||
|
else:
|
||
|
reply_to = utils.get_message_id(reply_to)
|
||
|
|
||
|
if isinstance(message, types.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, types.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,
|
||
|
parse_mode=None, # explicitly disable parse_mode to force using even empty formatting_entities
|
||
|
schedule=schedule
|
||
|
)
|
||
|
|
||
|
request = functions.messages.SendMessageRequest(
|
||
|
peer=entity,
|
||
|
message=message.message or '',
|
||
|
silent=silent,
|
||
|
background=background,
|
||
|
reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),
|
||
|
reply_markup=markup,
|
||
|
entities=message.entities,
|
||
|
clear_draft=clear_draft,
|
||
|
no_webpage=not isinstance(
|
||
|
message.media, types.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 = functions.messages.SendMessageRequest(
|
||
|
peer=entity,
|
||
|
message=message,
|
||
|
entities=formatting_entities,
|
||
|
no_webpage=not link_preview,
|
||
|
reply_to=None if reply_to is None else types.InputReplyToMessage(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, types.UpdateShortSentMessage):
|
||
|
message = types.Message(
|
||
|
id=result.id,
|
||
|
peer_id=await self._get_peer(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,
|
||
|
reply_to=request.reply_to
|
||
|
)
|
||
|
message._finish_init(self, {}, entity)
|
||
|
return message
|
||
|
|
||
|
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,
|
||
|
drop_author: bool = None,
|
||
|
) -> 'typing.Sequence[types.Message]':
|
||
|
"""
|
||
|
Forwards the given messages to the specified entity.
|
||
|
|
||
|
If you want to "forward" a message without the forward header
|
||
|
(the "forwarded from" text), you should use `send_message` with
|
||
|
the original message instead. This will send a copy of it.
|
||
|
|
||
|
See also `Message.forward_to() <telethon.tl.custom.message.Message.forward_to>`.
|
||
|
|
||
|
Arguments
|
||
|
entity (`entity`):
|
||
|
To which entity the message(s) will be forwarded.
|
||
|
|
||
|
messages (`list` | `int` | `Message <telethon.tl.custom.message.Message>`):
|
||
|
The message(s) to forward, or their integer IDs.
|
||
|
|
||
|
from_peer (`entity`):
|
||
|
If the given messages are integer IDs and not instances
|
||
|
of the ``Message`` class, this *must* be specified in
|
||
|
order for the forward to work. This parameter indicates
|
||
|
the entity from which the messages should be forwarded.
|
||
|
|
||
|
silent (`bool`, optional):
|
||
|
Whether the message should notify people with sound or not.
|
||
|
Defaults to `False` (send with a notification sound unless
|
||
|
the person has the chat muted). Set it to `True` to alter
|
||
|
this behaviour.
|
||
|
|
||
|
background (`bool`, optional):
|
||
|
Whether the message should be forwarded in background.
|
||
|
|
||
|
with_my_score (`bool`, optional):
|
||
|
Whether forwarded should contain your game score.
|
||
|
|
||
|
as_album (`bool`, optional):
|
||
|
This flag no longer has any effect.
|
||
|
|
||
|
schedule (`hints.DateLike`, optional):
|
||
|
If set, the message(s) won't forward immediately, and
|
||
|
instead they will be scheduled to be automatically sent
|
||
|
at a later time.
|
||
|
|
||
|
Returns
|
||
|
The list of forwarded `Message <telethon.tl.custom.message.Message>`,
|
||
|
or a single one if a list wasn't provided as input.
|
||
|
|
||
|
Note that if all messages are invalid (i.e. deleted) the call
|
||
|
will fail with ``MessageIdInvalidError``. If only some are
|
||
|
invalid, the list will have `None` instead of those messages.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
# a single one
|
||
|
await client.forward_messages(chat, message)
|
||
|
# or
|
||
|
await client.forward_messages(chat, message_id, from_chat)
|
||
|
# or
|
||
|
await message.forward_to(chat)
|
||
|
|
||
|
# multiple
|
||
|
await client.forward_messages(chat, messages)
|
||
|
# or
|
||
|
await client.forward_messages(chat, message_ids, from_chat)
|
||
|
|
||
|
# Forwarding as a copy
|
||
|
await client.send_message(chat, 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, types.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 = from_peer or await self.get_input_entity(chunk[0].peer_id)
|
||
|
chunk = [m.id for m in chunk]
|
||
|
|
||
|
req = functions.messages.ForwardMessagesRequest(
|
||
|
from_peer=chat,
|
||
|
id=chunk,
|
||
|
to_peer=entity,
|
||
|
silent=silent,
|
||
|
background=background,
|
||
|
with_my_score=with_my_score,
|
||
|
schedule_date=schedule,
|
||
|
drop_author=drop_author
|
||
|
)
|
||
|
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, types.Message]',
|
||
|
message: 'hints.MessageLike' = None,
|
||
|
text: str = None,
|
||
|
*,
|
||
|
parse_mode: str = (),
|
||
|
attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
|
||
|
formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
|
||
|
link_preview: bool = True,
|
||
|
file: 'hints.FileLike' = None,
|
||
|
thumb: 'hints.FileLike' = None,
|
||
|
force_document: bool = False,
|
||
|
buttons: typing.Optional['hints.MarkupLike'] = None,
|
||
|
supports_streaming: bool = False,
|
||
|
schedule: 'hints.DateLike' = None
|
||
|
) -> 'types.Message':
|
||
|
"""
|
||
|
Edits the given message to change its text or media.
|
||
|
|
||
|
See also `Message.edit() <telethon.tl.custom.message.Message.edit>`.
|
||
|
|
||
|
Arguments
|
||
|
entity (`entity` | `Message <telethon.tl.custom.message.Message>`):
|
||
|
From which chat to edit the message. This can also be
|
||
|
the message to be edited, and the entity will be inferred
|
||
|
from it, so the next parameter will be assumed to be the
|
||
|
message text.
|
||
|
|
||
|
You may also pass a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`,
|
||
|
which is the only way to edit messages that were sent
|
||
|
after the user selects an inline query result.
|
||
|
|
||
|
message (`int` | `Message <telethon.tl.custom.message.Message>` | `str`):
|
||
|
The ID of the message (or `Message
|
||
|
<telethon.tl.custom.message.Message>` itself) to be edited.
|
||
|
If the `entity` was a `Message
|
||
|
<telethon.tl.custom.message.Message>`, then this message
|
||
|
will be treated as the new text.
|
||
|
|
||
|
text (`str`, optional):
|
||
|
The new text of the message. Does nothing if the `entity`
|
||
|
was a `Message <telethon.tl.custom.message.Message>`.
|
||
|
|
||
|
parse_mode (`object`, optional):
|
||
|
See the `TelegramClient.parse_mode
|
||
|
<telethon.client.messageparse.MessageParseMethods.parse_mode>`
|
||
|
property for allowed values. Markdown parsing will be used by
|
||
|
default.
|
||
|
|
||
|
attributes (`list`, optional):
|
||
|
Optional attributes that override the inferred ones, like
|
||
|
:tl:`DocumentAttributeFilename` and so on.
|
||
|
|
||
|
formatting_entities (`list`, optional):
|
||
|
A list of message formatting entities. When provided, the ``parse_mode`` is ignored.
|
||
|
|
||
|
link_preview (`bool`, optional):
|
||
|
Should the link preview be shown?
|
||
|
|
||
|
file (`str` | `bytes` | `file` | `media`, optional):
|
||
|
The file object that should replace the existing media
|
||
|
in the message.
|
||
|
|
||
|
thumb (`str` | `bytes` | `file`, optional):
|
||
|
Optional JPEG thumbnail (for documents). **Telegram will
|
||
|
ignore this parameter** unless you pass a ``.jpg`` file!
|
||
|
The file must also be small in dimensions and in disk size.
|
||
|
Successful thumbnails were files below 20kB and 320x320px.
|
||
|
Width/height and dimensions/size ratios may be important.
|
||
|
For Telegram to accept a thumbnail, you must provide the
|
||
|
dimensions of the underlying media through ``attributes=``
|
||
|
with :tl:`DocumentAttributesVideo` or by installing the
|
||
|
optional ``hachoir`` dependency.
|
||
|
|
||
|
force_document (`bool`, optional):
|
||
|
Whether to send the given file as a document or not.
|
||
|
|
||
|
buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`):
|
||
|
The matrix (list of lists), row list or button to be shown
|
||
|
after sending the message. This parameter will only work if
|
||
|
you have signed in as a bot. You can also pass your own
|
||
|
:tl:`ReplyMarkup` here.
|
||
|
|
||
|
supports_streaming (`bool`, optional):
|
||
|
Whether the sent video supports streaming or not. Note that
|
||
|
Telegram only recognizes as streamable some formats like MP4,
|
||
|
and others like AVI or MKV will not work. You should convert
|
||
|
these to MP4 before sending if you want them to be streamable.
|
||
|
Unsupported formats will result in ``VideoContentTypeError``.
|
||
|
|
||
|
schedule (`hints.DateLike`, optional):
|
||
|
If set, the message won't be edited immediately, and instead
|
||
|
it will be scheduled to be automatically edited at a later
|
||
|
time.
|
||
|
|
||
|
Note that this parameter will have no effect if you are
|
||
|
trying to edit a message that was sent via inline bots.
|
||
|
|
||
|
Returns
|
||
|
The edited `Message <telethon.tl.custom.message.Message>`,
|
||
|
unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which
|
||
|
case this method returns a boolean.
|
||
|
|
||
|
Raises
|
||
|
``MessageAuthorRequiredError`` if you're not the author of the
|
||
|
message but tried editing it anyway.
|
||
|
|
||
|
``MessageNotModifiedError`` if the contents of the message were
|
||
|
not modified at all.
|
||
|
|
||
|
``MessageIdInvalidError`` if the ID of the message is invalid
|
||
|
(the ID itself may be correct, but the message with that ID
|
||
|
cannot be edited). For example, when trying to edit messages
|
||
|
with a reply markup (or clear markup) this error will be raised.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
message = await client.send_message(chat, 'hello')
|
||
|
|
||
|
await client.edit_message(chat, message, 'hello!')
|
||
|
# or
|
||
|
await client.edit_message(chat, message.id, 'hello!!')
|
||
|
# or
|
||
|
await client.edit_message(message, 'hello!!!')
|
||
|
"""
|
||
|
if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
|
||
|
text = text or message
|
||
|
message = entity
|
||
|
elif isinstance(entity, types.Message):
|
||
|
text = message # Shift the parameters to the right
|
||
|
message = entity
|
||
|
entity = entity.peer_id
|
||
|
|
||
|
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(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
|
||
|
request = functions.messages.EditInlineBotMessageRequest(
|
||
|
id=entity,
|
||
|
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.
|
||
|
exported = self.session.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 = functions.messages.EditMessageRequest(
|
||
|
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[types.messages.AffectedMessages]':
|
||
|
"""
|
||
|
Deletes the given messages, optionally "for everyone".
|
||
|
|
||
|
See also `Message.delete() <telethon.tl.custom.message.Message.delete>`.
|
||
|
|
||
|
.. warning::
|
||
|
|
||
|
This method does **not** validate that the message IDs belong
|
||
|
to the chat that you passed! It's possible for the method to
|
||
|
delete messages from different private chats and small group
|
||
|
chats at once, so make sure to pass the right IDs.
|
||
|
|
||
|
Arguments
|
||
|
entity (`entity`):
|
||
|
From who the message will be deleted. This can actually
|
||
|
be `None` for normal chats, but **must** be present
|
||
|
for channels and megagroups.
|
||
|
|
||
|
message_ids (`list` | `int` | `Message <telethon.tl.custom.message.Message>`):
|
||
|
The IDs (or ID) or messages to be deleted.
|
||
|
|
||
|
revoke (`bool`, optional):
|
||
|
Whether the message should be deleted for everyone or not.
|
||
|
By default it has the opposite behaviour of official clients,
|
||
|
and it will delete the message for everyone.
|
||
|
|
||
|
`Since 24 March 2019
|
||
|
<https://telegram.org/blog/unsend-privacy-emoji>`_, you can
|
||
|
also revoke messages of any age (i.e. messages sent long in
|
||
|
the past) the *other* person sent in private conversations
|
||
|
(and of course your messages too).
|
||
|
|
||
|
Disabling this has no effect on channels or megagroups,
|
||
|
since it will unconditionally delete the message for everyone.
|
||
|
|
||
|
Returns
|
||
|
A list of :tl:`AffectedMessages`, each item being the result
|
||
|
for the delete calls of the messages in chunks of 100 each.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
await client.delete_messages(chat, messages)
|
||
|
"""
|
||
|
if not utils.is_list_like(message_ids):
|
||
|
message_ids = (message_ids,)
|
||
|
|
||
|
message_ids = (
|
||
|
m.id if isinstance(m, (
|
||
|
types.Message, types.MessageService, types.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:
|
||
|
return await self([functions.channels.DeleteMessagesRequest(
|
||
|
entity, list(c)) for c in utils.chunks(message_ids)])
|
||
|
else:
|
||
|
return await self([functions.messages.DeleteMessagesRequest(
|
||
|
list(c), revoke) for c in utils.chunks(message_ids)])
|
||
|
|
||
|
# endregion
|
||
|
|
||
|
# region Miscellaneous
|
||
|
|
||
|
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,
|
||
|
clear_reactions: bool = False) -> bool:
|
||
|
"""
|
||
|
Marks messages as read and optionally clears mentions.
|
||
|
|
||
|
This effectively marks a message as read (or more than one) in the
|
||
|
given conversation.
|
||
|
|
||
|
If neither message nor maximum ID are provided, all messages will be
|
||
|
marked as read by assuming that ``max_id = 0``.
|
||
|
|
||
|
If a message or maximum ID is provided, all the messages up to and
|
||
|
including such ID will be marked as read (for all messages whose ID
|
||
|
≤ max_id).
|
||
|
|
||
|
See also `Message.mark_read() <telethon.tl.custom.message.Message.mark_read>`.
|
||
|
|
||
|
Arguments
|
||
|
entity (`entity`):
|
||
|
The chat where these messages are located.
|
||
|
|
||
|
message (`list` | `Message <telethon.tl.custom.message.Message>`):
|
||
|
Either a list of messages or a single message.
|
||
|
|
||
|
max_id (`int`):
|
||
|
Until which message should the read acknowledge be sent for.
|
||
|
This has priority over the ``message`` parameter.
|
||
|
|
||
|
clear_mentions (`bool`):
|
||
|
Whether the mention badge should be cleared (so that
|
||
|
there are no more mentions) or not for the given entity.
|
||
|
|
||
|
If no message is provided, this will be the only action
|
||
|
taken.
|
||
|
|
||
|
clear_reactions (`bool`):
|
||
|
Whether the reactions badge should be cleared (so that
|
||
|
there are no more reaction notifications) or not for the given entity.
|
||
|
|
||
|
If no message is provided, this will be the only action
|
||
|
taken.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
# using a Message object
|
||
|
await client.send_read_acknowledge(chat, message)
|
||
|
# ...or using the int ID of a Message
|
||
|
await client.send_read_acknowledge(chat, message_id)
|
||
|
# ...or passing a list of messages to mark as read
|
||
|
await client.send_read_acknowledge(chat, messages)
|
||
|
"""
|
||
|
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(functions.messages.ReadMentionsRequest(entity))
|
||
|
if max_id is None and not clear_reactions:
|
||
|
return True
|
||
|
if clear_reactions:
|
||
|
await self(functions.messages.ReadReactionsRequest(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(functions.channels.ReadHistoryRequest(
|
||
|
utils.get_input_channel(entity), max_id=max_id))
|
||
|
else:
|
||
|
return await self(functions.messages.ReadHistoryRequest(
|
||
|
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
|
||
|
):
|
||
|
"""
|
||
|
Pins a message in a chat.
|
||
|
|
||
|
The default behaviour is to *not* notify members, unlike the
|
||
|
official applications.
|
||
|
|
||
|
See also `Message.pin() <telethon.tl.custom.message.Message.pin>`.
|
||
|
|
||
|
Arguments
|
||
|
entity (`entity`):
|
||
|
The chat where the message should be pinned.
|
||
|
|
||
|
message (`int` | `Message <telethon.tl.custom.message.Message>`):
|
||
|
The message or the message ID to pin. If it's
|
||
|
`None`, all messages will be unpinned instead.
|
||
|
|
||
|
notify (`bool`, optional):
|
||
|
Whether the pin should notify people or not.
|
||
|
|
||
|
pm_oneside (`bool`, optional):
|
||
|
Whether the message should be pinned for everyone or not.
|
||
|
By default it has the opposite behaviour of official clients,
|
||
|
and it will pin the message for both sides, in private chats.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
# Send and pin a message to annoy everyone
|
||
|
message = await client.send_message(chat, 'Pinotifying is fun!')
|
||
|
await client.pin_message(chat, message, notify=True)
|
||
|
"""
|
||
|
return await self._pin(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
|
||
|
):
|
||
|
"""
|
||
|
Unpins a message in a chat.
|
||
|
|
||
|
If no message ID is specified, all pinned messages will be unpinned.
|
||
|
|
||
|
See also `Message.unpin() <telethon.tl.custom.message.Message.unpin>`.
|
||
|
|
||
|
Arguments
|
||
|
entity (`entity`):
|
||
|
The chat where the message should be pinned.
|
||
|
|
||
|
message (`int` | `Message <telethon.tl.custom.message.Message>`):
|
||
|
The message or the message ID to unpin. If it's
|
||
|
`None`, all messages will be unpinned instead.
|
||
|
|
||
|
Example
|
||
|
.. code-block:: python
|
||
|
|
||
|
# Unpin all messages from a chat
|
||
|
await client.unpin_message(chat)
|
||
|
"""
|
||
|
return await self._pin(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(functions.messages.UnpinAllMessagesRequest(entity))
|
||
|
return
|
||
|
|
||
|
request = functions.messages.UpdatePinnedMessageRequest(
|
||
|
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)
|
||
|
|
||
|
# endregion
|
||
|
|
||
|
# endregion
|