diff --git a/discord/channel.py b/discord/channel.py index 9439684dc..915b33390 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2213,31 +2213,45 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): Attributes ---------- + id: :class:`int` + The direct message channel ID. + recipient: :class:`User` + The user you are participating with in the direct message channel. + me: :class:`ClientUser` + The user presenting yourself. last_message_id: Optional[:class:`int`] The last message ID of the message sent to this channel. It may *not* point to an existing or valid message. .. versionadded:: 2.0 - recipient: Optional[:class:`User`] - The user you are participating with in the direct message channel. - If this channel is received through the gateway, the recipient information - may not be always available. - me: :class:`ClientUser` - The user presenting yourself. - id: :class:`int` - The direct message channel ID. """ - __slots__ = ('id', 'recipient', 'me', 'last_message_id', '_state', '_accessed') + __slots__ = ( + 'id', + 'recipient', + 'me', + 'last_message_id', + '_message_request', + '_requested_at', + '_spam', + '_state', + '_accessed', + ) def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload): self._state: ConnectionState = state - self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') self.recipient: User = state.store_user(data['recipients'][0]) self.me: ClientUser = me self.id: int = int(data['id']) + self._update(data) self._accessed: bool = False + def _update(self, data: DMChannelPayload) -> None: + self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') + self._message_request: Optional[bool] = data.get('is_message_request') + self._requested_at: Optional[datetime.datetime] = utils.parse_time(data.get('is_message_request_timestamp')) + self._spam: bool = data.get('is_spam', False) + def _get_voice_client_key(self) -> Tuple[int, str]: return self.me.id, 'self_id' @@ -2249,7 +2263,7 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): async def _get_channel(self) -> Self: if not self._accessed: - await self._state.access_private_channel(self.id) + await self._state.call_connect(self.id) self._accessed = True return self @@ -2327,6 +2341,24 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): """ return self._state._get_message(self.last_message_id) if self.last_message_id else None + @property + def accepted(self) -> bool: + """:class:`bool`: Indicates if the message request is accepted. For regular direct messages, this is always ``True``.""" + return self._message_request or True + + @property + def requested_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: Returns the message request's creation time in UTC, if applicable.""" + return self._requested_at + + def is_message_request(self) -> bool: + """:class:`bool`: Indicates if the direct message is/was a message request.""" + return self._message_request is not None + + def is_spam(self) -> bool: + """:class:`bool`: Indicates if the direct message is a spam message.""" + return self._spam + def permissions_for(self, obj: Any = None, /) -> Permissions: """Handles permission resolution for a :class:`User`. @@ -2391,7 +2423,7 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): async def close(self): """|coro| - "Deletes" the channel. + Closes/"deletes" the channel. In reality, if you recreate a DM with the same user, all your message history will be there. @@ -2401,7 +2433,7 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): HTTPException Closing the channel failed. """ - await self._state.http.delete_channel(self.id) + await self._state.http.delete_channel(self.id, silent=False) async def connect( self, @@ -2451,6 +2483,41 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): await self._initial_ring() return await super().connect(timeout=timeout, reconnect=reconnect, cls=cls) + async def accept(self) -> DMChannel: + """|coro| + + Accepts a message request. + + Raises + ------- + HTTPException + Accepting the message request failed. + TypeError + The channel is not a message request or the request is already accepted. + """ + data = await self._state.http.accept_message_request(self.id) + # Of course Discord does not actually include these fields + data['is_message_request'] = False + if self._requested_at: + data['is_message_request_timestamp'] = utils.utcnow().isoformat() + data['is_spam'] = self._spam + + return DMChannel(state=self._state, data=data, me=self.me) + + async def decline(self) -> None: + """|coro| + + Declines a message request. This closes the channel. + + Raises + ------- + HTTPException + Declining the message request failed. + TypeError + The channel is not a message request or the request is already accepted. + """ + await self._state.http.decline_message_request(self.id) + class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): """Represents a Discord group channel. @@ -2500,10 +2567,10 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): self._state: ConnectionState = state self.id: int = int(data['id']) self.me: ClientUser = me - self._update_group(data) + self._update(data) self._accessed: bool = False - def _update_group(self, data: GroupChannelPayload) -> None: + def _update(self, data: GroupChannelPayload) -> None: self.owner_id: Optional[int] = utils._get_as_snowflake(data, 'owner_id') self._icon: Optional[str] = data.get('icon') self.name: Optional[str] = data.get('name') @@ -2518,7 +2585,7 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable): async def _get_channel(self) -> Self: if not self._accessed: - await self._state.access_private_channel(self.id) + await self._state.call_connect(self.id) self._accessed = True return self diff --git a/discord/gateway.py b/discord/gateway.py index a7ee1eb5c..e30dc7a0f 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -770,10 +770,10 @@ class DiscordWebSocket: _log.debug('Updating %s voice state to %s.', guild_id or 'client', payload) await self.send_as_json(payload) - async def access_dm(self, channel_id: Snowflake): + async def call_connect(self, channel_id: Snowflake): payload = {'op': self.CALL_CONNECT, 'd': {'channel_id': str(channel_id)}} - _log.debug('Sending ACCESS_DM for channel %s.', channel_id) + _log.debug('Requesting call connect for channel %s.', channel_id) await self.send_as_json(payload) async def request_commands( diff --git a/discord/http.py b/discord/http.py index a051e584e..f151a05ce 100644 --- a/discord/http.py +++ b/discord/http.py @@ -698,6 +698,30 @@ class HTTPClient: return self.request(Route('POST', '/users/@me/channels'), json=payload, context_properties=props) + def accept_message_request(self, channel_id: Snowflake) -> Response[channel.DMChannel]: + payload = { + 'consent_status': 2, + } + + return self.request(Route('PUT', '/channels/{channel_id}/recipients/@me', channel_id=channel_id), json=payload) + + def decline_message_request(self, channel_id: Snowflake) -> Response[channel.DMChannel]: + return self.request(Route('DELETE', '/channels/{channel_id}/recipients/@me', channel_id=channel_id)) + + def mark_message_request(self, channel_id: Snowflake) -> Response[channel.DMChannel]: + payload = { + 'consent_status': 1, + } + + return self.request(Route('PUT', '/channels/{channel_id}/recipients/@me', channel_id=channel_id), json=payload) + + def reset_message_request(self, channel_id: Snowflake) -> Response[channel.DMChannel]: + payload = { + 'consent_status': 0, + } + + return self.request(Route('PUT', '/channels/{channel_id}/recipients/@me', channel_id=channel_id), json=payload) + # Message management def send_message( diff --git a/discord/state.py b/discord/state.py index 2b336d74f..b23f20622 100644 --- a/discord/state.py +++ b/discord/state.py @@ -668,14 +668,11 @@ class ConnectionState: def private_channels(self) -> List[PrivateChannel]: return list(self._private_channels.values()) - async def access_private_channel(self, channel_id: int) -> None: - if (ws := self.ws) is None: + async def call_connect(self, channel_id: int) -> None: + if self.ws is None: return - try: - await ws.access_dm(channel_id) - except Exception as exc: - _log.warning('Sending ACCESS_DM failed for channel %s, (%s).', channel_id, exc) + await self.ws.call_connect(channel_id) def _get_private_channel(self, channel_id: Optional[int]) -> Optional[PrivateChannel]: # The keys of self._private_channels are ints @@ -879,10 +876,10 @@ class ConnectionState: self._relationships[r_id] = Relationship(state=self, data=relationship) # Private channel parsing - for pm in data.get('private_channels', []): + for pm in data.get('private_channels', []) + extra_data.get('lazy_private_channels', []): factory, _ = _private_channel_factory(pm['type']) if 'recipients' not in pm: - pm['recipients'] = [temp_users[int(u_id)] for u_id in pm.pop('recipient_ids')] # type: ignore + pm['recipients'] = [temp_users[int(u_id)] for u_id in pm.pop('recipient_ids')] self._add_private_channel(factory(me=user, data=pm, state=self)) # type: ignore # Extras @@ -1182,12 +1179,11 @@ class ConnectionState: def parse_channel_update(self, data: gw.ChannelUpdateEvent) -> None: channel_type = try_enum(ChannelType, data.get('type')) channel_id = int(data['id']) - if channel_type is ChannelType.group: + if channel_type in (ChannelType.private, ChannelType.group): channel = self._get_private_channel(channel_id) if channel is not None: old_channel = copy.copy(channel) - # The channel is a GroupChannel - channel._update_group(data) # type: ignore + channel._update(data) self.dispatch('private_channel_update', old_channel, channel) return else: diff --git a/discord/types/channel.py b/discord/types/channel.py index 65f16f80a..00e34d4f7 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -130,12 +130,16 @@ class DMChannel(_BaseChannel): type: Literal[1] last_message_id: Optional[Snowflake] recipients: List[PartialUser] + is_message_request: NotRequired[bool] + is_message_request_timestamp: NotRequired[str] + is_spam: NotRequired[bool] class GroupDMChannel(_BaseChannel): type: Literal[3] icon: Optional[str] owner_id: Snowflake + recipients: List[PartialUser] Channel = Union[GuildChannel, DMChannel, GroupDMChannel]