From e0c66129222e340236b315d9571e248d42b60b4f Mon Sep 17 00:00:00 2001 From: dolfies Date: Fri, 7 Apr 2023 22:49:41 -0400 Subject: [PATCH] Implement friend suggestions --- discord/client.py | 23 ++++++++- discord/enums.py | 15 +++--- discord/http.py | 18 +++++-- discord/relationship.py | 101 +++++++++++++++++++++++++++++++++++++-- discord/state.py | 12 ++++- discord/types/gateway.py | 9 +++- discord/types/user.py | 11 +++++ docs/api.rst | 39 +++++++++++++++ 8 files changed, 211 insertions(+), 17 deletions(-) diff --git a/discord/client.py b/discord/client.py index 4efb20804..6de23ee68 100644 --- a/discord/client.py +++ b/discord/client.py @@ -85,7 +85,7 @@ from .entitlements import Entitlement, Gift from .store import SKU, StoreListing, SubscriptionPlan from .guild_premium import * from .library import LibraryApplication -from .relationship import Relationship +from .relationship import FriendSuggestion, Relationship from .settings import UserSettings, LegacyUserSettings, TrackingSettings, EmailSettings from .affinity import * @@ -2743,6 +2743,27 @@ class Client: data = await state.http.get_relationships() return [Relationship(state=state, data=d) for d in data] + async def friend_suggestions(self) -> List[FriendSuggestion]: + """|coro| + + Retrieves all your friend suggestions. + + .. versionadded:: 2.1 + + Raises + ------- + HTTPException + Retrieving your friend suggestions failed. + + Returns + -------- + List[:class:`.FriendSuggestion`] + All your current friend suggestions. + """ + state = self._connection + data = await state.http.get_friend_suggestions() + return [FriendSuggestion(state=state, data=d) for d in data] + async def fetch_country_code(self) -> str: """|coro| diff --git a/discord/enums.py b/discord/enums.py index 0cd57341b..5f161c14a 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -785,13 +785,14 @@ class ReportType(Enum): class RelationshipAction(Enum): - send_friend_request = 'request' - unfriend = 'unfriend' - accept_request = 'accept' - deny_request = 'deny' - block = 'block' - unblock = 'unblock' - remove_pending_request = 'remove' + send_friend_request = 1 + unfriend = 2 + accept_request = 3 + deny_request = 4 + block = 5 + unblock = 6 + remove_pending_request = 7 + friend_suggestion = 8 class RequiredActionType(Enum): diff --git a/discord/http.py b/discord/http.py index 5ad01dafa..a5d24e437 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2556,7 +2556,6 @@ class HTTPClient: return self.request(Route('GET', '/users/@me/relationships')) def remove_relationship(self, user_id: Snowflake, *, action: RelationshipAction) -> Response[None]: - r = Route('DELETE', '/users/@me/relationships/{user_id}', user_id=user_id) if action is RelationshipAction.deny_request: # User Profile, Friends, DM Channel props = choice( ( @@ -2582,12 +2581,16 @@ class HTTPClient: else: props = ContextProperties.empty() - return self.request(r, context_properties=props) + return self.request(Route('DELETE', '/users/@me/relationships/{user_id}', user_id=user_id), context_properties=props) def add_relationship( self, user_id: Snowflake, type: Optional[int] = None, *, action: RelationshipAction ) -> Response[None]: r = Route('PUT', '/users/@me/relationships/{user_id}', user_id=user_id) + payload = {} + if type is not None: + payload['type'] = type + if action is RelationshipAction.accept_request: # User Profile, Friends, DM Channel props = choice( ( @@ -2613,10 +2616,13 @@ class HTTPClient: ContextProperties.from_dm_channel, ) )() + elif action is RelationshipAction.friend_suggestion: # Friends + props = ContextProperties.from_friends() + payload['from_friend_suggestion'] = True else: props = ContextProperties.empty() - return self.request(r, context_properties=props, json={'type': type} if type else None) + return self.request(r, context_properties=props, json=payload if payload else None) def send_friend_request(self, username: str, discriminator: Snowflake) -> Response[None]: r = Route('POST', '/users/@me/relationships') @@ -2628,6 +2634,12 @@ class HTTPClient: def edit_relationship(self, user_id: Snowflake, **payload) -> Response[None]: return self.request(Route('PATCH', '/users/@me/relationships/{user_id}', user_id=user_id), json=payload) + def get_friend_suggestions(self) -> Response[List[user.FriendSuggestion]]: + return self.request(Route('GET', '/friend-suggestions')) + + def delete_friend_suggestion(self, user_id: Snowflake) -> Response[None]: + return self.request(Route('DELETE', '/friend-suggestions/{user_id}', user_id=user_id)) + # Connections def get_connections(self) -> Response[List[user.Connection]]: diff --git a/discord/relationship.py b/discord/relationship.py index 570924da9..5224e51bf 100644 --- a/discord/relationship.py +++ b/discord/relationship.py @@ -26,7 +26,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Tuple, Union -from .enums import RelationshipAction, RelationshipType, Status, try_enum +from .enums import ConnectionType, RelationshipAction, RelationshipType, Status, try_enum from .mixins import Hashable from .object import Object from .utils import MISSING, parse_time @@ -38,14 +38,18 @@ if TYPE_CHECKING: from .activity import ActivityTypes from .state import ConnectionState, Presence from .types.gateway import RelationshipEvent - from .types.user import Relationship as RelationshipPayload + from .types.user import ( + FriendSuggestion as FriendSuggestionPayload, + FriendSuggestionReason as FriendSuggestionReasonPayload, + Relationship as RelationshipPayload, + ) from .user import User -# fmt: off __all__ = ( 'Relationship', + 'FriendSuggestionReason', + 'FriendSuggestion', ) -# fmt: on class Relationship(Hashable): @@ -323,3 +327,92 @@ class Relationship(Hashable): payload['nickname'] = nick await self._state.http.edit_relationship(self.user.id, **payload) + + +class FriendSuggestionReason: + """Represents a reason why a user was suggested as a friend to you. + + .. versionadded:: 2.1 + + Attributes + ----------- + platform: :class:`ConnectionType` + The platform the user was suggested from. + name: :class:`str` + The user's name on the platform. + """ + + __slots__ = ('type', 'platform', 'name') + + def __init__(self, data: FriendSuggestionReasonPayload): + # This entire model is unused by any client, so I have no idea what the type is + # Also because of this, I'm treating everything as optional just in case + self.type: int = data.get('type', 0) + self.platform: ConnectionType = try_enum(ConnectionType, data.get('platform', 'contacts')) + self.name: str = data.get('name', '') + + def __repr__(self) -> str: + return f'' + + +class FriendSuggestion(Hashable): + """Represents a friend suggestion on Discord. + + .. container:: operations + + .. describe:: x == y + + Checks if two friend suggestions are equal. + + .. describe:: x != y + + Checks if two friend suggestions are not equal. + + .. describe:: hash(x) + + Return the suggestion's hash. + + .. versionadded:: 2.1 + + Attributes + ----------- + user: :class:`User` + The suggested user. + reasons: List[:class:`FriendSuggestionReason`] + The reasons why the user was suggested. + """ + + __slots__ = ('user', 'reasons', '_state') + + def __init__(self, *, state: ConnectionState, data: FriendSuggestionPayload): + self._state = state + self.user = state.store_user(data['suggested_user']) + self.reasons = [FriendSuggestionReason(r) for r in data.get('reasons', [])] + + def __repr__(self) -> str: + return f'' + + async def accept(self) -> None: + """|coro| + + Accepts the friend suggestion. + This creates a :class:`Relationship` of type :class:`RelationshipType.outgoing_request`. + + Raises + ------- + HTTPException + Accepting the relationship failed. + """ + await self._state.http.add_relationship(self.user.id, action=RelationshipAction.friend_suggestion) + + async def delete(self) -> None: + """|coro| + + Ignores the friend suggestion. + + Raises + ------ + HTTPException + Deleting the relationship failed. + """ + await self._state.http.delete_friend_suggestion(self.user.id) diff --git a/discord/state.py b/discord/state.py index c1d7e5e4f..c9af9b604 100644 --- a/discord/state.py +++ b/discord/state.py @@ -63,7 +63,7 @@ from .channel import * from .channel import _channel_factory, _private_channel_factory from .raw_models import * from .member import Member -from .relationship import Relationship +from .relationship import Relationship, FriendSuggestion from .role import Role from .enums import ( ChannelType, @@ -2625,6 +2625,16 @@ class ConnectionState: new._update(data) self.dispatch('relationship_update', old, new) + def parse_friend_suggestion_create(self, data: gw.FriendSuggestionCreateEvent): + self.dispatch('friend_suggestion_add', FriendSuggestion(state=self, data=data)) + + def parse_friend_suggestion_delete(self, data: gw.FriendSuggestionDeleteEvent): + user_id = int(data['suggested_user_id']) + user = self.get_user(user_id) + if user: + self.dispatch('friend_suggestion_remove', user) + self.dispatch('raw_friend_suggestion_remove', user_id) + def parse_interaction_create(self, data) -> None: if 'nonce' not in data: # Sometimes interactions seem to be missing the nonce return diff --git a/discord/types/gateway.py b/discord/types/gateway.py index f227aa0e1..d624b0255 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -40,7 +40,7 @@ from .message import Message from .sticker import GuildSticker from .application import BaseAchievement, PartialApplication from .guild import ApplicationCommandCounts, Guild, UnavailableGuild, SupplementalGuild -from .user import Connection, User, PartialUser, ProtoSettingsType, Relationship, RelationshipType +from .user import Connection, FriendSuggestion, User, PartialUser, ProtoSettingsType, Relationship, RelationshipType from .threads import Thread, ThreadMember from .scheduled_event import GuildScheduledEvent from .channel import DMChannel, GroupDMChannel @@ -455,6 +455,13 @@ class RelationshipEvent(TypedDict): nickname: Optional[str] +FriendSuggestionCreateEvent = FriendSuggestion + + +class FriendSuggestionDeleteEvent(TypedDict): + suggested_user_id: Snowflake + + class ProtoSettings(TypedDict): proto: str type: ProtoSettingsType diff --git a/discord/types/user.py b/discord/types/user.py index 8490e601b..0e979a8e5 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -156,3 +156,14 @@ class Note(TypedDict): note: str user_id: Snowflake note_user_id: Snowflake + + +class FriendSuggestionReason(TypedDict): + name: str + platform_type: ConnectionType + type: int + + +class FriendSuggestion(TypedDict): + suggested_user: PartialUser + reasons: List[FriendSuggestionReason] diff --git a/docs/api.rst b/docs/api.rst index 2f14c878e..aae02156e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -665,6 +665,35 @@ Relationships :param after: The updated relationship. :type after: :class:`Relationship` +.. function:: on_friend_suggestion_add(friend_suggestion) + + Called when a :class:`FriendSuggestion` is created. + + .. versionadded:: 2.1 + + :param friend_suggestion: The friend suggestion that was created. + :type friend_suggestion: :class:`FriendSuggestion` + +.. function:: on_friend_suggestion_remove(user) + + Called when a :class:`FriendSuggestion` is removed. + + .. versionadded:: 2.1 + + :param user: The friend suggestion that was removed. + :type user: :class:`User` + +.. function:: on_raw_friend_suggestion_remove(user_id) + + Called when a :class:`FriendSuggestion` is removed. + Unlike :func:`on_message_edit`, this is called regardless + of the user being in the internal user cache or not. + + .. versionadded:: 2.1 + + :param user_id: The ID of the friend suggestion that was removed. + :type user_id: :class:`int` + Notes ~~~~~~ @@ -6426,6 +6455,16 @@ Relationship .. autoclass:: Relationship() :members: +.. attributetable:: FriendSuggestion + +.. autoclass:: FriendSuggestion() + :members: + +.. attributetable:: FriendSuggestionReason + +.. autoclass:: FriendSuggestionReason() + :members: + Settings ~~~~~~~~