From a468054cc3ff931b7e546805e4a028ea07dff89e Mon Sep 17 00:00:00 2001 From: dolfies Date: Sun, 7 May 2023 12:13:46 -0400 Subject: [PATCH] Implement tutorial management --- discord/__init__.py | 1 + discord/client.py | 9 +++ discord/http.py | 10 ++++ discord/state.py | 5 ++ discord/tutorial.py | 115 +++++++++++++++++++++++++++++++++++++++ discord/types/gateway.py | 34 +++++++----- docs/api.rst | 8 +++ 7 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 discord/tutorial.py diff --git a/discord/__init__.py b/discord/__init__.py index 9a2b68fc5..d1bf89db6 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -75,6 +75,7 @@ from .subscriptions import * from .team import * from .template import * from .threads import * +from .tutorial import * from .user import * from .voice_client import * from .webhook import * diff --git a/discord/client.py b/discord/client.py index 6512cb734..0e0b78e04 100644 --- a/discord/client.py +++ b/discord/client.py @@ -106,6 +106,7 @@ if TYPE_CHECKING: from .metadata import MetadataObject from .permissions import Permissions from .read_state import ReadState + from .tutorial import Tutorial from .types.snowflake import Snowflake as _Snowflake PrivateChannel = Union[DMChannel, GroupChannel] @@ -512,6 +513,14 @@ class Client: """ return self._connection.friend_suggestion_count + @property + def tutorial(self) -> Tutorial: + """:class:`.Tutorial`: The tutorial state of the connected client. + + .. versionadded:: 2.1 + """ + return self._connection.tutorial + def is_ready(self) -> bool: """:class:`bool`: Specifies if the client's internal cache is ready for use.""" return self._ready is not MISSING and self._ready.is_set() diff --git a/discord/http.py b/discord/http.py index fbd8aed2a..803e2acae 100644 --- a/discord/http.py +++ b/discord/http.py @@ -4556,6 +4556,8 @@ class HTTPClient: ) ) + # Recent Mentions + def get_recent_mentions( self, limit: int = 25, @@ -4579,6 +4581,14 @@ class HTTPClient: def delete_recent_mention(self, message_id: Snowflake) -> Response[None]: return self.request(Route('DELETE', '/users/@me/mentions/{message_id}', message_id=message_id)) + # Tutorial + + def confirm_tutorial_indicator(self, indicator: str) -> Response[None]: + return self.request(Route('PUT', '/tutorial/indicators/{indicator}', indicator=indicator)) + + def suppress_tutorial(self) -> Response[None]: + return self.request(Route('POST', '/tutorial/indicators/suppress')) + async def get_preferred_voice_regions(self) -> List[dict]: async with self.__session.get('https://latency.discord.media/rtc') as resp: if resp.status == 200: diff --git a/discord/state.py b/discord/state.py index a9a4038f2..8d7edcfe6 100644 --- a/discord/state.py +++ b/discord/state.py @@ -97,6 +97,7 @@ from .library import LibraryApplication from .automod import AutoModRule, AutoModAction from .audit_logs import AuditLogEntry from .read_state import ReadState +from .tutorial import Tutorial if TYPE_CHECKING: from typing_extensions import Self @@ -615,6 +616,7 @@ class ConnectionState: self._emojis: Dict[int, Emoji] = {} self._stickers: Dict[int, GuildSticker] = {} self._guilds: Dict[int, Guild] = {} + self.tutorial: Tutorial = Tutorial.default(self) self._read_states: Dict[int, Dict[int, ReadState]] = {} self.read_state_version: int = 0 @@ -1066,6 +1068,9 @@ class ConnectionState: if 'auth_token' in data: self.http._token(data['auth_token']) + if 'tutorial' in data and data['tutorial']: + self.tutorial = Tutorial(state=self, data=data['tutorial']) + # We're done del self._ready_data self.call_handlers('connect') diff --git a/discord/tutorial.py b/discord/tutorial.py new file mode 100644 index 000000000..5b5f7755e --- /dev/null +++ b/discord/tutorial.py @@ -0,0 +1,115 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Dolfies + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Sequence + +from .utils import SequenceProxy + +if TYPE_CHECKING: + from typing_extensions import Self + + from .state import ConnectionState + from .types.gateway import Tutorial as TutorialPayload + +# fmt: off +__all__ = ( + 'Tutorial', +) +# fmt: on + + +class Tutorial: + """Represents the Discord new user tutorial state. + + .. versionadded:: 2.1 + + Attributes + ----------- + suppressed: :class:`bool` + Whether the tutorial is suppressed or not. + """ + + __slots__ = ('suppressed', '_indicators', '_state') + + def __init__(self, *, data: TutorialPayload, state: ConnectionState): + self._state: ConnectionState = state + self.suppressed: bool = data.get('indicators_suppressed', True) + self._indicators: List[str] = data.get('indicators_confirmed', []) + + def __repr__(self) -> str: + return f'' + + @classmethod + def default(cls, state: ConnectionState) -> Self: + self = cls.__new__(cls) + self._state = state + self.suppressed = True + self._indicators = [] + return self + + @property + def indicators(self) -> Sequence[str]: + """Sequence[:class:`str`]: A list of the tutorial indicators that have been confirmed.""" + return SequenceProxy(self._indicators) + + async def suppress(self) -> None: + """|coro| + + Permanently suppresses all tutorial indicators. + + Raises + ------- + HTTPException + Suppressing the tutorial failed. + """ + await self._state.http.suppress_tutorial() + self.suppressed = True + + async def confirm(self, *indicators: str) -> None: + r"""|coro| + + Confirms a list of tutorial indicators. + + Parameters + ----------- + \*indicators: :class:`str` + The indicators to confirm. + + Raises + ------- + HTTPException + Confirming the tutorial indicators failed. + """ + req = self._state.http.confirm_tutorial_indicator + # The gateway does not send updates on the tutorial + # So we keep the state updated ourselves + for indicator in indicators: + if indicator not in self.indicators: + await req(indicator) + self._indicators.append(indicator) + + # Indicators are sorted alphabetically + self._indicators.sort() diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 78153d2c8..dd4e66dbf 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -74,20 +74,6 @@ class ShardInfo(TypedDict): shard_count: int -class ClientInfo(TypedDict): - version: int - os: str - client: str - - -class Session(TypedDict): - session_id: str - active: NotRequired[bool] - client_info: ClientInfo - status: StatusType - activities: List[Activity] - - class ResumedEvent(TypedDict): _trace: List[str] @@ -114,6 +100,7 @@ class ReadyEvent(ResumedEvent): session_id: str session_type: str shard: NotRequired[ShardInfo] + tutorial: Optional[Tutorial] user: User user_guild_settings: dict user_settings_proto: NotRequired[str] @@ -121,6 +108,20 @@ class ReadyEvent(ResumedEvent): v: int +class ClientInfo(TypedDict): + version: int + os: str + client: str + + +class Session(TypedDict): + session_id: str + active: NotRequired[bool] + client_info: ClientInfo + status: StatusType + activities: List[Activity] + + class MergedPresences(TypedDict): friends: List[UserPresenceUpdateEvent] guilds: List[List[PartialPresenceUpdate]] @@ -139,6 +140,11 @@ class VersionedReadState(TypedDict): partial: bool +class Tutorial(TypedDict): + indicators_suppressed: bool + indicators_confirmed: List[str] + + NoEvent = Literal[None] diff --git a/docs/api.rst b/docs/api.rst index c7e3e5ff1..017f7c500 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7533,6 +7533,14 @@ WelcomeScreen .. autoclass:: WelcomeChannel() :members: +Tutorial +~~~~~~~~ + +.. attributetable:: Tutorial + +.. autoclass:: Tutorial() + :members: + RawEvent ~~~~~~~~~