Skip to content

Commit

Permalink
Merge: pull request #1381 from interactions-py/unstable
Browse files Browse the repository at this point in the history
5.3.0
  • Loading branch information
LordOfPolls authored May 6, 2023
2 parents c3fc966 + da8a6dc commit 8963704
Show file tree
Hide file tree
Showing 18 changed files with 147 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pytest-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
RUN_TESTBOT: ${{ matrix.RUN_TESTBOT }}
run: |
pytest
pytest --cov=./ --cov-report xml:coverage.xml
coverage xml -i
- name: Upload Coverage
run: |
Expand Down
5 changes: 5 additions & 0 deletions interactions/api/events/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ class BaseEvent:
bot: "Client" = attrs.field(repr=False, kw_only=True, default=MISSING)
"""The client instance that dispatched this event."""

@property
def client(self) -> "Client":
"""The client instance that dispatched this event."""
return self.bot

@property
def resolved_name(self) -> str:
"""The name of the event, defaults to the class name if not overridden."""
Expand Down
3 changes: 3 additions & 0 deletions interactions/api/events/processors/reaction_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import interactions.api.events as events
from interactions.models import PartialEmoji, Reaction

from ._template import EventMixinTemplate, Processor

if TYPE_CHECKING:
Expand All @@ -14,6 +15,8 @@ class ReactionEvents(EventMixinTemplate):
async def _handle_message_reaction_change(self, event: "RawGatewayEvent", add: bool) -> None:
if member := event.data.get("member"):
author = self.cache.place_member_data(event.data.get("guild_id"), member)
elif guild_id := event.data.get("guild_id"):
author = await self.cache.fetch_member(guild_id, event.data.get("user_id"))
else:
author = await self.cache.fetch_user(event.data.get("user_id"))

Expand Down
16 changes: 12 additions & 4 deletions interactions/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,11 +571,17 @@ async def _async_wrap(_coro: Listener, _event: BaseEvent, *_args, **_kwargs) ->
):
await self.wait_until_ready()

if len(_event.__attrs_attrs__) == 2 and coro.event != "event":
# override_name & bot & logging
await _coro()
else:
# don't pass event object if listener doesn't expect it
if _coro.pass_event_object:
await _coro(_event, *_args, **_kwargs)
else:
if not _coro.warned_no_event_arg and len(_event.__attrs_attrs__) > 2 and _coro.event != "event":
self.logger.warning(
f"{_coro} is listening to {_coro.event} event which contains event data. "
f"Add an event argument to this listener to receive the event data object."
)
_coro.warned_no_event_arg = True
await _coro()
except asyncio.CancelledError:
pass
except Exception as e:
Expand Down Expand Up @@ -1202,6 +1208,8 @@ def add_listener(self, listener: Listener) -> None:
self.logger.debug(f"Listener {listener} has already been hooked, not re-hooking it again")
return

listener.lazy_parse_params()

if listener.event not in self.listeners:
self.listeners[listener.event] = []
self.listeners[listener.event].append(listener)
Expand Down
2 changes: 1 addition & 1 deletion interactions/ext/prefixed_commands/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ async def _register_command(self, event: CallbackAdded) -> None:
@listen("extension_unload")
async def _handle_ext_unload(self, event: ExtensionUnload) -> None:
"""Unregisters all prefixed commands in an extension as it is being unloaded."""
for name in self._ext_command_list[event.extension.extension_name]:
for name in self._ext_command_list[event.extension.extension_name].copy():
self.remove_command(name)

@listen("raw_message_create", is_default_listener=True)
Expand Down
24 changes: 22 additions & 2 deletions interactions/models/discord/auto_mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ class HarmfulLinkFilter(BaseTrigger):
repr=True,
metadata=docs("The type of trigger"),
)
...


@attrs.define(eq=False, order=False, hash=False, kw_only=True)
Expand Down Expand Up @@ -151,12 +150,24 @@ class MentionSpamTrigger(BaseTrigger):
)


