Skip to content

Commit 8963704

Browse files
authored
Merge: pull request #1381 from interactions-py/unstable
5.3.0
2 parents c3fc966 + da8a6dc commit 8963704

File tree

18 files changed

+147
-19
lines changed

18 files changed

+147
-19
lines changed

.github/workflows/pytest-push.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
4343
RUN_TESTBOT: ${{ matrix.RUN_TESTBOT }}
4444
run: |
45-
pytest
45+
pytest --cov=./ --cov-report xml:coverage.xml
4646
coverage xml -i
4747
- name: Upload Coverage
4848
run: |

interactions/api/events/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class BaseEvent:
2727
bot: "Client" = attrs.field(repr=False, kw_only=True, default=MISSING)
2828
"""The client instance that dispatched this event."""
2929

30+
@property
31+
def client(self) -> "Client":
32+
"""The client instance that dispatched this event."""
33+
return self.bot
34+
3035
@property
3136
def resolved_name(self) -> str:
3237
"""The name of the event, defaults to the class name if not overridden."""

interactions/api/events/processors/reaction_events.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import interactions.api.events as events
44
from interactions.models import PartialEmoji, Reaction
5+
56
from ._template import EventMixinTemplate, Processor
67

78
if TYPE_CHECKING:
@@ -14,6 +15,8 @@ class ReactionEvents(EventMixinTemplate):
1415
async def _handle_message_reaction_change(self, event: "RawGatewayEvent", add: bool) -> None:
1516
if member := event.data.get("member"):
1617
author = self.cache.place_member_data(event.data.get("guild_id"), member)
18+
elif guild_id := event.data.get("guild_id"):
19+
author = await self.cache.fetch_member(guild_id, event.data.get("user_id"))
1720
else:
1821
author = await self.cache.fetch_user(event.data.get("user_id"))
1922

interactions/client/client.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,11 +571,17 @@ async def _async_wrap(_coro: Listener, _event: BaseEvent, *_args, **_kwargs) ->
571571
):
572572
await self.wait_until_ready()
573573

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

1211+
listener.lazy_parse_params()
1212+
12051213
if listener.event not in self.listeners:
12061214
self.listeners[listener.event] = []
12071215
self.listeners[listener.event].append(listener)

interactions/ext/prefixed_commands/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ async def _register_command(self, event: CallbackAdded) -> None:
239239
@listen("extension_unload")
240240
async def _handle_ext_unload(self, event: ExtensionUnload) -> None:
241241
"""Unregisters all prefixed commands in an extension as it is being unloaded."""
242-
for name in self._ext_command_list[event.extension.extension_name]:
242+
for name in self._ext_command_list[event.extension.extension_name].copy():
243243
self.remove_command(name)
244244

245245
@listen("raw_message_create", is_default_listener=True)

interactions/models/discord/auto_mod.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ class HarmfulLinkFilter(BaseTrigger):
121121
repr=True,
122122
metadata=docs("The type of trigger"),
123123
)
124-
...
125124

126125

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

153152

153+
@attrs.define(eq=False, order=False, hash=False, kw_only=True)
154+
class MemberProfileTrigger(BaseTrigger):
155+
regex_patterns: list[str] = attrs.field(
156+
factory=list, repr=True, metadata=docs("The regex patterns to check against")
157+
)
158+
keyword_filter: str | list[str] = attrs.field(
159+
factory=list, repr=True, metadata=docs("The keywords to check against")
160+
)
161+
allow_list: list["Snowflake_Type"] = attrs.field(
162+
factory=list, repr=True, metadata=docs("The roles exempt from this rule")
163+
)
164+
165+
154166
@attrs.define(eq=False, order=False, hash=False, kw_only=True)
155167
class BlockMessage(BaseAction):
156168
"""blocks the content of a message according to the rule"""
157169

158170
type: AutoModAction = attrs.field(repr=False, default=AutoModAction.BLOCK_MESSAGE, converter=AutoModAction)
159-
...
160171

161172

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

177188

189+
@attrs.define(eq=False, order=False, hash=False, kw_only=False)
190+
class BlockMemberInteraction(BaseAction):
191+
"""Block a member from using text, voice, or other interactions"""
192+
193+
# this action has no metadata
194+
195+
178196
@attrs.define(eq=False, order=False, hash=False, kw_only=True)
179197
class AutoModRule(DiscordObject):
180198
"""A representation of an auto mod rule"""
@@ -345,11 +363,13 @@ def member(self) -> "Optional[Member]":
345363
AutoModAction.BLOCK_MESSAGE: BlockMessage,
346364
AutoModAction.ALERT_MESSAGE: AlertMessage,
347365
AutoModAction.TIMEOUT_USER: TimeoutUser,
366+
AutoModAction.BLOCK_MEMBER_INTERACTION: BlockMemberInteraction,
348367
}
349368

