Merge branch 'master' into sync

This commit is contained in:
Lonami Exo 2018-08-01 15:18:44 +02:00
commit 3177e3a956
55 changed files with 2776 additions and 1146 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ docs/
# Generated code # Generated code
telethon/tl/functions/ telethon/tl/functions/
telethon/tl/types/ telethon/tl/types/
telethon/tl/patched/
telethon/tl/alltlobjects.py telethon/tl/alltlobjects.py
telethon/errors/rpcerrorlist.py telethon/errors/rpcerrorlist.py

View File

@ -13,10 +13,12 @@ from .telegrambaseclient import TelegramBaseClient
from .users import UserMethods # Required for everything from .users import UserMethods # Required for everything
from .messageparse import MessageParseMethods # Required for messages from .messageparse import MessageParseMethods # Required for messages
from .uploads import UploadMethods # Required for messages to send files from .uploads import UploadMethods # Required for messages to send files
from .updates import UpdateMethods # Required for buttons (register callbacks)
from .buttons import ButtonMethods # Required for messages to use buttons
from .messages import MessageMethods from .messages import MessageMethods
from .chats import ChatMethods from .chats import ChatMethods
from .dialogs import DialogMethods from .dialogs import DialogMethods
from .downloads import DownloadMethods from .downloads import DownloadMethods
from .auth import AuthMethods from .auth import AuthMethods
from .updates import UpdateMethods from .bots import BotMethods
from .telegramclient import TelegramClient from .telegramclient import TelegramClient

View File

