Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5.14 #1744

Open
wants to merge 8 commits into
base: stable
Choose a base branch
from
Open

5.14 #1744

3 changes: 3 additions & 0 deletions interactions/api/events/processors/_template.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import functools
import inspect
import logging
from typing import TYPE_CHECKING, Callable, Coroutine

from interactions.client.const import Absent, MISSING, AsyncCallable
Expand Down Expand Up @@ -40,7 +41,9 @@ class EventMixinTemplate:

cache: "GlobalCache"
dispatch: Callable[["BaseEvent"], None]
fetch_members: bool
_init_interactions: Callable[[], Coroutine]
logger: logging.Logger
synchronise_interactions: Callable[[], Coroutine]
_user: ClientUser
_guild_event: asyncio.Event
Expand Down
14 changes: 0 additions & 14 deletions interactions/api/events/processors/_template.pyi

This file was deleted.

10 changes: 5 additions & 5 deletions interactions/api/events/processors/auto_mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,24 @@ class AutoModEvents(EventMixinTemplate):
@Processor.define()
async def _raw_auto_moderation_action_execution(self, event: "RawGatewayEvent") -> None:
action = AutoModerationAction.from_dict(event.data.copy(), self)
channel = self.get_channel(event.data.get("channel_id"))
guild = self.get_guild(event.data["guild_id"])
channel = self.cache.get_channel(event.data.get("channel_id"))
guild = self.cache.get_guild(event.data["guild_id"])
self.dispatch(events.AutoModExec(action, channel, guild))

@Processor.define()
async def raw_auto_moderation_rule_create(self, event: "RawGatewayEvent") -> None:
rule = AutoModRule.from_dict(event.data, self)
guild = self.get_guild(event.data["guild_id"])
guild = self.cache.get_guild(event.data["guild_id"])
self.dispatch(events.AutoModCreated(guild, rule))

@Processor.define()
async def raw_auto_moderation_rule_update(self, event: "RawGatewayEvent") -> None:
rule = AutoModRule.from_dict(event.data, self)
guild = self.get_guild(event.data["guild_id"])
guild = self.cache.get_guild(event.data["guild_id"])
self.dispatch(events.AutoModUpdated(guild, rule))

