mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-09 13:29:47 +00:00
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:
26
telethon/types/_core/__init__.py
Normal file
26
telethon/types/_core/__init__.py
Normal 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
|
||||
)}
|
45
telethon/types/_core/gzippacked.py
Normal file
45
telethon/types/_core/gzippacked.py
Normal 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
|
||||
}
|
47
telethon/types/_core/messagecontainer.py
Normal file
47
telethon/types/_core/messagecontainer.py
Normal 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)
|
34
telethon/types/_core/rpcresult.py
Normal file
34
telethon/types/_core/rpcresult.py
Normal 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
|
||||
}
|
34
telethon/types/_core/tlmessage.py
Normal file
34
telethon/types/_core/tlmessage.py
Normal 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
|
||||
}
|
13
telethon/types/_custom/__init__.py
Normal file
13
telethon/types/_custom/__init__.py
Normal 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
|
475
telethon/types/_custom/adminlogevent.py
Normal file
475
telethon/types/_custom/adminlogevent.py
Normal 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()
|
308
telethon/types/_custom/button.py
Normal file
308
telethon/types/_custom/button.py
Normal 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)
|
148
telethon/types/_custom/chatgetter.py
Normal file
148
telethon/types/_custom/chatgetter.py
Normal 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.
|
||||
"""
|
161
telethon/types/_custom/dialog.py
Normal file
161
telethon/types/_custom/dialog.py
Normal 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)
|
188
telethon/types/_custom/draft.py
Normal file
188
telethon/types/_custom/draft.py
Normal 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)
|
141
telethon/types/_custom/file.py
Normal file
141
telethon/types/_custom/file.py
Normal 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)
|
50
telethon/types/_custom/forward.py
Normal file
50
telethon/types/_custom/forward.py
Normal 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
|
450
telethon/types/_custom/inlinebuilder.py
Normal file
450
telethon/types/_custom/inlinebuilder.py
Normal 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')
|
176
telethon/types/_custom/inlineresult.py
Normal file
176
telethon/types/_custom/inlineresult.py
Normal 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)
|
83
telethon/types/_custom/inlineresults.py
Normal file
83
telethon/types/_custom/inlineresults.py
Normal 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)
|
9
telethon/types/_custom/inputsizedfile.py
Normal file
9
telethon/types/_custom/inputsizedfile.py
Normal 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
|
1148
telethon/types/_custom/message.py
Normal file
1148
telethon/types/_custom/message.py
Normal file
File diff suppressed because it is too large
Load Diff
146
telethon/types/_custom/messagebutton.py
Normal file
146
telethon/types/_custom/messagebutton.py
Normal 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)
|
138
telethon/types/_custom/participantpermissions.py
Normal file
138
telethon/types/_custom/participantpermissions.py
Normal 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.
|
||||
"""))
|
118
telethon/types/_custom/qrlogin.py
Normal file
118
telethon/types/_custom/qrlogin.py
Normal 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))
|
97
telethon/types/_custom/sendergetter.py
Normal file
97
telethon/types/_custom/sendergetter.py
Normal 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
222
telethon/types/tlobject.py
Normal 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
|
Reference in New Issue
Block a user