Telethon/telethon/events/chataction.py

459 lines
18 KiB
Python
Raw Normal View History

2018-04-05 21:14:22 +03:00
from .common import EventBuilder, EventCommon, name_inner_event
2020-07-04 13:18:39 +03:00
from .. import utils
from ..tl import types
2018-04-05 21:14:22 +03:00
@name_inner_event
class ChatAction(EventBuilder):
"""
Occurs on certain chat actions:
* Whenever a new chat is created.
* Whenever a chat's title or photo is changed or removed.
* Whenever a new message is pinned.
* Whenever a user scores in a game.
* Whenever a user joins or is added to the group.
* Whenever a user is removed or leaves a group if it has
less than 50 members or the removed user was a bot.
Note that "chat" refers to "small group, megagroup and broadcast
channel", whereas "group" refers to "small group and megagroup" only.
2020-02-20 12:18:26 +03:00
Example
.. code-block:: python
from telethon import events
@client.on(events.ChatAction)
async def handler(event):
# Welcome every new user
if event.user_joined:
await event.reply('Welcome to the group!')
2018-04-05 21:14:22 +03:00
"""
2018-07-19 02:47:32 +03:00
@classmethod
def build(cls, update, others=None, self_id=None):
# Rely on specific pin updates for unpins, but otherwise ignore them
# for new pins (we'd rather handle the new service message with pin,
# so that we can act on that message').
if isinstance(update, types.UpdatePinnedChannelMessages) and not update.pinned:
2019-06-30 14:23:18 +03:00
return cls.Event(types.PeerChannel(update.channel_id),
pin_ids=update.messages,
pin=update.pinned)
2018-04-05 21:14:22 +03:00
elif isinstance(update, types.UpdatePinnedMessages) and not update.pinned:
return cls.Event(update.peer,
pin_ids=update.messages,
pin=update.pinned)
2018-04-05 21:14:22 +03:00
elif isinstance(update, types.UpdateChatParticipantAdd):
2019-06-30 14:23:18 +03:00
return cls.Event(types.PeerChat(update.chat_id),
added_by=update.inviter_id or True,
users=update.user_id)
2018-04-05 21:14:22 +03:00
elif isinstance(update, types.UpdateChatParticipantDelete):
2019-06-30 14:23:18 +03:00
return cls.Event(types.PeerChat(update.chat_id),
kicked_by=True,
users=update.user_id)
2018-04-05 21:14:22 +03:00
# UpdateChannel is sent if we leave a channel, and the update._entities
# set by _process_update would let us make some guesses. However it's
# better not to rely on this. Rely only in MessageActionChatDeleteUser.
2018-04-05 21:14:22 +03:00
elif (isinstance(update, (
types.UpdateNewMessage, types.UpdateNewChannelMessage))
and isinstance(update.message, types.MessageService)):
msg = update.message
action = update.message.action
if isinstance(action, types.MessageActionChatJoinedByLink):
2019-06-30 14:23:18 +03:00
return cls.Event(msg,
added_by=True,
users=msg.from_id)
2018-04-05 21:14:22 +03:00
elif isinstance(action, types.MessageActionChatAddUser):
# If a user adds itself, it means they joined via the public chat username
added_by = ([msg.sender_id] == action.users) or msg.from_id
2019-06-30 14:23:18 +03:00
return cls.Event(msg,
added_by=added_by,
users=action.users)
2018-04-05 21:14:22 +03:00
elif isinstance(action, types.MessageActionChatDeleteUser):
2019-06-30 14:23:18 +03:00
return cls.Event(msg,
kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True,
2019-06-30 14:23:18 +03:00
users=action.user_id)
2018-04-05 21:14:22 +03:00
elif isinstance(action, types.MessageActionChatCreate):
2019-06-30 14:23:18 +03:00
return cls.Event(msg,
users=action.users,
created=True,
new_title=action.title)
2018-04-05 21:14:22 +03:00
elif isinstance(action, types.MessageActionChannelCreate):
2019-06-30 14:23:18 +03:00
return cls.Event(msg,
created=True,
users=msg.from_id,
new_title=action.title)
2018-04-05 21:14:22 +03:00
elif isinstance(action, types.MessageActionChatEditTitle):
2019-06-30 14:23:18 +03:00
return cls.Event(msg,
users=msg.from_id,
new_title=action.title)
2018-04-05 21:14:22 +03:00
elif isinstance(action, types.MessageActionChatEditPhoto):
2019-06-30 14:23:18 +03:00
return cls.Event(msg,
users=msg.from_id,
new_photo=action.photo)
2018-04-05 21:14:22 +03:00
elif isinstance(action, types.MessageActionChatDeletePhoto):
2019-06-30 14:23:18 +03:00
return cls.Event(msg,
users=msg.from_id,
new_photo=True)
elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to:
return cls.Event(msg,
pin_ids=[msg.reply_to_msg_id])
elif isinstance(action, types.MessageActionGameScore):
return cls.Event(msg,
new_score=action.score)
2018-04-05 21:14:22 +03:00
elif isinstance(update, types.UpdateChannelParticipant) \
and bool(update.new_participant) != bool(update.prev_participant):
# If members are hidden, bots will receive this update instead,
# as there won't be a service message. Promotions and demotions
# seem to have both new and prev participant, which are ignored
# by this event.
return cls.Event(types.PeerChannel(update.channel_id),
users=update.user_id,
added_by=update.actor_id if update.new_participant else None,
kicked_by=update.actor_id if update.prev_participant else None)
2018-04-05 21:14:22 +03:00
class Event(EventCommon):
"""
Represents the event of a new chat action.
Members:
2019-05-22 12:29:46 +03:00
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
The message invoked by this Chat Action.
2018-04-05 21:14:22 +03:00
new_pin (`bool`):
`True` if there is a new pin.
2018-04-05 21:14:22 +03:00
new_photo (`bool`):
`True` if there's a new chat photo (or it was removed).
2018-04-05 21:14:22 +03:00
photo (:tl:`Photo`, optional):
The new photo (or `None` if it was removed).
2018-04-05 21:14:22 +03:00
user_added (`bool`):
`True` if the user was added by some other.
2018-04-05 21:14:22 +03:00
user_joined (`bool`):
`True` if the user joined on their own.
2018-04-05 21:14:22 +03:00
user_left (`bool`):
`True` if the user left on their own.
2018-04-05 21:14:22 +03:00
user_kicked (`bool`):
`True` if the user was kicked by some other.
2018-04-05 21:14:22 +03:00
created (`bool`, optional):
`True` if this chat was just created.
2018-04-05 21:14:22 +03:00
2018-04-28 14:42:36 +03:00
new_title (`str`, optional):
2018-04-05 21:14:22 +03:00
The new title string for the chat, if applicable.
new_score (`str`, optional):
The new score string for the game, if applicable.
2018-04-05 21:14:22 +03:00
unpin (`bool`):
`True` if the existing pin gets unpinned.
2018-04-05 21:14:22 +03:00
"""
def __init__(self, where, new_photo=None,
2018-04-05 21:14:22 +03:00
added_by=None, kicked_by=None, created=None,
users=None, new_title=None, pin_ids=None, pin=None, new_score=None):
2018-04-05 21:14:22 +03:00
if isinstance(where, types.MessageService):
self.action_message = where
where = where.peer_id
2018-04-05 21:14:22 +03:00
else:
self.action_message = None
# TODO needs some testing (can there be more than one id, and do they follow pin order?)
# same in get_pinned_message
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)
2018-04-05 21:14:22 +03:00
self.new_pin = pin_ids is not None
self._pin_ids = pin_ids
self._pinned_messages = None
2018-04-05 21:14:22 +03:00
self.new_photo = new_photo is not None
self.photo = \
new_photo if isinstance(new_photo, types.Photo) else None
self._added_by = None
self._kicked_by = None
self.user_added = self.user_joined = self.user_left = \
self.user_kicked = self.unpin = False
2018-04-05 21:14:22 +03:00
if added_by is True:
self.user_joined = True
elif added_by:
self.user_added = True
self._added_by = added_by
# If `from_id` was not present (it's `True`) or the affected
# user was "kicked by itself", then it left. Else it was kicked.
if kicked_by is True or (users is not None and kicked_by == users):
2018-04-05 21:14:22 +03:00
self.user_left = True
elif kicked_by:
self.user_kicked = True
self._kicked_by = kicked_by
self.created = bool(created)
if isinstance(users, list):
self._user_ids = [utils.get_peer_id(u) for u in users]
elif users:
self._user_ids = [utils.get_peer_id(users)]
else:
self._user_ids = []
2018-04-05 21:14:22 +03:00
self._users = None
self._input_users = None
self.new_title = new_title
self.new_score = new_score
self.unpin = not pin
2018-04-05 21:14:22 +03:00
2018-05-31 14:30:22 +03:00
def _set_client(self, client):
super()._set_client(client)
if self.action_message:
self.action_message._finish_init(client, self._entities, None)
2018-05-31 14:30:22 +03:00
async def respond(self, *args, **kwargs):
2018-04-05 21:14:22 +03:00
"""
2018-05-17 13:00:22 +03:00
Responds to the chat action message (not as a reply). Shorthand for
2018-10-17 12:47:51 +03:00
`telethon.client.messages.MessageMethods.send_message` with
2018-05-17 13:00:22 +03:00
``entity`` already set.
2018-04-05 21:14:22 +03:00
"""
return await self._client.send_message(
2018-06-25 12:38:56 +03:00
await self.get_input_chat(), *args, **kwargs)
2018-04-05 21:14:22 +03:00
async def reply(self, *args, **kwargs):
2018-04-05 21:14:22 +03:00
"""
Replies to the chat action message (as a reply). Shorthand for
2018-10-17 12:47:51 +03:00
`telethon.client.messages.MessageMethods.send_message` with
2018-05-17 13:00:22 +03:00
both ``entity`` and ``reply_to`` already set.
2018-04-05 21:14:22 +03:00
2018-05-17 13:00:22 +03:00
Has the same effect as `respond` if there is no message.
2018-04-05 21:14:22 +03:00
"""
if not self.action_message:
return await self.respond(*args, **kwargs)
2018-04-05 21:14:22 +03:00
kwargs['reply_to'] = self.action_message.id
return await self._client.send_message(
2018-06-25 12:38:56 +03:00
await self.get_input_chat(), *args, **kwargs)
2018-04-05 21:14:22 +03:00
async def delete(self, *args, **kwargs):
2018-04-05 21:14:22 +03:00
"""
Deletes the chat action message. You're responsible for checking
whether you have the permission to do so, or to except the error
2018-05-17 13:00:22 +03:00
otherwise. Shorthand for
2018-10-17 12:47:51 +03:00
`telethon.client.messages.MessageMethods.delete_messages` with
2018-05-17 13:00:22 +03:00
``entity`` and ``message_ids`` already set.
2018-04-05 21:14:22 +03:00
Does nothing if no message action triggered this event.
"""
if not self.action_message:
return
return await self._client.delete_messages(
2018-06-25 12:38:56 +03:00
await self.get_input_chat(), [self.action_message],
*args, **kwargs
)
2018-04-05 21:14:22 +03:00
async def get_pinned_message(self):
2018-04-05 21:14:22 +03:00
"""
If ``new_pin`` is `True`, this returns the `Message
<telethon.tl.custom.message.Message>` object that was pinned.
2018-04-05 21:14:22 +03:00
"""
if self._pinned_messages is None:
await self.get_pinned_messages()
if self._pinned_messages:
return self._pinned_messages[0]
async def get_pinned_messages(self):
"""
If ``new_pin`` is `True`, this returns a `list` of `Message
<telethon.tl.custom.message.Message>` objects that were pinned.
"""
if not self._pin_ids:
return self._pin_ids # either None or empty list
2018-04-05 21:14:22 +03:00
chat = await self.get_input_chat()
if chat:
self._pinned_messages = await self._client.get_messages(
self._input_chat, ids=self._pin_ids)
2018-04-05 21:14:22 +03:00
return self._pinned_messages
2018-04-05 21:14:22 +03:00
@property
def added_by(self):
2018-04-05 21:14:22 +03:00
"""
The user who added ``users``, if applicable (`None` otherwise).
2018-04-05 21:14:22 +03:00
"""
if self._added_by and not isinstance(self._added_by, types.User):
aby = self._entities.get(utils.get_peer_id(self._added_by))
if aby:
self._added_by = aby
return self._added_by
async def get_added_by(self):
"""
Returns `added_by` but will make an API call if necessary.
"""
if not self.added_by and self._added_by:
self._added_by = await self._client.get_entity(self._added_by)
2018-04-05 21:14:22 +03:00
return self._added_by
@property
def kicked_by(self):
2018-04-05 21:14:22 +03:00
"""
The user who kicked ``users``, if applicable (`None` otherwise).
2018-04-05 21:14:22 +03:00
"""
if self._kicked_by and not isinstance(self._kicked_by, types.User):
kby = self._entities.get(utils.get_peer_id(self._kicked_by))
if kby:
self._kicked_by = kby
return self._kicked_by
async def get_kicked_by(self):
"""
Returns `kicked_by` but will make an API call if necessary.
"""
if not self.kicked_by and self._kicked_by:
self._kicked_by = await self._client.get_entity(self._kicked_by)
2018-04-05 21:14:22 +03:00
return self._kicked_by
@property
def user(self):
2018-04-05 21:14:22 +03:00
"""
The first user that takes part in this action. For example, who joined.
2018-04-05 21:14:22 +03:00
Might be `None` if the information can't be retrieved or
2018-04-05 21:14:22 +03:00
there is no user taking part.
"""
if self.users:
2018-04-05 21:14:22 +03:00
return self._users[0]
async def get_user(self):
"""
Returns `user` but will make an API call if necessary.
"""
if self.users or await self.get_users():
return self._users[0]
2018-11-19 14:06:18 +03:00
@property
def input_user(self):
2018-04-05 21:14:22 +03:00
"""
Input version of the ``self.user`` property.
"""
if self.input_users:
return self._input_users[0]
async def get_input_user(self):
"""
Returns `input_user` but will make an API call if necessary.
"""
if self.input_users or await self.get_input_users():
2018-04-05 21:14:22 +03:00
return self._input_users[0]
@property
def user_id(self):
"""
Returns the marked signed ID of the first user, if any.
"""
if self._user_ids:
return self._user_ids[0]
2018-04-05 21:14:22 +03:00
@property
def users(self):
2018-04-05 21:14:22 +03:00
"""
A list of users that take part in this action. For example, who joined.
2018-04-05 21:14:22 +03:00
Might be empty if the information can't be retrieved or there
are no users taking part.
"""
if not self._user_ids:
2018-04-05 21:14:22 +03:00
return []
if self._users is None:
self._users = [
self._entities[user_id]
for user_id in self._user_ids
if user_id in self._entities
]
return self._users
async def get_users(self):
"""
Returns `users` but will make an API call if necessary.
"""
if not self._user_ids:
return []
# Note: we access the property first so that it fills if needed
if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:
await self.action_message._reload_message()
self._users = [
u for u in self.action_message.action_entities
if isinstance(u, (types.User, types.UserEmpty))]
2018-04-05 21:14:22 +03:00
return self._users
@property
def input_users(self):
2018-04-05 21:14:22 +03:00
"""
Input version of the ``self.users`` property.
"""
if self._input_users is None and self._user_ids:
2018-04-05 21:14:22 +03:00
self._input_users = []
for user_id in self._user_ids:
# First try to get it from our entities
try:
self._input_users.append(utils.get_input_peer(self._entities[user_id]))
continue
2020-07-04 13:18:39 +03:00
except (KeyError, TypeError):
pass
# If missing, try from the entity cache
2018-04-05 21:14:22 +03:00
try:
self._input_users.append(self._client._mb_entity_cache.get(
utils.resolve_id(user_id)[0])._as_input_peer())
continue
except AttributeError:
2018-04-05 21:14:22 +03:00
pass
return self._input_users or []
async def get_input_users(self):
"""
Returns `input_users` but will make an API call if necessary.
"""
if not self._user_ids:
return []
# Note: we access the property first so that it fills if needed
if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message:
self._input_users = [
utils.get_input_peer(u)
for u in self.action_message.action_entities
if isinstance(u, (types.User, types.UserEmpty))]
return self._input_users or []
@property
def user_ids(self):
"""
Returns the marked signed ID of the users, if any.
"""
if self._user_ids:
return self._user_ids[:]