From d784e6dcab6717b25d27fa4b00fe559119e00638 Mon Sep 17 00:00:00 2001 From: dolfies Date: Wed, 29 Jan 2025 14:22:54 -0500 Subject: [PATCH] Fix group dm nickname implementation, add dm migration --- discord/channel.py | 80 ++++++++++++++++++++++++++++++++++++---- discord/http.py | 8 ++++ discord/state.py | 2 + discord/types/channel.py | 11 +++++- discord/types/gateway.py | 1 + 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index 25b408c15..bb7ef2b0f 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -115,6 +115,7 @@ if TYPE_CHECKING: DMChannel as DMChannelPayload, CategoryChannel as CategoryChannelPayload, GroupDMChannel as GroupChannelPayload, + GroupDMNickname as GroupDMNicknamePayload, ForumChannel as ForumChannelPayload, MediaChannel as MediaChannelPayload, ForumTag as ForumTagPayload, @@ -3815,6 +3816,48 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.Pr """ return Permissions._dm_permissions() + async def add_recipients(self, *recipients: Snowflake) -> GroupChannel: + r"""|coro| + + Adds recipients to this DM. This spawns a new group with the existing DM + recipient and the new recipients. + + A group can only have a maximum of 10 members. + Attempting to add more ends up in an exception. To + add a recipient to the group, you must have a relationship + with the user of type :attr:`RelationshipType.friend`. + + .. versionadded:: 2.1 + + Parameters + ----------- + \*recipients: :class:`~discord.abc.Snowflake` + An argument list of users to add to this group. + + Raises + ------- + TypeError + No recipients were provided. + Forbidden + You do not have permissions to add a recipient to this group. + HTTPException + Adding a recipient to this group failed. + + Returns + -------- + :class:`GroupChannel` + The newly created group channel. Due to a Discord limitation, + this will not contain complete recipient data. + """ + if len(recipients) < 1: + raise TypeError('add_recipients() missing 1 required positional argument') + + state = self._state + data = await state.http.convert_dm(self.id, recipients[0].id) + channel = GroupChannel(state=state, data=data, me=self.me) + await channel.add_recipients(*[r for r in recipients[1:]]) + return channel + def get_partial_message(self, message_id: int, /) -> PartialMessage: """Creates a :class:`PartialMessage` from the message ID. @@ -3899,7 +3942,6 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.Pr :class:`~discord.VoiceProtocol` A voice client that is fully connected to the voice server. """ - await self._get_channel() ret = await super().connect(timeout=timeout, reconnect=reconnect, cls=cls) if ring: @@ -4001,6 +4043,13 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc A mapping of users to their respective nicknames in the group channel. .. versionadded:: 2.0 + origin_channel_id: Optional[:class:`int`] + The ID of the DM this group channel originated from, if any. + This can only be accurately received in :func:`on_private_channel_create` + due to a Discord limitation. + + .. versionadded:: 2.1 + """ __slots__ = ( @@ -4012,6 +4061,7 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc 'managed', 'application_id', 'nicks', + 'origin_channel_id', '_icon', 'name', 'me', @@ -4033,7 +4083,17 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc self.last_pin_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('last_pin_timestamp')) self.managed: bool = data.get('managed', False) self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') - self.nicks: Dict[User, str] = {utils.get(self.recipients, id=int(k)): v for k, v in data.get('nicks', {}).items()} # type: ignore + self.nicks: Dict[User, str] = self._unroll_nicks(data.get('nicks', [])) + self.origin_channel_id: Optional[int] = utils._get_as_snowflake(data, 'origin_channel_id') + + def _unroll_nicks(self, data: List[GroupDMNicknamePayload]) -> Dict[User, str]: + ret = {} + for entry in data: + user_id = int(entry['id']) + user = utils.get(self.recipients, id=user_id) + if user: + ret[user] = entry['nick'] + return ret def _get_voice_client_key(self) -> Tuple[int, str]: return self.me.id, 'self_id' @@ -4110,6 +4170,17 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc return None return Asset._from_icon(self._state, self.id, self._icon, path='channel') + @property + def origin_channel(self) -> Optional[DMChannel]: + """Optional[:class:`DMChannel`]: The DM this group channel originated from, if any. + + This can only be accurately received in :func:`on_private_channel_create` + due to a Discord limitation. + + .. versionadded:: 2.1 + """ + return self._state._get_private_channel(self.origin_channel_id) if self.origin_channel_id else None # type: ignore + @property def created_at(self) -> datetime.datetime: """:class:`datetime.datetime`: Returns the channel's creation time in UTC.""" @@ -4295,7 +4366,6 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc Adding a recipient to this group failed. """ nicknames = {k.id: v for k, v in nicks.items()} if nicks else {} - await self._get_channel() req = self._state.http.add_group_recipient for recipient in recipients: await req(self.id, recipient.id, getattr(recipient, 'nick', (nicknames.get(recipient.id) if nicks else None))) @@ -4317,7 +4387,6 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc HTTPException Removing a recipient from this group failed. """ - await self._get_channel() req = self._state.http.remove_group_recipient for recipient in recipients: await req(self.id, recipient.id) @@ -4354,8 +4423,6 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc HTTPException Editing the group failed. """ - await self._get_channel() - payload = {} if name is not MISSING: payload['name'] = name @@ -4471,7 +4538,6 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc cls: Callable[[Client, discord.abc.VocalChannel], T] = VoiceClient, ring: bool = True, ) -> T: - await self._get_channel() ret = await super().connect(timeout=timeout, reconnect=reconnect, cls=cls) if ring: diff --git a/discord/http.py b/discord/http.py index 54c5be5fa..b5e62edaf 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1137,6 +1137,14 @@ class HTTPClient: context_properties=props, ) + def convert_dm(self, channel_id: Snowflake, user_id: Snowflake) -> Response[channel.GroupDMChannel]: + props = ContextProperties.from_add_friends_to_dm() + + return self.request( + Route('PUT', '/channels/{channel_id}/recipients/{user_id}', channel_id=channel_id, user_id=user_id), + context_properties=props, + ) + def remove_group_recipient(self, channel_id: Snowflake, user_id: Snowflake) -> Response[None]: return self.request( Route('DELETE', '/channels/{channel_id}/recipients/{user_id}', channel_id=channel_id, user_id=user_id) diff --git a/discord/state.py b/discord/state.py index 7f252c5d3..1ec8f783b 100644 --- a/discord/state.py +++ b/discord/state.py @@ -2296,6 +2296,8 @@ class ConnectionState: user = self.store_user(data['user']) channel.recipients.append(user) # type: ignore + if 'nick' in data: + channel.nicks[user] = data['nick'] self.dispatch('group_join', channel, user) def parse_channel_recipient_remove(self, data: gw.ChannelRecipientEvent) -> None: diff --git a/discord/types/channel.py b/discord/types/channel.py index 7e040de32..017418f86 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -22,7 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import List, Literal, Optional, TypedDict, Union +from typing import Dict, List, Literal, Optional, TypedDict, Union from typing_extensions import NotRequired from .user import PartialUser @@ -191,12 +191,21 @@ class DMChannel(_BaseChannel): is_spam: NotRequired[bool] +class GroupDMNickname(TypedDict): + id: Snowflake + nick: str + + class GroupDMChannel(_BaseChannel): type: Literal[3] name: Optional[str] icon: Optional[str] owner_id: Snowflake + application_id: NotRequired[Snowflake] + managed: NotRequired[bool] + nicks: NotRequired[List[GroupDMNickname]] recipients: List[PartialUser] + origin_channel_id: NotRequired[Snowflake] # Only present in CHANNEL_CREATE Channel = Union[GuildChannel, DMChannel, GroupDMChannel] diff --git a/discord/types/gateway.py b/discord/types/gateway.py index c30c80b39..459de5581 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -253,6 +253,7 @@ ChannelCreateEvent = ChannelUpdateEvent = ChannelDeleteEvent = _ChannelEvent class ChannelRecipientEvent(TypedDict): channel_id: Snowflake user: PartialUser + nick: str class ChannelPinsUpdateEvent(TypedDict):