diff --git a/discord/__init__.py b/discord/__init__.py index d6031ce3ac..8183780eb5 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -45,6 +45,7 @@ from .file import * from .flags import * from .guild import * +from .guild_builder import * from .http import * from .integrations import * from .interactions import * diff --git a/discord/client.py b/discord/client.py index 4db53e33e3..3efd1be964 100644 --- a/discord/client.py +++ b/discord/client.py @@ -42,16 +42,23 @@ from .backoff import ExponentialBackoff from .channel import PartialMessageable, _threaded_channel_factory from .emoji import AppEmoji, GuildEmoji -from .enums import ChannelType, Status +from .enums import ( + ChannelType, + ContentFilter, + NotificationLevel, + Status, + VerificationLevel, +) from .errors import * from .flags import ApplicationFlags, Intents from .gateway import * from .guild import Guild +from .guild_builder import GuildBuilder from .http import HTTPClient from .invite import Invite from .iterators import EntitlementIterator, GuildIterator from .mentions import AllowedMentions -from .monetization import SKU, Entitlement +from .monetization import SKU from .object import Object from .stage_instance import StageInstance from .state import ConnectionState @@ -1597,8 +1604,12 @@ async def create_guild( self, *, name: str, - icon: bytes = MISSING, + icon: bytes | None = MISSING, code: str = MISSING, + verification_level: VerificationLevel = MISSING, + content_filter: ContentFilter = MISSING, + notification_level: NotificationLevel = MISSING, + afk_timeout: int = MISSING, ) -> Guild: """|coro| @@ -1606,6 +1617,8 @@ async def create_guild( Bot accounts in more than 10 guilds are not allowed to create guilds. + Also see :meth:`.Client.build_guild`. + Parameters ---------- name: :class:`str` @@ -1617,30 +1630,108 @@ async def create_guild( The code for a template to create the guild with. .. versionadded:: 1.4 + verification_level: :class:`VerificationLevel` + The verification level. + + .. versionadded:: 2.7 + content_filter: :class:`ContentFilter` + The explicit content filter level. + + .. versionadded:: 2.7 + notification_level: :class:`NotificationLevel` + The default message notification level. + + .. versionadded:: 2.7 + afk_timeout: :class:`int` + The afk timeout in seconds. + + .. versionadded:: 2.7 Returns ------- :class:`.Guild` The guild created. This is not the same guild that is added to cache. - - Raises - ------ - :exc:`HTTPException` - Guild creation failed. - :exc:`InvalidArgument` - Invalid icon image format given. Must be PNG or JPG. """ - if icon is not MISSING: - icon_base64 = utils._bytes_to_base64_data(icon) - else: - icon_base64 = None + if code is MISSING: + code = None # type: ignore - if code: - data = await self.http.create_from_template(code, name, icon_base64) - else: - data = await self.http.create_guild(name, icon_base64) - return Guild(data=data, state=self._connection) + metadata = {} + + if verification_level is not MISSING: + metadata["verification_level"] = verification_level.value + if content_filter is not MISSING: + metadata["explicit_content_filter"] = content_filter.value + if notification_level is not MISSING: + metadata["default_message_notifications"] = notification_level.value + if afk_timeout is not MISSING: + metadata["afk_timeout"] = afk_timeout + + # TODO: remove support of passing ``None`` to ``icon``. + if icon is None: + icon = MISSING + + builder = GuildBuilder( + state=self._connection, name=name, icon=icon, code=code, metadata=metadata # type: ignore + ) + return await builder + + def build_guild( + self, + *, + name: str, + icon: bytes = MISSING, + verification_level: VerificationLevel = MISSING, + content_filter: ContentFilter = MISSING, + notification_level: NotificationLevel = MISSING, + afk_timeout: int = MISSING, + ) -> GuildBuilder: + """Creates a :class:`.GuildBuilder` object to create a guild. + + Also see :meth:`.Client.create_guild`. + + .. versionadded:: 2.7 + + Parameters + ---------- + name: :class:`str` + The guild name. + icon: :class:`bytes` + The :term:`py:bytes-like object` representing the icon. See :meth:`.ClientUser.edit` + for more details on what is expected. + verification_level: :class:`VerificationLevel` + The verification level. + content_filter: :class:`ContentFilter` + The explicit content filter level. + notification_level: :class:`NotificationLevel` + The defualt message notification level. + afk_timeout: :class:`int` + The afk timeout in seconds. + + Returns + ------- + :class:`GuildBuilder` + The guild builder to create the new guild. + """ + + metadata = {} + + if verification_level is not MISSING: + metadata["verification_level"] = verification_level.value + if content_filter is not MISSING: + metadata["explicit_content_filter"] = content_filter.value + if notification_level is not MISSING: + metadata["default_message_notifications"] = notification_level.value + if afk_timeout is not MISSING: + metadata["afk_timeout"] = afk_timeout + + return GuildBuilder( + state=self._connection, + name=name, + icon=icon, + code=None, + metadata=metadata, + ) async def fetch_stage_instance(self, channel_id: int, /) -> StageInstance: """|coro| diff --git a/discord/guild_builder.py b/discord/guild_builder.py new file mode 100644 index 0000000000..2ba9cdf719 --- /dev/null +++ b/discord/guild_builder.py @@ -0,0 +1,788 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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, Any, Coroutine, Generator, overload + +from . import utils +from .abc import Snowflake +from .colour import Color, Colour +from .emoji import Emoji, PartialEmoji +from .enums import ChannelType, VoiceRegion +from .flags import SystemChannelFlags +from .guild import Guild +from .permissions import PermissionOverwrite, Permissions +from .state import ConnectionState + +if TYPE_CHECKING: + from .types.guild import GuildCreate as GuildCreatePayload + from .types.role import Role as RolePayload + +MISSING = utils.MISSING + +__all__ = ( + "GuildBuilder", + "GuildBuilderChannel", + "GuildBuilderRole", +) + + +class GuildBuilder: + """Represents a Discord guild that is yet to be created. + + This is returned by :meth:`Client.create_guild` to allow users to modify the guild + properties before creating it. + + .. versionadded:: 2.7 + + .. container:: operations + + .. describe:: await x + + Creates the guild and returns a :class:`Guild` object. + + .. describe:: x() + + Returns a :ref:`coroutine ` that, when awaited, + returns the new :class:`Guild` object. + + Attributes + ---------- + code: Optional[:class:`str`] + The guild template code that the guild is going to be created with. + afk_channel_id: Optional[:class:`int`] + The AFK channel ID. Defaults to ``None``. + system_channel_id: Optional[:class:`int`] + The system channel ID. Defaults to ``None``. + system_channel_flags: Optional[:class:`SystemChannelFlags`] + The system channel flags. Defaults to ``None``. This requires :attr:`.system_channel_id` to be + not ``None``. + """ + + __slots__ = ( + "_state", + "_name", + "_icon", + "_channels", + "_roles", + "_metadata", + "afk_channel_id", + "system_channel_id", + "system_channel_flags", + "code", + ) + + def __init__( + self, + *, + state: ConnectionState, + name: str, + icon: bytes, + code: str | None, + metadata: dict[str, Any], + ) -> None: + self._state: ConnectionState = state + self._name: str = name + self._icon: bytes = icon + self._channels: dict[int, GuildBuilderChannel] = {} + self._roles: dict[int, GuildBuilderRole] = {} + self._metadata: dict[str, Any] = metadata + self.code: str | None = code + self.afk_channel_id: int | None = None + self.system_channel_id: int | None = None + self.system_channel_flags: SystemChannelFlags | None = None + + async def _do_create(self) -> Guild: + http = self._state.http + if self.code is not None: + data = await http.create_from_template( + self.code, + self.name, + utils._bytes_to_base64_data(self.icon) if self.icon else None, + ) + return Guild(data=data, state=self._state) + + payload: GuildCreatePayload = { + "name": self.name, + } + payload.update(self._metadata) # type: ignore + + if self._icon is not MISSING: + payload["icon"] = utils._bytes_to_base64_data(self._icon) + if self._channels is not MISSING: + payload["channels"] = [ch.to_dict() for ch in self._channels.values()] + if self._roles is not MISSING: + payload["roles"] = [role.to_dict() for role in self._roles.values()] + if self.afk_channel_id is not None: + payload["afk_channel_id"] = self.afk_channel_id + if self.system_channel_id is not None: + payload["system_channel_id"] = self.system_channel_id + if self.system_channel_flags is not None: + payload["system_channel_flags"] = self.system_channel_flags.value + + data = await http.create_guild(payload) + return Guild(data=data, state=self._state) + + def __await__(self) -> Generator[Any, Any, Guild]: + return self._do_create().__await__() + + def __call__(self) -> Coroutine[Any, Any, Guild]: + return self._do_create() + + @property + def channels(self) -> list[GuildBuilderChannel]: + """List[:class:`GuildBuilderChannel`]: Returns a read-only list containing all the + channels that are going to be created along the guild. + """ + if self._channels is MISSING: + return [] + return list(self._channels.values()) + + @property + def roles(self) -> list[GuildBuilderRole]: + """List[:class:`GuildBuilderRole`]: Returns a read-only list containing all the roles + that are going to be created along the guild. + """ + if self._roles is MISSING: + return [] + return list(self._roles.values()) + + @property + def name(self) -> str: + """:class:`str`: Returns the name of the guild that is going to be created.""" + return self._name + + @property + def icon(self) -> bytes | None: + """Optional[:class:`bytes`]: Returns the icon of the guild that is going to be created.""" + return self._icon if self._icon is not MISSING else None + + @overload + def add_channel( + self, + /, + *, + name: str, + type: ChannelType, + topic: str | None = None, + overwrites: dict[Snowflake, PermissionOverwrite] = ..., + nsfw: bool = ..., + category_id: int | None = None, + bitrate: int = ..., + user_limit: int = ..., + slowmode_delay: int = ..., + rtc_region: VoiceRegion = ..., + ) -> GuildBuilderChannel: ... + + @overload + def add_channel( + self, + channel: GuildBuilderChannel, + /, + ) -> GuildBuilderChannel: ... + + def add_channel( + self, + channel: GuildBuilderChannel | None = None, + /, + *, + name: str = MISSING, + type: ChannelType = MISSING, + topic: str | None = None, + overwrites: dict[Snowflake, PermissionOverwrite] = MISSING, + nsfw: bool = MISSING, + category_id: int | None = None, + bitrate: int = MISSING, + user_limit: int = MISSING, + slowmode_delay: int = MISSING, + rtc_region: VoiceRegion = MISSING, + ) -> GuildBuilderChannel: + """Adds a channel to the current guild. + + Parameters + ---------- + channel: :class:`GuildBuilderChannel` + The constructed channel object to add. If not provided you must provide + ``name`` and ``type``, and optionally any other parameter. + name: :class:`str` + The channel name. This is required if ``channel`` is not provided. + type: :class:`ChannelType` + The channel type. This is required if ``channel`` is not provided. + topic: Optional[:class:`str`] + The channel topic. + overwrites: Dict[:class:`~discord.abc.Snowflake`, :class:`PermissionOverwrite`] + The channel overwrites. + nsfw: :class:`bool` + Whether the channel is NSFW flagged. + category_id: Optional[:class:`int`] + The category placeholder ID this channel belongs to. + bitrate: :class:`int` + The channel bitrate. + user_limit: :class:`int` + The channel limit for number of members that can be connected on the channel. + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages in this channel. + A value of ``0`` denotes that it is disabled. + rtc_region: :class:`VoiceRegion` + The channel voice region. + + Returns + ------- + :class:`GuildBuilderChannel` + The channel. + """ + + if channel is None and name is MISSING and type is MISSING: + raise ValueError('You must either provide "channel" or "name" and "type".') + elif channel is not None and (name is not MISSING or type is not MISSING): + raise ValueError( + '"name" and "type" are not allowed if "channel" is provided.' + ) + elif channel is None and ( + (name is MISSING and topic is not MISSING) + or (name is not MISSING and type is MISSING) + ): + raise ValueError( + '"name" and "type" are required arguments if "channel" is not provided.' + ) + elif channel is not None and name is MISSING and type is MISSING: + if channel.id in self._channels: + raise ValueError( + f"A channel with ID {channel.id} already exists, make sure IDs are unique." + ) + + self._channels[channel.id] = channel + return channel + + id = len(self._channels) + 1 + channel = self._channels[id] = GuildBuilderChannel( + id=id, + name=name, + type=type, + topic=topic, + nsfw=nsfw, + category_id=category_id, + overwrites=overwrites, + bitrate=bitrate, + user_limit=user_limit, + slowmode_delay=slowmode_delay, + rtc_region=rtc_region, + ) + return channel + + def _create_default_role(self) -> GuildBuilderRole: + return GuildBuilderRole( + id=0, + name="everyone", + hoisted=True, + mentionable=True, + permissions=Permissions.text(), + colour=Colour(0), + icon=None, + ) + + def get_role(self, id: int, /) -> GuildBuilderRole | None: + """Returns the role with the provided id. + + Parameters + ---------- + id: :class:`int` + The ID of the role. If this is ``0`` it will return the default + role. + + .. note:: + + If you want to edit the default role you should use :meth:`.edit_default_role_permissions` + instead. + + Returns + ------- + Optional[:class:`GuildBuilderRole`] + The found role, or ``None``. + """ + if id == 0: + default_role = self._roles.get(id) + if not default_role: + default_role = self._roles[0] = self._create_default_role() + return default_role + return self._roles.get(id) + + def edit_default_role_permissions( + self, + perms_obj: Permissions | None = None, + **perms: bool, + ) -> GuildBuilderRole: + """Edits the default role permissions and returns it. + + Parameters + ---------- + perms_obj: Optional[:class:`Permissions`] + The permissions object. This is merged with ``perms``, if provided. + **perms: :class:`bool` + The permissions to allow or deny. This is merged with ``perms_obj``, if provided. + + Returns + ------- + :class:`GuildBuilderRole` + The default role. + """ + + default_role = self._roles.get(0) + + if default_role is None: + default_role = self._roles[0] = self._create_default_role() + + resolved = Permissions(**perms) + if perms_obj is not None: + resolved |= perms_obj + + default_role.permissions = resolved + return default_role + + def add_role( + self, + name: str, + *, + permissions: Permissions = MISSING, + hoisted: bool = False, + mentionable: bool = True, + colour: Colour | None = MISSING, + color: Color | None = MISSING, + icon: bytes | None = None, + emoji: str | PartialEmoji | Emoji | None = None, + ) -> GuildBuilderRole: + """Adds a role to the current guild. + + Parameters + ---------- + name: :class:`str` + The role name. + permissions: :class:`Permissions` + The role permissions. + hoisted: :class:`bool` + Whether the role members are displayed separately in the sidebar. + Defaults to ``False``. + mentionable: :class:`bool` + Whether everyone can mention this role. Defaults to ``True``. + colour: :class:`Colour` + The role colour. + color: :class:`Color` + The role color. This is an alias for ``colour``. + icon: Optional[:class:`bytes`] + The role icon. + emoji: Optional[Union[:class:`str`, :class:`PartialEmoji`, :class:`Emoji`]] + The role displayed emoji. + + Returns + ------- + :class:`GuildBuilderRole` + The role. + """ + + if not len(self._roles) > 1: + # If we add this role to the 0 index then we are editing + # the default role. + self._roles[0] = self._create_default_role() + + id = len(self._roles) + 1 + + if colour is not MISSING and color is not MISSING: + raise TypeError("Cannot provide both colour and color") + + resolved_colour = colour if colour is not MISSING else color + + if resolved_colour in (MISSING, None): + resolved_colour = Colour(0) + + if permissions is MISSING: + permissions = self._roles[0].permissions + + role = self._roles[id] = GuildBuilderRole( + id=id, + name=name, + hoisted=hoisted, + mentionable=mentionable, + permissions=permissions, + colour=resolved_colour, + icon=icon, + ) + role.emoji = emoji + return role + + +class GuildBuilderChannel: + """Represents a :class:`GuildBuilder` channel. + + .. versionadded:: 2.7 + + Parameters + ---------- + id: :class:`int` + The placeholder channel ID. + name: :class:`str` + The channel name. + type: :class:`ChannelType` + The channel type. + topic: Optional[:class:`str`] + The channel topic. + nsfw: :class:`bool` + Whether this channel is NSFW flagged. Defaults to ``False``. + category_id: Optional[:class:`int`] + The category placeholder ID this channel belongs to. + overwrites: Dict[:class:`~discord.abc.Snowflake`, :class:`PermissionOverwrite`] + The overwrites of this channel. The keys are expected to be roles placholder IDs and + will be treated as it. + bitrate: :class:`int` + The channel bitrate. + user_limit: :class:`int` + The channel limit for number of members that can be connected on the channel. + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages in this channel. + A value of ``0`` denotes that it is disabled. + rtc_region: :class:`VoiceRegion` + The channel voice region. + + Attributes + ---------- + id: :class:`int` + The placeholder channel ID. + name: :class:`str` + The channel name. + type: :class:`ChannelType` + The channel type. + topic: Optional[:class:`str`] + The channel topic. + nsfw: :class:`bool` + Whether the channel is NSFW flagged. + category_id: Optional[:class:`int`] + The category placeholder ID this channel belongs to. + """ + + __slots__ = ( + "id", + "name", + "type", + "topic", + "nsfw", + "category_id", + "_metadata", + ) + + def __init__( + self, + id: int, + name: str, + type: ChannelType, + topic: str | None, + *, + nsfw: bool = False, + category_id: int | None = None, + overwrites: dict[Snowflake, PermissionOverwrite] = MISSING, + bitrate: int = MISSING, + user_limit: int = MISSING, + slowmode_delay: int = MISSING, + rtc_region: VoiceRegion = MISSING, + ) -> None: + self.id: int = id + self.name: str = name + self.type: ChannelType = type + self.topic: str | None = topic + self.nsfw: bool = nsfw + self.category_id: int | None = category_id + + metadata = {} + + if overwrites is not MISSING: + metadata["overwrites"] = overwrites + if bitrate is not MISSING: + if self.type not in (ChannelType.voice, ChannelType.stage_voice): + raise ValueError("cannot set a bitrate to a non-voice channel") + + metadata["bitrate"] = bitrate + if user_limit is not MISSING: + if self.type not in (ChannelType.voice, ChannelType.stage_voice): + raise ValueError("cannot set a user_limit to a non-voice channel") + + metadata["user_limit"] = user_limit + if slowmode_delay is not MISSING and slowmode_delay != 0: + metadata["rate_limit_per_user"] = slowmode_delay + if rtc_region is not MISSING: + if self.type not in (ChannelType.voice, ChannelType.stage_voice): + raise ValueError("cannot set a rtc_region to a non-voice channel") + + metadata["rtc_region"] = rtc_region + + self._metadata: dict[str, Any] = metadata + + @property + def overwrites(self) -> dict[Snowflake, PermissionOverwrite]: + """Dict[:class:`Object`, :class:`PermissionOverwrite`]: Returns this channel's role overwrites.""" + return self._metadata.get("overwrites", {}) + + @overwrites.setter + def overwrites(self, ow: dict[Snowflake, PermissionOverwrite] | None) -> None: + if ow is not None: + self._metadata["ovewrites"] = ow + else: + self._metadata.pop("overwrites", None) + + @property + def bitrate(self) -> int | None: + """Optional[:class:`int`]: Returns the channel bitrate. + + This will return ``None`` if :attr:`GuildBuilderChannel.type` is not + :attr:`ChannelType.voice` or :attr:`ChannelType.stage_voice`. + """ + return self._metadata.get("bitrate") + + @bitrate.setter + def bitrate(self, value: int | None) -> None: + if self.type not in (ChannelType.voice, ChannelType.stage_voice): + raise ValueError("cannot set a bitrate to a non-voice channel") + + if value is not None: + self._metadata["bitrate"] = value + else: + self._metadata.pop("bitrate", None) + + @property + def user_limit(self) -> int | None: + """Optional[:class:`int`]: Returns the channel limit for number of members that + can be connected in the channel. + + This will return ``None`` if :attr:`GuildBuilderChannel.type` is not + :attr:`ChannelType.voice` or :attr:`ChannelType.stage_voice`. + """ + return self._metadata.get("user_limit") + + @user_limit.setter + def user_limit(self, value: int | None) -> None: + if self.type not in (ChannelType.voice, ChannelType.stage_voice): + raise ValueError("cannot set a user_limit to a non-voice channel") + + if value is not None: + self._metadata["user_limit"] = value + else: + self._metadata.pop("user_limit", None) + + @property + def slowmode_delay(self) -> int: + """:class:`int`: The number of seconds a member must wait between sending messages + in this channel. A value of ``0`` denotes that it is disabled. + """ + return self._metadata.get("rate_limit_per_user", 0) + + @slowmode_delay.setter + def slowmode_delay(self, value: int | None) -> None: + if value is not None and value != 0: + self._metadata["rate_limit_per_user"] = value + else: + self._metadata.pop("rate_limit_per_user", None) + + @property + def rtc_region(self) -> VoiceRegion | None: + """Optional[:class:`VoiceRegion`]: Returns the channel voice region. + + This will return ``None`` if :attr:`GuildBuilderChannel.type` is not + :attr:`ChannelType.voice` or :attr:`ChannelType.stage_voice`. + """ + return self._metadata.get("rtc_region") + + @rtc_region.setter + def rtc_region(self, region: VoiceRegion | None) -> None: + if self.type not in (ChannelType.voice, ChannelType.stage_voice): + raise ValueError("cannot set rtc_region to a non-voice channel") + + if region is not None: + self._metadata["rtc_region"] = region + else: + self._metadata.pop("rtc_region", None) + + def to_dict(self) -> dict[str, Any]: + payload = { + "id": str(self.id), + "name": self.name, + "type": self.type.value, + "nsfw": self.nsfw, + "topic": self.topic, + } + + overwrites: dict[Snowflake, PermissionOverwrite] | None = self._metadata.get( + "overwrites", None + ) + if overwrites is not None: + pairs = [] + + for target, ow in overwrites.items(): + allowed, denied = ow.pair() + pairs.append( + { + "target": target.id, + "type": 0, # role, + "allow": allowed.value, + "deny": denied.value, + }, + ) + + payload["permission_overwrites"] = pairs + + if bitrate := self._metadata.get("bitrate"): + payload["bitrate"] = bitrate + + if user_limit := self._metadata.get("user_limit"): + payload["user_limit"] = user_limit + + if slowmode := self._metadata.get("rate_limit_per_user"): + payload["rate_limit_per_user"] = slowmode + + if rtc_region := self._metadata.get("rtc_region"): + payload["rtc_region"] = rtc_region.value + + # TODO: add more fields + + return payload + + +class GuildBuilderRole: + """Represents a :class:`GuildBuilder` role. + + .. versionadded:: 2.7 + + Attributes + ---------- + id: :class:`int` + The placeholder role ID. + name: :class:`str` + The role name. + hoisted: :class:`bool` + Whether the role is hoisted. + mentionable: :class:`bool` + Whether the role is mentionable. + permissions: :class:`Permissions` + The permissions of this role. + icon: Optional[:class:`bytes`] + The emoji icon. + default: Optional[:class:`bool`] + Whether this role is the default role. If ``None`` it is autocalculated. + + .. note:: + + If a role is the default one, **you can only edit the permissions**. + """ + + __slots__ = ( + "id", + "name", + "hoisted", + "mentionable", + "permissions", + "_colour", + "icon", + "_unicode_emoji", + "default", + ) + + # TODO: edit __init__ to allow users to create the object without requiring GuildBuilder.add_role + def __init__( + self, + id: int, + name: str, + hoisted: bool, + mentionable: bool, + permissions: Permissions, + colour: Colour, + icon: bytes | None, + default: bool | None = None, + ) -> None: + self.id: int = id + self.name: str = name + self.hoisted: bool = hoisted + self.mentionable: bool = mentionable + self.permissions: Permissions = permissions + self._colour: int = colour.value + self.icon: bytes | None = icon + self._unicode_emoji: str | None = None + self.default: bool | None = default + + @property + def colour(self) -> Colour: + """:class:`Colour`: Returns this role colour. There is an alias for this named :attr:`.color`.""" + return Colour(self._colour) + + @colour.setter + def colour(self, value: Colour | None) -> None: + if value is not None: + self._colour = value.value + else: + self._colour = 0 + + @property + def color(self) -> Color: + """:class:`Color`: Returns this role color. There is an alias for this named :attr:`.colour`.""" + return self.colour + + @color.setter + def color(self, value: Color | None) -> None: + self.colour = value + + @property + def emoji(self) -> PartialEmoji | None: + """Optional[:class:`PartialEmoji`]: Returns the displayed emoji of this role.""" + if self._unicode_emoji is None: + return None + return PartialEmoji.from_str(self._unicode_emoji) + + @emoji.setter + def emoji(self, value: str | PartialEmoji | Emoji | None) -> None: + if value is None: + self._unicode_emoji = None + return + + if isinstance(value, str): + self._unicode_emoji = value + elif isinstance(value, (PartialEmoji, Emoji)): + self._unicode_emoji = str(value) + else: + raise TypeError( + f"expected a str, PartialEmoji, Emoji or None, not {value.__class__.__name__}" + ) + + def to_dict(self) -> RolePayload: + payload: RolePayload = { + "id": str(self.id), + "name": self.name, + "hoist": self.hoisted, + "color": self._colour, + "mentionable": self.mentionable, + "permissions": str(self.permissions.value), + "icon": ( + utils._bytes_to_base64_data(self.icon) + if self.icon is not None + else None + ), + "unicode_emoji": self._unicode_emoji, + } # type: ignore + return payload diff --git a/discord/http.py b/discord/http.py index 464710daac..4dd23e5b9b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1476,13 +1476,7 @@ def get_guild( def delete_guild(self, guild_id: Snowflake) -> Response[None]: return self.request(Route("DELETE", "/guilds/{guild_id}", guild_id=guild_id)) - def create_guild(self, name: str, icon: str | None) -> Response[guild.Guild]: - payload = { - "name": name, - } - if icon: - payload["icon"] = icon - + def create_guild(self, payload: guild.GuildCreate) -> Response[guild.Guild]: return self.request(Route("POST", "/guilds"), json=payload) def edit_guild( diff --git a/discord/types/guild.py b/discord/types/guild.py index cac645b272..1cccdda65a 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -25,11 +25,11 @@ from __future__ import annotations -from typing import Literal +from typing import Any, Literal from .._typed_dict import NotRequired, Required, TypedDict from .activity import PartialPresenceUpdate -from .channel import GuildChannel +from .channel import GuildChannel, PartialChannel from .emoji import Emoji from .member import Member from .role import Role @@ -190,3 +190,17 @@ class GuildMFAModify(TypedDict): class GuildBulkBan(TypedDict): banned_users: list[Snowflake] failed_users: list[Snowflake] + + +class GuildCreate(TypedDict): + name: str + icon: NotRequired[str] + verification_level: NotRequired[VerificationLevel] + default_message_notifications: NotRequired[DefaultMessageNotificationLevel] + explicit_content_filter: NotRequired[ExplicitContentFilterLevel] + roles: NotRequired[list[Role]] + channels: NotRequired[list[dict[str, Any]]] + afk_channel_id: NotRequired[Snowflake] + afk_timeout: NotRequired[int] + system_channel_id: NotRequired[Snowflake] + system_channel_flags: NotRequired[int]