Move custom and core objects to a new subpackage

This should keep it cleaner, as now _tl is fully auto-generated.
This commit is contained in:
Lonami Exo
2021-09-12 16:05:56 +02:00
parent c08d724baa
commit 604c3de070
23 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
"""
This module holds core "special" types, which are more convenient ways
to do stuff in a `telethon.network.mtprotosender.MTProtoSender` instance.
Only special cases are gzip-packed data, the response message (not a
client message), the message container which references these messages
and would otherwise conflict with the rest, and finally the RpcResult:
rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult;
Three things to note with this definition:
1. The constructor ID is actually ``42d36c2c``.
2. Those bytes are not read like the rest of bytes (length + payload).
They are actually the raw bytes of another object, which can't be
read directly because it depends on per-request information (since
some can return ``Vector<int>`` and ``Vector<long>``).
3. Those bytes may be gzipped data, which needs to be treated early.
"""
from .tlmessage import TLMessage
from .gzippacked import GzipPacked
from .messagecontainer import MessageContainer
from .rpcresult import RpcResult
core_objects = {x.CONSTRUCTOR_ID: x for x in (
GzipPacked, MessageContainer, RpcResult
)}

View File

@@ -0,0 +1,45 @@
import gzip
import struct
from .. import TLObject
class GzipPacked(TLObject):
CONSTRUCTOR_ID = 0x3072cfa1
def __init__(self, data):
self.data = data
@staticmethod
def gzip_if_smaller(content_related, data):
"""Calls bytes(request), and based on a certain threshold,
optionally gzips the resulting data. If the gzipped data is
smaller than the original byte array, this is returned instead.
Note that this only applies to content related requests.
"""
if content_related and len(data) > 512:
gzipped = bytes(GzipPacked(data))
return gzipped if len(gzipped) < len(data) else data
else:
return data
def __bytes__(self):
return struct.pack('<I', GzipPacked.CONSTRUCTOR_ID) + \
TLObject.serialize_bytes(gzip.compress(self.data))
@staticmethod
def read(reader):
constructor = reader.read_int(signed=False)
assert constructor == GzipPacked.CONSTRUCTOR_ID
return gzip.decompress(reader.tgread_bytes())
@classmethod
def from_reader(cls, reader):
return GzipPacked(gzip.decompress(reader.tgread_bytes()))
def to_dict(self):
return {
'_': 'GzipPacked',
'data': self.data
}

View File

@@ -0,0 +1,47 @@
from .tlmessage import TLMessage
from ..tlobject import TLObject
class MessageContainer(TLObject):
CONSTRUCTOR_ID = 0x73f1f8dc
# Maximum size in bytes for the inner payload of the container.
# Telegram will close the connection if the payload is bigger.
# The overhead of the container itself is subtracted.
MAXIMUM_SIZE = 1044456 - 8
# Maximum amount of messages that can't be sent inside a single
# container, inclusive. Beyond this limit Telegram will respond
# with BAD_MESSAGE 64 (invalid container).
#
# This limit is not 100% accurate and may in some cases be higher.
# However, sending up to 100 requests at once in a single container
# is a reasonable conservative value, since it could also depend on
# other factors like size per request, but we cannot know this.
MAXIMUM_LENGTH = 100
def __init__(self, messages):
self.messages = messages
def to_dict(self):
return {
'_': 'MessageContainer',
'messages':
[] if self.messages is None else [
None if x is None else x.to_dict() for x in self.messages
],
}
@classmethod
def from_reader(cls, reader):
# This assumes that .read_* calls are done in the order they appear
messages = []
for _ in range(reader.read_int()):
msg_id = reader.read_long()
seq_no = reader.read_int()
length = reader.read_int()
before = reader.tell_position()
obj = reader.tgread_object() # May over-read e.g. RpcResult
reader.set_position(before + length)
messages.append(TLMessage(msg_id, seq_no, obj))
return MessageContainer(messages)

View File

@@ -0,0 +1,34 @@
from .gzippacked import GzipPacked
from .. import TLObject, RpcError
class RpcResult(TLObject):
CONSTRUCTOR_ID = 0xf35c6d01
def __init__(self, req_msg_id, body, error):
self.req_msg_id = req_msg_id
self.body = body
self.error = error
@classmethod
def from_reader(cls, reader):
msg_id = reader.read_long()
inner_code = reader.read_int(signed=False)
if inner_code == RpcError.CONSTRUCTOR_ID:
return RpcResult(msg_id, None, RpcError.from_reader(reader))
if inner_code == GzipPacked.CONSTRUCTOR_ID:
return RpcResult(msg_id, GzipPacked.from_reader(reader).data, None)
reader.seek(-4)
# This reader.read() will read more than necessary, but it's okay.
# We could make use of MessageContainer's length here, but since
# it's not necessary we don't need to care about it.
return RpcResult(msg_id, reader.read(), None)
def to_dict(self):
return {
'_': 'RpcResult',
'req_msg_id': self.req_msg_id,
'body': self.body,
'error': self.error
}

View File

@@ -0,0 +1,34 @@
from .. import TLObject
class TLMessage(TLObject):
"""
https://core.telegram.org/mtproto/service_messages#simple-container.
Messages are what's ultimately sent to Telegram:
message msg_id:long seqno:int bytes:int body:bytes = Message;
Each message has its own unique identifier, and the body is simply
the serialized request that should be executed on the server, or
the response object from Telegram. Since the body is always a valid
object, it makes sense to store the object and not the bytes to
ease working with them.
There is no need to add serializing logic here since that can be
inlined and is unlikely to change. Thus these are only needed to
encapsulate responses.
"""
SIZE_OVERHEAD = 12
def __init__(self, msg_id, seq_no, obj):
self.msg_id = msg_id
self.seq_no = seq_no
self.obj = obj
def to_dict(self):
return {
'_': 'TLMessage',
'msg_id': self.msg_id,
'seq_no': self.seq_no,
'obj': self.obj
}

View File

@@ -0,0 +1,13 @@
from .adminlogevent import AdminLogEvent
from .draft import Draft
from .dialog import Dialog
from .inputsizedfile import InputSizedFile
from .messagebutton import MessageButton
from .forward import Forward
from .message import Message
from .button import Button
from .inlinebuilder import InlineBuilder
from .inlineresult import InlineResult
from .inlineresults import InlineResults
from .qrlogin import QRLogin
from .participantpermissions import ParticipantPermissions

View File