@attrs.define(eq=False, order=False, hash=False, kw_only=True)
class MemberProfileTrigger(BaseTrigger):
regex_patterns: list[str] = attrs.field(
factory=list, repr=True, metadata=docs("The regex patterns to check against")
)
keyword_filter: str | list[str] = attrs.field(
factory=list, repr=True, metadata=docs("The keywords to check against")
)
allow_list: list["Snowflake_Type"] = attrs.field(
factory=list, repr=True, metadata=docs("The roles exempt from this rule")
)


@attrs.define(eq=False, order=False, hash=False, kw_only=True)
class BlockMessage(BaseAction):
"""blocks the content of a message according to the rule"""

type: AutoModAction = attrs.field(repr=False, default=AutoModAction.BLOCK_MESSAGE, converter=AutoModAction)
...


@attrs.define(eq=False, order=False, hash=False, kw_only=True)
Expand All @@ -175,6 +186,13 @@ class TimeoutUser(BaseAction):
type: AutoModAction = attrs.field(repr=False, default=AutoModAction.TIMEOUT_USER, converter=AutoModAction)


@attrs.define(eq=False, order=False, hash=False, kw_only=False)
class BlockMemberInteraction(BaseAction):
"""Block a member from using text, voice, or other interactions"""

# this action has no metadata


@attrs.define(eq=False, order=False, hash=False, kw_only=True)
class AutoModRule(DiscordObject):
"""A representation of an auto mod rule"""
Expand Down Expand Up @@ -345,11 +363,13 @@ def member(self) -> "Optional[Member]":
AutoModAction.BLOCK_MESSAGE: BlockMessage,
AutoModAction.ALERT_MESSAGE: AlertMessage,
AutoModAction.TIMEOUT_USER: TimeoutUser,
AutoModAction.BLOCK_MEMBER_INTERACTION: BlockMemberInteraction,
}

TRIGGER_MAPPING = {
AutoModTriggerType.KEYWORD: KeywordTrigger,
AutoModTriggerType.HARMFUL_LINK: HarmfulLinkFilter,
AutoModTriggerType.KEYWORD_PRESET: KeywordPresetTrigger,
AutoModTriggerType.MENTION_SPAM: MentionSpamTrigger,
AutoModTriggerType.MEMBER_PROFILE: MemberProfileTrigger,
}
5 changes: 5 additions & 0 deletions interactions/models/discord/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -1901,6 +1901,11 @@ def permission_overwrites(self) -> List["PermissionOverwrite"]:
"""The permission overwrites for this channel."""
return []

@property
def clyde_created(self) -> bool:
"""Whether this thread was created by Clyde."""
return ChannelFlags.CLYDE_THREAD in self.flags

