Begin major entity overhaul

Introduce User and Chat as better alternatives to raw API.
Document the rationale and new intended usage.
This commit is contained in:
Lonami Exo 2022-02-12 11:29:28 +01:00
parent 2c4ff8803f
commit 9bfe4cddf5
3 changed files with 649 additions and 9 deletions

View File

@ -598,14 +598,14 @@ The supported values are:
If you prefer to avoid hardcoding strings, you may use ``telethon.enums.Participant``.
The size selector for ``client.download_profile_photo`` and ``client.download_media` is now using
The size selector for ``client.download_profile_photo`` and ``client.download_media`` is now using
an enumeration:
```
from telethon import enums
.. code-block:: python
await client.download_profile_photo(user, thumb=enums.Size.ORIGINAL)
```
from telethon import enums
await client.download_profile_photo(user, thumb=enums.Size.ORIGINAL)
This new selection mode is also smart enough to pick the "next best" size if the specified one
is not available. The parameter is known as ``thumb`` and not ``size`` because documents don't
@ -639,7 +639,100 @@ The message sender no longer is the channel when no sender is provided by Telegr
to patch this value for channels to be the same as the chat, but now it will be faithful to
Telegram's value.
// TODO actually provide the things mentioned here
Overhaul of users and chats are no longer raw API types
-------------------------------------------------------
Users and chats are no longer raw API types. The goal is to reduce the amount of raw API exposed
to the user, and to provide less confusing naming. This also means that **the sender and chat of
messages and events is now a different type**. If you were using `isinstance` to check the types,
you will need to update that code. However, if you were accessing things like the ``first_name``
or ``username``, you will be fine.
Raw API is not affected by this change. When using it, the raw :tl:`User`, :tl:`Chat` and
:tl:`Channel` are still returned.
For friendly methods and events, There are now two main entity types, `User` and `Chat`.
`User`\ s are active entities which can send messages and interact with eachother. There is an
account controlling them. `Chat`\ s are passive entities where multiple users can join and
interact with each other. This includes small groups, supergroups, and broadcast channels.
``event.get_sender``, ``event.sender``, ``event.get_chat``, and ``event.chat`` (as well as
the same methods on ``message`` and elsewhere) now return this new type. The ``sender`` and
``chat`` is **now always returned** (where it makes sense, so no sender in channel messages),
even if Telegram did not include information about it in the update. This means you can use
send messages to ``event.chat`` without worrying if Telegram included this information or not,
or even access ``event.chat.id``. This was often a papercut. However, if you need other
information like the title, you might still need to use ``await event.get_chat()``, which is
used to signify an API call might be necessary.
``event.get_input_sender``, ``event.input_sender``, ``message.get_input_sender`` and
``message.input_sender`` (among other variations) have been removed. Instead, a new ``compact``
method has been added to the new `User` and `Chat` types, which can be used to obtain a compact
representation of the sender. The "input" terminology is confusing for end-users, as it's mostly
an implementation detail of friendly methods. Because the return type would've been different
had these methods been kept, one would have had to review code using them regardless.
What this means is that, if you now want a compact way to store a user or chat for later use,
you should use ``compact``:
.. code-block:: python
compacted_user = message.sender.compact()
# store compacted_user in a database or elsewhere for later use
Public methods accept this type as input parameters. This means you can send messages to a
compacted user or chat, for example.
``event.is_private``, ``event.is_group`` and ``event.is_channel`` have **been removed** (among
other variations, such as in ``message``). It didn't make much sense to ask "is this event a
group", and there is no such thing as "group messages" currently either. Instead, it's sensible
to ask if the sender of a message is a group, or the chat of an event is a channel. New properties
have been added to both the `User` and `Chat` classes:
* ``.is_user`` will always be `True` for `User` and `False` for `Chat`.
* ``.is_group`` will be `False` for `User` and be `True` for small group chats and supergroups.
* ``.is_broadcast`` will be `False` for `User` and `True` for broadcast channels and broadcast groups.
Because the properties exist both in `User` and `Chat`, you do not need use `isinstance` to check
if a sender is a channel or if a chat is a user.
Some fields of the new `User` type differ from the naming or value type of its raw API counterpart:
* ``user.restriction_reason`` has been renamed to ``restriction_reasons`` (with a trailing **s**)
and now always returns a list.
* ``user.bot_chat_history`` has been renamed to ``user.bot_info.chat_history_access``.
* ``user.bot_nochats`` has been renamed to ``user.bot_info.private_only``.
* ``user.bot_inline_geo`` has been renamed to ``user.bot_info.inline_geo``.
* ``user.bot_info_version`` has been renamed to ``user.bot_info.version``.
* ``user.bot_inline_placeholder`` has been renamed to ``user.bot_info.inline_placeholder``.
The new ``user.bot_info`` field will be `None` for non-bots. The goal is to unify where this
information is found and reduce clutter in the main ``user`` type.
Some fields of the new `Chat` type differ from the naming or value type of its raw API counterpart:
* ``chat.date`` is currently not available. It's either the chat creation or join date, but due
to this inconsistency, it's not included to allow for a better solution in the future.
* ``chat.has_link`` is currently not available, to allow for a better alternative in the future.
* ``chat.has_geo`` is currently not available, to allow for a better alternative in the future.
* ``chat.call_active`` is currently not available, until it's decided what to do about calls.
* ``chat.call_not_empty`` is currently not available, until it's decided what to do about calls.
* ``chat.version`` was removed. It's an implementation detail.
* ``chat.min`` was removed. It's an implementation detail.
* ``chat.deactivated`` was removed. It's redundant with ``chat.migrated_to``.
* ``chat.forbidden`` has been added as a replacement for ``isinstance(chat, (ChatForbidden, ChannelForbidden))``.
* ``chat.forbidden_until`` has been added as a replacement for ``until_date`` in forbidden chats.
* ``chat.restriction_reason`` has been renamed to ``restriction_reasons`` (with a trailing **s**)
and now always returns a list.
* ``chat.migrated_to`` no longer returns a raw type, and instead returns this new `Chat` type.
If you have a need for these, please step in, and explain your use case, so we can work together
to implement a proper design.
Both the new `User` and `Chat` types offer a ``fetch`` method, which can be used to refetch the
instance with fresh information, including the full information about the user (such as the user's
biography or a chat's about description).
Using a flat list to define buttons will now create rows and not columns
@ -662,9 +755,9 @@ If you still want the old behaviour, wrap the list inside another list:
bot.send_message(chat, message, buttons=[[
# +
Button.inline('top'),
Button.inline('middle'),
Button.inline('bottom'),
Button.inline('left'),
Button.inline('center'),
Button.inline('right'),
]])
#+

View File

@ -0,0 +1,270 @@
from typing import Optional, List, TYPE_CHECKING
from datetime import datetime
import mimetypes
from .chatgetter import ChatGetter
from .sendergetter import SenderGetter
from .messagebutton import MessageButton
from .forward import Forward
from .file import File
from .inputfile import InputFile
from .inputmessage import InputMessage
from .button import build_reply_markup
from ..._misc import utils, helpers, tlobject, markdown, html
from ... import _tl, _misc
if TYPE_CHECKING:
from ..._misc import hints
def _fwd(field, doc):
def fget(self):
return getattr(self._message, field, None)
def fset(self, value):
object.__setattr__(self._message, field, value)
return property(fget, fset, None, doc)
class _InputChat:
"""
Input channels and peer chats use a different name for "id" which breaks the property forwarding.
This class simply holds the two fields with proper names.
"""
__slots__ = ('id', 'access_hash')
def __init__(self, input):
self.id = getattr(input, 'channel_id', None) or input.chat_id
self.access_hash = getattr(input, 'access_hash', None)
class Chat:
"""
Represents a :tl:`Chat` or :tl:`Channel` (or their empty and forbidden variants) from the API.
"""
id = _fwd('id', """
The chat identifier. This is the only property which will **always** be present.
""")
title = _fwd('title', """
The chat title. It will be `None` for empty chats.
""")
username = _fwd('username', """
The public `username` of the chat.
""")
participants_count = _fwd('participants_count', """
The number of participants who are currently joined to the chat.
It will be `None` for forbidden and empty chats or if the information isn't known.
""")
broadcast = _fwd('broadcast', """
`True` if the chat is a broadcast channel.
""")
megagroup = _fwd('megagroup', """
`True` if the chat is a supergroup.
""")
gigagroup = _fwd('gigagroup', """
`True` if the chat used to be a `megagroup` but is now a broadcast group.
""")
verified = _fwd('verified', """
`True` if the chat has been verified as official by Telegram.
""")
scam = _fwd('scam', """
`True` if the chat has been flagged as scam.
""")
fake = _fwd('fake', """
`True` if the chat has been flagged as fake.
""")
creator = _fwd('creator', """
`True` if the logged-in account is the creator of the chat.
""")
kicked = _fwd('kicked', """
`True` if the logged-in account was kicked from the chat.
""")
left = _fwd('left', """
`True` if the logged-in account has left the chat.
""")
restricted = _fwd('restricted', """
`True` if the logged-in account cannot write in the chat.
""")
slowmode_enabled = _fwd('slowmode_enabled', """
`True` if the chat currently has slowmode enabled.
""")
signatures = _fwd('signatures', """
`True` if signatures are enabled in a broadcast channel.
""")
admin_rights = _fwd('admin_rights', """
Administrator rights the logged-in account has in the chat.
""")
banned_rights = _fwd('banned_rights', """
Banned rights the logged-in account has in the chat.
""")
default_banned_rights = _fwd('default_banned_rights', """
The default banned rights for every non-admin user in the chat.
""")
@property
def forbidden(self):
"""
`True` if access to this channel is forbidden.
"""
return isinstance(self._chat, (_tl.ChatForbidden, _tl.ChannelForbidden))
@property
def forbidden_until(self):
"""
If access to the chat is only temporarily `forbidden`, returns when access will be regained.
"""
try:
return self._chat.until_date
except AttributeError:
return None
@property
def restriction_reasons(self):
"""
Returns a possibly-empty list of reasons why the chat is restricted to some platforms.
"""
try:
return self._chat.restriction_reason or []
except AttributeError:
return []
@property
def migrated_to(self):
"""
If the current chat has migrated to a larger group, returns the new `Chat`.
"""
try:
migrated = self._chat.migrated_to
except AttributeError:
migrated = None
return Chat(_InputChat(migrated), self._client) if migrated else None
def __init__(self):
raise TypeError('You cannot create Chat instances by hand!')
@classmethod
def _new(cls, client, chat):
self = cls.__new__(cls)
self._client = client
self._chat = chat
self._full = None
return self
async def fetch(self, *, full=False):
"""
Perform an API call to fetch fresh information about this chat.
Returns itself, but with the information fetched (allowing you to chain the call).
If ``full`` is ``True``, the full information about the user will be fetched,
which will include things like ``about``.
"""
return self
def compact(self):
"""
Return a compact representation of this user, useful for storing for later use.
"""
raise RuntimeError('TODO')
@property
def client(self):
"""
Returns the `TelegramClient <telethon.client.telegramclient.TelegramClient>`
which returned this user from a friendly method.
"""
return self._client
def to_dict(self):
return self._user.to_dict()
def __repr__(self):
return helpers.pretty_print(self)
def __str__(self):
return helpers.pretty_print(self, max_depth=2)
def stringify(self):
return helpers.pretty_print(self, indent=0)
@property
def is_user(self):
"""
Returns `False`.
This property also exists in `User`, where it returns `True`.
.. code-block:: python
if message.chat.is_user:
... # do stuff
"""
return False
@property
def is_group(self):
"""
Returns `True` if the chat is a small group chat or `megagroup`_.
This property also exists in `User`, where it returns `False`.
.. code-block:: python
if message.chat.is_group:
... # do stuff
.. _megagroup: https://telegram.org/blog/supergroups5k
"""
return True
@property
def is_broadcast(self):
"""
Returns `True` if the chat is a broadcast channel group chat or `broadcast group`_.
This property also exists in `User`, where it returns `False`.
.. code-block:: python
if message.chat.is_broadcast:
... # do stuff
.. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members
"""
return True
@property
def full_name(self):
"""
Returns `title`.
This property also exists in `User`, where it returns the first name and last name
concatenated.
.. code-block:: python
print(message.chat.full_name):
"""
return self.title

View File

@ -0,0 +1,277 @@
from typing import Optional, List, TYPE_CHECKING
from datetime import datetime
import mimetypes
from .chatgetter import ChatGetter
from .sendergetter import SenderGetter
from .messagebutton import MessageButton
from .forward import Forward
from .file import File
from .inputfile import InputFile
from .inputmessage import InputMessage
from .button import build_reply_markup
from ..._misc import utils, helpers, tlobject, markdown, html
from ... import _tl, _misc
if TYPE_CHECKING:
from ..._misc import hints
def _fwd(field, doc):
def fget(self):
return getattr(self._message, field, None)
def fset(self, value):
object.__setattr__(self._message, field, value)
return property(fget, fset, None, doc)
class BotInfo:
@property
def version(self):
"""
Version number of this information, incremented whenever it changes.
"""
return self._user.bot_info_version
@property
def chat_history_access(self):
"""
`True` if the bot has privacy mode disabled via @BotFather and can see *all* messages of the group.
"""
return self._user.bot_chat_history
@property
def private_only(self):
"""
`True` if the bot cannot be added to group and can only be used in private messages.
"""
return self._user.bot_nochats
@property
def inline_geo(self):
"""
`True` if the bot can request the user's geolocation when used in @bot inline mode.
"""
return self._user.bot_inline_geo
@property
def inline_placeholder(self):
"""
The placeholder to show when using the @bot inline mode.
"""
return self._user.bot_inline_placeholder
def __init__(self, user):
self._user = user
class User:
"""
Represents a :tl:`User` (or :tl:`UserEmpty`, or :tl:`UserFull`) from the API.
"""
id = _fwd('id', """
The user identifier. This is the only property which will **always** be present.
""")
first_name = _fwd('first_name', """
The user's first name. It will be ``None`` for deleted accounts.
""")
last_name = _fwd('last_name', """
The user's last name. It can be ``None``.
""")
username = _fwd('username', """
The user's @username. It can be ``None``.
""")
phone = _fwd('phone', """
The user's phone number. It can be ``None`` if the user is not in your contacts or their
privacy setting does not allow you to view the phone number.
""")
is_self = _fwd('is_self', """
``True`` if this user represents the logged-in account.
""")
bot = _fwd('bot', """
``True`` if this user is a bot created via @BotFather.
""")
contact = _fwd('contact', """
``True`` if this user is in the contact list of the logged-in account.
""")
mutual_contact = _fwd('mutual_contact', """
``True`` if this user is in the contact list of the logged-in account,
and the user also has the logged-in account in their contact list.
""")
deleted = _fwd('deleted', """
``True`` if this user belongs to a deleted account.
""")
verified = _fwd('verified', """
``True`` if this user represents an official account verified by Telegram.
""")
restricted = _fwd('restricted', """
`True` if the user has been restricted for some reason.
""")
support = _fwd('support', """
``True`` if this user belongs to an official account from Telegram Support.
""")
scam = _fwd('scam', """
``True`` if this user has been flagged as spam.
""")
fake = _fwd('fake', """
``True`` if this user has been flagged as fake.
""")
lang_code = _fwd('lang_code', """
Language code of the user, if it's known.
""")
@property
def restriction_reasons(self):
"""
Returns a possibly-empty list of reasons why the chat is restricted to some platforms.
"""
try:
return self._user.restriction_reason or []
except AttributeError:
return []
@property
def bot_info(self):
"""
Additional information about the user if it's a bot, `None` otherwise.
"""
return BotInfo(self._user) if self.bot else None
def __init__(self):
raise TypeError('You cannot create User instances by hand!')
@classmethod
def _new(cls, client, user):
self = cls.__new__(cls)
self._client = client
self._user = user
self._full = None
raise RuntimeError('self._i_need_to_include_participant_info')
return self
async def fetch(self, *, full=False):
"""
Perform an API call to fetch fresh information about this user.
Returns itself, but with the information fetched (allowing you to chain the call).
If ``full`` is ``True``, the full information about the user will be fetched,
which will include things like ``about``.
"""
# sender - might just be hash
# get sender - might be min
# sender fetch - never min
return self
def compact(self):
"""
Return a compact representation of this user, useful for storing for later use.
"""
raise RuntimeError('TODO')
@property
def client(self):
"""
Returns the `TelegramClient <telethon.client.telegramclient.TelegramClient>`
which returned this user from a friendly method.
"""
return self._client
def to_dict(self):
return self._user.to_dict()
def __repr__(self):
return helpers.pretty_print(self)
def __str__(self):
return helpers.pretty_print(self, max_depth=2)
def stringify(self):
return helpers.pretty_print(self, indent=0)
def download_profile_photo():
# why'd you want to access photo? just do this
pass
def get_profile_photos():
# this i can understand as you can pick other photos... sadly exposing raw api
pass
# TODO status, photo, and full properties
@property
def is_user(self):
"""
Returns `True`.
This property also exists in `Chat`, where it returns `False`.
.. code-block:: python
if message.sender.is_user:
... # do stuff
"""
return True
@property
def is_group(self):
"""
Returns `False`.
This property also exists in `Chat`, where it can return `True`.
.. code-block:: python
if message.sender.is_group:
... # do stuff
"""
return False
@property
def is_broadcast(self):
"""
Returns `False`.
This property also exists in `Chat`, where it can return `True`.
.. code-block:: python
if message.sender.is_broadcast:
... # do stuff
"""
return False
@property
def full_name(self):
"""
Returns the user's full name (first name and last name concatenated).
This property also exists in `Chat`, where it returns the title.
.. code-block:: python
print(message.sender.full_name):
"""
return f'{self.first_name} {self.last_name}' if self.last_name else self.first_name