@@ -0,0 +1,475 @@
from ... import _tl
from ..._misc.utils import get_input_peer
class AdminLogEvent:
"""
Represents a more friendly interface for admin log events.
Members:
original (:tl:`ChannelAdminLogEvent`):
The original :tl:`ChannelAdminLogEvent`.
entities (`dict`):
A dictionary mapping user IDs to :tl:`User`.
When `old` and `new` are :tl:`ChannelParticipant`, you can
use this dictionary to map the ``user_id``, ``kicked_by``,
``inviter_id`` and ``promoted_by`` IDs to their :tl:`User`.
user (:tl:`User`):
The user that caused this action (``entities[original.user_id]``).
input_user (:tl:`InputPeerUser`):
Input variant of `user`.
"""
def __init__(self, original, entities):
self.original = original
self.entities = entities
self.user = entities[original.user_id]
self.input_user = get_input_peer(self.user)
@property
def id(self):
"""
The ID of this event.
"""
return self.original.id
@property
def date(self):
"""
The date when this event occurred.
"""
return self.original.date
@property
def user_id(self):
"""
The ID of the user that triggered this event.
"""
return self.original.user_id
@property
def action(self):
"""
The original :tl:`ChannelAdminLogEventAction`.
"""
return self.original.action
@property
def old(self):
"""
The old value from the event.
"""
ori = self.original.action
if isinstance(ori, (
types.ChannelAdminLogEventActionChangeAbout,
types.ChannelAdminLogEventActionChangeTitle,
types.ChannelAdminLogEventActionChangeUsername,
types.ChannelAdminLogEventActionChangeLocation,
types.ChannelAdminLogEventActionChangeHistoryTTL,
)):
return ori.prev_value
elif isinstance(ori, types.ChannelAdminLogEventActionChangePhoto):
return ori.prev_photo
elif isinstance(ori, types.ChannelAdminLogEventActionChangeStickerSet):
return ori.prev_stickerset
elif isinstance(ori, types.ChannelAdminLogEventActionEditMessage):
return ori.prev_message
elif isinstance(ori, (
types.ChannelAdminLogEventActionParticipantToggleAdmin,
types.ChannelAdminLogEventActionParticipantToggleBan
)):
return ori.prev_participant
elif isinstance(ori, (
types.ChannelAdminLogEventActionToggleInvites,
types.ChannelAdminLogEventActionTogglePreHistoryHidden,
types.ChannelAdminLogEventActionToggleSignatures
)):
return not ori.new_value
elif isinstance(ori, types.ChannelAdminLogEventActionDeleteMessage):
return ori.message
elif isinstance(ori, types.ChannelAdminLogEventActionDefaultBannedRights):
return ori.prev_banned_rights
elif isinstance(ori, types.ChannelAdminLogEventActionDiscardGroupCall):
return ori.call
elif isinstance(ori, (
types.ChannelAdminLogEventActionExportedInviteDelete,
types.ChannelAdminLogEventActionExportedInviteRevoke,
types.ChannelAdminLogEventActionParticipantJoinByInvite,
)):
return ori.invite
elif isinstance(ori, types.ChannelAdminLogEventActionExportedInviteEdit):
return ori.prev_invite
@property
def new(self):
"""
The new value present in the event.
"""
ori = self.original.action
if isinstance(ori, (
types.ChannelAdminLogEventActionChangeAbout,
types.ChannelAdminLogEventActionChangeTitle,
types.ChannelAdminLogEventActionChangeUsername,
types.ChannelAdminLogEventActionToggleInvites,
types.ChannelAdminLogEventActionTogglePreHistoryHidden,
types.ChannelAdminLogEventActionToggleSignatures,
types.ChannelAdminLogEventActionChangeLocation,
types.ChannelAdminLogEventActionChangeHistoryTTL,
)):
return ori.new_value
elif isinstance(ori, types.ChannelAdminLogEventActionChangePhoto):
return ori.new_photo
elif isinstance(ori, types.ChannelAdminLogEventActionChangeStickerSet):
return ori.new_stickerset
elif isinstance(ori, types.ChannelAdminLogEventActionEditMessage):
return ori.new_message
elif isinstance(ori, (
types.ChannelAdminLogEventActionParticipantToggleAdmin,
types.ChannelAdminLogEventActionParticipantToggleBan
)):
return ori.new_participant
elif isinstance(ori, (
types.ChannelAdminLogEventActionParticipantInvite,
types.ChannelAdminLogEventActionParticipantVolume,
)):
return ori.participant
elif isinstance(ori, types.ChannelAdminLogEventActionDefaultBannedRights):
return ori.new_banned_rights
elif isinstance(ori, types.ChannelAdminLogEventActionStopPoll):
return ori.message
elif isinstance(ori, types.ChannelAdminLogEventActionStartGroupCall):
return ori.call
elif isinstance(ori, (
types.ChannelAdminLogEventActionParticipantMute,
types.ChannelAdminLogEventActionParticipantUnmute,
)):
return ori.participant
elif isinstance(ori, types.ChannelAdminLogEventActionToggleGroupCallSetting):
return ori.join_muted
elif isinstance(ori, types.ChannelAdminLogEventActionExportedInviteEdit):
return ori.new_invite
@property
def changed_about(self):
"""
Whether the channel's about was changed or not.
If `True`, `old` and `new` will be present as `str`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionChangeAbout)
@property
def changed_title(self):
"""
Whether the channel's title was changed or not.
If `True`, `old` and `new` will be present as `str`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionChangeTitle)
@property
def changed_username(self):
"""
Whether the channel's username was changed or not.
If `True`, `old` and `new` will be present as `str`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionChangeUsername)
@property
def changed_photo(self):
"""
Whether the channel's photo was changed or not.
If `True`, `old` and `new` will be present as :tl:`Photo`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionChangePhoto)
@property
def changed_sticker_set(self):
"""
Whether the channel's sticker set was changed or not.
If `True`, `old` and `new` will be present as :tl:`InputStickerSet`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionChangeStickerSet)
@property
def changed_message(self):
"""
Whether a message in this channel was edited or not.
If `True`, `old` and `new` will be present as
`Message <telethon.tl.custom.message.Message>`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionEditMessage)
@property
def deleted_message(self):
"""
Whether a message in this channel was deleted or not.
If `True`, `old` will be present as
`Message <telethon.tl.custom.message.Message>`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionDeleteMessage)
@property
def changed_admin(self):
"""
Whether the permissions for an admin in this channel
changed or not.
If `True`, `old` and `new` will be present as
:tl:`ChannelParticipant`.
"""
return isinstance(
self.original.action,
types.ChannelAdminLogEventActionParticipantToggleAdmin)
@property
def changed_restrictions(self):
"""
Whether a message in this channel was edited or not.
If `True`, `old` and `new` will be present as
:tl:`ChannelParticipant`.
"""
return isinstance(
self.original.action,
types.ChannelAdminLogEventActionParticipantToggleBan)
@property
def changed_invites(self):
"""
Whether the invites in the channel were toggled or not.
If `True`, `old` and `new` will be present as `bool`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionToggleInvites)
@property
def changed_location(self):
"""
Whether the location setting of the channel has changed or not.
If `True`, `old` and `new` will be present as :tl:`ChannelLocation`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionChangeLocation)
@property
def joined(self):
"""
Whether `user` joined through the channel's
public username or not.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionParticipantJoin)
@property
def joined_invite(self):
"""
Whether a new user joined through an invite
link to the channel or not.
If `True`, `new` will be present as
:tl:`ChannelParticipant`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionParticipantInvite)
@property
def left(self):
"""
Whether `user` left the channel or not.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionParticipantLeave)
@property
def changed_hide_history(self):
"""
Whether hiding the previous message history for new members
in the channel was toggled or not.
If `True`, `old` and `new` will be present as `bool`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionTogglePreHistoryHidden)
@property
def changed_signatures(self):
"""
Whether the message signatures in the channel were toggled
or not.
If `True`, `old` and `new` will be present as `bool`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionToggleSignatures)
@property
def changed_pin(self):
"""
Whether a new message in this channel was pinned or not.
If `True`, `new` will be present as
`Message <telethon.tl.custom.message.Message>`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionUpdatePinned)
@property
def changed_default_banned_rights(self):
"""
Whether the default banned rights were changed or not.
If `True`, `old` and `new` will
be present as :tl:`ChatBannedRights`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionDefaultBannedRights)
@property
def stopped_poll(self):
"""
Whether a poll was stopped or not.
If `True`, `new` will be present as
`Message <telethon.tl.custom.message.Message>`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionStopPoll)
@property
def started_group_call(self):
"""
Whether a group call was started or not.
If `True`, `new` will be present as :tl:`InputGroupCall`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionStartGroupCall)
@property
def discarded_group_call(self):
"""
Whether a group call was started or not.
If `True`, `old` will be present as :tl:`InputGroupCall`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionDiscardGroupCall)
@property
def user_muted(self):
"""
Whether a participant was muted in the ongoing group call or not.
If `True`, `new` will be present as :tl:`GroupCallParticipant`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionParticipantMute)
@property
def user_unmutted(self):
"""
Whether a participant was unmuted from the ongoing group call or not.
If `True`, `new` will be present as :tl:`GroupCallParticipant`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionParticipantUnmute)
@property
def changed_call_settings(self):
"""
Whether the group call settings were changed or not.
If `True`, `new` will be `True` if new users are muted on join.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionToggleGroupCallSetting)
@property
def changed_history_ttl(self):
"""
Whether the Time To Live of the message history has changed.
Messages sent after this change will have a ``ttl_period`` in seconds
indicating how long they should live for before being auto-deleted.
If `True`, `old` will be the old TTL, and `new` the new TTL, in seconds.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionChangeHistoryTTL)
@property
def deleted_exported_invite(self):
"""
Whether the exported chat invite has been deleted.
If `True`, `old` will be the deleted :tl:`ExportedChatInvite`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionExportedInviteDelete)
@property
def edited_exported_invite(self):
"""
Whether the exported chat invite has been edited.
If `True`, `old` and `new` will be the old and new
:tl:`ExportedChatInvite`, respectively.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionExportedInviteEdit)
@property
def revoked_exported_invite(self):
"""
Whether the exported chat invite has been revoked.
If `True`, `old` will be the revoked :tl:`ExportedChatInvite`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionExportedInviteRevoke)
@property
def joined_by_invite(self):
"""
Whether a new participant has joined with the use of an invite link.
If `True`, `old` will be pre-existing (old) :tl:`ExportedChatInvite`
used to join.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionParticipantJoinByInvite)
@property
def changed_user_volume(self):
"""
Whether a participant's volume in a call has been changed.
If `True`, `new` will be the updated :tl:`GroupCallParticipant`.
"""
return isinstance(self.original.action,
types.ChannelAdminLogEventActionParticipantVolume)
def __str__(self):
return str(self.original)
def stringify(self):
return self.original.stringify()

View File

@@ -0,0 +1,308 @@
from ... import _tl
from ..._misc import utils
class Button:
"""
.. note::
This class is used to **define** reply markups, e.g. when
sending a message or replying to events. When you access
`Message.buttons <telethon.tl.custom.message.Message.buttons>`
they are actually `MessageButton
<telethon.tl.custom.messagebutton.MessageButton>`,
so you might want to refer to that class instead.
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`, `url`, `auth`, `buy` and `game`
together to create inline buttons (under the message).
You can use `text`, `request_location`, `request_phone` and `request_poll`
together to create a reply markup (replaces the user keyboard).
You can also configure the aspect of the reply with these.
The latest message with a reply markup will be the one shown to the user
(messages contain the buttons, not the chat itself).
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, *, resize, single_use, selective):
self.button = button
self.resize = resize
self.single_use = single_use
self.selective = selective
@staticmethod
def _is_inline(button):
"""
Returns `True` if the button belongs to an inline keyboard.
"""
return isinstance(button, (
types.KeyboardButtonBuy,
types.KeyboardButtonCallback,
types.KeyboardButtonGame,
types.KeyboardButtonSwitchInline,
types.KeyboardButtonUrl,
types.InputKeyboardButtonUrlAuth
))
@staticmethod
def inline(text, data=None):
"""
Creates a new inline button with some payload data in it.
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 you need to store more than 64 bytes, consider saving the real
data in a database and a reference to that data inside the button.
When the user clicks this button, `events.CallbackQuery
<telethon.events.callbackquery.CallbackQuery>` will trigger with the
same data that the button contained, so that you can determine which
button was pressed.
"""
if not data:
data = text.encode('utf-8')
elif not isinstance(data, (bytes, bytearray, memoryview)):
data = str(data).encode('utf-8')
if len(data) > 64:
raise ValueError('Too many bytes for the data')
return types.KeyboardButtonCallback(text, data)
@staticmethod
def switch_inline(text, query='', same_peer=False):
"""
Creates a new inline 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.
When the user clicks this button, after a chat is selected, their
input field will be filled with the username of your bot followed
by the query text, ready to make inline queries.
"""
return types.KeyboardButtonSwitchInline(text, query, same_peer)
@staticmethod
def url(text, url=None):
"""
Creates a new inline button to open the desired URL on click.
If no `url` is given, the `text` will be used as said URL instead.
You cannot detect that the user clicked this button directly.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to open the displayed URL unless
the domain is trusted, and once confirmed the URL will open in their
device.
"""
return types.KeyboardButtonUrl(text, url or text)
@staticmethod
def auth(text, url=None, *, bot=None, write_access=False, fwd_text=None):
"""
Creates a new inline button to authorize the user at the given URL.
You should set the `url` to be on the same domain as the one configured
for the desired `bot` via `@BotFather <https://t.me/BotFather>`_ using
the ``/setdomain`` command.
For more information about letting the user login via Telegram to
a certain domain, see https://core.telegram.org/widgets/login.
If no `url` is specified, it will default to `text`.
Args:
bot (`hints.EntityLike`):
The bot that requires this authorization. By default, this
is the bot that is currently logged in (itself), although
you may pass a different input peer.
.. note::
For now, you cannot use ID or username for this argument.
If you want to use a different bot than the one currently
logged in, you must manually use `client.get_input_entity()
<telethon.client.users.UserMethods.get_input_entity>`.
write_access (`bool`):
Whether write access is required or not.
This is `False` by default (read-only access).
fwd_text (`str`):
The new text to show in the button if the message is
forwarded. By default, the button text will be the same.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to login to the specified domain.
"""
return types.InputKeyboardButtonUrlAuth(
text=text,
url=url or text,
bot=utils.get_input_user(bot or types.InputUserSelf()),
request_write_access=write_access,
fwd_text=fwd_text
)
@classmethod
def text(cls, text, *, resize=None, single_use=None, selective=None):
"""
Creates a new keyboard button with the given text.
Args:
resize (`bool`):
If present, the entire keyboard will be reconfigured to
be resized and be smaller if there are not many buttons.
single_use (`bool`):
If present, the entire keyboard will be reconfigured to
be usable only once before it hides itself.
selective (`bool`):
If present, the entire keyboard will be reconfigured to
be "selective". The keyboard will be shown only to specific
users. It will target users that are @mentioned in the text
of the message or to the sender of the message you reply to.
When the user clicks this button, a text message with the same text
as the button will be sent, and can be handled with `events.NewMessage
<telethon.events.newmessage.NewMessage>`. You cannot distinguish
between a button press and the user typing and sending exactly the
same text on their own.
"""
return cls(types.KeyboardButton(text),
resize=resize, single_use=single_use, selective=selective)
@classmethod
def request_location(cls, text, *,
resize=None, single_use=None, selective=None):
"""
Creates a new keyboard button to request the user's location on click.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to share their location with the
bot, and if confirmed a message with geo media will be sent.
"""
return cls(types.KeyboardButtonRequestGeoLocation(text),
resize=resize, single_use=single_use, selective=selective)
@classmethod
def request_phone(cls, text, *,
resize=None, single_use=None, selective=None):
"""
Creates a new keyboard button to request the user's phone on click.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to share their phone with the
bot, and if confirmed a message with contact media will be sent.
"""
return cls(types.KeyboardButtonRequestPhone(text),
resize=resize, single_use=single_use, selective=selective)
@classmethod
def request_poll(cls, text, *, force_quiz=False,
resize=None, single_use=None, selective=None):
"""
Creates a new keyboard button to request the user to create a poll.
If `force_quiz` is `False`, the user will be allowed to choose whether
they want their poll to be a quiz or not. Otherwise, the user will be
forced to create a quiz when creating the poll.
If a poll is a quiz, there will be only one answer that is valid, and
the votes cannot be retracted. Otherwise, users can vote and retract
the vote, and the pol might be multiple choice.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
When the user clicks this button, a screen letting the user create a
poll will be shown, and if they do create one, the poll will be sent.
"""
return cls(types.KeyboardButtonRequestPoll(text, quiz=force_quiz),
resize=resize, single_use=single_use, selective=selective)
@staticmethod
def clear(selective=None):
"""
Clears all keyboard buttons after sending a message with this markup.
When used, no other button should be present or it will be ignored.
``selective`` is as documented in `text`.
"""
return types.ReplyKeyboardHide(selective=selective)
@staticmethod
def force_reply(single_use=None, selective=None, placeholder=None):
"""
Forces a reply to the message with this markup. If used,
no other button should be present or it will be ignored.
``single_use`` and ``selective`` are as documented in `text`.
Args:
placeholder (str):
text to show the user at typing place of message.
If the placeholder is too long, Telegram applications will
crop the text (for example, to 64 characters and adding an
ellipsis (…) character as the 65th).
"""
return types.ReplyKeyboardForceReply(
single_use=single_use,
selective=selective,
placeholder=placeholder)
@staticmethod
def buy(text):
"""
Creates a new inline button to buy a product.
This can only be used when sending files of type
:tl:`InputMediaInvoice`, and must be the first button.
If the button is not specified, Telegram will automatically
add the button to the message. See the
`Payments API <https://core.telegram.org/api/payments>`__
documentation for more information.
"""
return types.KeyboardButtonBuy(text)
@staticmethod
def game(text):
"""
Creates a new inline button to start playing a game.
This should be used when sending files of type
:tl:`InputMediaGame`, and must be the first button.
See the
`Games <https://core.telegram.org/api/bots/games>`__
documentation for more information on using games.
"""
return types.KeyboardButtonGame(text)

View File

@@ -0,0 +1,148 @@
import abc
from ... import errors, utils, _tl
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.
"""
def __init__(self, chat_peer=None, *, input_chat=None, chat=None, broadcast=None):
self._chat_peer = chat_peer
self._input_chat = input_chat
self._chat = chat
self._broadcast = broadcast
self._client = None
@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 only need the ID, use `chat_id` instead.
If you need to call a method which needs
this chat, use `input_chat` instead.
If you're using `telethon.events`, use `get_chat()` instead.
"""
return self._chat
async def get_chat(self):
"""
Returns `chat`, but will make an API call to find the
chat unless it's already cached.
If you only need the ID, use `chat_id` instead.
If you need to call a method which needs
this chat, use `get_input_chat()` instead.
"""
# See `get_sender` for information about 'min'.
if (self._chat is None or getattr(self._chat, 'min', None))\
and await self.get_input_chat():
try:
self._chat =\
await self._client.get_entity(self._input_chat)
except ValueError:
await 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
<telethon.tl.custom.sendergetter.SenderGetter.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 and self._client:
try:
self._input_chat = self._client._entity_cache[self._chat_peer]
except KeyError:
pass
return self._input_chat
async 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 and self._client:
try:
# The chat may be recent, look in dialogs
target = self.chat_id
async 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 ``peer_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.
If there is a chat in the object, `chat_id` will *always* be set,
which is why you should use it instead of `chat.id <chat>`.
"""
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.
Returns `None` if there isn't enough information
(e.g. on `events.MessageDeleted <telethon.events.messagedeleted.MessageDeleted>`).
"""
return isinstance(self._chat_peer, _tl.PeerUser) if self._chat_peer else None
@property
def is_group(self):
"""
True if the message was sent on a group or megagroup.
Returns `None` if there isn't enough information
(e.g. on `events.MessageDeleted <telethon.events.messagedeleted.MessageDeleted>`).
"""
# TODO Cache could tell us more in the future
if self._broadcast is None and hasattr(self.chat, 'broadcast'):
self._broadcast = bool(self.chat.broadcast)
if isinstance(self._chat_peer, _tl.PeerChannel):
if self._broadcast is None:
return None
else:
return not self._broadcast
return isinstance(self._chat_peer, _tl.PeerChat)
@property
def is_channel(self):
"""`True` if the message was sent on a megagroup or channel."""
# The only case where chat peer could be none is in MessageDeleted,
# however those always have the peer in channels.
return isinstance(self._chat_peer, _tl.PeerChannel)
async def _refetch_chat(self):
"""
Re-fetches chat information through other means.
"""

View File

@@ -0,0 +1,161 @@
from . import Draft
from ... import _tl
from ..._misc import utils
class Dialog:
"""
Custom class that encapsulates a dialog (an open "conversation" with
someone, a group or a channel) providing an abstraction to easily
access the input version/normal entity/message etc. The library will
return instances of this class when calling :meth:`.get_dialogs()`.
Args:
dialog (:tl:`Dialog`):
The original ``Dialog`` instance.
pinned (`bool`):
Whether this dialog is pinned to the top or not.
folder_id (`folder_id`):
The folder ID that this dialog belongs to.
archived (`bool`):
Whether this dialog is archived or not (``folder_id is None``).
message (`Message <telethon.tl.custom.message.Message>`):
The last message sent on this dialog. Note that this member
will not be updated when new messages arrive, it's only set
on creation of the instance.
date (`datetime`):
The date of the last message sent on this dialog.
entity (`entity`):
The entity that belongs to this dialog (user, chat or channel).
input_entity (:tl:`InputPeer`):
Input version of the entity.
id (`int`):
The marked ID of the entity, which is guaranteed to be unique.
name (`str`):
Display name for this dialog. For chats and channels this is
their title, and for users it's "First-Name Last-Name".
title (`str`):
Alias for `name`.
unread_count (`int`):
How many messages are currently unread in this dialog. Note that
this value won't update when new messages arrive.
unread_mentions_count (`int`):
How many mentions are currently unread in this dialog. Note that
this value won't update when new messages arrive.
draft (`Draft <telethon.tl.custom.draft.Draft>`):
The draft object in this dialog. It will not be `None`,
so you can call ``draft.set_message(...)``.
is_user (`bool`):
`True` if the `entity` is a :tl:`User`.
is_group (`bool`):
`True` if the `entity` is a :tl:`Chat`
or a :tl:`Channel` megagroup.
is_channel (`bool`):
`True` if the `entity` is a :tl:`Channel`.
"""
def __init__(self, client, dialog, entities, message):
# Both entities and messages being dicts {ID: item}
self._client = client
self.dialog = dialog
self.pinned = bool(dialog.pinned)
self.folder_id = dialog.folder_id
self.archived = dialog.folder_id is not None
self.message = message
self.date = getattr(self.message, 'date', None)
self.entity = entities[utils.get_peer_id(dialog.peer)]
self.input_entity = utils.get_input_peer(self.entity)
self.id = utils.get_peer_id(self.entity) # ^ May be InputPeerSelf()
self.name = self.title = utils.get_display_name(self.entity)
self.unread_count = dialog.unread_count
self.unread_mentions_count = dialog.unread_mentions_count
self.draft = Draft(client, self.entity, self.dialog.draft)
self.is_user = isinstance(self.entity, _tl.User)
self.is_group = (
isinstance(self.entity, (_tl.Chat, _tl.ChatForbidden)) or
(isinstance(self.entity, _tl.Channel) and self.entity.megagroup)
)
self.is_channel = isinstance(self.entity, _tl.Channel)
async def send_message(self, *args, **kwargs):
"""
Sends a message to this dialog. This is just a wrapper around
``client.send_message(dialog.input_entity, *args, **kwargs)``.
"""
return await self._client.send_message(
self.input_entity, *args, **kwargs)
async def delete(self, revoke=False):
"""
Deletes the dialog from your dialog list. If you own the
channel this won't destroy it, only delete it from the list.
Shorthand for `telethon.client.dialogs.DialogMethods.delete_dialog`
with ``entity`` already set.
"""
# Pass the entire entity so the method can determine whether
# the `Chat` is deactivated (in which case we don't kick ourselves,
# or it would raise `PEER_ID_INVALID`).
await self._client.delete_dialog(self.entity, revoke=revoke)
async def archive(self, folder=1):
"""
Archives (or un-archives) this dialog.
Args:
folder (`int`, optional):
The folder to which the dialog should be archived to.
If you want to "un-archive" it, use ``folder=0``.
Returns:
The :tl:`Updates` object that the request produces.
Example:
.. code-block:: python
# Archiving
dialog.archive()
# Un-archiving
dialog.archive(0)
"""
return await self._client(_tl.fn.folders.EditPeerFolders([
_tl.InputFolderPeer(self.input_entity, folder_id=folder)
]))
def to_dict(self):
return {
'_': 'Dialog',
'name': self.name,
'date': self.date,
'draft': self.draft,
'message': self.message,
'entity': self.entity,
}
def __str__(self):
return _tl.TLObject.pretty_format(self.to_dict())
def stringify(self):
return _tl.TLObject.pretty_format(self.to_dict(), indent=0)

View File

@@ -0,0 +1,188 @@
import datetime
from ... import _tl
from ...errors import RPCError
from ..._misc import markdown
from ..._misc.utils import get_input_peer, get_peer
class Draft:
"""
Custom class that encapsulates a draft on the Telegram servers, providing
an abstraction to change the message conveniently. The library will return
instances of this class when calling :meth:`get_drafts()`.
Args:
date (`datetime`):
The date of the draft.
link_preview (`bool`):
Whether the link preview is enabled or not.
reply_to_msg_id (`int`):
The message ID that the draft will reply to.
"""
def __init__(self, client, entity, draft):
self._client = client
self._peer = get_peer(entity)
self._entity = entity
self._input_entity = get_input_peer(entity) if entity else None
if not draft or not isinstance(draft, _tl.DraftMessage):
draft = _tl.DraftMessage('', None, None, None, None)
self._text = markdown.unparse(draft.message, draft.entities)
self._raw_text = draft.message
self.date = draft.date
self.link_preview = not draft.no_webpage
self.reply_to_msg_id = draft.reply_to_msg_id
@property
def entity(self):
"""
The entity that belongs to this dialog (user, chat or channel).
"""
return self._entity
@property
def input_entity(self):
"""
Input version of the entity.
"""
if not self._input_entity:
try:
self._input_entity = self._client._entity_cache[self._peer]
except KeyError:
pass
return self._input_entity
async def get_entity(self):
"""
Returns `entity` but will make an API call if necessary.
"""
if not self.entity and await self.get_input_entity():
try:
self._entity =\
await self._client.get_entity(self._input_entity)
except ValueError:
pass
return self._entity
async def get_input_entity(self):
"""
Returns `input_entity` but will make an API call if necessary.
"""
# We don't actually have an API call we can make yet
# to get more info, but keep this method for consistency.
return self.input_entity
@property
def text(self):
"""
The markdown text contained in the draft. It will be
empty if there is no text (and hence no draft is set).
"""
return self._text
@property
def raw_text(self):
"""
The raw (text without formatting) contained in the draft.
It will be empty if there is no text (thus draft not set).
"""
return self._raw_text
@property
def is_empty(self):
"""
Convenience bool to determine if the draft is empty or not.
"""
return not self._text
async def set_message(
self, text=None, reply_to=0, parse_mode=(),
link_preview=None):
"""
Changes the draft message on the Telegram servers. The changes are
reflected in this object.
:param str text: New text of the draft.
Preserved if left as None.
:param int reply_to: Message ID to reply to.
Preserved if left as 0, erased if set to None.
:param bool link_preview: Whether to attach a web page preview.
Preserved if left as None.
:param str parse_mode: The parse mode to be used for the text.
:return bool: `True` on success.
"""
if text is None:
text = self._text
if reply_to == 0:
reply_to = self.reply_to_msg_id
if link_preview is None:
link_preview = self.link_preview
raw_text, entities =\
await self._client._parse_message_text(text, parse_mode)
result = await self._client(_tl.fn.SaveDraftRequest(
peer=self._peer,
message=raw_text,
no_webpage=not link_preview,
reply_to_msg_id=reply_to,
entities=entities
))
if result:
self._text = text
self._raw_text = raw_text
self.link_preview = link_preview
self.reply_to_msg_id = reply_to
self.date = datetime.datetime.now(tz=datetime.timezone.utc)
return result
async def send(self, clear=True, parse_mode=()):
"""
Sends the contents of this draft to the dialog. This is just a
wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``.
"""
await self._client.send_message(
self._peer, self.text, reply_to=self.reply_to_msg_id,
link_preview=self.link_preview, parse_mode=parse_mode,
clear_draft=clear
)
async def delete(self):
"""
Deletes this draft, and returns `True` on success.
"""
return await self.set_message(text='')
def to_dict(self):
try:
entity = self.entity
except RPCError as e:
entity = e
return {
'_': 'Draft',
'text': self.text,
'entity': entity,
'date': self.date,
'link_preview': self.link_preview,
'reply_to_msg_id': self.reply_to_msg_id
}
def __str__(self):
return _tl.TLObject.pretty_format(self.to_dict())
def stringify(self):
return _tl.TLObject.pretty_format(self.to_dict(), indent=0)

View File

@@ -0,0 +1,141 @@
import mimetypes
import os
from ..._misc import utils
from ... import _tl
class File:
"""
Convenience class over media like photos or documents, which
supports accessing the attributes in a more convenient way.
If any of the attributes are not present in the current media,
the properties will be `None`.
The original media is available through the ``media`` attribute.
"""
def __init__(self, media):
self.media = media
@property
def id(self):
"""
The bot-API style ``file_id`` representing this file.
.. note::
This file ID may not work under user accounts,
but should still be usable by bot accounts.
You can, however, still use it to identify
a file in for example a database.
"""
return utils.pack_bot_file_id(self.media)
@property
def name(self):
"""
The file name of this document.
"""
return self._from_attr(_tl.DocumentAttributeFilename, 'file_name')
@property
def ext(self):
"""
The extension from the mime type of this file.
If the mime type is unknown, the extension
from the file name (if any) will be used.
"""
return (
mime_tl.guess_extension(self.mime_type)
or os.path.splitext(self.name or '')[-1]
or None
)
@property
def mime_type(self):
"""
The mime-type of this file.
"""
if isinstance(self.media, _tl.Photo):
return 'image/jpeg'
elif isinstance(self.media, _tl.Document):
return self.media.mime_type
@property
def width(self):
"""
The width in pixels of this media if it's a photo or a video.
"""
if isinstance(self.media, _tl.Photo):
return max(getattr(s, 'w', 0) for s in self.media.sizes)
return self._from_attr((
_tl.DocumentAttributeImageSize, _tl.DocumentAttributeVideo), 'w')
@property
def height(self):
"""
The height in pixels of this media if it's a photo or a video.
"""
if isinstance(self.media, _tl.Photo):
return max(getattr(s, 'h', 0) for s in self.media.sizes)
return self._from_attr((
_tl.DocumentAttributeImageSize, _tl.DocumentAttributeVideo), 'h')
@property
def duration(self):
"""
The duration in seconds of the audio or video.
"""
return self._from_attr((
_tl.DocumentAttributeAudio, _tl.DocumentAttributeVideo), 'duration')
@property
def title(self):
"""
The title of the song.
"""
return self._from_attr(_tl.DocumentAttributeAudio, 'title')
@property
def performer(self):
"""
The performer of the song.
"""
return self._from_attr(_tl.DocumentAttributeAudio, 'performer')
@property
def emoji(self):
"""
A string with all emoji that represent the current sticker.
"""
return self._from_attr(_tl.DocumentAttributeSticker, 'alt')
@property
def sticker_set(self):
"""
The :tl:`InputStickerSet` to which the sticker file belongs.
"""
return self._from_attr(_tl.DocumentAttributeSticker, 'stickerset')
@property
def size(self):
"""
The size in bytes of this file.
For photos, this is the heaviest thumbnail, as it often repressents the largest dimensions.
"""
if isinstance(self.media, _tl.Photo):
return max(filter(None, map(utils._photo_size_byte_count, self.media.sizes)), default=None)
elif isinstance(self.media, _tl.Document):
return self.media.size
def _from_attr(self, cls, field):
if isinstance(self.media, _tl.Document):
for attr in self.media.attributes:
if isinstance(attr, cls):
return getattr(attr, field, None)

View File

@@ -0,0 +1,50 @@
from .chatgetter import ChatGetter
from .sendergetter import SenderGetter
from ..._misc import utils, helpers
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`):
The original :tl:`MessageFwdHeader` instance.
Any other attribute:
Attributes not described here are the same as those available
in the original :tl:`MessageFwdHeader`.
"""
def __init__(self, client, original, entities):
# Copy all the fields, not reference! It would cause memory cycles:
# self.original_fwd.original_fwd.original_fwd.original_fwd
# ...would be valid if we referenced.
self.__dict__.update(original.__dict__)
self.original_fwd = original
sender_id = sender = input_sender = peer = chat = input_chat = None
if original.from_id:
ty = helpers._entity_type(original.from_id)
if ty == helpers._EntityType.USER:
sender_id = utils.get_peer_id(original.from_id)
sender, input_sender = utils._get_entity_pair(
sender_id, entities, client._entity_cache)
elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL):
peer = original.from_id
chat, input_chat = utils._get_entity_pair(
utils.get_peer_id(peer), entities, client._entity_cache)
# This call resets the client
ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat)
SenderGetter.__init__(self, sender_id, sender=sender, input_sender=input_sender)
self._client = client
# TODO We could reload the message

