Skip to content

Commit afbbc07

Browse files
authored
Add support for poll result messages
1 parent ff2ad34 commit afbbc07

7 files changed

Lines changed: 136 additions & 5 deletions

File tree

discord/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ class MessageType(Enum):
266266
guild_incident_report_raid = 38
267267
guild_incident_report_false_alarm = 39
268268
purchase_notification = 44
269+
poll_result = 46
269270

270271

271272
class SpeakingState(Enum):

discord/message.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2268,6 +2268,13 @@ def __init__(
22682268
# the channel will be the correct type here
22692269
ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore
22702270

2271+
if self.type is MessageType.poll_result:
2272+
if isinstance(self.reference.resolved, self.__class__):
2273+
self._state._update_poll_results(self, self.reference.resolved)
2274+
else:
2275+
if self.reference.message_id:
2276+
self._state._update_poll_results(self, self.reference.message_id)
2277+
22712278
self.application: Optional[MessageApplication] = None
22722279
try:
22732280
application = data['application']
@@ -2634,6 +2641,7 @@ def is_system(self) -> bool:
26342641
MessageType.chat_input_command,
26352642
MessageType.context_menu_command,
26362643
MessageType.thread_starter_message,
2644+
MessageType.poll_result,
26372645
)
26382646

26392647
@utils.cached_slot_property('_cs_system_content')
@@ -2810,6 +2818,14 @@ def system_content(self) -> str:
28102818
if guild_product_purchase is not None:
28112819
return f'{self.author.name} has purchased {guild_product_purchase.product_name}!'
28122820

2821+
if self.type is MessageType.poll_result:
2822+
embed = self.embeds[0] # Will always have 1 embed
2823+
poll_title = utils.get(
2824+
embed.fields,
2825+
name='poll_question_text',
2826+
)
2827+
return f'{self.author.display_name}\'s poll {poll_title.value} has closed.' # type: ignore
2828+
28132829
# Fallback for unknown message types
28142830
return ''
28152831

discord/poll.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
import datetime
3131

32-
from .enums import PollLayoutType, try_enum
32+
from .enums import PollLayoutType, try_enum, MessageType
3333
from . import utils
3434
from .emoji import PartialEmoji, Emoji
3535
from .user import User
@@ -125,7 +125,16 @@ class PollAnswer:
125125
Whether the current user has voted to this answer or not.
126126
"""
127127

128-
__slots__ = ('media', 'id', '_state', '_message', '_vote_count', 'self_voted', '_poll')
128+
__slots__ = (
129+
'media',
130+
'id',
131+
'_state',
132+
'_message',
133+
'_vote_count',
134+
'self_voted',
135+
'_poll',
136+
'_victor',
137+
)
129138

130139
def __init__(
131140
self,
@@ -141,6 +150,7 @@ def __init__(
141150
self._vote_count: int = 0
142151
self.self_voted: bool = False
143152
self._poll: Poll = poll
153+
self._victor: bool = False
144154

145155
def _handle_vote_event(self, added: bool, self_voted: bool) -> None:
146156
if added:
@@ -210,6 +220,19 @@ def _to_dict(self) -> PollAnswerPayload:
210220
'poll_media': self.media.to_dict(),
211221
}
212222

223+
@property
224+
def victor(self) -> bool:
225+
""":class:`bool`: Whether the answer is the one that had the most
226+
votes when the poll ended.
227+
228+
.. versionadded:: 2.5
229+
230+
.. note::
231+
232+
If the poll has not ended, this will always return ``False``.
233+
"""
234+
return self._victor
235+
213236
async def voters(
214237
self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None
215238
) -> AsyncIterator[Union[User, Member]]:
@@ -325,6 +348,8 @@ class Poll:
325348
'_expiry',
326349
'_finalized',
327350
'_state',
351+
'_total_votes',
352+
'_victor_answer_id',
328353
)
329354

330355
def __init__(
@@ -348,6 +373,8 @@ def __init__(
348373
self._state: Optional[ConnectionState] = None
349374
self._finalized: bool = False
350375
self._expiry: Optional[datetime.datetime] = None
376+
self._total_votes: Optional[int] = None
377+
self._victor_answer_id: Optional[int] = None
351378

352379
def _update(self, message: Message) -> None:
353380
self._state = message._state
@@ -360,6 +387,33 @@ def _update(self, message: Message) -> None:
360387
self._expiry = message.poll.expires_at
361388
self._finalized = message.poll._finalized
362389
self._answers = message.poll._answers
390+
self._update_results_from_message(message)
391+
392+
def _update_results_from_message(self, message: Message) -> None:
393+
if message.type != MessageType.poll_result or not message.embeds:
394+
return
395+
396+
result_embed = message.embeds[0] # Will always have 1 embed
397+
fields: Dict[str, str] = {field.name: field.value for field in result_embed.fields} # type: ignore
398+
399+
total_votes = fields.get('total_votes')
400+
401+
if total_votes is not None:
402+
self._total_votes = int(total_votes)
403+
404+
victor_answer = fields.get('victor_answer_id')
405+
406+
if victor_answer is None:
407+
return # Can't do anything else without the victor answer
408+
409+
self._victor_answer_id = int(victor_answer)
410+
411+
victor_answer_votes = fields['victor_answer_votes']
412+
413+
answer = self._answers[self._victor_answer_id]
414+
answer._victor = True
415+
answer._vote_count = int(victor_answer_votes)
416+
self._answers[answer.id] = answer # Ensure update
363417

364418
def _update_results(self, data: PollResultPayload) -> None:
365419
self._finalized = data['is_finalized']
@@ -432,6 +486,32 @@ def answers(self) -> List[PollAnswer]:
432486
"""List[:class:`PollAnswer`]: Returns a read-only copy of the answers."""
433487
return list(self._answers.values())
434488

489+
@property
490+
def victor_answer_id(self) -> Optional[int]:
491+
"""Optional[:class:`int`]: The victor answer ID.
492+
493+
.. versionadded:: 2.5
494+
495+
.. note::
496+
497+
This will **always** be ``None`` for polls that have not yet finished.
498+
"""
499+
return self._victor_answer_id
500+
501+
@property
502+
def victor_answer(self) -> Optional[PollAnswer]:
503+
"""Optional[:class:`PollAnswer`]: The victor answer.
504+
505+
.. versionadded:: 2.5
506+
507+
.. note::
508+
509+
This will **always** be ``None`` for polls that have not yet finished.
510+
"""
511+
if self.victor_answer_id is None:
512+
return None
513+
return self.get_answer(self.victor_answer_id)
514+
435515
@property
436516
def expires_at(self) -> Optional[datetime.datetime]:
437517
"""Optional[:class:`datetime.datetime`]: A datetime object representing the poll expiry.
@@ -457,12 +537,20 @@ def created_at(self) -> Optional[datetime.datetime]:
457537