@ -16,7 +16,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
def start( def start(
self, self,
phone=lambda: input('Please enter your phone: '), phone=lambda: input('Please enter your phone (or bot token): '),
password=lambda: getpass.getpass('Please enter your password: '), password=lambda: getpass.getpass('Please enter your password: '),
*, *,
bot_token=None, force_sms=False, code_callback=None, bot_token=None, force_sms=False, code_callback=None,
@ -45,7 +45,8 @@ class AuthMethods(MessageParseMethods, UserMethods):
Args: Args:
phone (`str` | `int` | `callable`): phone (`str` | `int` | `callable`):
The phone (or callable without arguments to get it) The phone (or callable without arguments to get it)
to which the code will be sent. to which the code will be sent. If a bot-token-like
string is given, it will be used as such instead.
password (`callable`, optional): password (`callable`, optional):
The password for 2 Factor Authentication (2FA). The password for 2 Factor Authentication (2FA).
@ -118,14 +119,21 @@ class AuthMethods(MessageParseMethods, UserMethods):
if self.is_user_authorized(): if self.is_user_authorized():
return self return self
if not bot_token:
# Turn the callable into a valid phone number (or bot token)
while callable(phone):
value = phone()
if ':' in value:
# Bot tokens have 'user_id:access_hash' format
bot_token = value
break
phone = utils.parse_phone(value) or phone
if bot_token: if bot_token:
self.sign_in(bot_token=bot_token) self.sign_in(bot_token=bot_token)
return self return self
# Turn the callable into a valid phone number
while callable(phone):
phone = utils.parse_phone(phone()) or phone
me = None me = None
attempts = 0 attempts = 0
two_step_detected = False two_step_detected = False

45
telethon/client/bots.py Normal file
View File

@ -0,0 +1,45 @@
from .users import UserMethods
from ..tl import types, functions, custom
class BotMethods(UserMethods):
def inline_query(self, bot, query, *, offset=None, geo_point=None):
"""
Makes the given inline query to the specified bot
i.e. ``@vote My New Poll`` would be as follows:
>>> client = ...
>>> client.inline_query('vote', 'My New Poll')
Args:
bot (`entity`):
The bot entity to which the inline query should be made.
query (`str`):
The query that should be made to the bot.
offset (`str`, optional):
The string offset to use for the bot.
geo_point (:tl:`GeoPoint`, optional)
The geo point location information to send to the bot
for localised results. Available under some bots.
Returns:
A list of `custom.InlineResult
<telethon.tl.custom.inlineresult.InlineResult>`.
"""
bot = self.get_input_entity(bot)
result = self(functions.messages.GetInlineBotResultsRequest(
bot=bot,
peer=types.InputPeerEmpty(),
query=query,
offset=offset or '',
geo_point=geo_point
))
# TODO Custom InlineResults(UserList) class with more information
return [
custom.InlineResult(self, x, query_id=result.query_id)
for x in result.results
]

View File

@ -0,0 +1,69 @@
from .updates import UpdateMethods
from ..tl import types, custom
from .. import utils, events
class ButtonMethods(UpdateMethods):
def build_reply_markup(self, buttons, inline_only=False):
"""
Builds a :tl`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
the given buttons, or does nothing if either no buttons are
provided or the provided argument is already a reply markup.
This will add any event handlers defined in the
buttons and delete old ones not to call them twice,
so you should probably call this method manually for
serious bots instead re-adding handlers every time you
send a message. Magic can only go so far.
"""
if buttons is None:
return None
try:
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:
return buttons # crc32(b'ReplyMarkup'):
except AttributeError:
pass
if not utils.is_list_like(buttons):
buttons = [[buttons]]
elif not utils.is_list_like(buttons[0]):
buttons = [buttons]
is_inline = False
is_normal = False
rows = []
for row in buttons:
current = []
for button in row:
inline = custom.Button._is_inline(button)
is_inline |= inline
is_normal |= not inline
if isinstance(button, custom.Button):
if button.callback:
self.remove_event_handler(
button.callback, events.CallbackQuery)
self.add_event_handler(
button.callback,
events.CallbackQuery(data=button.data)
)
button = button.button
if button.SUBCLASS_OF_ID == 0xbad74a3:
# 0xbad74a3 == crc32(b'KeyboardButton')
current.append(button)
if current:
rows.append(types.KeyboardButtonRow(current))
if inline_only and is_normal:
raise ValueError('You cannot use non-inline buttons here')
elif is_inline == is_normal and is_normal:
raise ValueError('You cannot mix inline with normal buttons')
elif is_inline:
return types.ReplyInlineMarkup(rows)
elif is_normal:
return types.ReplyKeyboardMarkup(rows)

View File

@ -31,14 +31,14 @@ class ChatMethods(UserMethods):
This has no effect for normal chats or users. This has no effect for normal chats or users.
aggressive (`bool`, optional): aggressive (`bool`, optional):
Aggressively looks for all participants in the chat in Aggressively looks for all participants in the chat.
order to get more than 10,000 members (a hard limit
imposed by Telegram). Note that this might take a long
time (over 5 minutes), but is able to return over 90,000
participants on groups with 100,000 members.
This has no effect for groups or channels with less than This is useful for channels since 20 July 2018,
10,000 members, or if a ``filter`` is given. Telegram added a server-side limit where only the
first 200 members can be retrieved. With this flag
set, more than 200 will be often be retrieved.
This has no effect if a ``filter`` is given.
_total (`list`, optional): _total (`list`, optional):
A single-item list to pass the total parameter by reference. A single-item list to pass the total parameter by reference.
@ -73,20 +73,16 @@ class ChatMethods(UserMethods):
limit = float('inf') if limit is None else int(limit) limit = float('inf') if limit is None else int(limit)
if isinstance(entity, types.InputPeerChannel): if isinstance(entity, types.InputPeerChannel):
if _total or (aggressive and not filter): if _total:
total = (self(functions.channels.GetFullChannelRequest( _total[0] = (self(
entity functions.channels.GetFullChannelRequest(entity)
))).full_chat.participants_count )).full_chat.participants_count
if _total:
_total[0] = total
else:
total = 0
if limit == 0: if limit == 0:
return return
seen = set() seen = set()
if total > 10000 and aggressive and not filter: if aggressive and not filter:
requests = [functions.channels.GetParticipantsRequest( requests = [functions.channels.GetParticipantsRequest(
channel=entity, channel=entity,
filter=types.ChannelParticipantsSearch(search + chr(x)), filter=types.ChannelParticipantsSearch(search + chr(x)),

View File

@ -77,10 +77,14 @@ class DialogMethods(UserMethods):
if _total: if _total:
_total[0] = getattr(r, 'count', len(r.dialogs)) _total[0] = getattr(r, 'count', len(r.dialogs))
entities = {utils.get_peer_id(x): x entities = {utils.get_peer_id(x): x
for x in itertools.chain(r.users, r.chats)} for x in itertools.chain(r.users, r.chats)}
messages = {m.id: custom.Message(self, m, entities, None)
for m in r.messages} messages = {}
for m in r.messages:
m._finish_init(self, entities, None)
messages[m.id] = m
# Happens when there are pinned dialogs # Happens when there are pinned dialogs
if len(r.dialogs) > limit: if len(r.dialogs) > limit:

View File

@ -3,6 +3,7 @@ import io
import logging import logging
import os import os
import pathlib import pathlib
import urllib.request
from .users import UserMethods from .users import UserMethods
from .. import utils, helpers, errors from .. import utils, helpers, errors
@ -123,6 +124,9 @@ class DownloadMethods(UserMethods):
date = datetime.datetime.now() date = datetime.datetime.now()
media = message media = message
if isinstance(media, str):
media = utils.resolve_bot_file_id(media)
if isinstance(media, types.MessageMediaWebPage): if isinstance(media, types.MessageMediaWebPage):
if isinstance(media.webpage, types.WebPage): if isinstance(media.webpage, types.WebPage):
media = media.webpage.document or media.webpage.photo media = media.webpage.document or media.webpage.photo
@ -140,6 +144,10 @@ class DownloadMethods(UserMethods):
return self._download_contact( return self._download_contact(
media, file media, file
) )
elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)):
return self._download_web_document(
media, file, progress_callback
)
def download_file( def download_file(
self, input_location, file=None, *, part_size_kb=None, self, input_location, file=None, *, part_size_kb=None,
@ -200,10 +208,27 @@ class DownloadMethods(UserMethods):
else: else:
f = file f = file
# The used sender will change if ``FileMigrateError`` occurs dc_id, input_location = utils.get_input_location(input_location)
sender = self._sender exported = dc_id and self.session.dc_id != dc_id
exported = False if exported:
input_location = utils.get_input_location(input_location) try:
sender = self._borrow_exported_sender(dc_id)
except errors.DcIdInvalidError:
# Can't export a sender for the ID we are currently in
config = self(functions.help.GetConfigRequest())
for option in config.dc_options:
if option.ip_address == self.session.server_address:
self.session.set_dc(
option.id, option.ip_address, option.port)
self.session.save()
break
# TODO Figure out why the session may have the wrong DC ID
sender = self._sender
exported = False
else:
# The used sender will also change if ``FileMigrateError`` occurs
sender = self._sender
__log__.info('Downloading file in chunks of %d bytes', part_size) __log__.info('Downloading file in chunks of %d bytes', part_size)
try: try:
@ -281,19 +306,12 @@ class DownloadMethods(UserMethods):
progress_callback=progress_callback) progress_callback=progress_callback)
return file return file
def _download_document( @staticmethod
self, document, file, date, progress_callback): def _get_kind_and_names(attributes):
"""Specialized version of .download_media() for documents.""" """Gets kind and possible names for :tl:`DocumentAttribute`."""
if isinstance(document, types.MessageMediaDocument):
document = document.document
if not isinstance(document, types.Document):
return
file_size = document.size
kind = 'document' kind = 'document'
possible_names = [] possible_names = []
for attr in document.attributes: for attr in attributes:
if isinstance(attr, types.DocumentAttributeFilename): if isinstance(attr, types.DocumentAttributeFilename):
possible_names.insert(0, attr.file_name) possible_names.insert(0, attr.file_name)
@ -310,13 +328,24 @@ class DownloadMethods(UserMethods):
elif attr.voice: elif attr.voice:
kind = 'voice' kind = 'voice'
return kind, possible_names
def _download_document(
self, document, file, date, progress_callback):
"""Specialized version of .download_media() for documents."""
if isinstance(document, types.MessageMediaDocument):
document = document.document
if not isinstance(document, types.Document):
return
kind, possible_names = self._get_kind_and_names(document.attributes)
file = self._get_proper_filename( file = self._get_proper_filename(
file, kind, utils.get_extension(document), file, kind, utils.get_extension(document),
date=date, possible_names=possible_names date=date, possible_names=possible_names
) )
self.download_file( self.download_file(
document, file, file_size=file_size, document, file, file_size=document.size,
progress_callback=progress_callback) progress_callback=progress_callback)
return file return file
@ -356,6 +385,37 @@ class DownloadMethods(UserMethods):
return file return file
@classmethod
async def _download_web_document(cls, web, file, progress_callback):
"""
Specialized version of .download_media() for web documents.
"""
# TODO Better way to get opened handles of files and auto-close
if isinstance(file, str):
kind, possible_names = cls._get_kind_and_names(web.attributes)
file = cls._get_proper_filename(
file, kind, utils.get_extension(web),
possible_names=possible_names
)
f = open(file, 'wb')
else:
f = file
session = urllib.request.urlopen(web.url)
try:
# TODO Use progress_callback; get content length from response
# int(session.info().getheaders('Content-Length')[0])
# https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319
while True:
chunk = await session.read(128 * 1024)
if not chunk:
break
f.write(chunk)
finally:
session.close()
if isinstance(file, str):
f.close()
@staticmethod @staticmethod
def _get_proper_filename(file, kind, extension, def _get_proper_filename(file, kind, extension,
date=None, possible_names=None): date=None, possible_names=None):

View File

@ -133,6 +133,7 @@ class MessageParseMethods(UserMethods):
break break
if found: if found:
return custom.Message(self, found, entities, input_chat) found._finish_init(self, entities, input_chat)
return found
# endregion # endregion

View File

@ -6,13 +6,14 @@ from collections import UserList
from .messageparse import MessageParseMethods from .messageparse import MessageParseMethods
from .uploads import UploadMethods from .uploads import UploadMethods
from .buttons import ButtonMethods
from .. import utils from .. import utils
from ..tl import types, functions, custom from ..tl import types, functions, custom
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
class MessageMethods(UploadMethods, MessageParseMethods): class MessageMethods(UploadMethods, ButtonMethods, MessageParseMethods):
# region Public methods # region Public methods
@ -33,6 +34,14 @@ class MessageMethods(UploadMethods, MessageParseMethods):
entity (`entity`): entity (`entity`):
The entity from whom to retrieve the message history. 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.
limit (`int` | `None`, optional): limit (`int` | `None`, optional):
Number of messages to be retrieved. Due to limitations with Number of messages to be retrieved. Due to limitations with
the API retrieving more than 3000 messages will take longer the API retrieving more than 3000 messages will take longer
@ -100,6 +109,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
instead of being `max_id` as well since messages are returned instead of being `max_id` as well since messages are returned
in ascending order. in ascending order.
You cannot use this if both `entity` and `ids` are ``None``.
_total (`list`, optional): _total (`list`, optional):
A single-item list to pass the total parameter by reference. A single-item list to pass the total parameter by reference.
@ -113,9 +124,9 @@ class MessageMethods(UploadMethods, MessageParseMethods):
an higher limit, so you're free to set the ``batch_size`` that an higher limit, so you're free to set the ``batch_size`` that
you think may be good. you think may be good.
""" """
# It's possible to get messages by ID without their entity, so only # Note that entity being ``None`` is intended to get messages by
# fetch the input version if we're not using IDs or if it was given. # ID under no specific chat, and also to request a global search.
if not ids or entity: if entity:
entity = self.get_input_entity(entity) entity = self.get_input_entity(entity)
if ids: if ids:
@ -154,7 +165,19 @@ class MessageMethods(UploadMethods, MessageParseMethods):
from_id = None from_id = None
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: if not entity:
if reverse:
raise ValueError('Cannot reverse global search')
reverse = None
request = functions.messages.SearchGlobalRequest(
q=search or '',
offset_date=offset_date,
offset_peer=types.InputPeerEmpty(),
offset_id=offset_id,
limit=1
)
elif search is not None or filter or from_user:
if filter is None: if filter is None:
filter = types.InputMessagesFilterEmpty() filter = types.InputMessagesFilterEmpty()
request = functions.messages.SearchRequest( request = functions.messages.SearchRequest(
@ -239,7 +262,9 @@ class MessageMethods(UploadMethods, MessageParseMethods):
or from_id and message.from_id != from_id): or from_id and message.from_id != from_id):
continue continue
if reverse: if reverse is None:
pass
elif reverse:
if message.id <= last_id or message.id >= max_id: if message.id <= last_id or message.id >= max_id:
return return
else: else:
@ -252,7 +277,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
# IDs are returned in descending order (or asc if reverse). # IDs are returned in descending order (or asc if reverse).
last_id = message.id last_id = message.id
yield (custom.Message(self, message, entities, entity)) message._finish_init(self, entities, entity)
yield (message)
have += 1 have += 1
if len(r.messages) < request.limit: if len(r.messages) < request.limit:
@ -277,12 +303,15 @@ class MessageMethods(UploadMethods, MessageParseMethods):
break break
else: else:
request.offset_id = last_message.id request.offset_id = last_message.id
if isinstance(request, functions.messages.GetHistoryRequest): if isinstance(request, functions.messages.SearchRequest):
request.offset_date = last_message.date
else:
request.max_date = last_message.date request.max_date = last_message.date
else:
# getHistory and searchGlobal call it offset_date
request.offset_date = last_message.date
if reverse: if isinstance(request, functions.messages.SearchGlobalRequest):
request.offset_peer = last_message.input_chat
elif reverse:
# We want to skip the one we already have # We want to skip the one we already have
request.add_offset -= 1 request.add_offset -= 1
@ -330,7 +359,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
def send_message( def send_message(
self, entity, message='', *, reply_to=None, self, entity, message='', *, reply_to=None,
parse_mode=utils.Default, link_preview=True, file=None, parse_mode=utils.Default, link_preview=True, file=None,
force_document=False, clear_draft=False): force_document=False, clear_draft=False, buttons=None,
silent=None):
""" """
Sends the given message to the specified entity (user/chat/channel). Sends the given message to the specified entity (user/chat/channel).
@ -379,13 +409,26 @@ class MessageMethods(UploadMethods, MessageParseMethods):
Whether the existing draft should be cleared or not. Whether the existing draft should be cleared or not.
Has no effect when sending a file. Has no effect when sending a file.
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.
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.
Returns: Returns:
The sent `telethon.tl.custom.message.Message`. The sent `custom.Message <telethon.tl.custom.message.Message>`.
""" """
if file is not None: if file is not None:
return self.send_file( return self.send_file(
entity, file, caption=message, reply_to=reply_to, entity, file, caption=message, reply_to=reply_to,
parse_mode=parse_mode, force_document=force_document parse_mode=parse_mode, force_document=force_document,
buttons=buttons
) )
elif not message: elif not message:
raise ValueError( raise ValueError(
@ -414,12 +457,20 @@ class MessageMethods(UploadMethods, MessageParseMethods):
else: else:
reply_id = None reply_id = None
if buttons is None:
markup = message.reply_markup
else:
markup = self.build_reply_markup(buttons)
if silent is None:
silent = message.silent
request = functions.messages.SendMessageRequest( request = functions.messages.SendMessageRequest(
peer=entity, peer=entity,
message=message.message or '', message=message.message or '',
silent=message.silent, silent=silent,
reply_to_msg_id=reply_id, reply_to_msg_id=reply_id,
reply_markup=message.reply_markup, reply_markup=markup,
entities=message.entities, entities=message.entities,
clear_draft=clear_draft, clear_draft=clear_draft,
no_webpage=not isinstance( no_webpage=not isinstance(
@ -435,13 +486,15 @@ class MessageMethods(UploadMethods, MessageParseMethods):
entities=msg_ent, entities=msg_ent,
no_webpage=not link_preview, no_webpage=not link_preview,
reply_to_msg_id=utils.get_message_id(reply_to), reply_to_msg_id=utils.get_message_id(reply_to),
clear_draft=clear_draft clear_draft=clear_draft,
silent=silent,
reply_markup=self.build_reply_markup(buttons)
) )
result = self(request) result = self(request)
if isinstance(result, types.UpdateShortSentMessage): if isinstance(result, types.UpdateShortSentMessage):
to_id, cls = utils.resolve_id(utils.get_peer_id(entity)) to_id, cls = utils.resolve_id(utils.get_peer_id(entity))
return custom.Message(self, types.Message( message = types.Message(
id=result.id, id=result.id,
to_id=cls(to_id), to_id=cls(to_id),
message=message, message=message,
@ -449,11 +502,14 @@ class MessageMethods(UploadMethods, MessageParseMethods):
out=result.out, out=result.out,
media=result.media, media=result.media,
entities=result.entities entities=result.entities
), {}, input_chat=entity) )
message._finish_init(self, {}, entity)
return message
return self._get_response_message(request, result, entity) return self._get_response_message(request, result, entity)
def forward_messages(self, entity, messages, from_peer=None): def forward_messages(self, entity, messages, from_peer=None,
*, silent=None):
""" """
Forwards the given message(s) to the specified entity. Forwards the given message(s) to the specified entity.
@ -469,6 +525,11 @@ class MessageMethods(UploadMethods, MessageParseMethods):
of the ``Message`` class, this *must* be specified in of the ``Message`` class, this *must* be specified in
order for the forward to work. order for the forward to work.
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.
Returns: Returns:
The list of forwarded `telethon.tl.custom.message.Message`, The list of forwarded `telethon.tl.custom.message.Message`,
or a single one if a list wasn't provided as input. or a single one if a list wasn't provided as input.
@ -496,7 +557,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
req = functions.messages.ForwardMessagesRequest( req = functions.messages.ForwardMessagesRequest(
from_peer=from_peer, from_peer=from_peer,
id=[m if isinstance(m, int) else m.id for m in messages], id=[m if isinstance(m, int) else m.id for m in messages],
to_peer=entity to_peer=entity,
silent=silent
) )
result = self(req) result = self(req)
if isinstance(result, (types.Updates, types.UpdatesCombined)): if isinstance(result, (types.Updates, types.UpdatesCombined)):
@ -512,15 +574,16 @@ class MessageMethods(UploadMethods, MessageParseMethods):
random_to_id[update.random_id] = update.id random_to_id[update.random_id] = update.id
elif isinstance(update, ( elif isinstance(update, (
types.UpdateNewMessage, types.UpdateNewChannelMessage)): types.UpdateNewMessage, types.UpdateNewChannelMessage)):
id_to_message[update.message.id] = custom.Message( update.message._finish_init(self, entities, entity)
self, update.message, entities, input_chat=entity) id_to_message[update.message.id] = update.message
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
def edit_message( def edit_message(
self, entity, message=None, text=None, self, entity, message=None, text=None,
*, parse_mode=utils.Default, link_preview=True, file=None): *, parse_mode=utils.Default, link_preview=True, file=None,
buttons=None):
""" """
Edits the given message ID (to change its contents or disable preview). Edits the given message ID (to change its contents or disable preview).
@ -551,6 +614,13 @@ class MessageMethods(UploadMethods, MessageParseMethods):
The file object that should replace the existing media The file object that should replace the existing media
in the message. in the message.
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.
Examples: Examples:
>>> client = ... >>> client = ...
@ -586,7 +656,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
message=text, message=text,
no_webpage=not link_preview, no_webpage=not link_preview,
entities=msg_entities, entities=msg_entities,
media=media media=media,
reply_markup=self.build_reply_markup(buttons)
) )
msg = self._get_response_message(request, self(request), entity) msg = self._get_response_message(request, self(request), entity)
self._cache_media(msg, file, file_handle) self._cache_media(msg, file, file_handle)
@ -703,7 +774,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
total[0] = len(ids) total[0] = len(ids)
from_id = None # By default, no need to validate from_id from_id = None # By default, no need to validate from_id
if isinstance(entity, types.InputPeerChannel): if isinstance(entity, (types.InputChannel, types.InputPeerChannel)):
r = self(functions.channels.GetMessagesRequest(entity, ids)) r = self(functions.channels.GetMessagesRequest(entity, ids))
else: else:
r = self(functions.messages.GetMessagesRequest(ids)) r = self(functions.messages.GetMessagesRequest(ids))
@ -729,6 +800,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
from_id and utils.get_peer_id(message.to_id) != from_id): from_id and utils.get_peer_id(message.to_id) != from_id):
yield (None) yield (None)
else: else:
yield (custom.Message(self, message, entities, entity)) message._finish_init(self, entities, entity)
yield (message)
# endregion # endregion

View File

@ -1,4 +1,7 @@
import abc import abc
import asyncio
import collections
import inspect
import logging import logging
import platform import platform
import queue import queue
@ -219,6 +222,9 @@ class TelegramBaseClient(abc.ABC):
auto_reconnect_callback=self._handle_auto_reconnect auto_reconnect_callback=self._handle_auto_reconnect
) )
# Remember flood-waited requests to avoid making them again
self._flood_waited_requests = {}
# Cache ``{dc_id: (n, MTProtoSender)}`` for all borrowed senders, # Cache ``{dc_id: (n, MTProtoSender)}`` for all borrowed senders,
# being ``n`` the amount of borrows a given sender has; once ``n`` # being ``n`` the amount of borrows a given sender has; once ``n``
# reaches ``0`` it should be disconnected and removed. # reaches ``0`` it should be disconnected and removed.
@ -252,6 +258,11 @@ class TelegramBaseClient(abc.ABC):
self._events_pending_resolve = [] self._events_pending_resolve = []
self._event_resolve_lock = threading.Lock() self._event_resolve_lock = threading.Lock()
# Keep track of how many event builders there are for
# each type {type: count}. If there's at least one then
# the event will be built, and the same event be reused.
self._event_builders_count = collections.defaultdict(int)
# Default parse mode # Default parse mode
self._parse_mode = markdown self._parse_mode = markdown
@ -409,6 +420,9 @@ class TelegramBaseClient(abc.ABC):
if not sender: if not sender:
sender = self._create_exported_sender(dc_id) sender = self._create_exported_sender(dc_id)
sender.dc_id = dc_id sender.dc_id = dc_id
elif not n:
dc = self._get_dc(dc_id)
sender.connect(dc.ip_address, dc.port)
self._borrowed_senders[dc_id] = (n + 1, sender) self._borrowed_senders[dc_id] = (n + 1, sender)
@ -423,12 +437,10 @@ class TelegramBaseClient(abc.ABC):
dc_id = sender.dc_id dc_id = sender.dc_id
n, _ = self._borrowed_senders[dc_id] n, _ = self._borrowed_senders[dc_id]
n -= 1 n -= 1
if n > 0: self._borrowed_senders[dc_id] = (n, sender)
self._borrowed_senders[dc_id] = (n, sender) if not n:
else:
__log__.info('Disconnecting borrowed sender for DC %d', dc_id) __log__.info('Disconnecting borrowed sender for DC %d', dc_id)
sender.disconnect() sender.disconnect()
del self._borrowed_senders[dc_id]
def _get_cdn_client(self, cdn_redirect): def _get_cdn_client(self, cdn_redirect):
"""Similar to ._borrow_exported_client, but for CDNs""" """Similar to ._borrow_exported_client, but for CDNs"""

View File

@ -1,13 +1,13 @@
from . import ( from . import (
UpdateMethods, AuthMethods, DownloadMethods, DialogMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, BotMethods,
ChatMethods, MessageMethods, UploadMethods, MessageParseMethods, MessageMethods, ButtonMethods, UpdateMethods, UploadMethods,
UserMethods MessageParseMethods, UserMethods
) )
class TelegramClient( class TelegramClient(
UpdateMethods, AuthMethods, DownloadMethods, DialogMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, BotMethods,
ChatMethods, MessageMethods, UploadMethods, MessageParseMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods,
UserMethods MessageParseMethods, UserMethods
): ):
pass pass

View File

@ -79,6 +79,7 @@ class UpdateMethods(UserMethods):
event = events.Raw() event = events.Raw()
self._events_pending_resolve.append(event) self._events_pending_resolve.append(event)
self._event_builders_count[type(event)] += 1
self._event_builders.append((event, callback)) self._event_builders.append((event, callback))
def remove_event_handler(self, callback, event=None): def remove_event_handler(self, callback, event=None):
@ -97,6 +98,11 @@ class UpdateMethods(UserMethods):
i -= 1 i -= 1
ev, cb = self._event_builders[i] ev, cb = self._event_builders[i]
if cb == callback and (not event or isinstance(ev, event)): if cb == callback and (not event or isinstance(ev, event)):
type_ev = type(ev)
self._event_builders_count[type_ev] -= 1
if not self._event_builders_count[type_ev]:
del self._event_builders_count[type_ev]
del self._event_builders[i] del self._event_builders[i]
found += 1 found += 1
@ -164,7 +170,7 @@ class UpdateMethods(UserMethods):
for u in update.updates: for u in update.updates:
u._entities = entities u._entities = entities
self._handle_update(u) self._handle_update(u)
if isinstance(update, types.UpdateShort): elif isinstance(update, types.UpdateShort):
self._handle_update(update.update) self._handle_update(update.update)
else: else:
update._entities = getattr(update, '_entities', {}) update._entities = getattr(update, '_entities', {})
@ -177,7 +183,7 @@ class UpdateMethods(UserMethods):
syncio.create_task(self._dispatch_queue_updates) syncio.create_task(self._dispatch_queue_updates)
need_diff = False need_diff = False
if hasattr(update, 'pts'): if hasattr(update, 'pts') and update.pts is not None:
if self._state.pts and (update.pts - self._state.pts) > 1: if self._state.pts and (update.pts - self._state.pts) > 1:
need_diff = True need_diff = True
self._state.pts = update.pts self._state.pts = update.pts
@ -197,7 +203,7 @@ class UpdateMethods(UserMethods):
continue # We actually just want to act upon timeout continue # We actually just want to act upon timeout
except concurrent.futures.TimeoutError: except concurrent.futures.TimeoutError:
pass pass
except: except Exception as e:
continue # Any disconnected exception should be ignored continue # Any disconnected exception should be ignored
# We also don't really care about their result. # We also don't really care about their result.
@ -241,28 +247,35 @@ class UpdateMethods(UserMethods):
self._events_pending_resolve.clear() self._events_pending_resolve.clear()
for builder, callback in self._event_builders: # TODO We can improve this further
event = builder.build(update) # If we had a way to get all event builders for
if event: # a type instead looping over them all always.
if hasattr(event, '_set_client'): built = {builder: builder.build(update)
event._set_client(self) for builder in self._event_builders_count}
else:
event._client = self
event.original_update = update for builder, callback in self._event_builders:
try: event = built[type(builder)]
callback(event) if not event or not builder.filter(event):
except events.StopPropagation: continue
__log__.debug(
"Event handler '{}' stopped chain of " if hasattr(event, '_set_client'):
"propagation for event {}." event._set_client(self)
.format(callback.__name__, else:
type(event).__name__) event._client = self
)
break event.original_update = update
except: try:
__log__.exception('Unhandled exception on {}' callback(event)
.format(callback.__name__)) except events.StopPropagation:
name = getattr(callback, '__name__', repr(callback))
__log__.debug(
'Event handler "%s" stopped chain of propagation '
'for event %s.', name, type(event).__name__
)
break
except Exception:
name = getattr(callback, '__name__', repr(callback))
__log__.exception('Unhandled exception on %s', name)
def _handle_auto_reconnect(self): def _handle_auto_reconnect(self):
# Upon reconnection, we want to send getState # Upon reconnection, we want to send getState

View File

@ -9,20 +9,14 @@ from mimetypes import guess_type
from .messageparse import MessageParseMethods from .messageparse import MessageParseMethods
from .users import UserMethods from .users import UserMethods
from .buttons import ButtonMethods
from .. import utils, helpers from .. import utils, helpers
from ..tl import types, functions, custom from ..tl import types, functions, custom
try:
import hachoir
import hachoir.metadata
import hachoir.parser
except ImportError:
hachoir = None
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
class UploadMethods(MessageParseMethods, UserMethods): class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods):
# region Public methods # region Public methods
@ -30,7 +24,8 @@ class UploadMethods(MessageParseMethods, UserMethods):
self, entity, file, *, caption='', force_document=False, self, entity, file, *, caption='', force_document=False,
progress_callback=None, reply_to=None, attributes=None, progress_callback=None, reply_to=None, attributes=None,
thumb=None, allow_cache=True, parse_mode=utils.Default, thumb=None, allow_cache=True, parse_mode=utils.Default,
voice_note=False, video_note=False, **kwargs): voice_note=False, video_note=False, buttons=None, silent=None,
**kwargs):
""" """
Sends a file to the specified entity. Sends a file to the specified entity.
@ -46,7 +41,8 @@ class UploadMethods(MessageParseMethods, UserMethods):
Furthermore the file may be any media (a message, document, Furthermore the file may be any media (a message, document,
photo or similar) so that it can be resent without the need photo or similar) so that it can be resent without the need
to download and re-upload it again. to download and re-upload it again. Bot API ``file_id``
format is also supported.
If a list or similar is provided, the files in it will be If a list or similar is provided, the files in it will be
sent as an album in the order in which they appear, sliced sent as an album in the order in which they appear, sliced
@ -98,6 +94,18 @@ class UploadMethods(MessageParseMethods, UserMethods):
Set `allow_cache` to ``False`` if you sent the same file Set `allow_cache` to ``False`` if you sent the same file
without this setting before for it to work. without this setting before for it to work.
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.
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.
Notes: Notes:
If the ``hachoir3`` package (``hachoir`` module) is installed, If the ``hachoir3`` package (``hachoir`` module) is installed,
it will be used to determine metadata from audio and video files. it will be used to determine metadata from audio and video files.
@ -126,7 +134,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
result += self._send_album( result += self._send_album(
entity, images[:10], caption=caption, entity, images[:10], caption=caption,
progress_callback=progress_callback, reply_to=reply_to, progress_callback=progress_callback, reply_to=reply_to,
parse_mode=parse_mode parse_mode=parse_mode, silent=silent
) )
images = images[10:] images = images[10:]
@ -136,7 +144,8 @@ class UploadMethods(MessageParseMethods, UserMethods):
caption=caption, force_document=force_document, caption=caption, force_document=force_document,
progress_callback=progress_callback, reply_to=reply_to, progress_callback=progress_callback, reply_to=reply_to,
attributes=attributes, thumb=thumb, voice_note=voice_note, attributes=attributes, thumb=thumb, voice_note=voice_note,
video_note=video_note, **kwargs video_note=video_note, buttons=buttons, silent=silent,
**kwargs
)) ))
return result return result
@ -159,9 +168,10 @@ class UploadMethods(MessageParseMethods, UserMethods):
voice_note=voice_note, video_note=video_note voice_note=voice_note, video_note=video_note
) )
markup = self.build_reply_markup(buttons)
request = functions.messages.SendMediaRequest( request = functions.messages.SendMediaRequest(
entity, media, reply_to_msg_id=reply_to, message=caption, entity, media, reply_to_msg_id=reply_to, message=caption,
entities=msg_entities entities=msg_entities, reply_markup=markup, silent=silent
) )
msg = self._get_response_message(request, self(request), entity) msg = self._get_response_message(request, self(request), entity)
self._cache_media(msg, file, file_handle, force_document=force_document) self._cache_media(msg, file, file_handle, force_document=force_document)
@ -170,7 +180,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
def _send_album(self, entity, files, caption='', def _send_album(self, entity, files, caption='',
progress_callback=None, reply_to=None, progress_callback=None, reply_to=None,
parse_mode=utils.Default): parse_mode=utils.Default, silent=None):
"""Specialized version of .send_file for albums""" """Specialized version of .send_file for albums"""
# We don't care if the user wants to avoid cache, we will use it # We don't care if the user wants to avoid cache, we will use it
# anyway. Why? The cached version will be exactly the same thing # anyway. Why? The cached version will be exactly the same thing
@ -208,12 +218,15 @@ class UploadMethods(MessageParseMethods, UserMethods):
caption, msg_entities = captions.pop() caption, msg_entities = captions.pop()
else: else:
caption, msg_entities = '', None caption, msg_entities = '', None
media.append(types.InputSingleMedia(types.InputMediaPhoto(fh), message=caption, media.append(types.InputSingleMedia(
entities=msg_entities)) types.InputMediaPhoto(fh),
message=caption,
entities=msg_entities
))
# Now we can construct the multi-media request # Now we can construct the multi-media request
result = self(functions.messages.SendMultiMediaRequest( result = self(functions.messages.SendMultiMediaRequest(
entity, reply_to_msg_id=reply_to, multi_media=media entity, reply_to_msg_id=reply_to, multi_media=media, silent=silent
)) ))
return [ return [
self._get_response_message(update.id, result, entity) self._get_response_message(update.id, result, entity)
@ -262,7 +275,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
Returns: Returns:
:tl:`InputFileBig` if the file size is larger than 10MB, :tl:`InputFileBig` if the file size is larger than 10MB,
`telethon.tl.custom.input_sized_file.InputSizedFile` `telethon.tl.custom.inputsizedfile.InputSizedFile`
(subclass of :tl:`InputFile`) otherwise. (subclass of :tl:`InputFile`) otherwise.
""" """
if isinstance(file, (types.InputFile, types.InputFileBig)): if isinstance(file, (types.InputFile, types.InputFileBig)):
@ -377,10 +390,15 @@ class UploadMethods(MessageParseMethods, UserMethods):
return None, None # Can't turn whatever was given into media return None, None # Can't turn whatever was given into media
media = None media = None
file_handle = None
as_image = utils.is_image(file) and not force_document as_image = utils.is_image(file) and not force_document
use_cache = types.InputPhoto if as_image else types.InputDocument use_cache = types.InputPhoto if as_image else types.InputDocument
if isinstance(file, str) and re.match('https?://', file): if not isinstance(file, str):
file_handle = None file_handle = self.upload_file(
file, progress_callback=progress_callback,
use_cache=use_cache if allow_cache else None
)
elif re.match('https?://', file):
if as_image: if as_image:
media = types.InputMediaPhotoExternal(file) media = types.InputMediaPhotoExternal(file)
elif not force_document and utils.is_gif(file): elif not force_document and utils.is_gif(file):
@ -388,10 +406,9 @@ class UploadMethods(MessageParseMethods, UserMethods):
else: else:
media = types.InputMediaDocumentExternal(file) media = types.InputMediaDocumentExternal(file)
else: else:
file_handle = self.upload_file( bot_file = utils.resolve_bot_file_id(file)
file, progress_callback=progress_callback, if bot_file:
use_cache=use_cache if allow_cache else None media = utils.get_input_media(bot_file)
)
if media: if media:
pass # Already have media, don't check the rest pass # Already have media, don't check the rest
@ -404,74 +421,13 @@ class UploadMethods(MessageParseMethods, UserMethods):
elif as_image: elif as_image:
media = types.InputMediaUploadedPhoto(file_handle) media = types.InputMediaUploadedPhoto(file_handle)
else: else:
mime_type = None attributes, mime_type = utils.get_attributes(
if isinstance(file, str): file,
# Determine mime-type and attributes attributes=attributes,
# Take the first element by using [0] since it returns a tuple force_document=force_document,
mime_type = guess_type(file)[0] voice_note=voice_note,
attr_dict = { video_note=video_note
types.DocumentAttributeFilename: )
types.DocumentAttributeFilename(
os.path.basename(file))
}
if utils.is_audio(file) and hachoir:
with hachoir.parser.createParser(file) as parser:
m = hachoir.metadata.extractMetadata(parser)
attr_dict[types.DocumentAttributeAudio] = \
types.DocumentAttributeAudio(
voice=voice_note,
title=m.get('title') if m.has(
'title') else None,
performer=m.get('author') if m.has(
'author') else None,
duration=int(m.get('duration').seconds
if m.has('duration') else 0)
)
if not force_document and utils.is_video(file):
if hachoir:
with hachoir.parser.createParser(file) as parser:
m = hachoir.metadata.extractMetadata(parser)
doc = types.DocumentAttributeVideo(
round_message=video_note,
w=m.get('width') if m.has('width') else 0,
h=m.get('height') if m.has('height') else 0,
duration=int(m.get('duration').seconds
if m.has('duration') else 0)
)
else:
doc = types.DocumentAttributeVideo(
0, 1, 1, round_message=video_note)
attr_dict[types.DocumentAttributeVideo] = doc
else:
attr_dict = {
types.DocumentAttributeFilename:
types.DocumentAttributeFilename(
os.path.basename(
getattr(file, 'name',
None) or 'unnamed'))
}
if voice_note:
if types.DocumentAttributeAudio in attr_dict:
attr_dict[types.DocumentAttributeAudio].voice = True
else:
attr_dict[types.DocumentAttributeAudio] = \
types.DocumentAttributeAudio(0, voice=True)
# Now override the attributes if any. As we have a dict of
# {cls: instance}, we can override any class with the list
# of attributes provided by the user easily.
if attributes:
for a in attributes:
attr_dict[type(a)] = a
# Ensure we have a mime type, any; but it cannot be None
# 'The "octet-stream" subtype is used to indicate that a body
# contains arbitrary binary data.'
if not mime_type:
mime_type = 'application/octet-stream'
input_kw = {} input_kw = {}
if thumb: if thumb:
@ -480,7 +436,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
media = types.InputMediaUploadedDocument( media = types.InputMediaUploadedDocument(
file=file_handle, file=file_handle,
mime_type=mime_type, mime_type=mime_type,
attributes=list(attr_dict.values()), attributes=attributes,
**input_kw **input_kw
) )
return file_handle, media return file_handle, media

View File

@ -18,6 +18,20 @@ class UserMethods(TelegramBaseClient):
raise _NOT_A_REQUEST raise _NOT_A_REQUEST
r.resolve(self, utils) r.resolve(self, utils)
# Avoid making the request if it's already in a flood wait
if r.CONSTRUCTOR_ID in self._flood_waited_requests:
due = self._flood_waited_requests[r.CONSTRUCTOR_ID]
diff = round(due - time.time())
if diff <= 3: # Flood waits below 3 seconds are "ignored"
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
elif diff <= self.flood_sleep_threshold:
__log__.info('Sleeping early for %ds on flood wait', diff)
time.sleep(diff)
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
else:
raise errors.FloodWaitError(capture=diff)
request_index = 0
self._last_request = time.time() self._last_request = time.time()
for _ in range(self._request_retries): for _ in range(self._request_retries):
try: try:
@ -28,6 +42,7 @@ class UserMethods(TelegramBaseClient):
result = f.result() result = f.result()
self.session.process_entities(result) self.session.process_entities(result)
results.append(result) results.append(result)
request_index += 1
return results return results
else: else:
result = future.result() result = future.result()
@ -37,6 +52,12 @@ class UserMethods(TelegramBaseClient):
__log__.warning('Telegram is having internal issues %s: %s', __log__.warning('Telegram is having internal issues %s: %s',
e.__class__.__name__, e) e.__class__.__name__, e)
except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e: except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e:
if utils.is_list_like(request):
request = request[request_index]
self._flood_waited_requests\
[request.CONSTRUCTOR_ID] = time.time() + e.seconds
if e.seconds <= self.flood_sleep_threshold: if e.seconds <= self.flood_sleep_threshold:
__log__.info('Sleeping for %ds on flood wait', e.seconds) __log__.info('Sleeping for %ds on flood wait', e.seconds)
time.sleep(e.seconds) time.sleep(e.seconds)

View File

@ -1,13 +1,29 @@
""" """
AES IGE implementation in Python. This module may use libssl if available. AES IGE implementation in Python.
If available, cryptg will be used instead, otherwise
if available, libssl will be used instead, otherwise
the Python implementation will be used.
""" """
import os import os
import pyaes import pyaes
import logging
from . import libssl
__log__ = logging.getLogger(__name__)
try: try:
import cryptg import cryptg
__log__.info('cryptg detected, it will be used for encryption')
except ImportError: except ImportError:
cryptg = None cryptg = None
if libssl.encrypt_ige and libssl.decrypt_ige:
__log__.info('libssl detected, it will be used for encryption')
else:
__log__.info('cryptg module not installed and libssl not found, '
'falling back to (slower) Python encryption')
class AES: class AES:
@ -23,6 +39,8 @@ class AES:
""" """
if cryptg: if cryptg:
return cryptg.decrypt_ige(cipher_text, key, iv) return cryptg.decrypt_ige(cipher_text, key, iv)
if libssl.decrypt_ige:
return libssl.decrypt_ige(cipher_text, key, iv)
iv1 = iv[:len(iv) // 2] iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv) // 2:] iv2 = iv[len(iv) // 2:]
@ -56,13 +74,14 @@ class AES:
Encrypts the given text in 16-bytes blocks by using the Encrypts the given text in 16-bytes blocks by using the
given key and 32-bytes initialization vector. given key and 32-bytes initialization vector.
""" """
# Add random padding iff it's not evenly divisible by 16 already padding = len(plain_text) % 16
if len(plain_text) % 16 != 0: if padding:
padding_count = 16 - len(plain_text) % 16 plain_text += os.urandom(16 - padding)
plain_text += os.urandom(padding_count)
if cryptg: if cryptg:
return cryptg.encrypt_ige(plain_text, key, iv) return cryptg.encrypt_ige(plain_text, key, iv)
if libssl.encrypt_ige:
return libssl.encrypt_ige(plain_text, key, iv)
iv1 = iv[:len(iv) // 2] iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv) // 2:] iv2 = iv[len(iv) // 2:]

69
telethon/crypto/libssl.py Normal file
View File

@ -0,0 +1,69 @@
"""
Helper module around the system's libssl library if available for IGE mode.
"""
import ctypes
import ctypes.util
lib = ctypes.util.find_library('ssl')
if not lib:
decrypt_ige = None
encrypt_ige = None
else:
_libssl = ctypes.cdll.LoadLibrary(lib)
# https://github.com/openssl/openssl/blob/master/include/openssl/aes.h
AES_ENCRYPT = ctypes.c_int(1)
AES_DECRYPT = ctypes.c_int(0)
AES_MAXNR = 14
class AES_KEY(ctypes.Structure):
"""Helper class representing an AES key"""
_fields_ = [
('rd_key', ctypes.c_uint32 * (4 * (AES_MAXNR + 1))),
('rounds', ctypes.c_uint),
]
def decrypt_ige(cipher_text, key, iv):
aes_key = AES_KEY()
key_len = ctypes.c_int(8 * len(key))
key = (ctypes.c_ubyte * len(key))(*key)
iv = (ctypes.c_ubyte * len(iv))(*iv)
in_len = ctypes.c_size_t(len(cipher_text))
in_ptr = (ctypes.c_ubyte * len(cipher_text))(*cipher_text)
out_ptr = (ctypes.c_ubyte * len(cipher_text))()
_libssl.AES_set_decrypt_key(key, key_len, ctypes.byref(aes_key))
_libssl.AES_ige_encrypt(
ctypes.byref(in_ptr),
ctypes.byref(out_ptr),
in_len,
ctypes.byref(aes_key),
ctypes.byref(iv),
AES_DECRYPT
)
return bytes(out_ptr)
def encrypt_ige(plain_text, key, iv):
aes_key = AES_KEY()
key_len = ctypes.c_int(8 * len(key))
key = (ctypes.c_ubyte * len(key))(*key)
iv = (ctypes.c_ubyte * len(iv))(*iv)
in_len = ctypes.c_size_t(len(plain_text))
in_ptr = (ctypes.c_ubyte * len(plain_text))(*plain_text)
out_ptr = (ctypes.c_ubyte * len(plain_text))()
_libssl.AES_set_encrypt_key(key, key_len, ctypes.byref(aes_key))
_libssl.AES_ige_encrypt(
ctypes.byref(in_ptr),
ctypes.byref(out_ptr),
in_len,
ctypes.byref(aes_key),
ctypes.byref(iv),
AES_ENCRYPT
)
return bytes(out_ptr)

View File

@ -36,7 +36,7 @@ def report_error(code, message, report_method):
) )
url.read() url.read()
url.close() url.close()
except: except Exception as e:
"We really don't want to crash when just reporting an error" "We really don't want to crash when just reporting an error"
@ -65,5 +65,8 @@ def rpc_message_to_error(rpc_error, report_method=None):
capture = int(m.group(1)) if m.groups() else None capture = int(m.group(1)) if m.groups() else None
return cls(capture=capture) return cls(capture=capture)
cls = base_errors.get(rpc_error.error_code, RPCError) cls = base_errors.get(rpc_error.error_code)
return cls(rpc_error.error_message) if cls:
return cls(rpc_error.error_message)
return RPCError(rpc_error.error_code, rpc_error.error_message)

View File

@ -3,8 +3,13 @@ class RPCError(Exception):
code = None code = None
message = None message = None
def __init__(self, code, message):
super().__init__('RPCError {}: {}'.format(code, message))
self.code = code
self.message = message
def __reduce__(self): def __reduce__(self):
return type(self), () return type(self), (self.code, self.message)
class InvalidDCError(RPCError): class InvalidDCError(RPCError):

View File

@ -5,6 +5,8 @@ from .messageedited import MessageEdited
from .messageread import MessageRead from .messageread import MessageRead
from .newmessage import NewMessage from .newmessage import NewMessage
from .userupdate import UserUpdate from .userupdate import UserUpdate
from .callbackquery import CallbackQuery
from .inlinequery import InlineQuery
class StopPropagation(Exception): class StopPropagation(Exception):

View File

@ -0,0 +1,254 @@
import re
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions
from ..tl.custom.sendergetter import SenderGetter
@name_inner_event
class CallbackQuery(EventBuilder):
"""
Represents a callback query event (when an inline button is clicked).
Note that the `chats` parameter will **not** work with normal
IDs or peers if the clicked inline button comes from a "via bot"
message. The `chats` parameter also supports checking against the
`chat_instance` which should be used for inline callbacks.
Args:
data (`bytes` | `str` | `callable`, optional):
If set, the inline button payload data must match this data.
A UTF-8 string can also be given, a regex or a callable. For
instance, to check against ``'data_1'`` and ``'data_2'`` you
can use ``re.compile(b'data_')``.
"""
def __init__(self, chats=None, *, blacklist_chats=False, data=None):
super().__init__(chats=chats, blacklist_chats=blacklist_chats)
if isinstance(data, bytes):
self.data = data
elif isinstance(data, str):
self.data = data.encode('utf-8')
elif not data or callable(data):
self.data = data
elif hasattr(data, 'match') and callable(data.match):
if not isinstance(getattr(data, 'pattern', b''), bytes):
data = re.compile(data.pattern.encode('utf-8'), data.flags)
self.data = data.match
else:
raise TypeError('Invalid data type given')
@classmethod
def build(cls, update):
if isinstance(update, (types.UpdateBotCallbackQuery,
types.UpdateInlineBotCallbackQuery)):
event = cls.Event(update)
else:
return
event._entities = update._entities
return event
def filter(self, event):
if self.chats is not None:
inside = event.query.chat_instance in self.chats
if event.chat_id:
inside |= event.chat_id in self.chats
if inside == self.blacklist_chats:
return None
if self.data:
if callable(self.data):
event.data_match = self.data(event.query.data)
if not event.data_match:
return None
elif event.query.data != self.data:
return None
return event
class Event(EventCommon, SenderGetter):
"""
Represents the event of a new callback query.
Members:
query (:tl:`UpdateBotCallbackQuery`):
The original :tl:`UpdateBotCallbackQuery`.
data_match (`obj`, optional):
The object returned by the ``data=`` parameter
when creating the event builder, if any. Similar
to ``pattern_match`` for the new message event.
"""
def __init__(self, query):
super().__init__(chat_peer=getattr(query, 'peer', None),
msg_id=query.msg_id)
self.query = query
self.data_match = None
self._sender_id = query.user_id
self._input_sender = None
self._sender = None
self._message = None
self._answered = False
@property
def id(self):
"""
Returns the query ID. The user clicking the inline
button is the one who generated this random ID.
"""
return self.query.query_id
@property
def message_id(self):
"""
Returns the message ID to which the clicked inline button belongs.
"""
return self.query.msg_id
@property
def data(self):
"""
Returns the data payload from the original inline button.
"""
return self.query.data
@property
def chat_instance(self):
"""
Unique identifier for the chat where the callback occurred.
Useful for high scores in games.
"""
return self.query.chat_instance
def get_message(self):
"""
Returns the message to which the clicked inline button belongs.
"""
if self._message is not None:
return self._message
try:
chat = self.get_input_chat() if self.is_channel else None
self._message = self._client.get_messages(
chat, ids=self.query.msg_id)
except ValueError:
return
return self._message
def _refetch_sender(self):
self._sender = self._entities.get(self.sender_id)
if not self._sender:
return
self._input_sender = utils.get_input_peer(self._chat)
if not getattr(self._input_sender, 'access_hash', True):
# getattr with True to handle the InputPeerSelf() case
try:
self._input_sender = self._client.session.get_input_entity(
self._sender_id
)
except ValueError:
m = self.get_message()
if m:
self._sender = m._sender
self._input_sender = m._input_sender
def answer(
self, message=None, cache_time=0, *, url=None, alert=False):
"""
Answers the callback query (and stops the loading circle).
Args:
message (`str`, optional):
The toast message to show feedback to the user.
cache_time (`int`, optional):
For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
url (`str`, optional):
The URL to be opened in the user's client. Note that
the only valid URLs are those of games your bot has,
or alternatively a 't.me/your_bot?start=xyz' parameter.
alert (`bool`, optional):
Whether an alert (a pop-up dialog) should be used
instead of showing a toast. Defaults to ``False``.
"""
if self._answered:
return
self._answered = True
return self._client(
functions.messages.SetBotCallbackAnswerRequest(
query_id=self.query.query_id,
cache_time=cache_time,
alert=alert,
message=message,
url=url
)
)
def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). Shorthand for
`telethon.telegram_client.TelegramClient.send_message` with
``entity`` already set.
This method also creates a task to `answer` the callback.
"""
self._client.loop.create_task(self.answer())
return self._client.send_message(
self.get_input_chat(), *args, **kwargs)
def reply(self, *args, **kwargs):
"""
Replies to the message (as a reply). Shorthand for
`telethon.telegram_client.TelegramClient.send_message` with
both ``entity`` and ``reply_to`` already set.
This method also creates a task to `answer` the callback.
"""
self._client.loop.create_task(self.answer())
kwargs['reply_to'] = self.query.msg_id
return self._client.send_message(
self.get_input_chat(), *args, **kwargs)
def edit(self, *args, **kwargs):
"""
Edits the message iff it's outgoing. Shorthand for
`telethon.telegram_client.TelegramClient.edit_message` with
both ``entity`` and ``message`` already set.
Returns the edited :tl:`Message`.
This method also creates a task to `answer` the callback.
"""
self._client.loop.create_task(self.answer())
return self._client.edit_message(
self.get_input_chat(), self.query.msg_id,
*args, **kwargs
)
def delete(self, *args, **kwargs):
"""
Deletes the message. Shorthand for
`telethon.telegram_client.TelegramClient.delete_messages` with
``entity`` and ``message_ids`` already set.
If you need to delete more than one message at once, don't use
this `delete` method. Use a
`telethon.telegram_client.TelegramClient` instance directly.
This method also creates a task to `answer` the callback.
"""
self._client.loop.create_task(self.answer())
return self._client.delete_messages(
self.get_input_chat(), [self.query.msg_id],
*args, **kwargs
)

View File

@ -8,23 +8,24 @@ class ChatAction(EventBuilder):
""" """
Represents an action in a chat (such as user joined, left, or new pin). Represents an action in a chat (such as user joined, left, or new pin).
""" """
def build(self, update): @classmethod
def build(cls, update):
if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0: if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
# Telegram does not always send # Telegram does not always send
# UpdateChannelPinnedMessage for new pins # UpdateChannelPinnedMessage for new pins
# but always for unpin, with update.id = 0 # but always for unpin, with update.id = 0
event = ChatAction.Event(types.PeerChannel(update.channel_id), event = cls.Event(types.PeerChannel(update.channel_id),
unpin=True) unpin=True)
elif isinstance(update, types.UpdateChatParticipantAdd): elif isinstance(update, types.UpdateChatParticipantAdd):
event = ChatAction.Event(types.PeerChat(update.chat_id), event = cls.Event(types.PeerChat(update.chat_id),
added_by=update.inviter_id or True, added_by=update.inviter_id or True,
users=update.user_id) users=update.user_id)
elif isinstance(update, types.UpdateChatParticipantDelete): elif isinstance(update, types.UpdateChatParticipantDelete):
event = ChatAction.Event(types.PeerChat(update.chat_id), event = cls.Event(types.PeerChat(update.chat_id),
kicked_by=True, kicked_by=True,
users=update.user_id) users=update.user_id)
elif (isinstance(update, ( elif (isinstance(update, (
types.UpdateNewMessage, types.UpdateNewChannelMessage)) types.UpdateNewMessage, types.UpdateNewChannelMessage))
@ -32,53 +33,53 @@ class ChatAction(EventBuilder):
msg = update.message msg = update.message
action = update.message.action action = update.message.action
if isinstance(action, types.MessageActionChatJoinedByLink): if isinstance(action, types.MessageActionChatJoinedByLink):
event = ChatAction.Event(msg, event = cls.Event(msg,
added_by=True, added_by=True,
users=msg.from_id) users=msg.from_id)
elif isinstance(action, types.MessageActionChatAddUser): elif isinstance(action, types.MessageActionChatAddUser):
# If an user adds itself, it means they joined # If an user adds itself, it means they joined
added_by = ([msg.from_id] == action.users) or msg.from_id added_by = ([msg.from_id] == action.users) or msg.from_id
event = ChatAction.Event(msg, event = cls.Event(msg,
added_by=added_by, added_by=added_by,
users=action.users) users=action.users)
elif isinstance(action, types.MessageActionChatDeleteUser): elif isinstance(action, types.MessageActionChatDeleteUser):
event = ChatAction.Event(msg, event = cls.Event(msg,
kicked_by=msg.from_id or True, kicked_by=msg.from_id or True,
users=action.user_id) users=action.user_id)
elif isinstance(action, types.MessageActionChatCreate): elif isinstance(action, types.MessageActionChatCreate):
event = ChatAction.Event(msg, event = cls.Event(msg,
users=action.users, users=action.users,
created=True, created=True,
new_title=action.title) new_title=action.title)
elif isinstance(action, types.MessageActionChannelCreate): elif isinstance(action, types.MessageActionChannelCreate):
event = ChatAction.Event(msg, event = cls.Event(msg,
created=True, created=True,
users=msg.from_id, users=msg.from_id,
new_title=action.title) new_title=action.title)
elif isinstance(action, types.MessageActionChatEditTitle): elif isinstance(action, types.MessageActionChatEditTitle):
event = ChatAction.Event(msg, event = cls.Event(msg,
users=msg.from_id, users=msg.from_id,
new_title=action.title) new_title=action.title)
elif isinstance(action, types.MessageActionChatEditPhoto): elif isinstance(action, types.MessageActionChatEditPhoto):
event = ChatAction.Event(msg, event = cls.Event(msg,
users=msg.from_id, users=msg.from_id,
new_photo=action.photo) new_photo=action.photo)
elif isinstance(action, types.MessageActionChatDeletePhoto): elif isinstance(action, types.MessageActionChatDeletePhoto):
event = ChatAction.Event(msg, event = cls.Event(msg,
users=msg.from_id, users=msg.from_id,
new_photo=True) new_photo=True)
elif isinstance(action, types.MessageActionPinMessage): elif isinstance(action, types.MessageActionPinMessage):
# Telegram always sends this service message for new pins # Telegram always sends this service message for new pins
event = ChatAction.Event(msg, event = cls.Event(msg,
users=msg.from_id, users=msg.from_id,
new_pin=msg.reply_to_msg_id) new_pin=msg.reply_to_msg_id)
else: else:
return return
else: else:
return return
event._entities = update._entities event._entities = update._entities
return self._filter_event(event) return event
class Event(EventCommon): class Event(EventCommon):
""" """
@ -163,8 +164,7 @@ class ChatAction(EventBuilder):
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
if self.action_message: if self.action_message:
self.action_message = custom.Message( self.action_message._finish_init(client, self._entities, None)
client, self.action_message, self._entities, None)
def respond(self, *args, **kwargs): def respond(self, *args, **kwargs):
""" """

View File

@ -3,6 +3,7 @@ import warnings
from .. import utils from .. import utils
from ..tl import TLObject, types from ..tl import TLObject, types
from ..tl.custom.chatgetter import ChatGetter
def _into_id_set(client, chats): def _into_id_set(client, chats):
@ -42,8 +43,8 @@ class EventBuilder(abc.ABC):
Args: Args:
chats (`entity`, optional): chats (`entity`, optional):
May be one or more entities (username/peer/etc.). By default, May be one or more entities (username/peer/etc.), preferably IDs.
only matching chats will be handled. By default, only matching chats will be handled.
blacklist_chats (`bool`, optional): blacklist_chats (`bool`, optional):
Whether to treat the chats as a blacklist instead of Whether to treat the chats as a blacklist instead of
@ -51,21 +52,25 @@ class EventBuilder(abc.ABC):
will be handled *except* those specified in ``chats`` will be handled *except* those specified in ``chats``
which will be ignored if ``blacklist_chats=True``. which will be ignored if ``blacklist_chats=True``.
""" """
self_id = None
def __init__(self, chats=None, blacklist_chats=False): def __init__(self, chats=None, blacklist_chats=False):
self.chats = chats self.chats = chats
self.blacklist_chats = blacklist_chats self.blacklist_chats = blacklist_chats
self._self_id = None self._self_id = None
@classmethod
@abc.abstractmethod @abc.abstractmethod
def build(self, update): def build(cls, update):
"""Builds an event for the given update if possible, or returns None""" """Builds an event for the given update if possible, or returns None"""
def resolve(self, client): def resolve(self, client):
"""Helper method to allow event builders to be resolved before usage""" """Helper method to allow event builders to be resolved before usage"""
self.chats = _into_id_set(client, self.chats) self.chats = _into_id_set(client, self.chats)
self._self_id = (client.get_me(input_peer=True)).user_id if not EventBuilder.self_id:
EventBuilder.self_id = client.get_peer_id('me')
def _filter_event(self, event): def filter(self, event):
""" """
If the ID of ``event._chat_peer`` isn't in the chats set (or it is If the ID of ``event._chat_peer`` isn't in the chats set (or it is
but the set is a blacklist) returns ``None``, otherwise the event. but the set is a blacklist) returns ``None``, otherwise the event.
@ -79,13 +84,16 @@ class EventBuilder(abc.ABC):
return event return event
class EventCommon(abc.ABC): class EventCommon(ChatGetter, abc.ABC):
""" """
Intermediate class with common things to all events. Intermediate class with common things to all events.
All events (except `Raw`) have ``is_private``, ``is_group`` Remember that this class implements `ChatGetter
and ``is_channel`` boolean properties, as well as an <telethon.tl.custom.chatgetter.ChatGetter>` which
``original_update`` field containing the original :tl:`Update`. means you have access to all chat properties and methods.
In addition, you can access the `original_update`
field which contains the original :tl:`Update`.
""" """
_event_name = 'Event' _event_name = 'Event'
@ -96,64 +104,27 @@ class EventCommon(abc.ABC):
self._message_id = msg_id self._message_id = msg_id
self._input_chat = None self._input_chat = None
self._chat = None self._chat = None
self._broadcast = broadcast
self.original_update = None self.original_update = None
self.is_private = isinstance(chat_peer, types.PeerUser)
self.is_group = (
isinstance(chat_peer, (types.PeerChat, types.PeerChannel))
and not broadcast
)
self.is_channel = isinstance(chat_peer, types.PeerChannel)
def _set_client(self, client): def _set_client(self, client):
""" """
Setter so subclasses can act accordingly when the client is set. Setter so subclasses can act accordingly when the client is set.
""" """
self._client = client self._client = client
self._chat = self._entities.get(self.chat_id)
if not self._chat:
return
@property self._input_chat = utils.get_input_peer(self._chat)
def input_chat(self): if not getattr(self._input_chat, 'access_hash', True):
""" # getattr with True to handle the InputPeerSelf() case
This (:tl:`InputPeer`) is the input version of the chat where the
event occurred. This doesn't have things like username or similar,
but is still useful in some cases.
Note that this might not be available if the library doesn't have
enough information available.
"""
if self._input_chat is None and self._chat_peer is not None:
try: try:
self._input_chat =\ self._input_chat = self._client.session.get_input_entity(
self._client.session.get_input_entity(self._chat_peer) self._chat_peer
)
except ValueError: except ValueError:
pass self._input_chat = None
return self._input_chat
def get_input_chat(self):
"""
Returns `input_chat`, but will make an API call to find the
input chat unless it's already cached.
"""
if self.input_chat is None and self._chat_peer is not None:
ch = isinstance(self._chat_peer, types.PeerChannel)
if not ch and self._message_id is not None:
msg = self._client.get_messages(
None, ids=self._message_id)
self._chat = msg._chat
self._input_chat = msg._input_chat
else:
target = utils.get_peer_id(self._chat_peer)
for d in self._client.iter_dialogs(100):
if d.id == target:
self._chat = d.entity
self._input_chat = d.input_entity
# TODO Don't break, exhaust the iterator, otherwise
# async_generator raises RuntimeError: partially-
# exhausted async_generator 'xyz' garbage collected
# break
return self._input_chat
@property @property
def client(self): def client(self):
@ -162,44 +133,6 @@ class EventCommon(abc.ABC):
""" """
return self._client return self._client
@property
def chat(self):
"""
The :tl:`User`, :tl:`Chat` or :tl:`Channel` on which
the event occurred. This property may make an API call the first time
to get the most up to date version of the chat (mostly when the event
doesn't belong to a channel), so keep that in mind. You should use
`get_chat` instead, unless you want to avoid an API call.
"""
if not self.input_chat:
return None
if self._chat is None:
self._chat = self._entities.get(utils.get_peer_id(self._chat_peer))
return self._chat
def get_chat(self):
"""
Returns `chat`, but will make an API call to find the
chat unless it's already cached.
"""
if self.chat is None and self.get_input_chat():
try:
self._chat =\
self._client.get_entity(self._input_chat)
except ValueError:
pass
return self._chat
@property
def chat_id(self):
"""
Returns the marked integer ID of the chat, if any.
"""
if self._chat_peer:
return utils.get_peer_id(self._chat_peer)
def __str__(self): def __str__(self):
return TLObject.pretty_format(self.to_dict()) return TLObject.pretty_format(self.to_dict())

View File

@ -0,0 +1,177 @@
import inspect
import re
import asyncio
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions, custom
from ..tl.custom.sendergetter import SenderGetter
@name_inner_event
class InlineQuery(EventBuilder):
"""
Represents an inline query event (when someone writes ``'@my_bot query'``).
Args:
users (`entity`, optional):
May be one or more entities (username/peer/etc.), preferably IDs.
By default, only inline queries from these users will be handled.
blacklist_users (`bool`, optional):
Whether to treat the users as a blacklist instead of
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``users``
which will be ignored if ``blacklist_users=True``.
pattern (`str`, `callable`, `Pattern`, optional):
If set, only queries matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the message, a callable function that returns ``True``
if a message is acceptable, or a compiled regex pattern.
"""
def __init__(self, users=None, *, blacklist_users=False, pattern=None):
super().__init__(chats=users, blacklist_chats=blacklist_users)
if isinstance(pattern, str):
self.pattern = re.compile(pattern).match
elif not pattern or callable(pattern):
self.pattern = pattern
elif hasattr(pattern, 'match') and callable(pattern.match):
self.pattern = pattern.match
else:
raise TypeError('Invalid pattern type given')
@classmethod
def build(cls, update):
if isinstance(update, types.UpdateBotInlineQuery):
event = cls.Event(update)
else:
return
event._entities = update._entities
return event
def filter(self, event):
if self.pattern:
match = self.pattern(event.text)
if not match:
return
event.pattern_match = match
return super().filter(event)
class Event(EventCommon, SenderGetter):
"""
Represents the event of a new callback query.
Members:
query (:tl:`UpdateBotCallbackQuery`):
The original :tl:`UpdateBotCallbackQuery`.
pattern_match (`obj`, optional):
The resulting object from calling the passed ``pattern``
function, which is ``re.compile(...).match`` by default.
"""
def __init__(self, query):
super().__init__(chat_peer=types.PeerUser(query.user_id))
self.query = query
self.pattern_match = None
self._answered = False
self._sender_id = query.user_id
self._input_sender = None
self._sender = None
@property
def id(self):
"""
Returns the unique identifier for the query ID.
"""
return self.query.query_id
@property
def text(self):
"""
Returns the text the user used to make the inline query.
"""
return self.query.query
@property
def offset(self):
"""
???
"""
return self.query.offset
@property
def geo(self):
"""
If the user location is requested when using inline mode
and the user's device is able to send it, this will return
the :tl:`GeoPoint` with the position of the user.
"""
return
@property
def builder(self):
"""
Returns a new `inline result builder
<telethon.tl.custom.inline.InlineBuilder>`.
"""
return custom.InlineBuilder(self._client)
def answer(
self, results=None, cache_time=0, *,
gallery=False, private=False,
switch_pm=None, switch_pm_param=''):
"""
Answers the inline query with the given results.
Args:
results (`list`, optional):
A list of :tl:`InputBotInlineResult` to use.
You should use `builder` to create these:
.. code-block: python
builder = inline.builder
r1 = builder.article('Be nice', text='Have a nice day')
r2 = builder.article('Be bad', text="I don't like you")
inline.answer([r1, r2])
cache_time (`int`, optional):
For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
gallery (`bool`, optional):
Whether the results should show as a gallery (grid) or not.
private (`bool`, optional):
Whether the results should be cached by Telegram
(not private) or by the user's client (private).
switch_pm (`str`, optional):
If set, this text will be shown in the results
to allow the user to switch to private messages.
switch_pm_param (`str`, optional):
Optional parameter to start the bot with if
`switch_pm` was used.
"""
if self._answered:
return
if switch_pm:
switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param)
return self._client(
functions.messages.SetInlineBotResultsRequest(
query_id=self.query.query_id,
results=results,
cache_time=cache_time,
gallery=gallery,
private=private,
switch_pm=switch_pm
)
)

View File

@ -7,14 +7,15 @@ class MessageDeleted(EventBuilder):
""" """
Event fired when one or more messages are deleted. Event fired when one or more messages are deleted.
""" """
def build(self, update): @classmethod
def build(cls, update):
if isinstance(update, types.UpdateDeleteMessages): if isinstance(update, types.UpdateDeleteMessages):
event = MessageDeleted.Event( event = cls.Event(
deleted_ids=update.messages, deleted_ids=update.messages,
peer=None peer=None
) )
elif isinstance(update, types.UpdateDeleteChannelMessages): elif isinstance(update, types.UpdateDeleteChannelMessages):
event = MessageDeleted.Event( event = cls.Event(
deleted_ids=update.messages, deleted_ids=update.messages,
peer=types.PeerChannel(update.channel_id) peer=types.PeerChannel(update.channel_id)
) )
@ -22,19 +23,12 @@ class MessageDeleted(EventBuilder):
return return
event._entities = update._entities event._entities = update._entities
return self._filter_event(event) return event
class Event(EventCommon): class Event(EventCommon):
def __init__(self, deleted_ids, peer): def __init__(self, deleted_ids, peer):
super().__init__( super().__init__(
chat_peer=peer, msg_id=(deleted_ids or [0])[0] chat_peer=peer, msg_id=(deleted_ids or [0])[0]
) )
if peer is None:
# If it's not a channel ID, then it was private/small group.
# We can't know which one was exactly unless we logged all
# messages, but we can indicate that it was maybe either of
# both by setting them both to True.
self.is_private = self.is_group = True
self.deleted_id = None if not deleted_ids else deleted_ids[0] self.deleted_id = None if not deleted_ids else deleted_ids[0]
self.deleted_ids = deleted_ids self.deleted_ids = deleted_ids

View File

@ -8,15 +8,16 @@ class MessageEdited(NewMessage):
""" """
Event fired when a message has been edited. Event fired when a message has been edited.
""" """
def build(self, update): @classmethod
def build(cls, update):
if isinstance(update, (types.UpdateEditMessage, if isinstance(update, (types.UpdateEditMessage,
types.UpdateEditChannelMessage)): types.UpdateEditChannelMessage)):
event = MessageEdited.Event(update.message) event = cls.Event(update.message)
else: else:
return return
event._entities = update._entities event._entities = update._entities
return self._message_filter_event(event) return event
class Event(NewMessage.Event): class Event(NewMessage.Event):
pass # Required if we want a different name for it pass # Required if we want a different name for it

View File

@ -18,32 +18,36 @@ class MessageRead(EventBuilder):
super().__init__(chats, blacklist_chats) super().__init__(chats, blacklist_chats)
self.inbox = inbox self.inbox = inbox
def build(self, update): @classmethod
def build(cls, update):
if isinstance(update, types.UpdateReadHistoryInbox): if isinstance(update, types.UpdateReadHistoryInbox):
event = MessageRead.Event(update.peer, update.max_id, False) event = cls.Event(update.peer, update.max_id, False)
elif isinstance(update, types.UpdateReadHistoryOutbox): elif isinstance(update, types.UpdateReadHistoryOutbox):
event = MessageRead.Event(update.peer, update.max_id, True) event = cls.Event(update.peer, update.max_id, True)
elif isinstance(update, types.UpdateReadChannelInbox): elif isinstance(update, types.UpdateReadChannelInbox):
event = MessageRead.Event(types.PeerChannel(update.channel_id), event = cls.Event(types.PeerChannel(update.channel_id),
update.max_id, False) update.max_id, False)
elif isinstance(update, types.UpdateReadChannelOutbox): elif isinstance(update, types.UpdateReadChannelOutbox):
event = MessageRead.Event(types.PeerChannel(update.channel_id), event = cls.Event(types.PeerChannel(update.channel_id),
update.max_id, True) update.max_id, True)
elif isinstance(update, types.UpdateReadMessagesContents): elif isinstance(update, types.UpdateReadMessagesContents):
event = MessageRead.Event(message_ids=update.messages, event = cls.Event(message_ids=update.messages,
contents=True) contents=True)
elif isinstance(update, types.UpdateChannelReadMessagesContents): elif isinstance(update, types.UpdateChannelReadMessagesContents):
event = MessageRead.Event(types.PeerChannel(update.channel_id), event = cls.Event(types.PeerChannel(update.channel_id),
message_ids=update.messages, message_ids=update.messages,
contents=True) contents=True)
else: else:
return return
event._entities = update._entities
return event
def filter(self, event):
if self.inbox == event.outbox: if self.inbox == event.outbox:
return return
event._entities = update._entities return super().filter(event)
return self._filter_event(event)
class Event(EventCommon): class Event(EventCommon):
""" """

View File

@ -40,13 +40,12 @@ class NewMessage(EventBuilder):
def __init__(self, chats=None, *, blacklist_chats=False, def __init__(self, chats=None, *, blacklist_chats=False,
incoming=None, outgoing=None, incoming=None, outgoing=None,
from_users=None, forwards=None, pattern=None): from_users=None, forwards=None, pattern=None):
if incoming is not None and outgoing is None: if incoming and outgoing:
incoming = outgoing = None # Same as no filter
elif incoming is not None and outgoing is None:
outgoing = not incoming outgoing = not incoming
elif outgoing is not None and incoming is None: elif outgoing is not None and incoming is None:
incoming = not outgoing incoming = not outgoing
if incoming and outgoing:
self.incoming = self.outgoing = None # Same as no filter
elif all(x is not None and not x for x in (incoming, outgoing)): elif all(x is not None and not x for x in (incoming, outgoing)):
raise ValueError("Don't create an event handler if you " raise ValueError("Don't create an event handler if you "
"don't want neither incoming or outgoing!") "don't want neither incoming or outgoing!")
@ -75,14 +74,15 @@ class NewMessage(EventBuilder):
super().resolve(client) super().resolve(client)
self.from_users = _into_id_set(client, self.from_users) self.from_users = _into_id_set(client, self.from_users)
def build(self, update): @classmethod
def build(cls, update):
if isinstance(update, if isinstance(update,
(types.UpdateNewMessage, types.UpdateNewChannelMessage)): (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
if not isinstance(update.message, types.Message): if not isinstance(update.message, types.Message):
return # We don't care about MessageService's here return # We don't care about MessageService's here
event = NewMessage.Event(update.message) event = cls.Event(update.message)
elif isinstance(update, types.UpdateShortMessage): elif isinstance(update, types.UpdateShortMessage):
event = NewMessage.Event(types.Message( event = cls.Event(types.Message(
out=update.out, out=update.out,
mentioned=update.mentioned, mentioned=update.mentioned,
media_unread=update.media_unread, media_unread=update.media_unread,
@ -91,9 +91,9 @@ class NewMessage(EventBuilder):
# Note that to_id/from_id complement each other in private # Note that to_id/from_id complement each other in private
# messages, depending on whether the message was outgoing. # messages, depending on whether the message was outgoing.
to_id=types.PeerUser( to_id=types.PeerUser(
update.user_id if update.out else self._self_id update.user_id if update.out else cls.self_id
), ),
from_id=self._self_id if update.out else update.user_id, from_id=cls.self_id if update.out else update.user_id,
message=update.message, message=update.message,
date=update.date, date=update.date,
fwd_from=update.fwd_from, fwd_from=update.fwd_from,
@ -102,7 +102,7 @@ class NewMessage(EventBuilder):
entities=update.entities entities=update.entities
)) ))
elif isinstance(update, types.UpdateShortChatMessage): elif isinstance(update, types.UpdateShortChatMessage):
event = NewMessage.Event(types.Message( event = cls.Event(types.Message(
out=update.out, out=update.out,
mentioned=update.mentioned, mentioned=update.mentioned,
media_unread=update.media_unread, media_unread=update.media_unread,
@ -120,8 +120,6 @@ class NewMessage(EventBuilder):
else: else:
return return
event._entities = update._entities
# Make messages sent to ourselves outgoing unless they're forwarded. # Make messages sent to ourselves outgoing unless they're forwarded.
# This makes it consistent with official client's appearance. # This makes it consistent with official client's appearance.
ori = event.message ori = event.message
@ -129,9 +127,10 @@ class NewMessage(EventBuilder):
if ori.from_id == ori.to_id.user_id and not ori.fwd_from: if ori.from_id == ori.to_id.user_id and not ori.fwd_from:
event.message.out = True event.message.out = True
return self._message_filter_event(event) event._entities = update._entities
return event
def _message_filter_event(self, event): def filter(self, event):
if self._no_check: if self._no_check:
return event return event
@ -153,7 +152,7 @@ class NewMessage(EventBuilder):
return return
event.pattern_match = match event.pattern_match = match
return self._filter_event(event) return super().filter(event)
class Event(EventCommon): class Event(EventCommon):
""" """
@ -204,8 +203,7 @@ class NewMessage(EventBuilder):
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self.message = custom.Message( self.message._finish_init(client, self._entities, None)
client, self.message, self._entities, None)
self.__dict__['_init'] = True # No new attributes can be set self.__dict__['_init'] = True # No new attributes can be set
def __getattr__(self, item): def __getattr__(self, item):

View File

@ -25,6 +25,10 @@ class Raw(EventBuilder):
def resolve(self, client): def resolve(self, client):
pass pass
def build(self, update): @classmethod
if not self.types or isinstance(update, self.types): def build(cls, update):
return update return update
def filter(self, event):
if not self.types or isinstance(event, self.types):
return event

View File

@ -9,15 +9,16 @@ class UserUpdate(EventBuilder):
""" """
Represents an user update (gone online, offline, joined Telegram). Represents an user update (gone online, offline, joined Telegram).
""" """
def build(self, update): @classmethod
def build(cls, update):
if isinstance(update, types.UpdateUserStatus): if isinstance(update, types.UpdateUserStatus):
event = UserUpdate.Event(update.user_id, event = cls.Event(update.user_id,
status=update.status) status=update.status)
else: else:
return return
event._entities = update._entities event._entities = update._entities
return self._filter_event(event) return event
class Event(EventCommon): class Event(EventCommon):
""" """
@ -95,7 +96,8 @@ class UserUpdate(EventBuilder):
isinstance(status, types.UserStatusOnline) else None isinstance(status, types.UserStatusOnline) else None
if self.last_seen: if self.last_seen:
diff = datetime.datetime.now() - self.last_seen now = datetime.datetime.now(tz=datetime.timezone.utc)
diff = now - self.last_seen
if diff < datetime.timedelta(days=30): if diff < datetime.timedelta(days=30):
self.within_months = True self.within_months = True
if diff < datetime.timedelta(days=7): if diff < datetime.timedelta(days=7):

View File

@ -2,7 +2,7 @@
This module contains the BinaryReader utility class. This module contains the BinaryReader utility class.
""" """
import os import os
from datetime import datetime from datetime import datetime, timezone
from io import BufferedReader, BytesIO from io import BufferedReader, BytesIO
from struct import unpack from struct import unpack
@ -120,7 +120,10 @@ class BinaryReader:
into a Python datetime object. into a Python datetime object.
""" """
value = self.read_int() value = self.read_int()
return None if value == 0 else datetime.utcfromtimestamp(value) if value == 0:
return None
else:
return datetime.fromtimestamp(value, tz=timezone.utc)
def tgread_object(self): def tgread_object(self):
"""Reads a Telegram object.""" """Reads a Telegram object."""

View File

@ -67,6 +67,7 @@ class TcpClient:
if proxy is None: if proxy is None:
s = socket.socket(mode, socket.SOCK_STREAM) s = socket.socket(mode, socket.SOCK_STREAM)
else: else:
__log__.info('Connection will be made through proxy %s', proxy)
import socks import socks
s = socks.socksocket(mode, socket.SOCK_STREAM) s = socks.socksocket(mode, socket.SOCK_STREAM)
if isinstance(proxy, dict): if isinstance(proxy, dict):
@ -92,11 +93,16 @@ class TcpClient:
try: try:
if self._socket is None: if self._socket is None:
self._socket = self._create_socket(mode, self.proxy) self._socket = self._create_socket(mode, self.proxy)
if self.ssl and port == SSL_PORT: wrap_ssl = self.ssl and port == SSL_PORT
self._socket = ssl.wrap_socket(self._socket, **self.ssl) else:
wrap_ssl = False
self._socket.settimeout(self.timeout) self._socket.settimeout(self.timeout)
self._socket.connect(address) self._socket.connect(address)
if wrap_ssl:
self._socket = ssl.wrap_socket(
self._socket, do_handshake_on_connect=True, **self.ssl)
self._closed.clear() self._closed.clear()
except OSError as e: except OSError as e:
if e.errno in CONN_RESET_ERRNOS: if e.errno in CONN_RESET_ERRNOS:

View File

@ -1,6 +1,7 @@
"""Various helpers not related to the Telegram API itself""" """Various helpers not related to the Telegram API itself"""
import os import os
import struct import struct
import collections
from hashlib import sha1, sha256 from hashlib import sha1, sha256
@ -65,3 +66,22 @@ def get_password_hash(pw, current_salt):
return sha256(pw_hash).digest() return sha256(pw_hash).digest()
# endregion # endregion
# region Custom Classes
class TotalList(collections.UserList):
"""
A list with an extra `total` property, which may not match its `len`
since the total represents the total amount of items *available*
somewhere else, not the items *in this list*.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.total = 0
def __str__(self):
return '[{}, total={}]'.format(
', '.join(repr(x) for x in self), self.total)
# endregion

View File

@ -392,6 +392,7 @@ class MTProtoSender:
except socket.timeout: except socket.timeout:
continue continue
except concurrent.futures.CancelledError: except concurrent.futures.CancelledError:
self.disconnect()
return return
except Exception as e: except Exception as e:
if isinstance(e, ConnectionError): if isinstance(e, ConnectionError):
@ -431,6 +432,7 @@ class MTProtoSender:
except socket.timeout: except socket.timeout:
continue continue
except concurrent.futures.CancelledError: except concurrent.futures.CancelledError:
self.disconnect()
return return
except Exception as e: except Exception as e:
if isinstance(e, ConnectionError): if isinstance(e, ConnectionError):
@ -473,15 +475,19 @@ class MTProtoSender:
__log__.info('Server replied with an unknown type {:08x}: {!r}' __log__.info('Server replied with an unknown type {:08x}: {!r}'
.format(e.invalid_constructor_id, e.remaining)) .format(e.invalid_constructor_id, e.remaining))
continue continue
except: except concurrent.futures.CancelledError:
__log__.exception('Unhandled exception while unpacking') self.disconnect()
return
except Exception as e:
__log__.exception('Unhandled exception while unpacking %s',e)
time.sleep(1) time.sleep(1)
else: else:
try: try:
self._process_message(message) self._process_message(message)
except concurrent.futures.CancelledError: except concurrent.futures.CancelledError:
self.disconnect()
return return
except: except Exception as e:
__log__.exception('Unhandled exception while ' __log__.exception('Unhandled exception while '
'processing %s', message) 'processing %s', message)
time.sleep(1) time.sleep(1)
@ -512,6 +518,12 @@ class MTProtoSender:
__log__.debug('Handling RPC result for message %d', __log__.debug('Handling RPC result for message %d',
rpc_result.req_msg_id) rpc_result.req_msg_id)
if not message:
# TODO We should not get responses to things we never sent
__log__.info('Received response without parent request: {}'
.format(rpc_result.body))
return
if rpc_result.error: if rpc_result.error:
error = rpc_message_to_error(rpc_result.error) error = rpc_message_to_error(rpc_result.error)
self._send_queue.put_nowait(self.state.create_message( self._send_queue.put_nowait(self.state.create_message(
@ -520,8 +532,7 @@ class MTProtoSender:
if not message.future.cancelled(): if not message.future.cancelled():
message.future.set_exception(error) message.future.set_exception(error)
return else:
elif message:
# TODO Would be nice to avoid accessing a per-obj read_result # TODO Would be nice to avoid accessing a per-obj read_result
# Instead have a variable that indicated how the result should # Instead have a variable that indicated how the result should
# be read (an enum) and dispatch to read the result, mostly # be read (an enum) and dispatch to read the result, mostly
@ -531,11 +542,6 @@ class MTProtoSender:
if not message.future.cancelled(): if not message.future.cancelled():
message.future.set_result(result) message.future.set_result(result)
return
else:
# TODO We should not get responses to things we never sent
__log__.info('Received response without parent request: {}'
.format(rpc_result.body))
def _handle_container(self, message): def _handle_container(self, message):
""" """
@ -611,7 +617,7 @@ class MTProtoSender:
bad_msg = message.obj bad_msg = message.obj
msg = self._pending_messages.get(bad_msg.bad_msg_id) msg = self._pending_messages.get(bad_msg.bad_msg_id)
__log__.debug('Handling bad msg for message %d', bad_msg.bad_msg_id) __log__.debug('Handling bad msg %s', bad_msg)
if bad_msg.error_code in (16, 17): if bad_msg.error_code in (16, 17):
# Sent msg_id too low or too high (respectively). # Sent msg_id too low or too high (respectively).
# Use the current msg_id to determine the right time offset. # Use the current msg_id to determine the right time offset.

View File

@ -157,10 +157,20 @@ class MTProtoState:
Updates the time offset to the correct Updates the time offset to the correct
one given a known valid message ID. one given a known valid message ID.
""" """
bad = self._get_new_msg_id()
old = self.time_offset
now = int(time.time()) now = int(time.time())
correct = correct_msg_id >> 32 correct = correct_msg_id >> 32
self.time_offset = correct - now self.time_offset = correct - now
self._last_msg_id = 0
if self.time_offset != old:
self._last_msg_id = 0
__log__.debug(
'Updated time offset (old offset %d, bad %d, good %d, new %d)',
old, bad, correct_msg_id, self.time_offset
)
return self.time_offset return self.time_offset
def _get_seq_no(self, content_related): def _get_seq_no(self, content_related):

View File

@ -42,6 +42,10 @@ class MemorySession(Session):
self._server_address = server_address self._server_address = server_address
self._port = port self._port = port
@property
def dc_id(self):
return self._dc_id
@property @property
def server_address(self): def server_address(self):
return self._server_address return self._server_address

View File

@ -211,7 +211,8 @@ class SQLiteSession(MemorySession):
'where id = ?', entity_id) 'where id = ?', entity_id)
if row: if row:
pts, qts, date, seq = row pts, qts, date, seq = row
date = datetime.datetime.utcfromtimestamp(date) date = datetime.datetime.fromtimestamp(
date, tz=datetime.timezone.utc)
return types.updates.State(pts, qts, date, seq, unread_count=0) return types.updates.State(pts, qts, date, seq, unread_count=0)
def set_update_state(self, entity_id, state): def set_update_state(self, entity_id, state):

View File

@ -1,6 +1,9 @@
from .draft import Draft from .draft import Draft
from .dialog import Dialog from .dialog import Dialog
from .input_sized_file import InputSizedFile from .inputsizedfile import InputSizedFile
from .messagebutton import MessageButton from .messagebutton import MessageButton
from .forward import Forward from .forward import Forward
from .message import Message from .message import Message
from .button import Button
from .inline import InlineBuilder
from .inlineresult import InlineResult

View File

@ -0,0 +1,134 @@
from .. import types
class Button:
"""
Helper class to allow defining ``reply_markup`` when
sending a message with inline or keyboard buttons.
You should make use of the defined class methods to create button
instances instead making them yourself (i.e. don't do ``Button(...)``
but instead use methods line `Button.inline(...) <inline>` etc.)
You can use `inline`, `switch_inline` and `url`
together to create inline buttons (under the message).
You can use `text`, `request_location` and `request_phone`
together to create a reply markup (replaces the user keyboard).
You **cannot** mix the two type of buttons together,
and it will error if you try to do so.
The text for all buttons may be at most 142 characters.
If more characters are given, Telegram will cut the text
to 128 characters and add the ellipsis () character as
the 129.
"""
def __init__(self, button, callback=None):
self.button = button
self.callback = callback
self.is_inline = self._is_inline(button)
@property
def data(self):
if isinstance(self.button, types.KeyboardButtonCallback):
return self.button.data
@classmethod
def _is_inline(cls, button):
"""
Returns ``True`` if the button belongs to an inline keyboard.
"""
if isinstance(button, cls):
return button.is_inline
else:
return isinstance(button, (
types.KeyboardButtonCallback,
types.KeyboardButtonSwitchInline,
types.KeyboardButtonUrl
))
@classmethod
def inline(cls, text, callback=None, data=None):
"""
Creates a new inline button.
The `callback` parameter should be a function callback accepting
a single parameter (the triggered event on click) if specified.
Otherwise, you should register the event manually.
If `data` is omitted, the given `text` will be used as `data`.
In any case `data` should be either ``bytes`` or ``str``.
Note that the given `data` must be less or equal to 64 bytes.
If more than 64 bytes are passed as data, ``ValueError`` is raised.
"""
if not data:
data = text.encode('utf-8')
if len(data) > 64:
raise ValueError('Too many bytes for the data')
return cls(types.KeyboardButtonCallback(text, data), callback)
@classmethod
def switch_inline(cls, text, query='', same_peer=False):
"""
Creates a new button to switch to inline query.
If `query` is given, it will be the default text to be used
when making the inline query.
If ``same_peer is True`` the inline query will directly be
set under the currently opened chat. Otherwise, the user will
have to select a different dialog to make the query.
"""
return cls(types.KeyboardButtonSwitchInline(text, query, same_peer))
@classmethod
def url(cls, text, url=None):
"""
Creates a new button to open the desired URL upon clicking it.
If no `url` is given, the `text` will be used as said URL instead.
"""
return cls(types.KeyboardButtonUrl(text, url or text))
@classmethod
def text(cls, text):
"""
Creates a new button with the given text.
"""
return cls(types.KeyboardButton(text))
@classmethod
def request_location(cls, text):
"""
Creates a new button that will request
the user's location upon being clicked.
"""
return cls(types.KeyboardButtonRequestGeoLocation(text))
@classmethod
def request_phone(cls, text):
"""
Creates a new button that will request
the user's phone number upon being clicked.
"""
return cls(types.KeyboardButtonRequestPhone(text))
@classmethod
def clear(cls):
"""
Clears all the buttons. When used, no other
button should be present or it will be ignored.
"""
return types.ReplyKeyboardHide()
@classmethod
def force_reply(cls):
"""
Forces a reply. If used, no other button
should be present or it will be ignored.
"""
return types.ReplyKeyboardForceReply()

View File

@ -0,0 +1,114 @@
import abc
from ... import errors, utils
from ...tl import types
class ChatGetter(abc.ABC):
"""
Helper base class that introduces the `chat`, `input_chat`
and `chat_id` properties and `get_chat` and `get_input_chat`
methods.
Subclasses **must** have the following private members: `_chat`,
`_input_chat`, `_chat_peer`, `_broadcast` and `_client`. As an end
user, you should not worry about this.
"""
@property
def chat(self):
"""
Returns the :tl:`User`, :tl:`Chat` or :tl:`Channel` where this object
belongs to. It may be ``None`` if Telegram didn't send the chat.
If you're using `telethon.events`, use `get_chat` instead.
"""
return self._chat
def get_chat(self):
"""
Returns `chat`, but will make an API call to find the
chat unless it's already cached.
"""
if self._chat is None and self.get_input_chat():
try:
self._chat =\
self._client.get_entity(self._input_chat)
except ValueError:
self._refetch_chat()
return self._chat
@property
def input_chat(self):
"""
This :tl:`InputPeer` is the input version of the chat where the
message was sent. Similarly to `input_sender`, this doesn't have
things like username or similar, but still useful in some cases.
Note that this might not be available if the library doesn't
have enough information available.
"""
if self._input_chat is None and self._chat_peer:
try:
self._input_chat =\
self._client.session.get_input_entity(self._chat_peer)
except ValueError:
pass
return self._input_chat
def get_input_chat(self):
"""
Returns `input_chat`, but will make an API call to find the
input chat unless it's already cached.
"""
if self.input_chat is None and self.chat_id:
try:
# The chat may be recent, look in dialogs
target = self.chat_id
for d in self._client.iter_dialogs(100):
if d.id == target:
self._chat = d.entity
self._input_chat = d.input_entity
break
except errors.RPCError:
pass
return self._input_chat
@property
def chat_id(self):
"""
Returns the marked chat integer ID. Note that this value **will
be different** from `to_id` for incoming private messages, since
the chat *to* which the messages go is to your own person, but
the *chat* itself is with the one who sent the message.
TL;DR; this gets the ID that you expect.
"""
return utils.get_peer_id(self._chat_peer) if self._chat_peer else None
@property
def is_private(self):
"""True if the message was sent as a private message."""
return isinstance(self._chat_peer, types.PeerUser)
@property
def is_group(self):
"""True if the message was sent on a group or megagroup."""
if self._broadcast is None and self.chat:
self._broadcast = getattr(self.chat, 'broadcast', None)
return (
isinstance(self._chat_peer, (types.PeerChat, types.PeerChannel))
and not self._broadcast
)
@property
def is_channel(self):
"""True if the message was sent on a megagroup or channel."""
return isinstance(self._chat_peer, types.PeerChannel)
def _refetch_chat(self):
"""
Re-fetches chat information through other means.
"""

View File

@ -1,11 +1,19 @@
from ...utils import get_input_peer from .chatgetter import ChatGetter
from .sendergetter import SenderGetter
from ... import utils
from ...tl import types
class Forward: class Forward(ChatGetter, SenderGetter):
""" """
Custom class that encapsulates a :tl:`MessageFwdHeader` providing an Custom class that encapsulates a :tl:`MessageFwdHeader` providing an
abstraction to easily access information like the original sender. abstraction to easily access information like the original sender.
Remember that this class implements `ChatGetter
<telethon.tl.custom.chatgetter.ChatGetter>` and `SenderGetter
<telethon.tl.custom.sendergetter.SenderGetter>` which means you
have access to all their sender and chat properties and methods.
Attributes: Attributes:
original_fwd (:tl:`MessageFwdHeader`): original_fwd (:tl:`MessageFwdHeader`):
@ -19,105 +27,21 @@ class Forward:
self.__dict__ = original.__dict__ self.__dict__ = original.__dict__
self._client = client self._client = client
self.original_fwd = original self.original_fwd = original
self._sender_id = original.from_id
self._sender = entities.get(original.from_id) self._sender = entities.get(original.from_id)
self._chat = entities.get(original.channel_id)
self._input_sender =\ self._input_sender =\
get_input_peer(self._sender) if self._sender else None utils.get_input_peer(self._sender) if self._sender else None
self._input_chat =\
get_input_peer(self._chat) if self._chat else None
# TODO The pattern to get sender and chat is very similar self._broadcast = None
# and copy pasted in/to several places. Reuse the code. if original.channel_id:
# self._chat_peer = types.PeerChannel(original.channel_id)
# It could be an ABC with some ``resolve_sender`` abstract, self._chat = entities.get(utils.get_peer_id(self._chat_peer))
# so every subclass knew what tricks it can make to get else:
# the sender. self._chat_peer = None
self._chat = None
@property self._input_chat = \
def sender(self): utils.get_input_peer(self._chat) if self._chat else None
"""
The :tl:`User` that sent the original message. This may be ``None``
if it couldn't be found or the message wasn't forwarded from an user
but instead was forwarded from e.g. a channel.
"""
return self._sender
def get_sender(self): # TODO We could reload the message
"""
Returns `sender` but will make an API if necessary.
"""
if not self.sender and self.original_fwd.from_id:
try:
self._sender = self._client.get_entity(
self.get_input_sender())
except ValueError:
# TODO We could reload the message
pass
return self._sender
@property
def input_sender(self):
"""
Returns the input version of `user`.
"""
if not self._input_sender and self.original_fwd.from_id:
try:
self._input_sender = self._client.session.get_input_entity(
self.original_fwd.from_id)
except ValueError:
pass
return self._input_sender
def get_input_sender(self):
"""
Returns `input_sender` but will make an API call if necessary.
"""
# TODO We could reload the message
return self.input_sender
@property
def chat(self):
"""
The :tl:`Channel` where the original message was sent. This may be
``None`` if it couldn't be found or the message wasn't forwarded
from a channel but instead was forwarded from e.g. an user.
"""
return self._chat
def get_chat(self):
"""
Returns `chat` but will make an API if necessary.
"""
if not self.chat and self.original_fwd.channel_id:
try:
self._chat = self._client.get_entity(
self.get_input_chat())
except ValueError:
# TODO We could reload the message
pass
return self._chat
@property
def input_chat(self):
"""
Returns the input version of `chat`.
"""
if not self._input_chat and self.original_fwd.channel_id:
try:
self._input_chat = self._client.session.get_input_entity(
self.original_fwd.channel_id)
except ValueError:
pass
return self._input_chat
def get_input_chat(self):
"""
Returns `input_chat` but will make an API call if necessary.
"""
# TODO We could reload the message
return self.input_chat

View File

@ -0,0 +1,302 @@
import hashlib
from .. import functions, types
from ... import utils
class InlineBuilder:
"""
Helper class to allow defining inline queries ``results``.
Common arguments to all methods are
explained here to avoid repetition:
text (`str`, optional):
If present, the user will send a text
message with this text upon being clicked.
link_preview (`bool`, optional):
Whether to show a link preview in the sent
text message or not.
geo (:tl:`InputGeoPoint`, :tl:`GeoPoint`,
:tl:`InputMediaVenue`, :tl:`MessageMediaVenue`,
optional):
If present, it may either be a geo point or a venue.
period (int, optional):
The period in seconds to be used for geo points.
contact (:tl:`InputMediaContact`, :tl:`MessageMediaContact`,
optional):
If present, it must be the contact information to send.
game (`bool`, optional):
May be ``True`` to indicate that the game will be sent.
buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`,
:tl:`KeyboardButton`, optional):
Same as ``buttons`` for `client.send_message
<telethon.client.messages.MessageMethods.send_message>`.
parse_mode (`str`, optional):
Same as ``parse_mode`` for `client.send_message
<telethon.client.messageparse.MessageParseMethods.parse_mode>`.
id (`str`, optional):
The string ID to use for this result. If not present, it
will be the SHA256 hexadecimal digest of converting the
request with empty ID to ``bytes()``, so that the ID will
be deterministic for the same input.
"""
def __init__(self, client):
self._client = client
def article(
self, title, description=None,
*, url=None, thumb=None, content=None,
id=None, text=None, parse_mode=utils.Default, link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None
):
"""
Creates new inline result of article type.
Args:
title (`str`):
The title to be shown for this result.
description (`str`, optional):
Further explanation of what this result means.
url (`str`, optional):
The URL to be shown for this result.
thumb (:tl:`InputWebDocument`, optional):
The thumbnail to be shown for this result.
For now it has to be a :tl:`InputWebDocument` if present.
content (:tl:`InputWebDocument`, optional):
The content to be shown for this result.
For now it has to be a :tl:`InputWebDocument` if present.
"""
# TODO Does 'article' work always?
# article, photo, gif, mpeg4_gif, video, audio,
# voice, document, location, venue, contact, game
result = types.InputBotInlineResult(
id=id or '',
type='article',
send_message=self._message(
text=text, parse_mode=parse_mode, link_preview=link_preview,
geo=geo, period=period,
contact=contact,
game=game,
buttons=buttons
),
title=title,
description=description,
url=url,
thumb=thumb,
content=content
)
if id is None:
result.id = hashlib.sha256(bytes(result)).hexdigest()
return result
def photo(
self, file, *, id=None,
text=None, parse_mode=utils.Default, link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None
):
"""
Creates a new inline result of photo type.
Args:
file (`obj`, optional):
Same as ``file`` for `client.send_file
<telethon.client.uploads.UploadMethods.send_file>`.
"""
fh = self._client.upload_file(file, use_cache=types.InputPhoto)
if not isinstance(fh, types.InputPhoto):
r = self._client(functions.messages.UploadMediaRequest(
types.InputPeerEmpty(), media=types.InputMediaUploadedPhoto(fh)
))
fh = utils.get_input_photo(r.photo)
result = types.InputBotInlineResultPhoto(
id=id or '',
type='photo',
photo=fh,
send_message=self._message(
text=text, parse_mode=parse_mode, link_preview=link_preview,
geo=geo, period=period,
contact=contact,
game=game,
buttons=buttons
)
)
if id is None:
result.id = hashlib.sha256(bytes(result)).hexdigest()
return result
def document(
self, file, title=None, *, description=None, type=None,
mime_type=None, attributes=None, force_document=False,
voice_note=False, video_note=False, use_cache=True, id=None,
text=None, parse_mode=utils.Default, link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None
):
"""
Creates a new inline result of document type.
`use_cache`, `mime_type`, `attributes`, `force_document`,
`voice_note` and `video_note` are described in `client.send_file
<telethon.client.uploads.UploadMethods.send_file>`.
Args:
file (`obj`):
Same as ``file`` for `<client.send_file>
telethon.client.uploads.UploadMethods.send_file`.
title (`str`, optional):
The title to be shown for this result.
description (`str`, optional):
Further explanation of what this result means.
type (`str`, optional):
The type of the document. May be one of: photo, gif,
mpeg4_gif, video, audio, voice, document, sticker.
See "Type of the result" in https://core.telegram.org/bots/api.
"""
if type is None:
if voice_note:
type = 'voice'
else:
type = 'document'
use_cache = types.InputDocument if use_cache else None
fh = self._client.upload_file(file, use_cache=use_cache)
if not isinstance(fh, types.InputDocument):
attributes, mime_type = utils.get_attributes(
file,
mime_type=mime_type,
attributes=attributes,
force_document=force_document,
voice_note=voice_note,
video_note=video_note
)
r = self._client(functions.messages.UploadMediaRequest(
types.InputPeerEmpty(), media=types.InputMediaUploadedDocument(
fh,
mime_type=mime_type,
attributes=attributes,
nosound_video=None,
thumb=None
)))
fh = utils.get_input_document(r.document)
result = types.InputBotInlineResultDocument(
id=id or '',
type=type,
document=fh,
send_message=self._message(
text=text, parse_mode=parse_mode, link_preview=link_preview,
geo=geo, period=period,
contact=contact,
game=game,
buttons=buttons
),
title=title,
description=description
)
if id is None:
result.id = hashlib.sha256(bytes(result)).hexdigest()
return result
def game(
self, short_name, *, id=None,
text=None, parse_mode=utils.Default, link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None
):
"""
Creates a new inline result of game type.
Args:
short_name (`str`):
The short name of the game to use.
"""
result = types.InputBotInlineResultGame(
id=id or '',
short_name=short_name,
send_message=self._message(
text=text, parse_mode=parse_mode, link_preview=link_preview,
geo=geo, period=period,
contact=contact,
game=game,
buttons=buttons
)
)
if id is None:
result.id = hashlib.sha256(bytes(result)).hexdigest()
return result
def _message(
self, *,
text=None, parse_mode=utils.Default, link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None
):
if sum(1 for x in (text, geo, contact, game) if x) != 1:
raise ValueError('Can only use one of text, geo, contact or game')
markup = self._client.build_reply_markup(buttons, inline_only=True)
if text:
text, msg_entities = self._client._parse_message_text(
text, parse_mode
)
return types.InputBotInlineMessageText(
message=text,
no_webpage=not link_preview,
entities=msg_entities,
reply_markup=markup
)
elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)):
return types.InputBotInlineMessageMediaGeo(
geo_point=utils.get_input_geo(geo),
period=period,
reply_markup=markup
)
elif isinstance(geo, (types.InputMediaVenue, types.MessageMediaVenue)):
if isinstance(geo, types.InputMediaVenue):
geo_point = geo.geo_point
else:
geo_point = geo.geo
return types.InputBotInlineMessageMediaVenue(
geo_point=geo_point,
title=geo.title,
address=geo.address,
provider=geo.provider,
venue_id=geo.venue_id,
venue_type=geo.venue_type,
reply_markup=markup
)
elif isinstance(contact, (
types.InputMediaContact, types.MessageMediaContact)):
return types.InputBotInlineMessageMediaContact(
phone_number=contact.phone_number,
first_name=contact.first_name,
last_name=contact.last_name,
vcard=contact.vcard,
reply_markup=markup
)
elif game:
return types.InputBotInlineMessageGame(
reply_markup=markup
)
else:
raise ValueError('No text, game or valid geo or contact given')

View File

@ -0,0 +1,142 @@
from .. import types, functions
from ... import utils
class InlineResult:
"""
Custom class that encapsulates a bot inline result providing
an abstraction to easily access some commonly needed features
(such as clicking a result to select it).
Attributes:
result (:tl:`BotInlineResult`):
The original :tl:`BotInlineResult` object.
"""
ARTICLE = 'article'
PHOTO = 'photo'
GIF = 'gif'
VIDEO = 'video'
VIDEO_GIF = 'mpeg4_gif'
AUDIO = 'audio'
DOCUMENT = 'document'
LOCATION = 'location'
VENUE = 'venue'
CONTACT = 'contact'
GAME = 'game'
def __init__(self, client, original, query_id=None):
self._client = client
self.result = original
self._query_id = query_id
@property
def type(self):
"""
The always-present type of this result. It will be one of:
``'article'``, ``'photo'``, ``'gif'``, ``'mpeg4_gif'``, ``'video'``,
``'audio'``, ``'voice'``, ``'document'``, ``'location'``, ``'venue'``,
``'contact'``, ``'game'``.
You can access all of these constants through `InlineResult`,
such as `InlineResult.ARTICLE`, `InlineResult.VIDEO_GIF`, etc.
"""
return self.result.type
@property
def message(self):
"""
The always-present :tl:`BotInlineMessage` that
will be sent if `click` is called on this result.
"""
return self.result.send_message
@property
def title(self):
"""
The title for this inline result. It may be ``None``.
"""
return self.result.title
@property
def description(self):
"""
The description for this inline result. It may be ``None``.
"""
return self.result.description
@property
def url(self):
"""
The URL present in this inline results. If you want to "click"
this URL to open it in your browser, you should use Python's
`webbrowser.open(url)` for such task.
"""
if isinstance(self.result, types.BotInlineResult):
return self.result.url
@property
def photo(self):
"""
Returns either the :tl:`WebDocument` thumbnail for
normal results or the :tl:`Photo` for media results.
"""
if isinstance(self.result, types.BotInlineResult):
return self.result.thumb
elif isinstance(self.result, types.BotInlineMediaResult):
return self.result.photo
@property
def document(self):
"""
Returns either the :tl:`WebDocument` content for
normal results or the :tl:`Document` for media results.
"""
if isinstance(self.result, types.BotInlineResult):
return self.result.content
elif isinstance(self.result, types.BotInlineMediaResult):
return self.result.document
def click(self, entity, reply_to=None,
silent=False, clear_draft=False):
"""
Clicks this result and sends the associated `message`.
Args:
entity (`entity`):
The entity to which the message of this result should be sent.
reply_to (`int` | :tl:`Message`, optional):
If present, the sent message will reply to this ID or message.
silent (`bool`, optional):
If ``True``, the sent message will not notify the user(s).
clear_draft (`bool`, optional):
Whether the draft should be removed after sending the
message from this result or not. Defaults to ``False``.
"""
entity = self._client.get_input_entity(entity)
reply_id = None if reply_to is None else utils.get_message_id(reply_to)
req = functions.messages.SendInlineBotResultRequest(
peer=entity,
query_id=self._query_id,
id=self.result.id,
silent=silent,
clear_draft=clear_draft,
reply_to_msg_id=reply_id
)
return self._client._get_response_message(
req, self._client(req), entity)
def download_media(self, *args, **kwargs):
"""
Downloads the media in this result (if there is a document, the
document will be downloaded; otherwise, the photo will if present).
This is a wrapper around `client.download_media
<telethon.client.downloads.DownloadMethods.download_media>`.
"""
if self.document or self.photo:
return self._client.download_media(
self.document or self.photo, *args, **kwargs)

File diff suppressed because it is too large Load Diff

View File

@ -5,9 +5,9 @@ import webbrowser
class MessageButton: class MessageButton:
""" """
Custom class that encapsulates a message providing an abstraction to Custom class that encapsulates a message button providing
easily access some commonly needed features (such as the markdown text an abstraction to easily access some commonly needed features
or the text for a given message entity). (such as clicking the button itself).
Attributes: Attributes:

View File

@ -0,0 +1,74 @@
import abc
class SenderGetter(abc.ABC):
"""
Helper base class that introduces the `sender`, `input_sender`
and `sender_id` properties and `get_sender` and `get_input_sender`
methods.
Subclasses **must** have the following private members: `_sender`,
`_input_sender`, `_sender_id` and `_client`. As an end user, you
should not worry about this.
"""
@property
def sender(self):
"""
Returns the :tl:`User` that created this object. It may be ``None``
if the object has no sender or if Telegram didn't send the sender.
If you're using `telethon.events`, use `get_sender` instead.
"""
return self._sender
def get_sender(self):
"""
Returns `sender`, but will make an API call to find the
sender unless it's already cached.
"""
if self._sender is None and self.get_input_sender():
try:
self._sender =\
self._client.get_entity(self._input_sender)
except ValueError:
self._reload_message()
return self._sender
@property
def input_sender(self):
"""
This :tl:`InputPeer` is the input version of the user who
sent the message. Similarly to `input_chat`, this doesn't have
things like username or similar, but still useful in some cases.
Note that this might not be available if the library can't
find the input chat, or if the message a broadcast on a channel.
"""
if self._input_sender is None and self._sender_id:
try:
self._input_sender = self._client.session\
.get_input_entity(self._sender_id)
except ValueError:
pass
return self._input_sender
def get_input_sender(self):
"""
Returns `input_sender`, but will make an API call to find the
input sender unless it's already cached.
"""
if self.input_sender is None and self._sender_id:
self._refetch_sender()
return self._input_sender
@property
def sender_id(self):
"""
Returns the marked sender integer ID, if present.
"""
return self._sender_id
def _refetch_sender(self):
"""
Re-fetches sender information through other means.
"""

View File

@ -1,8 +1,12 @@
import abc
import struct import struct
from datetime import datetime, date from datetime import datetime, date, timedelta
class TLObject: class TLObject:
CONSTRUCTOR_ID = None
SUBCLASS_OF_ID = None
@staticmethod @staticmethod
def pretty_format(obj, indent=None): def pretty_format(obj, indent=None):
""" """
@ -24,10 +28,6 @@ class TLObject:
return '[{}]'.format( return '[{}]'.format(
', '.join(TLObject.pretty_format(x) for x in obj) ', '.join(TLObject.pretty_format(x) for x in obj)
) )
elif isinstance(obj, datetime):
return 'datetime.utcfromtimestamp({})'.format(
int(obj.timestamp())
)
else: else:
return repr(obj) return repr(obj)
else: else:
@ -69,11 +69,6 @@ class TLObject:
result.append('\t' * indent) result.append('\t' * indent)
result.append(']') result.append(']')
elif isinstance(obj, datetime):
result.append('datetime.utcfromtimestamp(')
result.append(repr(int(obj.timestamp())))
result.append(')')
else: else:
result.append(repr(obj)) result.append(repr(obj))
@ -125,6 +120,9 @@ class TLObject:
dt = int(datetime(dt.year, dt.month, dt.day).timestamp()) dt = int(datetime(dt.year, dt.month, dt.day).timestamp())
elif isinstance(dt, float): elif isinstance(dt, float):
dt = int(dt) dt = int(dt)
elif isinstance(dt, timedelta):
# Timezones are tricky. datetime.now() + ... timestamp() works
dt = int((datetime.now() + dt).timestamp())
if isinstance(dt, int): if isinstance(dt, int):
return struct.pack('<I', dt) return struct.pack('<I', dt)

View File

@ -2,36 +2,28 @@
Utilities for working with the Telegram API itself (such as handy methods Utilities for working with the Telegram API itself (such as handy methods
to convert between an entity like an User, Chat, etc. into its Input version) to convert between an entity like an User, Chat, etc. into its Input version)
""" """
import base64
import binascii
import itertools import itertools
import math import math
import mimetypes import mimetypes
import os import os
import re import re
import types import struct
from collections import UserList from collections import UserList
from mimetypes import guess_extension from mimetypes import guess_extension
from types import GeneratorType
from .extensions import markdown, html from .extensions import markdown, html
from .helpers import add_surrogate, del_surrogate from .helpers import add_surrogate, del_surrogate
from .tl.types import ( from .tl import types
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, try:
MessageMediaDocument, MessageMediaPhoto, PeerChannel, InputChannel, import hachoir
UserEmpty, InputUser, InputUserEmpty, InputUserSelf, InputPeerSelf, import hachoir.metadata
PeerChat, PeerUser, User, UserFull, UserProfilePhoto, Document, import hachoir.parser
MessageMediaContact, MessageMediaEmpty, MessageMediaGame, MessageMediaGeo, except ImportError:
MessageMediaUnsupported, MessageMediaVenue, InputMediaContact, hachoir = None
InputMediaDocument, InputMediaEmpty, InputMediaGame,
InputMediaGeoPoint, InputMediaPhoto, InputMediaVenue, InputDocument,
DocumentEmpty, InputDocumentEmpty, Message, GeoPoint, InputGeoPoint,
GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty,
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull,
InputMediaUploadedPhoto, DocumentAttributeFilename, photos,
TopPeer, InputNotifyPeer, InputMessageID, InputFileLocation,
InputDocumentFileLocation, PhotoSizeEmpty, InputDialogPeer
)
from .tl.types.contacts import ResolvedPeer
USERNAME_RE = re.compile( USERNAME_RE = re.compile(
r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
@ -73,7 +65,7 @@ def get_display_name(entity):
Gets the display name for the given entity, if it's an :tl:`User`, Gets the display name for the given entity, if it's an :tl:`User`,
:tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise. :tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise.
""" """
if isinstance(entity, User): if isinstance(entity, types.User):
if entity.last_name and entity.first_name: if entity.last_name and entity.first_name:
return '{} {}'.format(entity.first_name, entity.last_name) return '{} {}'.format(entity.first_name, entity.last_name)
elif entity.first_name: elif entity.first_name:
@ -83,7 +75,7 @@ def get_display_name(entity):
else: else:
return '' return ''
elif isinstance(entity, (Chat, Channel)): elif isinstance(entity, (types.Chat, types.Channel)):
return entity.title return entity.title
return '' return ''
@ -93,13 +85,15 @@ def get_extension(media):
"""Gets the corresponding extension for any Telegram media.""" """Gets the corresponding extension for any Telegram media."""
# Photos are always compressed as .jpg by Telegram # Photos are always compressed as .jpg by Telegram
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)): if isinstance(media, (types.UserProfilePhoto,
types.ChatPhoto, types.MessageMediaPhoto)):
return '.jpg' return '.jpg'
# Documents will come with a mime type # Documents will come with a mime type
if isinstance(media, MessageMediaDocument): if isinstance(media, types.MessageMediaDocument):
media = media.document media = media.document
if isinstance(media, Document): if isinstance(media, (
types.Document, types.WebDocument, types.WebDocumentNoProxy)):
if media.mime_type == 'application/octet-stream': if media.mime_type == 'application/octet-stream':
# Octet stream are just bytes, which have no default extension # Octet stream are just bytes, which have no default extension
return '' return ''
@ -131,38 +125,38 @@ def get_input_peer(entity, allow_self=True):
else: else:
_raise_cast_fail(entity, 'InputPeer') _raise_cast_fail(entity, 'InputPeer')
if isinstance(entity, User): if isinstance(entity, types.User):
if entity.is_self and allow_self: if entity.is_self and allow_self:
return InputPeerSelf() return types.InputPeerSelf()
else: else:
return InputPeerUser(entity.id, entity.access_hash or 0) return types.InputPeerUser(entity.id, entity.access_hash or 0)
if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)): if isinstance(entity, (types.Chat, types.ChatEmpty, types.ChatForbidden)):
return InputPeerChat(entity.id) return types.InputPeerChat(entity.id)
if isinstance(entity, (Channel, ChannelForbidden)): if isinstance(entity, (types.Channel, types.ChannelForbidden)):
return InputPeerChannel(entity.id, entity.access_hash or 0) return types.InputPeerChannel(entity.id, entity.access_hash or 0)
if isinstance(entity, InputUser): if isinstance(entity, types.InputUser):
return InputPeerUser(entity.user_id, entity.access_hash) return types.InputPeerUser(entity.user_id, entity.access_hash)
if isinstance(entity, InputChannel): if isinstance(entity, types.InputChannel):
return InputPeerChannel(entity.channel_id, entity.access_hash) return types.InputPeerChannel(entity.channel_id, entity.access_hash)
if isinstance(entity, InputUserSelf): if isinstance(entity, types.InputUserSelf):
return InputPeerSelf() return types.InputPeerSelf()
if isinstance(entity, UserEmpty): if isinstance(entity, types.UserEmpty):
return InputPeerEmpty() return types.InputPeerEmpty()
if isinstance(entity, UserFull): if isinstance(entity, types.UserFull):
return get_input_peer(entity.user) return get_input_peer(entity.user)
if isinstance(entity, ChatFull): if isinstance(entity, types.ChatFull):
return InputPeerChat(entity.id) return types.InputPeerChat(entity.id)
if isinstance(entity, PeerChat): if isinstance(entity, types.PeerChat):
return InputPeerChat(entity.chat_id) return types.InputPeerChat(entity.chat_id)
_raise_cast_fail(entity, 'InputPeer') _raise_cast_fail(entity, 'InputPeer')
@ -175,11 +169,11 @@ def get_input_channel(entity):
except AttributeError: except AttributeError:
_raise_cast_fail(entity, 'InputChannel') _raise_cast_fail(entity, 'InputChannel')
if isinstance(entity, (Channel, ChannelForbidden)): if isinstance(entity, (types.Channel, types.ChannelForbidden)):
return InputChannel(entity.id, entity.access_hash or 0) return types.InputChannel(entity.id, entity.access_hash or 0)
if isinstance(entity, InputPeerChannel): if isinstance(entity, types.InputPeerChannel):
return InputChannel(entity.channel_id, entity.access_hash) return types.InputChannel(entity.channel_id, entity.access_hash)
_raise_cast_fail(entity, 'InputChannel') _raise_cast_fail(entity, 'InputChannel')
@ -192,23 +186,23 @@ def get_input_user(entity):
except AttributeError: except AttributeError:
_raise_cast_fail(entity, 'InputUser') _raise_cast_fail(entity, 'InputUser')
if isinstance(entity, User): if isinstance(entity, types.User):
if entity.is_self: if entity.is_self:
return InputUserSelf() return types.InputUserSelf()
else: else:
return InputUser(entity.id, entity.access_hash or 0) return types.InputUser(entity.id, entity.access_hash or 0)
if isinstance(entity, InputPeerSelf): if isinstance(entity, types.InputPeerSelf):
return InputUserSelf() return types.InputUserSelf()
if isinstance(entity, (UserEmpty, InputPeerEmpty)): if isinstance(entity, (types.UserEmpty, types.InputPeerEmpty)):
return InputUserEmpty() return types.InputUserEmpty()
if isinstance(entity, UserFull): if isinstance(entity, types.UserFull):
return get_input_user(entity.user) return get_input_user(entity.user)
if isinstance(entity, InputPeerUser): if isinstance(entity, types.InputPeerUser):
return InputUser(entity.user_id, entity.access_hash) return types.InputUser(entity.user_id, entity.access_hash)
_raise_cast_fail(entity, 'InputUser') _raise_cast_fail(entity, 'InputUser')
@ -219,12 +213,12 @@ def get_input_dialog(dialog):
if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer')
return dialog return dialog
if dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') if dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
return InputDialogPeer(dialog) return types.InputDialogPeer(dialog)
except AttributeError: except AttributeError:
_raise_cast_fail(dialog, 'InputDialogPeer') _raise_cast_fail(dialog, 'InputDialogPeer')
try: try:
return InputDialogPeer(get_input_peer(dialog)) return types.InputDialogPeer(get_input_peer(dialog))
except TypeError: except TypeError:
pass pass
@ -239,16 +233,17 @@ def get_input_document(document):
except AttributeError: except AttributeError:
_raise_cast_fail(document, 'InputDocument') _raise_cast_fail(document, 'InputDocument')
if isinstance(document, Document): if isinstance(document, types.Document):
return InputDocument(id=document.id, access_hash=document.access_hash) return types.InputDocument(
id=document.id, access_hash=document.access_hash)
if isinstance(document, DocumentEmpty): if isinstance(document, types.DocumentEmpty):
return InputDocumentEmpty() return types.InputDocumentEmpty()
if isinstance(document, MessageMediaDocument): if isinstance(document, types.MessageMediaDocument):
return get_input_document(document.document) return get_input_document(document.document)
if isinstance(document, Message): if isinstance(document, types.Message):
return get_input_document(document.media) return get_input_document(document.media)
_raise_cast_fail(document, 'InputDocument') _raise_cast_fail(document, 'InputDocument')
@ -262,14 +257,14 @@ def get_input_photo(photo):
except AttributeError: except AttributeError:
_raise_cast_fail(photo, 'InputPhoto') _raise_cast_fail(photo, 'InputPhoto')
if isinstance(photo, photos.Photo): if isinstance(photo, types.photos.Photo):
photo = photo.photo photo = photo.photo
if isinstance(photo, Photo): if isinstance(photo, types.Photo):
return InputPhoto(id=photo.id, access_hash=photo.access_hash) return types.InputPhoto(id=photo.id, access_hash=photo.access_hash)
if isinstance(photo, PhotoEmpty): if isinstance(photo, types.PhotoEmpty):
return InputPhotoEmpty() return types.InputPhotoEmpty()
_raise_cast_fail(photo, 'InputPhoto') _raise_cast_fail(photo, 'InputPhoto')
@ -282,16 +277,16 @@ def get_input_geo(geo):
except AttributeError: except AttributeError:
_raise_cast_fail(geo, 'InputGeoPoint') _raise_cast_fail(geo, 'InputGeoPoint')
if isinstance(geo, GeoPoint): if isinstance(geo, types.GeoPoint):
return InputGeoPoint(lat=geo.lat, long=geo.long) return types.InputGeoPoint(lat=geo.lat, long=geo.long)
if isinstance(geo, GeoPointEmpty): if isinstance(geo, types.GeoPointEmpty):
return InputGeoPointEmpty() return types.InputGeoPointEmpty()
if isinstance(geo, MessageMediaGeo): if isinstance(geo, types.MessageMediaGeo):
return get_input_geo(geo.geo) return get_input_geo(geo.geo)
if isinstance(geo, Message): if isinstance(geo, types.Message):
return get_input_geo(geo.media) return get_input_geo(geo.media)
_raise_cast_fail(geo, 'InputGeoPoint') _raise_cast_fail(geo, 'InputGeoPoint')
@ -308,67 +303,67 @@ def get_input_media(media, is_photo=False):
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') elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
return InputMediaPhoto(media) return types.InputMediaPhoto(media)
elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument') elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument')
return InputMediaDocument(media) return types.InputMediaDocument(media)
except AttributeError: except AttributeError:
_raise_cast_fail(media, 'InputMedia') _raise_cast_fail(media, 'InputMedia')
if isinstance(media, MessageMediaPhoto): if isinstance(media, types.MessageMediaPhoto):
return InputMediaPhoto( return types.InputMediaPhoto(
id=get_input_photo(media.photo), id=get_input_photo(media.photo),
ttl_seconds=media.ttl_seconds ttl_seconds=media.ttl_seconds
) )
if isinstance(media, (Photo, photos.Photo, PhotoEmpty)): if isinstance(media, (types.Photo, types.photos.Photo, types.PhotoEmpty)):
return InputMediaPhoto( return types.InputMediaPhoto(
id=get_input_photo(media) id=get_input_photo(media)
) )
if isinstance(media, MessageMediaDocument): if isinstance(media, types.MessageMediaDocument):
return InputMediaDocument( return types.InputMediaDocument(
id=get_input_document(media.document), id=get_input_document(media.document),
ttl_seconds=media.ttl_seconds ttl_seconds=media.ttl_seconds
) )
if isinstance(media, (Document, DocumentEmpty)): if isinstance(media, (types.Document, types.DocumentEmpty)):
return InputMediaDocument( return types.InputMediaDocument(
id=get_input_document(media) id=get_input_document(media)
) )
if isinstance(media, FileLocation): if isinstance(media, types.FileLocation):
if is_photo: if is_photo:
return InputMediaUploadedPhoto(file=media) return types.InputMediaUploadedPhoto(file=media)
else: else:
return InputMediaUploadedDocument( return types.InputMediaUploadedDocument(
file=media, file=media,
mime_type='application/octet-stream', # unknown, assume bytes mime_type='application/octet-stream', # unknown, assume bytes
attributes=[DocumentAttributeFilename('unnamed')] attributes=[types.DocumentAttributeFilename('unnamed')]
) )
if isinstance(media, MessageMediaGame): if isinstance(media, types.MessageMediaGame):
return InputMediaGame(id=media.game.id) return types.InputMediaGame(id=media.game.id)
if isinstance(media, (ChatPhoto, UserProfilePhoto)): if isinstance(media, (types.ChatPhoto, types.UserProfilePhoto)):
if isinstance(media.photo_big, FileLocationUnavailable): if isinstance(media.photo_big, types.FileLocationUnavailable):
media = media.photo_small media = media.photo_small
else: else:
media = media.photo_big media = media.photo_big
return get_input_media(media, is_photo=True) return get_input_media(media, is_photo=True)
if isinstance(media, MessageMediaContact): if isinstance(media, types.MessageMediaContact):
return InputMediaContact( return types.InputMediaContact(
phone_number=media.phone_number, phone_number=media.phone_number,
first_name=media.first_name, first_name=media.first_name,
last_name=media.last_name, last_name=media.last_name,
vcard='' vcard=''
) )
if isinstance(media, MessageMediaGeo): if isinstance(media, types.MessageMediaGeo):
return InputMediaGeoPoint(geo_point=get_input_geo(media.geo)) return types.InputMediaGeoPoint(geo_point=get_input_geo(media.geo))
if isinstance(media, MessageMediaVenue): if isinstance(media, types.MessageMediaVenue):
return InputMediaVenue( return types.InputMediaVenue(
geo_point=get_input_geo(media.geo), geo_point=get_input_geo(media.geo),
title=media.title, title=media.title,
address=media.address, address=media.address,
@ -378,11 +373,12 @@ def get_input_media(media, is_photo=False):
) )
if isinstance(media, ( if isinstance(media, (
MessageMediaEmpty, MessageMediaUnsupported, types.MessageMediaEmpty, types.MessageMediaUnsupported,
ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable)): types.ChatPhotoEmpty, types.UserProfilePhotoEmpty,
return InputMediaEmpty() types.FileLocationUnavailable)):
return types.InputMediaEmpty()
if isinstance(media, Message): if isinstance(media, types.Message):
return get_input_media(media.media, is_photo=is_photo) return get_input_media(media.media, is_photo=is_photo)
_raise_cast_fail(media, 'InputMedia') _raise_cast_fail(media, 'InputMedia')
@ -392,11 +388,11 @@ def get_input_message(message):
"""Similar to :meth:`get_input_peer`, but for input messages.""" """Similar to :meth:`get_input_peer`, but for input messages."""
try: try:
if isinstance(message, int): # This case is really common too if isinstance(message, int): # This case is really common too
return InputMessageID(message) return types.InputMessageID(message)
elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'): elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'):
return message return message
elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'): elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'):
return InputMessageID(message.id) return types.InputMessageID(message.id)
except AttributeError: except AttributeError:
pass pass
@ -404,16 +400,13 @@ def get_input_message(message):
def get_message_id(message): def get_message_id(message):
"""Sanitizes the 'reply_to' parameter a user may send""" """Similar to :meth:`get_input_peer`, but for message IDs."""
if message is None: if message is None:
return None return None
if isinstance(message, int): if isinstance(message, int):
return message return message
if hasattr(message, 'original_message'):
return message.original_message.id
try: try:
if message.SUBCLASS_OF_ID == 0x790009e3: if message.SUBCLASS_OF_ID == 0x790009e3:
# hex(crc32(b'Message')) = 0x790009e3 # hex(crc32(b'Message')) = 0x790009e3
@ -424,6 +417,71 @@ def get_message_id(message):
raise TypeError('Invalid message type: {}'.format(type(message))) raise TypeError('Invalid message type: {}'.format(type(message)))
def get_attributes(file, *, attributes=None, mime_type=None,
force_document=False, voice_note=False, video_note=False):
"""
Get a list of attributes for the given file and
the mime type as a tuple ([attribute], mime_type).
"""
name = file if isinstance(file, str) else getattr(file, 'name', 'unnamed')
if mime_type is None:
mime_type = mimetypes.guess_type(name)[0]
attr_dict = {types.DocumentAttributeFilename:
types.DocumentAttributeFilename(os.path.basename(name))}
if is_audio(file) and hachoir is not None:
with hachoir.parser.createParser(file) as parser:
m = hachoir.metadata.extractMetadata(parser)
attr_dict[types.DocumentAttributeAudio] = \
types.DocumentAttributeAudio(
voice=voice_note,
title=m.get('title') if m.has('title') else None,
performer=m.get('author') if m.has('author') else None,
duration=int(m.get('duration').seconds
if m.has('duration') else 0)
)
if not force_document and is_video(file):
if hachoir:
with hachoir.parser.createParser(file) as parser:
m = hachoir.metadata.extractMetadata(parser)
doc = types.DocumentAttributeVideo(
round_message=video_note,
w=m.get('width') if m.has('width') else 0,
h=m.get('height') if m.has('height') else 0,
duration=int(m.get('duration').seconds
if m.has('duration') else 0)
)
else:
doc = types.DocumentAttributeVideo(
0, 1, 1, round_message=video_note)
attr_dict[types.DocumentAttributeVideo] = doc
if voice_note:
if types.DocumentAttributeAudio in attr_dict:
attr_dict[types.DocumentAttributeAudio].voice = True
else:
attr_dict[types.DocumentAttributeAudio] = \
types.DocumentAttributeAudio(0, voice=True)
# Now override the attributes if any. As we have a dict of
# {cls: instance}, we can override any class with the list
# of attributes provided by the user easily.
if attributes:
for a in attributes:
attr_dict[type(a)] = a
# Ensure we have a mime type, any; but it cannot be None
# 'The "octet-stream" subtype is used to indicate that a body
# contains arbitrary binary data.'
if not mime_type:
mime_type = 'application/octet-stream'
return list(attr_dict.values()), mime_type
def sanitize_parse_mode(mode): def sanitize_parse_mode(mode):
""" """
Converts the given parse mode into an object with Converts the given parse mode into an object with
@ -458,34 +516,42 @@ def sanitize_parse_mode(mode):
def get_input_location(location): def get_input_location(location):
"""Similar to :meth:`get_input_peer`, but for input messages.""" """
Similar to :meth:`get_input_peer`, but for input messages.
Note that this returns a tuple ``(dc_id, location)``, the
``dc_id`` being present if known.
"""
try: try:
if location.SUBCLASS_OF_ID == 0x1523d462: if location.SUBCLASS_OF_ID == 0x1523d462:
return location # crc32(b'InputFileLocation'): return None, location # crc32(b'InputFileLocation'):
except AttributeError: except AttributeError:
_raise_cast_fail(location, 'InputFileLocation') _raise_cast_fail(location, 'InputFileLocation')
if isinstance(location, Message): if isinstance(location, types.Message):
location = location.media location = location.media
if isinstance(location, MessageMediaDocument): if isinstance(location, types.MessageMediaDocument):
location = location.document location = location.document
elif isinstance(location, MessageMediaPhoto): elif isinstance(location, types.MessageMediaPhoto):
location = location.photo location = location.photo
if isinstance(location, Document): if isinstance(location, types.Document):
return InputDocumentFileLocation( return (location.dc_id, types.InputDocumentFileLocation(
location.id, location.access_hash, location.version) location.id, location.access_hash, location.version))
elif isinstance(location, Photo): elif isinstance(location, types.Photo):
try: try:
location = next(x for x in reversed(location.sizes) location = next(
if not isinstance(x, PhotoSizeEmpty)).location x for x in reversed(location.sizes)
if not isinstance(x, types.PhotoSizeEmpty)
).location
except StopIteration: except StopIteration:
pass pass
if isinstance(location, (FileLocation, FileLocationUnavailable)): if isinstance(location, (
return InputFileLocation( types.FileLocation, types.FileLocationUnavailable)):
location.volume_id, location.local_id, location.secret) return (getattr(location, 'dc_id', None), types.InputFileLocation(
location.volume_id, location.local_id, location.secret))
_raise_cast_fail(location, 'InputFileLocation') _raise_cast_fail(location, 'InputFileLocation')
@ -538,7 +604,7 @@ def is_list_like(obj):
other things), so just support the commonly known list-like objects. other things), so just support the commonly known list-like objects.
""" """
return isinstance(obj, (list, tuple, set, dict, return isinstance(obj, (list, tuple, set, dict,
UserList, types.GeneratorType)) UserList, GeneratorType))
def parse_phone(phone): def parse_phone(phone):
@ -609,7 +675,9 @@ def get_peer_id(peer, add_mark=True):
try: try:
if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6):
if isinstance(peer, (ResolvedPeer, InputNotifyPeer, TopPeer)): if isinstance(peer, (
types.contacts.ResolvedPeer, types.InputNotifyPeer,
types.TopPeer)):
peer = peer.peer peer = peer.peer
else: else:
# Not a Peer or an InputPeer, so first get its Input version # Not a Peer or an InputPeer, so first get its Input version
@ -618,16 +686,17 @@ def get_peer_id(peer, add_mark=True):
_raise_cast_fail(peer, 'int') _raise_cast_fail(peer, 'int')
# Set the right ID/kind, or raise if the TLObject is not recognised # Set the right ID/kind, or raise if the TLObject is not recognised
if isinstance(peer, (PeerUser, InputPeerUser)): if isinstance(peer, (types.PeerUser, types.InputPeerUser)):
return peer.user_id return peer.user_id
elif isinstance(peer, (PeerChat, InputPeerChat)): elif isinstance(peer, (types.PeerChat, types.InputPeerChat)):
# Check in case the user mixed things up to avoid blowing up # Check in case the user mixed things up to avoid blowing up
if not (0 < peer.chat_id <= 0x7fffffff): if not (0 < peer.chat_id <= 0x7fffffff):
peer.chat_id = resolve_id(peer.chat_id)[0] peer.chat_id = resolve_id(peer.chat_id)[0]
return -peer.chat_id if add_mark else peer.chat_id return -peer.chat_id if add_mark else peer.chat_id
elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)): elif isinstance(peer, (
if isinstance(peer, ChannelFull): types.PeerChannel, types.InputPeerChannel, types.ChannelFull)):
if isinstance(peer, types.ChannelFull):
# Special case: .get_input_peer can't return InputChannel from # Special case: .get_input_peer can't return InputChannel from
# ChannelFull since it doesn't have an .access_hash attribute. # ChannelFull since it doesn't have an .access_hash attribute.
i = peer.id i = peer.id
@ -637,7 +706,7 @@ def get_peer_id(peer, add_mark=True):
# Check in case the user mixed things up to avoid blowing up # Check in case the user mixed things up to avoid blowing up
if not (0 < i <= 0x7fffffff): if not (0 < i <= 0x7fffffff):
i = resolve_id(i)[0] i = resolve_id(i)[0]
if isinstance(peer, ChannelFull): if isinstance(peer, types.ChannelFull):
peer.id = i peer.id = i
else: else:
peer.channel_id = i peer.channel_id = i
@ -655,7 +724,7 @@ def get_peer_id(peer, add_mark=True):
def resolve_id(marked_id): def resolve_id(marked_id):
"""Given a marked ID, returns the original ID and its :tl:`Peer` type.""" """Given a marked ID, returns the original ID and its :tl:`Peer` type."""
if marked_id >= 0: if marked_id >= 0:
return marked_id, PeerUser return marked_id, types.PeerUser
# There have been report of chat IDs being 10000xyz, which means their # There have been report of chat IDs being 10000xyz, which means their
# marked version is -10000xyz, which in turn looks like a channel but # marked version is -10000xyz, which in turn looks like a channel but
@ -663,9 +732,105 @@ def resolve_id(marked_id):
# two zeroes. # two zeroes.
m = re.match(r'-100([^0]\d*)', str(marked_id)) m = re.match(r'-100([^0]\d*)', str(marked_id))
if m: if m:
return int(m.group(1)), PeerChannel return int(m.group(1)), types.PeerChannel
return -marked_id, PeerChat return -marked_id, types.PeerChat
def _rle_decode(data):
"""
Decodes run-length-encoded `data`.
"""
new = b''
last = b''
for cur in data:
cur = bytes([cur])
if last == b'\0':
new += last * ord(cur)
last = b''
else:
new += last
last = cur
return new + last
def resolve_bot_file_id(file_id):
"""
Given a Bot API-style `file_id`, returns the media it represents.
If the `file_id` is not valid, ``None`` is returned instead.
Note that the `file_id` does not have information such as image
dimensions or file size, so these will be zero if present.
For thumbnails, the photo ID and hash will always be zero.
"""
try:
data = file_id.encode('ascii')
except (UnicodeEncodeError, AttributeError):
return None
data.replace(b'-', b'+').replace(b'_', b'/') + b'=' * (len(data) % 4)
try:
data = base64.b64decode(data)
except binascii.Error:
return None
data = _rle_decode(data)
if data[-1] == b'\x02':
return None
data = data[:-1]
if len(data) == 24:
file_type, dc_id, media_id, access_hash = struct.unpack('<iiqq', data)
attributes = []
if file_type == 3 or file_type == 9:
attributes.append(types.DocumentAttributeAudio(
duration=0,
voice=file_type == 3
))
elif file_type == 4 or file_type == 13:
attributes.append(types.DocumentAttributeVideo(
duration=0,
w=0,
h=0,
round_message=file_type == 13
))
# elif file_type == 5: # other, cannot know which
elif file_type == 8:
attributes.append(types.DocumentAttributeSticker(
alt='',
stickerset=types.InputStickerSetEmpty()
))
elif file_type == 10:
attributes.append(types.DocumentAttributeAnimated())
print(file_type)
return types.Document(
id=media_id,
access_hash=access_hash,
date=None,
mime_type='',
size=0,
thumb=types.PhotoSizeEmpty('s'),
dc_id=dc_id,
version=0,
attributes=attributes
)
elif len(data) == 44:
(file_type, dc_id, media_id, access_hash,
volume_id, secret, local_id) = struct.unpack('<iiqqqqi', data)
# Thumbnails (small) always have ID 0; otherwise size 'x'
photo_size = 's' if media_id or access_hash else 'x'
return types.Photo(id=media_id, access_hash=access_hash, sizes=[
types.PhotoSize(photo_size, location=types.FileLocation(
dc_id=dc_id,
volume_id=volume_id,
secret=secret,
local_id=local_id
), w=0, h=0, size=0)
], date=None)
def get_appropriated_part_size(file_size): def get_appropriated_part_size(file_size):