View File

@@ -0,0 +1,450 @@
import hashlib
from ... import _tl
from ..._misc import utils
_TYPE_TO_MIMES = {
'gif': ['image/gif'], # 'video/mp4' too, but that's used for video
'article': ['text/html'],
'audio': ['audio/mpeg'],
'contact': [],
'file': ['application/pdf', 'application/zip'], # actually any
'geo': [],
'photo': ['image/jpeg'],
'sticker': ['image/webp', 'application/x-tgsticker'],
'venue': [],
'video': ['video/mp4'], # tdlib includes text/html for some reason
'voice': ['audio/ogg'],
}
class InlineBuilder:
"""
Helper class to allow defining `InlineQuery
<telethon.events.inlinequery.InlineQuery>` ``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
created :tl:`InputBotInlineResult` with empty ID to ``bytes()``,
so that the ID will be deterministic for the same input.
.. note::
If two inputs are exactly the same, their IDs will be the same
too. If you send two articles with the same ID, it will raise
``ResultIdDuplicateError``. Consider giving them an explicit
ID if you need to send two results that are the same.
"""
def __init__(self, client):
self._client = client
# noinspection PyIncorrectDocstring
async def article(
self, title, description=None,
*, url=None, thumb=None, content=None,
id=None, text=None, parse_mode=(), 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.
Example:
.. code-block:: python
results = [
# Option with title and description sending a message.
builder.article(
title='First option',
description='This is the first option',
text='Text sent after clicking this option',
),
# Option with title URL to be opened when clicked.
builder.article(
title='Second option',
url='https://example.com',
text='Text sent if the user clicks the option and not the URL',
),
# Sending a message with buttons.
# You can use a list or a list of lists to include more buttons.
builder.article(
title='Third option',
text='Text sent with buttons below',
buttons=Button.url('https://example.com'),
),
]
"""
# TODO Does 'article' work always?
# article, photo, gif, mpeg4_gif, video, audio,
# voice, document, location, venue, contact, game
result = _tl.InputBotInlineResult(
id=id or '',
type='article',
send_message=await 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
# noinspection PyIncorrectDocstring
async def photo(
self, file, *, id=None, include_media=True,
text=None, parse_mode=(), link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None
):
"""
Creates a new inline result of photo type.
Args:
include_media (`bool`, optional):
Whether the photo file used to display the result should be
included in the message itself or not. By default, the photo
is included, and the text parameter alters the caption.
file (`obj`, optional):
Same as ``file`` for `client.send_file()
<telethon.client.uploads.UploadMethods.send_file>`.
Example:
.. code-block:: python
results = [
# Sending just the photo when the user selects it.
builder.photo('/path/to/photo.jpg'),
# Including a caption with some in-memory photo.
photo_bytesio = ...
builder.photo(
photo_bytesio,
text='This will be the caption of the sent photo',
),
# Sending just the message without including the photo.
builder.photo(
photo,
text='This will be a normal text message',
include_media=False,
),
]
"""
try:
fh = utils.get_input_photo(file)
except TypeError:
_, media, _ = await self._client._file_to_media(
file, allow_cache=True, as_image=True
)
if isinstance(media, _tl.InputPhoto):
fh = media
else:
r = await self._client(_tl.fn.messages.UploadMedia(
_tl.InputPeerSelf(), media=media
))
fh = utils.get_input_photo(r.photo)
result = _tl.InputBotInlineResultPhoto(
id=id or '',
type='photo',
photo=fh,
send_message=await self._message(
text=text or '',
parse_mode=parse_mode,
link_preview=link_preview,
media=include_media,
geo=geo,
period=period,
contact=contact,
game=game,
buttons=buttons
)
)
if id is None:
result.id = hashlib.sha256(bytes(result)).hexdigest()
return result
# noinspection PyIncorrectDocstring
async 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=(), link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None,
include_media=True
):
"""
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: article, audio,
contact, file, geo, gif, photo, sticker, venue, video, voice.
It will be automatically set if ``mime_type`` is specified,
and default to ``'file'`` if no matching mime type is found.
you may need to pass ``attributes`` in order to use ``type``
effectively.
attributes (`list`, optional):
Optional attributes that override the inferred ones, like
:tl:`DocumentAttributeFilename` and so on.
include_media (`bool`, optional):
Whether the document file used to display the result should be
included in the message itself or not. By default, the document
is included, and the text parameter alters the caption.
Example:
.. code-block:: python
results = [
# Sending just the file when the user selects it.
builder.document('/path/to/file.pdf'),
# Including a caption with some in-memory file.
file_bytesio = ...
builder.document(
file_bytesio,
text='This will be the caption of the sent file',
),
# Sending just the message without including the file.
builder.document(
photo,
text='This will be a normal text message',
include_media=False,
),
]
"""
if type is None:
if voice_note:
type = 'voice'
elif mime_type:
for ty, mimes in _TYPE_TO_MIMES.items():
for mime in mimes:
if mime_type == mime:
type = ty
break
if type is None:
type = 'file'
try:
fh = utils.get_input_document(file)
except TypeError:
_, media, _ = await self._client._file_to_media(
file,
mime_type=mime_type,
attributes=attributes,
force_document=force_document,
voice_note=voice_note,
video_note=video_note,
allow_cache=use_cache
)
if isinstance(media, _tl.InputDocument):
fh = media
else:
r = await self._client(_tl.fn.messages.UploadMedia(
_tl.InputPeerSelf(), media=media
))
fh = utils.get_input_document(r.document)
result = _tl.InputBotInlineResultDocument(
id=id or '',
type=type,
document=fh,
send_message=await self._message(
# Empty string for text if there's media but text is None.
# We may want to display a document but send text; however
# default to sending the media (without text, i.e. stickers).
text=text or '',
parse_mode=parse_mode,
link_preview=link_preview,
media=include_media,
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
# noinspection PyIncorrectDocstring
async def game(
self, short_name, *, id=None,
text=None, parse_mode=(), 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 = _tl.InputBotInlineResultGame(
id=id or '',
short_name=short_name,
send_message=await 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
async def _message(
self, *,
text=None, parse_mode=(), link_preview=True, media=False,
geo=None, period=60, contact=None, game=False, buttons=None
):
# Empty strings are valid but false-y; if they're empty use dummy '\0'
args = ('\0' if text == '' else text, geo, contact, game)
if sum(1 for x in args if x is not None and x is not False) != 1:
raise ValueError(
'Must set exactly one of text, geo, contact or game (set {})'
.format(', '.join(x[0] for x in zip(
'text geo contact game'.split(), args) if x[1]) or 'none')
)
markup = self._client.build_reply_markup(buttons, inline_only=True)
if text is not None:
text, msg_entities = await self._client._parse_message_text(
text, parse_mode
)
if media:
# "MediaAuto" means it will use whatever media the inline
# result itself has (stickers, photos, or documents), while
# respecting the user's text (caption) and formatting.
return _tl.InputBotInlineMessageMediaAuto(
message=text,
entities=msg_entities,
reply_markup=markup
)
else:
return _tl.InputBotInlineMessageText(
message=text,
no_webpage=not link_preview,
entities=msg_entities,
reply_markup=markup
)
elif isinstance(geo, (_tl.InputGeoPoint, _tl.GeoPoint)):
return _tl.InputBotInlineMessageMediaGeo(
geo_point=utils.get_input_geo(geo),
period=period,
reply_markup=markup
)
elif isinstance(geo, (_tl.InputMediaVenue, _tl.MessageMediaVenue)):
if isinstance(geo, _tl.InputMediaVenue):
geo_point = geo.geo_point
else:
geo_point = geo.geo
return _tl.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, (
_tl.InputMediaContact, _tl.MessageMediaContact)):
return _tl.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 _tl.InputBotInlineMessageGame(
reply_markup=markup
)
else:
raise ValueError('No text, game or valid geo or contact given')

View File

@@ -0,0 +1,176 @@
from ... import _tl
from ..._misc 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.
"""
# tdlib types are the following (InlineQueriesManager::answer_inline_query @ 1a4a834):
# gif, article, audio, contact, file, geo, photo, sticker, venue, video, voice
#
# However, those documented in https://core.telegram.org/bots/api#inline-mode are different.
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, *, entity=None):
self._client = client
self.result = original
self._query_id = query_id
self._entity = entity
@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
async def click(self, entity=None, reply_to=None, comment_to=None,
silent=False, clear_draft=False, hide_via=False,
background=None):
"""
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` | `Message <telethon.tl.custom.message.Message>`, optional):
If present, the sent message will reply to this ID or message.
comment_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):
Similar to ``reply_to``, but replies in the linked group of a
broadcast channel instead (effectively leaving a "comment to"
the specified message).
silent (`bool`, optional):
Whether the message should notify people with sound or not.
Defaults to `False` (send with a notification sound unless
the person has the chat muted). Set it to `True` to alter
this behaviour.
clear_draft (`bool`, optional):
Whether the draft should be removed after sending the
message from this result or not. Defaults to `False`.
hide_via (`bool`, optional):
Whether the "via @bot" should be hidden or not.
Only works with certain bots (like @bing or @gif).
background (`bool`, optional):
Whether the message should be send in background.
"""
if entity:
entity = await self._client.get_input_entity(entity)
elif self._entity:
entity = self._entity
else:
raise ValueError('You must provide the entity where the result should be sent to')
if comment_to:
entity, reply_id = await self._client._get_comment_data(entity, comment_to)
else:
reply_id = None if reply_to is None else utils.get_message_id(reply_to)
req = _tl.fn.messages.SendInlineBotResult(
peer=entity,
query_id=self._query_id,
id=self.result.id,
silent=silent,
background=background,
clear_draft=clear_draft,
hide_via=hide_via,
reply_to_msg_id=reply_id
)
return self._client._get_response_message(
req, await self._client(req), entity)
async 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 await self._client.download_media(
self.document or self.photo, *args, **kwargs)