458538
@property
459539
def message(self) -> Optional[Message]:
460-
""":class:`Message`: The message this poll is from."""
540+
"""Optional[:class:`Message`]: The message this poll is from."""
461541
return self._message
462542

463543
@property
464544
def total_votes(self) -> int:
465-
""":class:`int`: Returns the sum of all the answer votes."""
545+
""":class:`int`: Returns the sum of all the answer votes.
546+
547+
If the poll has not yet finished, this is an approximate vote count.
548+
549+
.. versionchanged:: 2.5
550+
This now returns an exact vote count when updated from its poll results message.
551+
"""
552+
if self._total_votes is not None:
553+
return self._total_votes
466554
return sum([answer.vote_count for answer in self.answers])
467555

468556
def is_finalised(self) -> bool:

discord/state.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,27 @@ def _update_poll_counts(self, message: Message, answer_id: int, added: bool, sel
552552
poll._handle_vote(answer_id, added, self_voted)
553553
return poll
554554

555+
def _update_poll_results(self, from_: Message, to: Union[Message, int]) -> None:
556+
if isinstance(to, Message):
557+
cached = self._get_message(to.id)
558+
elif isinstance(to, int):
559+
cached = self._get_message(to)
560+
561+
if cached is None:
562+
return
563+
564+
to = cached
565+
else:
566+
return
567+
568+
if to.poll is None:
569+
return
570+
571+
to.poll._update_results_from_message(from_)
572+
573+
if cached is not None and cached.poll:
574+
cached.poll._update_results_from_message(from_)
575+
555576
async def chunker(
556577
self, guild_id: int, query: str = '', limit: int = 0, presences: bool = False, *, nonce: Optional[str] = None
557578
) -> None:

discord/types/embed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class EmbedAuthor(TypedDict, total=False):
7171
proxy_icon_url: str
7272

7373

74-
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link']
74+
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link', 'poll_result']
7575

7676

7777
class Embed(TypedDict, total=False):

discord/types/message.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class CallMessage(TypedDict):
174174
38,
175175
39,
176176
44,
177+
46,
177178
]
178179

179180

docs/api.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,6 +1887,10 @@ of :class:`enum.Enum`.
18871887

18881888
.. versionadded:: 2.5
18891889

1890+
.. attribute:: poll_result
1891+
1892+
The system message sent when a poll has closed.
1893+
18901894
.. class:: UserFlags
18911895

18921896
Represents Discord User flags.

0 commit comments

Comments
 (0)