Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2018-04-23 15:53:28 +02:00
commit 04a68f12cc
9 changed files with 142 additions and 60 deletions

View File

@ -204,9 +204,7 @@ class ChatAction(EventBuilder):
if isinstance(self._pinned_message, int) and await self.input_chat: if isinstance(self._pinned_message, int) and await self.input_chat:
r = await self._client(functions.channels.GetMessagesRequest( r = await self._client(functions.channels.GetMessagesRequest(
self._input_chat, [ self._input_chat, [self._pinned_message]
types.InputMessageID(self._pinned_message)
]
)) ))
try: try:
self._pinned_message = next( self._pinned_message = next(

View File

@ -118,15 +118,11 @@ class EventCommon(abc.ABC):
try: try:
if isinstance(chat, types.InputPeerChannel): if isinstance(chat, types.InputPeerChannel):
result = await self._client( result = await self._client(
functions.channels.GetMessagesRequest(chat, [ functions.channels.GetMessagesRequest(chat, [msg_id])
types.InputMessageID(msg_id)
])
) )
else: else:
result = await self._client( result = await self._client(
functions.messages.GetMessagesRequest([ functions.messages.GetMessagesRequest([msg_id])
types.InputMessageID(msg_id)
])
) )
except RPCError: except RPCError:
return None, None return None, None

View File

@ -101,16 +101,14 @@ class MessageRead(EventBuilder):
if not chat: if not chat:
self._messages = [] self._messages = []
elif isinstance(chat, types.InputPeerChannel): elif isinstance(chat, types.InputPeerChannel):
ids = [types.InputMessageID(x) for x in self._message_ids]
self._messages =\ self._messages =\
await self._client(functions.channels.GetMessagesRequest( await self._client(functions.channels.GetMessagesRequest(
chat, ids chat, self._message_ids
)).messages )).messages
else: else:
ids = [types.InputMessageID(x) for x in self._message_ids]
self._messages = \ self._messages = \
await self._client(functions.messages.GetMessagesRequest( await self._client(functions.messages.GetMessagesRequest(
ids self._message_ids
)).messages )).messages
return self._messages return self._messages

View File

@ -297,13 +297,11 @@ class NewMessage(EventBuilder):
if self._reply_message is None: if self._reply_message is None:
if isinstance(await self.input_chat, types.InputPeerChannel): if isinstance(await self.input_chat, types.InputPeerChannel):
r = await self._client(functions.channels.GetMessagesRequest( r = await self._client(functions.channels.GetMessagesRequest(
await self.input_chat, [ await self.input_chat, [self.message.reply_to_msg_id]
types.InputMessageID(self.message.reply_to_msg_id)
]
)) ))
else: else:
r = await self._client(functions.messages.GetMessagesRequest( r = await self._client(functions.messages.GetMessagesRequest(
[types.InputMessageID(self.message.reply_to_msg_id)] [self.message.reply_to_msg_id]
)) ))
if not isinstance(r, types.messages.MessagesNotModified): if not isinstance(r, types.messages.MessagesNotModified):
self._reply_message = r.messages[0] self._reply_message = r.messages[0]

View File

@ -17,7 +17,7 @@ CURRENT_VERSION = 3 # database version
class SQLiteSession(MemorySession): class SQLiteSession(MemorySession):
"""This session contains the required information to login into your """This session contains the required information to login into your
Telegram account. NEVER give the saved JSON file to anyone, since Telegram account. NEVER give the saved session file to anyone, since
they would gain instant access to all your messages and contacts. they would gain instant access to all your messages and contacts.
If you think the session has been compromised, close all the sessions If you think the session has been compromised, close all the sessions

View File

@ -9,7 +9,8 @@ from .crypto import rsa
from .errors import ( from .errors import (
RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, RPCError, BrokenAuthKeyError, ServerError, FloodWaitError,
FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError, FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError,
PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError,
RpcCallFailError
) )
from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .network import authenticator, MtProtoSender, Connection, ConnectionMode
from .sessions import Session, SQLiteSession from .sessions import Session, SQLiteSession
@ -544,9 +545,9 @@ class TelegramBareClient:
await self._reconnect(new_dc=e.new_dc) await self._reconnect(new_dc=e.new_dc)
return await self._invoke(call_receive, retry, *requests) return await self._invoke(call_receive, retry, *requests)
except ServerError as e: except (ServerError, RpcCallFailError) as e:
# Telegram is having some issues, just retry # Telegram is having some issues, just retry
__log__.error('Telegram servers are having internal errors %s', e) __log__.warning('Telegram is having internal issues: %s', e)
except (FloodWaitError, FloodTestPhoneWaitError) as e: except (FloodWaitError, FloodTestPhoneWaitError) as e:
__log__.warning('Request invoked too often, wait %ds', e.seconds) __log__.warning('Request invoked too often, wait %ds', e.seconds)

View File

@ -58,7 +58,7 @@ from .tl.functions.messages import (
SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest,
CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest,
UploadMediaRequest, EditMessageRequest, GetFullChatRequest, UploadMediaRequest, EditMessageRequest, GetFullChatRequest,
ForwardMessagesRequest ForwardMessagesRequest, SearchRequest
) )
from .tl.functions import channels from .tl.functions import channels
@ -780,9 +780,13 @@ class TelegramClient(TelegramBareClient):
if isinstance(message, Message): if isinstance(message, Message):
if (message.media if (message.media
and not isinstance(message.media, MessageMediaWebPage)): and not isinstance(message.media, MessageMediaWebPage)):
return await self.send_file(entity, message.media) return await self.send_file(entity, message.media,
caption=message.message,
entities=message.entities)
if utils.get_peer_id(entity) == utils.get_peer_id(message.to_id): if reply_to is not None:
reply_id = self._get_message_id(reply_to)
elif utils.get_peer_id(entity) == utils.get_peer_id(message.to_id):
reply_id = message.reply_to_msg_id reply_id = message.reply_to_msg_id
else: else:
reply_id = None reply_id = None
@ -879,20 +883,26 @@ class TelegramClient(TelegramBareClient):
result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id] result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
return result[0] if single else result return result[0] if single else result
async def edit_message(self, entity, message_id, message=None, async def edit_message(self, entity, message=None, text=None,
parse_mode='md', link_preview=True): parse_mode='md', link_preview=True):
""" """
Edits the given message ID (to change its contents or disable preview). Edits the given message ID (to change its contents or disable preview).
Args: Args:
entity (`entity`): entity (`entity` | :tl:`Message`):
From which chat to edit the 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.
message_id (`str`): message (`int` | :tl:`Message` | `str`):
The ID of the message (or ``Message`` itself) to be edited. The ID of the message (or :tl:`Message` itself) to be edited.
If the `entity` was a :tl:`Message`, then this message will be
treated as the new text.
message (`str`, optional): text (`str`, optional):
The new text of the message. The new text of the message. Does nothing if the `entity`
was a :tl:`Message`.
parse_mode (`str`, optional): parse_mode (`str`, optional):
Can be 'md' or 'markdown' for markdown-like parsing (default), Can be 'md' or 'markdown' for markdown-like parsing (default),
@ -903,6 +913,21 @@ class TelegramClient(TelegramBareClient):
link_preview (`bool`, optional): link_preview (`bool`, optional):
Should the link preview be shown? Should the link preview be shown?
Examples:
>>> async def main():
... client = await TelegramClient(...).start()
... message = await client.send_message('username', 'hello')
...
... await client.edit_message('username', message, 'hello!')
... # or
... await client.edit_message('username', message.id, 'Hello')
... # or
... await client.edit_message(message, 'Hello!')
...
>>> loop = ...
>>> loop.run_until_complete(main())
Raises: Raises:
``MessageAuthorRequiredError`` if you're not the author of the ``MessageAuthorRequiredError`` if you're not the author of the
message but try editing it anyway. message but try editing it anyway.
@ -913,12 +938,16 @@ class TelegramClient(TelegramBareClient):
Returns: Returns:
The edited :tl:`Message`. The edited :tl:`Message`.
""" """
message, msg_entities =\ if isinstance(entity, Message):
await self._parse_message_text(message, parse_mode) text = message # Shift the parameters to the right
message = entity
entity = entity.to_id
text, msg_entities = await self._parse_message_text(text, parse_mode)
request = EditMessageRequest( request = EditMessageRequest(
peer=await self.get_input_entity(entity), peer=await self.get_input_entity(entity),
id=self._get_message_id(message_id), id=self._get_message_id(message),
message=message, message=text,
no_webpage=not link_preview, no_webpage=not link_preview,
entities=msg_entities entities=msg_entities
) )
@ -967,10 +996,14 @@ class TelegramClient(TelegramBareClient):
async def iter_messages(self, entity, limit=20, offset_date=None, async def iter_messages(self, entity, limit=20, offset_date=None,
offset_id=0, max_id=0, min_id=0, add_offset=0, offset_id=0, max_id=0, min_id=0, add_offset=0,
search=None, filter=None, from_user=None,
batch_size=100, wait_time=None, _total=None): batch_size=100, wait_time=None, _total=None):
""" """
Iterator over the message history for the specified entity. Iterator over the message history for the specified entity.
If either `search`, `filter` or `from_user` are provided,
:tl:`messages.Search` will be used instead of :tl:`messages.getHistory`.
Args: Args:
entity (`entity`): entity (`entity`):
The entity from whom to retrieve the message history. The entity from whom to retrieve the message history.
@ -1002,6 +1035,17 @@ class TelegramClient(TelegramBareClient):
Additional message offset (all of the specified offsets + Additional message offset (all of the specified offsets +
this offset = older messages). 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 user will be returned.
batch_size (`int`): batch_size (`int`):
Messages will be returned in chunks of this size (100 is Messages will be returned in chunks of this size (100 is
the maximum). While it makes no sense to modify this value, the maximum). While it makes no sense to modify this value,
@ -1033,15 +1077,37 @@ class TelegramClient(TelegramBareClient):
""" """
entity = await self.get_input_entity(entity) entity = await self.get_input_entity(entity)
limit = float('inf') if limit is None else int(limit) limit = float('inf') if limit is None else int(limit)
if search is not None or filter or from_user:
request = SearchRequest(
peer=entity,
q=search or '',
filter=filter() if isinstance(filter, type) else filter,
min_date=None,
max_date=offset_date,
offset_id=offset_id,
add_offset=add_offset,
limit=1,
max_id=max_id,
min_id=min_id,
from_id=self.get_input_entity(from_user) if from_user else None
)
else:
request = GetHistoryRequest(
peer=entity,
limit=1,
offset_date=offset_date,
offset_id=offset_id,
min_id=min_id,
max_id=max_id,
add_offset=add_offset,
hash=0
)
if limit == 0: if limit == 0:
if not _total: if not _total:
return return
# No messages, but we still need to know the total message count # No messages, but we still need to know the total message count
result = await self(GetHistoryRequest( result = await self(request)
peer=entity, limit=1,
offset_date=None, offset_id=0, max_id=0, min_id=0,
add_offset=0, hash=0
))
_total[0] = getattr(result, 'count', len(result.messages)) _total[0] = getattr(result, 'count', len(result.messages))
return return
@ -1052,17 +1118,8 @@ class TelegramClient(TelegramBareClient):
batch_size = min(max(batch_size, 1), 100) batch_size = min(max(batch_size, 1), 100)
while have < limit: while have < limit:
# Telegram has a hard limit of 100 # Telegram has a hard limit of 100
real_limit = min(limit - have, batch_size) request.limit = min(limit - have, batch_size)
r = await self(GetHistoryRequest( r = await self(request)
peer=entity,
limit=real_limit,
offset_date=offset_date,
offset_id=offset_id,
max_id=max_id,
min_id=min_id,
add_offset=add_offset,
hash=0
))
if _total: if _total:
_total[0] = getattr(r, 'count', len(r.messages)) _total[0] = getattr(r, 'count', len(r.messages))
@ -1097,11 +1154,15 @@ class TelegramClient(TelegramBareClient):
yield message yield message
have += 1 have += 1
if len(r.messages) < real_limit: if len(r.messages) < request.limit:
break break
offset_id = r.messages[-1].id request.offset_id = r.messages[-1].id
offset_date = r.messages[-1].date if isinstance(request, GetHistoryRequest):
request.offset_date = r.messages[-1].date
else:
request.max_date = r.messages[-1].date
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
async def get_messages(self, *args, **kwargs): async def get_messages(self, *args, **kwargs):
@ -1479,8 +1540,14 @@ class TelegramClient(TelegramBareClient):
entity = await self.get_input_entity(entity) entity = await self.get_input_entity(entity)
reply_to = self._get_message_id(reply_to) reply_to = self._get_message_id(reply_to)
caption, msg_entities =\
await self._parse_message_text(caption, parse_mode) # Not document since it's subject to change.
# Needed when a Message is passed to send_message and it has media.
if 'entities' in kwargs:
msg_entities = kwargs['entities']
else:
caption, msg_entities =\
await self._parse_message_text(caption, parse_mode)
if not isinstance(file, (str, bytes, io.IOBase)): if not isinstance(file, (str, bytes, io.IOBase)):
# The user may pass a Message containing media (or the media, # The user may pass a Message containing media (or the media,
@ -2481,7 +2548,11 @@ class TelegramClient(TelegramBareClient):
If in the end the access hash required for the peer was not found, If in the end the access hash required for the peer was not found,
a ValueError will be raised. a ValueError will be raised.
Returns: Returns:
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`. :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`
or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``.
If you need to get the ID of yourself, you should use
`get_me` with ``input_peer=True``) instead.
""" """
if peer in ('me', 'self'): if peer in ('me', 'self'):
return InputPeerSelf() return InputPeerSelf()