View File

@@ -0,0 +1,83 @@
import time
from .inlineresult import InlineResult
class InlineResults(list):
"""
Custom class that encapsulates :tl:`BotResults` providing
an abstraction to easily access some commonly needed features
(such as clicking one of the results to select it)
Note that this is a list of `InlineResult
<telethon.tl.custom.inlineresult.InlineResult>`
so you can iterate over it or use indices to
access its elements. In addition, it has some
attributes.
Attributes:
result (:tl:`BotResults`):
The original :tl:`BotResults` object.
query_id (`int`):
The random ID that identifies this query.
cache_time (`int`):
For how long the results should be considered
valid. You can call `results_valid` at any
moment to determine if the results are still
valid or not.
users (:tl:`User`):
The users present in this inline query.
gallery (`bool`):
Whether these results should be presented
in a grid (as a gallery of images) or not.
next_offset (`str`, optional):
The string to be used as an offset to get
the next chunk of results, if any.
switch_pm (:tl:`InlineBotSwitchPM`, optional):
If presents, the results should show a button to
switch to a private conversation with the bot using
the text in this object.
"""
def __init__(self, client, original, *, entity=None):
super().__init__(InlineResult(client, x, original.query_id, entity=entity)
for x in original.results)
self.result = original
self.query_id = original.query_id
self.cache_time = original.cache_time
self._valid_until = time.time() + self.cache_time
self.users = original.users
self.gallery = bool(original.gallery)
self.next_offset = original.next_offset
self.switch_pm = original.switch_pm
def results_valid(self):
"""
Returns `True` if the cache time has not expired
yet and the results can still be considered valid.
"""
return time.time() < self._valid_until
def _to_str(self, item_function):
return ('[{}, query_id={}, cache_time={}, users={}, gallery={}, '
'next_offset={}, switch_pm={}]'.format(
', '.join(item_function(x) for x in self),
self.query_id,
self.cache_time,
self.users,
self.gallery,
self.next_offset,
self.switch_pm
))
def __str__(self):
return self._to_str(str)
def __repr__(self):
return self._to_str(repr)