350369
TRIGGER_MAPPING = {
351370
AutoModTriggerType.KEYWORD: KeywordTrigger,
352371
AutoModTriggerType.HARMFUL_LINK: HarmfulLinkFilter,
353372
AutoModTriggerType.KEYWORD_PRESET: KeywordPresetTrigger,
354373
AutoModTriggerType.MENTION_SPAM: MentionSpamTrigger,
374+
AutoModTriggerType.MEMBER_PROFILE: MemberProfileTrigger,
355375
}

interactions/models/discord/channel.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,11 @@ def permission_overwrites(self) -> List["PermissionOverwrite"]:
19011901
"""The permission overwrites for this channel."""
19021902
return []
19031903

1904+
@property
1905+
def clyde_created(self) -> bool:
1906+
"""Whether this thread was created by Clyde."""
1907+
return ChannelFlags.CLYDE_THREAD in self.flags
1908+
19041909
def permissions_for(self, instance: Snowflake_Type) -> Permissions:
19051910
"""
19061911
Calculates permissions for an instance

interactions/models/discord/enums.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,8 @@ class MessageFlags(DiscordIntFlag): # type: ignore
457457
"""This message contains a abusive website link, pops up a warning when clicked"""
458458
SILENT = 1 << 12
459459
"""This message should not trigger push or desktop notifications"""
460+
VOICE_MESSAGE = 1 << 13
461+
"""This message is a voice message"""
460462

461463
# Special members
462464
NONE = 0
@@ -551,6 +553,16 @@ class Permissions(DiscordIntFlag): # type: ignore
551553
"""Allows for using Activities (applications with the `EMBEDDED` flag) in a voice channel"""
552554
MODERATE_MEMBERS = 1 << 40
553555
"""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"""
556+
VIEW_CREATOR_MONETIZATION_ANALYTICS = 1 << 41
557+
"""Allows for viewing guild monetization insights"""
558+
USE_SOUNDBOARD = 1 << 42
559+
"""Allows for using the soundboard in a voice channel"""
560+
CREATE_GUILD_EXPRESSIONS = 1 << 43
561+
"""Allows for creating emojis, stickers, and soundboard sounds"""
562+
USE_EXTERNAL_SOUNDS = 1 << 45
563+
"""Allows the usage of custom sounds from other servers"""
564+
SEND_VOICE_MESSAGES = 1 << 46
565+
"""Allows for sending audio messages"""
554566

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

784798
# Special members
785799
NONE = 0
@@ -964,6 +978,7 @@ class AuditLogEventType(CursedIntEnum):
964978
ONBOARDING_UPDATE = 167
965979
GUILD_HOME_FEATURE_ITEM = 171
966980
GUILD_HOME_FEATURE_ITEM_UPDATE = 172
981+
BLOCKED_PHISHING_LINK = 180
967982
SERVER_GUIDE_CREATE = 190
968983
SERVER_GUIDE_UPDATE = 191
969984

@@ -974,16 +989,19 @@ class AutoModTriggerType(CursedIntEnum):
974989
SPAM = 3
975990
KEYWORD_PRESET = 4
976991
MENTION_SPAM = 5
992+
MEMBER_PROFILE = 6
977993

978994

979995
class AutoModAction(CursedIntEnum):
980996
BLOCK_MESSAGE = 1
981997
ALERT_MESSAGE = 2
982998
TIMEOUT_USER = 3
999+
BLOCK_MEMBER_INTERACTION = 4
9831000

9841001

9851002
class AutoModEvent(CursedIntEnum):
9861003
MESSAGE_SEND = 1
1004+
MEMBER_UPDATE = 2
9871005

9881006

9891007
class AutoModLanuguageType(Enum):

interactions/models/discord/guild.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,21 @@ def voice_states(self) -> List["models.VoiceState"]:
439439
# noinspection PyProtectedMember
440440
return [v_state for v_state in self._client.cache.voice_state_cache.values() if v_state._guild_id == self.id]
441441

442+
@property
443+
def mention_onboarding_customize(self) -> str:
444+
"""Return a mention string for the customise section of Onboarding"""
445+
return "<id:customize>"
446+
447+
@property
448+
def mention_onboarding_browse(self) -> str:
449+
"""Return a mention string for the browse section of Onboarding"""
450+
return "<id:browse>"
451+
452+
@property
453+
def mention_onboarding_guide(self) -> str:
454+
"""Return a mention string for the guide section of Onboarding"""
455+
return "<id:guide>"
456+
442457
async def fetch_member(self, member_id: Snowflake_Type, *, force: bool = False) -> Optional["models.Member"]:
443458
"""
444459
Return the Member with the given discord ID, fetching from the API if necessary.

