From 20c543f6729f081dd7963582387e0bac3ea03e9d Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:21:59 +0200 Subject: [PATCH] Add support for message call --- discord/message.py | 77 +++++++++++++++++++++++++++++++++++++++- discord/types/message.py | 6 ++++ discord/utils.py | 52 +++++++++++++++++++++++++++ docs/api.rst | 8 +++++ 4 files changed, 142 insertions(+), 1 deletion(-) diff --git a/discord/message.py b/discord/message.py index c6ef052ee..12a4c90ce 100644 --- a/discord/message.py +++ b/discord/message.py @@ -76,6 +76,7 @@ if TYPE_CHECKING: MessageActivity as MessageActivityPayload, RoleSubscriptionData as RoleSubscriptionDataPayload, MessageInteractionMetadata as MessageInteractionMetadataPayload, + CallMessage as CallMessagePayload, ) from .types.interactions import MessageInteraction as MessageInteractionPayload @@ -112,6 +113,7 @@ __all__ = ( 'MessageApplication', 'RoleSubscriptionInfo', 'MessageInteractionMetadata', + 'CallMessage', ) @@ -810,6 +812,51 @@ class MessageApplication: return None +class CallMessage: + """Represents a message's call data in a private channel from a :class:`~discord.Message`. + + .. versionadded:: 2.5 + + Attributes + ----------- + ended_timestamp: Optional[:class:`datetime.datetime`] + The timestamp the call has ended. + participants: List[:class:`User`] + A list of users that participated in the call. + """ + + __slots__ = ('_message', 'ended_timestamp', 'participants') + + def __repr__(self) -> str: + return f'' + + def __init__(self, *, state: ConnectionState, message: Message, data: CallMessagePayload): + self._message: Message = message + self.ended_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('ended_timestamp')) + self.participants: List[User] = [] + + for user_id in data['participants']: + user_id = int(user_id) + if user_id == self._message.author.id: + self.participants.append(self._message.author) # type: ignore # can't be a Member here + else: + user = state.get_user(user_id) + if user is not None: + self.participants.append(user) + + @property + def duration(self) -> datetime.timedelta: + """:class:`datetime.timedelta`: The duration the call has lasted or is already ongoing.""" + if self.ended_timestamp is None: + return utils.utcnow() - self._message.created_at + else: + return self.ended_timestamp - self._message.created_at + + def is_ended(self) -> bool: + """:class:`bool`: Whether the call is ended or not.""" + return self.ended_timestamp is not None + + class RoleSubscriptionInfo: """Represents a message's role subscription information. @@ -1770,6 +1817,10 @@ class Message(PartialMessage, Hashable): The poll attached to this message. .. versionadded:: 2.4 + call: Optional[:class:`CallMessage`] + The call associated with this message. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -1806,6 +1857,7 @@ class Message(PartialMessage, Hashable): 'position', 'interaction_metadata', 'poll', + 'call', ) if TYPE_CHECKING: @@ -1931,7 +1983,7 @@ class Message(PartialMessage, Hashable): else: self.role_subscription = RoleSubscriptionInfo(role_subscription) - for handler in ('author', 'member', 'mentions', 'mention_roles', 'components'): + for handler in ('author', 'member', 'mentions', 'mention_roles', 'components', 'call'): try: getattr(self, f'_handle_{handler}')(data[handler]) except KeyError: @@ -2117,6 +2169,13 @@ class Message(PartialMessage, Hashable): def _handle_interaction_metadata(self, data: MessageInteractionMetadataPayload): self.interaction_metadata = MessageInteractionMetadata(state=self._state, guild=self.guild, data=data) + def _handle_call(self, data: CallMessagePayload): + self.call: Optional[CallMessage] + if data is not None: + self.call = CallMessage(state=self._state, message=self, data=data) + else: + self.call = None + def _rebind_cached_references( self, new_guild: Guild, @@ -2421,6 +2480,22 @@ class Message(PartialMessage, Hashable): if self.type is MessageType.guild_incident_report_false_alarm: return f'{self.author.name} reported a false alarm in {self.guild}.' + if self.type is MessageType.call: + call_ended = self.call.ended_timestamp is not None # type: ignore # call can't be None here + missed = self._state.user not in self.call.participants # type: ignore # call can't be None here + + if call_ended: + duration = utils._format_call_duration(self.call.duration) # type: ignore # call can't be None here + if missed: + return 'You missed a call from {0.author.name} that lasted {1}.'.format(self, duration) + else: + return '{0.author.name} started a call that lasted {1}.'.format(self, duration) + else: + if missed: + return '{0.author.name} started a call. \N{EM DASH} Join the call'.format(self) + else: + return '{0.author.name} started a call.'.format(self) + # Fallback for unknown message types return '' diff --git a/discord/types/message.py b/discord/types/message.py index bdb3f10ef..995dc8b8b 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -116,6 +116,11 @@ class RoleSubscriptionData(TypedDict): is_renewal: bool +class CallMessage(TypedDict): + participants: SnowflakeList + ended_timestamp: NotRequired[Optional[str]] + + MessageType = Literal[ 0, 1, @@ -187,6 +192,7 @@ class Message(PartialMessage): position: NotRequired[int] role_subscription_data: NotRequired[RoleSubscriptionData] thread: NotRequired[Thread] + call: NotRequired[CallMessage] AllowedMentionType = Literal['roles', 'users', 'everyone'] diff --git a/discord/utils.py b/discord/utils.py index cb7d662b6..5d898b38b 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1468,3 +1468,55 @@ else: return msg.decode('utf-8') _ActiveDecompressionContext: Type[_DecompressionContext] = _ZlibDecompressionContext + + +def _format_call_duration(duration: datetime.timedelta) -> str: + seconds = duration.total_seconds() + + minutes_s = 60 + hours_s = minutes_s * 60 + days_s = hours_s * 24 + # Discord uses approx. 1/12 of 365.25 days (avg. days per year) + months_s = days_s * 30.4375 + years_s = months_s * 12 + + threshold_s = 45 + threshold_m = 45 + threshold_h = 21.5 + threshold_d = 25.5 + threshold_M = 10.5 + + if seconds < threshold_s: + formatted = "a few seconds" + elif seconds < (threshold_m * minutes_s): + minutes = round(seconds / minutes_s) + if minutes == 1: + formatted = "a minute" + else: + formatted = f"{minutes} minutes" + elif seconds < (threshold_h * hours_s): + hours = round(seconds / hours_s) + if hours == 1: + formatted = "an hour" + else: + formatted = f"{hours} hours" + elif seconds < (threshold_d * days_s): + days = round(seconds / days_s) + if days == 1: + formatted = "a day" + else: + formatted = f"{days} days" + elif seconds < (threshold_M * months_s): + months = round(seconds / months_s) + if months == 1: + formatted = "a month" + else: + formatted = f"{months} months" + else: + years = round(seconds / years_s) + if years == 1: + formatted = "a year" + else: + formatted = f"{years} years" + + return formatted diff --git a/docs/api.rst b/docs/api.rst index 4b88e4871..3531dde06 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5626,6 +5626,14 @@ PollMedia .. autoclass:: PollMedia :members: +CallMessage +~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: CallMessage + +.. autoclass:: CallMessage() + :members: + Exceptions ------------