View File

@@ -0,0 +1,9 @@
from ... import _tl
class InputSizedFile(_tl.InputFile):
"""InputFile class with two extra parameters: md5 (digest) and size"""
def __init__(self, id_, parts, name, md5, size):
super().__init__(id_, parts, name, md5.hexdigest())
self.md5 = md5.digest()
self.size = size

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
from ..._misc import password as pwd_mod
from ... import _tl
from ...errors import BotResponseTimeoutError
import webbrowser
import os
class MessageButton:
"""
.. note::
`Message.buttons <telethon.tl.custom.message.Message.buttons>`
are instances of this type. If you want to **define** a reply
markup for e.g. sending messages, refer to `Button
<telethon.tl.custom.button.Button>` instead.
Custom class that encapsulates a message button providing
an abstraction to easily access some commonly needed features
(such as clicking the button itself).
Attributes:
button (:tl:`KeyboardButton`):
The original :tl:`KeyboardButton` object.
"""
def __init__(self, client, original, chat, bot, msg_id):
self.button = original
self._bot = bot
self._chat = chat
self._msg_id = msg_id
self._client = client
@property
def client(self):
"""
Returns the `telethon.client.telegramclient.TelegramClient`
instance that created this instance.
"""
return self._client
@property
def text(self):
"""The text string of the button."""
return self.button.text
@property
def data(self):
"""The `bytes` data for :tl:`KeyboardButtonCallback` objects."""
if isinstance(self.button, _tl.KeyboardButtonCallback):
return self.button.data
@property
def inline_query(self):
"""The query `str` for :tl:`KeyboardButtonSwitchInline` objects."""
if isinstance(self.button, _tl.KeyboardButtonSwitchInline):
return self.button.query
@property
def url(self):
"""The url `str` for :tl:`KeyboardButtonUrl` objects."""
if isinstance(self.button, _tl.KeyboardButtonUrl):
return self.button.url
async def click(self, share_phone=None, share_geo=None, *, password=None):
"""
Emulates the behaviour of clicking this button.
If it's a normal :tl:`KeyboardButton` with text, a message will be
sent, and the sent `Message <telethon.tl.custom.message.Message>` returned.
If it's an inline :tl:`KeyboardButtonCallback` with text and data,
it will be "clicked" and the :tl:`BotCallbackAnswer` returned.
If it's an inline :tl:`KeyboardButtonSwitchInline` button, the
:tl:`StartBotRequest` will be invoked and the resulting updates
returned.
If it's a :tl:`KeyboardButtonUrl`, the URL of the button will
be passed to ``webbrowser.open`` and return `True` on success.
If it's a :tl:`KeyboardButtonRequestPhone`, you must indicate that you
want to ``share_phone=True`` in order to share it. Sharing it is not a
default because it is a privacy concern and could happen accidentally.
You may also use ``share_phone=phone`` to share a specific number, in
which case either `str` or :tl:`InputMediaContact` should be used.
If it's a :tl:`KeyboardButtonRequestGeoLocation`, you must pass a
tuple in ``share_geo=(longitude, latitude)``. Note that Telegram seems
to have some heuristics to determine impossible locations, so changing
this value a lot quickly may not work as expected. You may also pass a
:tl:`InputGeoPoint` if you find the order confusing.
"""
if isinstance(self.button, _tl.KeyboardButton):
return await self._client.send_message(
self._chat, self.button.text, parse_mode=None)
elif isinstance(self.button, _tl.KeyboardButtonCallback):
if password is not None:
pwd = await self._client(_tl.fn.account.GetPassword())
password = pwd_mod.compute_check(pwd, password)
req = _tl.fn.messages.GetBotCallbackAnswer(
peer=self._chat, msg_id=self._msg_id, data=self.button.data,
password=password
)
try:
return await self._client(req)
except BotResponseTimeoutError:
return None
elif isinstance(self.button, _tl.KeyboardButtonSwitchInline):
return await self._client(_tl.fn.messages.StartBot(
bot=self._bot, peer=self._chat, start_param=self.button.query
))
elif isinstance(self.button, _tl.KeyboardButtonUrl):
return webbrowser.open(self.button.url)
elif isinstance(self.button, _tl.KeyboardButtonGame):
req = _tl.fn.messages.GetBotCallbackAnswer(
peer=self._chat, msg_id=self._msg_id, game=True
)
try:
return await self._client(req)
except BotResponseTimeoutError:
return None
elif isinstance(self.button, _tl.KeyboardButtonRequestPhone):
if not share_phone:
raise ValueError('cannot click on phone buttons unless share_phone=True')
if share_phone == True or isinstance(share_phone, str):
me = await self._client.get_me()
share_phone = _tl.InputMediaContact(
phone_number=me.phone if share_phone == True else share_phone,
first_name=me.first_name or '',
last_name=me.last_name or '',
vcard=''
)
return await self._client.send_file(self._chat, share_phone)
elif isinstance(self.button, _tl.KeyboardButtonRequestGeoLocation):
if not share_geo:
raise ValueError('cannot click on geo buttons unless share_geo=(longitude, latitude)')
if isinstance(share_geo, (tuple, list)):
long, lat = share_geo
share_geo = _tl.InputMediaGeoPoint(_tl.InputGeoPoint(lat=lat, long=long))
return await self._client.send_file(self._chat, share_geo)

