Skip to content

5.13.2 #1719

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

Merged
merged 14 commits into from
Aug 27, 2024
Merged

5.13.2 #1719

Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions interactions/api/events/processors/message_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ async def _on_raw_message_poll_vote_add(self, event: "RawGatewayEvent") -> None:
"""
self.dispatch(
events.MessagePollVoteAdd(
event.data.get("user_id"),
event.data.get("channel_id"),
event.data.get("message_id"),
event.data.get("answer_id"),
event.data.get("guild_id", None),
event.data["channel_id"],
event.data["message_id"],
event.data["user_id"],
event.data["option"],
)
)

Expand All @@ -114,10 +114,10 @@ async def _on_raw_message_poll_vote_remove(self, event: "RawGatewayEvent") -> No
"""
self.dispatch(
events.MessagePollVoteRemove(
event.data.get("user_id"),
event.data.get("channel_id"),
event.data.get("message_id"),
event.data.get("answer_id"),
event.data.get("guild_id", None),
event.data["channel_id"],
event.data["message_id"],
event.data["user_id"],
event.data["option"],
)
)
2 changes: 1 addition & 1 deletion interactions/api/voice/voice_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ def send_packet(self, data: bytes, encoder, needs_encode=True) -> None:
self.timestamp += encoder.samples_per_frame

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

async def _identify(self) -> None:
Expand Down
1 change: 1 addition & 0 deletions interactions/models/discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ class EmbedType(Enum):
LINK = "link"
AUTOMOD_MESSAGE = "auto_moderation_message"
AUTOMOD_NOTIFICATION = "auto_moderation_notification"
POLL_RESULT = "poll_result"


class MessageActivityType(CursedIntEnum):
Expand Down
2 changes: 1 addition & 1 deletion interactions/models/discord/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any]
@property
def user(self) -> "models.User":
"""Get the user associated with this interaction."""
return self.client.get_user(self.user_id)
return self.client.get_user(self._user_id)


@attrs.define(eq=False, order=False, hash=False, kw_only=False)
Expand Down
2 changes: 1 addition & 1 deletion interactions/models/discord/poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class PollResults(DictSerializationMixin):

@attrs.define(eq=False, order=False, hash=False, kw_only=True)
class Poll(DictSerializationMixin):
question: PollMedia = attrs.field(repr=False)
question: PollMedia = attrs.field(repr=False, converter=PollMedia.from_dict)
"""The question of the poll. Only text media is supported."""
answers: list[PollAnswer] = attrs.field(repr=False, factory=list, converter=PollAnswer.from_list)
"""Each of the answers available in the poll, up to 10."""
Expand Down
25 changes: 12 additions & 13 deletions interactions/models/internal/application_commands.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
from collections import defaultdict
import inspect
import re
import typing
Expand Down Expand Up @@ -288,6 +289,8 @@ def _dm_permission_validator(self, attribute: str, value: bool) -> None:
def to_dict(self) -> dict:
data = super().to_dict()

data["name_localizations"] = self.name.to_locale_dict()

