Skip to content

Commit 369a291

Browse files
authored
Merge pull request #1719 from interactions-py/unstable
5.13.2
2 parents acd44d0 + d621ef7 commit 369a291

File tree

13 files changed

+149
-38
lines changed

13 files changed

+149
-38
lines changed

interactions/api/events/processors/message_events.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ async def _on_raw_message_poll_vote_add(self, event: "RawGatewayEvent") -> None:
9595
"""
9696
self.dispatch(
9797
events.MessagePollVoteAdd(
98+
event.data.get("user_id"),
99+
event.data.get("channel_id"),
100+
event.data.get("message_id"),
101+
event.data.get("answer_id"),
98102
event.data.get("guild_id", None),
99-
event.data["channel_id"],
100-
event.data["message_id"],
101-
event.data["user_id"],
102-
event.data["option"],
103103
)
104104
)
105105

@@ -114,10 +114,10 @@ async def _on_raw_message_poll_vote_remove(self, event: "RawGatewayEvent") -> No
114114
"""
115115
self.dispatch(
116116
events.MessagePollVoteRemove(
117+
event.data.get("user_id"),
118+
event.data.get("channel_id"),
119+
event.data.get("message_id"),
120+
event.data.get("answer_id"),
117121
event.data.get("guild_id", None),
118-
event.data["channel_id"],
119-
event.data["message_id"],
120-
event.data["user_id"],
121-
event.data["option"],
122122
)
123123
)

interactions/api/voice/voice_gateway.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ def send_packet(self, data: bytes, encoder, needs_encode=True) -> None:
350350
self.timestamp += encoder.samples_per_frame
351351

352352
async def send_heartbeat(self) -> None:
353-
await self.send_json({"op": OP.HEARTBEAT, "d": random.uniform(0.0, 1.0)})
353+
await self.send_json({"op": OP.HEARTBEAT, "d": random.getrandbits(64)})
354354
self.logger.debug("❤ Voice Connection is sending Heartbeat")
355355

356356
async def _identify(self) -> None:

interactions/client/client.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,9 @@ def add_command(self, func: Callable) -> None:
14891489
elif not isinstance(func, BaseCommand):
14901490
raise TypeError("Invalid command type")
14911491

1492+
for hook in self._add_command_hook:
1493+
hook(func)
1494+
14921495
if not func.callback:
14931496
# for group = SlashCommand(...) usage
14941497
return
@@ -1499,9 +1502,6 @@ def add_command(self, func: Callable) -> None:
14991502
else:
15001503
self.logger.debug(f"Added callback: {func.callback.__name__}")
15011504

1502-
for hook in self._add_command_hook:
1503-
hook(func)
1504-
15051505
self.dispatch(CallbackAdded(callback=func, extension=func.extension if hasattr(func, "extension") else None))
15061506

15071507
def _gather_callbacks(self) -> None:

interactions/ext/hybrid_commands/manager.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,27 @@ def _add_hybrid_command(self, callback: Callable):
8181
return
8282

8383
cmd = callback
84+
85+
if not cmd.callback or cmd._dummy_base:
86+
if cmd.group_name:
87+
if not (group := self.client.prefixed.get_command(f"{cmd.name} {cmd.group_name}")):
88+
group = base_subcommand_generator(
89+
str(cmd.group_name),
90+
list(_values_wrapper(cmd.group_name.to_locale_dict())) + cmd.aliases,
91+
str(cmd.group_name),
92+
group=True,
93+
)
94+
self.client.prefixed.commands[str(cmd.name)].add_command(group)
95+
elif not (base := self.client.prefixed.commands.get(str(cmd.name))):
96+
base = base_subcommand_generator(
97+
str(cmd.name),
98+
list(_values_wrapper(cmd.name.to_locale_dict())) + cmd.aliases,
99+
str(cmd.name),
100+
group=False,
101+
)
102+
self.client.prefixed.add_command(base)
103+
return
104+
84105
prefixed_transform = slash_to_prefixed(cmd)
85106