View File

@@ -0,0 +1,138 @@
from ... import _tl
def _admin_prop(field_name, doc):
"""
Helper method to build properties that return `True` if the user is an
administrator of a normal chat, or otherwise return `True` if the user
has a specific permission being an admin of a channel.
"""
def fget(self):
if not self.is_admin:
return False
if self.is_chat:
return True
return getattr(self.participant.admin_rights, field_name)
return {'fget': fget, 'doc': doc}
class ParticipantPermissions:
"""
Participant permissions information.
The properties in this objects are boolean values indicating whether the
user has the permission or not.
Example
.. code-block:: python
permissions = ...
if permissions.is_banned:
"this user is banned"
elif permissions.is_admin:
"this user is an administrator"
"""
def __init__(self, participant, chat: bool):
self.participant = participant
self.is_chat = chat
@property
def is_admin(self):
"""
Whether the user is an administrator of the chat or not. The creator
also counts as begin an administrator, since they have all permissions.
"""
return self.is_creator or isinstance(self.participant, (
types.ChannelParticipantAdmin,
types.ChatParticipantAdmin
))
@property
def is_creator(self):
"""
Whether the user is the creator of the chat or not.
"""
return isinstance(self.participant, (
types.ChannelParticipantCreator,
types.ChatParticipantCreator
))
@property
def has_default_permissions(self):
"""
Whether the user is a normal user of the chat (not administrator, but
not banned either, and has no restrictions applied).
"""
return isinstance(self.participant, (
types.ChannelParticipant,
types.ChatParticipant,
types.ChannelParticipantSelf
))
@property
def is_banned(self):
"""
Whether the user is banned in the chat.
"""
return isinstance(self.participant, types.ChannelParticipantBanned)
@property
def has_left(self):
"""
Whether the user left the chat.
"""
return isinstance(self.participant, types.ChannelParticipantLeft)
@property
def add_admins(self):
"""
Whether the administrator can add new administrators with the same or
less permissions than them.
"""
if not self.is_admin:
return False
if self.is_chat:
return self.is_creator
return self.participant.admin_rights.add_admins
ban_users = property(**_admin_prop('ban_users', """
Whether the administrator can ban other users or not.
"""))
pin_messages = property(**_admin_prop('pin_messages', """
Whether the administrator can pin messages or not.
"""))
invite_users = property(**_admin_prop('invite_users', """
Whether the administrator can add new users to the chat.
"""))
delete_messages = property(**_admin_prop('delete_messages', """
Whether the administrator can delete messages from other participants.
"""))
edit_messages = property(**_admin_prop('edit_messages', """
Whether the administrator can edit messages.
"""))
post_messages = property(**_admin_prop('post_messages', """
Whether the administrator can post messages in the broadcast channel.
"""))
change_info = property(**_admin_prop('change_info', """
Whether the administrator can change the information about the chat,
such as title or description.
"""))
anonymous = property(**_admin_prop('anonymous', """
Whether the administrator will remain anonymous when sending messages.
"""))
manage_call = property(**_admin_prop('manage_call', """
Whether the user will be able to manage group calls.
"""))