@Processor.define()
async def raw_auto_moderation_rule_delete(self, event: "RawGatewayEvent") -> None:
rule = AutoModRule.from_dict(event.data, self)
guild = self.get_guild(event.data["guild_id"])
guild = self.cache.get_guild(event.data["guild_id"])
self.dispatch(events.AutoModDeleted(guild, rule))
2 changes: 1 addition & 1 deletion interactions/api/events/processors/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def _raw_application_command_permissions_update(self, event: "RawGatewayEv
command_id = to_snowflake(event.data["id"])
application_id = to_snowflake(event.data["application_id"])

if guild := self.get_guild(guild_id):
if guild := self.cache.get_guild(guild_id):
if guild.permissions:
if command_id not in guild.command_permissions:
guild.command_permissions[command_id] = CommandPermissions(
Expand Down
2 changes: 1 addition & 1 deletion interactions/api/events/processors/voice_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
class VoiceEvents(EventMixinTemplate):
@Processor.define()
async def _on_raw_voice_state_update(self, event: "RawGatewayEvent") -> None:
if str(event.data["user_id"]) == str(self.user.id):
if str(event.data["user_id"]) == str(self._user.id):
# User is the bot itself
before = copy.copy(self.cache.get_bot_voice_state(event.data["guild_id"])) or None
after = await self.cache.place_voice_state_data(event.data, update_cache=False)
Expand Down
85 changes: 85 additions & 0 deletions interactions/api/http/http_requests/emojis.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,88 @@ async def delete_guild_emoji(
Route("DELETE", "/guilds/{guild_id}/emojis/{emoji_id}", guild_id=guild_id, emoji_id=emoji_id),
reason=reason,
)

async def get_application_emojis(self, application_id: "Snowflake_Type") -> list[discord_typings.EmojiData]:
"""
Fetch all emojis for this application

Args:
application_id: The id of the application

Returns:
List of emojis

"""
result = await self.request(Route("GET", f"/applications/{application_id}/emojis"))
result = cast(dict, result)
return cast(list[discord_typings.EmojiData], result["items"])

async def get_application_emoji(
self, application_id: "Snowflake_Type", emoji_id: "Snowflake_Type"
) -> discord_typings.EmojiData:
"""
Fetch an emoji for this application

Args:
application_id: The id of the application
emoji_id: The id of the emoji

Returns:
Emoji object

"""
result = await self.request(Route("GET", f"/applications/{application_id}/emojis/{emoji_id}"))
return cast(discord_typings.EmojiData, result)

async def create_application_emoji(
self, payload: dict, application_id: "Snowflake_Type", reason: str | None = None
) -> discord_typings.EmojiData:
"""
Create an emoji for this application

Args:
application_id: The id of the application
name: The name of the emoji
imagefile: The image file to use for the emoji

Returns:
Emoji object

"""
result = await self.request(
Route("POST", f"/applications/{application_id}/emojis"), payload=payload, reason=reason
)
return cast(discord_typings.EmojiData, result)

async def edit_application_emoji(
self, application_id: "Snowflake_Type", emoji_id: "Snowflake_Type", name: str
) -> discord_typings.EmojiData:
"""
Edit an emoji for this application

Args:
application_id: The id of the application
emoji_id: The id of the emoji
name: The new name for the emoji

Returns:
Emoji object

"""
result = await self.request(
Route("PATCH", f"/applications/{application_id}/emojis/{emoji_id}"), payload={"name": name}
)
return cast(discord_typings.EmojiData, result)

async def delete_application_emoji(
self, application_id: discord_typings.Snowflake, emoji_id: discord_typings.Snowflake
) -> None:
"""
Delete an emoji for this application

Args:
application_id: The id of the application
emoji_id: The id of the emoji

"""
await self.request(Route("DELETE", f"/applications/{application_id}/emojis/{emoji_id}"))
6 changes: 5 additions & 1 deletion interactions/api/http/http_requests/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ async def execute_webhook(
payload: dict,
wait: bool = False,
thread_id: "Snowflake_Type" = None,
thread_name: Optional[str] = None,
files: list["UPLOADABLE_TYPE"] | None = None,
) -> Optional[discord_typings.MessageData]:
"""
Expand All @@ -136,13 +137,16 @@ async def execute_webhook(
webhook_token: The token for the webhook
payload: The JSON payload for the message
wait: Waits for server confirmation of message send before response
thread_id: Send a message to the specified thread
thread_id: Send a message to the specified thread. Note that this cannot be used with `thread_name`
thread_name: Create a thread with this name. Note that this is only valid for forum channel and cannot be used with `thread_id`
files: The files to send with this message

Returns:
The sent `message`, if `wait` is True else None

"""
if thread_name is not None:
payload["thread_name"] = thread_name
return await self.request(
Route("POST", "/webhooks/{webhook_id}/{webhook_token}", webhook_id=webhook_id, webhook_token=webhook_token),
params=dict_filter_none({"wait": "true" if wait else "false", "thread_id": thread_id}),
Expand Down
10 changes: 10 additions & 0 deletions interactions/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,16 @@ def guilds(self) -> List["Guild"]:
"""Returns a list of all guilds the bot is in."""
return self.user.guilds

@property
def guild_count(self) -> int:
"""
Returns the number of guilds the bot is in.

This function is faster than using `len(client.guilds)` as it does not require using the cache.
As such, this is preferred when you only need the count of guilds.
"""
return self.user.guild_count

@property
def status(self) -> Status:
"""
Expand Down
6 changes: 3 additions & 3 deletions interactions/client/smart_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ def place_guild_data(self, data: discord_typings.GuildData) -> Guild:

"""
guild_id = to_snowflake(data["id"])
guild: Guild = self.guild_cache.get(guild_id)
guild: Guild | None = self.guild_cache.get(guild_id)
if guild is None:
guild = Guild.from_dict(data, self._client)
self.guild_cache[guild_id] = guild
Expand Down Expand Up @@ -914,7 +914,7 @@ def get_emoji(self, emoji_id: Optional["Snowflake_Type"]) -> Optional["CustomEmo
"""
return self.emoji_cache.get(to_optional_snowflake(emoji_id)) if self.emoji_cache is not None else None

def place_emoji_data(self, guild_id: "Snowflake_Type", data: discord_typings.EmojiData) -> "CustomEmoji":
def place_emoji_data(self, guild_id: "Snowflake_Type | None", data: discord_typings.EmojiData) -> "CustomEmoji":
"""
Take json data representing an emoji, process it, and cache it. This cache is disabled by default, start your bot with `Client(enable_emoji_cache=True)` to enable it.

Expand All @@ -929,7 +929,7 @@ def place_emoji_data(self, guild_id: "Snowflake_Type", data: discord_typings.Emo
with suppress(KeyError):
del data["guild_id"] # discord sometimes packages a guild_id - this will cause an exception

emoji = CustomEmoji.from_dict(data, self._client, to_snowflake(guild_id))
emoji = CustomEmoji.from_dict(data, self._client, to_optional_snowflake(guild_id))
if self.emoji_cache is not None:
self.emoji_cache[emoji.id] = emoji

Expand Down
2 changes: 1 addition & 1 deletion interactions/ext/debug_extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async def debug_info(self, ctx: InteractionContext) -> None:

e.add_field("Loaded Exts", ", ".join(self.bot.ext))

e.add_field("Guilds", str(len(self.bot.guilds)))
e.add_field("Guilds", str(self.bot.guild_count))

await ctx.send(embeds=[e])

Expand Down
35 changes: 35 additions & 0 deletions interactions/models/discord/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

from interactions.client.const import MISSING
from interactions.client.utils.attr_converters import optional
from interactions.client.utils.serializer import to_image_data
from interactions.models.discord.asset import Asset
from interactions.models.discord.emoji import CustomEmoji
from interactions.models.discord.enums import ApplicationFlags
from interactions.models.discord.file import UPLOADABLE_TYPE
from interactions.models.discord.snowflake import Snowflake_Type, to_snowflake
from interactions.models.discord.team import Team
from .base import DiscordObject
Expand Down Expand Up @@ -88,3 +91,35 @@ def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any]
def owner(self) -> "User":
"""The user object for the owner of this application"""
return self._client.cache.get_user(self.owner_id)

async def fetch_all_emoji(self) -> List[CustomEmoji]:
"""Fetch all emojis for this application"""
response = await self.client.http.get_application_emojis(self.id)
return [self.client.cache.place_emoji_data(None, emoji) for emoji in response]

async def fetch_emoji(self, emoji_id: Snowflake_Type) -> CustomEmoji:
"""Fetch an emoji for this application"""
response = await self.client.http.get_application_emoji(self.id, emoji_id)
return self.client.cache.place_emoji_data(None, response)

async def create_emoji(self, name: str, imagefile: UPLOADABLE_TYPE) -> CustomEmoji:
"""Create an emoji for this application"""
data_payload = {
"name": name,
"image": to_image_data(imagefile),
"roles": MISSING,
}

return self.client.cache.place_emoji_data(
None, await self.client.http.create_application_emoji(data_payload, self.id)
)

async def edit_emoji(self, emoji_id: Snowflake_Type, name: str) -> CustomEmoji:
"""Edit an emoji for this application"""
return self.client.cache.place_emoji_data(
None, await self.client.http.edit_application_emoji(self.id, emoji_id, name)
)

async def delete_emoji(self, emoji_id: Snowflake_Type) -> None:
"""Delete an emoji for this application"""
await self.client.http.delete_application_emoji(self.id, emoji_id)
2 changes: 1 addition & 1 deletion interactions/models/discord/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2399,7 +2399,7 @@ async def close_stage(self, reason: Absent[Optional[str]] = MISSING) -> None:


@attrs.define(eq=False, order=False, hash=False, kw_only=True)
class GuildForum(GuildChannel, InvitableMixin):
class GuildForum(GuildChannel, InvitableMixin, WebhookMixin):
available_tags: List[ThreadTag] = attrs.field(repr=False, factory=list)
"""A list of tags available to assign to threads"""
default_reaction_emoji: Optional[DefaultReaction] = attrs.field(repr=False, default=None)
Expand Down
43 changes: 30 additions & 13 deletions interactions/models/discord/emoji.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class PartialEmoji(SnowflakeObject, DictSerializationMixin):
"""The custom emoji name, or standard unicode emoji in string"""
animated: bool = attrs.field(repr=True, default=False)
"""Whether this emoji is animated"""
available: bool = attrs.field(repr=False, default=True)
"""whether this emoji can be used, may be false due to loss of Server Boosts"""

@classmethod
def from_str(cls, emoji_str: str, *, language: str = "alias") -> Optional["PartialEmoji"]:
Expand Down Expand Up @@ -120,7 +122,7 @@ class CustomEmoji(PartialEmoji, ClientObject):
_role_ids: List["Snowflake_Type"] = attrs.field(
repr=False, factory=list, converter=optional(list_converter(to_snowflake))
)
_guild_id: "Snowflake_Type" = attrs.field(repr=False, default=None, converter=to_snowflake)
_guild_id: "Optional[Snowflake_Type]" = attrs.field(repr=False, default=None, converter=optional(to_snowflake))

@classmethod
def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any]:
Expand All @@ -133,13 +135,13 @@ def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any]
return data

@classmethod
def from_dict(cls, data: Dict[str, Any], client: "Client", guild_id: int) -> "CustomEmoji":
def from_dict(cls, data: Dict[str, Any], client: "Client", guild_id: "Optional[Snowflake_Type]") -> "CustomEmoji":
data = cls._process_dict(data, client)
return cls(client=client, guild_id=guild_id, **cls._filter_kwargs(data, cls._get_init_keys()))

@property
def guild(self) -> "Guild":
"""The guild this emoji belongs to."""
def guild(self) -> "Optional[Guild]":
"""The guild this emoji belongs to, if applicable."""
return self._client.cache.get_guild(self._guild_id)

@property
Expand All @@ -160,6 +162,9 @@ def is_usable(self) -> bool:
if not self.available:
return False

if not self._guild_id: # likely an application emoji
return True

guild = self.guild
return any(e_role_id in guild.me._role_ids for e_role_id in self._role_ids)

Expand All @@ -182,14 +187,23 @@ async def edit(
The newly modified custom emoji.

"""
data_payload = dict_filter_none(
{
"name": name,
"roles": to_snowflake_list(roles) if roles else None,
}
)
if self._guild_id:
data_payload = dict_filter_none(
{
"name": name,
"roles": to_snowflake_list(roles) if roles else None,
}
)

updated_data = await self._client.http.modify_guild_emoji(
data_payload, self._guild_id, self.id, reason=reason
)
else:
if roles or reason:
raise ValueError("Cannot specify roles or reason for application emoji.")

updated_data = await self.client.http.edit_application_emoji(self.bot.app.id, self.id, name)

updated_data = await self._client.http.modify_guild_emoji(data_payload, self._guild_id, self.id, reason=reason)
self.update_from_dict(updated_data)
return self

Expand All @@ -202,9 +216,12 @@ async def delete(self, reason: Optional[str] = None) -> None:

"""
if not self._guild_id:
raise ValueError("Cannot delete emoji, no guild id set.")
if reason:
raise ValueError("Cannot specify reason for application emoji.")

await self._client.http.delete_guild_emoji(self._guild_id, self.id, reason=reason)
await self.client.http.delete_application_emoji(self._client.app.id, self.id)
else:
await self._client.http.delete_guild_emoji(self._guild_id, self.id, reason=reason)

@property
def url(self) -> str:
Expand Down
Loading
Loading