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

feat: implement onboarding #928

Merged
merged 66 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
40b1009
onboarding temp
Victorsitou Jan 6, 2023
037b858
Merge branch 'onboarding-temp' into feat/guild-onboarding
Victorsitou Feb 7, 2023
071c719
fix
Victorsitou Feb 9, 2023
76a8911
fix: remove onboarding edit
Victorsitou Feb 9, 2023
bbe55e9
feat: add new stuff
Victorsitou Feb 9, 2023
53a3f54
feat: remove stuff
Victorsitou Feb 9, 2023
292922f
revert
Victorsitou Feb 9, 2023
0d7313b
remove unnecessary stuff
Victorsitou Feb 9, 2023
183a6f6
revert unnecessary stuff
Victorsitou Feb 9, 2023
0b9ddd8
2.8 -> 2.9
Victorsitou Feb 9, 2023
2f0b741
fix: tests
Victorsitou Feb 9, 2023
de1424c
types
Victorsitou Feb 9, 2023
5f53a1f
feat: change the way emoji is handled
Victorsitou Feb 21, 2023
4ba2bd4
Merge remote-tracking branch 'upstream/master' into feat/guild-onboar…
Victorsitou Feb 21, 2023
a8fc694
docs: change changelog name
Victorsitou Feb 21, 2023
a86e7e8
fix: nox
Victorsitou Feb 21, 2023
af3f09c
fix: docs
Victorsitou Feb 21, 2023
5c52ba9
docs: this doesn't need permission
Victorsitou Feb 21, 2023
f57a29d
get -> fetch
Victorsitou Feb 26, 2023
2646122
Merge remote-tracking branch 'upstream/master' into feat/guild-onboar…
Victorsitou Mar 2, 2023
a7846ef
refactor: use `emoji._to_partial().to_dict()` instead
Victorsitou Mar 2, 2023
a62cd2a
misc: remove some todos/notes
Victorsitou Mar 2, 2023
7bce38c
fix: don't store useless id
Victorsitou Mar 2, 2023
90d9813
fix: improve code
Victorsitou Mar 2, 2023
cf92da3
feat: use Guild.get_channel instead of private attribute
Victorsitou Mar 2, 2023
2c704bd
feat: add `roles` and `channels` attributes to `OnboardingPromptOption`
Victorsitou Mar 2, 2023
4b0f2b3
tests: change OnboardingPrompt fixture
Victorsitou Mar 2, 2023
7e4998d
fix: this doesn't need state anymore
Victorsitou Mar 2, 2023
4659744
feat: remove useless x-super-properties
Victorsitou Mar 2, 2023
8782692
docs: apply suggestions
Victorsitou Mar 2, 2023
091d1a8
lint: fix pyright
Victorsitou Mar 2, 2023
2f4bcaa
Merge branch 'feat/guild-onboarding' of https://github.com/Victorsito…
Victorsitou Mar 2, 2023
61ddcf4
changelog: get -> fetch
Victorsitou Mar 3, 2023
1d8cd92
feat: use `_emoji_from_name_id` instead
Victorsitou Mar 3, 2023
6aa5f63
Merge branch 'master' into feat/guild-onboarding
Victorsitou Mar 3, 2023
53762b3
Merge branch 'master' into feat/guild-onboarding
Victorsitou Mar 8, 2023
5afb86a
tests: remove repr test
Victorsitou Mar 8, 2023
b16f9b5
docs: remove duplicate description
Victorsitou Mar 8, 2023
d12d625
Merge branch 'feat/guild-onboarding' of https://github.com/Victorsito…
Victorsitou Mar 8, 2023
d71866d
docs: update
Victorsitou Apr 1, 2023
2b81f4c
revert!: audit logs :c
Victorsitou Apr 1, 2023
eda527e
Merge remote-tracking branch 'upstream/master' into feat/guild-onboar…
Victorsitou Apr 1, 2023
98daea9
tests: fix test
Victorsitou Apr 1, 2023
aa528cf
docs: update
Victorsitou Apr 1, 2023
dae932b
feat: apply code suggestions
Victorsitou Apr 1, 2023
4228c60
refactor: make emoji handling easier
Victorsitou Apr 1, 2023
07d839b
types: make OnboardingPromptOption.description optional
Victorsitou Apr 1, 2023
52a487c
move stuff
Victorsitou Apr 1, 2023
7466f3a
refactor: avoid `_from_data` and create object in `__init__`
Victorsitou Apr 1, 2023
e08cf9f
test: update test
Victorsitou Apr 1, 2023
cfab72d
misc: minor fixes
Victorsitou Apr 1, 2023
7a1e2b2
chore: rename `roles_ids`/`channels_ids`
shiftinv Apr 1, 2023
c2ce2cc
docs: move stuff to Discord Model
Victorsitou Apr 2, 2023
d9d0c00
typing: add type annotations
Victorsitou Apr 2, 2023
7dd172f
Merge branch 'feat/guild-onboarding' of https://github.com/Victorsito…
Victorsitou Apr 2, 2023
90b746c
tests: fix text
Victorsitou Apr 2, 2023
b70ba05
chore: run codemod
Victorsitou Apr 2, 2023
adc4b01
Apply suggestions from code review
Victorsitou Apr 2, 2023
034b360
Merge remote-tracking branch 'upstream/master' into feat/guild-onboar…
Victorsitou Apr 8, 2023
32b870c
docs: update docs
Victorsitou Apr 8, 2023
9fc5769
Merge branch 'feat/guild-onboarding' of https://github.com/Victorsito…
Victorsitou Apr 8, 2023
59cf67b
docs: match docs of the same stuff
Victorsitou Apr 8, 2023
39790ac
docs: update docs
Victorsitou Apr 8, 2023
e46d50a
Merge remote-tracking branch 'upstream/master' into feat/guild-onboar…
Victorsitou Apr 17, 2023
202d52e
feat: rename `fetch_onboarding`
Victorsitou Apr 21, 2023
ac805a5
chore: empty commit to hopefully fix github
shiftinv Apr 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/928.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Implement Onboarding.
- Add :class:`Onboarding`, :class:`OnboardingPrompt` and :class:`OnboardingPromptOption`.
- Add :meth:`Guild.fetch_onboarding`.
Victorsitou marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions disnake/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from .mentions import *
from .message import *
from .object import *
from .onboarding import *
from .partial_emoji import *
from .permissions import *
from .player import *
Expand Down
6 changes: 6 additions & 0 deletions disnake/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"ThreadLayout",
"Event",
"ApplicationRoleConnectionMetadataType",
"OnboardingPromptType",
)