View File

@@ -0,0 +1,118 @@
import asyncio
import base64
import datetime
from ... import events, _tl
class QRLogin:
"""
QR login information.
Most of the time, you will present the `url` as a QR code to the user,
and while it's being shown, call `wait`.
"""
def __init__(self, client, ignored_ids):
self._client = client
self._request = _tl.fn.auth.ExportLoginToken(
self._client.api_id, self._client.api_hash, ignored_ids)
self._resp = None
async def recreate(self):
"""
Generates a new token and URL for a new QR code, useful if the code
has expired before it was imported.
"""
self._resp = await self._client(self._request)
@property
def token(self) -> bytes:
"""
The binary data representing the token.
It can be used by a previously-authorized client in a call to
:tl:`auth.importLoginToken` to log the client that originally
requested the QR login.
"""
return self._resp.token
@property
def url(self) -> str:
"""
The ``tg://login`` URI with the token. When opened by a Telegram
application where the user is logged in, it will import the login
token.
If you want to display a QR code to the user, this is the URL that
should be launched when the QR code is scanned (the URL that should
be contained in the QR code image you generate).
Whether you generate the QR code image or not is up to you, and the
library can't do this for you due to the vast ways of generating and
displaying the QR code that exist.
The URL simply consists of `token` base64-encoded.
"""
return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._resp.token).decode('utf-8').rstrip('='))
@property
def expires(self) -> datetime.datetime:
"""
The `datetime` at which the QR code will expire.
If you want to try again, you will need to call `recreate`.
"""
return self._resp.expires
async def wait(self, timeout: float = None):
"""
Waits for the token to be imported by a previously-authorized client,
either by scanning the QR, launching the URL directly, or calling the
import method.
This method **must** be called before the QR code is scanned, and
must be executing while the QR code is being scanned. Otherwise, the
login will not complete.
Will raise `asyncio.TimeoutError` if the login doesn't complete on
time.
Arguments
timeout (float):
The timeout, in seconds, to wait before giving up. By default
the library will wait until the token expires, which is often
what you want.
Returns
On success, an instance of :tl:`User`. On failure it will raise.
"""
if timeout is None:
timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds()
event = asyncio.Event()
async def handler(_update):
event.set()
self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken))
try:
# Will raise timeout error if it doesn't complete quick enough,
# which we want to let propagate
await asyncio.wait_for(event.wait(), timeout=timeout)
finally:
self._client.remove_event_handler(handler)
# We got here without it raising timeout error, so we can proceed
resp = await self._client(self._request)
if isinstance(resp, types.auth.LoginTokenMigrateTo):
await self._client._switch_dc(resp.dc_id)
resp = await self._client(_tl.fn.auth.ImportLoginToken(resp.token))
# resp should now be auth.loginTokenSuccess
if isinstance(resp, types.auth.LoginTokenSuccess):
user = resp.authorization.user
self._client._on_login(user)
return user
raise TypeError('Login token response was unexpected: {}'.format(resp))

