mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-02 19:20:09 +03:00
Merge branch 'master' into sync
This commit is contained in:
commit
3177e3a956
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
45
telethon/client/bots.py
Normal 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
|
||||||
|
]
|
69
telethon/client/buttons.py
Normal file
69
telethon/client/buttons.py
Normal 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)
|
|
@ -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)),
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
69
telethon/crypto/libssl.py
Normal 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)
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
254
telethon/events/callbackquery.py
Normal file
254
telethon/events/callbackquery.py
Normal 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
|
||||||
|
)
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
177
telethon/events/inlinequery.py
Normal file
177
telethon/events/inlinequery.py
Normal 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
|
||||||
|
)
|
||||||
|
)
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
134
telethon/tl/custom/button.py
Normal file
134
telethon/tl/custom/button.py
Normal 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()
|
114
telethon/tl/custom/chatgetter.py
Normal file
114
telethon/tl/custom/chatgetter.py
Normal 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.
|
||||||
|
"""
|
|
@ -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
|
|
||||||
|
|
302
telethon/tl/custom/inline.py
Normal file
302
telethon/tl/custom/inline.py
Normal 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')
|
142
telethon/tl/custom/inlineresult.py
Normal file
142
telethon/tl/custom/inlineresult.py
Normal 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
|
@ -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:
|
||||||
|
|
||||||
|
|
74
telethon/tl/custom/sendergetter.py
Normal file
74
telethon/tl/custom/sendergetter.py
Normal 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.
|
||||||
|
"""
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
@ -149,7 +149,7 @@ users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User></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
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user