View File

@ -25,7 +25,7 @@ from .tl.types import (
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull, FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull,
InputMediaUploadedPhoto, DocumentAttributeFilename, photos, InputMediaUploadedPhoto, DocumentAttributeFilename, photos,
TopPeer, InputNotifyPeer TopPeer, InputNotifyPeer, InputMessageID
) )
from .tl.types.contacts import ResolvedPeer from .tl.types.contacts import ResolvedPeer
@ -255,8 +255,12 @@ def get_input_media(media, is_photo=False):
it will be treated as an :tl:`InputMediaUploadedPhoto`. it will be treated as an :tl:`InputMediaUploadedPhoto`.
""" """
try: try:
if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'): if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia')
return media return media
elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
return InputMediaPhoto(media)
elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument')
return InputMediaDocument(media)
except AttributeError: except AttributeError:
_raise_cast_fail(media, 'InputMedia') _raise_cast_fail(media, 'InputMedia')
@ -333,6 +337,21 @@ def get_input_media(media, is_photo=False):
_raise_cast_fail(media, 'InputMedia') _raise_cast_fail(media, 'InputMedia')
def get_input_message(message):
"""Similar to :meth:`get_input_peer`, but for input messages."""
try:
if isinstance(message, int): # This case is really common too
return InputMessageID(message)
elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'):
return message
elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'):
return InputMessageID(message.id)
except AttributeError:
pass
_raise_cast_fail(message, 'InputMedia')
def is_image(file): def is_image(file):
""" """
Returns ``True`` if the file extension looks like an image file to Telegram. Returns ``True`` if the file extension looks like an image file to Telegram.

View File

@ -18,7 +18,8 @@ AUTO_CASTS = {
'InputChannel': 'utils.get_input_channel(await client.get_input_entity({}))', 'InputChannel': 'utils.get_input_channel(await client.get_input_entity({}))',
'InputUser': 'utils.get_input_user(await client.get_input_entity({}))', 'InputUser': 'utils.get_input_user(await client.get_input_entity({}))',
'InputMedia': 'utils.get_input_media({})', 'InputMedia': 'utils.get_input_media({})',
'InputPhoto': 'utils.get_input_photo({})' 'InputPhoto': 'utils.get_input_photo({})',
'InputMessage': 'utils.get_input_message({})'
} }
BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128', BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128',