Expand Down Expand Up @@ -1294,6 +1295,11 @@ class ApplicationRoleConnectionMetadataType(Enum):
boolean_not_equal = 8


class OnboardingPromptType(Enum):
multiple_choice = 0
dropdown = 1


T = TypeVar("T")


Expand Down
21 changes: 21 additions & 0 deletions disnake/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
from .iterators import AuditLogIterator, BanIterator, MemberIterator
from .member import Member, VoiceState
from .mixins import Hashable
from .onboarding import Onboarding
from .partial_emoji import PartialEmoji
from .permissions import PermissionOverwrite
from .role import Role
Expand Down Expand Up @@ -4632,6 +4633,26 @@ async def create_automod_rule(
)
return AutoModRule(data=data, guild=self)

async def fetch_onboarding(self) -> Onboarding:
Victorsitou marked this conversation as resolved.
Show resolved Hide resolved
"""|coro|

Retrieves the guild onboarding data.

.. versionadded:: 2.9

Raises
------
HTTPException
Retrieving the guild onboarding data failed.

Returns
-------
:class:`Onboarding`
The guild onboarding data.
"""
data = await self._state.http.get_guild_onboarding(self.id)
return Onboarding(data=data, guild=self)


PlaceholderID = NewType("PlaceholderID", int)