def permissions_for(self, instance: Snowflake_Type) -> Permissions:
"""
Calculates permissions for an instance
Expand Down
18 changes: 18 additions & 0 deletions interactions/models/discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@ class MessageFlags(DiscordIntFlag): # type: ignore
"""This message contains a abusive website link, pops up a warning when clicked"""
SILENT = 1 << 12
"""This message should not trigger push or desktop notifications"""
VOICE_MESSAGE = 1 << 13
"""This message is a voice message"""

# Special members
NONE = 0
Expand Down Expand Up @@ -551,6 +553,16 @@ class Permissions(DiscordIntFlag): # type: ignore
"""Allows for using Activities (applications with the `EMBEDDED` flag) in a voice channel"""
MODERATE_MEMBERS = 1 << 40
"""Allows for timing out users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels"""
VIEW_CREATOR_MONETIZATION_ANALYTICS = 1 << 41
"""Allows for viewing guild monetization insights"""
USE_SOUNDBOARD = 1 << 42
"""Allows for using the soundboard in a voice channel"""
CREATE_GUILD_EXPRESSIONS = 1 << 43
"""Allows for creating emojis, stickers, and soundboard sounds"""
USE_EXTERNAL_SOUNDS = 1 << 45
"""Allows the usage of custom sounds from other servers"""
SEND_VOICE_MESSAGES = 1 << 46
"""Allows for sending audio messages"""

# Shortcuts/grouping/aliases
REQUIRES_MFA = (
Expand Down Expand Up @@ -780,6 +792,8 @@ class SystemChannelFlags(DiscordIntFlag):
class ChannelFlags(DiscordIntFlag):
PINNED = 1 << 1
""" Thread is pinned to the top of its parent forum channel """
CLYDE_THREAD = 1 << 8
"""This thread was created by Clyde"""

# Special members
NONE = 0
Expand Down Expand Up @@ -964,6 +978,7 @@ class AuditLogEventType(CursedIntEnum):
ONBOARDING_UPDATE = 167
GUILD_HOME_FEATURE_ITEM = 171
GUILD_HOME_FEATURE_ITEM_UPDATE = 172
BLOCKED_PHISHING_LINK = 180
SERVER_GUIDE_CREATE = 190
SERVER_GUIDE_UPDATE = 191

Expand All @@ -974,16 +989,19 @@ class AutoModTriggerType(CursedIntEnum):
SPAM = 3
KEYWORD_PRESET = 4
MENTION_SPAM = 5
MEMBER_PROFILE = 6


class AutoModAction(CursedIntEnum):
BLOCK_MESSAGE = 1
ALERT_MESSAGE = 2
TIMEOUT_USER = 3
BLOCK_MEMBER_INTERACTION = 4


class AutoModEvent(CursedIntEnum):
MESSAGE_SEND = 1
MEMBER_UPDATE = 2


class AutoModLanuguageType(Enum):
Expand Down
15 changes: 15 additions & 0 deletions interactions/models/discord/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,21 @@ def voice_states(self) -> List["models.VoiceState"]:
# noinspection PyProtectedMember
return [v_state for v_state in self._client.cache.voice_state_cache.values() if v_state._guild_id == self.id]

@property
def mention_onboarding_customize(self) -> str:
"""Return a mention string for the customise section of Onboarding"""
return "<id:customize>"

@property
def mention_onboarding_browse(self) -> str:
"""Return a mention string for the browse section of Onboarding"""
return "<id:browse>"

@property
def mention_onboarding_guide(self) -> str:
"""Return a mention string for the guide section of Onboarding"""
return "<id:guide>"

async def fetch_member(self, member_id: Snowflake_Type, *, force: bool = False) -> Optional["models.Member"]:
"""
Return the Member with the given discord ID, fetching from the API if necessary.
Expand Down
18 changes: 18 additions & 0 deletions interactions/models/discord/message.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import base64
import re
from dataclasses import dataclass
from typing import (
Expand Down Expand Up @@ -89,12 +90,22 @@ class Attachment(DiscordObject):
"""width of file (if image)"""
ephemeral: bool = attrs.field(repr=False, default=False)
"""whether this attachment is ephemeral"""
duration_secs: Optional[int] = attrs.field(repr=False, default=None)
"""the duration of the audio file (currently for voice messages)"""
waveform: bytearray = attrs.field(repr=False, default=None)
"""base64 encoded bytearray representing a sampled waveform (currently for voice messages)"""

@property
def resolution(self) -> tuple[Optional[int], Optional[int]]:
"""Returns the image resolution of the attachment file"""
return self.height, self.width

@classmethod
def _process_dict(cls, data: Dict[str, Any], _) -> Dict[str, Any]:
if waveform := data.pop("waveform", None):
data["waveform"] = bytearray(base64.b64decode(waveform))
return data


@attrs.define(eq=False, order=False, hash=False, kw_only=True)
class ChannelMention(DiscordObject):
Expand Down Expand Up @@ -369,6 +380,13 @@ def thread(self) -> "models.TYPE_THREAD_CHANNEL":
"""The thread that was started from this message, if any"""
return self._client.cache.get_channel(self.id)

@property
def editable(self) -> bool:
"""Whether this message can be edited by the current user"""
if self.author.id == self._client.user.id:
return MessageFlags.VOICE_MESSAGE not in self.flags
return False

async def fetch_referenced_message(self, *, force: bool = False) -> Optional["Message"]:
"""
Fetch the message this message is referencing, if any.
Expand Down
8 changes: 7 additions & 1 deletion interactions/models/discord/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ class BaseUser(DiscordObject, _SendDMMixin):
global_name: str | None = attrs.field(
repr=True, metadata=docs("The user's chosen display name, platform-wide"), default=None
)
discriminator: int = attrs.field(repr=True, metadata=docs("The user's 4-digit discord-tag"))
discriminator: str = attrs.field(
repr=True, metadata=docs("The user's 4-digit discord-tag"), default="0"
) # will likely be removed in future api version
avatar: "Asset" = attrs.field(repr=False, metadata=docs("The user's default avatar"))

def __str__(self) -> str:
Expand All @@ -62,13 +64,17 @@ def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any]
if not isinstance(data["avatar"], Asset):
if data["avatar"]:
data["avatar"] = Asset.from_path_hash(client, f"avatars/{data['id']}/{{}}", data["avatar"])
elif data["discriminator"] == "0":
data["avatar"] = Asset(client, f"{Asset.BASE}/embed/avatars/{(int(data['id']) >> 22) % 5}")
else:
data["avatar"] = Asset(client, f"{Asset.BASE}/embed/avatars/{int(data['discriminator']) % 5}")
return data

@property
def tag(self) -> str:
"""Returns the user's Discord tag."""
if self.discriminator == "0":
return f"@{self.username}"
return f"{self.username}#{self.discriminator}"

@property
Expand Down
2 changes: 1 addition & 1 deletion interactions/models/discord/user.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class _SendDMMixin(SendMixin):
class FakeBaseUserMixin(DiscordObject, _SendDMMixin):
username: str
global_name: str | None
discriminator: int
discriminator: str
avatar: Asset
def __str__(self) -> str: ...
@classmethod
Expand Down
1 change: 1 addition & 0 deletions interactions/models/internal/application_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ def group(
group_name=name,
group_description=description,
scopes=self.scopes,
default_member_permissions=self.default_member_permissions,
dm_permission=self.dm_permission,
checks=self.checks.copy() if inherit_checks else [],
)
Expand Down
18 changes: 17 additions & 1 deletion interactions/models/internal/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from interactions.models.internal.application_commands import (
OptionType,
CallbackType,
SlashCommandChoice,
SlashCommandOption,
InteractionCommand,
)
Expand Down Expand Up @@ -780,6 +781,16 @@ async def edit_origin(
self.message_id = message.id
return message

@property
def component(self) -> typing.Optional[BaseComponent]:
"""The component that was interacted with."""
if self.message is None or self.message.components is None:
return None
for action_row in self.message.components:
for component in action_row.components:
if component.custom_id == self.custom_id:
return component


class ModalContext(InteractionContext):
responses: dict[str, str]
Expand Down Expand Up @@ -852,7 +863,9 @@ def option_processing_hook(self, option: dict) -> None:
self.focussed_option = SlashCommandOption.from_dict(option)
return

async def send(self, choices: typing.Iterable[str | int | float | dict[str, int | float | str]]) -> None:
async def send(
self, choices: typing.Iterable[str | int | float | dict[str, int | float | str] | SlashCommandChoice]
) -> None:
"""
Send your autocomplete choices to discord. Choices must be either a list of strings, or a dictionary following the following format:
Expand Down Expand Up @@ -882,6 +895,9 @@ async def send(self, choices: typing.Iterable[str | int | float | dict[str, int
if isinstance(choice, dict):
name = choice["name"]
value = choice["value"]
elif isinstance(choice, SlashCommandChoice):
name = choice.name
value = choice.value
else:
name = str(choice)
value = choice
Expand Down
6 changes: 3 additions & 3 deletions interactions/models/internal/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def __new__(cls, bot: "Client", *args, **kwargs) -> "Extension":
for _name, val in callables:
if isinstance(val, models.BaseCommand):
val.extension = instance
val = wrap_partial(val, instance)
val = val.copy_with_binding(instance)
bot.add_command(val)
instance._commands.append(val)

Expand All @@ -110,12 +110,12 @@ def __new__(cls, bot: "Client", *args, **kwargs) -> "Extension":

elif isinstance(val, models.Listener):
val.extension = instance
val = wrap_partial(val, instance)
val = val.copy_with_binding(instance)
bot.add_listener(val) # type: ignore
instance._listeners.append(val)
elif isinstance(val, models.GlobalAutoComplete):
val.extension = instance
val = wrap_partial(val, instance)
val = val.copy_with_binding(instance)
bot.add_global_autocomplete(val)
bot.dispatch(events.ExtensionCommandParse(extension=instance, callables=callables))

Expand Down
Loading

0 comments on commit 8963704

Please sign in to comment.