Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for message call #9911

Merged
merged 11 commits into from
Oct 10, 2024
77 changes: 76 additions & 1 deletion discord/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
MessageActivity as MessageActivityPayload,
RoleSubscriptionData as RoleSubscriptionDataPayload,
MessageInteractionMetadata as MessageInteractionMetadataPayload,
CallMessage as CallMessagePayload,
)

from .types.interactions import MessageInteraction as MessageInteractionPayload
Expand Down Expand Up @@ -112,6 +113,7 @@
'MessageApplication',
'RoleSubscriptionInfo',
'MessageInteractionMetadata',
'CallMessage',
)


Expand Down Expand Up @@ -810,6 +812,51 @@ def cover(self) -> Optional[Asset]:
return None


class CallMessage:
"""Represents a message's call data in a private channel from a :class:`~discord.Message`.

.. versionadded:: 2.5
Puncher1 marked this conversation as resolved.
Show resolved Hide resolved

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'<CallMessage participants={self.participants!r}>'

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.

Expand Down Expand Up @@ -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__ = (
Expand Down Expand Up @@ -1806,6 +1857,7 @@ class Message(PartialMessage, Hashable):
'position',
'interaction_metadata',
'poll',
'call',
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -1931,7 +1983,7 @@ def __init__(
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:
Expand Down Expand Up @@ -2117,6 +2169,13 @@ def _handle_interaction(self, data: MessageInteractionPayload):
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,
Expand Down Expand Up @@ -2421,6 +2480,22 @@ def system_content(self) -> str:
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 ''

Expand Down
6 changes: 6 additions & 0 deletions discord/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ class RoleSubscriptionData(TypedDict):
is_renewal: bool


class CallMessage(TypedDict):
participants: SnowflakeList
ended_timestamp: NotRequired[Optional[str]]


MessageType = Literal[
0,
1,
Expand Down Expand Up @@ -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']
Expand Down
52 changes: 52 additions & 0 deletions discord/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@
def _to_json(obj: Any) -> str:
return orjson.dumps(obj).decode('utf-8')

_from_json = orjson.loads # type: ignore

Check warning on line 666 in discord/utils.py

View workflow job for this annotation

GitHub Actions / check 3.x

Unnecessary "# type: ignore" comment

else:

Expand Down Expand Up @@ -1468,3 +1468,55 @@
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
8 changes: 8 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5626,6 +5626,14 @@ PollMedia
.. autoclass:: PollMedia
:members:

CallMessage
~~~~~~~~~~~~~~~~~~~

.. attributetable:: CallMessage

.. autoclass:: CallMessage()
:members:


Exceptions
------------
Expand Down
Loading