Expand Down
8 changes: 8 additions & 0 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
invite,
member,
message,
onboarding,
role,
sticker,
template,
Expand Down Expand Up @@ -2167,6 +2168,8 @@ def edit_guild_welcome_screen(
r = Route("PATCH", "/guilds/{guild_id}/welcome-screen", guild_id=guild_id)
return self.request(r, json=payload, reason=reason)

# Auto moderation

def get_auto_moderation_rules(self, guild_id: Snowflake) -> Response[List[automod.AutoModRule]]:
return self.request(
Route("GET", "/guilds/{guild_id}/auto-moderation/rules", guild_id=guild_id)
Expand Down Expand Up @@ -2256,6 +2259,11 @@ def delete_auto_moderation_rule(
reason=reason,
)

# Guild Onboarding

def get_guild_onboarding(self, guild_id: Snowflake) -> Response[onboarding.Onboarding]:
return self.request(Route("GET", "/guilds/{guild_id}/onboarding", guild_id=guild_id))

# Application commands (global)

def get_global_commands(
Expand Down
201 changes: 201 additions & 0 deletions disnake/onboarding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations

from typing import TYPE_CHECKING, FrozenSet, List, Optional, Union

from .enums import OnboardingPromptType, try_enum
from .mixins import Hashable

if TYPE_CHECKING:
from .emoji import Emoji, PartialEmoji
from .guild import Guild, GuildChannel
from .role import Role
from .types.onboarding import (
Onboarding as OnboardingPayload,
OnboardingPrompt as OnboardingPromptPayload,
OnboardingPromptOption as OnboardingPromptOptionPayload,
)

__all__ = (
"Onboarding",
"OnboardingPrompt",
"OnboardingPromptOption",
)


class Onboarding:
"""Represents a guild onboarding object.

.. versionadded:: 2.9

Attributes
----------
guild: :class:`Guild`
The guild this onboarding is part of.
prompts: List[:class:`OnboardingPrompt`]
The prompts shown during onboarding and in community customization.
enabled: :class:`bool`
Whether onboarding is enabled.
default_channel_ids: FrozenSet[:class:`int`]
The IDs of the channels that will automatically be shown to new members.
"""

__slots__ = (
"guild",
"prompts",
"enabled",
"default_channel_ids",
)

def __init__(self, *, guild: Guild, data: OnboardingPayload) -> None:
self.guild: Guild = guild
self._from_data(data)

def _from_data(self, data: OnboardingPayload) -> None:
self.prompts: List[OnboardingPrompt] = [
OnboardingPrompt(data=prompt, guild=self.guild) for prompt in data["prompts"]
]
self.enabled: bool = data["enabled"]
self.default_channel_ids: FrozenSet[int] = (
frozenset(map(int, exempt_channels))
if (exempt_channels := data["default_channel_ids"])
else frozenset()
)

def __repr__(self) -> str:
return (
f"<Onboarding guild={self.guild!r} prompts={self.prompts!r} enabled={self.enabled!r}>"
)

@property
def default_channels(self) -> List[GuildChannel]:
"""List[:class:`abc.GuildChannel`]: The list of channels that will be shown to new members by default."""
return list(filter(None, map(self.guild.get_channel, self.default_channel_ids)))


class OnboardingPrompt(Hashable):
"""Represents an onboarding prompt.

.. versionadded:: 2.9

Attributes
----------
id: :class:`int`
The onboarding prompt's ID.
type: :class:`OnboardingPromptType`
The onboarding prompt's type.
options: List[:class:`OnboardingPromptOption`]
The onboarding prompt's options.
title: :class:`str`
The onboarding prompt's title.
single_select: :class:`bool`
Whether users are limited to selecting one option for the prompt.
required: :class:`bool`
Whether the prompt is required before a user completes the onboarding flow.
in_onboarding: :class:`bool`
Whether the prompt is present in the onboarding flow.
If ``False``, the prompt will only appear in community customization.
"""

__slots__ = (
"guild",
"id",
"title",
"options",
"single_select",
"required",
"in_onboarding",
"type",
)

def __init__(self, *, guild: Guild, data: OnboardingPromptPayload) -> None:
self.guild = guild

self.id: int = int(data["id"])
self.title: str = data["title"]
self.options: List[OnboardingPromptOption] = [
OnboardingPromptOption(data=option, guild=guild) for option in data["options"]
]
self.single_select: bool = data["single_select"]
self.required: bool = data["required"]
self.in_onboarding: bool = data["in_onboarding"]
self.type: OnboardingPromptType = try_enum(OnboardingPromptType, data["type"])

def __str__(self) -> str:
return self.title

def __repr__(self) -> str:
return (
f"<OnboardingPrompt id={self.id!r} title={self.title!r} options={self.options!r}"
f" single_select={self.single_select!r} required={self.required!r}"
f" in_onboarding={self.in_onboarding!r} type={self.type!r}>"
)


class OnboardingPromptOption(Hashable):
Victorsitou marked this conversation as resolved.
Show resolved Hide resolved
"""Represents an onboarding prompt option.

.. versionadded:: 2.9

Attributes
----------
id: :class:`int`
The prompt option's ID.
emoji: Optional[Union[:class:`PartialEmoji`, :class:`Emoji`]]
Victorsitou marked this conversation as resolved.
Show resolved Hide resolved
The prompt option's emoji.
title: :class:`str`
The prompt option's title.
description: Optional[:class:`str`]
The prompt option's description.
role_ids: FrozenSet[:class:`int`]
The IDs of the roles that will be added to the user when they select this option.
channel_ids: FrozenSet[:class:`int`]
The IDs of the channels that will be shown to the user when they select this option.
"""

__slots__ = ("id", "title", "description", "emoji", "guild", "role_ids", "channel_ids")

def __init__(self, *, guild: Guild, data: OnboardingPromptOptionPayload) -> None:
# NOTE: The ID may sometimes be a UNIX timestamp since
# Onboarding changes are saved locally until you send the API request (that's how it works in client)
# so the API needs the timestamp to know what ID it needs to create, should we just add a note about it
# or "try" to create the ID ourselves?
# I'm not sure if this also happens for OnboardingPrompt
Victorsitou marked this conversation as resolved.
Show resolved Hide resolved
self.guild: Guild = guild

self.id: int = int(data["id"])
self.title: str = data["title"]
self.description: Optional[str] = data["description"]
self.role_ids: FrozenSet[int] = (
frozenset(map(int, roles_ids)) if (roles_ids := data.get("role_ids")) else frozenset()
)
self.channel_ids: FrozenSet[int] = (
frozenset(map(int, channels_ids))
if (channels_ids := data.get("channel_ids"))
else frozenset()
)

self.emoji: Optional[Union[Emoji, PartialEmoji]]
Victorsitou marked this conversation as resolved.
Show resolved Hide resolved
if emoji_data := data.get("emoji"):
self.emoji = guild._state.get_reaction_emoji(emoji_data)
else:
self.emoji = None

def __str__(self) -> str:
return self.title

def __repr__(self) -> str:
return (
f"<OnboardingPromptOption id={self.id!r} title={self.title!r} "
f"description={self.description!r} emoji={self.emoji!r}>"
)

@property
def roles(self) -> List[Role]:
"""List[:class:`Role`]: A list of roles that will be added to the user when they select this option."""
return list(filter(None, map(self.guild.get_role, self.role_ids)))

@property
def channels(self) -> List[GuildChannel]:
"""List[:class:`abc.GuildChannel`]: A list of channels that the user will see when they select this option."""
return list(filter(None, map(self.guild.get_channel, self.channel_ids)))
4 changes: 2 additions & 2 deletions disnake/partial_emoji.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def _emoji_to_name_id(
# utility method for unusual emoji model in forums
@staticmethod
def _emoji_from_name_id(
name: Optional[str], id: Optional[int], *, state: ConnectionState
name: Optional[str], id: Optional[int], animated: bool = False, *, state: ConnectionState
) -> Optional[Union[Emoji, PartialEmoji]]:
if not (name or id):
return None
Expand All @@ -286,6 +286,6 @@ def _emoji_from_name_id(
# This may change in a future API version, but for now we'll just have to accept it.
name=name or "",
id=id,
# `animated` is unknown but presumably we already got the (animated) emoji from the guild cache at this point
animated=animated,
)
return emoji
34 changes: 34 additions & 0 deletions disnake/types/onboarding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SPDX-License-Identifier: MIT

from typing import List, Literal, Optional, TypedDict

from .emoji import Emoji
from .snowflake import Snowflake, SnowflakeList

OnboardingPromptType = Literal[0, 1]


class OnboardingPromptOption(TypedDict):
id: Snowflake
title: str
description: Optional[str]
emoji: Emoji
role_ids: SnowflakeList
channel_ids: SnowflakeList


class OnboardingPrompt(TypedDict):
id: Snowflake
title: str
options: List[OnboardingPromptOption]
single_select: bool
required: bool
in_onboarding: bool
type: OnboardingPromptType


class Onboarding(TypedDict):
guild_id: Snowflake
prompts: List[OnboardingPrompt]
default_channel_ids: SnowflakeList
enabled: bool
Loading