86107
if self.use_slash_command_msg:
@@ -91,7 +112,7 @@ def _add_hybrid_command(self, callback: Callable):
91112
if not (base := self.client.prefixed.commands.get(str(cmd.name))):
92113
base = base_subcommand_generator(
93114
str(cmd.name),
94-
list(_values_wrapper(cmd.name.to_locale_dict())) + cmd.aliases,
115+
list(_values_wrapper(cmd.name.to_locale_dict())),
95116
str(cmd.name),
96117
group=False,
97118
)
@@ -102,7 +123,7 @@ def _add_hybrid_command(self, callback: Callable):
102123
if not (group := base.subcommands.get(str(cmd.group_name))):
103124
group = base_subcommand_generator(
104125
str(cmd.group_name),
105-
list(_values_wrapper(cmd.group_name.to_locale_dict())) + cmd.aliases,
126+
list(_values_wrapper(cmd.group_name.to_locale_dict())),
106127
str(cmd.group_name),
107128
group=True,
108129
)

interactions/models/discord/enums.py

+1
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ class EmbedType(Enum):
448448
LINK = "link"
449449
AUTOMOD_MESSAGE = "auto_moderation_message"
450450
AUTOMOD_NOTIFICATION = "auto_moderation_notification"
451+
POLL_RESULT = "poll_result"
451452

452453

453454
class MessageActivityType(CursedIntEnum):

interactions/models/discord/message.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any]
275275
@property
276276
def user(self) -> "models.User":
277277
"""Get the user associated with this interaction."""
278-
return self.client.get_user(self.user_id)
278+
return self.client.get_user(self._user_id)
279279

280280

281281
@attrs.define(eq=False, order=False, hash=False, kw_only=False)

interactions/models/discord/poll.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class PollResults(DictSerializationMixin):
8686

8787
@attrs.define(eq=False, order=False, hash=False, kw_only=True)
8888
class Poll(DictSerializationMixin):
89-
question: PollMedia = attrs.field(repr=False)
89+
question: PollMedia = attrs.field(repr=False, converter=PollMedia.from_dict)
9090
"""The question of the poll. Only text media is supported."""
9191
answers: list[PollAnswer] = attrs.field(repr=False, factory=list, converter=PollAnswer.from_list)
9292
"""Each of the answers available in the poll, up to 10."""

interactions/models/internal/application_commands.py

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
from collections import defaultdict
23
import inspect
34
import re
45
import typing
@@ -288,6 +289,8 @@ def _dm_permission_validator(self, attribute: str, value: bool) -> None:
288289
def to_dict(self) -> dict:
289290
data = super().to_dict()
290291