if self.default_member_permissions is not None:
data["default_member_permissions"] = str(int(self.default_member_permissions))
else:
Expand Down Expand Up @@ -1466,9 +1469,9 @@ def application_commands_to_dict( # noqa: C901
`Client.interactions` should be the variable passed to this

"""
cmd_bases = {} # {cmd_base: [commands]}
cmd_bases: defaultdict[str, list[InteractionCommand]] = defaultdict(list) # {cmd_base: [commands]}
"""A store of commands organised by their base command"""
output = {}
output: defaultdict["Snowflake_Type", list[dict]] = defaultdict(list)
"""The output dictionary"""

def squash_subcommand(subcommands: List) -> Dict:
Expand Down Expand Up @@ -1514,9 +1517,6 @@ def squash_subcommand(subcommands: List) -> Dict:
for _scope, cmds in commands.items():
for cmd in cmds.values():
cmd_name = str(cmd.name)
if cmd_name not in cmd_bases:
cmd_bases[cmd_name] = [cmd]
continue
if cmd not in cmd_bases[cmd_name]:
cmd_bases[cmd_name].append(cmd)

Expand Down Expand Up @@ -1556,15 +1556,14 @@ def squash_subcommand(subcommands: List) -> Dict:
cmd.nsfw = nsfw
# end validation of attributes
cmd_data = squash_subcommand(cmd_list)

for s in scopes:
output[s].append(cmd_data)
else:
scopes = cmd_list[0].scopes
cmd_data = cmd_list[0].to_dict()
for s in scopes:
if s not in output:
output[s] = [cmd_data]
continue
output[s].append(cmd_data)
return output
for cmd in cmd_list:
for s in cmd.scopes:
output[s].append(cmd.to_dict())
return dict(output)


def _compare_commands(local_cmd: dict, remote_cmd: dict) -> bool:
Expand Down
6 changes: 3 additions & 3 deletions interactions/models/internal/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ def from_dict(cls, client: "ClientT", payload: dict) -> Self:
instance.guild_locale = payload.get("guild_locale", instance.locale)
instance._context_type = payload.get("type", 0)
instance.resolved = Resolved.from_dict(client, payload["data"].get("resolved", {}), payload.get("guild_id"))
instance.entitlements = Entitlement.from_list(payload["entitlements"], client)
instance.entitlements = Entitlement.from_list(payload.get("entitlements", []), client)
instance.context = ContextType(payload["context"]) if payload.get("context") else None
instance.authorizing_integration_owners = {
IntegrationType(int(integration_type)): Snowflake(owner_id)
Expand Down Expand Up @@ -345,8 +345,8 @@ def author_permissions(self) -> Permissions:
return Permissions(0)

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

@property
def expires_at(self) -> Timestamp:
Expand Down
2 changes: 1 addition & 1 deletion interactions/models/internal/tasks/triggers.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,4 @@ def __init__(self, cron: str, tz: "_TzInfo" = timezone.utc) -> None:
self.tz = tz

def next_fire(self) -> datetime | None:
return croniter(self.cron, datetime.now(tz=self.tz)).next(datetime)
return croniter(self.cron, self.last_call_time.astimezone(self.tz)).next(datetime)
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,12 @@ exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"]

[tool.coverage.run]
omit = ["tests/*"]
source = ["interactions"]

[build-system]
requires = ["setuptools", "tomli"]
build-backend = "setuptools.build_meta"

[tools.coverage.run]
source = ["interactions"]

[tool.pytest.ini_options]
addopts = "-l -ra --durations=2 --junitxml=TestResults.xml"
doctest_optionflags = "NORMALIZE_WHITESPACE"
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
aiohttp
attrs>=22.1
audioop-lts; python_version>='3.13'
croniter
discord-typings>=0.9.0
emoji
Expand Down
93 changes: 92 additions & 1 deletion tests/test_bot.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import asyncio

Check failure on line 1 in tests/test_bot.py

View workflow job for this annotation

GitHub Actions / Pytest Results

test_bot.test_emoji

failed on teardown with "exceptiongroup.ExceptionGroup: errors while tearing down <Module test_bot.py> (2 sub-exceptions)"
Raw output
+ Exception Group Traceback (most recent call last):
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/runner.py", line 341, in from_call
  |     result: TResult | None = func()
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/runner.py", line 242, in <lambda>
  |     lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pluggy/_hooks.py", line 513, in __call__
  |     return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pluggy/_manager.py", line 120, in _hookexec
  |     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pluggy/_callers.py", line 139, in _multicall
  |     raise exception.with_traceback(exception.__traceback__)
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pluggy/_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/threadexception.py", line 97, in pytest_runtest_teardown
  |     yield from thread_exception_runtest_hook()
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/threadexception.py", line 68, in thread_exception_runtest_hook
  |     yield
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pluggy/_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/unraisableexception.py", line 100, in pytest_runtest_teardown
  |     yield from unraisable_exception_runtest_hook()
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/unraisableexception.py", line 70, in unraisable_exception_runtest_hook
  |     yield
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pluggy/_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/logging.py", line 855, in pytest_runtest_teardown
  |     yield from self._runtest_for(item, "teardown")
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/logging.py", line 831, in _runtest_for
  |     yield
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pluggy/_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/capture.py", line 884, in pytest_runtest_teardown
  |     return (yield)
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pluggy/_callers.py", line 103, in _multicall
  |     res = hook_impl.function(*args)
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/runner.py", line 189, in pytest_runtest_teardown
  |     item.session._setupstate.teardown_exact(nextitem)
  |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/runner.py", line 557, in teardown_exact
  |     raise exceptions[0]
  | exceptiongroup.ExceptionGroup: errors while tearing down <Module test_bot.py> (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/runner.py", line 546, in teardown_exact
    |     fin()
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/fixtures.py", line 1031, in finish
    |     raise exceptions[0]
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/fixtures.py", line 1020, in finish
    |     fin()
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pytest_asyncio/plugin.py", line 341, in finalizer
    |     event_loop.run_until_complete(async_finalizer())
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/asyncio/base_events.py", line 624, in run_until_complete
    |     self._check_closed()
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/asyncio/base_events.py", line 515, in _check_closed
    |     raise RuntimeError('Event loop is closed')
    | RuntimeError: Event loop is closed
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/runner.py", line 546, in teardown_exact
    |     fin()
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/fixtures.py", line 1031, in finish
    |     raise exceptions[0]
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/_pytest/fixtures.py", line 1020, in finish
    |     fin()
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/pytest_asyncio/plugin.py", line 341, in finalizer
    |     event_loop.run_until_complete(async_finalizer())
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/asyncio/base_events.py", line 624, in run_until_complete
    |     self._check_closed()
    |   File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/asyncio/base_events.py", line 515, in _check_closed
    |     raise RuntimeError('Event loop is closed')
    | RuntimeError: Event loop is closed
    +------------------------------------
import logging
import os
from asyncio import AbstractEventLoop
from contextlib import suppress
from datetime import datetime
from datetime import datetime, timedelta

import pytest
import pytest_asyncio
Expand Down Expand Up @@ -33,6 +33,8 @@
ParagraphText,
Message,
GuildVoice,
Poll,
PollMedia,
)
from interactions.models.discord.asset import Asset
from interactions.models.discord.components import ActionRow, Button, StringSelectMenu
Expand Down Expand Up @@ -432,6 +434,95 @@
await thread.delete()


@pytest.mark.asyncio
async def test_polls(bot: Client, channel: GuildText) -> None:
msg = await channel.send("Polls Tests")
thread = await msg.create_thread("Test Thread")

try:
poll_1 = Poll.create("Test Poll", duration=1, answers=["Answer 1", "Answer 2"])
test_data_1 = {
"question": {"text": "Test Poll"},
"layout_type": 1,
"duration": 1,
"allow_multiselect": False,
"answers": [{"poll_media": {"text": "Answer 1"}}, {"poll_media": {"text": "Answer 2"}}],
}
poll_1_dict = poll_1.to_dict()
for key in poll_1_dict.keys():
assert poll_1_dict[key] == test_data_1[key]

msg_1 = await thread.send(poll=poll_1)

assert msg_1.poll is not None
assert msg_1.poll.question.to_dict() == PollMedia(text="Test Poll").to_dict()
assert msg_1.poll.expiry <= msg_1.created_at + timedelta(hours=1, minutes=1)
poll_1_answer_medias = [poll_answer.poll_media.to_dict() for poll_answer in msg_1.poll.answers]
assert poll_1_answer_medias == [
PollMedia.create(text="Answer 1").to_dict(),
PollMedia.create(text="Answer 2").to_dict(),
]

poll_2 = Poll.create("Test Poll 2", duration=1, allow_multiselect=True)
poll_2.add_answer("Answer 1")
poll_2.add_answer("Answer 2")
test_data_2 = {
"question": {"text": "Test Poll 2"},
"layout_type": 1,
"duration": 1,
"allow_multiselect": True,
"answers": [{"poll_media": {"text": "Answer 1"}}, {"poll_media": {"text": "Answer 2"}}],
}
poll_2_dict = poll_2.to_dict()
for key in poll_2_dict.keys():
assert poll_2_dict[key] == test_data_2[key]
msg_2 = await thread.send(poll=poll_2)

assert msg_2.poll is not None
assert msg_2.poll.question.to_dict() == PollMedia(text="Test Poll 2").to_dict()
assert msg_2.poll.expiry <= msg_2.created_at + timedelta(hours=1, minutes=1)
assert msg_2.poll.allow_multiselect
poll_2_answer_medias = [poll_answer.poll_media.to_dict() for poll_answer in msg_2.poll.answers]
assert poll_2_answer_medias == [
PollMedia.create(text="Answer 1").to_dict(),
PollMedia.create(text="Answer 2").to_dict(),
]

poll_3 = Poll.create(
"Test Poll 3",
duration=1,
answers=[PollMedia.create(text="One", emoji="1️⃣"), PollMedia.create(text="Two", emoji="2️⃣")],
)
test_data_3 = {
"question": {"text": "Test Poll 3"},
"layout_type": 1,
"duration": 1,
"allow_multiselect": False,
"answers": [
{"poll_media": {"text": "One", "emoji": {"name": "1️⃣", "animated": False}}},
{"poll_media": {"text": "Two", "emoji": {"name": "2️⃣", "animated": False}}},
],
}
poll_3_dict = poll_3.to_dict()
for key in poll_3_dict.keys():
assert poll_3_dict[key] == test_data_3[key]

msg_3 = await thread.send(poll=poll_3)

assert msg_3.poll is not None
assert msg_3.poll.question.to_dict() == PollMedia(text="Test Poll 3").to_dict()
assert msg_3.poll.expiry <= msg_3.created_at + timedelta(hours=1, minutes=1)
poll_3_answer_medias = [poll_answer.poll_media.to_dict() for poll_answer in msg_3.poll.answers]
assert poll_3_answer_medias == [
PollMedia.create(text="One", emoji="1️⃣").to_dict(),
PollMedia.create(text="Two", emoji="2️⃣").to_dict(),
]

finally:
with suppress(interactions.errors.NotFound):
await thread.delete()


@pytest.mark.asyncio
async def test_webhooks(bot: Client, guild: Guild, channel: GuildText) -> None:
test_thread = await channel.create_thread("Test Thread")
Expand Down Expand Up @@ -484,7 +575,7 @@

await asyncio.sleep(2)

assert vc.player._sent_payloads != _before

Check failure on line 578 in tests/test_bot.py

View workflow job for this annotation

GitHub Actions / Pytest Results

test_bot.test_voice

AssertionError: assert 217 != 217 + where 217 = <Player(Thread-4, stopped daemon 140020186859072)>._sent_payloads + where <Player(Thread-4, stopped daemon 140020186859072)> = <ActiveVoiceState: channel=GuildVoice(id=1276807605355937833, name='_test_voice_two-d5dfe8e', type=<ChannelType.GUILD_VOICE: 2>) guild=Guild(id=1041449294022070292, name='NAFF Test Suite', description=None) volume=0.5 playing=False audio=<AudioVolume: tests/test_audio.mp3>>.player
Raw output
bot = <interactions.client.client.Client object at 0x7f59025a23b0>
guild = Guild(id=1041449294022070292, name='NAFF Test Suite', description=None)

    @pytest.mark.asyncio
    async def test_voice(bot: Client, guild: Guild) -> None:
        test_channel = await guild.create_voice_channel(f"_test_voice-{bot.suffix}")
        test_channel_two = await guild.create_voice_channel(f"_test_voice_two-{bot.suffix}")
        try:
            try:
                import nacl  # noqa
            except ImportError:
                # testing on a non-voice extra
                return
    
            vc = await test_channel.connect(deafened=True)
            assert vc == bot.get_bot_voice_state(guild.id)
    
            audio = AudioVolume("tests/test_audio.mp3")
            vc.play_no_wait(audio)
            await asyncio.sleep(5)
    
            assert len(vc.current_audio.buffer) != 0
            assert vc.player._sent_payloads != 0
    
            await vc.move(test_channel_two)
            await asyncio.sleep(2)
    
            _before = vc.player._sent_payloads
    
            await test_channel_two.connect(deafened=True)
    
            await asyncio.sleep(2)
    
>           assert vc.player._sent_payloads != _before
E           AssertionError: assert 217 != 217
E            +  where 217 = <Player(Thread-4, stopped daemon 140020186859072)>._sent_payloads
E            +    where <Player(Thread-4, stopped daemon 140020186859072)> = <ActiveVoiceState: channel=GuildVoice(id=1276807605355937833, name='_test_voice_two-d5dfe8e', type=<ChannelType.GUILD_VOICE: 2>) guild=Guild(id=1041449294022070292, name='NAFF Test Suite', description=None) volume=0.5 playing=False audio=<AudioVolume: tests/test_audio.mp3>>.player

_before    = 217
audio      = <AudioVolume: tests/test_audio.mp3>
bot        = <interactions.client.client.Client object at 0x7f59025a23b0>
guild      = Guild(id=1041449294022070292, name='NAFF Test Suite', description=None)
nacl       = <module 'nacl' from '/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/nacl/__init__.py'>
test_channel = GuildVoice(id=1276807604164886621, name='_test_voice-d5dfe8e', type=<ChannelType.GUILD_VOICE: 2>)
test_channel_two = GuildVoice(id=1276807605355937833, name='_test_voice_two-d5dfe8e', type=<ChannelType.GUILD_VOICE: 2>)
vc         = <ActiveVoiceState: channel=None guild=Guild(id=1041449294022070292, name='NAFF Test Suite', description=None) volume=0.5 playing=False audio=<AudioVolume: tests/test_audio.mp3>>

tests/test_bot.py:578: AssertionError

vc.volume = 1
await asyncio.sleep(1)
Expand Down
Loading