diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e6b9ad0ce4..6175ece47f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -34,6 +34,10 @@ If the bug report is missing this information then it'll take us longer to fix t Submitting a pull request is fairly simple, just make sure it focuses on a single aspect and doesn't manage to have scope creep and it's probably good to go. It would be incredibly lovely if the style is consistent to that found in the project. This project follows PEP-8 guidelines (mostly) with a column limit of 125. +### Licensing + +By submitting a pull request, you agree that; 1) You hold the copyright on all submitted code inside said pull request; 2) You agree to transfer all rights to the owner of this repository, and; 3) If you are found to be in fault with any of the above, we shall not be held responsible in any way after the pull request has been merged. + ### Git Commit Guidelines - Use present tense (e.g. "Add feature" not "Added feature") diff --git a/README.rst b/README.rst index 263fcd9355..95bde78729 100644 --- a/README.rst +++ b/README.rst @@ -72,22 +72,20 @@ Quick Example import discord - class MyClient(discord.Client): - async def on_ready(self): - print("Logged on as", self.user) - - async def on_message(self, message): - # don't respond to ourselves - if message.author == self.user: - return - - if message.content == "ping": - await message.channel.send("pong") - - client = MyClient() - client.run("token") + bot = discord.Bot() + + @bot.slash_command() + async def hello(ctx, name: str = None): + name = name or ctx.author.name + await ctx.send(f"Hello {name}!") + + @bot.user_command(name="Say Hello") + async def hi(ctx, user): + await ctx.send(f"{ctx.author.mention} says hello to {user.name}!") + + bot.run("token") -Bot Example +Normal Commands Example ~~~~~~~~~~~~~ .. code:: py @@ -110,4 +108,5 @@ Links - `Documentation `_ - `Official Discord Server `_ +- `Discord Developers `_ - `Discord API `_ diff --git a/discord/__init__.py b/discord/__init__.py index 89d8081cc4..16fccf752d 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -62,6 +62,8 @@ from .bot import * from .app import * from .cog import Cog +from .welcome_screen import * + class VersionInfo(NamedTuple): major: int diff --git a/discord/abc.py b/discord/abc.py index 6d06a12d46..ba5984e57a 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -62,6 +62,7 @@ 'GuildChannel', 'Messageable', 'Connectable', + 'Mentionable' ) T = TypeVar('T', bound=VoiceProtocol) @@ -1141,6 +1142,7 @@ class Messageable: - :class:`~discord.Member` - :class:`~discord.ext.commands.Context` - :class:`~discord.Thread` + - :class:`~discord.ApplicationContext` """ __slots__ = () @@ -1690,4 +1692,4 @@ async def connect( class Mentionable: # TODO: documentation, methods if needed - pass \ No newline at end of file + pass diff --git a/discord/app/commands.py b/discord/app/commands.py index b0528221bd..fc634d97fd 100644 --- a/discord/app/commands.py +++ b/discord/app/commands.py @@ -405,13 +405,13 @@ async def _invoke(self, ctx: ApplicationContext) -> None: <= SlashCommandOptionType.role.value ): name = "member" if op.input_type.name == "user" else op.input_type.name - arg = await get_or_fetch(ctx.guild, name, int(arg)) + arg = await get_or_fetch(ctx.guild, name, int(arg), default=int(arg)) elif op.input_type == SlashCommandOptionType.mentionable: - try: - arg = await get_or_fetch(ctx.guild, "member", int(arg)) - except NotFound: - arg = await get_or_fetch(ctx.guild, "role", int(arg)) + arg_id = int(arg) + arg = await get_or_fetch(ctx.guild, "member", arg_id) + if arg is None: + arg = ctx.guild.get_role(arg_id) or arg_id kwargs[op.name] = arg @@ -466,7 +466,7 @@ def _update_copy(self, kwargs: Dict[str, Any]): class Option: def __init__( - self, input_type: Any, /, description = None,**kwargs + self, input_type: Any, /, description: str = None, **kwargs ) -> None: self.name: Optional[str] = kwargs.pop("name", None) self.description = description or "No description provided" @@ -501,9 +501,11 @@ def __init__(self, name: str, value: Optional[Union[str, int, float]] = None): def to_dict(self) -> Dict[str, Union[str, int, float]]: return {"name": self.name, "value": self.value} -def option(name, type, **kwargs): +def option(name, type=None, **kwargs): """A decorator that can be used instead of typehinting Option""" def decor(func): + nonlocal type + type = type or func.__annotations__.get(name, str) func.__annotations__[name] = Option(type, **kwargs) return func return decor diff --git a/discord/app/context.py b/discord/app/context.py index 6996137eb9..88acfae1cb 100644 --- a/discord/app/context.py +++ b/discord/app/context.py @@ -24,8 +24,11 @@ from typing import TYPE_CHECKING, Optional, Union +import discord.abc + if TYPE_CHECKING: import discord + from discord.state import ConnectionState from ..guild import Guild from ..interactions import Interaction, InteractionResponse @@ -33,10 +36,9 @@ from ..message import Message from ..user import User from ..utils import cached_property -from ..context_managers import Typing -class ApplicationContext: +class ApplicationContext(discord.abc.Messageable): """Represents a Discord interaction context. This class is not created manually and is instead passed to application @@ -58,6 +60,10 @@ def __init__(self, bot: "discord.Bot", interaction: Interaction): self.bot = bot self.interaction = interaction self.command = None + self._state: ConnectionState = self.interaction._state + + async def _get_channel(self) -> discord.abc.Messageable: + return self.channel @cached_property def channel(self): @@ -87,9 +93,6 @@ def user(self) -> Optional[Union[Member, User]]: def voice_client(self): return self.guild.voice_client - def typing(self): - return Typing(self.channel) - @cached_property def response(self) -> InteractionResponse: return self.interaction.response @@ -100,11 +103,6 @@ def response(self) -> InteractionResponse: def respond(self): return self.followup.send if self.response.is_done() else self.interaction.response.send_message - @property - def send(self): - """Behaves like :attr:`~discord.abc.Messagable.send` if the response is done, else behaves like :attr:`~discord.app.ApplicationContext.respond`""" - return self.channel.send if self.response.is_done() else self.respond - @property def defer(self): return self.interaction.response.defer diff --git a/discord/bot.py b/discord/bot.py index e41fbaa895..2b2ee56acd 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -232,7 +232,7 @@ async def process_application_commands(self, interaction: Interaction) -> None: else: self.dispatch('application_command_completion', ctx) - def slash_command(self, **kwargs) -> SlashCommand: + def slash_command(self, **kwargs): """A shortcut decorator that invokes :func:`.ApplicationCommandMixin.command` and adds it to the internal command list via :meth:`~.ApplicationCommandMixin.add_application_command`. This shortcut is made specifically for :class:`.SlashCommand`. @@ -247,7 +247,7 @@ def slash_command(self, **kwargs) -> SlashCommand: """ return self.application_command(cls=SlashCommand, **kwargs) - def user_command(self, **kwargs) -> UserCommand: + def user_command(self, **kwargs): """A shortcut decorator that invokes :func:`.ApplicationCommandMixin.command` and adds it to the internal command list via :meth:`~.ApplicationCommandMixin.add_application_command`. This shortcut is made specifically for :class:`.UserCommand`. @@ -262,7 +262,7 @@ def user_command(self, **kwargs) -> UserCommand: """ return self.application_command(cls=UserCommand, **kwargs) - def message_command(self, **kwargs) -> MessageCommand: + def message_command(self, **kwargs): """A shortcut decorator that invokes :func:`.ApplicationCommandMixin.command` and adds it to the internal command list via :meth:`~.ApplicationCommandMixin.add_application_command`. This shortcut is made specifically for :class:`.MessageCommand`. diff --git a/discord/channel.py b/discord/channel.py index be3315cf1a..2164cfc5f1 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -45,7 +45,7 @@ import discord.abc from .permissions import PermissionOverwrite, Permissions -from .enums import ChannelType, StagePrivacyLevel, try_enum, VoiceRegion, VideoQualityMode +from .enums import ChannelType, InviteTarget, StagePrivacyLevel, try_enum, VoiceRegion, VideoQualityMode from .mixins import Hashable from .object import Object from . import utils @@ -55,6 +55,7 @@ from .stage_instance import StageInstance from .threads import Thread from .iterators import ArchivedThreadIterator +from .invite import Invite __all__ = ( 'TextChannel', @@ -1037,6 +1038,64 @@ async def edit(self, *, reason=None, **options): # the payload will always be the proper channel payload return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + async def create_activity_invite(self, event:str, **kwargs) -> Invite: + """|coro| + + A shortcut method that creates an instant activity invite. + + You must have the :attr:`~discord.Permissions.create_instant_invite` permission to + do this. + + Parameters + ------------ + event: :class:`str` + The event to create an invite for. + max_age: :class:`int` + How long the invite should last in seconds. If it's 0 then the invite + doesn't expire. Defaults to ``0``. + max_uses: :class:`int` + How many uses the invite could be used for. If it's 0 then there + are unlimited uses. Defaults to ``0``. + temporary: :class:`bool` + Denotes that the invite grants temporary membership + (i.e. they get kicked after they disconnect). Defaults to ``False``. + unique: :class:`bool` + Indicates if a unique invite URL should be created. Defaults to True. + If this is set to ``False`` then it will return a previously created + invite. + reason: Optional[:class:`str`] + The reason for creating this invite. Shows up on the audit log. + + + Raises + ------- + InvalidArgument + If the event is not a valid event. + ~discord.HTTPException + Invite creation failed. + + Returns + -------- + :class:`~discord.Invite` + The invite that was created. + """ + + application_ids = { + 'youtube' : 755600276941176913, + 'poker' : 755827207812677713, + 'betrayal': 773336526917861400, + 'fishing' : 814288819477020702, + 'chess' : 832012774040141894, + } + event = application_ids.get(event) + if event is None: + raise InvalidArgument('Invalid event.') + + return await self.create_invite( + target_type=InviteTarget.embedded_application, + target_application_id=event, + **kwargs + ) class StageChannel(VocalGuildChannel): """Represents a Discord guild stage channel. diff --git a/discord/client.py b/discord/client.py index b6198d1090..20e9a629b5 100644 --- a/discord/client.py +++ b/discord/client.py @@ -460,6 +460,8 @@ async def login(self, token: str) -> None: Raises ------ + TypeError + The token was in invalid type. :exc:`.LoginFailure` The wrong credentials are passed. :exc:`.HTTPException` @@ -467,6 +469,8 @@ async def login(self, token: str) -> None: usually when it isn't 200 or the known incorrect credentials passing status code. """ + if not isinstance(token, str): + raise TypeError(f"token must be of type str, not {token.__class__.__name__}") _log.info('logging in using static token') diff --git a/discord/colour.py b/discord/colour.py index 2833e6225b..8cf52dc2ec 100644 --- a/discord/colour.py +++ b/discord/colour.py @@ -325,5 +325,13 @@ def yellow(cls: Type[CT]) -> CT: """ return cls(0xFEE75C) + @classmethod + def nitro_pink(cls: Type[CT]) -> CT: + """A factory method that returns a :class:`Colour` with a value of ``0xf47fff``. + + .. versionadded:: 2.0 + """ + return cls(0xf47fff) + Color = Colour diff --git a/discord/embeds.py b/discord/embeds.py index 7033a10e78..0ee29981e8 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -424,6 +424,22 @@ def set_image(self: E, *, url: MaybeEmpty[Any]) -> E: return self + def remove_image(self: E) -> E: + """Removes the embed's image. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 2.0 + """ + try: + del self._image + except AttributeError: + pass + + return self + + @property def thumbnail(self) -> _EmbedMediaProxy: """Returns an ``EmbedProxy`` denoting the thumbnail contents. @@ -466,6 +482,21 @@ def set_thumbnail(self: E, *, url: MaybeEmpty[Any]) -> E: return self + def remove_thumbnail(self: E) -> E: + """Removes the embed's thumbnail. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 2.0 + """ + try: + del self._thumbnail + except AttributeError: + pass + + return self + @property def video(self) -> _EmbedVideoProxy: """Returns an ``EmbedProxy`` denoting the video contents. diff --git a/discord/errors.py b/discord/errors.py index 85cbca1007..b82892a286 100644 --- a/discord/errors.py +++ b/discord/errors.py @@ -289,7 +289,9 @@ def __init__(self, interaction: Interaction): class ExtensionError(DiscordException): """Base exception for extension related errors. + This inherits from :exc:`~discord.DiscordException`. + Attributes ------------ name: :class:`str` @@ -304,6 +306,7 @@ def __init__(self, message: Optional[str] = None, *args: Any, name: str) -> None class ExtensionAlreadyLoaded(ExtensionError): """An exception raised when an extension has already been loaded. + This inherits from :exc:`ExtensionError` """ def __init__(self, name: str) -> None: @@ -311,6 +314,7 @@ def __init__(self, name: str) -> None: class ExtensionNotLoaded(ExtensionError): """An exception raised when an extension was not loaded. + This inherits from :exc:`ExtensionError` """ def __init__(self, name: str) -> None: @@ -318,6 +322,7 @@ def __init__(self, name: str) -> None: class NoEntryPointError(ExtensionError): """An exception raised when an extension does not have a ``setup`` entry point function. + This inherits from :exc:`ExtensionError` """ def __init__(self, name: str) -> None: @@ -325,7 +330,9 @@ def __init__(self, name: str) -> None: class ExtensionFailed(ExtensionError): """An exception raised when an extension failed to load during execution of the module or ``setup`` entry point. + This inherits from :exc:`ExtensionError` + Attributes ----------- name: :class:`str` @@ -341,14 +348,17 @@ def __init__(self, name: str, original: Exception) -> None: class ExtensionNotFound(ExtensionError): """An exception raised when an extension is not found. + This inherits from :exc:`ExtensionError` + .. versionchanged:: 1.3 Made the ``original`` attribute always None. + Attributes ----------- name: :class:`str` The extension that had the error. """ def __init__(self, name: str) -> None: - msg = f'Extension {name!r} could not be loaded.' + msg = f'Extension {name!r} could not be found.' super().__init__(msg, name=name) diff --git a/discord/guild.py b/discord/guild.py index 41545f773b..35a73e1dfe 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -76,6 +76,7 @@ from .threads import Thread, ThreadMember from .sticker import GuildSticker from .file import File +from .welcome_screen import WelcomeScreen, WelcomeScreenChannel __all__ = ( @@ -2942,3 +2943,101 @@ async def change_voice_state( ws = self._state._get_websocket(self.id) channel_id = channel.id if channel else None await ws.voice_state(self.id, channel_id, self_mute, self_deaf) + + async def welcome_screen(self): + """|coro| + + Returns the :class:`WelcomeScreen` of the guild. + + The guild must have ``COMMUNITY`` in :attr:`~Guild.features`. + + You must have the :attr:`~Permissions.manage_guild` permission in order to get this. + + .. versionadded:: 2.0 + + Raises + ------- + Forbidden + You do not have the proper permissions to get this. + HTTPException + Retrieving the welcome screen failed somehow. + NotFound + The guild doesn't has a welcome screen or community feature is disabled. + + + Returns + -------- + :class:`WelcomeScreen` + The welcome screen of guild. + """ + data = await self._state.http.get_welcome_screen(self.id) + return WelcomeScreen(data=data, guild=self) + + + @overload + async def edit_welcome_screen( + self, + *, + description: Optional[str] = ..., + welcome_channels: Optional[List[WelcomeChannel]] = ..., + enabled: Optional[bool] = ..., + ) -> WelcomeScreen: + ... + + @overload + async def edit_welcome_screen(self) -> None: + ... + + + async def edit_welcome_screen(self, **options): + """|coro| + + A shorthand for :attr:`WelcomeScreen.edit` without fetching the welcome screen. + + You must have the :attr:`~Permissions.manage_guild` permission in the + guild to do this. + + The guild must have ``COMMUNITY`` in :attr:`Guild.features` + + Parameters + ------------ + + description: Optional[:class:`str`] + The new description of welcome screen. + welcome_channels: Optional[List[:class:`WelcomeChannel`]] + The welcome channels. The order of the channels would be same as the passed list order. + enabled: Optional[:class:`bool`] + Whether the welcome screen should be displayed. + + Raises + ------- + + HTTPException + Editing the welcome screen failed somehow. + Forbidden + You don't have permissions to edit the welcome screen. + NotFound + This welcome screen does not exist. + + Returns + -------- + + :class:`WelcomeScreen` + The edited welcome screen. + """ + + welcome_channels = options.get('welcome_channels', []) + welcome_channels_data = [] + + for channel in welcome_channels: + if not isinstance(channel, WelcomeScreenChannel): + raise TypeError('welcome_channels parameter must be a list of WelcomeScreenChannel.') + + welcome_channels_data.append(channel.to_dict()) + + options['welcome_channels'] = welcome_channels_data + + if options: + new = await self._state.http.edit_welcome_screen(self.id, options) + return WelcomeScreen(data=new, guild=self) + diff --git a/discord/http.py b/discord/http.py index 7a4c2adced..74888ee716 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1490,6 +1490,22 @@ def delete_channel_permissions( ) -> Response[None]: r = Route('DELETE', '/channels/{channel_id}/permissions/{target}', channel_id=channel_id, target=target) return self.request(r, reason=reason) + + # Welcome Screen + + def get_welcome_screen(self, guild_id: Snowflake) -> Response[welcome_screen.WelcomeScreen]: + return self.request(Route('GET', '/guilds/{guild_id}/welcome-screen', guild_id=guild_id)) + + def edit_welcome_screen(self, guild_id: Snowflake, payload: Any) -> Response[welcome_screen.WelcomeScreen]: + keys = ( + 'description', + 'welcome_channels', + 'enabled', + ) + payload = { + key: val for key, val in payload.items() if key in keys + } + return self.request(Route('PATCH', '/guilds/{guild_id}/welcome-screen', guild_id=guild_id), json=payload) # Voice management diff --git a/discord/raw_models.py b/discord/raw_models.py index cda754d1b9..589e0af079 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -26,6 +26,8 @@ from typing import TYPE_CHECKING, Optional, Set, List +from .enums import ChannelType, try_enum + if TYPE_CHECKING: from .types.raw_models import ( MessageDeleteEvent, @@ -34,11 +36,14 @@ MessageUpdateEvent, ReactionClearEvent, ReactionClearEmojiEvent, - IntegrationDeleteEvent + IntegrationDeleteEvent, + ThreadDeleteEvent, ) from .message import Message from .partial_emoji import PartialEmoji from .member import Member + from .threads import Thread + __all__ = ( @@ -49,6 +54,7 @@ 'RawReactionClearEvent', 'RawReactionClearEmojiEvent', 'RawIntegrationDeleteEvent', + 'RawThreadDeleteEvent', ) @@ -276,3 +282,33 @@ def __init__(self, data: IntegrationDeleteEvent) -> None: self.application_id: Optional[int] = int(data['application_id']) except KeyError: self.application_id: Optional[int] = None + +class RawThreadDeleteEvent(_RawReprMixin): + """Represents the payload for :func:`on_raw_thread_delete` event. + + .. versionadded:: 2.0 + + Attributes + ---------- + + thread_id: :class:`int` + The ID of the thread that was deleted. + thread_type: :class:`discord.ChannelType` + The channel type of the deleted thread. + guild_id: :class:`int` + The ID of the guild the deleted thread belonged to. + parent_id: :class:`int` + The ID of the channel the thread belonged to. + thread: Optional[:class:`discord.Thread`] + The thread that was deleted. This may be ``None`` if deleted thread is not found in internal cache. + """ + __slots__ = ('thread_id', 'thread_type', 'guild_id', 'parent_id', 'thread') + + def __init__(self, data: ThreadDeleteEvent) -> None: + self.thread_id: int = int(data['id']) + self.thread_type: ChannelType = try_enum(ChannelType, int(data['type'])) + self.guild_id: int = int(data['guild_id']) + self.parent_id: int = int(data['parent_id']) + self.thread: Optional[Thread] = None + + diff --git a/discord/state.py b/discord/state.py index 2534e7aac4..5c1a5ec08c 100644 --- a/discord/state.py +++ b/discord/state.py @@ -850,12 +850,18 @@ def parse_thread_update(self, data) -> None: def parse_thread_delete(self, data) -> None: guild_id = int(data['guild_id']) guild = self._get_guild(guild_id) + if guild is None: _log.debug('THREAD_DELETE referencing an unknown guild ID: %s. Discarding', guild_id) return - thread_id = int(data['id']) - thread = guild.get_thread(thread_id) + raw = RawThreadDeleteEvent(data) + thread = guild.get_thread(raw.thread_id) + raw.thread = thread + + self.dispatch('raw_thread_delete', raw) + + if thread is not None: guild._remove_thread(thread) # type: ignore self.dispatch('thread_delete', thread) diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index 3c45b299c1..1a6ed8e934 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -85,3 +85,10 @@ class _IntegrationDeleteEventOptional(TypedDict, total=False): class IntegrationDeleteEvent(_IntegrationDeleteEventOptional): id: Snowflake guild_id: Snowflake + +class ThreadDeleteEvent(TypedDict, total=False): + thread_id: Snowflake + thread_type: int + guild_id: Snowflake + parent_id: Snowflake + diff --git a/discord/ui/view.py b/discord/ui/view.py index acabd20c13..670f05eff5 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -402,7 +402,7 @@ def refresh(self, components: List[Component]): item = _component_to_item(component) if not item.is_dispatchable(): continue - children.append(component) + children.append(item) else: older.refresh_component(component) children.append(older) diff --git a/discord/utils.py b/discord/utils.py index eef27ced61..3479914ed3 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -61,7 +61,7 @@ import types import warnings -from .errors import InvalidArgument +from .errors import InvalidArgument, HTTPException try: import orjson @@ -448,11 +448,14 @@ def get(iterable: Iterable[T], **attrs: Any) -> Optional[T]: return elem return None -async def get_or_fetch(obj, attr: str, id: int): +async def get_or_fetch(obj, attr: str, id: int, *, default: Any = None): # TODO: Document this getter = getattr(obj, f'get_{attr}')(id) if getter is None: - getter = await getattr(obj, f'fetch_{attr}')(id) + try: + getter = await getattr(obj, f'fetch_{attr}')(id) + except HTTPException: + return default return getter def _unique(iterable: Iterable[T]) -> List[T]: diff --git a/discord/welcome_screen.py b/discord/welcome_screen.py new file mode 100644 index 0000000000..9f11ccc0c9 --- /dev/null +++ b/discord/welcome_screen.py @@ -0,0 +1,219 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +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, Union, overload +from .utils import _get_as_snowflake, get +from .partial_emoji import _EmojiTag + +if TYPE_CHECKING: + from .types.welcome_screen import ( + WelcomeScreen as WelcomeScreenPayload, + WelcomeScreenChannel as WelcomeScreenChannelPayload, + ) + from .guild import Guild + from .abc import Snowflake + from .partial_emoji import PartialEmoji + from .emoji import Emoji + +__all__ = ( + 'WelcomeScreen', + 'WelcomeScreenChannel', +) + +class WelcomeScreenChannel: + """Represents a welcome channel displayed on :class:`WelcomeScreen` + + .. versionadded:: 2.0 + + Attributes + ---------- + + channel: :class:`abc.Snowflake` + The channel that is being referenced. + description: :class:`str` + The description of channel that is shown on the welcome screen. + emoji: :class:`Union[Emoji, PartialEmoji, str]` + The emoji of channel that is shown on welcome screen. + """ + def __init__(self, channel: Snowflake, description: str, emoji: Union[Emoji, PartialEmoji, str]): + self.channel = channel + self.description = description + self.emoji = emoji + + def __repr__(self): + return f'WelcomeScreenChannel(channel={self.channel} description={self.description})' + + def to_dict(self) -> WelcomeScreenChannelPayload: + dict_: WelcomeScreenChannelPayload = { + 'channel_id': self.channel.id, + 'description': self.description, + 'emoji_id': None, + 'emoji_name': None, + } + + if isinstance(self.emoji, _EmojiTag): + # custom guild emoji + dict_['emoji_id'] = self.emoji.id # type: ignore + dict_['emoji_name'] = self.emoji.name # type: ignore + else: + # unicode emoji or None + dict_['emoji_name'] = self.emoji + dict_['emoji_id'] = None # type: ignore + + return dict_ + + + @classmethod + def _from_dict(cls, data: WelcomeScreenChannelPayload, guild: Guild) -> WelcomeChannel: + channel_id = _get_as_snowflake(data, 'channel_id') + channel = guild.get_channel(channel_id) + description = data.get('description') + _emoji_id = _get_as_snowflake(data, 'emoji_id') + _emoji_name = data.get('emoji_name') + + if _emoji_id: + # custom guild emoji + emoji = get(guild.emojis, id=_emoji_id) + else: + # unicode emoji or None + emoji = _emoji_name + + return cls(channel=channel, description=description, emoji=emoji) # type: ignore + + + +class WelcomeScreen: + """Represents the welcome screen of a guild. + + .. versionadded:: 2.0 + + Attributes + ---------- + + description: :class:`str` + The description text displayed on the welcome screen. + welcome_channels: List[:class:`WelcomeScreenChannel`] + A list of channels displayed on welcome screen. + """ + + def __init__(self, data: WelcomeScreenPayload, guild: Guild): + self._guild = guild + self._update(data) + + def __repr__(self): + return f' bool: + """:class:`bool`: Indicates whether the welcome screen is enabled or not.""" + return 'WELCOME_SCREEN_ENABLED' in self._guild.features + + @property + def guild(self) -> Guild: + """:class:`Guild`: The guild this welcome screen belongs to.""" + return self._guild + + + @overload + async def edit( + self, + *, + description: Optional[str] = ..., + welcome_channels: Optional[List[WelcomeChannel]] = ..., + enabled: Optional[bool] = ..., + ) -> None: + ... + + @overload + async def edit(self) -> None: + ... + + async def edit(self, **options): + """|coro| + + Edits the welcome screen. + + You must have the :attr:`~Permissions.manage_guild` permission in the + guild to do this. + + Usage: :: + rules_channel = guild.get_channel(12345678) + announcements_channel = guild.get_channel(87654321) + custom_emoji = utils.get(guild.emojis, name='loudspeaker') + await welcome_screen.edit( + description='This is a very cool community server!', + welcome_channels=[ + WelcomeChannel(channel=rules_channel, description='Read the rules!', emoji='👨‍🏫'), + WelcomeChannel(channel=announcements_channel, description='Watch out for announcements!', emoji=custom_emoji), + ] + ) + + .. note:: + Welcome channels can only accept custom emojis if :attr:`~Guild.premium_tier` is level 2 or above. + + Parameters + ------------ + + description: Optional[:class:`str`] + The new description of welcome screen. + welcome_channels: Optional[List[:class:`WelcomeChannel`]] + The welcome channels. The order of the channels would be same as the passed list order. + enabled: Optional[:class:`bool`] + Whether the welcome screen should be displayed. + + Raises + ------- + + HTTPException + Editing the welcome screen failed somehow. + Forbidden + You don't have permissions to edit the welcome screen. + NotFound + This welcome screen does not exist. + + """ + + welcome_channels = options.get('welcome_channels', []) + welcome_channels_data = [] + + for channel in welcome_channels: + if not isinstance(channel, WelcomeScreenChannel): + raise TypeError('welcome_channels parameter must be a list of WelcomeScreenChannel.') + + welcome_channels_data.append(channel.to_dict()) + + options['welcome_channels'] = welcome_channels_data + + if options: + new = await self._guild._state.http.edit_welcome_screen(self._guild.id, options) + self._update(new) + + return self \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst index 0bd611886c..7f4939eeb4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -759,7 +759,9 @@ to handle it, which defaults to print a traceback and ignoring the exception. .. function:: on_thread_delete(thread) - Called whenever a thread is deleted. + Called whenever a thread is deleted. If the deleted thread isn't found in internal cache + then this will not be called. Archived threads are not in the cache. Consider using :func:`on_raw_thread_delete` + Note that you can get the guild from :attr:`Thread.guild`. @@ -770,6 +772,14 @@ to handle it, which defaults to print a traceback and ignoring the exception. :param thread: The thread that got deleted. :type thread: :class:`Thread` +.. function:: on_raw_thread_delete(payload) + + Called whenever a thread is deleted. Unlike :func:`on_thread_delete` this is called + regardless of the state of the internal cache. + + :param payload: The raw event payload data. + :type payload: :class:`RawThreadDeleteEvent` + .. function:: on_thread_member_join(member) on_thread_member_remove(member) @@ -3821,6 +3831,22 @@ Template .. autoclass:: Template() :members: + +WelcomeScreen +~~~~~~~~~~~~~~~ + +.. attributetable:: WelcomeScreen + +.. autoclass:: WelcomeScreen() + :members: + +WelcomeScreenChannel +~~~~~~~~~~~~~~~ + +.. attributetable:: WelcomeScreenChannel + +.. autoclass:: WelcomeScreenChannel() + :members: WidgetChannel ~~~~~~~~~~~~~~~ @@ -3943,6 +3969,14 @@ RawIntegrationDeleteEvent .. autoclass:: RawIntegrationDeleteEvent() :members: +RawThreadDeleteEvent +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawThreadDeleteEvent + +.. autoclass:: RawThreadDeleteEvent() + :members: + PartialWebhookGuild ~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/app_commands/slash_basic.py b/examples/app_commands/slash_basic.py index 979f4d9a51..2c42005f9a 100644 --- a/examples/app_commands/slash_basic.py +++ b/examples/app_commands/slash_basic.py @@ -2,24 +2,28 @@ bot = discord.Bot() -# If you use commands.Bot, @bot.slash_command should be used for -# slash commands. You can use @bot.slash_command with discord.Bot as well +# Note: If you want you can use commands.Bot instead of discord.Bot +# Use discord.Bot if you don't want prefixed message commands -@bot.command(guild_ids=[...]) # create a slash command for the supplied guilds +# With discord.Bot you can use @bot.command as an alias +# of @bot.slash_command but this is overriden by commands.Bot + + +@bot.slash_command(guild_ids=[...]) # create a slash command for the supplied guilds async def hello(ctx): """Say hello to the bot""" # the command description can be supplied as the docstring await ctx.send(f"Hello {ctx.author}!") -@bot.command( +@bot.slash_command( name="hi" ) # Not passing in guild_ids creates a global slash command (might take an hour to register) async def global_command(ctx, num: int): # Takes one integer parameter await ctx.send(f"This is a global command, {num}!") -@bot.command(guild_ids=[...]) +@bot.slash_command(guild_ids=[...]) async def joined( ctx, member: discord.Member = None ): # Passing a default value makes the argument optional diff --git a/examples/app_commands/slash_options.py b/examples/app_commands/slash_options.py index 5575c39566..358d0a5a9f 100644 --- a/examples/app_commands/slash_options.py +++ b/examples/app_commands/slash_options.py @@ -7,7 +7,7 @@ # slash commands. You can use @bot.slash_command with discord.Bot as well -@bot.command(guild_ids=[...]) +@bot.slash_command(guild_ids=[...]) async def hello( ctx, name: Option(str, "Enter your name"), @@ -15,3 +15,5 @@ async def hello( age: Option(int, "Enter your age", required=False, default=18), ): await ctx.send(f"Hello {name}") + +bot.run('TOKEN')