View File

@ -1,3 +1,3 @@
# Versions should comply with PEP440. # Versions should comply with PEP440.
# This line is parsed in setup.py: # This line is parsed in setup.py:
__version__ = '1.0.4' __version__ = '1.1.1'

View File

@ -66,3 +66,5 @@ FLOOD_WAIT_X=A wait of {} seconds is required
FLOOD_TEST_PHONE_WAIT_X=A wait of {} seconds is required in the test servers FLOOD_TEST_PHONE_WAIT_X=A wait of {} seconds is required in the test servers
TAKEOUT_INIT_DELAY_X=A wait of {} seconds is required before being able to initiate the takeout TAKEOUT_INIT_DELAY_X=A wait of {} seconds is required before being able to initiate the takeout
CHAT_NOT_MODIFIED=The chat or channel wasn't modified (title, invites, username, admins, etc. are the same) CHAT_NOT_MODIFIED=The chat or channel wasn't modified (title, invites, username, admins, etc. are the same)
URL_INVALID=The URL used was invalid (e.g. when answering a callback with an URL that's not t.me/yourbot or your game's URL)
USER_NOT_PARTICIPANT=The target user is not a member of the specified megagroup or channel

File diff suppressed because one or more lines are too long

View File

@ -149,7 +149,7 @@ users.getUsers#0d91a548 id:Vector&lt;InputUser&gt; = Vector&lt;User&gt;</pre>
<li id="bool"><b>Bool</b>: <li id="bool"><b>Bool</b>:
Either <code>True</code> or <code>False</code>. Either <code>True</code> or <code>False</code>.
</li> </li>
<li id="true"><b>true</b>: <li id="true"><b>flag</b>:
These arguments aren't actually sent but rather encoded as flags. These arguments aren't actually sent but rather encoded as flags.
Any truthy value (<code>True</code>, <code>7</code>) will enable Any truthy value (<code>True</code>, <code>7</code>) will enable
this flag, although it's recommended to use <code>True</code> or this flag, although it's recommended to use <code>True</code> or