View File

@@ -0,0 +1,97 @@
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.
"""
def __init__(self, sender_id=None, *, sender=None, input_sender=None):
self._sender_id = sender_id
self._sender = sender
self._input_sender = input_sender
self._client = None
@property
def sender(self):
"""
Returns the :tl:`User` or :tl:`Channel` that sent this object.
It may be `None` if Telegram didn't send the sender.
If you only need the ID, use `sender_id` instead.
If you need to call a method which needs
this chat, use `input_sender` instead.
If you're using `telethon.events`, use `get_sender()` instead.
"""
return self._sender
async def get_sender(self):
"""
Returns `sender`, but will make an API call to find the
sender unless it's already cached.
If you only need the ID, use `sender_id` instead.
If you need to call a method which needs
this sender, use `get_input_sender()` instead.
"""
# ``sender.min`` is present both in :tl:`User` and :tl:`Channel`.
# It's a flag that will be set if only minimal information is
# available (such as display name, but username may be missing),
# in which case we want to force fetch the entire thing because
# the user explicitly called a method. If the user is okay with
# cached information, they may use the property instead.
if (self._sender is None or getattr(self._sender, 'min', None)) \
and await self.get_input_sender():
try:
self._sender =\
await self._client.get_entity(self._input_sender)
except ValueError:
await self._refetch_sender()
return self._sender
@property
def input_sender(self):
"""
This :tl:`InputPeer` is the input version of the user/channel who
sent the message. Similarly to `input_chat
<telethon.tl.custom.chatgetter.ChatGetter.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 and self._client:
try:
self._input_sender = \
self._client._entity_cache[self._sender_id]
except KeyError:
pass
return self._input_sender
async 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 and self._client:
await self._refetch_sender()
return self._input_sender
@property
def sender_id(self):
"""
Returns the marked sender integer ID, if present.
If there is a sender in the object, `sender_id` will *always* be set,
which is why you should use it instead of `sender.id <sender>`.
"""
return self._sender_id
async def _refetch_sender(self):
"""
Re-fetches sender information through other means.
"""

222
telethon/types/tlobject.py Normal file
View File

@@ -0,0 +1,222 @@
import base64
import json
import struct
from datetime import datetime, date, timedelta, timezone
import time
_EPOCH_NAIVE = datetime(*time.gmtime(0)[:6])
_EPOCH_NAIVE_LOCAL = datetime(*time.localtime(0)[:6])
_EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc)
def _datetime_to_timestamp(dt):
# If no timezone is specified, it is assumed to be in utc zone
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
# We use .total_seconds() method instead of simply dt.timestamp(),
# because on Windows the latter raises OSError on datetimes ~< datetime(1970,1,1)
secs = int((dt - _EPOCH).total_seconds())
# Make sure it's a valid signed 32 bit integer, as used by Telegram.
# This does make very large dates wrap around, but it's the best we
# can do with Telegram's limitations.
return struct.unpack('i', struct.pack('I', secs & 0xffffffff))[0]
def _json_default(value):
if isinstance(value, bytes):
return base64.b64encode(value).decode('ascii')
elif isinstance(value, datetime):
return value.isoformat()
else:
return repr(value)
class TLObject:
CONSTRUCTOR_ID = None
SUBCLASS_OF_ID = None
@staticmethod
def pretty_format(obj, indent=None):
"""
Pretty formats the given object as a string which is returned.
If indent is None, a single line will be returned.
"""
if indent is None:
if isinstance(obj, TLObject):
obj = obj.to_dict()
if isinstance(obj, dict):
return '{}({})'.format(obj.get('_', 'dict'), ', '.join(
'{}={}'.format(k, TLObject.pretty_format(v))
for k, v in obj.items() if k != '_'
))
elif isinstance(obj, str) or isinstance(obj, bytes):
return repr(obj)
elif hasattr(obj, '__iter__'):
return '[{}]'.format(
', '.join(TLObject.pretty_format(x) for x in obj)
)
else:
return repr(obj)
else:
result = []
if isinstance(obj, TLObject):
obj = obj.to_dict()
if isinstance(obj, dict):
result.append(obj.get('_', 'dict'))
result.append('(')
if obj:
result.append('\n')
indent += 1
for k, v in obj.items():
if k == '_':
continue
result.append('\t' * indent)
result.append(k)
result.append('=')
result.append(TLObject.pretty_format(v, indent))
result.append(',\n')
result.pop() # last ',\n'
indent -= 1
result.append('\n')
result.append('\t' * indent)
result.append(')')
elif isinstance(obj, str) or isinstance(obj, bytes):
result.append(repr(obj))
elif hasattr(obj, '__iter__'):
result.append('[\n')
indent += 1
for x in obj:
result.append('\t' * indent)
result.append(TLObject.pretty_format(x, indent))
result.append(',\n')
indent -= 1
result.append('\t' * indent)
result.append(']')
else:
result.append(repr(obj))
return ''.join(result)
@staticmethod
def serialize_bytes(data):
"""Write bytes by using Telegram guidelines"""
if not isinstance(data, bytes):
if isinstance(data, str):
data = data.encode('utf-8')
else:
raise TypeError(
'bytes or str expected, not {}'.format(type(data)))
r = []
if len(data) < 254:
padding = (len(data) + 1) % 4
if padding != 0:
padding = 4 - padding
r.append(bytes([len(data)]))
r.append(data)
else:
padding = len(data) % 4
if padding != 0:
padding = 4 - padding
r.append(bytes([
254,
len(data) % 256,
(len(data) >> 8) % 256,
(len(data) >> 16) % 256
]))
r.append(data)
r.append(bytes(padding))
return b''.join(r)
@staticmethod
def serialize_datetime(dt):
if not dt and not isinstance(dt, timedelta):
return b'\0\0\0\0'
if isinstance(dt, datetime):
dt = _datetime_to_timestamp(dt)
elif isinstance(dt, date):
dt = _datetime_to_timestamp(datetime(dt.year, dt.month, dt.day))
elif isinstance(dt, float):
dt = int(dt)
elif isinstance(dt, timedelta):
# Timezones are tricky. datetime.utcnow() + ... timestamp() works
dt = _datetime_to_timestamp(datetime.utcnow() + dt)
if isinstance(dt, int):
return struct.pack('<i', dt)
raise TypeError('Cannot interpret "{}" as a date.'.format(dt))
def __eq__(self, o):
return isinstance(o, type(self)) and self.to_dict() == o.to_dict()
def __ne__(self, o):
return not isinstance(o, type(self)) or self.to_dict() != o.to_dict()
def __str__(self):
return TLObject.pretty_format(self)
def stringify(self):
return TLObject.pretty_format(self, indent=0)
def to_dict(self):
raise NotImplementedError
def to_json(self, fp=None, default=_json_default, **kwargs):
"""
Represent the current `TLObject` as JSON.
If ``fp`` is given, the JSON will be dumped to said
file pointer, otherwise a JSON string will be returned.
Note that bytes and datetimes cannot be represented
in JSON, so if those are found, they will be base64
encoded and ISO-formatted, respectively, by default.
"""
d = self.to_dict()
if fp:
return json.dump(d, fp, default=default, **kwargs)
else:
return json.dumps(d, default=default, **kwargs)
def __bytes__(self):
try:
return self._bytes()
except AttributeError:
# If a type is wrong (e.g. expected `TLObject` but `int` was
# provided) it will try to access `._bytes()`, which will fail
# with `AttributeError`. This occurs in fact because the type
# was wrong, so raise the correct error type.
raise TypeError('a TLObject was expected but found something else')
# Custom objects will call `(...)._bytes()` and not `bytes(...)` so that
# if the wrong type is used (e.g. `int`) we won't try allocating a huge
# amount of data, which would cause a `MemoryError`.
def _bytes(self):
raise NotImplementedError
@classmethod
def from_reader(cls, reader):
raise NotImplementedError
class TLRequest(TLObject):
"""
Represents a content-related `TLObject` (a request that can be sent).
"""
@staticmethod
def read_result(reader):
return reader.tgread_object()
async def resolve(self, client, utils):
pass