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
|
||||
telethon/tl/functions/
|
||||
telethon/tl/types/
|
||||
telethon/tl/patched/
|
||||
telethon/tl/alltlobjects.py
|
||||
telethon/errors/rpcerrorlist.py
|
||||
|
||||
|
|
|
@ -13,10 +13,12 @@ from .telegrambaseclient import TelegramBaseClient
|
|||
from .users import UserMethods # Required for everything
|
||||
from .messageparse import MessageParseMethods # Required for messages
|
||||
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 .chats import ChatMethods
|
||||
from .dialogs import DialogMethods
|
||||
from .downloads import DownloadMethods
|
||||
from .auth import AuthMethods
|
||||
from .updates import UpdateMethods
|
||||
from .bots import BotMethods
|
||||
from .telegramclient import TelegramClient
|
||||
|
|
|
@ -16,7 +16,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
def start(
|
||||
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: '),
|
||||
*,
|
||||
bot_token=None, force_sms=False, code_callback=None,
|
||||
|
@ -45,7 +45,8 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
Args:
|
||||
phone (`str` | `int` | `callable`):
|
||||
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):
|
||||
The password for 2 Factor Authentication (2FA).
|
||||
|
@ -118,14 +119,21 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
if self.is_user_authorized():
|
||||
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:
|
||||
self.sign_in(bot_token=bot_token)
|
||||
return self
|
||||
|
||||
# Turn the callable into a valid phone number
|
||||
while callable(phone):
|
||||
phone = utils.parse_phone(phone()) or phone
|
||||
|
||||
me = None
|
||||
attempts = 0
|
||||
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.
|
||||
|
||||
aggressive (`bool`, optional):
|
||||
Aggressively looks for all participants in the chat in
|
||||
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.
|
||||
Aggressively looks for all participants in the chat.
|
||||
|
||||
This has no effect for groups or channels with less than
|
||||
10,000 members, or if a ``filter`` is given.
|
||||
This is useful for channels since 20 July 2018,
|
||||
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):
|
||||
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)
|
||||
if isinstance(entity, types.InputPeerChannel):
|
||||
if _total or (aggressive and not filter):
|
||||
total = (self(functions.channels.GetFullChannelRequest(
|
||||
entity
|
||||
))).full_chat.participants_count
|
||||
if _total:
|
||||
_total[0] = total
|
||||
else:
|
||||
total = 0
|
||||
if _total:
|
||||
_total[0] = (self(
|
||||
functions.channels.GetFullChannelRequest(entity)
|
||||
)).full_chat.participants_count
|
||||
|
||||
if limit == 0:
|
||||
return
|
||||
|
||||
seen = set()
|
||||
if total > 10000 and aggressive and not filter:
|
||||
if aggressive and not filter:
|
||||
requests = [functions.channels.GetParticipantsRequest(
|
||||
channel=entity,
|
||||
filter=types.ChannelParticipantsSearch(search + chr(x)),
|
||||
|
|
|
@ -77,10 +77,14 @@ class DialogMethods(UserMethods):
|
|||
|
||||
if _total:
|
||||
_total[0] = getattr(r, 'count', len(r.dialogs))
|
||||
|
||||
entities = {utils.get_peer_id(x): x
|
||||
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
|
||||
if len(r.dialogs) > limit:
|
||||
|
|
|
@ -3,6 +3,7 @@ import io
|
|||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import urllib.request
|
||||
|
||||
from .users import UserMethods
|
||||
from .. import utils, helpers, errors
|
||||
|
@ -123,6 +124,9 @@ class DownloadMethods(UserMethods):
|
|||
date = datetime.datetime.now()
|
||||
media = message
|
||||
|
||||
if isinstance(media, str):
|
||||
media = utils.resolve_bot_file_id(media)
|
||||
|
||||
if isinstance(media, types.MessageMediaWebPage):
|
||||
if isinstance(media.webpage, types.WebPage):
|
||||
media = media.webpage.document or media.webpage.photo
|
||||
|
@ -140,6 +144,10 @@ class DownloadMethods(UserMethods):
|
|||
return self._download_contact(
|
||||
media, file
|
||||
)
|
||||
elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)):
|
||||
return self._download_web_document(
|
||||
media, file, progress_callback
|
||||
)
|
||||
|
||||
def download_file(
|
||||
self, input_location, file=None, *, part_size_kb=None,
|
||||
|
@ -200,10 +208,27 @@ class DownloadMethods(UserMethods):
|
|||
else:
|
||||
f = file
|
||||
|
||||
# The used sender will change if ``FileMigrateError`` occurs
|
||||
sender = self._sender
|
||||
exported = False
|
||||
input_location = utils.get_input_location(input_location)
|
||||
dc_id, input_location = utils.get_input_location(input_location)
|
||||
exported = dc_id and self.session.dc_id != dc_id
|
||||
if exported:
|
||||
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)
|
||||
try:
|
||||
|
@ -281,19 +306,12 @@ class DownloadMethods(UserMethods):
|
|||
progress_callback=progress_callback)
|
||||
return file
|
||||
|
||||
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
|
||||
|
||||
file_size = document.size
|
||||
|
||||
@staticmethod
|
||||
def _get_kind_and_names(attributes):
|
||||
"""Gets kind and possible names for :tl:`DocumentAttribute`."""
|
||||
kind = 'document'
|
||||
possible_names = []
|
||||
for attr in document.attributes:
|
||||
for attr in attributes:
|
||||
if isinstance(attr, types.DocumentAttributeFilename):
|
||||
possible_names.insert(0, attr.file_name)
|
||||
|
||||
|
@ -310,13 +328,24 @@ class DownloadMethods(UserMethods):
|
|||
elif attr.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, kind, utils.get_extension(document),
|
||||
date=date, possible_names=possible_names
|
||||
)
|
||||
|
||||
self.download_file(
|
||||
document, file, file_size=file_size,
|
||||
document, file, file_size=document.size,
|
||||
progress_callback=progress_callback)
|
||||
return file
|
||||
|
||||
|
@ -356,6 +385,37 @@ class DownloadMethods(UserMethods):
|
|||
|
||||
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
|
||||
def _get_proper_filename(file, kind, extension,
|
||||
date=None, possible_names=None):
|
||||
|
|
|
@ -133,6 +133,7 @@ class MessageParseMethods(UserMethods):
|
|||
break
|
||||
|
||||
if found:
|
||||
return custom.Message(self, found, entities, input_chat)
|
||||
found._finish_init(self, entities, input_chat)
|
||||
return found
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -6,13 +6,14 @@ from collections import UserList
|
|||
|
||||
from .messageparse import MessageParseMethods
|
||||
from .uploads import UploadMethods
|
||||
from .buttons import ButtonMethods
|
||||
from .. import utils
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MessageMethods(UploadMethods, MessageParseMethods):
|
||||
class MessageMethods(UploadMethods, ButtonMethods, MessageParseMethods):
|
||||
|
||||
# region Public methods
|
||||
|
||||
|
@ -33,6 +34,14 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
entity (`entity`):
|
||||
The entity from whom to retrieve the message history.
|
||||
|
||||
It may be ``None`` to perform a global search, or
|
||||
to get messages by their ID from no particular chat.
|
||||
Note that some of the offsets will not work if this
|
||||
is the case.
|
||||
|
||||
Note that if you want to perform a global search,
|
||||
you **must** set a non-empty `search` string.
|
||||
|
||||
limit (`int` | `None`, optional):
|
||||
Number of messages to be retrieved. Due to limitations with
|
||||
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
|
||||
in ascending order.
|
||||
|
||||
You cannot use this if both `entity` and `ids` are ``None``.
|
||||
|
||||
_total (`list`, optional):
|
||||
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
|
||||
you think may be good.
|
||||
"""
|
||||
# It's possible to get messages by ID without their entity, so only
|
||||
# fetch the input version if we're not using IDs or if it was given.
|
||||
if not ids or entity:
|
||||
# Note that entity being ``None`` is intended to get messages by
|
||||
# ID under no specific chat, and also to request a global search.
|
||||
if entity:
|
||||
entity = self.get_input_entity(entity)
|
||||
|
||||
if ids:
|
||||
|
@ -154,7 +165,19 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
|
||||
from_id = None
|
||||
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:
|
||||
filter = types.InputMessagesFilterEmpty()
|
||||
request = functions.messages.SearchRequest(
|
||||
|
@ -239,7 +262,9 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
or from_id and message.from_id != from_id):
|
||||
continue
|
||||
|
||||
if reverse:
|
||||
if reverse is None:
|
||||
pass
|
||||
elif reverse:
|
||||
if message.id <= last_id or message.id >= max_id:
|
||||
return
|
||||
else:
|
||||
|
@ -252,7 +277,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
# IDs are returned in descending order (or asc if reverse).
|
||||
last_id = message.id
|
||||
|
||||
yield (custom.Message(self, message, entities, entity))
|
||||
message._finish_init(self, entities, entity)
|
||||
yield (message)
|
||||
have += 1
|
||||
|
||||
if len(r.messages) < request.limit:
|
||||
|
@ -277,12 +303,15 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
break
|
||||
else:
|
||||
request.offset_id = last_message.id
|
||||
if isinstance(request, functions.messages.GetHistoryRequest):
|
||||
request.offset_date = last_message.date
|
||||
else:
|
||||
if isinstance(request, functions.messages.SearchRequest):
|
||||
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
|
||||
request.add_offset -= 1
|
||||
|
||||
|
@ -330,7 +359,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
def send_message(
|
||||
self, entity, message='', *, reply_to=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).
|
||||
|
||||
|
@ -379,13 +409,26 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
Whether the existing draft should be cleared or not.
|
||||
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:
|
||||
The sent `telethon.tl.custom.message.Message`.
|
||||
The sent `custom.Message <telethon.tl.custom.message.Message>`.
|
||||
"""
|
||||
if file is not None:
|
||||
return self.send_file(
|
||||
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:
|
||||
raise ValueError(
|
||||
|
@ -414,12 +457,20 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
else:
|
||||
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(
|
||||
peer=entity,
|
||||
message=message.message or '',
|
||||
silent=message.silent,
|
||||
silent=silent,
|
||||
reply_to_msg_id=reply_id,
|
||||
reply_markup=message.reply_markup,
|
||||
reply_markup=markup,
|
||||
entities=message.entities,
|
||||
clear_draft=clear_draft,
|
||||
no_webpage=not isinstance(
|
||||
|
@ -435,13 +486,15 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
entities=msg_ent,
|
||||
no_webpage=not link_preview,
|
||||
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)
|
||||
if isinstance(result, types.UpdateShortSentMessage):
|
||||
to_id, cls = utils.resolve_id(utils.get_peer_id(entity))
|
||||
return custom.Message(self, types.Message(
|
||||
message = types.Message(
|
||||
id=result.id,
|
||||
to_id=cls(to_id),
|
||||
message=message,
|
||||
|
@ -449,11 +502,14 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
out=result.out,
|
||||
media=result.media,
|
||||
entities=result.entities
|
||||
), {}, input_chat=entity)
|
||||
)
|
||||
message._finish_init(self, {}, entity)
|
||||
return message
|
||||
|
||||
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.
|
||||
|
||||
|
@ -469,6 +525,11 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
of the ``Message`` class, this *must* be specified in
|
||||
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:
|
||||
The list of forwarded `telethon.tl.custom.message.Message`,
|
||||
or a single one if a list wasn't provided as input.
|
||||
|
@ -496,7 +557,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
req = functions.messages.ForwardMessagesRequest(
|
||||
from_peer=from_peer,
|
||||
id=[m if isinstance(m, int) else m.id for m in messages],
|
||||
to_peer=entity
|
||||
to_peer=entity,
|
||||
silent=silent
|
||||
)
|
||||
result = self(req)
|
||||
if isinstance(result, (types.Updates, types.UpdatesCombined)):
|
||||
|
@ -512,15 +574,16 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
random_to_id[update.random_id] = update.id
|
||||
elif isinstance(update, (
|
||||
types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
||||
id_to_message[update.message.id] = custom.Message(
|
||||
self, update.message, entities, input_chat=entity)
|
||||
update.message._finish_init(self, entities, entity)
|
||||
id_to_message[update.message.id] = update.message
|
||||
|
||||
result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
|
||||
return result[0] if single else result
|
||||
|
||||
def edit_message(
|
||||
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).
|
||||
|
||||
|
@ -551,6 +614,13 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
The file object that should replace the existing media
|
||||
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:
|
||||
|
||||
>>> client = ...
|
||||
|
@ -586,7 +656,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
message=text,
|
||||
no_webpage=not link_preview,
|
||||
entities=msg_entities,
|
||||
media=media
|
||||
media=media,
|
||||
reply_markup=self.build_reply_markup(buttons)
|
||||
)
|
||||
msg = self._get_response_message(request, self(request), entity)
|
||||
self._cache_media(msg, file, file_handle)
|
||||
|
@ -703,7 +774,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
total[0] = len(ids)
|
||||
|
||||
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))
|
||||
else:
|
||||
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):
|
||||
yield (None)
|
||||
else:
|
||||
yield (custom.Message(self, message, entities, entity))
|
||||
message._finish_init(self, entities, entity)
|
||||
yield (message)
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import abc
|
||||
import asyncio
|
||||
import collections
|
||||
import inspect
|
||||
import logging
|
||||
import platform
|
||||
import queue
|
||||
|
@ -219,6 +222,9 @@ class TelegramBaseClient(abc.ABC):
|
|||
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,
|
||||
# being ``n`` the amount of borrows a given sender has; once ``n``
|
||||
# reaches ``0`` it should be disconnected and removed.
|
||||
|
@ -252,6 +258,11 @@ class TelegramBaseClient(abc.ABC):
|
|||
self._events_pending_resolve = []
|
||||
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
|
||||
self._parse_mode = markdown
|
||||
|
||||
|
@ -409,6 +420,9 @@ class TelegramBaseClient(abc.ABC):
|
|||
if not sender:
|
||||
sender = self._create_exported_sender(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)
|
||||
|
||||
|
@ -423,12 +437,10 @@ class TelegramBaseClient(abc.ABC):
|
|||
dc_id = sender.dc_id
|
||||
n, _ = self._borrowed_senders[dc_id]
|
||||
n -= 1
|
||||
if n > 0:
|
||||
self._borrowed_senders[dc_id] = (n, sender)
|
||||
else:
|
||||
self._borrowed_senders[dc_id] = (n, sender)
|
||||
if not n:
|
||||
__log__.info('Disconnecting borrowed sender for DC %d', dc_id)
|
||||
sender.disconnect()
|
||||
del self._borrowed_senders[dc_id]
|
||||
|
||||
def _get_cdn_client(self, cdn_redirect):
|
||||
"""Similar to ._borrow_exported_client, but for CDNs"""
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
from . import (
|
||||
UpdateMethods, AuthMethods, DownloadMethods, DialogMethods,
|
||||
ChatMethods, MessageMethods, UploadMethods, MessageParseMethods,
|
||||
UserMethods
|
||||
AuthMethods, DownloadMethods, DialogMethods, ChatMethods, BotMethods,
|
||||
MessageMethods, ButtonMethods, UpdateMethods, UploadMethods,
|
||||
MessageParseMethods, UserMethods
|
||||
)
|
||||
|
||||
|
||||
class TelegramClient(
|
||||
UpdateMethods, AuthMethods, DownloadMethods, DialogMethods,
|
||||
ChatMethods, MessageMethods, UploadMethods, MessageParseMethods,
|
||||
UserMethods
|
||||
AuthMethods, DownloadMethods, DialogMethods, ChatMethods, BotMethods,
|
||||
MessageMethods, UploadMethods, ButtonMethods, UpdateMethods,
|
||||
MessageParseMethods, UserMethods
|
||||
):
|
||||
pass
|
||||
|
|
|
@ -79,6 +79,7 @@ class UpdateMethods(UserMethods):
|
|||
event = events.Raw()
|
||||
|
||||
self._events_pending_resolve.append(event)
|
||||
self._event_builders_count[type(event)] += 1
|
||||
self._event_builders.append((event, callback))
|
||||
|
||||
def remove_event_handler(self, callback, event=None):
|
||||
|
@ -97,6 +98,11 @@ class UpdateMethods(UserMethods):
|
|||
i -= 1
|
||||
ev, cb = self._event_builders[i]
|
||||
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]
|
||||
found += 1
|
||||
|
||||
|
@ -164,7 +170,7 @@ class UpdateMethods(UserMethods):
|
|||
for u in update.updates:
|
||||
u._entities = entities
|
||||
self._handle_update(u)
|
||||
if isinstance(update, types.UpdateShort):
|
||||
elif isinstance(update, types.UpdateShort):
|
||||
self._handle_update(update.update)
|
||||
else:
|
||||
update._entities = getattr(update, '_entities', {})
|
||||
|
@ -177,7 +183,7 @@ class UpdateMethods(UserMethods):
|
|||
syncio.create_task(self._dispatch_queue_updates)
|
||||
|
||||
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:
|
||||
need_diff = True
|
||||
self._state.pts = update.pts
|
||||
|
@ -197,7 +203,7 @@ class UpdateMethods(UserMethods):
|
|||
continue # We actually just want to act upon timeout
|
||||
except concurrent.futures.TimeoutError:
|
||||
pass
|
||||
except:
|
||||
except Exception as e:
|
||||
continue # Any disconnected exception should be ignored
|
||||
|
||||
# We also don't really care about their result.
|
||||
|
@ -241,28 +247,35 @@ class UpdateMethods(UserMethods):
|
|||
|
||||
self._events_pending_resolve.clear()
|
||||
|
||||
for builder, callback in self._event_builders:
|
||||
event = builder.build(update)
|
||||
if event:
|
||||
if hasattr(event, '_set_client'):
|
||||
event._set_client(self)
|
||||
else:
|
||||
event._client = self
|
||||
# TODO We can improve this further
|
||||
# If we had a way to get all event builders for
|
||||
# a type instead looping over them all always.
|
||||
built = {builder: builder.build(update)
|
||||
for builder in self._event_builders_count}
|
||||
|
||||
event.original_update = update
|
||||
try:
|
||||
callback(event)
|
||||
except events.StopPropagation:
|
||||
__log__.debug(
|
||||
"Event handler '{}' stopped chain of "
|
||||
"propagation for event {}."
|
||||
.format(callback.__name__,
|
||||
type(event).__name__)
|
||||
)
|
||||
break
|
||||
except:
|
||||
__log__.exception('Unhandled exception on {}'
|
||||
.format(callback.__name__))
|
||||
for builder, callback in self._event_builders:
|
||||
event = built[type(builder)]
|
||||
if not event or not builder.filter(event):
|
||||
continue
|
||||
|
||||
if hasattr(event, '_set_client'):
|
||||
event._set_client(self)
|
||||
else:
|
||||
event._client = self
|
||||
|
||||
event.original_update = update
|
||||
try:
|
||||
callback(event)
|
||||
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):
|
||||
# Upon reconnection, we want to send getState
|
||||
|
|
|
@ -9,20 +9,14 @@ from mimetypes import guess_type
|
|||
|
||||
from .messageparse import MessageParseMethods
|
||||
from .users import UserMethods
|
||||
from .buttons import ButtonMethods
|
||||
from .. import utils, helpers
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
try:
|
||||
import hachoir
|
||||
import hachoir.metadata
|
||||
import hachoir.parser
|
||||
except ImportError:
|
||||
hachoir = None
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UploadMethods(MessageParseMethods, UserMethods):
|
||||
class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods):
|
||||
|
||||
# region Public methods
|
||||
|
||||
|
@ -30,7 +24,8 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
self, entity, file, *, caption='', force_document=False,
|
||||
progress_callback=None, reply_to=None, attributes=None,
|
||||
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.
|
||||
|
||||
|
@ -46,7 +41,8 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
Furthermore the file may be any media (a message, document,
|
||||
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
|
||||
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
|
||||
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:
|
||||
If the ``hachoir3`` package (``hachoir`` module) is installed,
|
||||
it will be used to determine metadata from audio and video files.
|
||||
|
@ -126,7 +134,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
result += self._send_album(
|
||||
entity, images[:10], caption=caption,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
parse_mode=parse_mode
|
||||
parse_mode=parse_mode, silent=silent
|
||||
)
|
||||
images = images[10:]
|
||||
|
||||
|
@ -136,7 +144,8 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
caption=caption, force_document=force_document,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
attributes=attributes, thumb=thumb, voice_note=voice_note,
|
||||
video_note=video_note, **kwargs
|
||||
video_note=video_note, buttons=buttons, silent=silent,
|
||||
**kwargs
|
||||
))
|
||||
|
||||
return result
|
||||
|
@ -159,9 +168,10 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
voice_note=voice_note, video_note=video_note
|
||||
)
|
||||
|
||||
markup = self.build_reply_markup(buttons)
|
||||
request = functions.messages.SendMediaRequest(
|
||||
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)
|
||||
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='',
|
||||
progress_callback=None, reply_to=None,
|
||||
parse_mode=utils.Default):
|
||||
parse_mode=utils.Default, silent=None):
|
||||
"""Specialized version of .send_file for albums"""
|
||||
# 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
|
||||
|
@ -208,12 +218,15 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
caption, msg_entities = captions.pop()
|
||||
else:
|
||||
caption, msg_entities = '', None
|
||||
media.append(types.InputSingleMedia(types.InputMediaPhoto(fh), message=caption,
|
||||
entities=msg_entities))
|
||||
media.append(types.InputSingleMedia(
|
||||
types.InputMediaPhoto(fh),
|
||||
message=caption,
|
||||
entities=msg_entities
|
||||
))
|
||||
|
||||
# Now we can construct the multi-media request
|
||||
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 [
|
||||
self._get_response_message(update.id, result, entity)
|
||||
|
@ -262,7 +275,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
Returns:
|
||||
: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.
|
||||
"""
|
||||
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
|
||||
|
||||
media = None
|
||||
file_handle = None
|
||||
as_image = utils.is_image(file) and not force_document
|
||||
use_cache = types.InputPhoto if as_image else types.InputDocument
|
||||
if isinstance(file, str) and re.match('https?://', file):
|
||||
file_handle = None
|
||||
if not isinstance(file, str):
|
||||
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:
|
||||
media = types.InputMediaPhotoExternal(file)
|
||||
elif not force_document and utils.is_gif(file):
|
||||
|
@ -388,10 +406,9 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
else:
|
||||
media = types.InputMediaDocumentExternal(file)
|
||||
else:
|
||||
file_handle = self.upload_file(
|
||||
file, progress_callback=progress_callback,
|
||||
use_cache=use_cache if allow_cache else None
|
||||
)
|
||||
bot_file = utils.resolve_bot_file_id(file)
|
||||
if bot_file:
|
||||
media = utils.get_input_media(bot_file)
|
||||
|
||||
if media:
|
||||
pass # Already have media, don't check the rest
|
||||
|
@ -404,74 +421,13 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
elif as_image:
|
||||
media = types.InputMediaUploadedPhoto(file_handle)
|
||||
else:
|
||||
mime_type = None
|
||||
if isinstance(file, str):
|
||||
# Determine mime-type and attributes
|
||||
# Take the first element by using [0] since it returns a tuple
|
||||
mime_type = guess_type(file)[0]
|
||||
attr_dict = {
|
||||
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'
|
||||
attributes, mime_type = utils.get_attributes(
|
||||
file,
|
||||
attributes=attributes,
|
||||
force_document=force_document,
|
||||
voice_note=voice_note,
|
||||
video_note=video_note
|
||||
)
|
||||
|
||||
input_kw = {}
|
||||
if thumb:
|
||||
|
@ -480,7 +436,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
media = types.InputMediaUploadedDocument(
|
||||
file=file_handle,
|
||||
mime_type=mime_type,
|
||||
attributes=list(attr_dict.values()),
|
||||
attributes=attributes,
|
||||
**input_kw
|
||||
)
|
||||
return file_handle, media
|
||||
|
|
|
@ -18,6 +18,20 @@ class UserMethods(TelegramBaseClient):
|
|||
raise _NOT_A_REQUEST
|
||||
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()
|
||||
for _ in range(self._request_retries):
|
||||
try:
|
||||
|
@ -28,6 +42,7 @@ class UserMethods(TelegramBaseClient):
|
|||
result = f.result()
|
||||
self.session.process_entities(result)
|
||||
results.append(result)
|
||||
request_index += 1
|
||||
return results
|
||||
else:
|
||||
result = future.result()
|
||||
|
@ -37,6 +52,12 @@ class UserMethods(TelegramBaseClient):
|
|||
__log__.warning('Telegram is having internal issues %s: %s',
|
||||
e.__class__.__name__, 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:
|
||||
__log__.info('Sleeping for %ds on flood wait', 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 pyaes
|
||||
import logging
|
||||
from . import libssl
|
||||
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
try:
|
||||
import cryptg
|
||||
__log__.info('cryptg detected, it will be used for encryption')
|
||||
except ImportError:
|
||||
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:
|
||||
|
@ -23,6 +39,8 @@ class AES:
|
|||
"""
|
||||
if cryptg:
|
||||
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]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
@ -56,13 +74,14 @@ class AES:
|
|||
Encrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
# Add random padding iff it's not evenly divisible by 16 already
|
||||
if len(plain_text) % 16 != 0:
|
||||
padding_count = 16 - len(plain_text) % 16
|
||||
plain_text += os.urandom(padding_count)
|
||||
padding = len(plain_text) % 16
|
||||
if padding:
|
||||
plain_text += os.urandom(16 - padding)
|
||||
|
||||
if cryptg:
|
||||
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]
|
||||
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.close()
|
||||
except:
|
||||
except Exception as e:
|
||||
"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
|
||||
return cls(capture=capture)
|
||||
|
||||
cls = base_errors.get(rpc_error.error_code, RPCError)
|
||||
return cls(rpc_error.error_message)
|
||||
cls = base_errors.get(rpc_error.error_code)
|
||||
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
|
||||
message = None
|
||||
|
||||
def __init__(self, code, message):
|
||||
super().__init__('RPCError {}: {}'.format(code, message))
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
def __reduce__(self):
|
||||
return type(self), ()
|
||||
return type(self), (self.code, self.message)
|
||||
|
||||
|
||||
class InvalidDCError(RPCError):
|
||||
|
|
|
@ -5,6 +5,8 @@ from .messageedited import MessageEdited
|
|||
from .messageread import MessageRead
|
||||
from .newmessage import NewMessage
|
||||
from .userupdate import UserUpdate
|
||||
from .callbackquery import CallbackQuery
|
||||
from .inlinequery import InlineQuery
|
||||
|
||||
|
||||
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).
|
||||
"""
|
||||
def build(self, update):
|
||||
@classmethod
|
||||
def build(cls, update):
|
||||
if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
|
||||
# Telegram does not always send
|
||||
# UpdateChannelPinnedMessage for new pins
|
||||
# but always for unpin, with update.id = 0
|
||||
event = ChatAction.Event(types.PeerChannel(update.channel_id),
|
||||
unpin=True)
|
||||
event = cls.Event(types.PeerChannel(update.channel_id),
|
||||
unpin=True)
|
||||
|
||||
elif isinstance(update, types.UpdateChatParticipantAdd):
|
||||
event = ChatAction.Event(types.PeerChat(update.chat_id),
|
||||
added_by=update.inviter_id or True,
|
||||
users=update.user_id)
|
||||
event = cls.Event(types.PeerChat(update.chat_id),
|
||||
added_by=update.inviter_id or True,
|
||||
users=update.user_id)
|
||||
|
||||
elif isinstance(update, types.UpdateChatParticipantDelete):
|
||||
event = ChatAction.Event(types.PeerChat(update.chat_id),
|
||||
kicked_by=True,
|
||||
users=update.user_id)
|
||||
event = cls.Event(types.PeerChat(update.chat_id),
|
||||
kicked_by=True,
|
||||
users=update.user_id)
|
||||
|
||||
elif (isinstance(update, (
|
||||
types.UpdateNewMessage, types.UpdateNewChannelMessage))
|
||||
|
@ -32,53 +33,53 @@ class ChatAction(EventBuilder):
|
|||
msg = update.message
|
||||
action = update.message.action
|
||||
if isinstance(action, types.MessageActionChatJoinedByLink):
|
||||
event = ChatAction.Event(msg,
|
||||
added_by=True,
|
||||
users=msg.from_id)
|
||||
event = cls.Event(msg,
|
||||
added_by=True,
|
||||
users=msg.from_id)
|
||||
elif isinstance(action, types.MessageActionChatAddUser):
|
||||
# If an user adds itself, it means they joined
|
||||
added_by = ([msg.from_id] == action.users) or msg.from_id
|
||||
event = ChatAction.Event(msg,
|
||||
added_by=added_by,
|
||||
users=action.users)
|
||||
event = cls.Event(msg,
|
||||
added_by=added_by,
|
||||
users=action.users)
|
||||
elif isinstance(action, types.MessageActionChatDeleteUser):
|
||||
event = ChatAction.Event(msg,
|
||||
kicked_by=msg.from_id or True,
|
||||
users=action.user_id)
|
||||
event = cls.Event(msg,
|
||||
kicked_by=msg.from_id or True,
|
||||
users=action.user_id)
|
||||
elif isinstance(action, types.MessageActionChatCreate):
|
||||
event = ChatAction.Event(msg,
|
||||
users=action.users,
|
||||
created=True,
|
||||
new_title=action.title)
|
||||
event = cls.Event(msg,
|
||||
users=action.users,
|
||||
created=True,
|
||||
new_title=action.title)
|
||||
elif isinstance(action, types.MessageActionChannelCreate):
|
||||
event = ChatAction.Event(msg,
|
||||
created=True,
|
||||
users=msg.from_id,
|
||||
new_title=action.title)
|
||||
event = cls.Event(msg,
|
||||
created=True,
|
||||
users=msg.from_id,
|
||||
new_title=action.title)
|
||||
elif isinstance(action, types.MessageActionChatEditTitle):
|
||||
event = ChatAction.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_title=action.title)
|
||||
event = cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_title=action.title)
|
||||
elif isinstance(action, types.MessageActionChatEditPhoto):
|
||||
event = ChatAction.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_photo=action.photo)
|
||||
event = cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_photo=action.photo)
|
||||
elif isinstance(action, types.MessageActionChatDeletePhoto):
|
||||
event = ChatAction.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_photo=True)
|
||||
event = cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_photo=True)
|
||||
elif isinstance(action, types.MessageActionPinMessage):
|
||||
# Telegram always sends this service message for new pins
|
||||
event = ChatAction.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_pin=msg.reply_to_msg_id)
|
||||
event = cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_pin=msg.reply_to_msg_id)
|
||||
else:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
event._entities = update._entities
|
||||
return self._filter_event(event)
|
||||
return event
|
||||
|
||||
class Event(EventCommon):
|
||||
"""
|
||||
|
@ -163,8 +164,7 @@ class ChatAction(EventBuilder):
|
|||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
if self.action_message:
|
||||
self.action_message = custom.Message(
|
||||
client, self.action_message, self._entities, None)
|
||||
self.action_message._finish_init(client, self._entities, None)
|
||||
|
||||
def respond(self, *args, **kwargs):
|
||||
"""
|
||||
|
|
|
@ -3,6 +3,7 @@ import warnings
|
|||
|
||||
from .. import utils
|
||||
from ..tl import TLObject, types
|
||||
from ..tl.custom.chatgetter import ChatGetter
|
||||
|
||||
|
||||
def _into_id_set(client, chats):
|
||||
|
@ -42,8 +43,8 @@ class EventBuilder(abc.ABC):
|
|||
|
||||
Args:
|
||||
chats (`entity`, optional):
|
||||
May be one or more entities (username/peer/etc.). By default,
|
||||
only matching chats will be handled.
|
||||
May be one or more entities (username/peer/etc.), preferably IDs.
|
||||
By default, only matching chats will be handled.
|
||||
|
||||
blacklist_chats (`bool`, optional):
|
||||
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``
|
||||
which will be ignored if ``blacklist_chats=True``.
|
||||
"""
|
||||
self_id = None
|
||||
|
||||
def __init__(self, chats=None, blacklist_chats=False):
|
||||
self.chats = chats
|
||||
self.blacklist_chats = blacklist_chats
|
||||
self._self_id = None
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def build(self, update):
|
||||
def build(cls, update):
|
||||
"""Builds an event for the given update if possible, or returns None"""
|
||||
|
||||
def resolve(self, client):
|
||||
"""Helper method to allow event builders to be resolved before usage"""
|
||||
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
|
||||
but the set is a blacklist) returns ``None``, otherwise the event.
|
||||
|
@ -79,13 +84,16 @@ class EventBuilder(abc.ABC):
|
|||
return event
|
||||
|
||||
|
||||
class EventCommon(abc.ABC):
|
||||
class EventCommon(ChatGetter, abc.ABC):
|
||||
"""
|
||||
Intermediate class with common things to all events.
|
||||
|
||||
All events (except `Raw`) have ``is_private``, ``is_group``
|
||||
and ``is_channel`` boolean properties, as well as an
|
||||
``original_update`` field containing the original :tl:`Update`.
|
||||
Remember that this class implements `ChatGetter
|
||||
<telethon.tl.custom.chatgetter.ChatGetter>` which
|
||||
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'
|
||||
|
||||
|
@ -96,64 +104,27 @@ class EventCommon(abc.ABC):
|
|||
self._message_id = msg_id
|
||||
self._input_chat = None
|
||||
self._chat = None
|
||||
self._broadcast = broadcast
|
||||
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):
|
||||
"""
|
||||
Setter so subclasses can act accordingly when the client is set.
|
||||
"""
|
||||
self._client = client
|
||||
self._chat = self._entities.get(self.chat_id)
|
||||
if not self._chat:
|
||||
return
|
||||
|
||||
@property
|
||||
def input_chat(self):
|
||||
"""
|
||||
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:
|
||||
self._input_chat = utils.get_input_peer(self._chat)
|
||||
if not getattr(self._input_chat, 'access_hash', True):
|
||||
# getattr with True to handle the InputPeerSelf() case
|
||||
try:
|
||||
self._input_chat =\
|
||||
self._client.session.get_input_entity(self._chat_peer)
|
||||
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_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
|
||||
self._input_chat = None
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
|
@ -162,44 +133,6 @@ class EventCommon(abc.ABC):
|
|||
"""
|
||||
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):
|
||||
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.
|
||||
"""
|
||||
def build(self, update):
|
||||
@classmethod
|
||||
def build(cls, update):
|
||||
if isinstance(update, types.UpdateDeleteMessages):
|
||||
event = MessageDeleted.Event(
|
||||
event = cls.Event(
|
||||
deleted_ids=update.messages,
|
||||
peer=None
|
||||
)
|
||||
elif isinstance(update, types.UpdateDeleteChannelMessages):
|
||||
event = MessageDeleted.Event(
|
||||
event = cls.Event(
|
||||
deleted_ids=update.messages,
|
||||
peer=types.PeerChannel(update.channel_id)
|
||||
)
|
||||
|
@ -22,19 +23,12 @@ class MessageDeleted(EventBuilder):
|
|||
return
|
||||
|
||||
event._entities = update._entities
|
||||
return self._filter_event(event)
|
||||
return event
|
||||
|
||||
class Event(EventCommon):
|
||||
def __init__(self, deleted_ids, peer):
|
||||
super().__init__(
|
||||
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_ids = deleted_ids
|
||||
|
|
|
@ -8,15 +8,16 @@ class MessageEdited(NewMessage):
|
|||
"""
|
||||
Event fired when a message has been edited.
|
||||
"""
|
||||
def build(self, update):
|
||||
@classmethod
|
||||
def build(cls, update):
|
||||
if isinstance(update, (types.UpdateEditMessage,
|
||||
types.UpdateEditChannelMessage)):
|
||||
event = MessageEdited.Event(update.message)
|
||||
event = cls.Event(update.message)
|
||||
else:
|
||||
return
|
||||
|
||||
event._entities = update._entities
|
||||
return self._message_filter_event(event)
|
||||
return event
|
||||
|
||||
class Event(NewMessage.Event):
|
||||
pass # Required if we want a different name for it
|
||||
|
|
|
@ -18,32 +18,36 @@ class MessageRead(EventBuilder):
|
|||
super().__init__(chats, blacklist_chats)
|
||||
self.inbox = inbox
|
||||
|
||||
def build(self, update):
|
||||
@classmethod
|
||||
def build(cls, update):
|
||||
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):
|
||||
event = MessageRead.Event(update.peer, update.max_id, True)
|
||||
event = cls.Event(update.peer, update.max_id, True)
|
||||
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)
|
||||
elif isinstance(update, types.UpdateReadChannelOutbox):
|
||||
event = MessageRead.Event(types.PeerChannel(update.channel_id),
|
||||
update.max_id, True)
|
||||
event = cls.Event(types.PeerChannel(update.channel_id),
|
||||
update.max_id, True)
|
||||
elif isinstance(update, types.UpdateReadMessagesContents):
|
||||
event = MessageRead.Event(message_ids=update.messages,
|
||||
contents=True)
|
||||
event = cls.Event(message_ids=update.messages,
|
||||
contents=True)
|
||||
elif isinstance(update, types.UpdateChannelReadMessagesContents):
|
||||
event = MessageRead.Event(types.PeerChannel(update.channel_id),
|
||||
message_ids=update.messages,
|
||||
contents=True)
|
||||
event = cls.Event(types.PeerChannel(update.channel_id),
|
||||
message_ids=update.messages,
|
||||
contents=True)
|
||||
else:
|
||||
return
|
||||
|
||||
event._entities = update._entities
|
||||
return event
|
||||
|
||||
def filter(self, event):
|
||||
if self.inbox == event.outbox:
|
||||
return
|
||||
|
||||
event._entities = update._entities
|
||||
return self._filter_event(event)
|
||||
return super().filter(event)
|
||||
|
||||
class Event(EventCommon):
|
||||
"""
|
||||
|
|
|
@ -40,13 +40,12 @@ class NewMessage(EventBuilder):
|
|||
def __init__(self, chats=None, *, blacklist_chats=False,
|
||||
incoming=None, outgoing=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
|
||||
elif outgoing is not None and incoming is None:
|
||||
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)):
|
||||
raise ValueError("Don't create an event handler if you "
|
||||
"don't want neither incoming or outgoing!")
|
||||
|
@ -75,14 +74,15 @@ class NewMessage(EventBuilder):
|
|||
super().resolve(client)
|
||||
self.from_users = _into_id_set(client, self.from_users)
|
||||
|
||||
def build(self, update):
|
||||
@classmethod
|
||||
def build(cls, update):
|
||||
if isinstance(update,
|
||||
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
||||
if not isinstance(update.message, types.Message):
|
||||
return # We don't care about MessageService's here
|
||||
event = NewMessage.Event(update.message)
|
||||
event = cls.Event(update.message)
|
||||
elif isinstance(update, types.UpdateShortMessage):
|
||||
event = NewMessage.Event(types.Message(
|
||||
event = cls.Event(types.Message(
|
||||
out=update.out,
|
||||
mentioned=update.mentioned,
|
||||
media_unread=update.media_unread,
|
||||
|
@ -91,9 +91,9 @@ class NewMessage(EventBuilder):
|
|||
# Note that to_id/from_id complement each other in private
|
||||
# messages, depending on whether the message was outgoing.
|
||||
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,
|
||||
date=update.date,
|
||||
fwd_from=update.fwd_from,
|
||||
|
@ -102,7 +102,7 @@ class NewMessage(EventBuilder):
|
|||
entities=update.entities
|
||||
))
|
||||
elif isinstance(update, types.UpdateShortChatMessage):
|
||||
event = NewMessage.Event(types.Message(
|
||||
event = cls.Event(types.Message(
|
||||
out=update.out,
|
||||
mentioned=update.mentioned,
|
||||
media_unread=update.media_unread,
|
||||
|
@ -120,8 +120,6 @@ class NewMessage(EventBuilder):
|
|||
else:
|
||||
return
|
||||
|
||||
event._entities = update._entities
|
||||
|
||||
# Make messages sent to ourselves outgoing unless they're forwarded.
|
||||
# This makes it consistent with official client's appearance.
|
||||
ori = event.message
|
||||
|
@ -129,9 +127,10 @@ class NewMessage(EventBuilder):
|
|||
if ori.from_id == ori.to_id.user_id and not ori.fwd_from:
|
||||
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:
|
||||
return event
|
||||
|
||||
|
@ -153,7 +152,7 @@ class NewMessage(EventBuilder):
|
|||
return
|
||||
event.pattern_match = match
|
||||
|
||||
return self._filter_event(event)
|
||||
return super().filter(event)
|
||||
|
||||
class Event(EventCommon):
|
||||
"""
|
||||
|
@ -204,8 +203,7 @@ class NewMessage(EventBuilder):
|
|||
|
||||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self.message = custom.Message(
|
||||
client, self.message, self._entities, None)
|
||||
self.message._finish_init(client, self._entities, None)
|
||||
self.__dict__['_init'] = True # No new attributes can be set
|
||||
|
||||
def __getattr__(self, item):
|
||||
|
|
|
@ -25,6 +25,10 @@ class Raw(EventBuilder):
|
|||
def resolve(self, client):
|
||||
pass
|
||||
|
||||
def build(self, update):
|
||||
if not self.types or isinstance(update, self.types):
|
||||
return update
|
||||
@classmethod
|
||||
def build(cls, 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).
|
||||
"""
|
||||
def build(self, update):
|
||||
@classmethod
|
||||
def build(cls, update):
|
||||
if isinstance(update, types.UpdateUserStatus):
|
||||
event = UserUpdate.Event(update.user_id,
|
||||
status=update.status)
|
||||
event = cls.Event(update.user_id,
|
||||
status=update.status)
|
||||
else:
|
||||
return
|
||||
|
||||
event._entities = update._entities
|
||||
return self._filter_event(event)
|
||||
return event
|
||||
|
||||
class Event(EventCommon):
|
||||
"""
|
||||
|
@ -95,7 +96,8 @@ class UserUpdate(EventBuilder):
|
|||
isinstance(status, types.UserStatusOnline) else None
|
||||
|
||||
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):
|
||||
self.within_months = True
|
||||
if diff < datetime.timedelta(days=7):
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
This module contains the BinaryReader utility class.
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from io import BufferedReader, BytesIO
|
||||
from struct import unpack
|
||||
|
||||
|
@ -120,7 +120,10 @@ class BinaryReader:
|
|||
into a Python datetime object.
|
||||
"""
|
||||
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):
|
||||
"""Reads a Telegram object."""
|
||||
|
|
|
@ -67,6 +67,7 @@ class TcpClient:
|
|||
if proxy is None:
|
||||
s = socket.socket(mode, socket.SOCK_STREAM)
|
||||
else:
|
||||
__log__.info('Connection will be made through proxy %s', proxy)
|
||||
import socks
|
||||
s = socks.socksocket(mode, socket.SOCK_STREAM)
|
||||
if isinstance(proxy, dict):
|
||||
|
@ -92,11 +93,16 @@ class TcpClient:
|
|||
try:
|
||||
if self._socket is None:
|
||||
self._socket = self._create_socket(mode, self.proxy)
|
||||
if self.ssl and port == SSL_PORT:
|
||||
self._socket = ssl.wrap_socket(self._socket, **self.ssl)
|
||||
wrap_ssl = self.ssl and port == SSL_PORT
|
||||
else:
|
||||
wrap_ssl = False
|
||||
|
||||
self._socket.settimeout(self.timeout)
|
||||
self._socket.connect(address)
|
||||
if wrap_ssl:
|
||||
self._socket = ssl.wrap_socket(
|
||||
self._socket, do_handshake_on_connect=True, **self.ssl)
|
||||
|
||||
self._closed.clear()
|
||||
except OSError as e:
|
||||
if e.errno in CONN_RESET_ERRNOS:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Various helpers not related to the Telegram API itself"""
|
||||
import os
|
||||
import struct
|
||||
import collections
|
||||
from hashlib import sha1, sha256
|
||||
|
||||
|
||||
|
@ -65,3 +66,22 @@ def get_password_hash(pw, current_salt):
|
|||
return sha256(pw_hash).digest()
|
||||
|
||||
# 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:
|
||||
continue
|
||||
except concurrent.futures.CancelledError:
|
||||
self.disconnect()
|
||||
return
|
||||
except Exception as e:
|
||||
if isinstance(e, ConnectionError):
|
||||
|
@ -431,6 +432,7 @@ class MTProtoSender:
|
|||
except socket.timeout:
|
||||
continue
|
||||
except concurrent.futures.CancelledError:
|
||||
self.disconnect()
|
||||
return
|
||||
except Exception as e:
|
||||
if isinstance(e, ConnectionError):
|
||||
|
@ -473,15 +475,19 @@ class MTProtoSender:
|
|||
__log__.info('Server replied with an unknown type {:08x}: {!r}'
|
||||
.format(e.invalid_constructor_id, e.remaining))
|
||||
continue
|
||||
except:
|
||||
__log__.exception('Unhandled exception while unpacking')
|
||||
except concurrent.futures.CancelledError:
|
||||
self.disconnect()
|
||||
return
|
||||
except Exception as e:
|
||||
__log__.exception('Unhandled exception while unpacking %s',e)
|
||||
time.sleep(1)
|
||||
else:
|
||||
try:
|
||||
self._process_message(message)
|
||||
except concurrent.futures.CancelledError:
|
||||
self.disconnect()
|
||||
return
|
||||
except:
|
||||
except Exception as e:
|
||||
__log__.exception('Unhandled exception while '
|
||||
'processing %s', message)
|
||||
time.sleep(1)
|
||||
|
@ -512,6 +518,12 @@ class MTProtoSender:
|
|||
__log__.debug('Handling RPC result for message %d',
|
||||
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:
|
||||
error = rpc_message_to_error(rpc_result.error)
|
||||
self._send_queue.put_nowait(self.state.create_message(
|
||||
|
@ -520,8 +532,7 @@ class MTProtoSender:
|
|||
|
||||
if not message.future.cancelled():
|
||||
message.future.set_exception(error)
|
||||
return
|
||||
elif message:
|
||||
else:
|
||||
# TODO Would be nice to avoid accessing a per-obj read_result
|
||||
# Instead have a variable that indicated how the result should
|
||||
# be read (an enum) and dispatch to read the result, mostly
|
||||
|
@ -531,11 +542,6 @@ class MTProtoSender:
|
|||
|
||||
if not message.future.cancelled():
|
||||
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):
|
||||
"""
|
||||
|
@ -611,7 +617,7 @@ class MTProtoSender:
|
|||
bad_msg = message.obj
|
||||
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):
|
||||
# Sent msg_id too low or too high (respectively).
|
||||
# Use the current msg_id to determine the right time offset.
|
||||
|
|
|
@ -157,10 +157,20 @@ class MTProtoState:
|
|||
Updates the time offset to the correct
|
||||
one given a known valid message ID.
|
||||
"""
|
||||
bad = self._get_new_msg_id()
|
||||
old = self.time_offset
|
||||
|
||||
now = int(time.time())
|
||||
correct = correct_msg_id >> 32
|
||||
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
|
||||
|
||||
def _get_seq_no(self, content_related):
|
||||
|
|
|
@ -42,6 +42,10 @@ class MemorySession(Session):
|
|||
self._server_address = server_address
|
||||
self._port = port
|
||||
|
||||
@property
|
||||
def dc_id(self):
|
||||
return self._dc_id
|
||||
|
||||
@property
|
||||
def server_address(self):
|
||||
return self._server_address
|
||||
|
|
|
@ -211,7 +211,8 @@ class SQLiteSession(MemorySession):
|
|||
'where id = ?', entity_id)
|
||||
if 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)
|
||||
|
||||
def set_update_state(self, entity_id, state):
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
from .draft import Draft
|
||||
from .dialog import Dialog
|
||||
from .input_sized_file import InputSizedFile
|
||||
from .inputsizedfile import InputSizedFile
|
||||
from .messagebutton import MessageButton
|
||||
from .forward import Forward
|
||||
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
|
||||
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:
|
||||
|
||||
original_fwd (:tl:`MessageFwdHeader`):
|
||||
|
@ -19,105 +27,21 @@ class Forward:
|
|||
self.__dict__ = original.__dict__
|
||||
self._client = client
|
||||
self.original_fwd = original
|
||||
|
||||
self._sender_id = original.from_id
|
||||
self._sender = entities.get(original.from_id)
|
||||
self._chat = entities.get(original.channel_id)
|
||||
|
||||
self._input_sender =\
|
||||
get_input_peer(self._sender) if self._sender else None
|
||||
self._input_chat =\
|
||||
get_input_peer(self._chat) if self._chat else None
|
||||
utils.get_input_peer(self._sender) if self._sender else None
|
||||
|
||||
# TODO The pattern to get sender and chat is very similar
|
||||
# and copy pasted in/to several places. Reuse the code.
|
||||
#
|
||||
# It could be an ABC with some ``resolve_sender`` abstract,
|
||||
# so every subclass knew what tricks it can make to get
|
||||
# the sender.
|
||||
self._broadcast = None
|
||||
if original.channel_id:
|
||||
self._chat_peer = types.PeerChannel(original.channel_id)
|
||||
self._chat = entities.get(utils.get_peer_id(self._chat_peer))
|
||||
else:
|
||||
self._chat_peer = None
|
||||
self._chat = None
|
||||
|
||||
@property
|
||||
def sender(self):
|
||||
"""
|
||||
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
|
||||
self._input_chat = \
|
||||
utils.get_input_peer(self._chat) if self._chat else None
|
||||
|
||||
def get_sender(self):
|
||||
"""
|
||||
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
|
||||
# TODO We could reload the message
|
||||
|
|
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:
|
||||
"""
|
||||
Custom class that encapsulates a message providing an abstraction to
|
||||
easily access some commonly needed features (such as the markdown text
|
||||
or the text for a given message entity).
|
||||
Custom class that encapsulates a message button providing
|
||||
an abstraction to easily access some commonly needed features
|
||||
(such as clicking the button itself).
|
||||
|
||||
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
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
|
||||
class TLObject:
|
||||
CONSTRUCTOR_ID = None
|
||||
SUBCLASS_OF_ID = None
|
||||
|
||||
@staticmethod
|
||||
def pretty_format(obj, indent=None):
|
||||
"""
|
||||
|
@ -24,10 +28,6 @@ class TLObject:
|
|||
return '[{}]'.format(
|
||||
', '.join(TLObject.pretty_format(x) for x in obj)
|
||||
)
|
||||
elif isinstance(obj, datetime):
|
||||
return 'datetime.utcfromtimestamp({})'.format(
|
||||
int(obj.timestamp())
|
||||
)
|
||||
else:
|
||||
return repr(obj)
|
||||
else:
|
||||
|
@ -69,11 +69,6 @@ class TLObject:
|
|||
result.append('\t' * indent)
|
||||
result.append(']')
|
||||
|
||||
elif isinstance(obj, datetime):
|
||||
result.append('datetime.utcfromtimestamp(')
|
||||
result.append(repr(int(obj.timestamp())))
|
||||
result.append(')')
|
||||
|
||||
else:
|
||||
result.append(repr(obj))
|
||||
|
||||
|
@ -125,6 +120,9 @@ class TLObject:
|
|||
dt = int(datetime(dt.year, dt.month, dt.day).timestamp())
|
||||
elif isinstance(dt, float):
|
||||
dt = int(dt)
|
||||
elif isinstance(dt, timedelta):
|
||||
# Timezones are tricky. datetime.now() + ... timestamp() works
|
||||
dt = int((datetime.now() + dt).timestamp())
|
||||
|
||||
if isinstance(dt, int):
|
||||
return struct.pack('<I', dt)
|
||||
|
|
|
@ -2,36 +2,28 @@
|
|||
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)
|
||||
"""
|
||||
import base64
|
||||
import binascii
|
||||
import itertools
|
||||
import math
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import types
|
||||
import struct
|
||||
from collections import UserList
|
||||
from mimetypes import guess_extension
|
||||
from types import GeneratorType
|
||||
|
||||
from .extensions import markdown, html
|
||||
from .helpers import add_surrogate, del_surrogate
|
||||
from .tl.types import (
|
||||
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
||||
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
||||
MessageMediaDocument, MessageMediaPhoto, PeerChannel, InputChannel,
|
||||
UserEmpty, InputUser, InputUserEmpty, InputUserSelf, InputPeerSelf,
|
||||
PeerChat, PeerUser, User, UserFull, UserProfilePhoto, Document,
|
||||
MessageMediaContact, MessageMediaEmpty, MessageMediaGame, MessageMediaGeo,
|
||||
MessageMediaUnsupported, MessageMediaVenue, InputMediaContact,
|
||||
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
|
||||
from .tl import types
|
||||
|
||||
try:
|
||||
import hachoir
|
||||
import hachoir.metadata
|
||||
import hachoir.parser
|
||||
except ImportError:
|
||||
hachoir = None
|
||||
|
||||
USERNAME_RE = re.compile(
|
||||
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`,
|
||||
: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:
|
||||
return '{} {}'.format(entity.first_name, entity.last_name)
|
||||
elif entity.first_name:
|
||||
|
@ -83,7 +75,7 @@ def get_display_name(entity):
|
|||
else:
|
||||
return ''
|
||||
|
||||
elif isinstance(entity, (Chat, Channel)):
|
||||
elif isinstance(entity, (types.Chat, types.Channel)):
|
||||
return entity.title
|
||||
|
||||
return ''
|
||||
|
@ -93,13 +85,15 @@ def get_extension(media):
|
|||
"""Gets the corresponding extension for any Telegram media."""
|
||||
|
||||
# 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'
|
||||
|
||||
# Documents will come with a mime type
|
||||
if isinstance(media, MessageMediaDocument):
|
||||
if isinstance(media, types.MessageMediaDocument):
|
||||
media = media.document
|
||||
if isinstance(media, Document):
|
||||
if isinstance(media, (
|
||||
types.Document, types.WebDocument, types.WebDocumentNoProxy)):
|
||||
if media.mime_type == 'application/octet-stream':
|
||||
# Octet stream are just bytes, which have no default extension
|
||||
return ''
|
||||
|
@ -131,38 +125,38 @@ def get_input_peer(entity, allow_self=True):
|
|||
else:
|
||||
_raise_cast_fail(entity, 'InputPeer')
|
||||
|
||||
if isinstance(entity, User):
|
||||
if isinstance(entity, types.User):
|
||||
if entity.is_self and allow_self:
|
||||
return InputPeerSelf()
|
||||
return types.InputPeerSelf()
|
||||
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)):
|
||||
return InputPeerChat(entity.id)
|
||||
if isinstance(entity, (types.Chat, types.ChatEmpty, types.ChatForbidden)):
|
||||
return types.InputPeerChat(entity.id)
|
||||
|
||||
if isinstance(entity, (Channel, ChannelForbidden)):
|
||||
return InputPeerChannel(entity.id, entity.access_hash or 0)
|
||||
if isinstance(entity, (types.Channel, types.ChannelForbidden)):
|
||||
return types.InputPeerChannel(entity.id, entity.access_hash or 0)
|
||||
|
||||
if isinstance(entity, InputUser):
|
||||
return InputPeerUser(entity.user_id, entity.access_hash)
|
||||
if isinstance(entity, types.InputUser):
|
||||
return types.InputPeerUser(entity.user_id, entity.access_hash)
|
||||
|
||||
if isinstance(entity, InputChannel):
|
||||
return InputPeerChannel(entity.channel_id, entity.access_hash)
|
||||
if isinstance(entity, types.InputChannel):
|
||||
return types.InputPeerChannel(entity.channel_id, entity.access_hash)
|
||||
|
||||
if isinstance(entity, InputUserSelf):
|
||||
return InputPeerSelf()
|
||||
if isinstance(entity, types.InputUserSelf):
|
||||
return types.InputPeerSelf()
|
||||
|
||||
if isinstance(entity, UserEmpty):
|
||||
return InputPeerEmpty()
|
||||
if isinstance(entity, types.UserEmpty):
|
||||
return types.InputPeerEmpty()
|
||||
|
||||
if isinstance(entity, UserFull):
|
||||
if isinstance(entity, types.UserFull):
|
||||
return get_input_peer(entity.user)
|
||||
|
||||
if isinstance(entity, ChatFull):
|
||||
return InputPeerChat(entity.id)
|
||||
if isinstance(entity, types.ChatFull):
|
||||
return types.InputPeerChat(entity.id)
|
||||
|
||||
if isinstance(entity, PeerChat):
|
||||
return InputPeerChat(entity.chat_id)
|
||||
if isinstance(entity, types.PeerChat):
|
||||
return types.InputPeerChat(entity.chat_id)
|
||||
|
||||
_raise_cast_fail(entity, 'InputPeer')
|
||||
|
||||
|
@ -175,11 +169,11 @@ def get_input_channel(entity):
|
|||
except AttributeError:
|
||||
_raise_cast_fail(entity, 'InputChannel')
|
||||
|
||||
if isinstance(entity, (Channel, ChannelForbidden)):
|
||||
return InputChannel(entity.id, entity.access_hash or 0)
|
||||
if isinstance(entity, (types.Channel, types.ChannelForbidden)):
|
||||
return types.InputChannel(entity.id, entity.access_hash or 0)
|
||||
|
||||
if isinstance(entity, InputPeerChannel):
|
||||
return InputChannel(entity.channel_id, entity.access_hash)
|
||||
if isinstance(entity, types.InputPeerChannel):
|
||||
return types.InputChannel(entity.channel_id, entity.access_hash)
|
||||
|
||||
_raise_cast_fail(entity, 'InputChannel')
|
||||
|
||||
|
@ -192,23 +186,23 @@ def get_input_user(entity):
|
|||
except AttributeError:
|
||||
_raise_cast_fail(entity, 'InputUser')
|
||||
|
||||
if isinstance(entity, User):
|
||||
if isinstance(entity, types.User):
|
||||
if entity.is_self:
|
||||
return InputUserSelf()
|
||||
return types.InputUserSelf()
|
||||
else:
|
||||
return InputUser(entity.id, entity.access_hash or 0)
|
||||
return types.InputUser(entity.id, entity.access_hash or 0)
|
||||
|
||||
if isinstance(entity, InputPeerSelf):
|
||||
return InputUserSelf()
|
||||
if isinstance(entity, types.InputPeerSelf):
|
||||
return types.InputUserSelf()
|
||||
|
||||
if isinstance(entity, (UserEmpty, InputPeerEmpty)):
|
||||
return InputUserEmpty()
|
||||
if isinstance(entity, (types.UserEmpty, types.InputPeerEmpty)):
|
||||
return types.InputUserEmpty()
|
||||
|
||||
if isinstance(entity, UserFull):
|
||||
if isinstance(entity, types.UserFull):
|
||||
return get_input_user(entity.user)
|
||||
|
||||
if isinstance(entity, InputPeerUser):
|
||||
return InputUser(entity.user_id, entity.access_hash)
|
||||
if isinstance(entity, types.InputPeerUser):
|
||||
return types.InputUser(entity.user_id, entity.access_hash)
|
||||
|
||||
_raise_cast_fail(entity, 'InputUser')
|
||||
|
||||
|
@ -219,12 +213,12 @@ def get_input_dialog(dialog):
|
|||
if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer')
|
||||
return dialog
|
||||
if dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
|
||||
return InputDialogPeer(dialog)
|
||||
return types.InputDialogPeer(dialog)
|
||||
except AttributeError:
|
||||
_raise_cast_fail(dialog, 'InputDialogPeer')
|
||||
|
||||
try:
|
||||
return InputDialogPeer(get_input_peer(dialog))
|
||||
return types.InputDialogPeer(get_input_peer(dialog))
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
|
@ -239,16 +233,17 @@ def get_input_document(document):
|
|||
except AttributeError:
|
||||
_raise_cast_fail(document, 'InputDocument')
|
||||
|
||||
if isinstance(document, Document):
|
||||
return InputDocument(id=document.id, access_hash=document.access_hash)
|
||||
if isinstance(document, types.Document):
|
||||
return types.InputDocument(
|
||||
id=document.id, access_hash=document.access_hash)
|
||||
|
||||
if isinstance(document, DocumentEmpty):
|
||||
return InputDocumentEmpty()
|
||||
if isinstance(document, types.DocumentEmpty):
|
||||
return types.InputDocumentEmpty()
|
||||
|
||||
if isinstance(document, MessageMediaDocument):
|
||||
if isinstance(document, types.MessageMediaDocument):
|
||||
return get_input_document(document.document)
|
||||
|
||||
if isinstance(document, Message):
|
||||
if isinstance(document, types.Message):
|
||||
return get_input_document(document.media)
|
||||
|
||||
_raise_cast_fail(document, 'InputDocument')
|
||||
|
@ -262,14 +257,14 @@ def get_input_photo(photo):
|
|||
except AttributeError:
|
||||
_raise_cast_fail(photo, 'InputPhoto')
|
||||
|
||||
if isinstance(photo, photos.Photo):
|
||||
if isinstance(photo, types.photos.Photo):
|
||||
photo = photo.photo
|
||||
|
||||
if isinstance(photo, Photo):
|
||||
return InputPhoto(id=photo.id, access_hash=photo.access_hash)
|
||||
if isinstance(photo, types.Photo):
|
||||
return types.InputPhoto(id=photo.id, access_hash=photo.access_hash)
|
||||
|
||||
if isinstance(photo, PhotoEmpty):
|
||||
return InputPhotoEmpty()
|
||||
if isinstance(photo, types.PhotoEmpty):
|
||||
return types.InputPhotoEmpty()
|
||||
|
||||
_raise_cast_fail(photo, 'InputPhoto')
|
||||
|
||||
|
@ -282,16 +277,16 @@ def get_input_geo(geo):
|
|||
except AttributeError:
|
||||
_raise_cast_fail(geo, 'InputGeoPoint')
|
||||
|
||||
if isinstance(geo, GeoPoint):
|
||||
return InputGeoPoint(lat=geo.lat, long=geo.long)
|
||||
if isinstance(geo, types.GeoPoint):
|
||||
return types.InputGeoPoint(lat=geo.lat, long=geo.long)
|
||||
|
||||
if isinstance(geo, GeoPointEmpty):
|
||||
return InputGeoPointEmpty()
|
||||
if isinstance(geo, types.GeoPointEmpty):
|
||||
return types.InputGeoPointEmpty()
|
||||
|
||||
if isinstance(geo, MessageMediaGeo):
|
||||
if isinstance(geo, types.MessageMediaGeo):
|
||||
return get_input_geo(geo.geo)
|
||||
|
||||
if isinstance(geo, Message):
|
||||
if isinstance(geo, types.Message):
|
||||
return get_input_geo(geo.media)
|
||||
|
||||
_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')
|
||||
return media
|
||||
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')
|
||||
return InputMediaDocument(media)
|
||||
return types.InputMediaDocument(media)
|
||||
except AttributeError:
|
||||
_raise_cast_fail(media, 'InputMedia')
|
||||
|
||||
if isinstance(media, MessageMediaPhoto):
|
||||
return InputMediaPhoto(
|
||||
if isinstance(media, types.MessageMediaPhoto):
|
||||
return types.InputMediaPhoto(
|
||||
id=get_input_photo(media.photo),
|
||||
ttl_seconds=media.ttl_seconds
|
||||
)
|
||||
|
||||
if isinstance(media, (Photo, photos.Photo, PhotoEmpty)):
|
||||
return InputMediaPhoto(
|
||||
if isinstance(media, (types.Photo, types.photos.Photo, types.PhotoEmpty)):
|
||||
return types.InputMediaPhoto(
|
||||
id=get_input_photo(media)
|
||||
)
|
||||
|
||||
if isinstance(media, MessageMediaDocument):
|
||||
return InputMediaDocument(
|
||||
if isinstance(media, types.MessageMediaDocument):
|
||||
return types.InputMediaDocument(
|
||||
id=get_input_document(media.document),
|
||||
ttl_seconds=media.ttl_seconds
|
||||
)
|
||||
|
||||
if isinstance(media, (Document, DocumentEmpty)):
|
||||
return InputMediaDocument(
|
||||
if isinstance(media, (types.Document, types.DocumentEmpty)):
|
||||
return types.InputMediaDocument(
|
||||
id=get_input_document(media)
|
||||
)
|
||||
|
||||
if isinstance(media, FileLocation):
|
||||
if isinstance(media, types.FileLocation):
|
||||
if is_photo:
|
||||
return InputMediaUploadedPhoto(file=media)
|
||||
return types.InputMediaUploadedPhoto(file=media)
|
||||
else:
|
||||
return InputMediaUploadedDocument(
|
||||
return types.InputMediaUploadedDocument(
|
||||
file=media,
|
||||
mime_type='application/octet-stream', # unknown, assume bytes
|
||||
attributes=[DocumentAttributeFilename('unnamed')]
|
||||
attributes=[types.DocumentAttributeFilename('unnamed')]
|
||||
)
|
||||
|
||||
if isinstance(media, MessageMediaGame):
|
||||
return InputMediaGame(id=media.game.id)
|
||||
if isinstance(media, types.MessageMediaGame):
|
||||
return types.InputMediaGame(id=media.game.id)
|
||||
|
||||
if isinstance(media, (ChatPhoto, UserProfilePhoto)):
|
||||
if isinstance(media.photo_big, FileLocationUnavailable):
|
||||
if isinstance(media, (types.ChatPhoto, types.UserProfilePhoto)):
|
||||
if isinstance(media.photo_big, types.FileLocationUnavailable):
|
||||
media = media.photo_small
|
||||
else:
|
||||
media = media.photo_big
|
||||
return get_input_media(media, is_photo=True)
|
||||
|
||||
if isinstance(media, MessageMediaContact):
|
||||
return InputMediaContact(
|
||||
if isinstance(media, types.MessageMediaContact):
|
||||
return types.InputMediaContact(
|
||||
phone_number=media.phone_number,
|
||||
first_name=media.first_name,
|
||||
last_name=media.last_name,
|
||||
vcard=''
|
||||
)
|
||||
|
||||
if isinstance(media, MessageMediaGeo):
|
||||
return InputMediaGeoPoint(geo_point=get_input_geo(media.geo))
|
||||
if isinstance(media, types.MessageMediaGeo):
|
||||
return types.InputMediaGeoPoint(geo_point=get_input_geo(media.geo))
|
||||
|
||||
if isinstance(media, MessageMediaVenue):
|
||||
return InputMediaVenue(
|
||||
if isinstance(media, types.MessageMediaVenue):
|
||||
return types.InputMediaVenue(
|
||||
geo_point=get_input_geo(media.geo),
|
||||
title=media.title,
|
||||
address=media.address,
|
||||
|
@ -378,11 +373,12 @@ def get_input_media(media, is_photo=False):
|
|||
)
|
||||
|
||||
if isinstance(media, (
|
||||
MessageMediaEmpty, MessageMediaUnsupported,
|
||||
ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable)):
|
||||
return InputMediaEmpty()
|
||||
types.MessageMediaEmpty, types.MessageMediaUnsupported,
|
||||
types.ChatPhotoEmpty, types.UserProfilePhotoEmpty,
|
||||
types.FileLocationUnavailable)):
|
||||
return types.InputMediaEmpty()
|
||||
|
||||
if isinstance(media, Message):
|
||||
if isinstance(media, types.Message):
|
||||
return get_input_media(media.media, is_photo=is_photo)
|
||||
|
||||
_raise_cast_fail(media, 'InputMedia')
|
||||
|
@ -392,11 +388,11 @@ def get_input_message(message):
|
|||
"""Similar to :meth:`get_input_peer`, but for input messages."""
|
||||
try:
|
||||
if isinstance(message, int): # This case is really common too
|
||||
return InputMessageID(message)
|
||||
return types.InputMessageID(message)
|
||||
elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'):
|
||||
return message
|
||||
elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'):
|
||||
return InputMessageID(message.id)
|
||||
return types.InputMessageID(message.id)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
@ -404,16 +400,13 @@ def get_input_message(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:
|
||||
return None
|
||||
|
||||
if isinstance(message, int):
|
||||
return message
|
||||
|
||||
if hasattr(message, 'original_message'):
|
||||
return message.original_message.id
|
||||
|
||||
try:
|
||||
if message.SUBCLASS_OF_ID == 0x790009e3:
|
||||
# hex(crc32(b'Message')) = 0x790009e3
|
||||
|
@ -424,6 +417,71 @@ def get_message_id(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):
|
||||
"""
|
||||
Converts the given parse mode into an object with
|
||||
|
@ -458,34 +516,42 @@ def sanitize_parse_mode(mode):
|
|||
|
||||
|
||||
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:
|
||||
if location.SUBCLASS_OF_ID == 0x1523d462:
|
||||
return location # crc32(b'InputFileLocation'):
|
||||
return None, location # crc32(b'InputFileLocation'):
|
||||
except AttributeError:
|
||||
_raise_cast_fail(location, 'InputFileLocation')
|
||||
|
||||
if isinstance(location, Message):
|
||||
if isinstance(location, types.Message):
|
||||
location = location.media
|
||||
|
||||
if isinstance(location, MessageMediaDocument):
|
||||
if isinstance(location, types.MessageMediaDocument):
|
||||
location = location.document
|
||||
elif isinstance(location, MessageMediaPhoto):
|
||||
elif isinstance(location, types.MessageMediaPhoto):
|
||||
location = location.photo
|
||||
|
||||
if isinstance(location, Document):
|
||||
return InputDocumentFileLocation(
|
||||
location.id, location.access_hash, location.version)
|
||||
elif isinstance(location, Photo):
|
||||
if isinstance(location, types.Document):
|
||||
return (location.dc_id, types.InputDocumentFileLocation(
|
||||
location.id, location.access_hash, location.version))
|
||||
elif isinstance(location, types.Photo):
|
||||
try:
|
||||
location = next(x for x in reversed(location.sizes)
|
||||
if not isinstance(x, PhotoSizeEmpty)).location
|
||||
location = next(
|
||||
x for x in reversed(location.sizes)
|
||||
if not isinstance(x, types.PhotoSizeEmpty)
|
||||
).location
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
if isinstance(location, (FileLocation, FileLocationUnavailable)):
|
||||
return InputFileLocation(
|
||||
location.volume_id, location.local_id, location.secret)
|
||||
if isinstance(location, (
|
||||
types.FileLocation, types.FileLocationUnavailable)):
|
||||
return (getattr(location, 'dc_id', None), types.InputFileLocation(
|
||||
location.volume_id, location.local_id, location.secret))
|
||||
|
||||
_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.
|
||||
"""
|
||||
return isinstance(obj, (list, tuple, set, dict,
|
||||
UserList, types.GeneratorType))
|
||||
UserList, GeneratorType))
|
||||
|
||||
|
||||
def parse_phone(phone):
|
||||
|
@ -609,7 +675,9 @@ def get_peer_id(peer, add_mark=True):
|
|||
|
||||
try:
|
||||
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
|
||||
else:
|
||||
# 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')
|
||||
|
||||
# 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
|
||||
elif isinstance(peer, (PeerChat, InputPeerChat)):
|
||||
elif isinstance(peer, (types.PeerChat, types.InputPeerChat)):
|
||||
# Check in case the user mixed things up to avoid blowing up
|
||||
if not (0 < peer.chat_id <= 0x7fffffff):
|
||||
peer.chat_id = resolve_id(peer.chat_id)[0]
|
||||
|
||||
return -peer.chat_id if add_mark else peer.chat_id
|
||||
elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)):
|
||||
if isinstance(peer, ChannelFull):
|
||||
elif isinstance(peer, (
|
||||
types.PeerChannel, types.InputPeerChannel, types.ChannelFull)):
|
||||
if isinstance(peer, types.ChannelFull):
|
||||
# Special case: .get_input_peer can't return InputChannel from
|
||||
# ChannelFull since it doesn't have an .access_hash attribute.
|
||||
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
|
||||
if not (0 < i <= 0x7fffffff):
|
||||
i = resolve_id(i)[0]
|
||||
if isinstance(peer, ChannelFull):
|
||||
if isinstance(peer, types.ChannelFull):
|
||||
peer.id = i
|
||||
else:
|
||||
peer.channel_id = i
|
||||
|
@ -655,7 +724,7 @@ def get_peer_id(peer, add_mark=True):
|
|||
def resolve_id(marked_id):
|
||||
"""Given a marked ID, returns the original ID and its :tl:`Peer` type."""
|
||||
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
|
||||
# marked version is -10000xyz, which in turn looks like a channel but
|
||||
|
@ -663,9 +732,105 @@ def resolve_id(marked_id):
|
|||
# two zeroes.
|
||||
m = re.match(r'-100([^0]\d*)', str(marked_id))
|
||||
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):
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# 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
|
||||
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)
|
||||
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>:
|
||||
Either <code>True</code> or <code>False</code>.
|
||||
</li>
|
||||
<li id="true"><b>true</b>:
|
||||
<li id="true"><b>flag</b>:
|
||||
These arguments aren't actually sent but rather encoded as flags.
|
||||
Any truthy value (<code>True</code>, <code>7</code>) will enable
|
||||
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.')
|
||||
otherwise = True
|
||||
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
|
||||
|
||||
if arg.type in {'InputPeer', 'InputUser', 'InputChannel'}:
|
||||
|
@ -370,11 +371,12 @@ def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
|
|||
bold=True)
|
||||
|
||||
# Type row
|
||||
friendly_type = 'flag' if arg.type == 'true' else arg.type
|
||||
if arg.is_generic:
|
||||
docs.add_row('!' + arg.type, align='center')
|
||||
docs.add_row('!' + friendly_type, align='center')
|
||||
else:
|
||||
docs.add_row(
|
||||
arg.type, align='center', link=
|
||||
friendly_type, align='center', link=
|
||||
path_for_type(arg.type, relative_to=filename)
|
||||
)
|
||||
|
||||
|
|
|
@ -36,6 +36,13 @@ NAMED_AUTO_CASTS = {
|
|||
BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128',
|
||||
'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(
|
||||
out_dir, depth, kind, namespace_tlobjects, type_constructors):
|
||||
|
@ -130,11 +137,14 @@ def _write_modules(
|
|||
|
||||
# Generate the class for every TLObject
|
||||
for t in tlobjects:
|
||||
_write_source_code(t, kind, builder, type_constructors)
|
||||
builder.current_indent = 0
|
||||
if t.fullname in PATCHED_TYPES:
|
||||
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.
|
||||
builder.writeln('')
|
||||
builder.writeln()
|
||||
for line in type_defs:
|
||||
builder.writeln(line)
|
||||
|
||||
|
@ -618,11 +628,38 @@ def _write_arg_read_code(builder, arg, args, name):
|
|||
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):
|
||||
builder.writeln(AUTO_GEN_NOTICE)
|
||||
builder.writeln()
|
||||
|
||||
builder.writeln('from . import types, functions')
|
||||
builder.writeln('from . import types, functions, patched')
|
||||
builder.writeln()
|
||||
|
||||
# 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)
|
||||
for tlobject in tlobjects:
|
||||
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:
|
||||
builder.write('.' + tlobject.namespace)
|
||||
builder.write('.{}', tlobject.namespace)
|
||||
|
||||
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):
|
||||
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
|
||||
namespace_functions = defaultdict(list)
|
||||
namespace_types = defaultdict(list)
|
||||
namespace_patched = defaultdict(list)
|
||||
|
||||
# Group {type: [constructors]} to generate the documentation
|
||||
type_constructors = defaultdict(list)
|
||||
|
@ -663,11 +702,15 @@ def generate_tlobjects(tlobjects, layer, import_depth, output_dir):
|
|||
else:
|
||||
namespace_types[tlobject.namespace].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',
|
||||
namespace_functions, type_constructors)
|
||||
_write_modules(get_file('types'), import_depth, 'TLObject',
|
||||
namespace_types, type_constructors)
|
||||
_write_patched(get_file('patched'), namespace_patched)
|
||||
|
||||
filename = os.path.join(get_file('alltlobjects.py'))
|
||||
with open(filename, 'w', encoding='utf-8') as file:
|
||||
|
|
Loading…
Reference in New Issue
Block a user