interactions/models/discord/message.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import base64
23
import re
34
from dataclasses import dataclass
45
from typing import (
@@ -89,12 +90,22 @@ class Attachment(DiscordObject):
8990
"""width of file (if image)"""
9091
ephemeral: bool = attrs.field(repr=False, default=False)
9192
"""whether this attachment is ephemeral"""
93+
duration_secs: Optional[int] = attrs.field(repr=False, default=None)
94+
"""the duration of the audio file (currently for voice messages)"""
95+
waveform: bytearray = attrs.field(repr=False, default=None)
96+
"""base64 encoded bytearray representing a sampled waveform (currently for voice messages)"""
9297

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

103+
@classmethod
104+
def _process_dict(cls, data: Dict[str, Any], _) -> Dict[str, Any]:
105+
if waveform := data.pop("waveform", None):
106+
data["waveform"] = bytearray(base64.b64decode(waveform))
107+
return data
108+
98109

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

383+
@property
384+
def editable(self) -> bool:
385+
"""Whether this message can be edited by the current user"""
386+
if self.author.id == self._client.user.id:
387+
return MessageFlags.VOICE_MESSAGE not in self.flags
388+
return False
389+
372390
async def fetch_referenced_message(self, *, force: bool = False) -> Optional["Message"]:
373391
"""
374392
Fetch the message this message is referencing, if any.

interactions/models/discord/user.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ class BaseUser(DiscordObject, _SendDMMixin):
5151
global_name: str | None = attrs.field(
5252
repr=True, metadata=docs("The user's chosen display name, platform-wide"), default=None
5353
)
54-
discriminator: int = attrs.field(repr=True, metadata=docs("The user's 4-digit discord-tag"))
54+
discriminator: str = attrs.field(
55+
repr=True, metadata=docs("The user's 4-digit discord-tag"), default="0"
56+
) # will likely be removed in future api version
5557
avatar: "Asset" = attrs.field(repr=False, metadata=docs("The user's default avatar"))
5658

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

6973
@property
7074
def tag(self) -> str:
7175
"""Returns the user's Discord tag."""
76+
if self.discriminator == "0":
77+
return f"@{self.username}"
7278
return f"{self.username}#{self.discriminator}"
7379

7480
@property

interactions/models/discord/user.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class _SendDMMixin(SendMixin):
3939
class FakeBaseUserMixin(DiscordObject, _SendDMMixin):
4040
username: str
4141
global_name: str | None
42-
discriminator: int
42+
discriminator: str
4343
avatar: Asset
4444
def __str__(self) -> str: ...
4545
@classmethod

interactions/models/internal/application_commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@ def group(
720720
group_name=name,
721721
group_description=description,
722722
scopes=self.scopes,
723+
default_member_permissions=self.default_member_permissions,
723724
dm_permission=self.dm_permission,
724725
checks=self.checks.copy() if inherit_checks else [],
725726
)

interactions/models/internal/context.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from interactions.models.internal.application_commands import (
3737
OptionType,
3838
CallbackType,
39+
SlashCommandChoice,
3940
SlashCommandOption,
4041
InteractionCommand,
4142
)
@@ -780,6 +781,16 @@ async def edit_origin(
780781
self.message_id = message.id
781782
return message
782783

784+
@property
785+
def component(self) -> typing.Optional[BaseComponent]:
786+
"""The component that was interacted with."""
787+
if self.message is None or self.message.components is None:
788+
return None
789+
for action_row in self.message.components:
790+
for component in action_row.components:
791+
if component.custom_id == self.custom_id:
792+
return component
793+
783794

784795
class ModalContext(InteractionContext):
785796
responses: dict[str, str]
@@ -852,7 +863,9 @@ def option_processing_hook(self, option: dict) -> None:
852863
self.focussed_option = SlashCommandOption.from_dict(option)
853864
return
854865

855-
async def send(self, choices: typing.Iterable[str | int | float | dict[str, int | float | str]]) -> None:
866+
async def send(
867+
self, choices: typing.Iterable[str | int | float | dict[str, int | float | str] | SlashCommandChoice]
868+
) -> None:
856869
"""
857870
Send your autocomplete choices to discord. Choices must be either a list of strings, or a dictionary following the following format:
858871
@@ -882,6 +895,9 @@ async def send(self, choices: typing.Iterable[str | int | float | dict[str, int
882895
if isinstance(choice, dict):
883896
name = choice["name"]
884897
value = choice["value"]
898+
elif isinstance(choice, SlashCommandChoice):
899+
name = choice.name
900+
value = choice.value
885901
else:
886902
name = str(choice)
887903
value = choice

interactions/models/internal/extension.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def __new__(cls, bot: "Client", *args, **kwargs) -> "Extension":
101101
for _name, val in callables:
102102
if isinstance(val, models.BaseCommand):
103103
val.extension = instance
104-
val = wrap_partial(val, instance)
104+
val = val.copy_with_binding(instance)
105105
bot.add_command(val)
106106
instance._commands.append(val)
107107

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

111111
elif isinstance(val, models.Listener):
112112
val.extension = instance
113-
val = wrap_partial(val, instance)
113+
val = val.copy_with_binding(instance)
114114
bot.add_listener(val) # type: ignore
115115
instance._listeners.append(val)
116116
elif isinstance(val, models.GlobalAutoComplete):
117117
val.extension = instance
118-
val = wrap_partial(val, instance)
118+
val = val.copy_with_binding(instance)
119119
bot.add_global_autocomplete(val)
120120
bot.dispatch(events.ExtensionCommandParse(extension=instance, callables=callables))
121121

0 commit comments

Comments
 (0)