292+
data["name_localizations"] = self.name.to_locale_dict()
293+
291294
if self.default_member_permissions is not None:
292295
data["default_member_permissions"] = str(int(self.default_member_permissions))
293296
else:
@@ -1466,9 +1469,9 @@ def application_commands_to_dict( # noqa: C901
14661469
`Client.interactions` should be the variable passed to this
14671470
14681471
"""
1469-
cmd_bases = {} # {cmd_base: [commands]}
1472+
cmd_bases: defaultdict[str, list[InteractionCommand]] = defaultdict(list) # {cmd_base: [commands]}
14701473
"""A store of commands organised by their base command"""
1471-
output = {}
1474+
output: defaultdict["Snowflake_Type", list[dict]] = defaultdict(list)
14721475
"""The output dictionary"""
14731476

14741477
def squash_subcommand(subcommands: List) -> Dict:
@@ -1514,9 +1517,6 @@ def squash_subcommand(subcommands: List) -> Dict:
15141517
for _scope, cmds in commands.items():
15151518
for cmd in cmds.values():
15161519
cmd_name = str(cmd.name)
1517-
if cmd_name not in cmd_bases:
1518-
cmd_bases[cmd_name] = [cmd]
1519-
continue
15201520
if cmd not in cmd_bases[cmd_name]:
15211521
cmd_bases[cmd_name].append(cmd)
15221522

@@ -1556,15 +1556,14 @@ def squash_subcommand(subcommands: List) -> Dict:
15561556
cmd.nsfw = nsfw
15571557
# end validation of attributes
15581558
cmd_data = squash_subcommand(cmd_list)
1559+
1560+
for s in scopes:
1561+
output[s].append(cmd_data)
15591562
else:
1560-
scopes = cmd_list[0].scopes
1561-
cmd_data = cmd_list[0].to_dict()
1562-
for s in scopes:
1563-
if s not in output:
1564-
output[s] = [cmd_data]
1565-
continue
1566-
output[s].append(cmd_data)
1567-
return output
1563+
for cmd in cmd_list:
1564+
for s in cmd.scopes:
1565+
output[s].append(cmd.to_dict())
1566+
return dict(output)
15681567

15691568

15701569
def _compare_commands(local_cmd: dict, remote_cmd: dict) -> bool:

interactions/models/internal/context.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def from_dict(cls, client: "ClientT", payload: dict) -> Self:
296296
instance.guild_locale = payload.get("guild_locale", instance.locale)
297297
instance._context_type = payload.get("type", 0)
298298
instance.resolved = Resolved.from_dict(client, payload["data"].get("resolved", {}), payload.get("guild_id"))
299-
instance.entitlements = Entitlement.from_list(payload["entitlements"], client)
299+
instance.entitlements = Entitlement.from_list(payload.get("entitlements", []), client)
300300
instance.context = ContextType(payload["context"]) if payload.get("context") else None
301301
instance.authorizing_integration_owners = {
302302
IntegrationType(int(integration_type)): Snowflake(owner_id)
@@ -345,8 +345,8 @@ def author_permissions(self) -> Permissions:
345345
return Permissions(0)
346346

347347
@property
348-
def command(self) -> InteractionCommand:
349-
return self.client._interaction_lookup[self._command_name]
348+
def command(self) -> typing.Optional[InteractionCommand]:
349+
return self.client._interaction_lookup.get(self._command_name)
350350

351351
@property
352352
def expires_at(self) -> Timestamp:

interactions/models/internal/tasks/triggers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,4 @@ def __init__(self, cron: str, tz: "_TzInfo" = timezone.utc) -> None:
163163
self.tz = tz
164164

165165
def next_fire(self) -> datetime | None:
166-
return croniter(self.cron, datetime.now(tz=self.tz)).next(datetime)
166+
return croniter(self.cron, self.last_call_time.astimezone(self.tz)).next(datetime)

pyproject.toml

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "interactions.py"
3-
version = "5.13.1"
3+
version = "5.13.2"
44
description = "Easy, simple, scalable and modular: a Python API wrapper for interactions."
55
authors = ["LordOfPolls <[email protected]>"]
66

@@ -93,14 +93,12 @@ exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"]
9393

9494
[tool.coverage.run]
9595
omit = ["tests/*"]
96+
source = ["interactions"]
9697

9798
[build-system]
9899
requires = ["setuptools", "tomli"]
99100
build-backend = "setuptools.build_meta"
100101

101-
[tools.coverage.run]
102-
source = ["interactions"]
103-
104102
[tool.pytest.ini_options]
105103
addopts = "-l -ra --durations=2 --junitxml=TestResults.xml"
106104
doctest_optionflags = "NORMALIZE_WHITESPACE"

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
aiohttp
22
attrs>=22.1
3+
audioop-lts; python_version>='3.13'
34
croniter
45
discord-typings>=0.9.0
56
emoji

tests/test_bot.py

+92-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
from asyncio import AbstractEventLoop
55
from contextlib import suppress
6-
from datetime import datetime
6+
from datetime import datetime, timedelta
77

88
import pytest
99
import pytest_asyncio
@@ -33,6 +33,8 @@
3333
ParagraphText,
3434
Message,
3535
GuildVoice,
36+
Poll,
37+
PollMedia,
3638
)
3739
from interactions.models.discord.asset import Asset
3840
from interactions.models.discord.components import ActionRow, Button, StringSelectMenu
@@ -432,6 +434,95 @@ async def test_components(bot: Client, channel: GuildText) -> None:
432434
await thread.delete()
433435

434436

437+
@pytest.mark.asyncio
438+
async def test_polls(bot: Client, channel: GuildText) -> None:
439+
msg = await channel.send("Polls Tests")
440+
thread = await msg.create_thread("Test Thread")
441+
442+
try:
443+
poll_1 = Poll.create("Test Poll", duration=1, answers=["Answer 1", "Answer 2"])
444+
test_data_1 = {
445+
"question": {"text": "Test Poll"},
446+
"layout_type": 1,
447+
"duration": 1,
448+
"allow_multiselect": False,
449+
"answers": [{"poll_media": {"text": "Answer 1"}}, {"poll_media": {"text": "Answer 2"}}],
450+
}
451+
poll_1_dict = poll_1.to_dict()
452+
for key in poll_1_dict.keys():
453+
assert poll_1_dict[key] == test_data_1[key]
454+
455+
msg_1 = await thread.send(poll=poll_1)
456+
457+
assert msg_1.poll is not None
458+
assert msg_1.poll.question.to_dict() == PollMedia(text="Test Poll").to_dict()
459+
assert msg_1.poll.expiry <= msg_1.created_at + timedelta(hours=1, minutes=1)
460+
poll_1_answer_medias = [poll_answer.poll_media.to_dict() for poll_answer in msg_1.poll.answers]
461+
assert poll_1_answer_medias == [
462+
PollMedia.create(text="Answer 1").to_dict(),
463+
PollMedia.create(text="Answer 2").to_dict(),
464+
]
465+
466+
poll_2 = Poll.create("Test Poll 2", duration=1, allow_multiselect=True)
467+
poll_2.add_answer("Answer 1")
468+
poll_2.add_answer("Answer 2")
469+
test_data_2 = {
470+
"question": {"text": "Test Poll 2"},
471+
"layout_type": 1,
472+
"duration": 1,
473+
"allow_multiselect": True,
474+
"answers": [{"poll_media": {"text": "Answer 1"}}, {"poll_media": {"text": "Answer 2"}}],
475+
}
476+
poll_2_dict = poll_2.to_dict()
477+
for key in poll_2_dict.keys():
478+
assert poll_2_dict[key] == test_data_2[key]
479+
msg_2 = await thread.send(poll=poll_2)
480+
481+
assert msg_2.poll is not None
482+
assert msg_2.poll.question.to_dict() == PollMedia(text="Test Poll 2").to_dict()
483+
assert msg_2.poll.expiry <= msg_2.created_at + timedelta(hours=1, minutes=1)
484+
assert msg_2.poll.allow_multiselect
485+
poll_2_answer_medias = [poll_answer.poll_media.to_dict() for poll_answer in msg_2.poll.answers]
486+
assert poll_2_answer_medias == [
487+
PollMedia.create(text="Answer 1").to_dict(),
488+
PollMedia.create(text="Answer 2").to_dict(),
489+
]
490+
491+
poll_3 = Poll.create(
492+
"Test Poll 3",
493+
duration=1,
494+
answers=[PollMedia.create(text="One", emoji="1️⃣"), PollMedia.create(text="Two", emoji="2️⃣")],
495+
)
496+
test_data_3 = {
497+
"question": {"text": "Test Poll 3"},
498+
"layout_type": 1,
499+
"duration": 1,
500+
"allow_multiselect": False,
501+
"answers": [
502+
{"poll_media": {"text": "One", "emoji": {"name": "1️⃣", "animated": False}}},
503+
{"poll_media": {"text": "Two", "emoji": {"name": "2️⃣", "animated": False}}},
504+
],
505+
}
506+
poll_3_dict = poll_3.to_dict()
507+
for key in poll_3_dict.keys():
508+
assert poll_3_dict[key] == test_data_3[key]
509+
510+
msg_3 = await thread.send(poll=poll_3)
511+
512+
assert msg_3.poll is not None
513+
assert msg_3.poll.question.to_dict() == PollMedia(text="Test Poll 3").to_dict()
514+
assert msg_3.poll.expiry <= msg_3.created_at + timedelta(hours=1, minutes=1)
515+
poll_3_answer_medias = [poll_answer.poll_media.to_dict() for poll_answer in msg_3.poll.answers]
516+
assert poll_3_answer_medias == [
517+
PollMedia.create(text="One", emoji="1️⃣").to_dict(),
518+
PollMedia.create(text="Two", emoji="2️⃣").to_dict(),
519+
]
520+
521+
finally:
522+
with suppress(interactions.errors.NotFound):
523+
await thread.delete()
524+
525+
435526
@pytest.mark.asyncio
436527
async def test_webhooks(bot: Client, guild: Guild, channel: GuildText) -> None:
437528
test_thread = await channel.create_thread("Test Thread")

0 commit comments

Comments
 (0)