diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bae06de8e..2153c3e0a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: requirements-txt-fixer name: Requirements @@ -30,12 +30,12 @@ repos: - id: check-merge-conflict name: Merge Conflicts - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.0.282' + rev: 'v0.1.5' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.11.0 hooks: - id: black name: Black Formatting diff --git a/docs/src/API Reference/API Reference/ext/hybrid_commands/index.md b/docs/src/API Reference/API Reference/ext/hybrid_commands/index.md index c2721707d..54b0d1cb4 100644 --- a/docs/src/API Reference/API Reference/ext/hybrid_commands/index.md +++ b/docs/src/API Reference/API Reference/ext/hybrid_commands/index.md @@ -1,3 +1,8 @@ +--- +search: + exclude: true +--- + # Hybrid Commands Index - [Context](context) diff --git a/docs/src/API Reference/API Reference/ext/index.md b/docs/src/API Reference/API Reference/ext/index.md index 9e0753164..c59e1d35b 100644 --- a/docs/src/API Reference/API Reference/ext/index.md +++ b/docs/src/API Reference/API Reference/ext/index.md @@ -1,3 +1,8 @@ +--- +search: + exclude: true +--- + # Ext Index These files contain useful features that help you develop a bot diff --git a/docs/src/API Reference/API Reference/ext/prefixed_commands/index.md b/docs/src/API Reference/API Reference/ext/prefixed_commands/index.md index 7c2f70db7..3f56f32df 100644 --- a/docs/src/API Reference/API Reference/ext/prefixed_commands/index.md +++ b/docs/src/API Reference/API Reference/ext/prefixed_commands/index.md @@ -1,3 +1,8 @@ +--- +search: + exclude: true +--- + # Prefixed Commands Index - [Command](command) diff --git a/docs/src/API Reference/API Reference/models/Discord/index.md b/docs/src/API Reference/API Reference/models/Discord/index.md index a0281bbb7..d56908be7 100644 --- a/docs/src/API Reference/API Reference/models/Discord/index.md +++ b/docs/src/API Reference/API Reference/models/Discord/index.md @@ -1,3 +1,8 @@ +--- +search: + exclude: true +--- + # Discord Models Index - [Activity](activity) diff --git a/docs/src/API Reference/API Reference/models/Internal/index.md b/docs/src/API Reference/API Reference/models/Internal/index.md index 05870d026..464717957 100644 --- a/docs/src/API Reference/API Reference/models/Internal/index.md +++ b/docs/src/API Reference/API Reference/models/Internal/index.md @@ -1,3 +1,8 @@ +--- +search: + exclude: true +--- + # Internal Models Index - [Active Voice State](active_voice_state) diff --git a/docs/src/API Reference/API Reference/models/Misc/index.md b/docs/src/API Reference/API Reference/models/Misc/index.md index e58937c7e..15822e153 100644 --- a/docs/src/API Reference/API Reference/models/Misc/index.md +++ b/docs/src/API Reference/API Reference/models/Misc/index.md @@ -1,3 +1,8 @@ +--- +search: + exclude: true +--- + # Misc Models Index - [Iterator](iterator) diff --git a/docs/src/API Reference/API Reference/models/index.md b/docs/src/API Reference/API Reference/models/index.md index 60181e494..d1eda2bc2 100644 --- a/docs/src/API Reference/API Reference/models/index.md +++ b/docs/src/API Reference/API Reference/models/index.md @@ -1,3 +1,8 @@ +--- +search: + exclude: true +--- + # Models Within these pages, you will find a list of all available models within interactions.py. diff --git a/docs/src/Guides/01 Getting Started.md b/docs/src/Guides/01 Getting Started.md index ca59cc573..bda0b6058 100644 --- a/docs/src/Guides/01 Getting Started.md +++ b/docs/src/Guides/01 Getting Started.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Introduction Ready to get your Python on and create a Discord bot? This guide's got you covered with installation options and a basic bot code example. diff --git a/docs/src/Guides/02 Creating Your Bot.md b/docs/src/Guides/02 Creating Your Bot.md index e3d73e956..32f5265aa 100644 --- a/docs/src/Guides/02 Creating Your Bot.md +++ b/docs/src/Guides/02 Creating Your Bot.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Creating Your Bot To make a bot on Discord, you must first create an application on Discord. Thankfully, Discord has made this process very simple: diff --git a/docs/src/Guides/03 Creating Commands.md b/docs/src/Guides/03 Creating Commands.md index abe04237a..0863ab90f 100644 --- a/docs/src/Guides/03 Creating Commands.md +++ b/docs/src/Guides/03 Creating Commands.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Slash Commands So you want to make a slash command (or interaction, as they are officially called), but don't know how to get started? @@ -263,7 +268,7 @@ In there, you have three seconds to return whatever choices you want to the user from interactions import AutocompleteContext @my_command_function.autocomplete("string_option") -async def autocomplete(self, ctx: AutocompleteContext): +async def autocomplete(ctx: AutocompleteContext): string_option_input = ctx.input_text # can be empty # you can use ctx.kwargs.get("name") to get the current state of other options - note they can be empty too @@ -507,7 +512,7 @@ import traceback from interactions.api.events import CommandError @listen(CommandError, disable_default_listeners=True) # tell the dispatcher that this replaces the default listener -async def on_command_error(self, event: CommandError): +async def on_command_error(event: CommandError): traceback.print_exception(event.error) if not event.ctx.responded: await event.ctx.send("Something went wrong.") @@ -521,13 +526,16 @@ If your bot is complex enough, you might find yourself wanting to use custom mod To do this, you'll want to use a string option, and define a converter. Information on how to use converters can be found [on the converter page](../08 Converters). -## Prefixed/Text Commands +## Hybrid Commands + +!!! note + Prefixed commands, called by Discord as "text commands" and sometimes called "message commands" (not to be confused with Context Menu Message Commands), are commands that are triggered when a user sends a normal message with a designated "prefix" in front of them (ie `!my_command`). -To use prefixed commands, instead of typing `/my_command`, you will need to type instead `!my_command`, provided that the prefix you set is `!`. + interactions.py contains an extension for making these commands, which you can [read about here](/interactions.py/Guides/26 Prefixed Commands). Hybrid commands are are slash commands that also get converted to an equivalent prefixed command under the hood. They are their own extension, and require [prefixed commands to be set up beforehand](/interactions.py/Guides/26 Prefixed Commands). After that, use the `setup` function in the `hybrid_commands` extension in your main bot file. -Your setup can (but doesn't necessarily have to) look like this: +Your setup should look similar to this: ```python import interactions @@ -549,7 +557,7 @@ async def my_command_function(ctx: HybridContext): await ctx.send("Hello World") ``` -Suggesting you are using the default mention settings for your bot, you should be able to run this command by `@BotPing my_command`. +Suggesting you are using the default mention settings for your bot, you should be able to run this command by typing out `@BotPing my_command` or using the slash command `/my_command`. Both will work largely equivalently. As you can see, the only difference between hybrid commands and slash commands, from a developer perspective, is that they use `HybridContext`, which attempts to seamlessly allow using the same context for slash and prefixed commands. You can always get the underlying context via `inner_context`, though. diff --git a/docs/src/Guides/04 Context Menus.md b/docs/src/Guides/04 Context Menus.md index 7d9aa63f7..4f24864c4 100644 --- a/docs/src/Guides/04 Context Menus.md +++ b/docs/src/Guides/04 Context Menus.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Context Menus Context menus are interactions under the hood. Defining them is very similar. diff --git a/docs/src/Guides/05 Components.md b/docs/src/Guides/05 Components.md index 763d170f1..4c56240ee 100644 --- a/docs/src/Guides/05 Components.md +++ b/docs/src/Guides/05 Components.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Components Components (Buttons, Select Menus and soon Text Input Fields) can be added to any message by passing them to the `components` argument in any `.send()` method. diff --git a/docs/src/Guides/06 Modals.md b/docs/src/Guides/06 Modals.md index 2960bdf8f..29f870a99 100644 --- a/docs/src/Guides/06 Modals.md +++ b/docs/src/Guides/06 Modals.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Modals Modals are basically popups which a user can use to send text information to your bot. As of the writing of this guide, you can use two components in a modal: diff --git a/docs/src/Guides/08 Converters.md b/docs/src/Guides/08 Converters.md index 56c3e873d..fa58c019b 100644 --- a/docs/src/Guides/08 Converters.md +++ b/docs/src/Guides/08 Converters.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Converters If your bot is complex enough, you might find yourself wanting to use custom models in your commands. Converters are classes that allow you to do just that, and can be used in both slash and prefixed commands. diff --git a/docs/src/Guides/10 Events.md b/docs/src/Guides/10 Events.md index 1253eee59..931403398 100644 --- a/docs/src/Guides/10 Events.md +++ b/docs/src/Guides/10 Events.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Events Events (in interactions.py) are pieces of information that are sent whenever something happens in Discord or in the library itself - this includes channel updates, message sending, the bot starting up, and more. diff --git a/docs/src/Guides/20 Extensions.md b/docs/src/Guides/20 Extensions.md index c782f72d7..3e2489107 100644 --- a/docs/src/Guides/20 Extensions.md +++ b/docs/src/Guides/20 Extensions.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Extensions ## Introduction diff --git a/docs/src/Guides/22 Live Patching.md b/docs/src/Guides/22 Live Patching.md index 4644c314f..13604c8f8 100644 --- a/docs/src/Guides/22 Live Patching.md +++ b/docs/src/Guides/22 Live Patching.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Live Patching interactions.py has a few built-in extensions that add some features, primarily for debugging. One of these extensions that you can enable separately is to add [`jurigged`](https://github.com/breuleux/jurigged) for live patching of code. diff --git a/docs/src/Guides/23 Voice.md b/docs/src/Guides/23 Voice.md index aa3c708d0..9be431860 100644 --- a/docs/src/Guides/23 Voice.md +++ b/docs/src/Guides/23 Voice.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Voice Support So you want to start playing some 🎵tunes🎶 in voice channels? Well let's get that going for you. diff --git a/docs/src/Guides/24 Localisation.md b/docs/src/Guides/24 Localisation.md index 43dfa1cc0..f29d8317a 100644 --- a/docs/src/Guides/24 Localisation.md +++ b/docs/src/Guides/24 Localisation.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Localising So your bot has grown, and now you need to ~~localize~~ localise your bot. Well thank god we support localisation then, huh? diff --git a/docs/src/Guides/25 Error Tracking.md b/docs/src/Guides/25 Error Tracking.md index 14b7ffca2..5a348c6e0 100644 --- a/docs/src/Guides/25 Error Tracking.md +++ b/docs/src/Guides/25 Error Tracking.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Error Tracking So, you've finally got your bot running on a server somewhere. Chances are, you're not checking the console output 24/7, looking for exceptions. diff --git a/docs/src/Guides/26 Prefixed Commands.md b/docs/src/Guides/26 Prefixed Commands.md index e58d24b15..d1c2a4d99 100644 --- a/docs/src/Guides/26 Prefixed Commands.md +++ b/docs/src/Guides/26 Prefixed Commands.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Creating Prefixed Commands Prefixed commands, called by Discord as "text commands" and sometimes called "message commands" (not to be confused with Context Menu Message Commands), are commands that are triggered when a user sends a normal message with a designated "prefix" in front of them. diff --git a/docs/src/Guides/30 Pagination.md b/docs/src/Guides/30 Pagination.md index 21ed4c5a3..c0e1101a5 100644 --- a/docs/src/Guides/30 Pagination.md +++ b/docs/src/Guides/30 Pagination.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Pagination > Pagination, also known as paging, is the process of dividing a document into discrete pages, either electronic pages or printed pages. diff --git a/docs/src/Guides/40 Tasks.md b/docs/src/Guides/40 Tasks.md index f7efeb912..6310d4e24 100644 --- a/docs/src/Guides/40 Tasks.md +++ b/docs/src/Guides/40 Tasks.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Tasks Tasks are background processes that can be used to asynchronously run code with a specified trigger. diff --git a/docs/src/Guides/80 Sharding.md b/docs/src/Guides/80 Sharding.md index 0fafb64d9..9fb636f66 100644 --- a/docs/src/Guides/80 Sharding.md +++ b/docs/src/Guides/80 Sharding.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Sharding Oh damn, your bot is getting pretty big, huh? Well I guess its time we discuss sharding. diff --git a/docs/src/Guides/90 Example.md b/docs/src/Guides/90 Example.md index 70690371f..0f1ab9dce 100644 --- a/docs/src/Guides/90 Example.md +++ b/docs/src/Guides/90 Example.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Examples ## `main.py` diff --git a/docs/src/Guides/97 Migration From D.py.md b/docs/src/Guides/97 Migration From D.py.md index c5d67fff6..e1b86576f 100644 --- a/docs/src/Guides/97 Migration From D.py.md +++ b/docs/src/Guides/97 Migration From D.py.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Migrating from discord.py 1. interactions.py requires python 3.10 (as compared to dpy's 3.5), you may need to upgrade python. diff --git a/docs/src/Guides/98 Migration from 4.X.md b/docs/src/Guides/98 Migration from 4.X.md index c37bf2804..398c8d152 100644 --- a/docs/src/Guides/98 Migration from 4.X.md +++ b/docs/src/Guides/98 Migration from 4.X.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Migrating from 4.X Version 5.X (and beyond) is a major rewrite of interactions.py compared to 4.X, though there have been major improvements to compensate for the change. 5.X was designed to be more stable and flexible, solving many of the bugs and UX issues 4.X had while also adding additional features you may like. diff --git a/docs/src/Guides/99 2.x Migration_NAFF.md b/docs/src/Guides/99 2.x Migration_NAFF.md index 9170a1156..d873d4c51 100644 --- a/docs/src/Guides/99 2.x Migration_NAFF.md +++ b/docs/src/Guides/99 2.x Migration_NAFF.md @@ -1,3 +1,8 @@ +--- +search: + boost: 3 +--- + # Migrating from NAFF Oh hey! So you're migrating from NAFF to interactions.py? Well lets get you sorted. diff --git a/docs/src/Guides/index.md b/docs/src/Guides/index.md index a02ced783..0209e6ebe 100644 --- a/docs/src/Guides/index.md +++ b/docs/src/Guides/index.md @@ -1,3 +1,8 @@ +--- +search: + exclude: true +--- + Let's be honest; reading API documentation is a bit of a pain. These guides are meant to help you get started with the library and offer a point of reference. diff --git a/docs/src/index.md b/docs/src/index.md index cc1b5aeca..85dcc199c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -3,6 +3,8 @@ hide: - navigation - toc - feedback +search: + exclude: true --- We hope this documentation is helpful for you, but don't just ++ctrl+c++ and ++ctrl+v++. diff --git a/interactions/api/events/processors/scheduled_events.py b/interactions/api/events/processors/scheduled_events.py index 4993c1ca2..afc46aa8d 100644 --- a/interactions/api/events/processors/scheduled_events.py +++ b/interactions/api/events/processors/scheduled_events.py @@ -37,14 +37,16 @@ async def _on_raw_guild_scheduled_event_delete(self, event: "RawGatewayEvent") - @Processor.define() async def _on_raw_guild_scheduled_event_user_add(self, event: "RawGatewayEvent") -> None: - scheduled_event = self.cache.get_scheduled_event(event.data.get("guild_scheduled_event_id")) - user = self.cache.get_user(event.data.get("user_id")) - - self.dispatch(events.GuildScheduledEventUserAdd(event.data.get("guild_id"), scheduled_event, user)) + self.dispatch( + events.GuildScheduledEventUserAdd( + event.data["guild_id"], event.data["guild_scheduled_event_id"], event.data["user_id"] + ) + ) @Processor.define() async def _on_raw_guild_scheduled_event_user_remove(self, event: "RawGatewayEvent") -> None: - scheduled_event = self.cache.get_scheduled_event(event.data.get("guild_scheduled_event_id")) - user = self.cache.get_user(event.data.get("user_id")) - - self.dispatch(events.GuildScheduledEventUserRemove(event.data.get("guild_id"), scheduled_event, user)) + self.dispatch( + events.GuildScheduledEventUserRemove( + event.data["guild_id"], event.data["guild_scheduled_event_id"], event.data["user_id"] + ) + ) diff --git a/interactions/api/gateway/state.py b/interactions/api/gateway/state.py index aa90917ae..2678ed7bc 100644 --- a/interactions/api/gateway/state.py +++ b/interactions/api/gateway/state.py @@ -128,7 +128,7 @@ async def _ws_connect(self) -> None: except Exception as e: self.client.dispatch(events.Disconnect()) - self.wrapped_logger("".join(traceback.format_exception(type(e), e, e.__traceback__))) + self.wrapped_logger(logging.ERROR, "".join(traceback.format_exception(type(e), e, e.__traceback__))) def wrapped_logger(self, level: int, message: str, **kwargs) -> None: """ diff --git a/interactions/api/http/http_requests/scheduled_events.py b/interactions/api/http/http_requests/scheduled_events.py index 7649ca220..abee75bfa 100644 --- a/interactions/api/http/http_requests/scheduled_events.py +++ b/interactions/api/http/http_requests/scheduled_events.py @@ -58,11 +58,11 @@ async def get_scheduled_event( return await self.request( Route( "GET", - "/guilds/{guild_id}/scheduled-events/{scheduled_event_id}", + "/guilds/{guild_id}/scheduled-events/{scheduled_event_id}?with_user_count={with_user_count}", guild_id=guild_id, scheduled_event_id=scheduled_event_id, + with_user_count=with_user_count, ), - params={"with_user_count": with_user_count}, ) async def create_scheduled_event( diff --git a/interactions/client/client.py b/interactions/client/client.py index 9ce1d7d30..96cdef32b 100644 --- a/interactions/client/client.py +++ b/interactions/client/client.py @@ -2461,6 +2461,19 @@ def get_bot_voice_state(self, guild_id: "Snowflake_Type") -> Optional[ActiveVoic """ return self._connection_state.get_voice_state(guild_id) + def mention_command(self, name: str, scope: int = 0) -> str: + """ + Returns a string that would mention the interaction specified. + + Args: + name: The name of the interaction. + scope: The scope of the interaction. Defaults to 0, the global scope. + + Returns: + str: The interaction's mention in the specified scope. + """ + return self.interactions_by_scope[scope][name].mention(scope) + async def change_presence( self, status: Optional[Union[str, Status]] = Status.ONLINE, diff --git a/interactions/client/smart_cache.py b/interactions/client/smart_cache.py index 575f69741..799ae619d 100644 --- a/interactions/client/smart_cache.py +++ b/interactions/client/smart_cache.py @@ -924,6 +924,38 @@ def get_scheduled_event(self, scheduled_event_id: "Snowflake_Type") -> Optional[ """ return self.scheduled_events_cache.get(to_snowflake(scheduled_event_id)) + async def fetch_scheduled_event( + self, + guild_id: "Snowflake_Type", + scheduled_event_id: "Snowflake_Type", + with_user_count: bool = False, + *, + force: bool = False, + ) -> "ScheduledEvent": + """ + Fetch a scheduled event based on the guild and its own ID. + + Args: + guild_id: The ID of the guild this event belongs to + scheduled_event_id: The ID of the event + with_user_count: Whether to include the user count in the response. + force: If the cache should be ignored, and the event should be fetched from the API + + Returns: + The scheduled event if found + """ + if not force: + if scheduled_event := self.get_scheduled_event(scheduled_event_id): + if int(scheduled_event._guild_id) == int(guild_id) and ( + not with_user_count or scheduled_event.user_count is not MISSING + ): + return scheduled_event + + scheduled_event_data = await self._client.http.get_scheduled_event( + guild_id, scheduled_event_id, with_user_count=with_user_count + ) + return self.place_scheduled_event_data(scheduled_event_data) + def place_scheduled_event_data(self, data: discord_typings.GuildScheduledEventData) -> "ScheduledEvent": """ Take json data representing a scheduled event, process it, and cache it. diff --git a/interactions/client/utils/input_utils.py b/interactions/client/utils/input_utils.py index a4fc47a99..66e27fc8f 100644 --- a/interactions/client/utils/input_utils.py +++ b/interactions/client/utils/input_utils.py @@ -2,6 +2,7 @@ import re import typing from enum import IntFlag +from interactions.models.discord.snowflake import Snowflake from typing import Any, Dict, Union, Optional import aiohttp # type: ignore @@ -25,8 +26,8 @@ import msgspec.json as json def enc_hook(obj: Any) -> int: - # msgspec doesnt support IntFlags - if isinstance(obj, IntFlag): + # msgspec doesnt support IntFlags or interactions.Snowflakes + if isinstance(obj, (IntFlag, Snowflake)): return int(obj) raise TypeError(f"Object of type {type(obj)} is not JSON serializable") diff --git a/interactions/client/utils/misc_utils.py b/interactions/client/utils/misc_utils.py old mode 100644 new mode 100755 diff --git a/interactions/ext/hybrid_commands/hybrid_slash.py b/interactions/ext/hybrid_commands/hybrid_slash.py index 1a6e3f1ab..b367cf80b 100644 --- a/interactions/ext/hybrid_commands/hybrid_slash.py +++ b/interactions/ext/hybrid_commands/hybrid_slash.py @@ -327,6 +327,10 @@ def slash_to_prefixed(cmd: HybridSlashCommand) -> _HybridToPrefixedCommand: # n if cmd.aliases: prefixed_cmd.aliases.extend(cmd.aliases) + # copy over binding from slash command, if any + # can't be done in init due to how _binding works + prefixed_cmd._binding = cmd._binding + if not cmd.dm_permission: prefixed_cmd.add_check(guild_only()) diff --git a/interactions/ext/paginators.py b/interactions/ext/paginators.py index 83f881329..07f345d06 100644 --- a/interactions/ext/paginators.py +++ b/interactions/ext/paginators.py @@ -245,11 +245,13 @@ def create_from_list( pages = [] page = "" for entry in content: + if len(entry) > page_size: + continue if len(page) + len(f"\n{entry}") <= page_size: page += f"{entry}\n" else: pages.append(Page(page, prefix=prefix, suffix=suffix)) - page = "" + page = "f{entry}\n" if page != "": pages.append(Page(page, prefix=prefix, suffix=suffix)) return cls(client, pages=pages, timeout_interval=timeout) diff --git a/interactions/models/discord/components.py b/interactions/models/discord/components.py index 632089b5e..a5c023534 100644 --- a/interactions/models/discord/components.py +++ b/interactions/models/discord/components.py @@ -1,15 +1,22 @@ import contextlib import uuid from abc import abstractmethod -from typing import Any, Dict, Iterator, List, Optional, Union +from typing import Any, Dict, Iterator, List, Optional, Union, TYPE_CHECKING +import attrs import discord_typings +import interactions.models.discord as d_models +from interactions.models.discord.snowflake import Snowflake from interactions.client.const import ACTION_ROW_MAX_ITEMS, MISSING from interactions.client.mixins.serialization import DictSerializationMixin +from interactions.models.discord.base import DiscordObject from interactions.models.discord.emoji import PartialEmoji, process_emoji from interactions.models.discord.enums import ButtonStyle, ChannelType, ComponentType +if TYPE_CHECKING: + import interactions.models.discord + __all__ = ( "BaseComponent", "InteractiveComponent", @@ -26,6 +33,8 @@ "spread_to_rows", "get_components_ids", "TYPE_COMPONENT_MAPPING", + "SelectDefaultValues", + "DefaultableSelectMenu", ) @@ -325,6 +334,87 @@ def to_dict(self) -> discord_typings.SelectMenuComponentData: } +@attrs.define(eq=False, order=False, hash=False, slots=False) +class SelectDefaultValues(DiscordObject): + id: Snowflake + """ID of a user, role, or channel""" + type: str + """Type of value that id represents. Either "user", "role", or "channel""" + + @classmethod + def from_object(cls, obj: DiscordObject) -> "SelectDefaultValues": + """Create a default value from a discord object.""" + match obj: + case d_models.User(): + return cls(id=obj.id, type="user") + case d_models.Member(): + return cls(id=obj.id, type="user") + case d_models.BaseChannel(): + return cls(id=obj.id, type="channel") + case d_models.Role(): + return cls(id=obj.id, type="role") + case _: + raise TypeError( + f"Cannot convert {obj} of type {type(obj)} to a SelectDefaultValues - Expected User, Channel, Member, or Role" + ) + + +class DefaultableSelectMenu(BaseSelectMenu): + default_values: list[ + Union[ + "interactions.models.discord.BaseUser", + "interactions.models.discord.Role", + "interactions.models.discord.BaseChannel", + "interactions.models.discord.Member", + SelectDefaultValues, + ] + ] | None = None + + def __init__( + self, + defaults: list[ + Union[ + "interactions.models.discord.BaseUser", + "interactions.models.discord.Role", + "interactions.models.discord.BaseChannel", + "interactions.models.discord.Member", + SelectDefaultValues, + ] + ] + | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.default_values = defaults + + def add_default_value( + self, + value: Union[ + "interactions.models.discord.BaseUser", + "interactions.models.discord.Role", + "interactions.models.discord.BaseChannel", + "interactions.models.discord.Member", + SelectDefaultValues, + ], + ) -> None: + if self.default_values is None: + self.default_values = [] + self.default_values.append(value) + + def to_dict(self) -> discord_typings.SelectMenuComponentData: + data = super().to_dict() + if self.default_values is not None: + data["default_values"] = [ # type: ignore # waiting on discord typings to update + value.to_dict() + if isinstance(value, SelectDefaultValues) + else SelectDefaultValues.from_object(value).to_dict() + for value in self.default_values + ] + + # Discord handles the type checking, no need to do it here + return data + + class StringSelectOption(BaseComponent): """ Represents a select option. @@ -461,7 +551,7 @@ def to_dict(self) -> discord_typings.SelectMenuComponentData: } -class UserSelectMenu(BaseSelectMenu): +class UserSelectMenu(DefaultableSelectMenu): """ Represents a user select component. @@ -481,6 +571,16 @@ def __init__( min_values: int = 1, max_values: int = 1, custom_id: str | None = None, + default_values: list[ + Union[ + "interactions.models.discord.BaseUser", + "interactions.models.discord.Role", + "interactions.models.discord.BaseChannel", + "interactions.models.discord.Member", + SelectDefaultValues, + ], + ] + | None = None, disabled: bool = False, ) -> None: super().__init__( @@ -489,12 +589,13 @@ def __init__( max_values=max_values, custom_id=custom_id, disabled=disabled, + defaults=default_values, ) self.type: ComponentType = ComponentType.USER_SELECT -class RoleSelectMenu(BaseSelectMenu): +class RoleSelectMenu(DefaultableSelectMenu): """ Represents a user select component. @@ -515,6 +616,16 @@ def __init__( max_values: int = 1, custom_id: str | None = None, disabled: bool = False, + default_values: list[ + Union[ + "interactions.models.discord.BaseUser", + "interactions.models.discord.Role", + "interactions.models.discord.BaseChannel", + "interactions.models.discord.Member", + SelectDefaultValues, + ], + ] + | None = None, ) -> None: super().__init__( placeholder=placeholder, @@ -522,12 +633,13 @@ def __init__( max_values=max_values, custom_id=custom_id, disabled=disabled, + defaults=default_values, ) self.type: ComponentType = ComponentType.ROLE_SELECT -class MentionableSelectMenu(BaseSelectMenu): +class MentionableSelectMenu(DefaultableSelectMenu): def __init__( self, *, @@ -536,6 +648,16 @@ def __init__( max_values: int = 1, custom_id: str | None = None, disabled: bool = False, + default_values: list[ + Union[ + "interactions.models.discord.BaseUser", + "interactions.models.discord.Role", + "interactions.models.discord.BaseChannel", + "interactions.models.discord.Member", + SelectDefaultValues, + ], + ] + | None = None, ) -> None: super().__init__( placeholder=placeholder, @@ -543,12 +665,13 @@ def __init__( max_values=max_values, custom_id=custom_id, disabled=disabled, + defaults=default_values, ) self.type: ComponentType = ComponentType.MENTIONABLE_SELECT -class ChannelSelectMenu(BaseSelectMenu): +class ChannelSelectMenu(DefaultableSelectMenu): def __init__( self, *, @@ -558,6 +681,16 @@ def __init__( max_values: int = 1, custom_id: str | None = None, disabled: bool = False, + default_values: list[ + Union[ + "interactions.models.discord.BaseUser", + "interactions.models.discord.Role", + "interactions.models.discord.BaseChannel", + "interactions.models.discord.Member", + SelectDefaultValues, + ], + ] + | None = None, ) -> None: super().__init__( placeholder=placeholder, @@ -565,6 +698,7 @@ def __init__( max_values=max_values, custom_id=custom_id, disabled=disabled, + defaults=default_values, ) self.channel_types: list[ChannelType] | None = channel_types or [] diff --git a/interactions/models/discord/emoji.py b/interactions/models/discord/emoji.py index 7c59abc75..0364a7e5e 100644 --- a/interactions/models/discord/emoji.py +++ b/interactions/models/discord/emoji.py @@ -209,7 +209,7 @@ async def delete(self, reason: Optional[str] = None) -> None: @property def url(self) -> str: """CDN url for the emoji.""" - return f"https://cdn.discordapp.net/emojis/{self.id}.{'gif' if self.animated else 'png'}" + return f"https://cdn.discordapp.com/emojis/{self.id}.{'gif' if self.animated else 'png'}" def process_emoji_req_format(emoji: Optional[Union[PartialEmoji, dict, str]]) -> Optional[str]: diff --git a/interactions/models/discord/enums.py b/interactions/models/discord/enums.py index c201f6d50..6957c818f 100644 --- a/interactions/models/discord/enums.py +++ b/interactions/models/discord/enums.py @@ -340,6 +340,8 @@ class PremiumType(CursedIntEnum): """Using Nitro Classic""" NITRO = 2 """Full Nitro membership""" + NITRO_BASIC = 3 + """Basic Nitro membership""" class MessageType(CursedIntEnum): diff --git a/interactions/models/discord/guild.py b/interactions/models/discord/guild.py index 5551f2bf7..3b2e780be 100644 --- a/interactions/models/discord/guild.py +++ b/interactions/models/discord/guild.py @@ -1259,27 +1259,45 @@ async def list_scheduled_events(self, with_user_count: bool = False) -> List["mo scheduled_events_data = await self._client.http.list_schedules_events(self.id, with_user_count) return [self._client.cache.place_scheduled_event_data(data) for data in scheduled_events_data] + def get_scheduled_event(self, scheduled_event_id: Snowflake_Type) -> Optional["models.ScheduledEvent"]: + """ + Gets a scheduled event from the cache by id. + + Args: + scheduled_event_id: The id of the scheduled event. + + Returns: + The scheduled event. If the event does not exist, returns None. + + """ + event = self._client.cache.get_scheduled_event(scheduled_event_id) + return None if event and int(event._guild_id) != self.id else event + async def fetch_scheduled_event( - self, scheduled_event_id: Snowflake_Type, with_user_count: bool = False + self, + scheduled_event_id: Snowflake_Type, + with_user_count: bool = False, + *, + force: bool = False, ) -> Optional["models.ScheduledEvent"]: """ - Get a scheduled event by id. + Fetches a scheduled event by id. Args: scheduled_event_id: The id of the scheduled event. with_user_count: Whether to include the user count in the response. + force: If the cache should be ignored, and the event should be fetched from the API Returns: The scheduled event. If the event does not exist, returns None. """ try: - scheduled_event_data = await self._client.http.get_scheduled_event( - self.id, scheduled_event_id, with_user_count + return await self._client.cache.fetch_scheduled_event( + self.id, scheduled_event_id, with_user_count=with_user_count, force=force ) except NotFound: return None - return self._client.cache.place_scheduled_event_data(scheduled_event_data) async def create_scheduled_event( self, diff --git a/interactions/models/discord/role.py b/interactions/models/discord/role.py index 5161f912d..2f81a0f92 100644 --- a/interactions/models/discord/role.py +++ b/interactions/models/discord/role.py @@ -2,12 +2,12 @@ from typing import Any, TYPE_CHECKING import attrs - from interactions.client.const import MISSING, T, Missing from interactions.client.utils import nulled_boolean_get from interactions.client.utils.attr_converters import optional as optional_c -from interactions.client.utils.serializer import dict_filter +from interactions.client.utils.serializer import dict_filter, to_image_data from interactions.models.discord.asset import Asset +from interactions.models.discord.file import UPLOADABLE_TYPE from interactions.models.discord.color import COLOR_TYPES, Color, process_color from interactions.models.discord.emoji import PartialEmoji from interactions.models.discord.enums import Permissions @@ -187,6 +187,8 @@ async def edit( color: Color | COLOR_TYPES | None = None, hoist: bool | None = None, mentionable: bool | None = None, + icon: bytes | UPLOADABLE_TYPE | None = None, + unicode_emoji: str | None = None, ) -> "Role": """ Edit this role, all arguments are optional. @@ -197,6 +199,8 @@ async def edit( color: The color of the role hoist: whether the role should be displayed separately in the sidebar mentionable: whether the role should be mentionable + icon: (Guild Level 2+) Bytes-like object representing the icon; supports PNG, JPEG and WebP + unicode_emoji: (Guild Level 2+) Unicode emoji for the role; can't be used with icon Returns: Role with updated information @@ -204,6 +208,11 @@ async def edit( """ color = process_color(color) + if icon and unicode_emoji: + raise ValueError("Cannot pass both icon and unicode_emoji") + if icon: + icon = to_image_data(icon) + payload = dict_filter( { "name": name, @@ -211,6 +220,8 @@ async def edit( "color": color, "hoist": hoist, "mentionable": mentionable, + "icon": icon, + "unicode_emoji": unicode_emoji, } ) diff --git a/interactions/models/discord/scheduled_event.py b/interactions/models/discord/scheduled_event.py index a958ce4f5..2f2837299 100644 --- a/interactions/models/discord/scheduled_event.py +++ b/interactions/models/discord/scheduled_event.py @@ -49,7 +49,7 @@ class ScheduledEvent(DiscordObject): """The id of an entity associated with a guild scheduled event""" entity_metadata: Optional[Dict[str, Any]] = attrs.field(repr=False, default=MISSING) # TODO make this """The metadata associated with the entity_type""" - user_count: int = attrs.field(repr=False, default=MISSING) + user_count: Absent[int] = attrs.field(repr=False, default=MISSING) # TODO make this optional and None in 6.0 """Amount of users subscribed to the scheduled event""" cover: Asset | None = attrs.field(repr=False, default=None) """The cover image of this event""" diff --git a/interactions/models/internal/application_commands.py b/interactions/models/internal/application_commands.py index 8ed71fddf..b6cde4f0d 100644 --- a/interactions/models/internal/application_commands.py +++ b/interactions/models/internal/application_commands.py @@ -535,7 +535,7 @@ def _is_optional(anno: typing.Any) -> bool: def _remove_optional(t: OptionType | type) -> Any: - non_optional_args: tuple[type] = tuple(a for a in typing.get_args(t) if a is not types.NoneType) # noqa + non_optional_args: tuple[type] = tuple(a for a in typing.get_args(t) if a is not types.NoneType) if len(non_optional_args) == 1: return non_optional_args[0] return typing.Union[non_optional_args] # type: ignore @@ -609,8 +609,8 @@ def _add_option_from_anno_method(self, name: str, option: SlashCommandOption) -> if not self.options: self.options = [] - if option.name is None: - option.name = name + if option.name.default is None: + option.name = LocalisedName.converter(name) else: option.argument_name = name diff --git a/interactions/models/internal/extension.py b/interactions/models/internal/extension.py index 178ff1070..844600b7a 100644 --- a/interactions/models/internal/extension.py +++ b/interactions/models/internal/extension.py @@ -22,7 +22,7 @@ class Extension: """ - A class that allows you to separate your commands and listeners into separate files. Skins require an entrypoint in the same file called `setup`, this function allows client to load the Extension. + A class that allows you to separate your commands and listeners into separate files. Extensions require an entrypoint in the same file called `setup`, this function allows client to load the Extension. ??? Hint "Example Usage:" ```python diff --git a/interactions/models/internal/tasks/triggers.py b/interactions/models/internal/tasks/triggers.py index ab4bc89ae..1c1f3a13e 100644 --- a/interactions/models/internal/tasks/triggers.py +++ b/interactions/models/internal/tasks/triggers.py @@ -105,7 +105,11 @@ def next_fire(self) -> datetime | None: ) if target.tzinfo == timezone.utc: target = target.astimezone(now.tzinfo) - target = target.replace(tzinfo=None) + # target can fall behind or go forward a day, but all we need is the time itself + # to be converted + # to ensure it's on the same day as "now" and not break the next if statement, + # we can just replace the date with now's date + target = target.replace(year=now.year, month=now.month, day=now.day, tzinfo=None) if target <= self.last_call_time: target += timedelta(days=1) diff --git a/mkdocs.yml b/mkdocs.yml index 00030829d..2b7d2bed2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -121,11 +121,12 @@ plugins: show_if_no_docstring: True - minify: minify_html: true - # keep this at the bottom of the plugins list - if you are building without insiders, comment it out - - privacy: - # Downloads all external resources and stores them locally - externals: bundle + + - group: enabled: !ENV [ DEPLOY, False ] + plugins: + - privacy: + externals: bundle markdown_extensions: @@ -141,8 +142,8 @@ markdown_extensions: - pymdownx.inlinehilite - pymdownx.superfences - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.keys - pymdownx.saneheaders - pymdownx.smartsymbols diff --git a/pyproject.toml b/pyproject.toml index 1d28bd18b..903e92f06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "interactions.py" -version = "5.10.0" +version = "5.11.0" description = "Easy, simple, scalable and modular: a Python API wrapper for interactions." authors = [ "LordOfPolls ", diff --git a/setup.py b/setup.py index 02cdc3a52..5aed815d4 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ ], project_urls={ "Discord": "https://discord.gg/KkgMBVuEkx", - "Documentation": "https://naff-docs.readthedocs.io/en/latest/", # TODO: replace + "Documentation": "https://interactions-py.github.io/interactions.py/", }, extras_require=extras_require, )