From 1ed6fcfab25f939a1f432222946c5044d2993911 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Sun, 19 May 2024 19:47:05 +0700 Subject: [PATCH 01/72] Initial commit on implementing polls --- hikari/polls.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hikari/polls.py diff --git a/hikari/polls.py b/hikari/polls.py new file mode 100644 index 0000000000..e69de29bb2 From 2c53e902900c096d9cd4ddfc9d5c852e21c4833a Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Sun, 19 May 2024 19:51:40 +0700 Subject: [PATCH 02/72] Towncrier chore --- changes/1922.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1922.feature.md diff --git a/changes/1922.feature.md b/changes/1922.feature.md new file mode 100644 index 0000000000..726d30bb4f --- /dev/null +++ b/changes/1922.feature.md @@ -0,0 +1 @@ +Add support for built-in polls From 89088f1d245d26f91827186565c34c7ded0e43a9 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Sun, 19 May 2024 20:35:15 +0700 Subject: [PATCH 03/72] New poll endpoints --- hikari/internal/routes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py index 0de1038050..c4ba700d7c 100644 --- a/hikari/internal/routes.py +++ b/hikari/internal/routes.py @@ -340,6 +340,10 @@ def compile_to_file( POST_CHANNEL_WEBHOOKS: typing.Final[Route] = Route(POST, "/channels/{channel}/webhooks") GET_CHANNEL_WEBHOOKS: typing.Final[Route] = Route(GET, "/channels/{channel}/webhooks") +# Polls +GET_ANSWER_VOTERS: typing.Final[Route] = Route(GET, "/channels/{channel}/polls/{message}/answer/{answer}") +END_POLL: typing.Final[Route] = Route(POST, "/channels/{channel}/polls/{message}/expire") + # Reactions GET_REACTIONS: typing.Final[Route] = Route(GET, "/channels/{channel}/messages/{message}/reactions/{emoji}") DELETE_ALL_REACTIONS: typing.Final[Route] = Route(DELETE, "/channels/{channel}/messages/{message}/reactions") From 2888d76651225112371ab5a78a9195696b8c0ada Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 20 May 2024 16:33:35 +0700 Subject: [PATCH 04/72] Ensure route naming consistency --- hikari/internal/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py index c4ba700d7c..d8b1c456d2 100644 --- a/hikari/internal/routes.py +++ b/hikari/internal/routes.py @@ -342,7 +342,7 @@ def compile_to_file( # Polls GET_ANSWER_VOTERS: typing.Final[Route] = Route(GET, "/channels/{channel}/polls/{message}/answer/{answer}") -END_POLL: typing.Final[Route] = Route(POST, "/channels/{channel}/polls/{message}/expire") +POST_END_POLL: typing.Final[Route] = Route(POST, "/channels/{channel}/polls/{message}/expire") # Reactions GET_REACTIONS: typing.Final[Route] = Route(GET, "/channels/{channel}/messages/{message}/reactions/{emoji}") From f31effbf9d037b5d0ae04481a37316045ba8a548 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 20 May 2024 21:05:29 +0700 Subject: [PATCH 05/72] Basic implementation of poll-related classes --- hikari/polls.py | 332 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) diff --git a/hikari/polls.py b/hikari/polls.py index e69de29bb2..f11ff212fa 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2024-present PythonTryHard +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Polls and poll-related objects.""" # TODO: Improve this docstring + +from __future__ import annotations + +__all__: typing.Sequence[str] = ( + "PollMedia", + "PollAnswer", + "PollResult", + "PollAnswerCount", + "PollLayoutType", + "PartialPoll", + "PollCreate", + "PollObject", +) + +import typing + +import attrs + +from hikari.emojis import Emoji +from hikari.internal import attrs_extensions +from hikari.internal import enums + +if typing.TYPE_CHECKING: + import datetime + + +@attrs_extensions.with_copy +@attrs.define(hash=False, kw_only=True, weakref_slot=False) +class PollMedia: + """Common object backing a poll's questions and answers.""" + + text: typing.Optional[str] = attrs.field(default=None, repr=True) + """The text of the element, or [`None`][] if not present.""" + + emoji: typing.Optional[Emoji] = attrs.field(default=None, repr=True) + """The emoji of the element, or [`None`][] if not present.""" + + +@attrs_extensions.with_copy +@attrs.define(hash=False, kw_only=True, weakref_slot=False) +class PollAnswer: + """Represents an answer to a poll.""" + + answer_id: int = attrs.field(repr=True) + """The ID that labels this answer.""" # Is this user-settable? + + poll_media: PollMedia = attrs.field(repr=True) + """The [media][hikari.polls.PollMedia] associated with this answer.""" + + +@attrs_extensions.with_copy +@attrs.define(hash=False, kw_only=True, weakref_slot=False) +class PollResult: + """Represents a poll result.""" + + __slots__: typing.Sequence[str] = ("_is_finalized", "_answer_counts") + + is_finalized: bool = attrs.field(repr=True) + """Whether the poll is finalized and the votes are precisely counted.""" + + answer_counts: typing.Sequence[PollAnswerCount] = attrs.field(repr=True) + """The counts for each answer.""" + + +@attrs_extensions.with_copy +@attrs.define(hash=False, kw_only=True, weakref_slot=False) +class PollAnswerCount: + """Represents the count of a poll answer.""" + + answer_id: int = attrs.field(repr=True) + """The ID of the answer.""" + + count: int = attrs.field(repr=True) + """The number of votes for this answer.""" + + me_voted: bool = attrs.field(repr=True) + """Whether the current user voted for this answer.""" + + +class PollLayoutType(int, enums.Enum): + """Layout of a poll.""" + + DEFAULT = 1 + """The default layout of a poll.""" + + +class PartialPoll: + """Base class for all poll objects.""" + + __slots__: typing.Sequence[str] = ("_question", "_answers", "_allow_multiselect", "_layout_type", "_counter",) + + def __init__( + self, + question: str, + allow_multiselect: bool, + layout_type: typing.Union[int, PollLayoutType], + ): + self._question = PollMedia(text=question) # Only text is supported for question + self._allow_multiselect = allow_multiselect + self._layout_type = layout_type + + # Answer is required, but we want users to user add_answer() instead of + # providing at initialization. + # + # Considering that answer ID can be arbitrary, `list`-based approaches + # like that of hikari.embeds.Embed._fields, while feasible to implement, + # would decrease long-term maintainability. I'm opting to use a `dict` + # here to simplify the implementation with some performance trade-off + # due to hashmap overhead. + self._answers: typing.MutableMapping[int, PollAnswer] = {} # TODO: Do we need to set to None? + + # Counter to keep track of the answer IDs + # + # Discord (at the time of writing this) starts at 1, I'm opting to follow + # the behaviour. + self._counter = 1 + + @property + def question(self) -> PollMedia: + """Returns the question of the poll.""" + return self._question + + @question.setter + def question(self, value: str) -> None: + self._question = PollMedia(text=value) + + @property + def allow_multiselect(self) -> bool: + """Returns whether the poll allows multiple answers.""" + return self._allow_multiselect + + @allow_multiselect.setter + def allow_multiselect(self, value: bool) -> None: + self._allow_multiselect = value + + @property + def layout_type(self) -> PollLayoutType: + """Returns the layout type of the poll.""" + return PollLayoutType(self._layout_type) + + @layout_type.setter + def layout_type(self, value: typing.Union[int, PollLayoutType]) -> None: + self._layout_type = value + + @property + def answers(self) -> typing.MutableMapping[int, PollAnswer]: + """Returns the answers of the poll. + + !!! note + Use [`hikari.polls.Poll.add_answer`][] to add a new answer, + [`hikari.polls.Poll.edit_answer`][] to edit an existing answer, or + [`hikari.polls.Poll.remove_answer`][] to remove a answer. + """ + return self._answers + + +class PollCreate(PartialPoll): + """Used to create a poll.""" # TODO: Improve this docstring + + __slots__: typing.Sequence[str] = ("_duration",) + + def __init__( + self, + question: str, + duration: int, + allow_multiselect: bool, + layout_type: typing.Union[int, PollLayoutType] = PollLayoutType.DEFAULT, + ): + + super().__init__( + question=question, + allow_multiselect=allow_multiselect, + layout_type=layout_type + ) + self._duration = duration + + @property + def duration(self) -> int: + """Returns the duration of the poll.""" + return self._duration + + @duration.setter + def duration(self, value: int) -> None: + self._duration = value + + def add_answer(self, text: str, emoji: typing.Optional[Emoji]) -> PartialPoll: + """ + Add an answer to the poll. + + Parameters + ---------- + text + The text of the answer to add. + emoji + The emoji associated with the answer. + + Returns + ------- + PartialPoll + This poll. Allows for call chaining. + """ + answer_id = self._counter + self._counter += 1 + + new_answer = PollAnswer(answer_id=answer_id, poll_media=PollMedia(text=text, emoji=emoji)) + self._answers[answer_id] = new_answer + + return self + + def edit_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) -> PartialPoll: + """ + Edit an answer in the poll. + + Parameters + ---------- + answer_id + The ID of the answer to edit. + text + The new text of the answer. + emoji + The new emoji associated with the answer. + + Returns + ------- + PartialPoll + This poll. Allows for call chaining. + + Raises + ------ + KeyError + Raised when the answer ID is not found in the poll. + """ + answer = self._answers.get(answer_id, None) + if answer is None: + raise KeyError(f"Answer ID {answer_id} not found in the poll.") + + new_poll_media = PollMedia(text=text, emoji=emoji) + answer.poll_media = new_poll_media + + return self + + def remove_answer(self, answer_id: int) -> PartialPoll: + """ + Remove an answer from the poll. + + Parameters + ---------- + answer_id + The ID of the answer to remove. + + Returns + ------- + PartialPoll + This poll. Allows for call chaining. + + Raises + ------ + KeyError + Raised when the answer ID is not found in the poll. + """ + if answer_id not in self._answers: + raise KeyError(f"Answer ID {answer_id} not found in the poll.") + + del self._answers[answer_id] + + return self + + +class PollObject(PartialPoll): + """Represents an existing poll.""" + + __slots__: typing.Sequence[str] = ( + "_expiry", + "_results", + ) + + def __init__( + self, + question: str, + answers: typing.MutableMapping[int, PollAnswer], + allow_multiselect: bool, + expiry: datetime.datetime, + results: typing.Optional[PollResult], + layout_type: typing.Union[int, PollLayoutType] = PollLayoutType.DEFAULT, + ): + super().__init__( + question=question, + allow_multiselect=allow_multiselect, + layout_type=layout_type + ) + self._answers = answers + self._expiry = expiry + self._results = results + + @property + def expiry(self) -> datetime.datetime: + """Returns whether the poll has expired.""" + return self._expiry + + @property + def results(self) -> typing.Optional[PollResult]: + """Returns the result of the poll. + + !!! note + According to Discord, their backend does not always return `results`, + this is meant to be interpreted as "unknown result" rather than "no + result". Please refer to the [official documentation](https://discord.com/developers/docs/resources/poll#poll-results-object) + for more information. + """ + return self._results From 36e5963f74a5aa2b3473fc76df61228a3619c678 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 20 May 2024 21:08:25 +0700 Subject: [PATCH 06/72] I forgor to format my code --- hikari/polls.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index f11ff212fa..5ed941b392 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -109,14 +109,9 @@ class PollLayoutType(int, enums.Enum): class PartialPoll: """Base class for all poll objects.""" - __slots__: typing.Sequence[str] = ("_question", "_answers", "_allow_multiselect", "_layout_type", "_counter",) + __slots__: typing.Sequence[str] = ("_question", "_answers", "_allow_multiselect", "_layout_type", "_counter") - def __init__( - self, - question: str, - allow_multiselect: bool, - layout_type: typing.Union[int, PollLayoutType], - ): + def __init__(self, question: str, allow_multiselect: bool, layout_type: typing.Union[int, PollLayoutType]): self._question = PollMedia(text=question) # Only text is supported for question self._allow_multiselect = allow_multiselect self._layout_type = layout_type @@ -189,11 +184,7 @@ def __init__( layout_type: typing.Union[int, PollLayoutType] = PollLayoutType.DEFAULT, ): - super().__init__( - question=question, - allow_multiselect=allow_multiselect, - layout_type=layout_type - ) + super().__init__(question=question, allow_multiselect=allow_multiselect, layout_type=layout_type) self._duration = duration @property @@ -291,10 +282,7 @@ def remove_answer(self, answer_id: int) -> PartialPoll: class PollObject(PartialPoll): """Represents an existing poll.""" - __slots__: typing.Sequence[str] = ( - "_expiry", - "_results", - ) + __slots__: typing.Sequence[str] = ("_expiry", "_results") def __init__( self, @@ -305,11 +293,7 @@ def __init__( results: typing.Optional[PollResult], layout_type: typing.Union[int, PollLayoutType] = PollLayoutType.DEFAULT, ): - super().__init__( - question=question, - allow_multiselect=allow_multiselect, - layout_type=layout_type - ) + super().__init__(question=question, allow_multiselect=allow_multiselect, layout_type=layout_type) self._answers = answers self._expiry = expiry self._results = results @@ -326,7 +310,8 @@ def results(self) -> typing.Optional[PollResult]: !!! note According to Discord, their backend does not always return `results`, this is meant to be interpreted as "unknown result" rather than "no - result". Please refer to the [official documentation](https://discord.com/developers/docs/resources/poll#poll-results-object) + result". Please refer to the + [official documentation](https://discord.com/developers/docs/resources/poll#poll-results-object) for more information. """ return self._results From b7979c42fe45da375e3aa2a83997d55dc8d44839 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 20 May 2024 21:31:30 +0700 Subject: [PATCH 07/72] Poll-specific events --- hikari/events/poll_events.py | 104 +++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 hikari/events/poll_events.py diff --git a/hikari/events/poll_events.py b/hikari/events/poll_events.py new file mode 100644 index 0000000000..e68da39da8 --- /dev/null +++ b/hikari/events/poll_events.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2024-present PythonTryHard +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Events related to polls.""" + +from __future__ import annotations + +__all__: typing.Sequence[str] = ("PollVoteAdd", "PollVoteRemove") + +import typing + +import attrs + +from hikari.events import shard_events +from hikari.internal import attrs_extensions + +if typing.TYPE_CHECKING: + from hikari import snowflakes + from hikari import traits + from hikari.api import shard as gateway_shard + + +@attrs_extensions.with_copy +@attrs.define(kw_only=True, weakref_slot=False) +class PollVoteAdd(shard_events.ShardEvent): + """Event that is fired when a user add their vote to a poll. + + If the poll allows multiple selection, one event will be fired for each vote. + """ + + app: traits.RESTAware = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) + # <>. + + shard: gateway_shard.GatewayShard = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) + # <>. + + user_id: snowflakes.Snowflake = attrs.field() + """ID of the user that added their vote to the poll.""" + + channel_id: snowflakes.Snowflake = attrs.field() + """ID of the channel that the poll is in.""" + + message_id: snowflakes.Snowflake = attrs.field() + """ID of the message that the poll is in.""" + + guild_id: typing.Optional[snowflakes.Snowflake] = attrs.field(default=None) + """ID of the guild that the poll is in. + + This will be [None][] if the poll is in a DM channel. + """ + + answer_id: int = attrs.field() + """ID of the answer that the user voted for.""" + + +@attrs_extensions.with_copy +@attrs.define(kw_only=True, weakref_slot=False) +class PollVoteRemove(shard_events.ShardEvent): + """Event that is fired when a user remove their vote to a poll. + + If the poll allows multiple selection, one event will be fired for each vote. + """ + + app: traits.RESTAware = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) + # <>. + + shard: gateway_shard.GatewayShard = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) + # <>. + + user_id: snowflakes.Snowflake = attrs.field() + """ID of the user that removed their vote from the poll.""" + + channel_id: snowflakes.Snowflake = attrs.field() + """ID of the channel that the poll is in.""" + + message_id: snowflakes.Snowflake = attrs.field() + """ID of the message that the poll is in.""" + + guild_id: typing.Optional[snowflakes.Snowflake] = attrs.field(default=None) + """ID of the guild that the poll is in. + + This will be [None][] if the poll is in a DM channel. + """ + + answer_id: int = attrs.field() + """ID of the answer that the user remove their vote from.""" From a32ac57330dcd3efa154f46f182a4f51d960b370 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 20 May 2024 21:37:34 +0700 Subject: [PATCH 08/72] Only import hikari.Emoji on type checking --- hikari/polls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hikari/polls.py b/hikari/polls.py index 5ed941b392..13ffdc2000 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -38,13 +38,14 @@ import attrs -from hikari.emojis import Emoji from hikari.internal import attrs_extensions from hikari.internal import enums if typing.TYPE_CHECKING: import datetime + from hikari.emojis import Emoji + @attrs_extensions.with_copy @attrs.define(hash=False, kw_only=True, weakref_slot=False) From 153fab3997ebb8864c7a8e1c2380e9520982ce22 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 20 May 2024 21:54:21 +0700 Subject: [PATCH 09/72] Remove erroneous `__slots__` from PollResult Having an `attrs` class with `weakref_slot=False` at the same time as a predefined `__slots__` will cause `attrs` to throw a fit: TypeError: 'property' object is not iterable --- hikari/polls.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index 13ffdc2000..48ab43901e 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -76,8 +76,6 @@ class PollAnswer: class PollResult: """Represents a poll result.""" - __slots__: typing.Sequence[str] = ("_is_finalized", "_answer_counts") - is_finalized: bool = attrs.field(repr=True) """Whether the poll is finalized and the votes are precisely counted.""" From 3daae579c36f49dd1202f45274857e03759d7019 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 20 May 2024 21:54:53 +0700 Subject: [PATCH 10/72] Integrate poll classes and events into hikari --- hikari/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hikari/__init__.py b/hikari/__init__.py index 33a19959ad..a138b55ace 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -86,6 +86,7 @@ from hikari.events.member_events import * from hikari.events.message_events import * from hikari.events.monetization_events import * +from hikari.events.poll_events import * from hikari.events.reaction_events import * from hikari.events.role_events import * from hikari.events.scheduled_events import * @@ -116,6 +117,7 @@ from hikari.messages import * from hikari.monetization import * from hikari.permissions import * +from hikari.polls import * from hikari.presences import * from hikari.scheduled_events import * from hikari.sessions import * From aaab4c4977a0d55570a73d451e0690b889ecc060 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 27 May 2024 11:11:11 +0700 Subject: [PATCH 11/72] Implement poll in EntityFactory and REST --- hikari/api/entity_factory.py | 31 ++++++++++++++ hikari/api/rest.py | 4 ++ hikari/impl/entity_factory.py | 75 ++++++++++++++++++++++++++++++++++ hikari/impl/rest.py | 10 +++++ hikari/polls.py | 15 +++++-- tests/hikari/impl/test_rest.py | 2 + 6 files changed, 134 insertions(+), 3 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 5eb97f5df4..24a923c5b5 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -42,6 +42,7 @@ from hikari import invites as invite_models from hikari import messages as message_models from hikari import monetization as entitlement_models + from hikari import polls as poll_models from hikari import presences as presence_models from hikari import scheduled_events as scheduled_events_models from hikari import sessions as gateway_models @@ -1967,3 +1968,33 @@ def deserialize_sku(self, payload: data_binding.JSONObject) -> entitlement_model hikari.monetization.SKU The deserialized SKU object. """ + + @abc.abstractmethod + def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.PollObject: + """Parse a raw payload from Discord into a poll object. + + Parameters + ---------- + payload + The JSON payload to deserialize. + + Returns + ------- + hikari.polls.PollObject + The deserialized poll object. + """ + + @abc.abstractmethod + def serialize_poll(self, poll: poll_models.PollCreate) -> data_binding.JSONObject: + """Serialize a poll object to a json serializable dict. + + Parameters + ---------- + poll + The poll object to serialize. + + Returns + ------- + hikari.internal.data_binding.JSONObject + The serialized representation of the poll. + """ diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 23cc255d34..64ee0ba02b 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -49,6 +49,7 @@ from hikari import messages as messages_ from hikari import monetization from hikari import permissions as permissions_ + from hikari import polls from hikari import sessions from hikari import snowflakes from hikari import stickers as stickers_ @@ -984,6 +985,7 @@ async def create_message( components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls.PollCreate] = undefined.UNDEFINED, sticker: undefined.UndefinedOr[snowflakes.SnowflakeishOr[stickers_.PartialSticker]] = undefined.UNDEFINED, stickers: undefined.UndefinedOr[ snowflakes.SnowflakeishSequence[stickers_.PartialSticker] @@ -1061,6 +1063,8 @@ async def create_message( If provided, the message embed. embeds If provided, the message embeds. + poll + If provided, the poll to create. sticker If provided, the object or ID of a sticker to send on the message. diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index e52c206153..5b545bdeb8 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -48,6 +48,7 @@ from hikari import messages as message_models from hikari import monetization as monetization_models from hikari import permissions as permission_models +from hikari import polls as poll_models from hikari import presences as presence_models from hikari import scheduled_events as scheduled_events_models from hikari import sessions as gateway_models @@ -3744,3 +3745,77 @@ def deserialize_sku(self, payload: data_binding.JSONObject) -> monetization_mode slug=payload["slug"], flags=monetization_models.SKUFlags(payload["flags"]), ) + + ############### + # POLL MODELS # + ############### + def _deserialize_poll_media(self, payload: data_binding.JSONObject) -> poll_models.PollMedia: + emoji_payload = payload.get("emoji") + return poll_models.PollMedia( + text=payload.get("text"), emoji=self.deserialize_emoji(emoji_payload) if emoji_payload else None + ) + + def _deserialize_poll_answer_count(self, payload: data_binding.JSONObject) -> poll_models.PollAnswerCount: + return poll_models.PollAnswerCount( + answer_id=payload["answer_id"], count=payload["count"], me_voted=payload["me_voted"] + ) + + def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.PollObject: + question = payload["question"] + expiry = time.iso8601_datetime_string_to_datetime(payload["expiry"]) + allow_multiselect = payload["allow_multiple_options"] + layout_type = poll_models.PollLayoutType(payload["layout_type"]) + + answers: typing.MutableMapping[int, poll_models.PollAnswer] = {} + for _answer_payload in payload["answers"]: + answer_id = _answer_payload["answer_id"] + poll_media = self._deserialize_poll_media(_answer_payload) + + answers[answer_id] = poll_models.PollAnswer(answer_id=answer_id, poll_media=poll_media) + + _result_payload: typing.Optional[data_binding.JSONObject] = None + if (_result_payload := payload.get("result")) is not None: + is_finalized = _result_payload.get("is_finalized") + + answer_counts = tuple( + self._deserialize_poll_answer_count(item) for item in _result_payload.get("answer_counts") + ) + results = poll_models.PollResult(is_finalized=is_finalized, answer_counts=answer_counts) + else: + results = None + + return poll_models.PollObject( + question=question, + answers=answers, + expiry=expiry, + allow_multiselect=allow_multiselect, + layout_type=layout_type, + results=results, + ) + + def _serialize_poll_partial_emoji(self, emoji: typing.Optional[emoji_models.Emoji]) -> data_binding.JSONObject: + if isinstance(emoji, emoji_models.UnicodeEmoji): + return {"name": emoji.name} + elif isinstance(emoji, emoji_models.CustomEmoji): + return {"name": emoji.name, "id": emoji.name} + return {} + + def serialize_poll(self, poll: poll_models.PollCreate) -> data_binding.JSONObject: + answers = [] + for answer_id, answer in poll.answers.items(): + # FIXME: Typing is **very** dodgy here. Revise this before shipping. + poll_media: typing.MutableMapping[str, typing.Any] = {"text": answer.poll_media.text} + + answer_emoji = self._serialize_poll_partial_emoji(answer.poll_media.emoji) + if answer_emoji: + poll_media["emoji"] = answer_emoji + + answers.append({"answer_id": answer_id, "poll_media": poll_media}) + + return { + "question": poll.question.text, + "answers": answers, + "expiry": poll.duration, + "allow_multiple_options": poll.allow_multiselect, + "layout_type": poll.layout_type, + } diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 9d3d4b5f1d..027f1866ff 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -61,6 +61,7 @@ from hikari import messages as messages_ from hikari import monetization from hikari import permissions as permissions_ +from hikari import polls from hikari import scheduled_events from hikari import snowflakes from hikari import traits @@ -1369,6 +1370,7 @@ def _build_message_payload( # noqa: C901- Function too complex ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls.PollCreate] = undefined.UNDEFINED, sticker: undefined.UndefinedOr[snowflakes.SnowflakeishOr[stickers_.PartialSticker]] = undefined.UNDEFINED, stickers: undefined.UndefinedOr[ snowflakes.SnowflakeishSequence[stickers_.PartialSticker] @@ -1450,12 +1452,18 @@ def _build_message_payload( # noqa: C901- Function too complex final_attachments.extend(embed_attachments) serialized_embeds.append(embed_payload) + serialized_poll: undefined.UndefinedOr[data_binding.JSONObject] = undefined.UNDEFINED + if poll is not undefined.UNDEFINED: + if poll is not None: + serialized_poll = self._entity_factory.serialize_poll(poll) + body = data_binding.JSONObjectBuilder() body.put("content", content, conversion=lambda v: v if v is None else str(v)) body.put("tts", tts) body.put("flags", flags) body.put("embeds", serialized_embeds) body.put("components", serialized_components) + body.put("poll", serialized_poll) body.put_snowflake_array("sticker_ids", (sticker,) if sticker else stickers) if not edit or not undefined.all_undefined(mentions_everyone, mentions_reply, user_mentions, role_mentions): @@ -1500,6 +1508,7 @@ async def create_message( components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls.PollCreate] = undefined.UNDEFINED, sticker: undefined.UndefinedOr[snowflakes.SnowflakeishOr[stickers_.PartialSticker]] = undefined.UNDEFINED, stickers: undefined.UndefinedOr[ snowflakes.SnowflakeishSequence[stickers_.PartialSticker] @@ -1526,6 +1535,7 @@ async def create_message( components=components, embed=embed, embeds=embeds, + poll=poll, sticker=sticker, stickers=stickers, tts=tts, diff --git a/hikari/polls.py b/hikari/polls.py index 48ab43901e..be74718211 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -47,6 +47,13 @@ from hikari.emojis import Emoji +def _ensure_optional_emoji(emoji: typing.Optional[typing.Union[str, Emoji]]) -> Emoji | None: + """Ensure the object is a [hikari.emojis.Emoji][].""" + if emoji is not None: + return Emoji.parse(emoji) if isinstance(emoji, str) else emoji + return None + + @attrs_extensions.with_copy @attrs.define(hash=False, kw_only=True, weakref_slot=False) class PollMedia: @@ -214,12 +221,14 @@ def add_answer(self, text: str, emoji: typing.Optional[Emoji]) -> PartialPoll: answer_id = self._counter self._counter += 1 - new_answer = PollAnswer(answer_id=answer_id, poll_media=PollMedia(text=text, emoji=emoji)) + new_answer = PollAnswer( + answer_id=answer_id, poll_media=PollMedia(text=text, emoji=_ensure_optional_emoji(emoji)) + ) self._answers[answer_id] = new_answer return self - def edit_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) -> PartialPoll: + def edit_answer(self, answer_id: int, text: str, emoji: typing.Optional[typing.Union[str, Emoji]]) -> PartialPoll: """ Edit an answer in the poll. @@ -246,7 +255,7 @@ def edit_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) if answer is None: raise KeyError(f"Answer ID {answer_id} not found in the poll.") - new_poll_media = PollMedia(text=text, emoji=emoji) + new_poll_media = PollMedia(text=text, emoji=_ensure_optional_emoji(emoji)) answer.poll_media = new_poll_media return self diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index a781f1f6ca..11ff6d13aa 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -2481,6 +2481,7 @@ async def test_create_message_when_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], + poll=undefined.UNDEFINED, sticker=54234, stickers=[564123, 431123], tts=True, @@ -2540,6 +2541,7 @@ async def test_create_message_when_no_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], + poll=undefined.UNDEFINED, sticker=543345, stickers=[123321, 6572345], tts=True, From 4be409fd3bb27111b283e7aee737cad00d0e719a Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 27 May 2024 11:11:50 +0700 Subject: [PATCH 12/72] Update typing stub --- hikari/__init__.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi index 97c59acd0c..e2ec667b47 100644 --- a/hikari/__init__.pyi +++ b/hikari/__init__.pyi @@ -60,6 +60,7 @@ from hikari.events.lifetime_events import * from hikari.events.member_events import * from hikari.events.message_events import * from hikari.events.monetization_events import * +from hikari.events.poll_events import * from hikari.events.reaction_events import * from hikari.events.role_events import * from hikari.events.scheduled_events import * @@ -90,6 +91,7 @@ from hikari.locales import * from hikari.messages import * from hikari.monetization import * from hikari.permissions import * +from hikari.polls import * from hikari.presences import * from hikari.scheduled_events import * from hikari.sessions import * From 8d3220b18797ed11c06d8d25a5ef7081582faace Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 27 May 2024 17:19:58 +0700 Subject: [PATCH 13/72] Apparently `Emoji` needs to be imported ...oops Signed-off-by: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> --- hikari/polls.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index be74718211..b7c6f3a7fc 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -38,14 +38,13 @@ import attrs +from hikari.emojis import Emoji from hikari.internal import attrs_extensions from hikari.internal import enums if typing.TYPE_CHECKING: import datetime - from hikari.emojis import Emoji - def _ensure_optional_emoji(emoji: typing.Optional[typing.Union[str, Emoji]]) -> Emoji | None: """Ensure the object is a [hikari.emojis.Emoji][].""" From a330fcdf16d32694b5edac9e43774899913196d9 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Mon, 27 May 2024 22:22:55 +1000 Subject: [PATCH 14/72] added `_serialize_poll_media()` added `_serialize_poll_media()` and fixed polls question serialisation. --- hikari/impl/entity_factory.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 5b545bdeb8..1ddb1e0c37 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3800,20 +3800,24 @@ def _serialize_poll_partial_emoji(self, emoji: typing.Optional[emoji_models.Emoj return {"name": emoji.name, "id": emoji.name} return {} - def serialize_poll(self, poll: poll_models.PollCreate) -> data_binding.JSONObject: - answers = [] - for answer_id, answer in poll.answers.items(): - # FIXME: Typing is **very** dodgy here. Revise this before shipping. - poll_media: typing.MutableMapping[str, typing.Any] = {"text": answer.poll_media.text} + def _serialize_poll_media(self, poll_media: poll_models.PollMedia) -> data_binding.JSONObject: + # FIXME: Typing is **very** dodgy here. Revise this before shipping. + + serialised_poll_media: typing.MutableMapping[str, typing.Any] = {"text": poll_media.text} - answer_emoji = self._serialize_poll_partial_emoji(answer.poll_media.emoji) - if answer_emoji: - poll_media["emoji"] = answer_emoji + answer_emoji = self._serialize_poll_partial_emoji(poll_media.emoji) + if answer_emoji: + serialised_poll_media["emoji"] = answer_emoji - answers.append({"answer_id": answer_id, "poll_media": poll_media}) + return serialised_poll_media + + def serialize_poll(self, poll: poll_models.PollCreate) -> data_binding.JSONObject: + answers: typing.MutableSequence[typing.Any] = [] + for answer_id, answer in poll.answers.items(): + answers.append({"answer_id": answer_id, "poll_media": self._serialize_poll_media(answer.poll_media)}) return { - "question": poll.question.text, + "question": self._serialize_poll_media(poll.question), "answers": answers, "expiry": poll.duration, "allow_multiple_options": poll.allow_multiselect, From ff5413ea71be8ba7a3f1bab827b6f0cabb069739 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Mon, 27 May 2024 22:23:57 +1000 Subject: [PATCH 15/72] Removal of counter The removal of the counter has been added, so that the user can choose there own options, however, they may need to add a check because in its current form, you can override other answers. --- hikari/polls.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index b7c6f3a7fc..018b6369fc 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -131,12 +131,6 @@ def __init__(self, question: str, allow_multiselect: bool, layout_type: typing.U # due to hashmap overhead. self._answers: typing.MutableMapping[int, PollAnswer] = {} # TODO: Do we need to set to None? - # Counter to keep track of the answer IDs - # - # Discord (at the time of writing this) starts at 1, I'm opting to follow - # the behaviour. - self._counter = 1 - @property def question(self) -> PollMedia: """Returns the question of the poll.""" @@ -201,7 +195,7 @@ def duration(self) -> int: def duration(self, value: int) -> None: self._duration = value - def add_answer(self, text: str, emoji: typing.Optional[Emoji]) -> PartialPoll: + def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) -> PartialPoll: """ Add an answer to the poll. @@ -217,13 +211,13 @@ def add_answer(self, text: str, emoji: typing.Optional[Emoji]) -> PartialPoll: PartialPoll This poll. Allows for call chaining. """ - answer_id = self._counter - self._counter += 1 new_answer = PollAnswer( answer_id=answer_id, poll_media=PollMedia(text=text, emoji=_ensure_optional_emoji(emoji)) ) - self._answers[answer_id] = new_answer + + # FIXME: Not sure if this is ideal, but this will override an item, if it has the same answer id. + self._answers.update({answer_id: new_answer}) return self From f7f58b78ed067411fe583b7a8d914268ddd51e52 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 27 May 2024 21:38:32 +0700 Subject: [PATCH 16/72] Credit's where credit's due --- hikari/polls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/polls.py b/hikari/polls.py index 018b6369fc..82b7b983d1 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright (c) 2024-present PythonTryHard +# Copyright (c) 2024-present PythonTryHard, MPlatypus # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal From a5dbfe751859b8c089692ee3a724a069d6f8c1fa Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 27 May 2024 21:41:13 +0700 Subject: [PATCH 17/72] Throw error on adding answer with existing ID Co-authored-by: MPlatypus --- hikari/polls.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index 82b7b983d1..0f00d28bce 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -211,12 +211,16 @@ def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) - PartialPoll This poll. Allows for call chaining. """ + # Raise an exception when user tries to add an answer with an already + # existing ID. While this is against the "spirit" of hikari, we want + # add_answer to only "add" answers, not "edit" them. That job is for + # edit_answer.g + if answer_id in self._answers: + raise KeyError(f"Answer ID {answer_id} already exists in the poll.") new_answer = PollAnswer( answer_id=answer_id, poll_media=PollMedia(text=text, emoji=_ensure_optional_emoji(emoji)) ) - - # FIXME: Not sure if this is ideal, but this will override an item, if it has the same answer id. self._answers.update({answer_id: new_answer}) return self From a6ba51ec27c90f0adb3e2a8762fc803c5d00670c Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 27 May 2024 21:53:01 +0700 Subject: [PATCH 18/72] Apparently we don't need to set _answer to None "Apparently". --- hikari/polls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/polls.py b/hikari/polls.py index 0f00d28bce..cfed65652f 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -129,7 +129,7 @@ def __init__(self, question: str, allow_multiselect: bool, layout_type: typing.U # would decrease long-term maintainability. I'm opting to use a `dict` # here to simplify the implementation with some performance trade-off # due to hashmap overhead. - self._answers: typing.MutableMapping[int, PollAnswer] = {} # TODO: Do we need to set to None? + self._answers: typing.MutableMapping[int, PollAnswer] = {} @property def question(self) -> PollMedia: From ea9de9eae9ccc29f889e67dee2cd4dd158976e7a Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 27 May 2024 22:03:07 +0700 Subject: [PATCH 19/72] Docstring, dammit --- hikari/polls.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index cfed65652f..5126ea0c69 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -201,6 +201,8 @@ def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) - Parameters ---------- + answer_id + The ID of the answer to add. text The text of the answer to add. emoji @@ -210,6 +212,11 @@ def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) - ------- PartialPoll This poll. Allows for call chaining. + + Raises + ------ + KeyError + If the answer ID already exists in the poll. """ # Raise an exception when user tries to add an answer with an already # existing ID. While this is against the "spirit" of hikari, we want @@ -245,8 +252,8 @@ def edit_answer(self, answer_id: int, text: str, emoji: typing.Optional[typing.U Raises ------ - KeyError - Raised when the answer ID is not found in the poll. + KeyError + Raised when the answer ID is not found in the poll. """ answer = self._answers.get(answer_id, None) if answer is None: @@ -273,8 +280,8 @@ def remove_answer(self, answer_id: int) -> PartialPoll: Raises ------ - KeyError - Raised when the answer ID is not found in the poll. + KeyError + Raised when the answer ID is not found in the poll. """ if answer_id not in self._answers: raise KeyError(f"Answer ID {answer_id} not found in the poll.") From a7fa5a6b7b55a2101d5ff8e513c691938dfda280 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Mon, 27 May 2024 22:30:50 +0700 Subject: [PATCH 20/72] Apply suggestions from code review - De-underscore `answer_payload`. - Remove unnecessary `dict.get` from `deserialize_poll`. - Streamline serialization of poll in `_build_message_payload`. - API endpoint naming consistency. - Correct typing targeting Python 3.8 Co-authored-by: davfsa Signed-off-by: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> --- hikari/impl/entity_factory.py | 14 ++++++-------- hikari/impl/rest.py | 5 +---- hikari/internal/routes.py | 2 +- hikari/polls.py | 2 +- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 1ddb1e0c37..043acb7e93 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3767,22 +3767,20 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll layout_type = poll_models.PollLayoutType(payload["layout_type"]) answers: typing.MutableMapping[int, poll_models.PollAnswer] = {} - for _answer_payload in payload["answers"]: + for answer_payload in payload["answers"]: answer_id = _answer_payload["answer_id"] - poll_media = self._deserialize_poll_media(_answer_payload) + poll_media = self._deserialize_poll_media(answer_payload) answers[answer_id] = poll_models.PollAnswer(answer_id=answer_id, poll_media=poll_media) - _result_payload: typing.Optional[data_binding.JSONObject] = None - if (_result_payload := payload.get("result")) is not None: - is_finalized = _result_payload.get("is_finalized") + results = None + if (result_payload := payload.get("result")) is not None: + is_finalized = result_payload["is_finalized") answer_counts = tuple( - self._deserialize_poll_answer_count(item) for item in _result_payload.get("answer_counts") + self._deserialize_poll_answer_count(item) for item in _result_payload["answer_counts"] ) results = poll_models.PollResult(is_finalized=is_finalized, answer_counts=answer_counts) - else: - results = None return poll_models.PollObject( question=question, diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 027f1866ff..54c49f7a39 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1452,10 +1452,7 @@ def _build_message_payload( # noqa: C901- Function too complex final_attachments.extend(embed_attachments) serialized_embeds.append(embed_payload) - serialized_poll: undefined.UndefinedOr[data_binding.JSONObject] = undefined.UNDEFINED - if poll is not undefined.UNDEFINED: - if poll is not None: - serialized_poll = self._entity_factory.serialize_poll(poll) + body.put("poll", serialized_poll, conversion=self._entity_factory.serialize_poll) body = data_binding.JSONObjectBuilder() body.put("content", content, conversion=lambda v: v if v is None else str(v)) diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py index d8b1c456d2..d7937ec3c4 100644 --- a/hikari/internal/routes.py +++ b/hikari/internal/routes.py @@ -341,7 +341,7 @@ def compile_to_file( GET_CHANNEL_WEBHOOKS: typing.Final[Route] = Route(GET, "/channels/{channel}/webhooks") # Polls -GET_ANSWER_VOTERS: typing.Final[Route] = Route(GET, "/channels/{channel}/polls/{message}/answer/{answer}") +GET_POLL_ANSWER: typing.Final[Route] = Route(GET, "/channels/{channel}/polls/{message}/answer/{answer}") POST_END_POLL: typing.Final[Route] = Route(POST, "/channels/{channel}/polls/{message}/expire") # Reactions diff --git a/hikari/polls.py b/hikari/polls.py index 5126ea0c69..23860756db 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -46,7 +46,7 @@ import datetime -def _ensure_optional_emoji(emoji: typing.Optional[typing.Union[str, Emoji]]) -> Emoji | None: +def _ensure_optional_emoji(emoji: typing.Union[str, Emoji, None]) -> typing.Optional[emojis.Emoji]: """Ensure the object is a [hikari.emojis.Emoji][].""" if emoji is not None: return Emoji.parse(emoji) if isinstance(emoji, str) else emoji From e7768d3a36fa4021e4c263d7021593dae691bd77 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 08:35:03 +0700 Subject: [PATCH 21/72] `Emoji` is now `emojis.Emoji` --- hikari/polls.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index 23860756db..6c5bd0405d 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -38,7 +38,7 @@ import attrs -from hikari.emojis import Emoji +from hikari import emojis from hikari.internal import attrs_extensions from hikari.internal import enums @@ -46,10 +46,10 @@ import datetime -def _ensure_optional_emoji(emoji: typing.Union[str, Emoji, None]) -> typing.Optional[emojis.Emoji]: +def _ensure_optional_emoji(emoji: typing.Optional[typing.Union[str, emojis.Emoji]]) -> emojis.Emoji | None: """Ensure the object is a [hikari.emojis.Emoji][].""" if emoji is not None: - return Emoji.parse(emoji) if isinstance(emoji, str) else emoji + return emojis.Emoji.parse(emoji) if isinstance(emoji, str) else emoji return None @@ -61,7 +61,7 @@ class PollMedia: text: typing.Optional[str] = attrs.field(default=None, repr=True) """The text of the element, or [`None`][] if not present.""" - emoji: typing.Optional[Emoji] = attrs.field(default=None, repr=True) + emoji: typing.Optional[emojis.Emoji] = attrs.field(default=None, repr=True) """The emoji of the element, or [`None`][] if not present.""" @@ -195,7 +195,7 @@ def duration(self) -> int: def duration(self, value: int) -> None: self._duration = value - def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) -> PartialPoll: + def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[emojis.Emoji]) -> PartialPoll: """ Add an answer to the poll. @@ -232,7 +232,9 @@ def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[Emoji]) - return self - def edit_answer(self, answer_id: int, text: str, emoji: typing.Optional[typing.Union[str, Emoji]]) -> PartialPoll: + def edit_answer( + self, answer_id: int, text: str, emoji: typing.Optional[typing.Union[str, emojis.Emoji]] + ) -> PartialPoll: """ Edit an answer in the poll. From 65c333fd8e38c559b8e0d5a3e359b5e11071a9e1 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 08:39:43 +0700 Subject: [PATCH 22/72] `PollObject` is now `Poll` --- hikari/api/entity_factory.py | 4 ++-- hikari/impl/entity_factory.py | 4 ++-- hikari/polls.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 24a923c5b5..f686d7e0ea 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -1970,7 +1970,7 @@ def deserialize_sku(self, payload: data_binding.JSONObject) -> entitlement_model """ @abc.abstractmethod - def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.PollObject: + def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll: """Parse a raw payload from Discord into a poll object. Parameters @@ -1980,7 +1980,7 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll Returns ------- - hikari.polls.PollObject + hikari.polls.Poll The deserialized poll object. """ diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 043acb7e93..0e77fae132 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3760,7 +3760,7 @@ def _deserialize_poll_answer_count(self, payload: data_binding.JSONObject) -> po answer_id=payload["answer_id"], count=payload["count"], me_voted=payload["me_voted"] ) - def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.PollObject: + def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll: question = payload["question"] expiry = time.iso8601_datetime_string_to_datetime(payload["expiry"]) allow_multiselect = payload["allow_multiple_options"] @@ -3782,7 +3782,7 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll ) results = poll_models.PollResult(is_finalized=is_finalized, answer_counts=answer_counts) - return poll_models.PollObject( + return poll_models.Poll( question=question, answers=answers, expiry=expiry, diff --git a/hikari/polls.py b/hikari/polls.py index 6c5bd0405d..46c95f5f6d 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -31,7 +31,7 @@ "PollLayoutType", "PartialPoll", "PollCreate", - "PollObject", + "Poll", ) import typing @@ -293,7 +293,7 @@ def remove_answer(self, answer_id: int) -> PartialPoll: return self -class PollObject(PartialPoll): +class Poll(PartialPoll): """Represents an existing poll.""" __slots__: typing.Sequence[str] = ("_expiry", "_results") From 6102e92f010b8ae954bd42e762b1ac0b6be67d73 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 08:44:50 +0700 Subject: [PATCH 23/72] Correct `duration` key in `serialize_poll` --- hikari/impl/entity_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 0e77fae132..5a2f1fd233 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3817,7 +3817,7 @@ def serialize_poll(self, poll: poll_models.PollCreate) -> data_binding.JSONObjec return { "question": self._serialize_poll_media(poll.question), "answers": answers, - "expiry": poll.duration, + "duration": poll.duration, "allow_multiple_options": poll.allow_multiselect, "layout_type": poll.layout_type, } From 2347199c85d5d26610089927617cb7e8163daa0f Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 08:50:12 +0700 Subject: [PATCH 24/72] Finalize a7fa5a6b on de-underscoring and `.get()` --- hikari/impl/entity_factory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 5a2f1fd233..dce5fd8e5a 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3768,17 +3768,17 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll answers: typing.MutableMapping[int, poll_models.PollAnswer] = {} for answer_payload in payload["answers"]: - answer_id = _answer_payload["answer_id"] + answer_id = answer_payload["answer_id"] poll_media = self._deserialize_poll_media(answer_payload) answers[answer_id] = poll_models.PollAnswer(answer_id=answer_id, poll_media=poll_media) results = None if (result_payload := payload.get("result")) is not None: - is_finalized = result_payload["is_finalized") + is_finalized = result_payload["is_finalized"] answer_counts = tuple( - self._deserialize_poll_answer_count(item) for item in _result_payload["answer_counts"] + self._deserialize_poll_answer_count(item) for item in result_payload["answer_counts"] ) results = poll_models.PollResult(is_finalized=is_finalized, answer_counts=answer_counts) From b2730beb2a601684b7d197225effa5b89b5a9399 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 08:56:25 +0700 Subject: [PATCH 25/72] Change event name for consistency --- hikari/events/poll_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/events/poll_events.py b/hikari/events/poll_events.py index e68da39da8..84a54ceeee 100644 --- a/hikari/events/poll_events.py +++ b/hikari/events/poll_events.py @@ -40,7 +40,7 @@ @attrs_extensions.with_copy @attrs.define(kw_only=True, weakref_slot=False) -class PollVoteAdd(shard_events.ShardEvent): +class PollVoteCreate(shard_events.ShardEvent): """Event that is fired when a user add their vote to a poll. If the poll allows multiple selection, one event will be fired for each vote. @@ -73,7 +73,7 @@ class PollVoteAdd(shard_events.ShardEvent): @attrs_extensions.with_copy @attrs.define(kw_only=True, weakref_slot=False) -class PollVoteRemove(shard_events.ShardEvent): +class PollVoteDelete(shard_events.ShardEvent): """Event that is fired when a user remove their vote to a poll. If the poll allows multiple selection, one event will be fired for each vote. From 0d2de421ede04cb0a349153a55e75fafb7646852 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 09:00:35 +0700 Subject: [PATCH 26/72] I forgor to update `__all__` --- hikari/events/poll_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/events/poll_events.py b/hikari/events/poll_events.py index 84a54ceeee..218cb3e59c 100644 --- a/hikari/events/poll_events.py +++ b/hikari/events/poll_events.py @@ -23,7 +23,7 @@ from __future__ import annotations -__all__: typing.Sequence[str] = ("PollVoteAdd", "PollVoteRemove") +__all__: typing.Sequence[str] = ("PollVoteCreate", "PollVoteDelete") import typing From b5bc8355fc2def7e3f099ff61fb928be78812e59 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 09:29:21 +0700 Subject: [PATCH 27/72] Dav please --- hikari/impl/rest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 54c49f7a39..8118614d41 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1452,15 +1452,13 @@ def _build_message_payload( # noqa: C901- Function too complex final_attachments.extend(embed_attachments) serialized_embeds.append(embed_payload) - body.put("poll", serialized_poll, conversion=self._entity_factory.serialize_poll) - body = data_binding.JSONObjectBuilder() body.put("content", content, conversion=lambda v: v if v is None else str(v)) body.put("tts", tts) body.put("flags", flags) body.put("embeds", serialized_embeds) body.put("components", serialized_components) - body.put("poll", serialized_poll) + body.put("poll", poll, conversion=self._entity_factory.serialize_poll) body.put_snowflake_array("sticker_ids", (sticker,) if sticker else stickers) if not edit or not undefined.all_undefined(mentions_everyone, mentions_reply, user_mentions, role_mentions): From cfb444628941a35bef5063fcbfb6b78786fee337 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 11:06:06 +0700 Subject: [PATCH 28/72] Use UNDEFINED for nullable guild in poll events --- hikari/events/poll_events.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hikari/events/poll_events.py b/hikari/events/poll_events.py index 218cb3e59c..14c570fdb8 100644 --- a/hikari/events/poll_events.py +++ b/hikari/events/poll_events.py @@ -29,6 +29,7 @@ import attrs +from hikari import undefined from hikari.events import shard_events from hikari.internal import attrs_extensions @@ -61,10 +62,10 @@ class PollVoteCreate(shard_events.ShardEvent): message_id: snowflakes.Snowflake = attrs.field() """ID of the message that the poll is in.""" - guild_id: typing.Optional[snowflakes.Snowflake] = attrs.field(default=None) + guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = attrs.field(default=undefined.UNDEFINED) """ID of the guild that the poll is in. - This will be [None][] if the poll is in a DM channel. + This will be [hikari.undefined.UNDEFINED][] if the poll is in a DM channel. """ answer_id: int = attrs.field() @@ -94,10 +95,10 @@ class PollVoteDelete(shard_events.ShardEvent): message_id: snowflakes.Snowflake = attrs.field() """ID of the message that the poll is in.""" - guild_id: typing.Optional[snowflakes.Snowflake] = attrs.field(default=None) + guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = attrs.field(default=undefined.UNDEFINED) """ID of the guild that the poll is in. - This will be [None][] if the poll is in a DM channel. + This will be [hikari.undefined.UNDEFINED][] if the poll is in a DM channel. """ answer_id: int = attrs.field() From 9fc84a92fe27be98af310d84381aba72acbe6da8 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 11:06:40 +0700 Subject: [PATCH 29/72] Nox --- hikari/impl/entity_factory.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index dce5fd8e5a..b189bf2762 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3777,9 +3777,7 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll if (result_payload := payload.get("result")) is not None: is_finalized = result_payload["is_finalized"] - answer_counts = tuple( - self._deserialize_poll_answer_count(item) for item in result_payload["answer_counts"] - ) + answer_counts = tuple(self._deserialize_poll_answer_count(item) for item in result_payload["answer_counts"]) results = poll_models.PollResult(is_finalized=is_finalized, answer_counts=answer_counts) return poll_models.Poll( From e6d642f8a23c304aaf6955b8e8833972fd92e133 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 11:07:04 +0700 Subject: [PATCH 30/72] Implement poll events in EventFactory --- hikari/api/event_factory.py | 42 +++++++++++++++++++++++++++++++++++ hikari/impl/event_factory.py | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py index 38b9bb18d1..bb855dc8d6 100644 --- a/hikari/api/event_factory.py +++ b/hikari/api/event_factory.py @@ -49,6 +49,7 @@ from hikari.events import member_events from hikari.events import message_events from hikari.events import monetization_events + from hikari.events import poll_events from hikari.events import reaction_events from hikari.events import role_events from hikari.events import scheduled_events @@ -1408,3 +1409,44 @@ def deserialize_entitlement_update_event( hikari.events.entitlement_events.EntitlementUpdateEvent The parsed entitlement update event object. """ + + ################ + # POLL EVENTS # + ################ + @abc.abstractmethod + def deserialize_poll_vote_create_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> poll_events.PollVoteCreate: + """Parse a raw payload from Discord into a poll vote create event object. + + Parameters + ---------- + shard + The shard that emitted this event. + payload + The dict payload to parse. + + Returns + ------- + hikari.events.poll_events.PollVoteCreate + The parsed poll vote create event object. + """ + + @abc.abstractmethod + def deserialize_poll_vote_delete_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> poll_events.PollVoteDelete: + """Parse a raw payload from Discord into a poll vote delete event object. + + Parameters + ---------- + shard + The shard that emitted this event. + payload + The dict payload to parse. + + Returns + ------- + hikari.events.poll_events.PollVoteDelete + The parsed poll vote delete event object. + """ diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 465c72caf0..791e9d990d 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -46,6 +46,7 @@ from hikari.events import member_events from hikari.events import message_events from hikari.events import monetization_events +from hikari.events import poll_events from hikari.events import reaction_events from hikari.events import role_events from hikari.events import scheduled_events @@ -954,3 +955,45 @@ def deserialize_entitlement_delete_event( return monetization_events.EntitlementDeleteEvent( app=self._app, shard=shard, entitlement=self._app.entity_factory.deserialize_entitlement(payload) ) + + ################ + # POLL EVENTS # + ################ + def deserialize_poll_vote_create_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> poll_events.PollVoteCreate: + ... + payload_guild_id = payload.get("guild_id") + if payload_guild_id is not None: + guild_id = snowflakes.Snowflake(payload_guild_id) + else: + guild_id = None + + return poll_events.PollVoteCreate( + app=self._app, + shard=shard, + user_id=snowflakes.Snowflake(payload["user_id"]), + channel_id=snowflakes.Snowflake(payload["channel_id"]), + message_id=snowflakes.Snowflake(payload["message_id"]), + guild_id=guild_id, + answer_id=payload["answer_id"], + ) + + def deserialize_poll_vote_delete_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> poll_events.PollVoteDelete: + payload_guild_id = payload.get("guild_id") + if payload_guild_id is not None: + guild_id = snowflakes.Snowflake(payload_guild_id) + else: + guild_id = None + + return poll_events.PollVoteDelete( + app=self._app, + shard=shard, + user_id=snowflakes.Snowflake(payload["user_id"]), + channel_id=snowflakes.Snowflake(payload["channel_id"]), + message_id=snowflakes.Snowflake(payload["message_id"]), + guild_id=guild_id, + answer_id=payload["answer_id"], + ) From 7fe8a12b3cffb9a3bca8978676cf3909935354de Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 11:20:42 +0700 Subject: [PATCH 31/72] Correct fallback value for guild_id. Added 2 new FIXMEs. --- hikari/events/poll_events.py | 2 +- hikari/impl/event_factory.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/hikari/events/poll_events.py b/hikari/events/poll_events.py index 14c570fdb8..2d3693fefd 100644 --- a/hikari/events/poll_events.py +++ b/hikari/events/poll_events.py @@ -62,7 +62,7 @@ class PollVoteCreate(shard_events.ShardEvent): message_id: snowflakes.Snowflake = attrs.field() """ID of the message that the poll is in.""" - guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = attrs.field(default=undefined.UNDEFINED) + guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = attrs.field() """ID of the guild that the poll is in. This will be [hikari.undefined.UNDEFINED][] if the poll is in a DM channel. diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 791e9d990d..2356eb1cb1 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -964,10 +964,14 @@ def deserialize_poll_vote_create_event( ) -> poll_events.PollVoteCreate: ... payload_guild_id = payload.get("guild_id") + + # FIXME: Appeasing mypy for now. Is there a better to do this? + guild_id: undefined.UndefinedOr[snowflakes.Snowflake] + if payload_guild_id is not None: guild_id = snowflakes.Snowflake(payload_guild_id) else: - guild_id = None + guild_id = undefined.UNDEFINED return poll_events.PollVoteCreate( app=self._app, @@ -983,10 +987,14 @@ def deserialize_poll_vote_delete_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject ) -> poll_events.PollVoteDelete: payload_guild_id = payload.get("guild_id") + + # FIXME: Appeasing mypy for now. Is there a better to do this? + guild_id: undefined.UndefinedOr[snowflakes.Snowflake] + if payload_guild_id is not None: guild_id = snowflakes.Snowflake(payload_guild_id) else: - guild_id = None + guild_id = undefined.UNDEFINED return poll_events.PollVoteDelete( app=self._app, From 405458ddbf2da9359ceb173bfa5d0303ef411569 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 12:00:48 +0700 Subject: [PATCH 32/72] `PollCreate` is now `PollBuilder` --- hikari/polls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/polls.py b/hikari/polls.py index 46c95f5f6d..2a4fbe94b0 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -170,7 +170,7 @@ def answers(self) -> typing.MutableMapping[int, PollAnswer]: return self._answers -class PollCreate(PartialPoll): +class PollBuilder(PartialPoll): """Used to create a poll.""" # TODO: Improve this docstring __slots__: typing.Sequence[str] = ("_duration",) From 313d67398e56cf8edcf9942ca74e41c076bef2e1 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Tue, 28 May 2024 12:02:34 +0700 Subject: [PATCH 33/72] ....shit --- hikari/api/entity_factory.py | 2 +- hikari/impl/rest.py | 4 ++-- hikari/polls.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index f686d7e0ea..818807c513 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -1985,7 +1985,7 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll """ @abc.abstractmethod - def serialize_poll(self, poll: poll_models.PollCreate) -> data_binding.JSONObject: + def serialize_poll(self, poll: poll_models.PollBuilder) -> data_binding.JSONObject: """Serialize a poll object to a json serializable dict. Parameters diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 8118614d41..169c2af87d 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1370,7 +1370,7 @@ def _build_message_payload( # noqa: C901- Function too complex ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedOr[polls.PollCreate] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls.PollBuilder] = undefined.UNDEFINED, sticker: undefined.UndefinedOr[snowflakes.SnowflakeishOr[stickers_.PartialSticker]] = undefined.UNDEFINED, stickers: undefined.UndefinedOr[ snowflakes.SnowflakeishSequence[stickers_.PartialSticker] @@ -1503,7 +1503,7 @@ async def create_message( components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedOr[polls.PollCreate] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls.PollBuilder] = undefined.UNDEFINED, sticker: undefined.UndefinedOr[snowflakes.SnowflakeishOr[stickers_.PartialSticker]] = undefined.UNDEFINED, stickers: undefined.UndefinedOr[ snowflakes.SnowflakeishSequence[stickers_.PartialSticker] diff --git a/hikari/polls.py b/hikari/polls.py index 2a4fbe94b0..13d3059937 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -30,7 +30,7 @@ "PollAnswerCount", "PollLayoutType", "PartialPoll", - "PollCreate", + "PollBuilder", "Poll", ) From fd21d873104c57290a23536ccf10c4825d87e552 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Tue, 28 May 2024 16:06:08 +1000 Subject: [PATCH 34/72] fixed naming of poll events in event manager --- hikari/impl/event_manager.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 683b4fc984..9f309be487 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -888,3 +888,13 @@ async def on_entitlement_delete(self, shard: gateway_shard.GatewayShard, payload async def on_entitlement_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway-events#entitlement-update for more info.""" await self.dispatch(self._event_factory.deserialize_entitlement_update_event(shard, payload)) + + @event_manager_base.filtered(poll_events.PollVoteAdd) + async def on_message_poll_vote_add(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-add for more info.""" + await self.dispatch(self._event_factory.deserialize_poll_create_event(shard, payload)) + + @event_manager_base.filtered(poll_events.PollVoteRemove) + async def on_message_poll_vote_remove(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-remove for more info.""" + await self.dispatch(self._event_factory.deserialize_poll_delete_event(shard, payload)) From 6a4c892df1c5dfa3369fe3c2ffc73ad5ab47ed8a Mon Sep 17 00:00:00 2001 From: MPlaty Date: Tue, 28 May 2024 16:06:25 +1000 Subject: [PATCH 35/72] added poll vote add/remove intents --- hikari/intents.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/hikari/intents.py b/hikari/intents.py index 391a44be5d..e0d3518423 100644 --- a/hikari/intents.py +++ b/hikari/intents.py @@ -335,6 +335,20 @@ class Intents(enums.Flag): * `GUILD_SCHEDULED_EVENT_USER_REMOVE` """ + GUILD_MESSAGE_POLLS = 1 << 24 + """Subscribes to the events listed below. + + * `MESSAGE_POLL_VOTE_ADD` + * `MESSAGE_POLL_VOTE_REMOVE` + """ + + DIRECT_MESSAGE_POLLS = 1 << 25 + """Subscribes to the events listed below. + + * `MESSAGE_POLL_VOTE_ADD` + * `MESSAGE_POLL_VOTE_REMOVE` + """ + # Annoyingly, enums hide classmethods and staticmethods from __dir__ in # EnumMeta which means if I make methods to generate these, then stuff # will not be documented by pdoc. Alas, my dream of being smart with @@ -380,6 +394,9 @@ class Intents(enums.Flag): ALL_MESSAGES = DM_MESSAGES | GUILD_MESSAGES """All message intents.""" + ALL_POLLS = GUILD_MESSAGE_POLLS | DIRECT_MESSAGE_POLLS + """All poll intents.""" + ALL_MESSAGE_REACTIONS = DM_MESSAGE_REACTIONS | GUILD_MESSAGE_REACTIONS """All message reaction intents.""" From 6f0b79f2bee9d8909de501ee28c20953fca05ee6 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Tue, 28 May 2024 16:09:02 +1000 Subject: [PATCH 36/72] forgor nox --- hikari/impl/event_manager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 9f309be487..47c7f7fc89 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -890,11 +890,15 @@ async def on_entitlement_update(self, shard: gateway_shard.GatewayShard, payload await self.dispatch(self._event_factory.deserialize_entitlement_update_event(shard, payload)) @event_manager_base.filtered(poll_events.PollVoteAdd) - async def on_message_poll_vote_add(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None: + async def on_message_poll_vote_add( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: """See https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-add for more info.""" await self.dispatch(self._event_factory.deserialize_poll_create_event(shard, payload)) @event_manager_base.filtered(poll_events.PollVoteRemove) - async def on_message_poll_vote_remove(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None: + async def on_message_poll_vote_remove( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: """See https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-remove for more info.""" await self.dispatch(self._event_factory.deserialize_poll_delete_event(shard, payload)) From bf344902841d3e69e8c3995b50b2245381fd0cd2 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Tue, 28 May 2024 16:16:15 +1000 Subject: [PATCH 37/72] added extra intents. --- hikari/intents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hikari/intents.py b/hikari/intents.py index e0d3518423..cd7517d970 100644 --- a/hikari/intents.py +++ b/hikari/intents.py @@ -366,6 +366,7 @@ class Intents(enums.Flag): | GUILD_MESSAGE_TYPING | GUILD_MODERATION | GUILD_SCHEDULED_EVENTS + | GUILD_MESSAGE_POLLS ) """All unprivileged guild-related intents.""" @@ -388,7 +389,7 @@ class Intents(enums.Flag): use. """ - ALL_DMS = DM_MESSAGES | DM_MESSAGE_TYPING | DM_MESSAGE_REACTIONS + ALL_DMS = DM_MESSAGES | DM_MESSAGE_TYPING | DM_MESSAGE_REACTIONS | DIRECT_MESSAGE_POLLS """All direct message channel (non-guild bound) intents.""" ALL_MESSAGES = DM_MESSAGES | GUILD_MESSAGES From 543d20c8c2ac0f985e31fe802b39e71b0cc8f469 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Tue, 28 May 2024 17:17:09 +1000 Subject: [PATCH 38/72] Fixed PollCreate to PollBuilder --- hikari/api/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 64ee0ba02b..4b489333b4 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -985,7 +985,7 @@ async def create_message( components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedOr[polls.PollCreate] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls.PollBuilder] = undefined.UNDEFINED, sticker: undefined.UndefinedOr[snowflakes.SnowflakeishOr[stickers_.PartialSticker]] = undefined.UNDEFINED, stickers: undefined.UndefinedOr[ snowflakes.SnowflakeishSequence[stickers_.PartialSticker] From 917e35ec7cb73cab70589aaf0388ead27e72ad84 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Tue, 28 May 2024 17:18:51 +1000 Subject: [PATCH 39/72] fixed emojis, and answer handling. As `answer_id`'s are handled by discord, we have made it so that the user cannot input their own `answer_id`. It now closely matches how embeds work. --- hikari/impl/entity_factory.py | 10 +++--- hikari/polls.py | 64 +++++++++++++++-------------------- 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index b189bf2762..badb1bba54 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3766,12 +3766,12 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll allow_multiselect = payload["allow_multiple_options"] layout_type = poll_models.PollLayoutType(payload["layout_type"]) - answers: typing.MutableMapping[int, poll_models.PollAnswer] = {} + answers: typing.MutableSequence[poll_models.PollAnswer] = [] for answer_payload in payload["answers"]: answer_id = answer_payload["answer_id"] poll_media = self._deserialize_poll_media(answer_payload) - answers[answer_id] = poll_models.PollAnswer(answer_id=answer_id, poll_media=poll_media) + answers.append(poll_models.PollAnswer(answer_id=answer_id, poll_media=poll_media)) results = None if (result_payload := payload.get("result")) is not None: @@ -3807,10 +3807,10 @@ def _serialize_poll_media(self, poll_media: poll_models.PollMedia) -> data_bindi return serialised_poll_media - def serialize_poll(self, poll: poll_models.PollCreate) -> data_binding.JSONObject: + def serialize_poll(self, poll: poll_models.PollBuilder) -> data_binding.JSONObject: answers: typing.MutableSequence[typing.Any] = [] - for answer_id, answer in poll.answers.items(): - answers.append({"answer_id": answer_id, "poll_media": self._serialize_poll_media(answer.poll_media)}) + for answer in poll.answers: + answers.append({"poll_media": self._serialize_poll_media(answer.poll_media)}) return { "question": self._serialize_poll_media(poll.question), diff --git a/hikari/polls.py b/hikari/polls.py index 13d3059937..19976481b0 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -39,6 +39,7 @@ import attrs from hikari import emojis +from hikari import undefined from hikari.internal import attrs_extensions from hikari.internal import enums @@ -71,7 +72,7 @@ class PollAnswer: """Represents an answer to a poll.""" answer_id: int = attrs.field(repr=True) - """The ID that labels this answer.""" # Is this user-settable? + """The ID that labels this answer.""" poll_media: PollMedia = attrs.field(repr=True) """The [media][hikari.polls.PollMedia] associated with this answer.""" @@ -129,7 +130,7 @@ def __init__(self, question: str, allow_multiselect: bool, layout_type: typing.U # would decrease long-term maintainability. I'm opting to use a `dict` # here to simplify the implementation with some performance trade-off # due to hashmap overhead. - self._answers: typing.MutableMapping[int, PollAnswer] = {} + self._answers: typing.MutableSequence[PollAnswer] = [] @property def question(self) -> PollMedia: @@ -159,7 +160,7 @@ def layout_type(self, value: typing.Union[int, PollLayoutType]) -> None: self._layout_type = value @property - def answers(self) -> typing.MutableMapping[int, PollAnswer]: + def answers(self) -> typing.Sequence[PollAnswer]: """Returns the answers of the poll. !!! note @@ -171,7 +172,15 @@ def answers(self) -> typing.MutableMapping[int, PollAnswer]: class PollBuilder(PartialPoll): - """Used to create a poll.""" # TODO: Improve this docstring + """Poll Builder. + + Build a new poll to send as a message to discord. + + Parameters + ---------- + question + The question you wish to ask. + """ # TODO: Improve this docstring __slots__: typing.Sequence[str] = ("_duration",) @@ -195,14 +204,12 @@ def duration(self) -> int: def duration(self, value: int) -> None: self._duration = value - def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[emojis.Emoji]) -> PartialPoll: + def add_answer(self, text: str, emoji: typing.Optional[emojis.Emoji]) -> PartialPoll: """ Add an answer to the poll. Parameters ---------- - answer_id - The ID of the answer to add. text The text of the answer to add. emoji @@ -212,28 +219,20 @@ def add_answer(self, answer_id: int, text: str, emoji: typing.Optional[emojis.Em ------- PartialPoll This poll. Allows for call chaining. - - Raises - ------ - KeyError - If the answer ID already exists in the poll. """ - # Raise an exception when user tries to add an answer with an already - # existing ID. While this is against the "spirit" of hikari, we want - # add_answer to only "add" answers, not "edit" them. That job is for - # edit_answer.g - if answer_id in self._answers: - raise KeyError(f"Answer ID {answer_id} already exists in the poll.") - - new_answer = PollAnswer( - answer_id=answer_id, poll_media=PollMedia(text=text, emoji=_ensure_optional_emoji(emoji)) + + self._answers.append( + PollAnswer(answer_id=-1, poll_media=PollMedia(text=text, emoji=_ensure_optional_emoji(emoji))) ) - self._answers.update({answer_id: new_answer}) return self def edit_answer( - self, answer_id: int, text: str, emoji: typing.Optional[typing.Union[str, emojis.Emoji]] + self, + index: int, + *, + text: typing.Optional[str] = None, + emoji: undefined.UndefinedNoneOr[typing.Union[str, emojis.Emoji]] = undefined.UNDEFINED, ) -> PartialPoll: """ Edit an answer in the poll. @@ -249,20 +248,14 @@ def edit_answer( Returns ------- - PartialPoll This poll. Allows for call chaining. - - Raises - ------ - KeyError - Raised when the answer ID is not found in the poll. """ - answer = self._answers.get(answer_id, None) - if answer is None: - raise KeyError(f"Answer ID {answer_id} not found in the poll.") - new_poll_media = PollMedia(text=text, emoji=_ensure_optional_emoji(emoji)) - answer.poll_media = new_poll_media + answer = self._answers[index] + if text: + answer.poll_media.text = text + if emoji is not undefined.UNDEFINED: + answer.poll_media.emoji = _ensure_optional_emoji(emoji) return self @@ -285,8 +278,6 @@ def remove_answer(self, answer_id: int) -> PartialPoll: KeyError Raised when the answer ID is not found in the poll. """ - if answer_id not in self._answers: - raise KeyError(f"Answer ID {answer_id} not found in the poll.") del self._answers[answer_id] @@ -301,7 +292,6 @@ class Poll(PartialPoll): def __init__( self, question: str, - answers: typing.MutableMapping[int, PollAnswer], allow_multiselect: bool, expiry: datetime.datetime, results: typing.Optional[PollResult], From 515325e802a0b2e6bf0464b3756ff468a9113025 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Tue, 28 May 2024 21:10:00 +1000 Subject: [PATCH 40/72] Fixed naming --- hikari/impl/event_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 47c7f7fc89..5df700656f 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -889,16 +889,16 @@ async def on_entitlement_update(self, shard: gateway_shard.GatewayShard, payload """See https://discord.com/developers/docs/topics/gateway-events#entitlement-update for more info.""" await self.dispatch(self._event_factory.deserialize_entitlement_update_event(shard, payload)) - @event_manager_base.filtered(poll_events.PollVoteAdd) + @event_manager_base.filtered(poll_events.PollVoteCreate) async def on_message_poll_vote_add( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject ) -> None: """See https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-add for more info.""" - await self.dispatch(self._event_factory.deserialize_poll_create_event(shard, payload)) + await self.dispatch(self._event_factory.deserialize_poll_vote_create_event(shard, payload)) - @event_manager_base.filtered(poll_events.PollVoteRemove) + @event_manager_base.filtered(poll_events.PollVoteDelete) async def on_message_poll_vote_remove( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject ) -> None: """See https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-remove for more info.""" - await self.dispatch(self._event_factory.deserialize_poll_delete_event(shard, payload)) + await self.dispatch(self._event_factory.deserialize_poll_vote_delete_event(shard, payload)) From 151748263a1091c4234e1ad679a3526994f445bb Mon Sep 17 00:00:00 2001 From: MPlaty Date: Tue, 28 May 2024 21:10:18 +1000 Subject: [PATCH 41/72] fixed naming and docs --- hikari/polls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index 19976481b0..8ce2d57968 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -239,8 +239,8 @@ def edit_answer( Parameters ---------- - answer_id - The ID of the answer to edit. + index + The index of the answer you want to edit. text The new text of the answer. emoji From d6fbf6700c6c9594113335bf083dc79378f35c20 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:05:53 +0700 Subject: [PATCH 42/72] Migrate remnant `PollCreate` --- hikari/impl/entity_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index b189bf2762..e1496cd3da 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3807,7 +3807,7 @@ def _serialize_poll_media(self, poll_media: poll_models.PollMedia) -> data_bindi return serialised_poll_media - def serialize_poll(self, poll: poll_models.PollCreate) -> data_binding.JSONObject: + def serialize_poll(self, poll: poll_models.PollBuilder) -> data_binding.JSONObject: answers: typing.MutableSequence[typing.Any] = [] for answer_id, answer in poll.answers.items(): answers.append({"answer_id": answer_id, "poll_media": self._serialize_poll_media(answer.poll_media)}) From e911c06fe79eb26604aeda37297e3323bd2c9ec6 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:06:16 +0700 Subject: [PATCH 43/72] Inline the two `_deserialize` --- hikari/impl/entity_factory.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index e1496cd3da..7aa2089272 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3749,12 +3749,6 @@ def deserialize_sku(self, payload: data_binding.JSONObject) -> monetization_mode ############### # POLL MODELS # ############### - def _deserialize_poll_media(self, payload: data_binding.JSONObject) -> poll_models.PollMedia: - emoji_payload = payload.get("emoji") - return poll_models.PollMedia( - text=payload.get("text"), emoji=self.deserialize_emoji(emoji_payload) if emoji_payload else None - ) - def _deserialize_poll_answer_count(self, payload: data_binding.JSONObject) -> poll_models.PollAnswerCount: return poll_models.PollAnswerCount( answer_id=payload["answer_id"], count=payload["count"], me_voted=payload["me_voted"] @@ -3769,7 +3763,11 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll answers: typing.MutableMapping[int, poll_models.PollAnswer] = {} for answer_payload in payload["answers"]: answer_id = answer_payload["answer_id"] - poll_media = self._deserialize_poll_media(answer_payload) + + emoji = answer_payload["emoji"] + poll_media = poll_models.PollMedia( + text=answer_payload["text"], emoji=self.deserialize_emoji(emoji) if emoji else None + ) answers[answer_id] = poll_models.PollAnswer(answer_id=answer_id, poll_media=poll_media) @@ -3777,7 +3775,14 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll if (result_payload := payload.get("result")) is not None: is_finalized = result_payload["is_finalized"] - answer_counts = tuple(self._deserialize_poll_answer_count(item) for item in result_payload["answer_counts"]) + answer_counts = tuple( + poll_models.PollAnswerCount( + answer_id=payload["answer_id"], + count=payload["count"], + me_voted=payload["me_voted"] + ) + for payload in result_payload["answer_counts"] + ) results = poll_models.PollResult(is_finalized=is_finalized, answer_counts=answer_counts) return poll_models.Poll( From 629a1bb5b8ff42f8045b5ace2dd984ec8e6b8046 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:41:25 +0700 Subject: [PATCH 44/72] Inline _serialize_poll_partial_emoji --- hikari/impl/entity_factory.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 7aa2089272..83ea88743b 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3794,21 +3794,14 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll results=results, ) - def _serialize_poll_partial_emoji(self, emoji: typing.Optional[emoji_models.Emoji]) -> data_binding.JSONObject: - if isinstance(emoji, emoji_models.UnicodeEmoji): - return {"name": emoji.name} - elif isinstance(emoji, emoji_models.CustomEmoji): - return {"name": emoji.name, "id": emoji.name} - return {} - def _serialize_poll_media(self, poll_media: poll_models.PollMedia) -> data_binding.JSONObject: # FIXME: Typing is **very** dodgy here. Revise this before shipping. - serialised_poll_media: typing.MutableMapping[str, typing.Any] = {"text": poll_media.text} - answer_emoji = self._serialize_poll_partial_emoji(poll_media.emoji) - if answer_emoji: - serialised_poll_media["emoji"] = answer_emoji + if isinstance(poll_media.emoji, emoji_models.UnicodeEmoji): + serialised_poll_media["emoji"] = {"name": poll_media.emoji.name} + elif isinstance(poll_media.emoji, emoji_models.CustomEmoji): + serialised_poll_media["emoji"] = {"name": poll_media.emoji.name, "id": poll_media.emoji.id} return serialised_poll_media From 9e8a50c630f676c119217b054354e45797c2b101 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:57:11 +0700 Subject: [PATCH 45/72] Minor improvement to docstrings --- hikari/polls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/polls.py b/hikari/polls.py index 8ce2d57968..aabf6a113d 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -248,6 +248,7 @@ def edit_answer( Returns ------- + PartialPoll This poll. Allows for call chaining. """ From 1e05101e444717408067dccade369c6d573d6f5e Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:57:29 +0700 Subject: [PATCH 46/72] What the hell Platy? --- hikari/polls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/polls.py b/hikari/polls.py index aabf6a113d..43a1c7e5ee 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -293,6 +293,7 @@ class Poll(PartialPoll): def __init__( self, question: str, + answers: typing.Sequence[PollAnswer], allow_multiselect: bool, expiry: datetime.datetime, results: typing.Optional[PollResult], From 001e2e2346b7e1468535c29b1e66f02b4b98138a Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:36:27 +0700 Subject: [PATCH 47/72] Add missing `poll_events` import --- hikari/impl/event_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 5df700656f..683ba17a83 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -44,6 +44,7 @@ from hikari.events import member_events from hikari.events import message_events from hikari.events import monetization_events +from hikari.events import poll_events from hikari.events import reaction_events from hikari.events import role_events from hikari.events import scheduled_events From aaed77e42320d1ce662cfca05a57e89bd51102af Mon Sep 17 00:00:00 2001 From: MPlaty Date: Wed, 10 Jul 2024 23:22:57 +1000 Subject: [PATCH 48/72] added poll endpoints added poll endpoints for fetching poll voters, and ending a poll --- hikari/api/rest.py | 77 +++++++++++++++++++++++++++++++++++++++++++++ hikari/impl/rest.py | 35 +++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 4b489333b4..793d3b21ff 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -8210,3 +8210,80 @@ async def delete_test_entitlement( hikari.errors.InternalServerError If an internal error occurs on Discord while handling the request. """ + + @abc.abstractmethod + async def fetch_poll_voters( + self, + channel: snowflakes.SnowflakeishOr[channels_.TextableChannel], + message: snowflakes.SnowflakeishOr[messages_.PartialMessage], + answer_id: int, + /, + *, + after: undefined.UndefinedOr[snowflakes.SnowflakeishOr[users.PartialUser]] = undefined.UNDEFINED, + limit: undefined.UndefinedOr[int] = undefined.UNDEFINED + + ) -> typing.AsyncIterator[users.User]: + """Fetch poll voters. + + Parameters + ---------- + channel + The channel the poll is in. + message + The message the poll is in. + answer_id + The answers id. + after + The votes to collect, after this user voted. + limit + The amount of votes to collect. Maximum 100, default 25 + + Returns + ------- + typing.AsyncIterator[users.User] + An async iterator of User objects. + Raises + ------ + hikari.errors.BadRequestError + If any of the fields that are passed have an invalid value. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the entitlement was not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def delete_poll( + self, + channel: snowflakes.SnowflakeishOr[channels_.TextableChannel], + message: snowflakes.SnowflakeishOr[messages_.PartialMessage], + / + ) -> None: + """Delete poll. + + Parameters + ---------- + channel + The channel the poll is in. + message + The message the poll is in. + + Raises + ------ + hikari.errors.BadRequestError + If any of the fields that are passed have an invalid value. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the entitlement was not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 169c2af87d..32f9b00c24 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -4472,3 +4472,38 @@ async def delete_test_entitlement( ) -> None: route = routes.DELETE_APPLICATION_TEST_ENTITLEMENT.compile(application=application, entitlement=entitlement) await self._request(route) + + async def fetch_poll_voters( + self, + channel: snowflakes.SnowflakeishOr[channels_.TextableChannel], + message: snowflakes.SnowflakeishOr[messages_.PartialMessage], + answer_id: int, + /, + *, + after: undefined.UndefinedOr[snowflakes.SnowflakeishOr[users.PartialUser]] = undefined.UNDEFINED, + limit: undefined.UndefinedOr[int] = undefined.UNDEFINED + + ) -> typing.AsyncIterator[users.User]: + query = data_binding.StringMapBuilder() + + query.put("after", after) + query.put("limit", limit) + + route = routes.GET_POLL_ANSWER.compile(channel=channel, message=message, answer=answer_id) + + response = await self._request(route, query=query) + + assert isinstance(response, list) + + for user in response: + yield self._entity_factory.deserialize_user(user) + + async def delete_poll( + self, + channel: snowflakes.SnowflakeishOr[channels_.TextableChannel], + message: snowflakes.SnowflakeishOr[messages_.PartialMessage], + / + ) -> None: + route = routes.POST_END_POLL.compile(channel=channel, message=message) + + await self._request(route) From 46a425cbd483a455a7803e9c5a936ad2fe4d86e2 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Wed, 10 Jul 2024 23:31:20 +1000 Subject: [PATCH 49/72] ruff formatting + changed to sequence --- hikari/api/rest.py | 13 ++++++------- hikari/impl/rest.py | 18 ++++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 793d3b21ff..74fc3d4f9c 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -8220,9 +8220,8 @@ async def fetch_poll_voters( /, *, after: undefined.UndefinedOr[snowflakes.SnowflakeishOr[users.PartialUser]] = undefined.UNDEFINED, - limit: undefined.UndefinedOr[int] = undefined.UNDEFINED - - ) -> typing.AsyncIterator[users.User]: + limit: undefined.UndefinedOr[int] = undefined.UNDEFINED, + ) -> typing.Sequence[users.User]: """Fetch poll voters. Parameters @@ -8240,8 +8239,8 @@ async def fetch_poll_voters( Returns ------- - typing.AsyncIterator[users.User] - An async iterator of User objects. + typing.Sequence[users.User] + An sequence of Users. Raises ------ hikari.errors.BadRequestError @@ -8256,13 +8255,13 @@ async def fetch_poll_voters( hikari.errors.InternalServerError If an internal error occurs on Discord while handling the request. """ - + @abc.abstractmethod async def delete_poll( self, channel: snowflakes.SnowflakeishOr[channels_.TextableChannel], message: snowflakes.SnowflakeishOr[messages_.PartialMessage], - / + /, ) -> None: """Delete poll. diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 32f9b00c24..ad71f039fc 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -4481,29 +4481,27 @@ async def fetch_poll_voters( /, *, after: undefined.UndefinedOr[snowflakes.SnowflakeishOr[users.PartialUser]] = undefined.UNDEFINED, - limit: undefined.UndefinedOr[int] = undefined.UNDEFINED - - ) -> typing.AsyncIterator[users.User]: + limit: undefined.UndefinedOr[int] = undefined.UNDEFINED, + ) -> typing.Sequence[users.User]: query = data_binding.StringMapBuilder() query.put("after", after) query.put("limit", limit) - + route = routes.GET_POLL_ANSWER.compile(channel=channel, message=message, answer=answer_id) - + response = await self._request(route, query=query) assert isinstance(response, list) - for user in response: - yield self._entity_factory.deserialize_user(user) - + return [self._entity_factory.deserialize_user(payload) for payload in response] + async def delete_poll( self, channel: snowflakes.SnowflakeishOr[channels_.TextableChannel], message: snowflakes.SnowflakeishOr[messages_.PartialMessage], - / + /, ) -> None: route = routes.POST_END_POLL.compile(channel=channel, message=message) - + await self._request(route) From 38dd771aae5544c702ca133cf0899471b3588ef6 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 20:54:43 +0700 Subject: [PATCH 50/72] Nox format --- hikari/impl/entity_factory.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 61018abf52..0883eb0c12 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3777,9 +3777,7 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll answer_counts = tuple( poll_models.PollAnswerCount( - answer_id=payload["answer_id"], - count=payload["count"], - me_voted=payload["me_voted"] + answer_id=payload["answer_id"], count=payload["count"], me_voted=payload["me_voted"] ) for payload in result_payload["answer_counts"] ) From 1dc17f419bc1364218ddc6a31c69ce695447d3e3 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:03:34 +0700 Subject: [PATCH 51/72] Resolve typing conflict of `_answer` field `PollBuilder` (preferably) wants a read-only field, while `Poll` wants a read-write field. Having `_answer` be a `MutableSequence` in base class `PartialPoll` cannot satisfy both at the same. This commit moves the field to each subclasses to resolve that conflict. --- hikari/polls.py | 50 +++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index 43a1c7e5ee..e2ae3a8ea8 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -122,16 +122,6 @@ def __init__(self, question: str, allow_multiselect: bool, layout_type: typing.U self._allow_multiselect = allow_multiselect self._layout_type = layout_type - # Answer is required, but we want users to user add_answer() instead of - # providing at initialization. - # - # Considering that answer ID can be arbitrary, `list`-based approaches - # like that of hikari.embeds.Embed._fields, while feasible to implement, - # would decrease long-term maintainability. I'm opting to use a `dict` - # here to simplify the implementation with some performance trade-off - # due to hashmap overhead. - self._answers: typing.MutableSequence[PollAnswer] = [] - @property def question(self) -> PollMedia: """Returns the question of the poll.""" @@ -159,17 +149,6 @@ def layout_type(self) -> PollLayoutType: def layout_type(self, value: typing.Union[int, PollLayoutType]) -> None: self._layout_type = value - @property - def answers(self) -> typing.Sequence[PollAnswer]: - """Returns the answers of the poll. - - !!! note - Use [`hikari.polls.Poll.add_answer`][] to add a new answer, - [`hikari.polls.Poll.edit_answer`][] to edit an existing answer, or - [`hikari.polls.Poll.remove_answer`][] to remove a answer. - """ - return self._answers - class PollBuilder(PartialPoll): """Poll Builder. @@ -195,6 +174,16 @@ def __init__( super().__init__(question=question, allow_multiselect=allow_multiselect, layout_type=layout_type) self._duration = duration + # Answer is required, but we want users to user add_answer() instead of + # providing at initialization. + # + # Considering that answer ID can be arbitrary, `list`-based approaches + # like that of hikari.embeds.Embed._fields, while feasible to implement, + # would decrease long-term maintainability. I'm opting to use a `dict` + # here to simplify the implementation with some performance trade-off + # due to hashmap overhead. + self._answers: typing.MutableSequence[PollAnswer] = [] + @property def duration(self) -> int: """Returns the duration of the poll.""" @@ -204,6 +193,17 @@ def duration(self) -> int: def duration(self, value: int) -> None: self._duration = value + @property + def answers(self) -> typing.Iterable[PollAnswer]: + """Returns the answers of the poll. + + !!! note + Use [`hikari.polls.Poll.add_answer`][] to add a new answer, + [`hikari.polls.Poll.edit_answer`][] to edit an existing answer, or + [`hikari.polls.Poll.remove_answer`][] to remove an answer. + """ + return self._answers + def add_answer(self, text: str, emoji: typing.Optional[emojis.Emoji]) -> PartialPoll: """ Add an answer to the poll. @@ -220,7 +220,6 @@ def add_answer(self, text: str, emoji: typing.Optional[emojis.Emoji]) -> Partial PartialPoll This poll. Allows for call chaining. """ - self._answers.append( PollAnswer(answer_id=-1, poll_media=PollMedia(text=text, emoji=_ensure_optional_emoji(emoji))) ) @@ -251,7 +250,6 @@ def edit_answer( PartialPoll This poll. Allows for call chaining. """ - answer = self._answers[index] if text: answer.poll_media.text = text @@ -279,7 +277,6 @@ def remove_answer(self, answer_id: int) -> PartialPoll: KeyError Raised when the answer ID is not found in the poll. """ - del self._answers[answer_id] return self @@ -304,6 +301,11 @@ def __init__( self._expiry = expiry self._results = results + @property + def answers(self) -> typing.Iterable[PollAnswer]: + """Returns the answers of the poll.""" + return self._answers + @property def expiry(self) -> datetime.datetime: """Returns whether the poll has expired.""" From 13ef0cd3f5151f3ee3e0467e7c45d4d75bafea33 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Thu, 11 Jul 2024 00:39:00 +1000 Subject: [PATCH 52/72] added some more tests. added a few more tests (NOT FULLY FUNCTIONAL) --- hikari/impl/entity_factory.py | 12 +++++-- hikari/impl/event_manager.py | 1 + hikari/messages.py | 4 +++ hikari/polls.py | 1 + tests/hikari/impl/test_entity_factory.py | 41 ++++++++++++++++++++++++ tests/hikari/impl/test_rest.py | 22 +++++++++++-- 6 files changed, 76 insertions(+), 5 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index badb1bba54..ee0aa2fa1e 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3045,6 +3045,10 @@ def deserialize_partial_message( # noqa: C901 - Too complex if "embeds" in payload: embeds = [self.deserialize_embed(embed) for embed in payload["embeds"]] + poll: undefined.UndefinedOr[poll_models.Poll] = undefined.UNDEFINED + if "poll" in payload: + poll = self.deserialize_poll(payload["poll"]) + reactions: undefined.UndefinedOr[typing.List[message_models.Reaction]] = undefined.UNDEFINED if "reactions" in payload: reactions = [self._deserialize_message_reaction(reaction) for reaction in payload["reactions"]] @@ -3118,6 +3122,7 @@ def deserialize_partial_message( # noqa: C901 - Too complex is_tts=payload.get("tts", undefined.UNDEFINED), attachments=attachments, embeds=embeds, + poll=poll, reactions=reactions, is_pinned=payload.get("pinned", undefined.UNDEFINED), webhook_id=snowflakes.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else undefined.UNDEFINED, @@ -3761,15 +3766,16 @@ def _deserialize_poll_answer_count(self, payload: data_binding.JSONObject) -> po ) def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll: - question = payload["question"] + question = payload["question"]["text"] expiry = time.iso8601_datetime_string_to_datetime(payload["expiry"]) - allow_multiselect = payload["allow_multiple_options"] + allow_multiselect = payload["allow_multiselect"] layout_type = poll_models.PollLayoutType(payload["layout_type"]) answers: typing.MutableSequence[poll_models.PollAnswer] = [] for answer_payload in payload["answers"]: answer_id = answer_payload["answer_id"] - poll_media = self._deserialize_poll_media(answer_payload) + _LOGGER.warning(answer_payload) + poll_media = self._deserialize_poll_media(answer_payload["poll_media"]) answers.append(poll_models.PollAnswer(answer_id=answer_id, poll_media=poll_media)) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 5df700656f..683ba17a83 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -44,6 +44,7 @@ from hikari.events import member_events from hikari.events import message_events from hikari.events import monetization_events +from hikari.events import poll_events from hikari.events import reaction_events from hikari.events import role_events from hikari.events import scheduled_events diff --git a/hikari/messages.py b/hikari/messages.py index 5b53f7c7eb..fa3e6f0dd0 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -58,6 +58,7 @@ from hikari import channels as channels_ from hikari import embeds as embeds_ from hikari import emojis as emojis_ + from hikari import polls as polls_ from hikari import stickers as stickers_ from hikari import users as users_ from hikari.api import special_endpoints @@ -529,6 +530,9 @@ class PartialMessage(snowflakes.Unique): embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = attrs.field(hash=False, eq=False, repr=False) """The message embeds.""" + poll: undefined.UndefinedOr[polls_.Poll] = attrs.field(hash=False, eq=False, repr=False) + """The message poll.""" + reactions: undefined.UndefinedOr[typing.Sequence[Reaction]] = attrs.field(hash=False, eq=False, repr=False) """The message reactions.""" diff --git a/hikari/polls.py b/hikari/polls.py index 8ce2d57968..dc5d306a6c 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -292,6 +292,7 @@ class Poll(PartialPoll): def __init__( self, question: str, + answers: typing.Sequence[PollAnswer], allow_multiselect: bool, expiry: datetime.datetime, results: typing.Optional[PollResult], diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 35a9be678e..24cf99dd6a 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -7193,3 +7193,44 @@ def test_deserialize_sku(self, entity_factory_impl, sku_payload): assert sku.slug == "hashire-sori-yo-kaze-no-you-ni-tsukimihara-wo-padoru-padoru" assert sku.flags == (monetization_models.SKUFlags.AVAILABLE | monetization_models.SKUFlags.GUILD_SUBSCRIPTION) assert isinstance(sku, monetization_models.SKU) + + ########### + # POLLS # + ########### + + @pytest.fixture + def poll_payload(self): + return { + "question": {"text": "fruit"}, + "answers": [ + {"answer_id": 1, "poll_media": {"text": "apple", "emoji": "🍏"}}, + {"answer_id": 2, "poll_media": {"text": "banana", "emoji": "🍌"}}, + {"answer_id": 3, "poll_media": {"text": "carrot", "emoji": "🥕"}} + ], + "expiry": "2021-02-01T18:03:20.888000+00:00", + "allow_multiselect": True, + "layout_type": 1, + } + + def test_deserialize_poll(self, entity_factory_impl, poll_payload): + poll = entity_factory_impl.deserialize_poll(poll_payload) + + assert poll.question.text == "fruit" + assert poll.question.emoji is None + assert len(poll.answers) == 3 + assert poll.answers[0].answer_id == 1 + assert poll.answers[0].poll_media.text == "apple" + assert poll.answers[0].poll_media.emoji == "🍏" + assert poll.answers[1].answer_id == 2 + assert poll.answers[1].poll_media.text == "banana" + assert poll.answers[1].poll_media.emoji == "🍌" + assert poll.answers[2].answer_id == 3 + assert poll.answers[2].poll_media.text == "carrot" + assert poll.answers[2].poll_media.emoji == "🥕" + + assert poll.expiry == datetime.datetime( + 2021, 2, 1, 18, 3, 20, 888000, tzinfo=datetime.timezone.utc + ) + + def test_serialize_poll(self, entity_factory_impl): + poll = entity_factory_impl.serialize_poll(sku_payload) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 11ff6d13aa..aecf0d430e 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -2438,13 +2438,14 @@ async def test_fetch_message(self, rest_client): rest_client._request.assert_awaited_once_with(expected_route) rest_client._entity_factory.deserialize_message.assert_called_once_with({"id": "456"}) - async def test_create_message_when_form(self, rest_client): + async def test_create_message_when_form(self, rest_client: rest.RESTClientImpl): attachment_obj = object() attachment_obj2 = object() component_obj = object() component_obj2 = object() embed_obj = object() embed_obj2 = object() + poll_obj = object() mock_form = mock.Mock() mock_body = data_binding.JSONObjectBuilder() mock_body.put("testing", "ensure_in_test") @@ -2461,6 +2462,7 @@ async def test_create_message_when_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], + poll=poll_obj, sticker=54234, stickers=[564123, 431123], tts=True, @@ -2481,7 +2483,7 @@ async def test_create_message_when_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], - poll=undefined.UNDEFINED, + poll=poll_obj, sticker=54234, stickers=[564123, 431123], tts=True, @@ -6433,3 +6435,19 @@ async def test_delete_scheduled_event(self, rest_client: rest.RESTClientImpl): await rest_client.delete_scheduled_event(StubModel(54531123), StubModel(123321123321)) rest_client._request.assert_awaited_once_with(expected_route) + + async def test_fetch_poll_voters(self, rest_client: rest.RESTClientImpl): + expected_route = routes.GET_POLL_ANSWER.compile(channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4)) + rest_client._request = mock.AsyncMock() + + await rest_client.fetch_poll_voters(StubModel(45874392), StubModel(398475938475), StubModel(4), after=StubModel(43587935), limit=6) + + rest_client._request.assert_awaited_once_with(expected_route, query={"after": "43587935", "limit": "6"}) + + async def test_end_poll(self, rest_client: rest.RESTClientImpl): + expected_route = routes.POST_END_POLL.compile(channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4)) + rest_client._request = mock.AsyncMock() + + await rest_client.delete_poll(StubModel(45874392), StubModel(398475938475)) + + rest_client._request.assert_awaited_once_with(expected_route) From 9d8cb1723d49917e3f18feda3bf54a9da8e0deca Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:39:49 +0700 Subject: [PATCH 53/72] Rework Poll events - Add QoL get()/fetch() methods to poll events (copy pasted from various other events) - Pull up fields and methods into a common base class due to identical structure of the two events - Standardize names (i.e. `PollVoteCreate` -> `PollVoteCreatEvent`) - Remove the two FIXMEs --- hikari/api/event_factory.py | 8 +- hikari/events/poll_events.py | 170 +++++++++++++++++++++++++++++------ hikari/impl/event_factory.py | 12 +-- hikari/impl/event_manager.py | 4 +- 4 files changed, 152 insertions(+), 42 deletions(-) diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py index bb855dc8d6..baa292daba 100644 --- a/hikari/api/event_factory.py +++ b/hikari/api/event_factory.py @@ -1416,7 +1416,7 @@ def deserialize_entitlement_update_event( @abc.abstractmethod def deserialize_poll_vote_create_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject - ) -> poll_events.PollVoteCreate: + ) -> poll_events.PollVoteCreateEvent: """Parse a raw payload from Discord into a poll vote create event object. Parameters @@ -1428,14 +1428,14 @@ def deserialize_poll_vote_create_event( Returns ------- - hikari.events.poll_events.PollVoteCreate + hikari.events.poll_events.PollVoteCreateEvent The parsed poll vote create event object. """ @abc.abstractmethod def deserialize_poll_vote_delete_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject - ) -> poll_events.PollVoteDelete: + ) -> poll_events.PollVoteDeleteEvent: """Parse a raw payload from Discord into a poll vote delete event object. Parameters @@ -1447,6 +1447,6 @@ def deserialize_poll_vote_delete_event( Returns ------- - hikari.events.poll_events.PollVoteDelete + hikari.events.poll_events.PollVoteDeleteEvent The parsed poll vote delete event object. """ diff --git a/hikari/events/poll_events.py b/hikari/events/poll_events.py index 2d3693fefd..a93862fad6 100644 --- a/hikari/events/poll_events.py +++ b/hikari/events/poll_events.py @@ -23,7 +23,7 @@ from __future__ import annotations -__all__: typing.Sequence[str] = ("PollVoteCreate", "PollVoteDelete") +__all__: typing.Sequence[str] = ("PollVoteCreateEvent", "PollVoteDeleteEvent") import typing @@ -34,18 +34,18 @@ from hikari.internal import attrs_extensions if typing.TYPE_CHECKING: + from hikari import channels + from hikari import guilds from hikari import snowflakes from hikari import traits + from hikari import users from hikari.api import shard as gateway_shard @attrs_extensions.with_copy @attrs.define(kw_only=True, weakref_slot=False) -class PollVoteCreate(shard_events.ShardEvent): - """Event that is fired when a user add their vote to a poll. - - If the poll allows multiple selection, one event will be fired for each vote. - """ +class BasePollVoteEvent(shard_events.ShardEvent): + """Event base for any event that involves a user voting on a poll.""" app: traits.RESTAware = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) # <>. @@ -71,35 +71,149 @@ class PollVoteCreate(shard_events.ShardEvent): answer_id: int = attrs.field() """ID of the answer that the user voted for.""" + def get_guild(self) -> typing.Optional[guilds.GatewayGuild]: + """Get the cached guild that this event relates to, if known. + + If not, return [`None`][]. + + Returns + ------- + typing.Optional[hikari.guilds.GatewayGuild] + The gateway guild this event relates to, if known. Otherwise, + this will return [`None`][]. + """ + if not isinstance(self.app, traits.CacheAware): + return None + + if isinstance(self.guild_id, undefined.UndefinedType): + return None + + return self.app.cache.get_available_guild(self.guild_id) or self.app.cache.get_unavailable_guild(self.guild_id) + + async def fetch_guild(self) -> typing.Optional[guilds.RESTGuild]: + """Perform an API call to fetch the guild that this event relates to. + + Returns + ------- + hikari.guilds.RESTGuild + The guild that this event occurred in. + + Raises + ------ + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.ForbiddenError + If you are not part of the guild. + hikari.errors.NotFoundError + If the guild is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + if isinstance(self.guild_id, undefined.UndefinedType): + return None + + return await self.app.rest.fetch_guild(self.guild_id) + + def get_channel(self) -> typing.Optional[channels.PermissibleGuildChannel]: + """Get the cached channel that this event relates to, if known. + + If not, return [`None`][]. + + Returns + ------- + typing.Optional[hikari.channels.GuildChannel] + The cached channel this event relates to. If not known, this + will return [`None`][] instead. + """ + if not isinstance(self.app, traits.CacheAware): + return None + + return self.app.cache.get_guild_channel(self.channel_id) + + async def fetch_channel(self) -> channels.GuildChannel: + """Perform an API call to fetch the details about this channel. + + !!! note + For [`hikari.events.channel_events.GuildChannelDeleteEvent`][] events, this will always raise + an exception, since the channel will have already been removed. + + Returns + ------- + hikari.channels.GuildChannel + A derivative of [`hikari.channels.GuildChannel`][]. The + actual type will vary depending on the type of channel this event + concerns. + + Raises + ------ + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.ForbiddenError + If you are missing the [`hikari.permissions.Permissions.VIEW_CHANNEL`][] permission in the channel. + hikari.errors.NotFoundError + If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + channel = await self.app.rest.fetch_channel(self.channel_id) + assert isinstance(channel, channels.GuildChannel) + return channel + + def get_user(self) -> typing.Optional[users.User]: + """Get the cached user that is typing, if known. + + Returns + ------- + typing.Optional[hikari.users.User] + The user, if known. + """ + if isinstance(self.app, traits.CacheAware): + return self.app.cache.get_user(self.user_id) + + return None + + async def fetch_user(self) -> users.User: + """Perform an API call to fetch an up-to-date image of this user. + + Returns + ------- + hikari.users.User + The user. + + Raises + ------ + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the user is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + return await self.app.rest.fetch_user(self.user_id) + @attrs_extensions.with_copy @attrs.define(kw_only=True, weakref_slot=False) -class PollVoteDelete(shard_events.ShardEvent): - """Event that is fired when a user remove their vote to a poll. +class PollVoteCreateEvent(BasePollVoteEvent): + """Event that is fired when a user add their vote to a poll. If the poll allows multiple selection, one event will be fired for each vote. """ - app: traits.RESTAware = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) - # <>. - - shard: gateway_shard.GatewayShard = attrs.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) - # <>. - - user_id: snowflakes.Snowflake = attrs.field() - """ID of the user that removed their vote from the poll.""" - - channel_id: snowflakes.Snowflake = attrs.field() - """ID of the channel that the poll is in.""" - message_id: snowflakes.Snowflake = attrs.field() - """ID of the message that the poll is in.""" - - guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = attrs.field(default=undefined.UNDEFINED) - """ID of the guild that the poll is in. +@attrs_extensions.with_copy +@attrs.define(kw_only=True, weakref_slot=False) +class PollVoteDeleteEvent(BasePollVoteEvent): + """Event that is fired when a user remove their vote to a poll. - This will be [hikari.undefined.UNDEFINED][] if the poll is in a DM channel. + If the poll allows multiple selection, one event will be fired for each vote. """ - - answer_id: int = attrs.field() - """ID of the answer that the user remove their vote from.""" diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 2356eb1cb1..1569e0813d 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -961,19 +961,17 @@ def deserialize_entitlement_delete_event( ################ def deserialize_poll_vote_create_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject - ) -> poll_events.PollVoteCreate: + ) -> poll_events.PollVoteCreateEvent: ... payload_guild_id = payload.get("guild_id") - # FIXME: Appeasing mypy for now. Is there a better to do this? guild_id: undefined.UndefinedOr[snowflakes.Snowflake] - if payload_guild_id is not None: guild_id = snowflakes.Snowflake(payload_guild_id) else: guild_id = undefined.UNDEFINED - return poll_events.PollVoteCreate( + return poll_events.PollVoteCreateEvent( app=self._app, shard=shard, user_id=snowflakes.Snowflake(payload["user_id"]), @@ -985,18 +983,16 @@ def deserialize_poll_vote_create_event( def deserialize_poll_vote_delete_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject - ) -> poll_events.PollVoteDelete: + ) -> poll_events.PollVoteDeleteEvent: payload_guild_id = payload.get("guild_id") - # FIXME: Appeasing mypy for now. Is there a better to do this? guild_id: undefined.UndefinedOr[snowflakes.Snowflake] - if payload_guild_id is not None: guild_id = snowflakes.Snowflake(payload_guild_id) else: guild_id = undefined.UNDEFINED - return poll_events.PollVoteDelete( + return poll_events.PollVoteDeleteEvent( app=self._app, shard=shard, user_id=snowflakes.Snowflake(payload["user_id"]), diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 683ba17a83..fbde6f5b52 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -890,14 +890,14 @@ async def on_entitlement_update(self, shard: gateway_shard.GatewayShard, payload """See https://discord.com/developers/docs/topics/gateway-events#entitlement-update for more info.""" await self.dispatch(self._event_factory.deserialize_entitlement_update_event(shard, payload)) - @event_manager_base.filtered(poll_events.PollVoteCreate) + @event_manager_base.filtered(poll_events.PollVoteCreateEvent) async def on_message_poll_vote_add( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject ) -> None: """See https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-add for more info.""" await self.dispatch(self._event_factory.deserialize_poll_vote_create_event(shard, payload)) - @event_manager_base.filtered(poll_events.PollVoteDelete) + @event_manager_base.filtered(poll_events.PollVoteDeleteEvent) async def on_message_poll_vote_remove( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject ) -> None: From 064baed96fee9ca8a889e0bbd9340f096804df3d Mon Sep 17 00:00:00 2001 From: MPlaty Date: Thu, 11 Jul 2024 01:19:25 +1000 Subject: [PATCH 54/72] fixed failing tests. All failing tests have now been fixed. --- hikari/api/rest.py | 1 + hikari/impl/entity_factory.py | 5 +++++ hikari/impl/rest.py | 2 ++ hikari/internal/cache.py | 4 ++++ hikari/messages.py | 4 ++++ tests/hikari/impl/test_cache.py | 4 ++++ tests/hikari/impl/test_entity_factory.py | 15 ++++++++------- tests/hikari/impl/test_rest.py | 18 +++++++++++++++--- tests/hikari/test_messages.py | 9 +++++++++ 9 files changed, 52 insertions(+), 10 deletions(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 74fc3d4f9c..6f53afbccf 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -1211,6 +1211,7 @@ async def edit_message( ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + poll: undefined.UndefinedNoneOr[polls.PollBuilder] = undefined.UNDEFINED, mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED, mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED, user_mentions: undefined.UndefinedOr[ diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index ee0aa2fa1e..a438519806 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3162,6 +3162,10 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model embeds = [self.deserialize_embed(embed) for embed in payload["embeds"]] + poll: typing.Optional[poll_models.Poll] = None + if "polls" in payload: + poll = self.deserialize_poll(payload["poll"]) + if "reactions" in payload: reactions = [self._deserialize_message_reaction(reaction) for reaction in payload["reactions"]] else: @@ -3218,6 +3222,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model is_tts=payload["tts"], attachments=attachments, embeds=embeds, + poll=poll, reactions=reactions, is_pinned=payload["pinned"], webhook_id=snowflakes.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else None, diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index ad71f039fc..aca4cfcd18 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1587,6 +1587,7 @@ async def edit_message( ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + poll: undefined.UndefinedNoneOr[polls.PollBuilder] = undefined.UNDEFINED, mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED, mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED, user_mentions: undefined.UndefinedOr[ @@ -1606,6 +1607,7 @@ async def edit_message( components=components, embed=embed, embeds=embeds, + poll=poll, flags=flags, mentions_everyone=mentions_everyone, mentions_reply=mentions_reply, diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py index cc3f752406..02e654c37b 100644 --- a/hikari/internal/cache.py +++ b/hikari/internal/cache.py @@ -72,6 +72,7 @@ from hikari import applications from hikari import channels as channels_ from hikari import components as components_ + from hikari import polls as polls_ from hikari import traits from hikari import users as users_ from hikari.interactions import base_interactions @@ -715,6 +716,7 @@ class MessageData(BaseData[messages.Message]): mentions_everyone: undefined.UndefinedOr[bool] = attrs.field() attachments: typing.Tuple[messages.Attachment, ...] = attrs.field() embeds: typing.Tuple[embeds_.Embed, ...] = attrs.field() + poll: typing.Optional[polls_.Poll] = attrs.field() reactions: typing.Tuple[messages.Reaction, ...] = attrs.field() is_pinned: bool = attrs.field() webhook_id: typing.Optional[snowflakes.Snowflake] = attrs.field() @@ -783,6 +785,7 @@ def build_from_entity( mentions_everyone=message.mentions_everyone, attachments=tuple(map(copy.copy, message.attachments)), embeds=tuple(map(_copy_embed, message.embeds)), + poll=message.poll, reactions=tuple(map(copy.copy, message.reactions)), is_pinned=message.is_pinned, webhook_id=message.webhook_id, @@ -828,6 +831,7 @@ def build_entity(self, app: traits.RESTAware, /) -> messages.Message: mentions_everyone=self.mentions_everyone, attachments=tuple(map(copy.copy, self.attachments)), embeds=tuple(map(_copy_embed, self.embeds)), + poll=self.poll, reactions=tuple(map(copy.copy, self.reactions)), is_pinned=self.is_pinned, webhook_id=self.webhook_id, diff --git a/hikari/messages.py b/hikari/messages.py index fa3e6f0dd0..9355007a54 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -759,6 +759,7 @@ async def edit( ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + poll: undefined.UndefinedNoneOr[polls_.PollBuilder] = undefined.UNDEFINED, mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED, mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED, user_mentions: undefined.UndefinedOr[ @@ -914,6 +915,7 @@ async def edit( components=components, embed=embed, embeds=embeds, + poll=poll, mentions_everyone=mentions_everyone, mentions_reply=mentions_reply, user_mentions=user_mentions, @@ -931,6 +933,7 @@ async def respond( components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls_.PollBuilder] = undefined.UNDEFINED, sticker: undefined.UndefinedOr[snowflakes.SnowflakeishOr[stickers_.PartialSticker]] = undefined.UNDEFINED, stickers: undefined.UndefinedOr[ snowflakes.SnowflakeishSequence[stickers_.PartialSticker] @@ -1098,6 +1101,7 @@ async def respond( components=components, embed=embed, embeds=embeds, + poll=poll, sticker=sticker, stickers=stickers, tts=tts, diff --git a/tests/hikari/impl/test_cache.py b/tests/hikari/impl/test_cache.py index d2aeb44cd4..bb83582d2c 100644 --- a/tests/hikari/impl/test_cache.py +++ b/tests/hikari/impl/test_cache.py @@ -29,6 +29,7 @@ from hikari import guilds from hikari import invites from hikari import messages +from hikari import polls from hikari import snowflakes from hikari import stickers from hikari import undefined @@ -2754,6 +2755,7 @@ def test__build_message(self, cache_impl): mock_attachment = mock.MagicMock(messages.Attachment) mock_embed_field = mock.MagicMock(embeds.EmbedField) mock_embed = mock.MagicMock(embeds.Embed, fields=(mock_embed_field,)) + mock_poll = mock.MagicMock(polls.Poll) mock_sticker = mock.MagicMock(stickers.PartialSticker) mock_reaction = mock.MagicMock(messages.Reaction) mock_activity = mock.MagicMock(messages.MessageActivity) @@ -2782,6 +2784,7 @@ def test__build_message(self, cache_impl): mentions_everyone=False, attachments=(mock_attachment,), embeds=(mock_embed,), + poll=mock_poll, reactions=(mock_reaction,), is_pinned=False, webhook_id=snowflakes.Snowflake(3123123), @@ -2869,6 +2872,7 @@ def test__build_message_with_null_fields(self, cache_impl): mentions_everyone=undefined.UNDEFINED, attachments=(), embeds=(), + poll=None, reactions=(), is_pinned=False, webhook_id=None, diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 24cf99dd6a..e6584c259c 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -5662,6 +5662,7 @@ def message_payload( custom_emoji_payload, partial_application_payload, embed_payload, + poll_payload, referenced_message, action_row_payload, partial_sticker_payload, @@ -5687,6 +5688,7 @@ def message_payload( "mention_channels": [{"id": "456", "guild_id": "678", "type": 1, "name": "hikari-testing"}], "attachments": [attachment_payload], "embeds": [embed_payload], + "poll": poll_payload, "reactions": [{"emoji": custom_emoji_payload, "count": 100, "me": True}], "pinned": True, "webhook_id": "1234", @@ -7203,9 +7205,9 @@ def poll_payload(self): return { "question": {"text": "fruit"}, "answers": [ - {"answer_id": 1, "poll_media": {"text": "apple", "emoji": "🍏"}}, - {"answer_id": 2, "poll_media": {"text": "banana", "emoji": "🍌"}}, - {"answer_id": 3, "poll_media": {"text": "carrot", "emoji": "🥕"}} + {"answer_id": 1, "poll_media": {"text": "apple", "emoji": {"name": "🍏"}}}, + {"answer_id": 2, "poll_media": {"text": "banana", "emoji": {"name": "🍌"}}}, + {"answer_id": 3, "poll_media": {"text": "carrot", "emoji": {"name": "🥕"}}}, ], "expiry": "2021-02-01T18:03:20.888000+00:00", "allow_multiselect": True, @@ -7228,9 +7230,8 @@ def test_deserialize_poll(self, entity_factory_impl, poll_payload): assert poll.answers[2].poll_media.text == "carrot" assert poll.answers[2].poll_media.emoji == "🥕" - assert poll.expiry == datetime.datetime( - 2021, 2, 1, 18, 3, 20, 888000, tzinfo=datetime.timezone.utc - ) + assert poll.expiry == datetime.datetime(2021, 2, 1, 18, 3, 20, 888000, tzinfo=datetime.timezone.utc) def test_serialize_poll(self, entity_factory_impl): - poll = entity_factory_impl.serialize_poll(sku_payload) + # poll = entity_factory_impl.serialize_poll(sku_payload) + pass diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index aecf0d430e..880332a7cf 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -2583,6 +2583,7 @@ async def test_edit_message_when_form(self, rest_client): component_obj2 = object() embed_obj = object() embed_obj2 = object() + poll_obj = object() mock_form = mock.Mock() mock_body = data_binding.JSONObjectBuilder() mock_body.put("testing", "ensure_in_test") @@ -2600,6 +2601,7 @@ async def test_edit_message_when_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], + poll=poll_obj, mentions_everyone=False, user_mentions=[9876], role_mentions=[1234], @@ -2615,6 +2617,7 @@ async def test_edit_message_when_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], + poll=poll_obj, flags=120, mentions_everyone=False, mentions_reply=undefined.UNDEFINED, @@ -2635,6 +2638,7 @@ async def test_edit_message_when_no_form(self, rest_client): component_obj2 = object() embed_obj = object() embed_obj2 = object() + poll_obj = object() mock_body = data_binding.JSONObjectBuilder() mock_body.put("testing", "ensure_in_test") expected_route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=123456789, message=987654321) @@ -2651,6 +2655,7 @@ async def test_edit_message_when_no_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], + poll=poll_obj, mentions_everyone=False, user_mentions=[9876], role_mentions=[1234], @@ -2666,6 +2671,7 @@ async def test_edit_message_when_no_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], + poll=poll_obj, flags=120, mentions_everyone=False, mentions_reply=undefined.UNDEFINED, @@ -6437,15 +6443,21 @@ async def test_delete_scheduled_event(self, rest_client: rest.RESTClientImpl): rest_client._request.assert_awaited_once_with(expected_route) async def test_fetch_poll_voters(self, rest_client: rest.RESTClientImpl): - expected_route = routes.GET_POLL_ANSWER.compile(channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4)) + expected_route = routes.GET_POLL_ANSWER.compile( + channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4) + ) rest_client._request = mock.AsyncMock() - await rest_client.fetch_poll_voters(StubModel(45874392), StubModel(398475938475), StubModel(4), after=StubModel(43587935), limit=6) + await rest_client.fetch_poll_voters( + StubModel(45874392), StubModel(398475938475), StubModel(4), after=StubModel(43587935), limit=6 + ) rest_client._request.assert_awaited_once_with(expected_route, query={"after": "43587935", "limit": "6"}) async def test_end_poll(self, rest_client: rest.RESTClientImpl): - expected_route = routes.POST_END_POLL.compile(channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4)) + expected_route = routes.POST_END_POLL.compile( + channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4) + ) rest_client._request = mock.AsyncMock() await rest_client.delete_poll(StubModel(45874392), StubModel(398475938475)) diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index 027697c8de..ae92d32dbc 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -104,6 +104,7 @@ def message(): mentions_everyone=False, attachments=(), embeds=(), + poll=object(), reactions=(), is_pinned=True, webhook_id=None, @@ -171,6 +172,7 @@ async def test_edit(self, message): message.channel_id = 456 embed = object() embeds = [object(), object()] + poll = object() component = object() components = object(), object() attachment = object() @@ -179,6 +181,7 @@ async def test_edit(self, message): content="test content", embed=embed, embeds=embeds, + poll=poll, attachment=attachment, attachments=[attachment, attachment], component=component, @@ -195,6 +198,7 @@ async def test_edit(self, message): content="test content", embed=embed, embeds=embeds, + poll=poll, attachment=attachment, attachments=[attachment, attachment], component=component, @@ -212,6 +216,7 @@ async def test_respond(self, message): message.channel_id = 456 embed = object() embeds = [object(), object()] + poll = object() roles = [object()] attachment = object() attachments = [object()] @@ -222,6 +227,7 @@ async def test_respond(self, message): content="test content", embed=embed, embeds=embeds, + poll=poll, attachment=attachment, attachments=attachments, component=component, @@ -242,6 +248,7 @@ async def test_respond(self, message): content="test content", embed=embed, embeds=embeds, + poll=poll, attachment=attachment, attachments=attachments, component=component, @@ -268,6 +275,7 @@ async def test_respond_when_reply_is_True(self, message): content=undefined.UNDEFINED, embed=undefined.UNDEFINED, embeds=undefined.UNDEFINED, + poll=undefined.UNDEFINED, attachment=undefined.UNDEFINED, attachments=undefined.UNDEFINED, component=undefined.UNDEFINED, @@ -294,6 +302,7 @@ async def test_respond_when_reply_is_False(self, message): content=undefined.UNDEFINED, embed=undefined.UNDEFINED, embeds=undefined.UNDEFINED, + poll=undefined.UNDEFINED, attachment=undefined.UNDEFINED, attachments=undefined.UNDEFINED, component=undefined.UNDEFINED, From add6bf589cb60279f5c9358bd0f3c72fa06d94e0 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 23:00:25 +0700 Subject: [PATCH 55/72] Appease flake8 --- hikari/api/rest.py | 1 + hikari/impl/entity_factory.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 6f53afbccf..79aea4bb5d 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -8242,6 +8242,7 @@ async def fetch_poll_voters( ------- typing.Sequence[users.User] An sequence of Users. + Raises ------ hikari.errors.BadRequestError diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index e7b62635ee..917f979329 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3007,7 +3007,7 @@ def _deserialize_message_interaction(self, payload: data_binding.JSONObject) -> user=self.deserialize_user(payload["user"]), ) - def deserialize_partial_message( # noqa: C901 - Too complex + def deserialize_partial_message( # noqa: C901,CFQ001 - Too complex, too long self, payload: data_binding.JSONObject ) -> message_models.PartialMessage: author: undefined.UndefinedOr[user_models.User] = undefined.UNDEFINED From fbe5d1ab5adf7f71501d5976a0ee0f78144f3cf5 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 10 Jul 2024 23:49:21 +0700 Subject: [PATCH 56/72] Appease mypy --- hikari/impl/entity_factory.py | 2 +- hikari/impl/rest.py | 2 +- hikari/internal/cache.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 917f979329..321e6f5e1c 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3162,7 +3162,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model embeds = [self.deserialize_embed(embed) for embed in payload["embeds"]] - poll: typing.Optional[poll_models.Poll] = None + poll: undefined.UndefinedOr[poll_models.Poll] = undefined.UNDEFINED if "polls" in payload: poll = self.deserialize_poll(payload["poll"]) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index aca4cfcd18..27f132cc5e 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1370,7 +1370,7 @@ def _build_message_payload( # noqa: C901- Function too complex ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedOr[polls.PollBuilder] = undefined.UNDEFINED, + poll: undefined.UndefinedNoneOr[polls.PollBuilder] = undefined.UNDEFINED, sticker: undefined.UndefinedOr[snowflakes.SnowflakeishOr[stickers_.PartialSticker]] = undefined.UNDEFINED, stickers: undefined.UndefinedOr[ snowflakes.SnowflakeishSequence[stickers_.PartialSticker] diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py index 02e654c37b..bbd8fac419 100644 --- a/hikari/internal/cache.py +++ b/hikari/internal/cache.py @@ -716,7 +716,7 @@ class MessageData(BaseData[messages.Message]): mentions_everyone: undefined.UndefinedOr[bool] = attrs.field() attachments: typing.Tuple[messages.Attachment, ...] = attrs.field() embeds: typing.Tuple[embeds_.Embed, ...] = attrs.field() - poll: typing.Optional[polls_.Poll] = attrs.field() + poll: undefined.UndefinedOr[polls_.Poll] = attrs.field() reactions: typing.Tuple[messages.Reaction, ...] = attrs.field() is_pinned: bool = attrs.field() webhook_id: typing.Optional[snowflakes.Snowflake] = attrs.field() From 700521eab05f79f299a4d0cd38182482e6f378e3 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Thu, 11 Jul 2024 00:59:42 +0700 Subject: [PATCH 57/72] Correct docstring after renaming --- hikari/polls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hikari/polls.py b/hikari/polls.py index e2ae3a8ea8..36e4078a87 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -198,9 +198,9 @@ def answers(self) -> typing.Iterable[PollAnswer]: """Returns the answers of the poll. !!! note - Use [`hikari.polls.Poll.add_answer`][] to add a new answer, - [`hikari.polls.Poll.edit_answer`][] to edit an existing answer, or - [`hikari.polls.Poll.remove_answer`][] to remove an answer. + Use [`hikari.polls.PollBuilder.add_answer`][] to add a new answer, + [`hikari.polls.PollBuilder.edit_answer`][] to edit an existing answer, or + [`hikari.polls.PollBuilder.remove_answer`][] to remove an answer. """ return self._answers From 250cb47af2b7466ffb343e69fdb4c78941f8a197 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Thu, 11 Jul 2024 01:00:00 +0700 Subject: [PATCH 58/72] Edit message endpoint doesn't support polls --- hikari/api/rest.py | 1 - hikari/impl/rest.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 79aea4bb5d..2c80d786b7 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -1211,7 +1211,6 @@ async def edit_message( ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedNoneOr[polls.PollBuilder] = undefined.UNDEFINED, mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED, mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED, user_mentions: undefined.UndefinedOr[ diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 27f132cc5e..5c359c284f 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1587,7 +1587,6 @@ async def edit_message( ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedNoneOr[polls.PollBuilder] = undefined.UNDEFINED, mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED, mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED, user_mentions: undefined.UndefinedOr[ @@ -1607,7 +1606,6 @@ async def edit_message( components=components, embed=embed, embeds=embeds, - poll=poll, flags=flags, mentions_everyone=mentions_everyone, mentions_reply=mentions_reply, From bfd338e6b979d0b934ddb6516bd7e9fc0a1e32c6 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Thu, 11 Jul 2024 01:11:20 +0700 Subject: [PATCH 59/72] Improve names and docstrings --- hikari/api/rest.py | 6 +++--- hikari/impl/rest.py | 2 +- tests/hikari/impl/test_rest.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 2c80d786b7..b3f922bcde 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -8222,7 +8222,7 @@ async def fetch_poll_voters( after: undefined.UndefinedOr[snowflakes.SnowflakeishOr[users.PartialUser]] = undefined.UNDEFINED, limit: undefined.UndefinedOr[int] = undefined.UNDEFINED, ) -> typing.Sequence[users.User]: - """Fetch poll voters. + """Fetch users that voted for a specific answer. Parameters ---------- @@ -8258,13 +8258,13 @@ async def fetch_poll_voters( """ @abc.abstractmethod - async def delete_poll( + async def end_poll( self, channel: snowflakes.SnowflakeishOr[channels_.TextableChannel], message: snowflakes.SnowflakeishOr[messages_.PartialMessage], /, ) -> None: - """Delete poll. + """End a poll. Parameters ---------- diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 5c359c284f..f0c5251fa1 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -4496,7 +4496,7 @@ async def fetch_poll_voters( return [self._entity_factory.deserialize_user(payload) for payload in response] - async def delete_poll( + async def end_poll( self, channel: snowflakes.SnowflakeishOr[channels_.TextableChannel], message: snowflakes.SnowflakeishOr[messages_.PartialMessage], diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 880332a7cf..da37ab435f 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -6460,6 +6460,6 @@ async def test_end_poll(self, rest_client: rest.RESTClientImpl): ) rest_client._request = mock.AsyncMock() - await rest_client.delete_poll(StubModel(45874392), StubModel(398475938475)) + await rest_client.end_poll(StubModel(45874392), StubModel(398475938475)) rest_client._request.assert_awaited_once_with(expected_route) From 385e62d936f896b7afcc99c7d5db73ef34c5c450 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Thu, 11 Jul 2024 15:58:49 +1000 Subject: [PATCH 60/72] finished most tests. Most, or all tests have been completed. - test_serialize_poll - test_deserialize_poll_vote_create_event - test_deserialize_poll_vote_delete_event --- hikari/api/rest.py | 3 ++- hikari/impl/entity_factory.py | 10 ++++---- hikari/impl/event_factory.py | 29 +++++----------------- hikari/impl/rest.py | 2 +- hikari/internal/cache.py | 2 +- hikari/messages.py | 2 +- hikari/polls.py | 7 ++---- tests/hikari/impl/test_entity_factory.py | 22 +++++++++++++++-- tests/hikari/impl/test_event_factory.py | 31 ++++++++++++++++++++++++ 9 files changed, 69 insertions(+), 39 deletions(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 6f53afbccf..eb0262a08e 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -1211,7 +1211,7 @@ async def edit_message( ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedNoneOr[polls.PollBuilder] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls.PollBuilder] = undefined.UNDEFINED, mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED, mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED, user_mentions: undefined.UndefinedOr[ @@ -8242,6 +8242,7 @@ async def fetch_poll_voters( ------- typing.Sequence[users.User] An sequence of Users. + Raises ------ hikari.errors.BadRequestError diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index a438519806..abe4b32b2a 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3007,7 +3007,7 @@ def _deserialize_message_interaction(self, payload: data_binding.JSONObject) -> user=self.deserialize_user(payload["user"]), ) - def deserialize_partial_message( # noqa: C901 - Too complex + def deserialize_partial_message( # noqa: C901, CFQ001 - Too complex, Exceeds allowed length self, payload: data_binding.JSONObject ) -> message_models.PartialMessage: author: undefined.UndefinedOr[user_models.User] = undefined.UNDEFINED @@ -3162,7 +3162,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model embeds = [self.deserialize_embed(embed) for embed in payload["embeds"]] - poll: typing.Optional[poll_models.Poll] = None + poll: undefined.UndefinedOr[poll_models.Poll] = undefined.UNDEFINED if "polls" in payload: poll = self.deserialize_poll(payload["poll"]) @@ -3813,7 +3813,7 @@ def _serialize_poll_media(self, poll_media: poll_models.PollMedia) -> data_bindi serialised_poll_media: typing.MutableMapping[str, typing.Any] = {"text": poll_media.text} answer_emoji = self._serialize_poll_partial_emoji(poll_media.emoji) - if answer_emoji: + if answer_emoji != {}: serialised_poll_media["emoji"] = answer_emoji return serialised_poll_media @@ -3827,6 +3827,6 @@ def serialize_poll(self, poll: poll_models.PollBuilder) -> data_binding.JSONObje "question": self._serialize_poll_media(poll.question), "answers": answers, "duration": poll.duration, - "allow_multiple_options": poll.allow_multiselect, - "layout_type": poll.layout_type, + "allow_multiselect": poll.allow_multiselect, + "layout_type": poll.layout_type.value, } diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 2356eb1cb1..f8a9ab9012 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -962,46 +962,29 @@ def deserialize_entitlement_delete_event( def deserialize_poll_vote_create_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject ) -> poll_events.PollVoteCreate: - ... - payload_guild_id = payload.get("guild_id") - - # FIXME: Appeasing mypy for now. Is there a better to do this? - guild_id: undefined.UndefinedOr[snowflakes.Snowflake] - - if payload_guild_id is not None: - guild_id = snowflakes.Snowflake(payload_guild_id) - else: - guild_id = undefined.UNDEFINED - return poll_events.PollVoteCreate( app=self._app, shard=shard, user_id=snowflakes.Snowflake(payload["user_id"]), channel_id=snowflakes.Snowflake(payload["channel_id"]), message_id=snowflakes.Snowflake(payload["message_id"]), - guild_id=guild_id, + guild_id=( + snowflakes.Snowflake(payload["guild_id"]) if payload.get("guild_id", None) else undefined.UNDEFINED + ), answer_id=payload["answer_id"], ) def deserialize_poll_vote_delete_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject ) -> poll_events.PollVoteDelete: - payload_guild_id = payload.get("guild_id") - - # FIXME: Appeasing mypy for now. Is there a better to do this? - guild_id: undefined.UndefinedOr[snowflakes.Snowflake] - - if payload_guild_id is not None: - guild_id = snowflakes.Snowflake(payload_guild_id) - else: - guild_id = undefined.UNDEFINED - return poll_events.PollVoteDelete( app=self._app, shard=shard, user_id=snowflakes.Snowflake(payload["user_id"]), channel_id=snowflakes.Snowflake(payload["channel_id"]), message_id=snowflakes.Snowflake(payload["message_id"]), - guild_id=guild_id, + guild_id=( + snowflakes.Snowflake(payload["guild_id"]) if payload.get("guild_id", None) else undefined.UNDEFINED + ), answer_id=payload["answer_id"], ) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index aca4cfcd18..0aedd65adc 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1587,7 +1587,7 @@ async def edit_message( ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedNoneOr[polls.PollBuilder] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls.PollBuilder] = undefined.UNDEFINED, mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED, mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED, user_mentions: undefined.UndefinedOr[ diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py index 02e654c37b..bbd8fac419 100644 --- a/hikari/internal/cache.py +++ b/hikari/internal/cache.py @@ -716,7 +716,7 @@ class MessageData(BaseData[messages.Message]): mentions_everyone: undefined.UndefinedOr[bool] = attrs.field() attachments: typing.Tuple[messages.Attachment, ...] = attrs.field() embeds: typing.Tuple[embeds_.Embed, ...] = attrs.field() - poll: typing.Optional[polls_.Poll] = attrs.field() + poll: undefined.UndefinedOr[polls_.Poll] = attrs.field() reactions: typing.Tuple[messages.Reaction, ...] = attrs.field() is_pinned: bool = attrs.field() webhook_id: typing.Optional[snowflakes.Snowflake] = attrs.field() diff --git a/hikari/messages.py b/hikari/messages.py index 9355007a54..08cf75e46e 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -759,7 +759,7 @@ async def edit( ] = undefined.UNDEFINED, embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED, embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - poll: undefined.UndefinedNoneOr[polls_.PollBuilder] = undefined.UNDEFINED, + poll: undefined.UndefinedOr[polls_.PollBuilder] = undefined.UNDEFINED, mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED, mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED, user_mentions: undefined.UndefinedOr[ diff --git a/hikari/polls.py b/hikari/polls.py index dc5d306a6c..42008a83ed 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -204,7 +204,7 @@ def duration(self) -> int: def duration(self, value: int) -> None: self._duration = value - def add_answer(self, text: str, emoji: typing.Optional[emojis.Emoji]) -> PartialPoll: + def add_answer(self, text: str, emoji: typing.Optional[typing.Union[emojis.Emoji, str]]) -> PartialPoll: """ Add an answer to the poll. @@ -220,7 +220,6 @@ def add_answer(self, text: str, emoji: typing.Optional[emojis.Emoji]) -> Partial PartialPoll This poll. Allows for call chaining. """ - self._answers.append( PollAnswer(answer_id=-1, poll_media=PollMedia(text=text, emoji=_ensure_optional_emoji(emoji))) ) @@ -250,7 +249,6 @@ def edit_answer( ------- This poll. Allows for call chaining. """ - answer = self._answers[index] if text: answer.poll_media.text = text @@ -278,7 +276,6 @@ def remove_answer(self, answer_id: int) -> PartialPoll: KeyError Raised when the answer ID is not found in the poll. """ - del self._answers[answer_id] return self @@ -292,7 +289,7 @@ class Poll(PartialPoll): def __init__( self, question: str, - answers: typing.Sequence[PollAnswer], + answers: typing.MutableSequence[PollAnswer], allow_multiselect: bool, expiry: datetime.datetime, results: typing.Optional[PollResult], diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index e6584c259c..a9a218465a 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -41,6 +41,7 @@ from hikari import messages as message_models from hikari import monetization as monetization_models from hikari import permissions as permission_models +from hikari import polls from hikari import presences as presence_models from hikari import scheduled_events as scheduled_event_models from hikari import sessions as gateway_models @@ -7233,5 +7234,22 @@ def test_deserialize_poll(self, entity_factory_impl, poll_payload): assert poll.expiry == datetime.datetime(2021, 2, 1, 18, 3, 20, 888000, tzinfo=datetime.timezone.utc) def test_serialize_poll(self, entity_factory_impl): - # poll = entity_factory_impl.serialize_poll(sku_payload) - pass + poll = polls.PollBuilder("fruit", 1, allow_multiselect=True, layout_type=polls.PollLayoutType.DEFAULT) + + poll.add_answer("apple", "🍏") + poll.add_answer("banana", "🍌") + poll.add_answer("carrot", "🥕") + + payload = entity_factory_impl.serialize_poll(poll) + + assert payload == { + "question": {"text": "fruit"}, + "answers": [ + {"poll_media": {"text": "apple", "emoji": {"name": "🍏"}}}, + {"poll_media": {"text": "banana", "emoji": {"name": "🍌"}}}, + {"poll_media": {"text": "carrot", "emoji": {"name": "🥕"}}}, + ], + "duration": 1, + "allow_multiselect": True, + "layout_type": 1, + } diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py index cb862ff7e3..dee799bf9e 100644 --- a/tests/hikari/impl/test_event_factory.py +++ b/tests/hikari/impl/test_event_factory.py @@ -38,6 +38,7 @@ from hikari.events import member_events from hikari.events import message_events from hikari.events import monetization_events +from hikari.events import poll_events from hikari.events import reaction_events from hikari.events import role_events from hikari.events import scheduled_events @@ -1517,3 +1518,33 @@ def test_deserialize_entitlement_delete_event(self, event_factory, mock_app, moc event = event_factory.deserialize_entitlement_delete_event(mock_shard, payload) assert isinstance(event, monetization_events.EntitlementDeleteEvent) + + ########### + # POLLS # + ########### + + def test_deserialize_poll_vote_create_event(self, event_factory, mock_app, mock_shard): + payload = { + "user_id": "3847382", + "channel_id": "4598743", + "message_id": "458437954", + "guild_id": "3589273", + "answer_id": 1, + } + + event = event_factory.deserialize_poll_vote_create_event(mock_shard, payload) + + assert isinstance(event, poll_events.PollVoteCreate) + + def test_deserialize_poll_vote_delete_event(self, event_factory, mock_app, mock_shard): + payload = { + "user_id": "3847382", + "channel_id": "4598743", + "message_id": "458437954", + "guild_id": "3589273", + "answer_id": 1, + } + + event = event_factory.deserialize_poll_vote_delete_event(mock_shard, payload) + + assert isinstance(event, poll_events.PollVoteDelete) From 51fe4e4eb13f6b3e34f9321f258bad789792847d Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:50:46 +0700 Subject: [PATCH 61/72] Correct edit_message tests --- tests/hikari/impl/test_rest.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index da37ab435f..0a01a09e24 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -2583,7 +2583,6 @@ async def test_edit_message_when_form(self, rest_client): component_obj2 = object() embed_obj = object() embed_obj2 = object() - poll_obj = object() mock_form = mock.Mock() mock_body = data_binding.JSONObjectBuilder() mock_body.put("testing", "ensure_in_test") @@ -2601,7 +2600,6 @@ async def test_edit_message_when_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], - poll=poll_obj, mentions_everyone=False, user_mentions=[9876], role_mentions=[1234], @@ -2617,7 +2615,6 @@ async def test_edit_message_when_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], - poll=poll_obj, flags=120, mentions_everyone=False, mentions_reply=undefined.UNDEFINED, @@ -2638,7 +2635,6 @@ async def test_edit_message_when_no_form(self, rest_client): component_obj2 = object() embed_obj = object() embed_obj2 = object() - poll_obj = object() mock_body = data_binding.JSONObjectBuilder() mock_body.put("testing", "ensure_in_test") expected_route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=123456789, message=987654321) @@ -2655,7 +2651,6 @@ async def test_edit_message_when_no_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], - poll=poll_obj, mentions_everyone=False, user_mentions=[9876], role_mentions=[1234], @@ -2671,7 +2666,6 @@ async def test_edit_message_when_no_form(self, rest_client): components=[component_obj2], embed=embed_obj, embeds=[embed_obj2], - poll=poll_obj, flags=120, mentions_everyone=False, mentions_reply=undefined.UNDEFINED, From eb41493ed615f8237893f5d2311a82f3fed3ed6b Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:51:27 +0700 Subject: [PATCH 62/72] Correct poll answer payload extraction --- hikari/impl/entity_factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index b24cd4fb7d..a8f50b58e2 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3774,9 +3774,9 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll for answer_payload in payload["answers"]: answer_id = answer_payload["answer_id"] - emoji = answer_payload["emoji"] + emoji = answer_payload["poll_media"]["emoji"] poll_media = poll_models.PollMedia( - text=answer_payload["text"], emoji=self.deserialize_emoji(emoji) if emoji else None + text=answer_payload["poll_media"]["text"], emoji=self.deserialize_emoji(emoji) if emoji else None ) answers.append(poll_models.PollAnswer(answer_id=answer_id, poll_media=poll_media)) From 25ce6f281dfb46b235a576b9be28527d15d55d20 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:51:54 +0700 Subject: [PATCH 63/72] Correct event name in test --- tests/hikari/impl/test_event_factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py index dee799bf9e..e668a39fbe 100644 --- a/tests/hikari/impl/test_event_factory.py +++ b/tests/hikari/impl/test_event_factory.py @@ -1534,7 +1534,7 @@ def test_deserialize_poll_vote_create_event(self, event_factory, mock_app, mock_ event = event_factory.deserialize_poll_vote_create_event(mock_shard, payload) - assert isinstance(event, poll_events.PollVoteCreate) + assert isinstance(event, poll_events.PollVoteCreateEvent) def test_deserialize_poll_vote_delete_event(self, event_factory, mock_app, mock_shard): payload = { @@ -1547,4 +1547,4 @@ def test_deserialize_poll_vote_delete_event(self, event_factory, mock_app, mock_ event = event_factory.deserialize_poll_vote_delete_event(mock_shard, payload) - assert isinstance(event, poll_events.PollVoteDelete) + assert isinstance(event, poll_events.PollVoteDeleteEvent) From afeeebfe8fffe48e19de3bee0b6951179e048359 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:00:13 +0700 Subject: [PATCH 64/72] ...missed some Continuation of 51fe4e4eb13f6b3e34f9321f258bad789792847d --- hikari/messages.py | 1 - tests/hikari/test_messages.py | 1 - 2 files changed, 2 deletions(-) diff --git a/hikari/messages.py b/hikari/messages.py index 08cf75e46e..703dfae6a8 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -915,7 +915,6 @@ async def edit( components=components, embed=embed, embeds=embeds, - poll=poll, mentions_everyone=mentions_everyone, mentions_reply=mentions_reply, user_mentions=user_mentions, diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index ae92d32dbc..d855a4e864 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -198,7 +198,6 @@ async def test_edit(self, message): content="test content", embed=embed, embeds=embeds, - poll=poll, attachment=attachment, attachments=[attachment, attachment], component=component, From 3440a1546eff5ba5faab6a93f006d9b57ed34609 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:25:01 +0700 Subject: [PATCH 65/72] Fix test_fetch_poll_voter occasionally not working Running `nox -s pytest` would randomly throws an error for failing `assert isinstance(response, list)`. Failed on my machine, does not fail on Platy's machine. This fixes the issue by making sure `AsyncMock` returns a proper list instead of an `AsyncMock` object. Co-authored by: davfsa Thanks dav! --- tests/hikari/impl/test_rest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 0a01a09e24..a1231cb7cf 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -6440,7 +6440,8 @@ async def test_fetch_poll_voters(self, rest_client: rest.RESTClientImpl): expected_route = routes.GET_POLL_ANSWER.compile( channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4) ) - rest_client._request = mock.AsyncMock() + # FIXME: Test that returned values get deserialized correctly + rest_client._request = mock.AsyncMock(return_value=[]) await rest_client.fetch_poll_voters( StubModel(45874392), StubModel(398475938475), StubModel(4), after=StubModel(43587935), limit=6 From 9b33958bd7aab62dfaae5eeee7c5c43253230f20 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Thu, 1 Aug 2024 13:55:31 +1000 Subject: [PATCH 66/72] added missing tests. on_message_poll_vote_add on_message_poll_vote_remove fixed get_poll_answer test. --- tests/hikari/impl/test_event_manager.py | 30 +++++++++++++++++++++++++ tests/hikari/impl/test_rest.py | 15 ++++++++----- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py index 4f28f7029d..5ba3d6fd4d 100644 --- a/tests/hikari/impl/test_event_manager.py +++ b/tests/hikari/impl/test_event_manager.py @@ -1692,3 +1692,33 @@ async def test_on_guild_audit_log_entry_create( event_manager_impl.dispatch.assert_awaited_once_with( event_factory.deserialize_audit_log_entry_create_event.return_value ) + + @pytest.mark.asyncio + async def test_on_message_poll_vote_add( + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + mock_payload = mock.Mock() + + await event_manager_impl.on_message_poll_vote_add(shard, mock_payload) + + event_factory.deserialize_poll_vote_create_event.assert_called_once_with(shard, mock_payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_audit_log_entry_create_event.return_value + ) + + @pytest.mark.asyncio + async def test_on_message_poll_vote_remove( + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + mock_payload = mock.Mock() + + await event_manager_impl.on_message_poll_vote_remove(shard, mock_payload) + + event_factory.deserialize_poll_vote_delete_event.assert_called_once_with(shard, mock_payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_audit_log_entry_create_event.return_value + ) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index a1231cb7cf..fdf1a2389b 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -460,7 +460,7 @@ def stream(self, executor): return FileResource -@pytest.fixture + def file_resource_patch(file_resource): resource = file_resource("some data") with mock.patch.object(files, "ensure_resource", return_value=resource): @@ -6440,12 +6440,15 @@ async def test_fetch_poll_voters(self, rest_client: rest.RESTClientImpl): expected_route = routes.GET_POLL_ANSWER.compile( channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4) ) - # FIXME: Test that returned values get deserialized correctly - rest_client._request = mock.AsyncMock(return_value=[]) + + rest_client._request = mock.AsyncMock(return_value=[{"id":"1234"}]) - await rest_client.fetch_poll_voters( - StubModel(45874392), StubModel(398475938475), StubModel(4), after=StubModel(43587935), limit=6 - ) + with mock.patch.object(rest_client._entity_factory, "deserialize_user", return_value=mock.Mock()) as patched_deserialize_user: + await rest_client.fetch_poll_voters( + StubModel(45874392), StubModel(398475938475), StubModel(4), after=StubModel(43587935), limit=6 + ) + + patched_deserialize_user.assert_called_once_with({"id":"1234"}) rest_client._request.assert_awaited_once_with(expected_route, query={"after": "43587935", "limit": "6"}) From 46d9cbce8a53b0d56684796126e2ba478e2682a7 Mon Sep 17 00:00:00 2001 From: MPlaty Date: Thu, 1 Aug 2024 14:22:18 +1000 Subject: [PATCH 67/72] Added tests for polls --- tests/hikari/impl/test_event_manager.py | 8 +-- tests/hikari/impl/test_rest.py | 11 ++-- tests/hikari/test_polls.py | 68 +++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 tests/hikari/test_polls.py diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py index 5ba3d6fd4d..b53fd5d132 100644 --- a/tests/hikari/impl/test_event_manager.py +++ b/tests/hikari/impl/test_event_manager.py @@ -1695,9 +1695,7 @@ async def test_on_guild_audit_log_entry_create( @pytest.mark.asyncio async def test_on_message_poll_vote_add( - event_manager_impl: event_manager.EventManagerImpl, - shard: mock.Mock, - event_factory: event_factory_.EventFactory, + event_manager_impl: event_manager.EventManagerImpl, shard: mock.Mock, event_factory: event_factory_.EventFactory ): mock_payload = mock.Mock() @@ -1710,9 +1708,7 @@ async def test_on_message_poll_vote_add( @pytest.mark.asyncio async def test_on_message_poll_vote_remove( - event_manager_impl: event_manager.EventManagerImpl, - shard: mock.Mock, - event_factory: event_factory_.EventFactory, + event_manager_impl: event_manager.EventManagerImpl, shard: mock.Mock, event_factory: event_factory_.EventFactory ): mock_payload = mock.Mock() diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index fdf1a2389b..bf07c10bfc 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -460,7 +460,6 @@ def stream(self, executor): return FileResource - def file_resource_patch(file_resource): resource = file_resource("some data") with mock.patch.object(files, "ensure_resource", return_value=resource): @@ -6440,15 +6439,17 @@ async def test_fetch_poll_voters(self, rest_client: rest.RESTClientImpl): expected_route = routes.GET_POLL_ANSWER.compile( channel=StubModel(45874392), message=StubModel(398475938475), answer=StubModel(4) ) - - rest_client._request = mock.AsyncMock(return_value=[{"id":"1234"}]) - with mock.patch.object(rest_client._entity_factory, "deserialize_user", return_value=mock.Mock()) as patched_deserialize_user: + rest_client._request = mock.AsyncMock(return_value=[{"id": "1234"}]) + + with mock.patch.object( + rest_client._entity_factory, "deserialize_user", return_value=mock.Mock() + ) as patched_deserialize_user: await rest_client.fetch_poll_voters( StubModel(45874392), StubModel(398475938475), StubModel(4), after=StubModel(43587935), limit=6 ) - patched_deserialize_user.assert_called_once_with({"id":"1234"}) + patched_deserialize_user.assert_called_once_with({"id": "1234"}) rest_client._request.assert_awaited_once_with(expected_route, query={"after": "43587935", "limit": "6"}) diff --git a/tests/hikari/test_polls.py b/tests/hikari/test_polls.py new file mode 100644 index 0000000000..f0fedf9ab9 --- /dev/null +++ b/tests/hikari/test_polls.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE.\ + +from hikari import polls + + +class TestPollBuilder: + def test_add_answer(self): + poll = polls.PollBuilder("question", 1, False) + + poll.add_answer("beanos", None) + + assert len(list(poll.answers)) == 1 + + assert list(poll.answers)[0] == polls.PollAnswer( + answer_id=-1, poll_media=polls.PollMedia(text="beanos", emoji=None) + ) + + def test_edit_answer(self): + poll = polls.PollBuilder("question", 1, False) + + poll.add_answer("beanos", None) + + assert len(list(poll.answers)) == 1 + + assert list(poll.answers)[0] == polls.PollAnswer( + answer_id=-1, poll_media=polls.PollMedia(text="beanos", emoji=None) + ) + + poll.edit_answer(0, emoji="🫘") + + assert list(poll.answers)[0] == polls.PollAnswer( + answer_id=-1, poll_media=polls.PollMedia(text="beanos", emoji="🫘") + ) + + def test_remove_answer(self): + poll = polls.PollBuilder("question", 1, False) + + poll.add_answer("beanos", None) + + assert len(list(poll.answers)) == 1 + + assert list(poll.answers)[0] == polls.PollAnswer( + answer_id=-1, poll_media=polls.PollMedia(text="beanos", emoji=None) + ) + + poll.remove_answer(0) + + assert len(list(poll.answers)) == 0 From 9f0e4c88b0b43bfb7e8f58dcf2ee2ba2da57b73a Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:23:14 +0700 Subject: [PATCH 68/72] Remove leftover inlined method --- hikari/impl/entity_factory.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index a8f50b58e2..5d9a2425ed 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3759,11 +3759,6 @@ def deserialize_sku(self, payload: data_binding.JSONObject) -> monetization_mode ############### # POLL MODELS # ############### - def _deserialize_poll_answer_count(self, payload: data_binding.JSONObject) -> poll_models.PollAnswerCount: - return poll_models.PollAnswerCount( - answer_id=payload["answer_id"], count=payload["count"], me_voted=payload["me_voted"] - ) - def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll: question = payload["question"]["text"] expiry = time.iso8601_datetime_string_to_datetime(payload["expiry"]) From ff06143b7c5a790394e9242180286f3e57160245 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:13:57 +0700 Subject: [PATCH 69/72] Update copyright holder --- hikari/events/poll_events.py | 3 ++- hikari/polls.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/hikari/events/poll_events.py b/hikari/events/poll_events.py index a93862fad6..be386e3bda 100644 --- a/hikari/events/poll_events.py +++ b/hikari/events/poll_events.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright (c) 2024-present PythonTryHard +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/hikari/polls.py b/hikari/polls.py index 36e4078a87..ab46818c1f 100644 --- a/hikari/polls.py +++ b/hikari/polls.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright (c) 2024-present PythonTryHard, MPlatypus +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal From 466af5b6ad45708e920b0f11d6e06e4bada08dd4 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Sat, 10 Aug 2024 19:59:31 +0700 Subject: [PATCH 70/72] Fix failing poll tests --- tests/hikari/impl/test_event_manager.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py index b53fd5d132..d5b2d2b123 100644 --- a/tests/hikari/impl/test_event_manager.py +++ b/tests/hikari/impl/test_event_manager.py @@ -1694,8 +1694,11 @@ async def test_on_guild_audit_log_entry_create( ) @pytest.mark.asyncio - async def test_on_message_poll_vote_add( - event_manager_impl: event_manager.EventManagerImpl, shard: mock.Mock, event_factory: event_factory_.EventFactory + async def test_on_message_poll_vote_create( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, ): mock_payload = mock.Mock() @@ -1703,12 +1706,15 @@ async def test_on_message_poll_vote_add( event_factory.deserialize_poll_vote_create_event.assert_called_once_with(shard, mock_payload) event_manager_impl.dispatch.assert_awaited_once_with( - event_factory.deserialize_audit_log_entry_create_event.return_value + event_factory.deserialize_poll_vote_create_event.return_value ) @pytest.mark.asyncio - async def test_on_message_poll_vote_remove( - event_manager_impl: event_manager.EventManagerImpl, shard: mock.Mock, event_factory: event_factory_.EventFactory + async def test_on_message_poll_vote_delete( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, ): mock_payload = mock.Mock() @@ -1716,5 +1722,5 @@ async def test_on_message_poll_vote_remove( event_factory.deserialize_poll_vote_delete_event.assert_called_once_with(shard, mock_payload) event_manager_impl.dispatch.assert_awaited_once_with( - event_factory.deserialize_audit_log_entry_create_event.return_value + event_factory.deserialize_poll_vote_delete_event.return_value ) From f2131f6950a9e80cc5003c5489ed1fb409eac52f Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Sat, 10 Aug 2024 20:00:52 +0700 Subject: [PATCH 71/72] I guess this is fine --- hikari/impl/entity_factory.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 5d9a2425ed..dec40c1b4a 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -3798,7 +3798,6 @@ def deserialize_poll(self, payload: data_binding.JSONObject) -> poll_models.Poll ) def _serialize_poll_media(self, poll_media: poll_models.PollMedia) -> data_binding.JSONObject: - # FIXME: Typing is **very** dodgy here. Revise this before shipping. serialised_poll_media: typing.MutableMapping[str, typing.Any] = {"text": poll_media.text} if isinstance(poll_media.emoji, emoji_models.UnicodeEmoji): From a751cc4bc934880a94d625fd94ff63e7344bc6f9 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Sat, 10 Aug 2024 23:10:09 +0700 Subject: [PATCH 72/72] I did a dumb --- tests/hikari/impl/test_rest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index bf07c10bfc..509c786f92 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -460,6 +460,7 @@ def stream(self, executor): return FileResource +@pytest.fixture def file_resource_patch(file_resource): resource = file_resource("some data") with mock.patch.object(files, "ensure_resource", return_value=resource):