View File

@ -188,7 +188,8 @@ def _get_description(arg):
desc.append('If left unspecified, it will be inferred automatically.') desc.append('If left unspecified, it will be inferred automatically.')
otherwise = True otherwise = True
elif arg.is_flag: elif arg.is_flag:
desc.append('This argument can be omitted.') desc.append('This argument defaults to '
'<code>None</code> and can be omitted.')
otherwise = True otherwise = True
if arg.type in {'InputPeer', 'InputUser', 'InputChannel'}: if arg.type in {'InputPeer', 'InputUser', 'InputChannel'}:
@ -370,11 +371,12 @@ def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
bold=True) bold=True)
# Type row # Type row
friendly_type = 'flag' if arg.type == 'true' else arg.type
if arg.is_generic: if arg.is_generic:
docs.add_row('!' + arg.type, align='center') docs.add_row('!' + friendly_type, align='center')
else: else:
docs.add_row( docs.add_row(
arg.type, align='center', link= friendly_type, align='center', link=
path_for_type(arg.type, relative_to=filename) path_for_type(arg.type, relative_to=filename)
) )

View File

@ -36,6 +36,13 @@ NAMED_AUTO_CASTS = {
BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128', BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128',
'int256', 'double', 'Bool', 'true', 'date') 'int256', 'double', 'Bool', 'true', 'date')
# Patched types {fullname: custom.ns.Name}
PATCHED_TYPES = {
'messageEmpty': 'message.Message',
'message': 'message.Message',
'messageService': 'message.Message'
}
def _write_modules( def _write_modules(
out_dir, depth, kind, namespace_tlobjects, type_constructors): out_dir, depth, kind, namespace_tlobjects, type_constructors):
@ -130,11 +137,14 @@ def _write_modules(
# Generate the class for every TLObject # Generate the class for every TLObject
for t in tlobjects: for t in tlobjects:
_write_source_code(t, kind, builder, type_constructors) if t.fullname in PATCHED_TYPES:
builder.current_indent = 0 builder.writeln('{} = None # Patched', t.class_name)
else:
_write_source_code(t, kind, builder, type_constructors)
builder.current_indent = 0
# Write the type definitions generated earlier. # Write the type definitions generated earlier.
builder.writeln('') builder.writeln()
for line in type_defs: for line in type_defs:
builder.writeln(line) builder.writeln(line)
@ -618,11 +628,38 @@ def _write_arg_read_code(builder, arg, args, name):
arg.is_flag = True arg.is_flag = True
def _write_patched(out_dir, namespace_tlobjects):
os.makedirs(out_dir, exist_ok=True)
for ns, tlobjects in namespace_tlobjects.items():
file = os.path.join(out_dir, '{}.py'.format(ns or '__init__'))
with open(file, 'w', encoding='utf-8') as f,\
SourceBuilder(f) as builder:
builder.writeln(AUTO_GEN_NOTICE)
builder.writeln('import struct')
builder.writeln('from .. import types, custom')
builder.writeln()
for t in tlobjects:
builder.writeln('class {}(custom.{}):', t.class_name,
PATCHED_TYPES[t.fullname])
_write_to_dict(t, builder)
_write_to_bytes(t, builder)
_write_from_reader(t, builder)
builder.current_indent = 0
builder.writeln()
builder.writeln(
'types.{1}{0} = {0}', t.class_name,
'{}.'.format(t.namespace) if t.namespace else ''
)
builder.writeln()
def _write_all_tlobjects(tlobjects, layer, builder): def _write_all_tlobjects(tlobjects, layer, builder):
builder.writeln(AUTO_GEN_NOTICE) builder.writeln(AUTO_GEN_NOTICE)
builder.writeln() builder.writeln()
builder.writeln('from . import types, functions') builder.writeln('from . import types, functions, patched')
builder.writeln() builder.writeln()
# Create a constant variable to indicate which layer this is # Create a constant variable to indicate which layer this is
@ -636,9 +673,14 @@ def _write_all_tlobjects(tlobjects, layer, builder):
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
for tlobject in tlobjects: for tlobject in tlobjects:
builder.write('{:#010x}: ', tlobject.id) builder.write('{:#010x}: ', tlobject.id)
builder.write('functions' if tlobject.is_function else 'types') # TODO Probably circular dependency
if tlobject.fullname in PATCHED_TYPES:
builder.write('patched')
else:
builder.write('functions' if tlobject.is_function else 'types')
if tlobject.namespace: if tlobject.namespace:
builder.write('.' + tlobject.namespace) builder.write('.{}', tlobject.namespace)
builder.writeln('.{},', tlobject.class_name) builder.writeln('.{},', tlobject.class_name)
@ -647,13 +689,10 @@ def _write_all_tlobjects(tlobjects, layer, builder):
def generate_tlobjects(tlobjects, layer, import_depth, output_dir): def generate_tlobjects(tlobjects, layer, import_depth, output_dir):
get_file = functools.partial(os.path.join, output_dir)
os.makedirs(get_file('functions'), exist_ok=True)
os.makedirs(get_file('types'), exist_ok=True)
# Group everything by {namespace: [tlobjects]} to generate __init__.py # Group everything by {namespace: [tlobjects]} to generate __init__.py
namespace_functions = defaultdict(list) namespace_functions = defaultdict(list)
namespace_types = defaultdict(list) namespace_types = defaultdict(list)
namespace_patched = defaultdict(list)
# Group {type: [constructors]} to generate the documentation # Group {type: [constructors]} to generate the documentation
type_constructors = defaultdict(list) type_constructors = defaultdict(list)
@ -663,11 +702,15 @@ def generate_tlobjects(tlobjects, layer, import_depth, output_dir):
else: else:
namespace_types[tlobject.namespace].append(tlobject) namespace_types[tlobject.namespace].append(tlobject)
type_constructors[tlobject.result].append(tlobject) type_constructors[tlobject.result].append(tlobject)
if tlobject.fullname in PATCHED_TYPES:
namespace_patched[tlobject.namespace].append(tlobject)
get_file = functools.partial(os.path.join, output_dir)
_write_modules(get_file('functions'), import_depth, 'TLRequest', _write_modules(get_file('functions'), import_depth, 'TLRequest',
namespace_functions, type_constructors) namespace_functions, type_constructors)
_write_modules(get_file('types'), import_depth, 'TLObject', _write_modules(get_file('types'), import_depth, 'TLObject',
namespace_types, type_constructors) namespace_types, type_constructors)
_write_patched(get_file('patched'), namespace_patched)
filename = os.path.join(get_file('alltlobjects.py')) filename = os.path.join(get_file('alltlobjects.py'))
with open(filename, 'w', encoding='utf-8') as file: with open(filename, 'w', encoding='utf-8') as file: