From 225a4b5b4745fc867f054c66e033ff2212cea72f Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:21:29 +0200 Subject: [PATCH 01/95] Added new permissions --- general/polls/permissions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/general/polls/permissions.py b/general/polls/permissions.py index 5195de9d3..7d4b6992e 100644 --- a/general/polls/permissions.py +++ b/general/polls/permissions.py @@ -10,4 +10,7 @@ def description(self) -> str: return t.polls.permissions[self.name] team_poll = auto() + read = auto() + write = auto() delete = auto() + anonymous_bypass = auto() From 6681aef999bb52eab48ed74b63e9f70fea01d921 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:25:29 +0200 Subject: [PATCH 02/95] Rewrote en.yml with more translation-strings --- general/polls/translations/en.yml | 176 +++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 5 deletions(-) diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index e779d9255..fcbd97149 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -1,26 +1,192 @@ +commands: + poll: + poll: poll commands + quick: small poll with default options + new: advanced poll with more options + delete: delete polls + voted: show who voted on the poll (only works if not anonymous or if the poll-owner uses the command) + list: show the active polls + settings: + settings: poll settings + roles_weights: manage weight for certain roles + duration: set the default hours a poll should be open + max_duration: set the maximum duration a poll can be opened + votes: set the default amount of votes a user can have on polls + anonymous: set if user can see who voted on a poll + fair: manage if role weights impact on default polls + yes_no: add thumbs-up/down emotes on a message + team_yes_no: starts a yes/no poll and shows, which teamler has not voted yet. + permissions: team_poll: start a team poll + read: read poll configuration + write: edit poll configuration delete: delete polls + anonymous_bypass: can see user, even if poll is anonymous + +error: + weight_too_small: "Weight cant be lower than `0.1`" + cant_set_weight: Can't set weight! + not_poll: Mesage doesn't contains a poll + no_teamlers: No user with team-role found! + cant_pin: + title: Error + description: Can't pin any more messages in {} + +poll_config: + title: Default poll configuration + duration: + name: "**Duration**" + time: + one: "{cnt} hour" + many: "{cnt} hours" + unlimited: max duration + max_duration: + name: "**Max Duration**" + time: + one: "{cnt} days" + many: "{cnt} days" + choices: + name: "**Choices per user**" + amount: + one: "{cnt} choice per user" + many: "{cnt} choices per user" + unlimited: unlimited + anonymous: + name: "**Anonymous**" + fair: + name: "**Fair polls**" + roles: + name: "**Role Weights**" + ev_row: "{} -> `{}x`" + row: "\n<@&{}> -> `{}x`" + +polls: + title: Active polls + row: "\n[`{}`]({}) by <@{}> until {}" + team_row: "\n:star: [`{}`]({}) by <@{}> until {}" + +role_weight: + set: "Set vote weight for <@&{}> to `{}`" + reset: "Vote weight has been reset for <@&{}>" + +weight_everyone: + set: "Set vote weight for the default role to `{}`" + reset: Vote weight for the default role has been reset + +duration: + set: + one: "Set default duration for poll to {cnt} hour" + many: "Set default duration for poll to {cnt} hours" + reset: "Set the default duration for polls to unlimited" + +max_duration: + set: + one: "Set maximum duration for a poll to {cnt} day" + many: "Set maximum duration for a poll to {cnt} days" + +votes: + set: + one: "Set default votes for a poll to {cnt} vote" + many: "Set default votes for a poll to {cnt} votes" + reset: "Set the default votes for polls to unlimited" + +voted: + title: Votes + row: "\n <@{}> -> Options: {}" + +anonymous: + is_on: made default poll votes anonymous + is_off: made default poll votes visible + +fair: + is_on: made default poll votes fair + is_off: made default poll votes based on roles + +select: + place: Select Options + placeholder: + one: "Select an option!" + many: "Select up to {cnt} options!" + label: "Option {}." + +delete: + confirm_text: Are you sure that you want to delete this poll? + +usage: + poll: | + + [emoji1] + [emojiX] [optionX] + +yes_no: + in_favor: "Yes" + against: "No" + abstention: "Abstention" + option_string: "{}\n:thumbsup: Yes\n:thumbsdown: No\n:zzz: Abstention" + count: + one: "{cnt} vote ({}%)" + many: "{cnt} votes ({}%)" + +option: + field: + name: "**Votes: {} ({}%)**" + +skip: + message: skip + title: Skipped + description: Skipped poll wizard -> default poll created! + +wizard: + title: Poll wizard + description: Set arguments for an advanced poll + skip: + name: Skip setup + value: To skip the setup type `skip` + arg: Arguments + args: | + ``` + --type {standard,team}, -T {standard,team} + standard or team embed [Default: 'standard'] + + --deadline DEADLINE, -D DEADLINE + time when the poll should be closed [Default: server settings] + + --anonymous {True,False}, -A {True,False} + people can see who voted or not [Default: server settings] + + --choices CHOICES, -C CHOICES + the amount of votes someone can set [Default: multiple choices] + + --fair {True,False}, -F {True,False} + all roles have the same vote weight [Default: server settings] + ``` + example: + name: Example + value: | + `--duration 6 --choices 4 -A True` + + --> Creates an anonymous, 6 hours long poll with 4 select choices for every user poll: Poll team_poll: Team Poll +poll_voted: "Vote was added to the poll" +team_yn_poll_forbidden: You are not allowed to use a team poll! vote_explanation: Vote using the reactions below! too_many_options: You specified too many options. The maximum amount is {}. option_too_long: Options are limited to {} characters. missing_options: Missing options option_duplicated: You may not use the same emoji twice! empty_option: Empty option -poll_usage: | - - [emoji1] - [emojiX] [optionX] team_role_not_set: Team role is not set. team_role_no_members: The team role has no members. teampoll_all_voted: "All teamlers voted :white_check_mark:" teamlers_missing: one: "{last} hasn't voted yet." many: "{teamlers} and {last} haven't voted yet." -created_by: Created by @{} ({}) +footer: Ends at {} UTC +footer_closed: Closed can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option" foreign_message: "You are not allowed to add yes/no reactions to foreign messages!" could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}. +no_polls: No current active polls. From 194fb03ac9d927c63c509dfa300f7f8e8464b714 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:26:43 +0200 Subject: [PATCH 03/95] Added settings.py --- general/polls/settings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 general/polls/settings.py diff --git a/general/polls/settings.py b/general/polls/settings.py new file mode 100644 index 000000000..c5d7a646f --- /dev/null +++ b/general/polls/settings.py @@ -0,0 +1,10 @@ +from PyDrocsid.settings import Settings + + +class PollsDefaultSettings(Settings): + duration = 0 # 0 for max_duration duration (duration in hours) + max_duration = 7 # max duration (duration in days) + max_choices = 0 # 0 for unlimited choices + everyone_power = 1.0 + anonymous = False + fair = False From 400ec67845481619173f671b587fa3329e363923 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:30:54 +0200 Subject: [PATCH 04/95] Added models for polls, options on polls and user who voted on options --- general/polls/models.py | 125 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 general/polls/models.py diff --git a/general/polls/models.py b/general/polls/models.py new file mode 100644 index 000000000..11e35d319 --- /dev/null +++ b/general/polls/models.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import enum +from datetime import datetime +from typing import Optional, Union + +from discord.utils import utcnow +from sqlalchemy import BigInteger, Boolean, Column, Enum, Float, ForeignKey, Text +from sqlalchemy.orm import relationship + +from PyDrocsid.database import Base, UTCDateTime, db + + +class PollType(enum.Enum): + TEAM = "team" + STANDARD = "standard" + + +class Poll(Base): + __tablename__ = "poll" + + id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True) + + options: list[Option] = relationship("Option", back_populates="poll", cascade="all, delete") + + message_id: Union[Column, int] = Column(BigInteger, unique=True) + message_url: Union[Column, str] = Column(Text(256)) + guild_id: Union[Column, int] = Column(BigInteger) + interaction_message_id: Union[Column, int] = Column(BigInteger, unique=True) + channel_id: Union[Column, int] = Column(BigInteger) + owner_id: Union[Column, int] = Column(BigInteger) + timestamp: Union[Column, datetime] = Column(UTCDateTime) + title: Union[Column, str] = Column(Text(256)) + poll_type: Union[Column, PollType] = Column(Enum(PollType)) + end_time: Union[Column, datetime] = Column(UTCDateTime) + anonymous: Union[Column, bool] = Column(Boolean) + can_delete: Union[Column, bool] = Column(Boolean) + fair: Union[Column, bool] = Column(Boolean) + active: Union[Column, bool] = Column(Boolean) + max_choices: Union[Column, int] = Column(BigInteger) + + @staticmethod + async def create( + message_id: int, + message_url: str, + guild_id: int, + channel: int, + owner: int, + title: str, + options: list[tuple[str, str]], + end: Optional[datetime], + anonymous: bool, + can_delete: bool, + poll_type: enum.Enum, + interaction: int, + fair: bool, + max_choices: int, + ) -> Poll: + row = Poll( + message_id=message_id, + message_url=message_url, + guild_id=guild_id, + channel_id=channel, + owner_id=owner, + timestamp=utcnow(), + title=title, + poll_type=poll_type, + end_time=end, + anonymous=anonymous, + can_delete=can_delete, + interaction_message_id=interaction, + fair=fair, + active=True, + max_choices=max_choices, + ) + for position, poll_option in enumerate(options): + row.options.append( + await Option.create( + poll=message_id, emote=poll_option[0], option_text=poll_option[1], field_position=position + ) + ) + + await db.add(row) + return row + + async def remove(self): + await db.delete(self) + + +class Option(Base): + __tablename__ = "poll_option" + + id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True) + poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll.message_id")) + votes: list[PollVote] = relationship("PollVote", back_populates="option", cascade="all, delete") + poll: Poll = relationship("Poll", back_populates="options") + emote: Union[Column, str] = Column(Text(30)) + option: Union[Column, str] = Column(Text(250)) + field_position: Union[Column, int] = Column(BigInteger) + + @staticmethod + async def create(poll: int, emote: str, option_text: str, field_position: int) -> Option: + options = Option(poll_id=poll, emote=emote, option=option_text, field_position=field_position) + await db.add(options) + return options + + +class PollVote(Base): + __tablename__ = "voted_user" + + id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True) + user_id: Union[Column, int] = Column(BigInteger) + option_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll_option.id")) + option: Option = relationship("Option", back_populates="votes") + vote_weight: Union[Column, float] = Column(Float) + poll_id: Union[Column, int] = Column(BigInteger) + + @staticmethod + async def create(user_id: int, option_id: int, vote_weight: float, poll_id: int): + row = PollVote(user_id=user_id, option_id=option_id, vote_weight=vote_weight, poll_id=poll_id) + await db.add(row) + return row + + async def remove(self): + await db.delete(self) From e1932e7b06a9727b43794097ac3dd432308e2c5a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:31:41 +0200 Subject: [PATCH 05/95] Added models for role weights and function to sync redis --- general/polls/models.py | 59 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/general/polls/models.py b/general/polls/models.py index 11e35d319..7cadb11b7 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -4,11 +4,14 @@ from datetime import datetime from typing import Optional, Union +from discord import Role from discord.utils import utcnow from sqlalchemy import BigInteger, Boolean, Column, Enum, Float, ForeignKey, Text from sqlalchemy.orm import relationship -from PyDrocsid.database import Base, UTCDateTime, db +from PyDrocsid.database import Base, UTCDateTime, db, filter_by, select +from PyDrocsid.environment import CACHE_TTL +from PyDrocsid.redis import redis class PollType(enum.Enum): @@ -16,6 +19,24 @@ class PollType(enum.Enum): STANDARD = "standard" +async def sync_redis(role_id: int = None) -> list[dict[str, int | float]]: + out = [] + + async with redis.pipeline() as pipe: + if role_id: + await pipe.delete(f"poll_role_weight={role_id}") + weights: RoleWeight + async for weights in await db.stream(select(RoleWeight)): + await pipe.delete(key := f"poll_role_weight={role_id or weights.role_id}") + save = {"role": int(weights.role_id), "weight": float(weights.weight)} + out.append(save) + await pipe.setex(key, CACHE_TTL, str(weights.weight)) + + await pipe.execute() + + return out + + class Poll(Base): __tablename__ = "poll" @@ -123,3 +144,39 @@ async def create(user_id: int, option_id: int, vote_weight: float, poll_id: int) async def remove(self): await db.delete(self) + + +class RoleWeight(Base): + __tablename__ = "role_weight" + + id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True) + guild_id: Union[Column, int] = Column(BigInteger) + role_id: Union[Column, int] = Column(BigInteger, unique=True) + weight: Union[Column, float] = Column(Float) + timestamp: Union[Column, datetime] = Column(UTCDateTime) + + @staticmethod + async def create(guild_id: int, role: int, weight: float) -> RoleWeight: + role_weight = RoleWeight(guild_id=guild_id, role_id=role, weight=weight, timestamp=utcnow()) + await db.add(role_weight) + await sync_redis() + return role_weight + + async def remove(self) -> None: + await db.delete(self) + await sync_redis(self.role_id) + + @staticmethod + async def get(guild: int) -> list[RoleWeight]: + return await db.all(filter_by(RoleWeight, guild_id=guild)) + + @staticmethod + async def get_highest(user_roles: list[Role]) -> float: + weight: float = 0.0 + for role in user_roles: + _weight = await redis.get(f"poll_role_weight={role.id}") + + if _weight and weight < (_weight := float(_weight)): + weight = _weight + + return weight From 26359f985966c4d8426c266afd6de7edda2ad8b5 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:37:15 +0200 Subject: [PATCH 06/95] Improved constants --- general/polls/cog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index decfed960..814d5e63b 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -23,9 +23,9 @@ tg = t.g t = t.polls -MAX_OPTIONS = 20 # Discord reactions limit +MAX_OPTIONS = 25 # Discord select menu limit -default_emojis = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase] +DEFAULT_EMOJIS = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase] async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: @@ -244,7 +244,7 @@ def __init__(self, ctx: Context, line: str, number: int): self.emoji = unicode_emoji self.option = text.strip() else: - self.emoji = default_emojis[number] + self.emoji = DEFAULT_EMOJIS[number] self.option = line if name_to_emoji["wastebasket"] == self.emoji: From 93e9e21456793daf8f74f1f871b430c8b66fa43c Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:38:23 +0200 Subject: [PATCH 07/95] Improved PollOption-class --- general/polls/cog.py | 59 ++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 814d5e63b..70328f0a0 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -4,7 +4,7 @@ from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji from discord.ext import commands -from discord.ext.commands import CommandError, Context, guild_only +from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only from discord.utils import utcnow from PyDrocsid.cog import Cog @@ -28,6 +28,32 @@ DEFAULT_EMOJIS = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase] +class PollOption: + emoji: str = None + option: str = None + + async def init(self, ctx: Context, line: str, number: int): + if not line: + raise CommandError(t.empty_option) + + emoji_candidate, *option = line.split() + option = " ".join(option) + try: + self.emoji = str(await EmojiConverter().convert(ctx, emoji_candidate)) + except EmojiNotFound: + if (unicode_emoji := emoji_candidate) in emoji_to_name: + self.emoji = unicode_emoji + else: + self.emoji = DEFAULT_EMOJIS[number] + option = f"{emoji_candidate} {option}" + self.option = option + + return self + + def __str__(self): + return f"{self.emoji} {self.option}" if self.option else self.emoji + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): @@ -221,34 +247,3 @@ async def team_yesno(self, ctx: Context, *, text: str): await message.add_reaction(name_to_emoji["-1"]) except Forbidden: raise CommandError(t.could_not_add_reactions(message.channel.mention)) - - -class PollOption: - def __init__(self, ctx: Context, line: str, number: int): - if not line: - raise CommandError(t.empty_option) - - emoji_candidate, *text = line.lstrip().split(" ") - text = " ".join(text) - - custom_emoji_match = re.fullmatch(r"", emoji_candidate) - if custom_emoji := ctx.bot.get_emoji(int(custom_emoji_match.group(1))) if custom_emoji_match else None: - self.emoji = custom_emoji - self.option = text.strip() - elif (unicode_emoji := emoji_candidate) in emoji_to_name: - self.emoji = unicode_emoji - self.option = text.strip() - elif (match := re.match(r"^:([^: ]+):$", emoji_candidate)) and ( - unicode_emoji := name_to_emoji.get(match.group(1).replace(":", "")) - ): - self.emoji = unicode_emoji - self.option = text.strip() - else: - self.emoji = DEFAULT_EMOJIS[number] - self.option = line - - if name_to_emoji["wastebasket"] == self.emoji: - raise CommandError(t.can_not_use_wastebucket_as_option) - - def __str__(self): - return f"{self.emoji} {self.option}" if self.option else self.emoji From 807c0675725c5976f38d544522d082b41529e3c7 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:39:37 +0200 Subject: [PATCH 08/95] Added class for creating select-views --- general/polls/cog.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 70328f0a0..1cbcce1b1 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -5,6 +5,7 @@ from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji from discord.ext import commands from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only +from discord.ui import Select, View from discord.utils import utcnow from PyDrocsid.cog import Cog @@ -54,6 +55,14 @@ def __str__(self): return f"{self.emoji} {self.option}" if self.option else self.emoji +def create_select_view(select_obj: Select, timeout: float = None) -> View: + """returns a view object""" + view = View(timeout=timeout) + view.add_item(select_obj) + + return view + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From 5f5855869e1b4fe2347e3735633a89ef95c90b66 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:40:39 +0200 Subject: [PATCH 09/95] Removed re-import + add function for calculating percentages on polls --- general/polls/cog.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 1cbcce1b1..c5a9122a2 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -1,4 +1,3 @@ -import re import string from typing import Optional, Tuple @@ -17,6 +16,7 @@ from PyDrocsid.util import check_wastebasket, is_teamler from .colors import Colors +from .models import Poll from .permissions import PollsPermission from ...contributor import Contributor @@ -63,6 +63,13 @@ def create_select_view(select_obj: Select, timeout: float = None) -> View: return view +def get_percentage(poll: Poll) -> list[tuple[float, float]]: + """returns the amount of votes and the percentage of an option""" + values: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options] + + return [(float(value), float(round(((value / sum(values)) * 100), 2))) for value in values] + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From bd623c640b80863ba7dde946325c6fd4bb5f355a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:41:31 +0200 Subject: [PATCH 10/95] Added function to send wizzard embed for helping to setup polls --- general/polls/cog.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index c5a9122a2..36ff6300e 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -70,6 +70,19 @@ def get_percentage(poll: Poll) -> list[tuple[float, float]]: return [(float(value), float(round(((value / sum(values)) * 100), 2))) for value in values] +def build_wizard(skip: bool = False) -> Embed: + """creates a help embed for setting up advanced polls""" + if skip: + return Embed(title=t.skip.title, description=t.skip.description, color=Colors.Polls) + + embed = Embed(title=t.wizard.title, description=t.wizard.description, color=Colors.Polls) + embed.add_field(name=t.wizard.arg, value=t.wizard.args, inline=False) + embed.add_field(name=t.wizard.example.name, value=t.wizard.example.value, inline=False) + embed.add_field(name=t.wizard.skip.name, value=t.wizard.skip.value, inline=False) + + return embed + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From d708bf1e4c4d6d491835a99f7a3432d4e5905180 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:42:58 +0200 Subject: [PATCH 11/95] Added function to get a parser for parsing args from messages --- general/polls/cog.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 36ff6300e..a7b913104 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -1,4 +1,5 @@ import string +from argparse import ArgumentParser from typing import Optional, Tuple from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji @@ -16,8 +17,9 @@ from PyDrocsid.util import check_wastebasket, is_teamler from .colors import Colors -from .models import Poll +from .models import Poll, PollType from .permissions import PollsPermission +from .settings import PollsDefaultSettings from ...contributor import Contributor @@ -83,6 +85,28 @@ def build_wizard(skip: bool = False) -> Embed: return embed +async def get_parser() -> ArgumentParser: + """creates a parser object with options for advanced polls""" + parser = ArgumentParser() + parser.add_argument( + "--type", + "-T", + default=PollType.STANDARD.value, + choices=[PollType.STANDARD.value, PollType.TEAM.value], + type=str, + ) + parser.add_argument("--deadline", "-D", default=await PollsDefaultSettings.duration.get(), type=int) + parser.add_argument( + "--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool, choices=[True, False] + ) + parser.add_argument( + "--choices", "-C", default=await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS, type=int + ) + parser.add_argument("--fair", "-F", default=await PollsDefaultSettings.fair.get(), type=bool, choices=[True, False]) + + return parser + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From e1bb5bd848f1786d39ae575c269123d73a363b97 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:43:34 +0200 Subject: [PATCH 12/95] Added function to calc endtime for polls --- general/polls/cog.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index a7b913104..873542963 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -1,7 +1,9 @@ import string from argparse import ArgumentParser +from datetime import datetime from typing import Optional, Tuple +from dateutil.relativedelta import relativedelta from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji from discord.ext import commands from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only @@ -107,6 +109,11 @@ async def get_parser() -> ArgumentParser: return parser +def calc_end_time(duration: Optional[float]) -> Optional[datetime]: + """returns the time when a poll should be closed""" + return utcnow() + relativedelta(hours=int(duration)) if duration else None + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From 8528cb9a4365f787b197dcd3607228dfe4e8d3e2 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:47:00 +0200 Subject: [PATCH 13/95] Added function to handle deleted poll messages --- general/polls/cog.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 873542963..eb97c9cfc 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -4,12 +4,13 @@ from typing import Optional, Tuple from dateutil.relativedelta import relativedelta -from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji +from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound from discord.ext import commands from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only from discord.ui import Select, View from discord.utils import utcnow +from PyDrocsid.database import db from PyDrocsid.cog import Cog from PyDrocsid.embeds import EmbedLimits from PyDrocsid.emojis import emoji_to_name, name_to_emoji @@ -114,6 +115,29 @@ def calc_end_time(duration: Optional[float]) -> Optional[datetime]: return utcnow() + relativedelta(hours=int(duration)) if duration else None +async def handle_deleted_messages(bot, message_id: int): + """if a message containing a poll gets deleted, this function deletes the interaction message (both direction)""" + deleted_embed: Poll | None = await db.get(Poll, message_id=message_id) + deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message_id) + + if not deleted_embed and not deleted_interaction: + return + + poll = deleted_embed or deleted_interaction + channel = await bot.fetch_channel(poll.channel_id) + try: + if deleted_interaction: + msg: Message | None = await channel.fetch_message(poll.message_id) + else: + msg: Message | None = await channel.fetch_message(poll.interaction_message_id) + except NotFound: + msg = None + + if msg: + await poll.remove() + await msg.delete() + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From 7042eef45ba4a8ea8c6b772eeb78bc35540d12ba Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:47:50 +0200 Subject: [PATCH 14/95] Added function to check if a poll is still open --- general/polls/cog.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index eb97c9cfc..1bc33c22b 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -138,6 +138,18 @@ async def handle_deleted_messages(bot, message_id: int): await msg.delete() +async def check_poll_time(poll: Poll) -> bool: + """checks if a poll has ended""" + if not poll.end_time: + await poll.remove() + return False + + elif poll.end_time < utcnow(): + return False + + return True + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From 24eaa17cd6d9b0aa0b55b82139914945795bc6d4 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:48:27 +0200 Subject: [PATCH 15/95] Added function to close polls and handle messages relating to polls --- general/polls/cog.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 1bc33c22b..385db80a6 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -150,6 +150,26 @@ async def check_poll_time(poll: Poll) -> bool: return True +async def close_poll(bot, poll: Poll): + """deletes the interaction message and edits the footer of the poll embed""" + try: + channel = await bot.fetch_channel(poll.channel_id) + embed_message = await channel.fetch_message(poll.message_id) + interaction_message = await channel.fetch_message(poll.interaction_message_id) + except NotFound: + poll.active = False + return + + await interaction_message.delete() + embed = embed_message.embeds[0] + embed.set_footer(text=t.footer_closed) + + await embed_message.edit(embed=embed) + await embed_message.unpin() + + poll.active = False + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From d577756f281d37496e2b94a3556201dae0323732 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:49:49 +0200 Subject: [PATCH 16/95] Added function to get a list of all team-members --- general/polls/cog.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 385db80a6..4237052c9 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -170,6 +170,21 @@ async def close_poll(bot, poll: Poll): poll.active = False +async def get_staff(guild: Guild, team_roles: list[str]) -> set[Member]: + """gets a list of all team members""" + teamlers: set[Member] = set() + for role_name in team_roles: + if not (team_role := guild.get_role(await RoleSettings.get(role_name))): + continue + + teamlers.update(member for member in team_role.members if not member.bot) + + if not teamlers: + raise CommandError(t.error.no_teamlers) + + return teamlers + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From ae44190f2d717ac1c1732b4a51baa0cd58604791 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:50:34 +0200 Subject: [PATCH 17/95] Added function edit polls --- general/polls/cog.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 4237052c9..5e9d71cb2 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -185,6 +185,27 @@ async def get_staff(guild: Guild, team_roles: list[str]) -> set[Member]: return teamlers +async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed: + """edits the poll embed, updating the votes and percentages""" + calc = get_percentage(poll) + for index, field in enumerate(embed.fields): + if field.name == tg.status: + missing.sort(key=lambda m: str(m).lower()) + *teamlers, last = (x.mention for x in missing) + teamlers: list[str] + embed.set_field_at( + index, + name=field.name, + value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1), + ) + else: + weight: float | int = calc[index][0] if not calc[index][0].is_integer() else int(calc[index][0]) + percentage: float | int = calc[index][1] if not calc[index][1].is_integer() else int(calc[index][1]) + embed.set_field_at(index, name=t.option.field.name(weight, percentage), value=field.value, inline=False) + + return embed + + async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: for embed in message.embeds: for i, field in enumerate(embed.fields): From 6a8e889b6eec07b0b2d53b484e469696921d0c7a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:52:05 +0200 Subject: [PATCH 18/95] Added MySelect-object to create custom callbacks for polls --- general/polls/cog.py | 64 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 5e9d71cb2..c8af80420 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -10,7 +10,7 @@ from discord.ui import Select, View from discord.utils import utcnow -from PyDrocsid.database import db +from PyDrocsid.database import db, db_wrapper from PyDrocsid.cog import Cog from PyDrocsid.embeds import EmbedLimits from PyDrocsid.emojis import emoji_to_name, name_to_emoji @@ -20,7 +20,7 @@ from PyDrocsid.util import check_wastebasket, is_teamler from .colors import Colors -from .models import Poll, PollType +from .models import Poll, PollType, RoleWeight, PollVote, Option from .permissions import PollsPermission from .settings import PollsDefaultSettings from ...contributor import Contributor @@ -254,6 +254,66 @@ async def send_poll( raise CommandError(t.could_not_add_reactions(ctx.channel.mention)) +class MySelect(Select): + """adds a method for handling interactions with the select menu""" + + @db_wrapper + async def callback(self, interaction): + user = interaction.user + selected_options: list = self.values + message: Message = await interaction.channel.fetch_message(interaction.custom_id) + embed: Embed = message.embeds[0] if message.embeds else None + poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + + if not poll or not embed: + return + + new_options: list[Option] = [option for option in poll.options if option.option in selected_options] + missing: list[Member] | None = None + + opt: Option + for opt in poll.options: + for vote in opt.votes: + if vote.user_id == user.id: + await vote.remove() + opt.votes.remove(vote) + + ev_pover = await PollsDefaultSettings.everyone_power.get() + if poll.fair: + user_weight: float = ev_pover + else: + highest_role = await RoleWeight.get_highest(user.roles) or 0 + user_weight: float = ev_pover if highest_role < ev_pover else highest_role + + for option in new_options: + option.votes.append( + await PollVote.create(option_id=option.id, user_id=user.id, poll_id=poll.id, vote_weight=user_weight) + ) + + if poll.poll_type == PollType.TEAM: + try: + teamlers: set[Member] = await get_staff(interaction.guild, ["team"]) + except CommandError: + await interaction.response.send_message(content=t.error.no_teamlers, ephemeral=True) + return + if user not in teamlers: + await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True) + return + + user_ids: set[int] = set() + for option in poll.options: + for vote in option.votes: + user_ids.add(vote.user_id) + + missing: list[Member] | None = [teamler for teamler in teamlers if teamler.id not in user_ids] + missing.sort(key=lambda m: str(m).lower()) + + embed = await edit_poll_embed(embed, poll, missing) + await message.edit(embed=embed) + await interaction.response.send_message(content=t.poll_voted, ephemeral=True) + + + class PollsCog(Cog, name="Polls"): CONTRIBUTORS = [Contributor.MaxiHuHe04, Contributor.Defelo, Contributor.TNT2k, Contributor.wolflu] From 7e5d09fdf0fb4240b69c5a63ae34108ab911c15c Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:53:24 +0200 Subject: [PATCH 19/95] Added function to send polls --- general/polls/cog.py | 76 ++++++++++++++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index c8af80420..c22fd9b8b 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -4,7 +4,7 @@ from typing import Optional, Tuple from dateutil.relativedelta import relativedelta -from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound +from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException from discord.ext import commands from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only from discord.ui import Select, View @@ -24,7 +24,7 @@ from .permissions import PollsPermission from .settings import PollsDefaultSettings from ...contributor import Contributor - +from ...pubsub import send_alert tg = t.g t = t.polls @@ -215,43 +215,80 @@ async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optiona async def send_poll( - ctx: Context, title: str, args: str, field: Optional[Tuple[str, str]] = None, allow_delete: bool = True -): - question, *options = [line.replace("\x00", "\n") for line in args.replace("\\\n", "\x00").split("\n") if line] + ctx: Context, + title: str, + poll_args: str, + max_choices: int = None, + field: Optional[tuple[str, str]] = None, + deadline: Optional[int] = None, +) -> tuple[Message, Message, list[tuple[str, str]], str]: + """sends a poll embed + view message containing the select field""" + + if not max_choices: + max_choices = t.poll_config.choices.unlimited + + question, *options = [line.replace("\x00", "\n") for line in poll_args.replace("\\\n", "\x00").split("\n") if line] if not options: raise CommandError(t.missing_options) - if len(options) > MAX_OPTIONS - allow_delete: - raise CommandError(t.too_many_options(MAX_OPTIONS - allow_delete)) + if len(options) > MAX_OPTIONS: + raise CommandError(t.too_many_options(MAX_OPTIONS)) + if field and len(options) >= MAX_OPTIONS: + raise CommandError(t.too_many_options(MAX_OPTIONS - 1)) - options = [PollOption(ctx, line, i) for i, line in enumerate(options)] + options = [await PollOption().init(ctx, line, i) for i, line in enumerate(options)] if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options): raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE)) embed = Embed(title=title, description=question, color=Colors.Polls, timestamp=utcnow()) embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url) - if allow_delete: - embed.set_footer(text=t.created_by(ctx.author, ctx.author.id), icon_url=ctx.author.display_avatar.url) - if len({x.emoji for x in options}) < len(options): + if deadline: + end_time = calc_end_time(deadline) + embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M"))) + + if len({option.emoji for option in options}) < len(options): raise CommandError(t.option_duplicated) for option in options: - embed.add_field(name="** **", value=str(option), inline=False) + embed.add_field(name=t.option.field.name(0, 0), value=str(option), inline=False) if field: embed.add_field(name=field[0], value=field[1], inline=False) - poll: Message = await ctx.send(embed=embed) + if not max_choices or isinstance(max_choices, str): + place = t.select.place + max_value = len(options) + else: + options_amount = len(options) if max_choices >= len(options) else max_choices + place: str = t.select.placeholder(cnt=options_amount) + max_value = options_amount + + msg = await ctx.send(embed=embed) + select_obj = MySelect( + custom_id=str(msg.id), + placeholder=place, + max_values=max_value, + options=[ + SelectOption(label=t.select.label(index + 1), emoji=option.emoji, description=option.option) + for index, option in enumerate(options) + ], + ) + view_msg = await ctx.send(view=create_select_view(select_obj=select_obj)) + + parsed_options: list[tuple[str, str]] = [(obj.emoji, t.select.label(ix)) for ix, obj in enumerate(options, start=1)] try: - for option in options: - await poll.add_reaction(option.emoji) - if allow_delete: - await poll.add_reaction(name_to_emoji["wastebasket"]) - except Forbidden: - raise CommandError(t.could_not_add_reactions(ctx.channel.mention)) + await msg.pin() + except HTTPException: + embed = Embed( + title=t.error.cant_pin.title, + description=t.error.cant_pin.description(ctx.channel.mention), + color=Colors.error, + ) + await send_alert(ctx.guild, embed) + return msg, view_msg, parsed_options, question class MySelect(Select): @@ -313,7 +350,6 @@ async def callback(self, interaction): await interaction.response.send_message(content=t.poll_voted, ephemeral=True) - class PollsCog(Cog, name="Polls"): CONTRIBUTORS = [Contributor.MaxiHuHe04, Contributor.Defelo, Contributor.TNT2k, Contributor.wolflu] From 6195a5b87f4478e970ed34b659af02676762cc0c Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:56:17 +0200 Subject: [PATCH 20/95] Removed function for getting team-poll-embeds --- general/polls/cog.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index c22fd9b8b..2400f6973 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -206,14 +206,6 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None return embed -async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]: - for embed in message.embeds: - for i, field in enumerate(embed.fields): - if tg.status == field.name: - return embed, i - return None, None - - async def send_poll( ctx: Context, title: str, From 9b90bdb76e82a8b8ac01e5cb39ac6a9dbd98f54f Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:57:06 +0200 Subject: [PATCH 21/95] Added Infinity into Contributor list --- general/polls/cog.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 2400f6973..bb4fa800c 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -343,7 +343,13 @@ async def callback(self, interaction): class PollsCog(Cog, name="Polls"): - CONTRIBUTORS = [Contributor.MaxiHuHe04, Contributor.Defelo, Contributor.TNT2k, Contributor.wolflu] + CONTRIBUTORS = [ + Contributor.MaxiHuHe04, + Contributor.Defelo, + Contributor.TNT2k, + Contributor.wolflu, + Contributor.Infinity, + ] def __init__(self, team_roles: list[str]): self.team_roles: list[str] = team_roles From 05b56a69d465e4812eea77216c5f48c4542a2c8e Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:58:10 +0200 Subject: [PATCH 22/95] Removed old functions for message reactions and old commands from Cog --- general/polls/cog.py | 140 ------------------------------------------- 1 file changed, 140 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index bb4fa800c..9126dd766 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -353,143 +353,3 @@ class PollsCog(Cog, name="Polls"): def __init__(self, team_roles: list[str]): self.team_roles: list[str] = team_roles - - async def get_reacted_teamlers(self, message: Optional[Message] = None) -> str: - guild: Guild = self.bot.guilds[0] - - teamlers: set[Member] = set() - for role_name in self.team_roles: - if not (team_role := guild.get_role(await RoleSettings.get(role_name))): - continue - - teamlers.update(member for member in team_role.members if not member.bot) - - if message: - for reaction in message.reactions: - if reaction.me: - teamlers.difference_update(await reaction.users().flatten()) - - teamlers: list[Member] = list(teamlers) - if not teamlers: - return t.teampoll_all_voted - - teamlers.sort(key=lambda m: str(m).lower()) - - *teamlers, last = (x.mention for x in teamlers) - teamlers: list[str] - return t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1) - - async def on_raw_reaction_add(self, message: Message, emoji: PartialEmoji, member: Member): - if member.bot or message.guild is None: - return - - if await check_wastebasket(message, member, emoji, t.created_by, PollsPermission.delete): - await message.delete() - raise StopEventHandling - - embed, index = await get_teampoll_embed(message) - if embed is None: - return - - if not await is_teamler(member): - try: - await message.remove_reaction(emoji, member) - except Forbidden: - pass - raise StopEventHandling - - for reaction in message.reactions: - if reaction.emoji == emoji.name: - break - else: - return - - if not reaction.me: - return - - value = await self.get_reacted_teamlers(message) - embed.set_field_at(index, name=tg.status, value=value, inline=False) - await message.edit(embed=embed) - - async def on_raw_reaction_remove(self, message: Message, _, member: Member): - if member.bot or message.guild is None: - return - embed, index = await get_teampoll_embed(message) - if embed is not None: - user_reacted = False - for reaction in message.reactions: - if reaction.me and member in await reaction.users().flatten(): - user_reacted = True - break - if not user_reacted and await is_teamler(member): - value = await self.get_reacted_teamlers(message) - embed.set_field_at(index, name=tg.status, value=value, inline=False) - await message.edit(embed=embed) - return - - @commands.command(usage=t.poll_usage, aliases=["vote"]) - @guild_only() - async def poll(self, ctx: Context, *, args: str): - """ - Starts a poll. Multiline options can be specified using a `\\` at the end of a line - """ - - await send_poll(ctx, t.poll, args) - - @commands.command(usage=t.poll_usage, aliases=["teamvote", "tp"]) - @PollsPermission.team_poll.check - @guild_only() - async def teampoll(self, ctx: Context, *, args: str): - """ - Starts a poll and shows, which teamler has not voted yet. - Multiline options can be specified using a `\\` at the end of a line - """ - - await send_poll( - ctx, t.team_poll, args, field=(tg.status, await self.get_reacted_teamlers()), allow_delete=False - ) - - @commands.command(aliases=["yn"]) - @guild_only() - async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Optional[str] = None): - """ - adds thumbsup and thumbsdown reactions to the message - """ - - if message is None or message.guild is None or text: - message = ctx.message - - if message.author != ctx.author and not await is_teamler(ctx.author): - raise CommandError(t.foreign_message) - - try: - await message.add_reaction(name_to_emoji["thumbsup"]) - await message.add_reaction(name_to_emoji["thumbsdown"]) - except Forbidden: - raise CommandError(t.could_not_add_reactions(message.channel.mention)) - - if message != ctx.message: - try: - await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) - except Forbidden: - pass - - @commands.command(aliases=["tyn"]) - @PollsPermission.team_poll.check - @guild_only() - async def team_yesno(self, ctx: Context, *, text: str): - """ - Starts a yes/no poll and shows, which teamler has not voted yet. - """ - - embed = Embed(title=t.team_poll, description=text, color=Colors.Polls, timestamp=utcnow()) - embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url) - - embed.add_field(name=tg.status, value=await self.get_reacted_teamlers(), inline=False) - - message: Message = await ctx.send(embed=embed) - try: - await message.add_reaction(name_to_emoji["+1"]) - await message.add_reaction(name_to_emoji["-1"]) - except Forbidden: - raise CommandError(t.could_not_add_reactions(message.channel.mention)) From fdaf86f7e585193c1506b11990d8a7479d37f8f6 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:59:08 +0200 Subject: [PATCH 23/95] Added startup-function to check if polls can be closed and reactivate running polls --- general/polls/cog.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 9126dd766..b5a79607a 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -10,7 +10,7 @@ from discord.ui import Select, View from discord.utils import utcnow -from PyDrocsid.database import db, db_wrapper +from PyDrocsid.database import db, db_wrapper, filter_by from PyDrocsid.cog import Cog from PyDrocsid.embeds import EmbedLimits from PyDrocsid.emojis import emoji_to_name, name_to_emoji @@ -20,7 +20,7 @@ from PyDrocsid.util import check_wastebasket, is_teamler from .colors import Colors -from .models import Poll, PollType, RoleWeight, PollVote, Option +from .models import Poll, PollType, RoleWeight, PollVote, Option, sync_redis from .permissions import PollsPermission from .settings import PollsDefaultSettings from ...contributor import Contributor @@ -353,3 +353,29 @@ class PollsCog(Cog, name="Polls"): def __init__(self, team_roles: list[str]): self.team_roles: list[str] = team_roles + + async def on_ready(self): + await sync_redis() + polls: list[Poll] = await db.all(filter_by(Poll, (Poll.options, Option.votes), active=True)) + for poll in polls: + if await check_poll_time(poll): + select_obj = MySelect( + custom_id=str(poll.message_id), + placeholder=t.select.placeholder(cnt=poll.max_choices), + max_values=poll.max_choices, + options=[ + SelectOption( + label=t.select.label(option.field_position + 1), + emoji=option.emote, + description=option.option, + ) + for option in poll.options + ], + ) + + self.bot.add_view(view=create_select_view(select_obj), message_id=poll.interaction_message_id) + + try: + self.poll_loop.start() + except RuntimeError: + self.poll_loop.restart() From d2b88acd913c5c32ce1e2ec8af1a449d01e4ba58 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:59:53 +0200 Subject: [PATCH 24/95] Added functions to handle deleted or edited messages containing polls --- general/polls/cog.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index b5a79607a..607916f71 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -4,7 +4,7 @@ from typing import Optional, Tuple from dateutil.relativedelta import relativedelta -from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException +from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException, RawMessageDeleteEvent from discord.ext import commands from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only from discord.ui import Select, View @@ -379,3 +379,9 @@ async def on_ready(self): self.poll_loop.start() except RuntimeError: self.poll_loop.restart() + + async def on_message_delete(self, message: Message): + await handle_deleted_messages(self.bot, message.id) + + async def on_raw_message_delete(self, event: RawMessageDeleteEvent): + await handle_deleted_messages(self.bot, event.message_id) From d5c05b875d20b7bf7bddb4e1244a735b6d244bfe Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:00:26 +0200 Subject: [PATCH 25/95] Added loop to check if a poll can be closed --- general/polls/cog.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 607916f71..f7f5f1863 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -5,7 +5,7 @@ from dateutil.relativedelta import relativedelta from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException, RawMessageDeleteEvent -from discord.ext import commands +from discord.ext import commands, tasks from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only from discord.ui import Select, View from discord.utils import utcnow @@ -385,3 +385,12 @@ async def on_message_delete(self, message: Message): async def on_raw_message_delete(self, event: RawMessageDeleteEvent): await handle_deleted_messages(self.bot, event.message_id) + + @tasks.loop(minutes=1) + @db_wrapper + async def poll_loop(self): + polls: list[Poll] = await db.all(filter_by(Poll, active=True)) + + for poll in polls: + if not await check_poll_time(poll): + await close_poll(self.bot, poll) From b6a83dca25ecfca46efa9109f23f7d2ea5f82ebe Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:02:12 +0200 Subject: [PATCH 26/95] Added poll-group-command + fixed indentation --- general/polls/cog.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index f7f5f1863..651c87aa7 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -3,10 +3,11 @@ from datetime import datetime from typing import Optional, Tuple +from PyDrocsid.command import docs from dateutil.relativedelta import relativedelta from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException, RawMessageDeleteEvent from discord.ext import commands, tasks -from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only +from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only, UserInputError from discord.ui import Select, View from discord.utils import utcnow @@ -380,17 +381,24 @@ async def on_ready(self): except RuntimeError: self.poll_loop.restart() - async def on_message_delete(self, message: Message): - await handle_deleted_messages(self.bot, message.id) + async def on_message_delete(self, message: Message): + await handle_deleted_messages(self.bot, message.id) - async def on_raw_message_delete(self, event: RawMessageDeleteEvent): - await handle_deleted_messages(self.bot, event.message_id) + async def on_raw_message_delete(self, event: RawMessageDeleteEvent): + await handle_deleted_messages(self.bot, event.message_id) - @tasks.loop(minutes=1) - @db_wrapper - async def poll_loop(self): - polls: list[Poll] = await db.all(filter_by(Poll, active=True)) + @tasks.loop(minutes=1) + @db_wrapper + async def poll_loop(self): + polls: list[Poll] = await db.all(filter_by(Poll, active=True)) - for poll in polls: - if not await check_poll_time(poll): - await close_poll(self.bot, poll) + for poll in polls: + if not await check_poll_time(poll): + await close_poll(self.bot, poll) + + @commands.group(name="poll", aliases=["vote"]) + @guild_only() + @docs(t.commands.poll.poll) + async def poll(self, ctx: Context): + if not ctx.subcommand_passed: + raise UserInputError From c620f5426292e817ba435a056aca68cfe1e6fb88 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:02:49 +0200 Subject: [PATCH 27/95] Added command to show all active polls --- general/polls/cog.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 651c87aa7..702b5460a 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -9,11 +9,11 @@ from discord.ext import commands, tasks from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only, UserInputError from discord.ui import Select, View -from discord.utils import utcnow +from discord.utils import utcnow, format_dt from PyDrocsid.database import db, db_wrapper, filter_by from PyDrocsid.cog import Cog -from PyDrocsid.embeds import EmbedLimits +from PyDrocsid.embeds import EmbedLimits, send_long_embed from PyDrocsid.emojis import emoji_to_name, name_to_emoji from PyDrocsid.events import StopEventHandling from PyDrocsid.settings import RoleSettings @@ -402,3 +402,28 @@ async def poll_loop(self): async def poll(self, ctx: Context): if not ctx.subcommand_passed: raise UserInputError + + @poll.command(name="list", aliases=["l"]) + @guild_only() + @docs(t.commands.poll.list) + async def poll_list(self, ctx: Context): + polls: list[Poll] = await db.all(filter_by(Poll, active=True, guild_id=ctx.guild.id)) + description = "" + for poll in polls: + if poll.poll_type == PollType.TEAM and not await PollsPermission.team_poll.check_permissions(ctx.author): + continue + if poll.poll_type == PollType.TEAM: + description += t.polls.team_row( + poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R") + ) + else: + description += t.polls.row( + poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R") + ) + + if polls and description: + embed: Embed = Embed(title=t.polls.title, description=description, color=Colors.Polls) + await send_long_embed(ctx, embed=embed, paginate=True) + + else: + await send_long_embed(ctx, embed=Embed(title=t.no_polls, color=Colors.error)) From 91e7db5a43996d975fa47988612841a855f5f5e8 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:03:23 +0200 Subject: [PATCH 28/95] Added command to delete polls --- general/polls/cog.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 702b5460a..5bee08ce5 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Optional, Tuple -from PyDrocsid.command import docs +from PyDrocsid.command import docs, add_reactions, Confirmation from dateutil.relativedelta import relativedelta from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException, RawMessageDeleteEvent from discord.ext import commands, tasks @@ -427,3 +427,31 @@ async def poll_list(self, ctx: Context): else: await send_long_embed(ctx, embed=Embed(title=t.no_polls, color=Colors.error)) + + @poll.command(name="delete", aliases=["del"]) + @docs(t.commands.poll.delete) + async def delete(self, ctx: Context, message: Message): + poll: Poll = await db.get(Poll, message_id=message.id) + if not poll: + raise CommandError(t.error.not_poll) + if ( + poll.can_delete + and not await PollsPermission.delete.check_permissions(ctx.author) + and not poll.owner_id == ctx.author.id + ): + raise PermissionError + elif not poll.can_delete and not poll.owner_id == ctx.author.id: + raise PermissionError # if delete is False, only the owner can delete it + + if not await Confirmation().run(ctx, t.delete.confirm_text): + return + + await message.delete() + await poll.remove() + try: + interaction_message: Message = await ctx.channel.fetch_message(poll.interaction_message_id) + await interaction_message.delete() + except NotFound: + pass + + await add_reactions(ctx.message, "white_check_mark") From b438bf3f15a619e55777f7153eaa0fc5081fdce2 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:03:51 +0200 Subject: [PATCH 29/95] Added command to see who voted on a poll --- general/polls/cog.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 5bee08ce5..d3d5a80fa 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -455,3 +455,32 @@ async def delete(self, ctx: Context, message: Message): pass await add_reactions(ctx.message, "white_check_mark") + + @poll.command(name="voted", aliases=["v"]) + @docs(t.commands.poll.voted) + async def voted(self, ctx: Context, message: Message): + poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + author = ctx.author + if not poll: + raise CommandError(t.error.not_poll) + if ( + poll.anonymous + and not await PollsPermission.anonymous_bypass.check_permissions(author) + and not poll.owner_id == author.id + ): + raise PermissionError + + users = {} + for option in poll.options: + for vote in option.votes: + if not users.get(str(vote.user_id)): + users[str(vote.user_id)] = [option.field_position + 1] + else: + users[str(vote.user_id)].append(option.field_position + 1) + + description = "" + for key, value in users.items(): + description += t.voted.row(key, value) + embed = Embed(title=t.voted.title, description=description, color=Colors.Polls) + + await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True) From 3615dc334e560c35cf4667af625aec98b1a0408a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:04:19 +0200 Subject: [PATCH 30/95] Added command-group for settings --- general/polls/cog.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index d3d5a80fa..196b09222 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -484,3 +484,44 @@ async def voted(self, ctx: Context, message: Message): embed = Embed(title=t.voted.title, description=description, color=Colors.Polls) await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True) + + @poll.group(name="settings", aliases=["s"]) + @PollsPermission.read.check + @docs(t.commands.poll.settings.settings) + async def settings(self, ctx: Context): + if ctx.subcommand_passed is not None: + if ctx.invoked_subcommand is None: + raise UserInputError + return + + embed = Embed(title=t.poll_config.title, color=Colors.Polls) + time: int = await PollsDefaultSettings.duration.get() + max_time: int = await PollsDefaultSettings.max_duration.get() + embed.add_field( + name=t.poll_config.duration.name, + value=t.poll_config.duration.time(cnt=time) + if not time <= 0 + else t.poll_config.duration.time(cnt=max_time * 24), + inline=False, + ) + embed.add_field( + name=t.poll_config.max_duration.name, value=t.poll_config.max_duration.time(cnt=max_time), inline=False + ) + choice: int = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS + embed.add_field( + name=t.poll_config.choices.name, + value=t.poll_config.choices.amount(cnt=choice) if not choice <= 0 else t.poll_config.choices.unlimited, + inline=False, + ) + anonymous: bool = await PollsDefaultSettings.anonymous.get() + embed.add_field(name=t.poll_config.anonymous.name, value=str(anonymous), inline=False) + fair: bool = await PollsDefaultSettings.fair.get() + embed.add_field(name=t.poll_config.fair.name, value=str(fair), inline=False) + roles = await RoleWeight.get(ctx.guild.id) + everyone: int = await PollsDefaultSettings.everyone_power.get() + base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone) + if roles: + base += "".join(t.poll_config.roles.row(role.role_id, role.weight) for role in roles) + embed.add_field(name=t.poll_config.roles.name, value=base, inline=False) + + await send_long_embed(ctx, embed, paginate=False) From 0b8594f29aba758d13050c0dacf8e501854598e0 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:04:59 +0200 Subject: [PATCH 31/95] Added settings command to change the weight for certain roles --- general/polls/cog.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 196b09222..a43dd4fbb 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -25,7 +25,7 @@ from .permissions import PollsPermission from .settings import PollsDefaultSettings from ...contributor import Contributor -from ...pubsub import send_alert +from ...pubsub import send_alert, send_to_changelog tg = t.g t = t.polls @@ -525,3 +525,28 @@ async def settings(self, ctx: Context): embed.add_field(name=t.poll_config.roles.name, value=base, inline=False) await send_long_embed(ctx, embed, paginate=False) + + @settings.command(name="roles_weights", aliases=["rw"]) + @PollsPermission.write.check + @docs(t.commands.poll.settings.roles_weights) + async def roles_weights(self, ctx: Context, role: Role, weight: float | None = None): + element = await db.get(RoleWeight, role_id=role.id) + + if not weight and not element: + raise CommandError(t.error.cant_set_weight) + + if weight and weight < 0.1: + raise CommandError(t.error.weight_too_small) + + if element and weight: + element.weight = weight + msg: str = t.role_weight.set(role.id, weight) + elif weight and not element: + await RoleWeight.create(ctx.guild.id, role.id, weight) + msg: str = t.role_weight.set(role.id, weight) + else: + await element.remove() + msg: str = t.role_weight.reset(role.id) + + await add_reactions(ctx.message, "white_check_mark") + await send_to_changelog(ctx.guild, msg) From af76860f292100c6e992d79008c3c2339c251c89 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:05:20 +0200 Subject: [PATCH 32/95] Added settings command to change the default duration for polls --- general/polls/cog.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index a43dd4fbb..d4c0e32e9 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -550,3 +550,17 @@ async def roles_weights(self, ctx: Context, role: Role, weight: float | None = N await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) + + @settings.command(name="duration", aliases=["d"]) + @PollsPermission.write.check + @docs(t.commands.poll.settings.duration) + async def duration(self, ctx: Context, hours: int | None = None): + if not hours: + hours = 0 + msg: str = t.duration.reset() + else: + msg: str = t.duration.set(cnt=hours) + + await PollsDefaultSettings.duration.set(hours) + await add_reactions(ctx.message, "white_check_mark") + await send_to_changelog(ctx.guild, msg) From d89a4acedebcb6c076bc56fc63e3571f07223952 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:05:39 +0200 Subject: [PATCH 33/95] Added settings command to change the maximum duration for polls --- general/polls/cog.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index d4c0e32e9..b3646ca75 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -564,3 +564,14 @@ async def duration(self, ctx: Context, hours: int | None = None): await PollsDefaultSettings.duration.set(hours) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) + + @settings.command(name="max_duration", aliases=["md"]) + @PollsPermission.write.check + @docs(t.commands.poll.settings.max_duration) + async def max_duration(self, ctx: Context, days: int | None = None): + days = days or 7 + msg: str = t.max_duration.set(cnt=days) + + await PollsDefaultSettings.max_duration.set(days) + await add_reactions(ctx.message, "white_check_mark") + await send_to_changelog(ctx.guild, msg) From 9b18c10887593d7ad647553af2422bc1046acbfe Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:06:03 +0200 Subject: [PATCH 34/95] Added settings command to change the maximum amount of votes per user on default polls --- general/polls/cog.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index b3646ca75..6a43366ee 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -575,3 +575,14 @@ async def max_duration(self, ctx: Context, days: int | None = None): await PollsDefaultSettings.max_duration.set(days) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) + + @settings.command(name="max_duration", aliases=["md"]) + @PollsPermission.write.check + @docs(t.commands.poll.settings.max_duration) + async def max_duration(self, ctx: Context, days: int | None = None): + days = days or 7 + msg: str = t.max_duration.set(cnt=days) + + await PollsDefaultSettings.max_duration.set(days) + await add_reactions(ctx.message, "white_check_mark") + await send_to_changelog(ctx.guild, msg) From cb99896f1d6461709966a61b79c8a503b4a927f5 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:08:22 +0200 Subject: [PATCH 35/95] Added settings command to change the maximum amount of votes per user on default polls --- general/polls/cog.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 6a43366ee..a6fe4f718 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -5,7 +5,8 @@ from PyDrocsid.command import docs, add_reactions, Confirmation from dateutil.relativedelta import relativedelta -from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException, RawMessageDeleteEvent +from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException, RawMessageDeleteEvent, \ + Role from discord.ext import commands, tasks from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only, UserInputError from discord.ui import Select, View @@ -576,13 +577,19 @@ async def max_duration(self, ctx: Context, days: int | None = None): await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) - @settings.command(name="max_duration", aliases=["md"]) + @settings.command(name="votes", aliases=["v", "choices", "c"]) @PollsPermission.write.check - @docs(t.commands.poll.settings.max_duration) - async def max_duration(self, ctx: Context, days: int | None = None): - days = days or 7 - msg: str = t.max_duration.set(cnt=days) + @docs(t.commands.poll.settings.votes) + async def votes(self, ctx: Context, votes: int | None = None): + if not votes: + votes = 0 + msg: str = t.votes.reset + else: + msg: str = t.votes.set(cnt=votes) - await PollsDefaultSettings.max_duration.set(days) + if not 0 < votes < MAX_OPTIONS: + votes = 0 + + await PollsDefaultSettings.max_choices.set(votes) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) From 26c6230b73e8f06d2cf1e34131d40566bce7bf54 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:08:48 +0200 Subject: [PATCH 36/95] Added settings command to change if default polls should be anonymous or not --- general/polls/cog.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index a6fe4f718..50f91b541 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -593,3 +593,13 @@ async def votes(self, ctx: Context, votes: int | None = None): await PollsDefaultSettings.max_choices.set(votes) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) + + @settings.command(name="anonymous", aliases=["a"]) + @PollsPermission.write.check + @docs(t.commands.poll.settings.anonymous) + async def anonymous(self, ctx: Context, status: bool): + msg: str = t.anonymous.is_on if status else t.anonymous.is_off + + await PollsDefaultSettings.anonymous.set(status) + await add_reactions(ctx.message, "white_check_mark") + await send_to_changelog(ctx.guild, msg) From 4bac601efd9c187b81b30dcb399cf8d9be0da35c Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:09:11 +0200 Subject: [PATCH 37/95] Added settings command to change if default polls should be fair or not --- general/polls/cog.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 50f91b541..787a9a1f3 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -603,3 +603,13 @@ async def anonymous(self, ctx: Context, status: bool): await PollsDefaultSettings.anonymous.set(status) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) + + @settings.command(name="fair", aliases=["f"]) + @PollsPermission.write.check + @docs(t.commands.poll.settings.fair) + async def fair(self, ctx: Context, status: bool): + msg: str = t.fair.is_on if status else t.fair.is_off + + await PollsDefaultSettings.fair.set(status) + await add_reactions(ctx.message, "white_check_mark") + await send_to_changelog(ctx.guild, msg) From 270cfeb07145993ba2881a5f81d67a75ccc2aefb Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:09:41 +0200 Subject: [PATCH 38/95] Added command to create quick polls with default settings --- general/polls/cog.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 787a9a1f3..5778767bd 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -613,3 +613,32 @@ async def fair(self, ctx: Context, status: bool): await PollsDefaultSettings.fair.set(status) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) + + @poll.command(name="quick", usage=t.usage.poll, aliases=["q"]) + @docs(t.commands.poll.quick) + async def quick(self, ctx: Context, *, args: str): + deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24 + max_choices = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS + anonymous = await PollsDefaultSettings.anonymous.get() + message, interaction, parsed_options, question = await send_poll( + ctx=ctx, title=t.poll, poll_args=args, max_choices=max_choices, deadline=deadline + ) + + await Poll.create( + message_id=message.id, + message_url=message.jump_url, + guild_id=ctx.guild.id, + channel=message.channel.id, + owner=ctx.author.id, + title=question, + end=calc_end_time(deadline), + anonymous=anonymous, + can_delete=True, + options=parsed_options, + poll_type=PollType.STANDARD, + interaction=interaction.id, + fair=await PollsDefaultSettings.fair.get(), + max_choices=max_choices, + ) + + await ctx.message.delete() From 0f2567e925cc29e4f51b5ec4ec392c5296625e2a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:10:16 +0200 Subject: [PATCH 39/95] Added command to create new polls with more advanced settings --- general/polls/cog.py | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 5778767bd..2b1871f82 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -642,3 +642,68 @@ async def quick(self, ctx: Context, *, args: str): ) await ctx.message.delete() + + @poll.command(name="new", usage=t.usage.poll) + @docs(t.commands.poll.new) + async def new(self, ctx: Context, *, options: str): + wizard = await ctx.send(embed=build_wizard()) + mess: Message = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author, timeout=60.0) + args = mess.content + + if args.lower() == t.skip.message: + await wizard.edit(embed=build_wizard(True), delete_after=5.0) + else: + await wizard.delete(delay=5.0) + await mess.delete() + + parser = await get_parser() + parsed: Namespace = parser.parse_known_args(args.split())[0] + + title: str = t.poll + poll_type: Enum | str = parsed.type.lower() + if poll_type == PollType.TEAM.value and await PollsPermission.team_poll.check_permissions(ctx.author): + poll_type = PollType.TEAM + title: str = t.team_poll + else: + poll_type = PollType.STANDARD + max_deadline = await PollsDefaultSettings.max_duration.get() * 24 + deadline: Union[list[str, str], int] = parsed.deadline + if isinstance(deadline, int): + deadline = deadline or max_deadline if deadline <= max_deadline else max_deadline + else: + deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24 + anonymous: bool = parsed.anonymous + choices: int = parsed.choices + + if poll_type == PollType.TEAM: + can_delete, fair = False, True + missing = list(await get_staff(self.bot.guilds[0], ["team"])) + missing.sort(key=lambda m: str(m).lower()) + *teamlers, last = (x.mention for x in missing) + teamlers: list[str] + field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) + else: + can_delete, fair = True, parsed.fair + field = None + + message, interaction, parsed_options, question = await send_poll( + ctx=ctx, title=title, poll_args=options, max_choices=choices, field=field, deadline=deadline + ) + await ctx.message.delete() + + await Poll.create( + message_id=message.id, + message_url=message.jump_url, + guild_id=ctx.guild.id, + channel=message.channel.id, + owner=ctx.author.id, + title=question, + end=calc_end_time(deadline), + anonymous=anonymous, + can_delete=can_delete, + options=parsed_options, + poll_type=poll_type, + interaction=interaction.id, + fair=fair, + max_choices=choices, + ) From 216731236a72c5f186c01b5d072a584f9dc244fa Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:11:26 +0200 Subject: [PATCH 40/95] Added simple yes-no-poll --- general/polls/cog.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 2b1871f82..77dd0d591 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -1,7 +1,8 @@ import string -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from datetime import datetime -from typing import Optional, Tuple +from enum import Enum +from typing import Optional, Tuple, Union from PyDrocsid.command import docs, add_reactions, Confirmation from dateutil.relativedelta import relativedelta @@ -707,3 +708,25 @@ async def new(self, ctx: Context, *, options: str): fair=fair, max_choices=choices, ) + + @commands.command(aliases=["yn"]) + @guild_only() + @docs(t.commands.yes_no) + async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Optional[str] = None): + if message is None or message.guild is None or text: + message = ctx.message + + if message.author != ctx.author and not await is_teamler(ctx.author): + raise CommandError(t.foreign_message) + + try: + await message.add_reaction(name_to_emoji["thumbsup"]) + await message.add_reaction(name_to_emoji["thumbsdown"]) + except Forbidden: + raise CommandError(t.could_not_add_reactions(message.channel.mention)) + + if message != ctx.message: + try: + await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) + except Forbidden: + pass From 912937177533f5bf0f805abcd668d7dcb2ff7936 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:12:00 +0200 Subject: [PATCH 41/95] Added advanced yes-no-poll for team-members --- general/polls/cog.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 77dd0d591..9bcba97c0 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -730,3 +730,41 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) except Forbidden: pass + + @commands.command(aliases=["tyn"]) + @PollsPermission.team_poll.check + @guild_only() + @docs(t.commands.team_yes_no) + async def team_yesno(self, ctx: Context, *, text: str): + options = t.yes_no.option_string(text) + + missing = list(await get_staff(self.bot.guilds[0], ["team"])) + missing.sort(key=lambda m: str(m).lower()) + *teamlers, last = (x.mention for x in missing) + teamlers: list[str] + field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) + + message, interaction, parsed_options, question = await send_poll( + ctx=ctx, + title=t.team_poll, + max_choices=1, + poll_args=options, + field=field, + deadline=await PollsDefaultSettings.max_duration.get() * 24, + ) + await Poll.create( + message_id=message.id, + message_url=message.jump_url, + guild_id=ctx.guild.id, + channel=message.channel.id, + owner=ctx.author.id, + title=question, + end=calc_end_time(await PollsDefaultSettings.max_duration.get() * 24), + anonymous=False, + can_delete=False, + options=parsed_options, + poll_type=PollType.TEAM, + interaction=interaction.id, + fair=True, + max_choices=1, + ) From 47bf10283934d16aa41f383d209efba7ae7cf64a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:13:25 +0200 Subject: [PATCH 42/95] Fixed imports + formatted code --- general/polls/cog.py | 224 ++++++++++++++++++++++--------------------- 1 file changed, 117 insertions(+), 107 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 9bcba97c0..03bdc21f4 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -2,33 +2,43 @@ from argparse import ArgumentParser, Namespace from datetime import datetime from enum import Enum -from typing import Optional, Tuple, Union +from typing import Optional, Union -from PyDrocsid.command import docs, add_reactions, Confirmation from dateutil.relativedelta import relativedelta -from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, NotFound, SelectOption, HTTPException, RawMessageDeleteEvent, \ - Role +from discord import ( + Embed, + Forbidden, + Guild, + HTTPException, + Member, + Message, + NotFound, + RawMessageDeleteEvent, + Role, + SelectOption, +) from discord.ext import commands, tasks -from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, guild_only, UserInputError +from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, UserInputError, guild_only from discord.ui import Select, View -from discord.utils import utcnow, format_dt +from discord.utils import format_dt, utcnow -from PyDrocsid.database import db, db_wrapper, filter_by from PyDrocsid.cog import Cog +from PyDrocsid.command import Confirmation, add_reactions, docs +from PyDrocsid.database import db, db_wrapper, filter_by from PyDrocsid.embeds import EmbedLimits, send_long_embed from PyDrocsid.emojis import emoji_to_name, name_to_emoji -from PyDrocsid.events import StopEventHandling from PyDrocsid.settings import RoleSettings from PyDrocsid.translations import t -from PyDrocsid.util import check_wastebasket, is_teamler +from PyDrocsid.util import is_teamler from .colors import Colors -from .models import Poll, PollType, RoleWeight, PollVote, Option, sync_redis +from .models import Option, Poll, PollType, PollVote, RoleWeight, sync_redis from .permissions import PollsPermission from .settings import PollsDefaultSettings from ...contributor import Contributor from ...pubsub import send_alert, send_to_changelog + tg = t.g t = t.polls @@ -118,97 +128,6 @@ def calc_end_time(duration: Optional[float]) -> Optional[datetime]: return utcnow() + relativedelta(hours=int(duration)) if duration else None -async def handle_deleted_messages(bot, message_id: int): - """if a message containing a poll gets deleted, this function deletes the interaction message (both direction)""" - deleted_embed: Poll | None = await db.get(Poll, message_id=message_id) - deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message_id) - - if not deleted_embed and not deleted_interaction: - return - - poll = deleted_embed or deleted_interaction - channel = await bot.fetch_channel(poll.channel_id) - try: - if deleted_interaction: - msg: Message | None = await channel.fetch_message(poll.message_id) - else: - msg: Message | None = await channel.fetch_message(poll.interaction_message_id) - except NotFound: - msg = None - - if msg: - await poll.remove() - await msg.delete() - - -async def check_poll_time(poll: Poll) -> bool: - """checks if a poll has ended""" - if not poll.end_time: - await poll.remove() - return False - - elif poll.end_time < utcnow(): - return False - - return True - - -async def close_poll(bot, poll: Poll): - """deletes the interaction message and edits the footer of the poll embed""" - try: - channel = await bot.fetch_channel(poll.channel_id) - embed_message = await channel.fetch_message(poll.message_id) - interaction_message = await channel.fetch_message(poll.interaction_message_id) - except NotFound: - poll.active = False - return - - await interaction_message.delete() - embed = embed_message.embeds[0] - embed.set_footer(text=t.footer_closed) - - await embed_message.edit(embed=embed) - await embed_message.unpin() - - poll.active = False - - -async def get_staff(guild: Guild, team_roles: list[str]) -> set[Member]: - """gets a list of all team members""" - teamlers: set[Member] = set() - for role_name in team_roles: - if not (team_role := guild.get_role(await RoleSettings.get(role_name))): - continue - - teamlers.update(member for member in team_role.members if not member.bot) - - if not teamlers: - raise CommandError(t.error.no_teamlers) - - return teamlers - - -async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed: - """edits the poll embed, updating the votes and percentages""" - calc = get_percentage(poll) - for index, field in enumerate(embed.fields): - if field.name == tg.status: - missing.sort(key=lambda m: str(m).lower()) - *teamlers, last = (x.mention for x in missing) - teamlers: list[str] - embed.set_field_at( - index, - name=field.name, - value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1), - ) - else: - weight: float | int = calc[index][0] if not calc[index][0].is_integer() else int(calc[index][0]) - percentage: float | int = calc[index][1] if not calc[index][1].is_integer() else int(calc[index][1]) - embed.set_field_at(index, name=t.option.field.name(weight, percentage), value=field.value, inline=False) - - return embed - - async def send_poll( ctx: Context, title: str, @@ -286,6 +205,97 @@ async def send_poll( return msg, view_msg, parsed_options, question +async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed: + """edits the poll embed, updating the votes and percentages""" + calc = get_percentage(poll) + for index, field in enumerate(embed.fields): + if field.name == tg.status: + missing.sort(key=lambda m: str(m).lower()) + *teamlers, last = (x.mention for x in missing) + teamlers: list[str] + embed.set_field_at( + index, + name=field.name, + value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1), + ) + else: + weight: float | int = calc[index][0] if not calc[index][0].is_integer() else int(calc[index][0]) + percentage: float | int = calc[index][1] if not calc[index][1].is_integer() else int(calc[index][1]) + embed.set_field_at(index, name=t.option.field.name(weight, percentage), value=field.value, inline=False) + + return embed + + +async def get_staff(guild: Guild, team_roles: list[str]) -> set[Member]: + """gets a list of all team members""" + teamlers: set[Member] = set() + for role_name in team_roles: + if not (team_role := guild.get_role(await RoleSettings.get(role_name))): + continue + + teamlers.update(member for member in team_role.members if not member.bot) + + if not teamlers: + raise CommandError(t.error.no_teamlers) + + return teamlers + + +async def handle_deleted_messages(bot, message_id: int): + """if a message containing a poll gets deleted, this function deletes the interaction message (both direction)""" + deleted_embed: Poll | None = await db.get(Poll, message_id=message_id) + deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message_id) + + if not deleted_embed and not deleted_interaction: + return + + poll = deleted_embed or deleted_interaction + channel = await bot.fetch_channel(poll.channel_id) + try: + if deleted_interaction: + msg: Message | None = await channel.fetch_message(poll.message_id) + else: + msg: Message | None = await channel.fetch_message(poll.interaction_message_id) + except NotFound: + msg = None + + if msg: + await poll.remove() + await msg.delete() + + +async def check_poll_time(poll: Poll) -> bool: + """checks if a poll has ended""" + if not poll.end_time: + await poll.remove() + return False + + elif poll.end_time < utcnow(): + return False + + return True + + +async def close_poll(bot, poll: Poll): + """deletes the interaction message and edits the footer of the poll embed""" + try: + channel = await bot.fetch_channel(poll.channel_id) + embed_message = await channel.fetch_message(poll.message_id) + interaction_message = await channel.fetch_message(poll.interaction_message_id) + except NotFound: + poll.active = False + return + + await interaction_message.delete() + embed = embed_message.embeds[0] + embed.set_footer(text=t.footer_closed) + + await embed_message.edit(embed=embed) + await embed_message.unpin() + + poll.active = False + + class MySelect(Select): """adds a method for handling interactions with the select menu""" @@ -437,9 +447,9 @@ async def delete(self, ctx: Context, message: Message): if not poll: raise CommandError(t.error.not_poll) if ( - poll.can_delete - and not await PollsPermission.delete.check_permissions(ctx.author) - and not poll.owner_id == ctx.author.id + poll.can_delete + and not await PollsPermission.delete.check_permissions(ctx.author) + and not poll.owner_id == ctx.author.id ): raise PermissionError elif not poll.can_delete and not poll.owner_id == ctx.author.id: @@ -466,9 +476,9 @@ async def voted(self, ctx: Context, message: Message): if not poll: raise CommandError(t.error.not_poll) if ( - poll.anonymous - and not await PollsPermission.anonymous_bypass.check_permissions(author) - and not poll.owner_id == author.id + poll.anonymous + and not await PollsPermission.anonymous_bypass.check_permissions(author) + and not poll.owner_id == author.id ): raise PermissionError From 6e5de4807f039914bf1bc3a6c7e85d8b936ec2f4 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:55:56 +0200 Subject: [PATCH 43/95] Rewrote endtime now using seconds instead datetimes (untested) + added new status enum --- general/polls/cog.py | 17 ++++++++--------- general/polls/models.py | 14 ++++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 03bdc21f4..15a4e666a 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -32,7 +32,7 @@ from PyDrocsid.util import is_teamler from .colors import Colors -from .models import Option, Poll, PollType, PollVote, RoleWeight, sync_redis +from .models import Option, Poll, PollStatus, PollType, PollVote, RoleWeight, sync_redis from .permissions import PollsPermission from .settings import PollsDefaultSettings from ...contributor import Contributor @@ -159,8 +159,7 @@ async def send_poll( embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url) if deadline: - end_time = calc_end_time(deadline) - embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M"))) + embed.set_footer(text=t.footer(calc_end_time(deadline).strftime("%Y-%m-%d %H:%M"))) if len({option.emoji for option in options}) < len(options): raise CommandError(t.option_duplicated) @@ -266,11 +265,11 @@ async def handle_deleted_messages(bot, message_id: int): async def check_poll_time(poll: Poll) -> bool: """checks if a poll has ended""" - if not poll.end_time: + if not poll.end_time and not poll.poll_type == PollType.TEAM: await poll.remove() return False - elif poll.end_time < utcnow(): + elif poll.timestamp + relativedelta(seconds=poll.end_time) < utcnow() and poll.status != PollStatus.ACTIVE: return False return True @@ -283,7 +282,7 @@ async def close_poll(bot, poll: Poll): embed_message = await channel.fetch_message(poll.message_id) interaction_message = await channel.fetch_message(poll.interaction_message_id) except NotFound: - poll.active = False + poll.status = PollStatus.CLOSED return await interaction_message.delete() @@ -642,7 +641,7 @@ async def quick(self, ctx: Context, *, args: str): channel=message.channel.id, owner=ctx.author.id, title=question, - end=calc_end_time(deadline), + end=deadline, anonymous=anonymous, can_delete=True, options=parsed_options, @@ -709,7 +708,7 @@ async def new(self, ctx: Context, *, options: str): channel=message.channel.id, owner=ctx.author.id, title=question, - end=calc_end_time(deadline), + end=deadline, anonymous=anonymous, can_delete=can_delete, options=parsed_options, @@ -769,7 +768,7 @@ async def team_yesno(self, ctx: Context, *, text: str): channel=message.channel.id, owner=ctx.author.id, title=question, - end=calc_end_time(await PollsDefaultSettings.max_duration.get() * 24), + end=await PollsDefaultSettings.max_duration.get() * 24, anonymous=False, can_delete=False, options=parsed_options, diff --git a/general/polls/models.py b/general/polls/models.py index 7cadb11b7..7c5e7e563 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -19,6 +19,12 @@ class PollType(enum.Enum): STANDARD = "standard" +class PollStatus(enum.Enum): + ACTIVE = 0 + PAUSED = 1 + CLOSED = 2 + + async def sync_redis(role_id: int = None) -> list[dict[str, int | float]]: out = [] @@ -53,11 +59,11 @@ class Poll(Base): timestamp: Union[Column, datetime] = Column(UTCDateTime) title: Union[Column, str] = Column(Text(256)) poll_type: Union[Column, PollType] = Column(Enum(PollType)) - end_time: Union[Column, datetime] = Column(UTCDateTime) + end_time: Union[Column, int] = Column(BigInteger) anonymous: Union[Column, bool] = Column(Boolean) can_delete: Union[Column, bool] = Column(Boolean) fair: Union[Column, bool] = Column(Boolean) - active: Union[Column, bool] = Column(Boolean) + status: Union[Column, PollStatus] = Column(Enum(PollStatus)) max_choices: Union[Column, int] = Column(BigInteger) @staticmethod @@ -69,7 +75,7 @@ async def create( owner: int, title: str, options: list[tuple[str, str]], - end: Optional[datetime], + end: Optional[int], anonymous: bool, can_delete: bool, poll_type: enum.Enum, @@ -91,7 +97,7 @@ async def create( can_delete=can_delete, interaction_message_id=interaction, fair=fair, - active=True, + status=0, max_choices=max_choices, ) for position, poll_option in enumerate(options): From b85fb6b3430ac76887351056813c1ea7d02a197e Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:57:45 +0200 Subject: [PATCH 44/95] Fixed small mistake --- general/polls/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/general/polls/models.py b/general/polls/models.py index 7c5e7e563..cfbe9897d 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -97,7 +97,7 @@ async def create( can_delete=can_delete, interaction_message_id=interaction, fair=fair, - status=0, + status=PollStatus.ACTIVE, max_choices=max_choices, ) for position, poll_option in enumerate(options): From 956ce4a3bcb5bf0c3161c252118bd4cc088feef7 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Tue, 28 Jun 2022 18:11:49 +0200 Subject: [PATCH 45/95] Resolved issues in code --- general/polls/cog.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 15a4e666a..bcd29a553 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -189,6 +189,7 @@ async def send_poll( ], ) view_msg = await ctx.send(view=create_select_view(select_obj=select_obj)) + await msg.create_thread(name=question) parsed_options: list[tuple[str, str]] = [(obj.emoji, t.select.label(ix)) for ix, obj in enumerate(options, start=1)] @@ -292,7 +293,7 @@ async def close_poll(bot, poll: Poll): await embed_message.edit(embed=embed) await embed_message.unpin() - poll.active = False + poll.status = PollStatus.PAUSED class MySelect(Select): @@ -368,7 +369,7 @@ def __init__(self, team_roles: list[str]): async def on_ready(self): await sync_redis() - polls: list[Poll] = await db.all(filter_by(Poll, (Poll.options, Option.votes), active=True)) + polls: list[Poll] = await db.all(filter_by(Poll, (Poll.options, Option.votes), status=PollStatus.ACTIVE)) for poll in polls: if await check_poll_time(poll): select_obj = MySelect( @@ -401,7 +402,7 @@ async def on_raw_message_delete(self, event: RawMessageDeleteEvent): @tasks.loop(minutes=1) @db_wrapper async def poll_loop(self): - polls: list[Poll] = await db.all(filter_by(Poll, active=True)) + polls: list[Poll] = await db.all(filter_by(Poll, status=PollStatus.ACTIVE)) for poll in polls: if not await check_poll_time(poll): @@ -414,11 +415,11 @@ async def poll(self, ctx: Context): if not ctx.subcommand_passed: raise UserInputError - @poll.command(name="list", aliases=["l"]) + @poll.command(name="list", aliases=["l"]) # TODO: Filter by ACTIVE and PAUSED @guild_only() @docs(t.commands.poll.list) async def poll_list(self, ctx: Context): - polls: list[Poll] = await db.all(filter_by(Poll, active=True, guild_id=ctx.guild.id)) + polls: list[Poll] = await db.all(filter_by(Poll, status=PollStatus.ACTIVE, guild_id=ctx.guild.id)) description = "" for poll in polls: if poll.poll_type == PollType.TEAM and not await PollsPermission.team_poll.check_permissions(ctx.author): From d815af705d3392ac4a2bb5a957f2b901e3727b86 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Tue, 28 Jun 2022 18:39:35 +0200 Subject: [PATCH 46/95] Added command barebones for team-polls --- general/polls/cog.py | 13 +++++++++++++ general/polls/settings.py | 4 ++++ general/polls/translations/en.yml | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index bcd29a553..d42ea3848 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -625,6 +625,19 @@ async def fair(self, ctx: Context, status: bool): await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) + @poll.group(name="team", aliases=["t"]) + @PollsPermission.team_poll.check + @docs(t.commands.poll.team.team) + async def team(self, ctx: Context): + if not ctx.subcommand_passed: + raise UserInputError + + @team.group(name="tp_settings", aliases=["s"]) + @PollsPermission.read.check + @docs(t.commands.poll.team.settings.settings) + async def tp_settings(self, ctx: Context): + pass + @poll.command(name="quick", usage=t.usage.poll, aliases=["q"]) @docs(t.commands.poll.quick) async def quick(self, ctx: Context, *, args: str): diff --git a/general/polls/settings.py b/general/polls/settings.py index c5d7a646f..a49b38079 100644 --- a/general/polls/settings.py +++ b/general/polls/settings.py @@ -8,3 +8,7 @@ class PollsDefaultSettings(Settings): everyone_power = 1.0 anonymous = False fair = False + + +class PollsTeamsSettings(Settings): + duration = 0 diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index fcbd97149..3e81671e9 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -14,6 +14,10 @@ commands: votes: set the default amount of votes a user can have on polls anonymous: set if user can see who voted on a poll fair: manage if role weights impact on default polls + team: + team: create and manage team-polls + settings: + settings: manage team-poll settings yes_no: add thumbs-up/down emotes on a message team_yes_no: starts a yes/no poll and shows, which teamler has not voted yet. From 2913553ac7be8754dacad3e89e4a45d7c591ded6 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Tue, 28 Jun 2022 18:43:57 +0200 Subject: [PATCH 47/95] Removed option for team-polls from argparse --- general/polls/cog.py | 9 +-------- general/polls/translations/en.yml | 3 --- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index d42ea3848..a79b12546 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -104,13 +104,6 @@ def build_wizard(skip: bool = False) -> Embed: async def get_parser() -> ArgumentParser: """creates a parser object with options for advanced polls""" parser = ArgumentParser() - parser.add_argument( - "--type", - "-T", - default=PollType.STANDARD.value, - choices=[PollType.STANDARD.value, PollType.TEAM.value], - type=str, - ) parser.add_argument("--deadline", "-D", default=await PollsDefaultSettings.duration.get(), type=int) parser.add_argument( "--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool, choices=[True, False] @@ -684,7 +677,7 @@ async def new(self, ctx: Context, *, options: str): parsed: Namespace = parser.parse_known_args(args.split())[0] title: str = t.poll - poll_type: Enum | str = parsed.type.lower() + poll_type: Enum | str = parsed.type.lower() # TODO Remove team-poll option if poll_type == PollType.TEAM.value and await PollsPermission.team_poll.check_permissions(ctx.author): poll_type = PollType.TEAM title: str = t.team_poll diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 3e81671e9..5bb4df843 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -150,9 +150,6 @@ wizard: arg: Arguments args: | ``` - --type {standard,team}, -T {standard,team} - standard or team embed [Default: 'standard'] - --deadline DEADLINE, -D DEADLINE time when the poll should be closed [Default: server settings] From bb04917d9fe268e6076ddf8ca3c6c0bfe392c17f Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 16:27:26 +0200 Subject: [PATCH 48/95] Added more command placeholders for team-polls --- general/polls/cog.py | 19 +++++++++++++++++-- general/polls/translations/en.yml | 3 +++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index a79b12546..3422f0b33 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -625,12 +625,27 @@ async def team(self, ctx: Context): if not ctx.subcommand_passed: raise UserInputError - @team.group(name="tp_settings", aliases=["s"]) + @team.group(name="settings", aliases=["s"]) @PollsPermission.read.check @docs(t.commands.poll.team.settings.settings) async def tp_settings(self, ctx: Context): pass + @team.command(name="unpin", aliases=["u"]) + @docs(t.commands.poll.team.unpin) + async def unpin(self, ctx: Context, message: Message): + pass + + @team.command(name="new", aliases=["n"]) + @docs(t.commands.poll.team.new) + async def team_new(self, ctx: Context, *, args: str): + pass + + @team.command(name="list", aliases=["l"]) + @docs(t.commands.poll.list) + async def list(self, ctx: Context): + pass + @poll.command(name="quick", usage=t.usage.poll, aliases=["q"]) @docs(t.commands.poll.quick) async def quick(self, ctx: Context, *, args: str): @@ -747,7 +762,7 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt except Forbidden: pass - @commands.command(aliases=["tyn"]) + @team.command(name="yes_no", aliases=["yn"]) @PollsPermission.team_poll.check @guild_only() @docs(t.commands.team_yes_no) diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 5bb4df843..92187e995 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -16,6 +16,9 @@ commands: fair: manage if role weights impact on default polls team: team: create and manage team-polls + unpin: unpin a team-poll + new: create a new team-poll + list: show all open team-polls settings: settings: manage team-poll settings yes_no: add thumbs-up/down emotes on a message From cfcefb1e42ea04a1589c7629c616a0b6b7147355 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 16:44:13 +0200 Subject: [PATCH 49/95] Added poll-list for team-polls and reworked sorting for poll-list --- general/polls/cog.py | 46 ++++++++++++++++--------------- general/polls/translations/en.yml | 5 ++-- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 3422f0b33..64c8a8eb2 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -289,6 +289,23 @@ async def close_poll(bot, poll: Poll): poll.status = PollStatus.PAUSED +async def get_poll_list_embed(ctx: Context, poll_type: PollType, state: PollStatus) -> Embed: + description = "" + polls: list[Poll] = await db.all(filter_by(Poll, status=state, guild_id=ctx.guild.id, poll_type=poll_type)) + + for poll in polls: + description += t.polls.row( + poll.title, poll.message_url, poll.owner_id, format_dt(calc_end_time(poll.end_time), style="R") + ) + + if polls and description: + embed: Embed = Embed(title=t.polls.title(poll_type.value), description=description, color=Colors.Polls) + else: + embed: Embed = Embed(title=t.no_polls(poll_type.value), color=Colors.error) + + return embed + + class MySelect(Select): """adds a method for handling interactions with the select menu""" @@ -408,30 +425,13 @@ async def poll(self, ctx: Context): if not ctx.subcommand_passed: raise UserInputError - @poll.command(name="list", aliases=["l"]) # TODO: Filter by ACTIVE and PAUSED + @poll.command(name="list", aliases=["l"]) @guild_only() @docs(t.commands.poll.list) - async def poll_list(self, ctx: Context): - polls: list[Poll] = await db.all(filter_by(Poll, status=PollStatus.ACTIVE, guild_id=ctx.guild.id)) - description = "" - for poll in polls: - if poll.poll_type == PollType.TEAM and not await PollsPermission.team_poll.check_permissions(ctx.author): - continue - if poll.poll_type == PollType.TEAM: - description += t.polls.team_row( - poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R") - ) - else: - description += t.polls.row( - poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R") - ) + async def poll_list(self, ctx: Context, active: bool = True): + embed = await get_poll_list_embed(ctx, PollType.STANDARD, PollStatus.ACTIVE if active else PollStatus.PAUSED) - if polls and description: - embed: Embed = Embed(title=t.polls.title, description=description, color=Colors.Polls) - await send_long_embed(ctx, embed=embed, paginate=True) - - else: - await send_long_embed(ctx, embed=Embed(title=t.no_polls, color=Colors.error)) + await send_long_embed(ctx, embed=embed, paginate=True) @poll.command(name="delete", aliases=["del"]) @docs(t.commands.poll.delete) @@ -644,7 +644,9 @@ async def team_new(self, ctx: Context, *, args: str): @team.command(name="list", aliases=["l"]) @docs(t.commands.poll.list) async def list(self, ctx: Context): - pass + embed = await get_poll_list_embed(ctx, PollType.TEAM, PollStatus.ACTIVE) + + await send_long_embed(ctx, embed=embed, paginate=True) @poll.command(name="quick", usage=t.usage.poll, aliases=["q"]) @docs(t.commands.poll.quick) diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 92187e995..08dfe8486 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -69,9 +69,8 @@ poll_config: row: "\n<@&{}> -> `{}x`" polls: - title: Active polls + title: Active {} polls row: "\n[`{}`]({}) by <@{}> until {}" - team_row: "\n:star: [`{}`]({}) by <@{}> until {}" role_weight: set: "Set vote weight for <@&{}> to `{}`" @@ -193,4 +192,4 @@ footer_closed: Closed can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option" foreign_message: "You are not allowed to add yes/no reactions to foreign messages!" could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}. -no_polls: No current active polls. +no_polls: No current active {}-polls. From bf623237e76e8d8c306c2ecbadd48aa4c1443d0e Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:09:57 +0200 Subject: [PATCH 50/95] Added option for RoleWeights on team-polls --- general/polls/cog.py | 4 ++-- general/polls/models.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 64c8a8eb2..099e6da13 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -522,7 +522,7 @@ async def settings(self, ctx: Context): embed.add_field(name=t.poll_config.anonymous.name, value=str(anonymous), inline=False) fair: bool = await PollsDefaultSettings.fair.get() embed.add_field(name=t.poll_config.fair.name, value=str(fair), inline=False) - roles = await RoleWeight.get(ctx.guild.id) + roles = await RoleWeight.get(ctx.guild.id, PollType.STANDARD) everyone: int = await PollsDefaultSettings.everyone_power.get() base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone) if roles: @@ -547,7 +547,7 @@ async def roles_weights(self, ctx: Context, role: Role, weight: float | None = N element.weight = weight msg: str = t.role_weight.set(role.id, weight) elif weight and not element: - await RoleWeight.create(ctx.guild.id, role.id, weight) + await RoleWeight.create(ctx.guild.id, role.id, weight, PollType.STANDARD) msg: str = t.role_weight.set(role.id, weight) else: await element.remove() diff --git a/general/polls/models.py b/general/polls/models.py index cfbe9897d..c5a49c4de 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -159,11 +159,14 @@ class RoleWeight(Base): guild_id: Union[Column, int] = Column(BigInteger) role_id: Union[Column, int] = Column(BigInteger, unique=True) weight: Union[Column, float] = Column(Float) + poll_type: Union[Column, PollType] = Column(Enum(PollType)) timestamp: Union[Column, datetime] = Column(UTCDateTime) @staticmethod - async def create(guild_id: int, role: int, weight: float) -> RoleWeight: - role_weight = RoleWeight(guild_id=guild_id, role_id=role, weight=weight, timestamp=utcnow()) + async def create(guild_id: int, role: int, weight: float, poll_type: PollType) -> RoleWeight: + role_weight = RoleWeight( + guild_id=guild_id, role_id=role, weight=weight, timestamp=utcnow(), poll_type=poll_type + ) await db.add(role_weight) await sync_redis() return role_weight @@ -173,8 +176,8 @@ async def remove(self) -> None: await sync_redis(self.role_id) @staticmethod - async def get(guild: int) -> list[RoleWeight]: - return await db.all(filter_by(RoleWeight, guild_id=guild)) + async def get(guild: int, poll_type: PollType) -> list[RoleWeight]: + return await db.all(filter_by(RoleWeight, guild_id=guild, poll_type=poll_type)) @staticmethod async def get_highest(user_roles: list[Role]) -> float: From 1f48ee4fb58f824050eb6bafa6fd86c140acf7a5 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:14:20 +0200 Subject: [PATCH 51/95] Reworked new-poll command now without team option --- general/polls/cog.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 099e6da13..252095066 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -1,7 +1,6 @@ import string from argparse import ArgumentParser, Namespace from datetime import datetime -from enum import Enum from typing import Optional, Union from dateutil.relativedelta import relativedelta @@ -693,13 +692,6 @@ async def new(self, ctx: Context, *, options: str): parser = await get_parser() parsed: Namespace = parser.parse_known_args(args.split())[0] - title: str = t.poll - poll_type: Enum | str = parsed.type.lower() # TODO Remove team-poll option - if poll_type == PollType.TEAM.value and await PollsPermission.team_poll.check_permissions(ctx.author): - poll_type = PollType.TEAM - title: str = t.team_poll - else: - poll_type = PollType.STANDARD max_deadline = await PollsDefaultSettings.max_duration.get() * 24 deadline: Union[list[str, str], int] = parsed.deadline if isinstance(deadline, int): @@ -708,7 +700,8 @@ async def new(self, ctx: Context, *, options: str): deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24 anonymous: bool = parsed.anonymous choices: int = parsed.choices - + can_delete, fair = True, parsed.fair + """ # Excluded code, need to be put into team-polls (new function) if poll_type == PollType.TEAM: can_delete, fair = False, True missing = list(await get_staff(self.bot.guilds[0], ["team"])) @@ -716,12 +709,10 @@ async def new(self, ctx: Context, *, options: str): *teamlers, last = (x.mention for x in missing) teamlers: list[str] field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) - else: - can_delete, fair = True, parsed.fair - field = None + """ message, interaction, parsed_options, question = await send_poll( - ctx=ctx, title=title, poll_args=options, max_choices=choices, field=field, deadline=deadline + ctx=ctx, title=t.poll, poll_args=options, max_choices=choices, deadline=deadline ) await ctx.message.delete() @@ -736,7 +727,7 @@ async def new(self, ctx: Context, *, options: str): anonymous=anonymous, can_delete=can_delete, options=parsed_options, - poll_type=poll_type, + poll_type=PollType.STANDARD, interaction=interaction.id, fair=fair, max_choices=choices, From d9cdf932bd9637f3b1f6dde89812b1072636f074 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:28:04 +0200 Subject: [PATCH 52/95] Added thread_id to poll model --- general/polls/cog.py | 15 +++++++++------ general/polls/models.py | 3 +++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 252095066..6d7b7eb62 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -127,7 +127,7 @@ async def send_poll( max_choices: int = None, field: Optional[tuple[str, str]] = None, deadline: Optional[int] = None, -) -> tuple[Message, Message, list[tuple[str, str]], str]: +) -> tuple[Message, Message, list[tuple[str, str]], str, int]: """sends a poll embed + view message containing the select field""" if not max_choices: @@ -181,7 +181,7 @@ async def send_poll( ], ) view_msg = await ctx.send(view=create_select_view(select_obj=select_obj)) - await msg.create_thread(name=question) + thread = await msg.create_thread(name=question) parsed_options: list[tuple[str, str]] = [(obj.emoji, t.select.label(ix)) for ix, obj in enumerate(options, start=1)] @@ -194,7 +194,7 @@ async def send_poll( color=Colors.error, ) await send_alert(ctx.guild, embed) - return msg, view_msg, parsed_options, question + return msg, view_msg, parsed_options, question, thread.id async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed: @@ -653,7 +653,7 @@ async def quick(self, ctx: Context, *, args: str): deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24 max_choices = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS anonymous = await PollsDefaultSettings.anonymous.get() - message, interaction, parsed_options, question = await send_poll( + message, interaction, parsed_options, question, thread_id = await send_poll( ctx=ctx, title=t.poll, poll_args=args, max_choices=max_choices, deadline=deadline ) @@ -672,6 +672,7 @@ async def quick(self, ctx: Context, *, args: str): interaction=interaction.id, fair=await PollsDefaultSettings.fair.get(), max_choices=max_choices, + thread=thread_id, ) await ctx.message.delete() @@ -711,7 +712,7 @@ async def new(self, ctx: Context, *, options: str): field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) """ - message, interaction, parsed_options, question = await send_poll( + message, interaction, parsed_options, question, thread_id = await send_poll( ctx=ctx, title=t.poll, poll_args=options, max_choices=choices, deadline=deadline ) await ctx.message.delete() @@ -731,6 +732,7 @@ async def new(self, ctx: Context, *, options: str): interaction=interaction.id, fair=fair, max_choices=choices, + thread=thread_id, ) @commands.command(aliases=["yn"]) @@ -768,7 +770,7 @@ async def team_yesno(self, ctx: Context, *, text: str): teamlers: list[str] field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) - message, interaction, parsed_options, question = await send_poll( + message, interaction, parsed_options, question, thread_id = await send_poll( ctx=ctx, title=t.team_poll, max_choices=1, @@ -791,4 +793,5 @@ async def team_yesno(self, ctx: Context, *, text: str): interaction=interaction.id, fair=True, max_choices=1, + thread=thread_id, ) diff --git a/general/polls/models.py b/general/polls/models.py index c5a49c4de..bc7f12f06 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -54,6 +54,7 @@ class Poll(Base): message_url: Union[Column, str] = Column(Text(256)) guild_id: Union[Column, int] = Column(BigInteger) interaction_message_id: Union[Column, int] = Column(BigInteger, unique=True) + thread_id: Union[Column, int] = Column(BigInteger, unique=True) channel_id: Union[Column, int] = Column(BigInteger) owner_id: Union[Column, int] = Column(BigInteger) timestamp: Union[Column, datetime] = Column(UTCDateTime) @@ -80,6 +81,7 @@ async def create( can_delete: bool, poll_type: enum.Enum, interaction: int, + thread: int, fair: bool, max_choices: int, ) -> Poll: @@ -96,6 +98,7 @@ async def create( anonymous=anonymous, can_delete=can_delete, interaction_message_id=interaction, + thread_id=thread, fair=fair, status=PollStatus.ACTIVE, max_choices=max_choices, From 9ab5a5a14092156ed802800a41a0fc41dd3415ea Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 19:28:34 +0200 Subject: [PATCH 53/95] Added function that will notify missing team-members on polls every day after the time is over --- general/polls/cog.py | 29 +++++++++++++++++++++++++++-- general/polls/translations/en.yml | 1 + 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 6d7b7eb62..a8d9946c5 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -17,7 +17,7 @@ SelectOption, ) from discord.ext import commands, tasks -from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, UserInputError, guild_only +from discord.ext.commands import Bot, CommandError, Context, EmojiConverter, EmojiNotFound, UserInputError, guild_only from discord.ui import Select, View from discord.utils import format_dt, utcnow @@ -233,6 +233,28 @@ async def get_staff(guild: Guild, team_roles: list[str]) -> set[Member]: return teamlers +async def notify_missing_staff(bot: Bot, poll: Poll): + thread = bot.get_channel(poll.thread_id) + if not thread: + return + try: + teamlers: set[Member] = await get_staff(bot.get_guild(poll.guild_id), ["team"]) + except CommandError: + await thread.send(embed=Embed(title=t.error.no_teamlers, color=Colors.error)) + return + + user_ids: set[int] = set() + for option in poll.options: + for vote in option.votes: + user_ids.add(vote.user_id) + + missing: list[Member] | None = [teamler for teamler in teamlers if teamler.id not in user_ids] + missing.sort(key=lambda m: str(m).lower()) + + desc = " ".join(f"<@{user}>" for user in missing) + await thread.send(t.team_poll_missing(desc)) + + async def handle_deleted_messages(bot, message_id: int): """if a message containing a poll gets deleted, this function deletes the interaction message (both direction)""" deleted_embed: Poll | None = await db.get(Poll, message_id=message_id) @@ -414,8 +436,11 @@ async def poll_loop(self): polls: list[Poll] = await db.all(filter_by(Poll, status=PollStatus.ACTIVE)) for poll in polls: - if not await check_poll_time(poll): + if not await check_poll_time(poll) and poll.poll_type == PollType.STANDARD: await close_poll(self.bot, poll) + elif not await check_poll_time(poll) and poll.poll_type == PollType.TEAM: + poll.end_time = poll.end_time + relativedelta(days=1) + await notify_missing_staff(self.bot, poll) @commands.group(name="poll", aliases=["vote"]) @guild_only() diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 08dfe8486..e98c4a2f7 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -174,6 +174,7 @@ wizard: poll: Poll team_poll: Team Poll poll_voted: "Vote was added to the poll" +team_poll_missing: "{} please vote on the poll!" team_yn_poll_forbidden: You are not allowed to use a team poll! vote_explanation: Vote using the reactions below! too_many_options: You specified too many options. The maximum amount is {}. From 3a2a651ddf95fcffe71cdedc66c568c01a049255 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 19:58:31 +0200 Subject: [PATCH 54/95] Footer on edit has now the new endtime for team-polls too --- general/polls/cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index a8d9946c5..2009152ab 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -214,6 +214,7 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None weight: float | int = calc[index][0] if not calc[index][0].is_integer() else int(calc[index][0]) percentage: float | int = calc[index][1] if not calc[index][1].is_integer() else int(calc[index][1]) embed.set_field_at(index, name=t.option.field.name(weight, percentage), value=field.value, inline=False) + embed.set_footer(text=t.footer(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) return embed From 07c965f05b6044f708e7afad4a95ec7cbeb989d9 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 20:41:35 +0200 Subject: [PATCH 55/95] Added commands to pause/unpause (standard) polls --- general/polls/cog.py | 42 +++++++++++++++++++++++++++---- general/polls/models.py | 6 ++--- general/polls/permissions.py | 3 +-- general/polls/translations/en.yml | 12 +++++---- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 2009152ab..f75d2308e 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -321,9 +321,11 @@ async def get_poll_list_embed(ctx: Context, poll_type: PollType, state: PollStat ) if polls and description: - embed: Embed = Embed(title=t.polls.title(poll_type.value), description=description, color=Colors.Polls) + embed: Embed = Embed( + title=t.polls.title(state.value, poll_type.value), description=description, color=Colors.Polls + ) else: - embed: Embed = Embed(title=t.no_polls(poll_type.value), color=Colors.error) + embed: Embed = Embed(title=t.error.no_polls(state.value, poll_type.value), color=Colors.error) return embed @@ -339,7 +341,7 @@ async def callback(self, interaction): embed: Embed = message.embeds[0] if message.embeds else None poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) - if not poll or not embed: + if not poll or not embed or not poll.status == PollStatus.ACTIVE: return new_options: list[Option] = [option for option in poll.options if option.option in selected_options] @@ -466,7 +468,7 @@ async def delete(self, ctx: Context, message: Message): raise CommandError(t.error.not_poll) if ( poll.can_delete - and not await PollsPermission.delete.check_permissions(ctx.author) + and not await PollsPermission.manage.check_permissions(ctx.author) and not poll.owner_id == ctx.author.id ): raise PermissionError @@ -495,7 +497,7 @@ async def voted(self, ctx: Context, message: Message): raise CommandError(t.error.not_poll) if ( poll.anonymous - and not await PollsPermission.anonymous_bypass.check_permissions(author) + and not await PollsPermission.manage.check_permissions(author) and not poll.owner_id == author.id ): raise PermissionError @@ -515,6 +517,36 @@ async def voted(self, ctx: Context, message: Message): await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True) + @poll.command(name="activate", aliases=["a"]) + @docs(t.commands.poll.activate) + async def activate(self, ctx: Context, message: Message): + poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + if not poll: + raise CommandError(t.error.not_poll) + if not ctx.author.id == poll.owner_id and not await PollsPermission.manage.check_permissions(ctx.author): + raise PermissionError + + if poll.status == PollStatus.ACTIVE: + raise CommandError(t.poll_status_not_changed("activated")) + else: + poll.status = PollStatus.ACTIVE + await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed("activated"))) + + @poll.command(name="pause", aliases=["p", "deactivate", "disable"]) + @docs(t.commands.poll.paused) + async def pause(self, ctx: Context, message: Message): + poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + if not poll: + raise CommandError(t.error.not_poll) + if not ctx.author.id == poll.owner_id and not await PollsPermission.manage.check_permissions(ctx.author): + raise PermissionError + + if poll.status == PollStatus.PAUSED: + raise CommandError(t.poll_status_not_changed("paused")) + else: + poll.status = PollStatus.PAUSED + await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed("paused"))) + @poll.group(name="settings", aliases=["s"]) @PollsPermission.read.check @docs(t.commands.poll.settings.settings) diff --git a/general/polls/models.py b/general/polls/models.py index bc7f12f06..2c54f76bd 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -20,9 +20,9 @@ class PollType(enum.Enum): class PollStatus(enum.Enum): - ACTIVE = 0 - PAUSED = 1 - CLOSED = 2 + ACTIVE = "active" + PAUSED = "paused" + CLOSED = "closed" async def sync_redis(role_id: int = None) -> list[dict[str, int | float]]: diff --git a/general/polls/permissions.py b/general/polls/permissions.py index 7d4b6992e..1f1b46565 100644 --- a/general/polls/permissions.py +++ b/general/polls/permissions.py @@ -12,5 +12,4 @@ def description(self) -> str: team_poll = auto() read = auto() write = auto() - delete = auto() - anonymous_bypass = auto() + manage = auto() diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index e98c4a2f7..5b1509c0f 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -4,6 +4,8 @@ commands: quick: small poll with default options new: advanced poll with more options delete: delete polls + activate: activate a paused poll + paused: pause an active poll voted: show who voted on the poll (only works if not anonymous or if the poll-owner uses the command) list: show the active polls settings: @@ -28,14 +30,14 @@ permissions: team_poll: start a team poll read: read poll configuration write: edit poll configuration - delete: delete polls - anonymous_bypass: can see user, even if poll is anonymous + manage: can manage polls (not settings) error: weight_too_small: "Weight cant be lower than `0.1`" cant_set_weight: Can't set weight! not_poll: Mesage doesn't contains a poll no_teamlers: No user with team-role found! + no_polls: No current {} {}-polls. cant_pin: title: Error description: Can't pin any more messages in {} @@ -69,7 +71,7 @@ poll_config: row: "\n<@&{}> -> `{}x`" polls: - title: Active {} polls + title: "List of {} {} polls" row: "\n[`{}`]({}) by <@{}> until {}" role_weight: @@ -174,6 +176,8 @@ wizard: poll: Poll team_poll: Team Poll poll_voted: "Vote was added to the poll" +poll_status_not_changed: Poll is already {}! +poll_status_changed: Poll is now {}! team_poll_missing: "{} please vote on the poll!" team_yn_poll_forbidden: You are not allowed to use a team poll! vote_explanation: Vote using the reactions below! @@ -190,7 +194,5 @@ teamlers_missing: many: "{teamlers} and {last} haven't voted yet." footer: Ends at {} UTC footer_closed: Closed -can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option" foreign_message: "You are not allowed to add yes/no reactions to foreign messages!" could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}. -no_polls: No current active {}-polls. From 7b36c617fbdf417886bcea6bfc5ffa34339e2edb Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 21:03:51 +0200 Subject: [PATCH 56/95] Improved pausing/unpausing of polls --- general/polls/cog.py | 48 +++++++++++++++++++++++++------ general/polls/translations/en.yml | 2 ++ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index f75d2308e..351e24f16 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -308,7 +308,7 @@ async def close_poll(bot, poll: Poll): await embed_message.edit(embed=embed) await embed_message.unpin() - poll.status = PollStatus.PAUSED + poll.status = PollStatus.CLOSED async def get_poll_list_embed(ctx: Context, poll_type: PollType, state: PollStatus) -> Embed: @@ -330,6 +330,32 @@ async def get_poll_list_embed(ctx: Context, poll_type: PollType, state: PollStat return embed +async def status_change(bot: Bot, poll: Poll): + try: + channel = await bot.fetch_channel(poll.channel_id) + embed_message = await channel.fetch_message(poll.message_id) + except NotFound: + if poll.status == PollStatus.ACTIVE: + poll.status = PollStatus.PAUSED + else: + poll.status = PollStatus.ACTIVE + return + + embed = embed_message.embeds[0] + if poll.status == PollStatus.ACTIVE: + poll.status = PollStatus.PAUSED + embed.set_footer(text=t.footer_paused) + embed.colour = Colors.grey + await embed_message.unpin() + else: + poll.status = PollStatus.ACTIVE + embed.set_footer(text=t.footer(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) + embed.colour = Colors.Polls + await embed_message.pin() + + await embed_message.edit(embed=embed) + + class MySelect(Select): """adds a method for handling interactions with the select menu""" @@ -341,7 +367,13 @@ async def callback(self, interaction): embed: Embed = message.embeds[0] if message.embeds else None poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) - if not poll or not embed or not poll.status == PollStatus.ACTIVE: + if not poll or not embed: + return + + if not poll.status == PollStatus.ACTIVE: + await interaction.response.send_message( + content=t.error.poll_cant_be_used(poll.status.value), ephemeral=True + ) return new_options: list[Option] = [option for option in poll.options if option.option in selected_options] @@ -528,9 +560,9 @@ async def activate(self, ctx: Context, message: Message): if poll.status == PollStatus.ACTIVE: raise CommandError(t.poll_status_not_changed("activated")) - else: - poll.status = PollStatus.ACTIVE - await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed("activated"))) + + await status_change(self.bot, poll) + await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed("activated"))) @poll.command(name="pause", aliases=["p", "deactivate", "disable"]) @docs(t.commands.poll.paused) @@ -543,9 +575,9 @@ async def pause(self, ctx: Context, message: Message): if poll.status == PollStatus.PAUSED: raise CommandError(t.poll_status_not_changed("paused")) - else: - poll.status = PollStatus.PAUSED - await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed("paused"))) + + await status_change(self.bot, poll) + await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed("paused"))) @poll.group(name="settings", aliases=["s"]) @PollsPermission.read.check diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 5b1509c0f..36a227c5c 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -38,6 +38,7 @@ error: not_poll: Mesage doesn't contains a poll no_teamlers: No user with team-role found! no_polls: No current {} {}-polls. + poll_cant_be_used: Poll can't be used because it's {} cant_pin: title: Error description: Can't pin any more messages in {} @@ -194,5 +195,6 @@ teamlers_missing: many: "{teamlers} and {last} haven't voted yet." footer: Ends at {} UTC footer_closed: Closed +footer_paused: Paused foreign_message: "You are not allowed to add yes/no reactions to foreign messages!" could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}. From 91e26255789bc6b7bdb79065be5e63b7901ede6b Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 21:06:19 +0200 Subject: [PATCH 57/95] Improved code --- general/polls/cog.py | 8 ++++---- general/polls/translations/en.yml | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 351e24f16..e222cf75e 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -559,10 +559,10 @@ async def activate(self, ctx: Context, message: Message): raise PermissionError if poll.status == PollStatus.ACTIVE: - raise CommandError(t.poll_status_not_changed("activated")) + raise CommandError(t.poll_status_not_changed(poll.status.value)) await status_change(self.bot, poll) - await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed("activated"))) + await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed(poll.status.value))) @poll.command(name="pause", aliases=["p", "deactivate", "disable"]) @docs(t.commands.poll.paused) @@ -574,10 +574,10 @@ async def pause(self, ctx: Context, message: Message): raise PermissionError if poll.status == PollStatus.PAUSED: - raise CommandError(t.poll_status_not_changed("paused")) + raise CommandError(t.poll_status_not_changed(poll.status.value)) await status_change(self.bot, poll) - await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed("paused"))) + await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed(poll.status.value))) @poll.group(name="settings", aliases=["s"]) @PollsPermission.read.check diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 36a227c5c..0a3d13dfc 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -197,4 +197,3 @@ footer: Ends at {} UTC footer_closed: Closed footer_paused: Paused foreign_message: "You are not allowed to add yes/no reactions to foreign messages!" -could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}. From a81596b3d5d71097f59f9970a0bf96ec7ed33b75 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 21:29:04 +0200 Subject: [PATCH 58/95] Improved code --- general/polls/cog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index e222cf75e..580b10385 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -742,7 +742,6 @@ async def list(self, ctx: Context): async def quick(self, ctx: Context, *, args: str): deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24 max_choices = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS - anonymous = await PollsDefaultSettings.anonymous.get() message, interaction, parsed_options, question, thread_id = await send_poll( ctx=ctx, title=t.poll, poll_args=args, max_choices=max_choices, deadline=deadline ) @@ -755,7 +754,7 @@ async def quick(self, ctx: Context, *, args: str): owner=ctx.author.id, title=question, end=deadline, - anonymous=anonymous, + anonymous=await PollsDefaultSettings.anonymous.get(), can_delete=True, options=parsed_options, poll_type=PollType.STANDARD, From f6e5b427820828698a622278bf69da0042b14a0a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 29 Jun 2022 22:58:05 +0200 Subject: [PATCH 59/95] Added first view for pie-chart --- general/polls/cog.py | 26 ++++++++++++++++++++++++++ general/polls/translations/en.yml | 2 ++ 2 files changed, 28 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index 580b10385..3d1d08ac7 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -3,6 +3,8 @@ from datetime import datetime from typing import Optional, Union +import matplotlib.pyplot as plt +import numpy as np from dateutil.relativedelta import relativedelta from discord import ( Embed, @@ -356,6 +358,21 @@ async def status_change(bot: Bot, poll: Poll): await embed_message.edit(embed=embed) +def show_results(poll: Poll): + data: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options] + if not any(data): + raise CommandError(t.error.no_votes) + data_tuple: list[tuple[int, float]] = [(i + 1, num) for i, num in enumerate(data) if num] + data_tuple.sort(key=lambda x: x[1]) + data_tuple = data_tuple[:10] + data_np = np.array([value for _, value in data_tuple]) + + plt.pie(data_np, autopct="%1.1f%%", startangle=0) + + plt.legend(bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data]) + plt.show() + + class MySelect(Select): """adds a method for handling interactions with the select menu""" @@ -549,6 +566,15 @@ async def voted(self, ctx: Context, message: Message): await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True) + @poll.command(name="results", aliases=["res"]) + @docs(t.commands.poll.result) + async def result(self, ctx: Context, message: Message): + poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + if not poll: + raise CommandError(t.error.not_poll) + + show_results(poll) + @poll.command(name="activate", aliases=["a"]) @docs(t.commands.poll.activate) async def activate(self, ctx: Context, message: Message): diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 0a3d13dfc..20bef3b34 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -7,6 +7,7 @@ commands: activate: activate a paused poll paused: pause an active poll voted: show who voted on the poll (only works if not anonymous or if the poll-owner uses the command) + result: see pie chart with results from a poll list: show the active polls settings: settings: poll settings @@ -38,6 +39,7 @@ error: not_poll: Mesage doesn't contains a poll no_teamlers: No user with team-role found! no_polls: No current {} {}-polls. + no_votes: No votes on this poll poll_cant_be_used: Poll can't be used because it's {} cant_pin: title: Error From b142f85769a0f0323c1c49c21f89e3ddcacf1298 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Jun 2022 17:39:59 +0200 Subject: [PATCH 60/95] Added function for sending a pi-chart for the first 10 votes --- general/polls/cog.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 3d1d08ac7..e546ca750 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -1,6 +1,7 @@ import string from argparse import ArgumentParser, Namespace from datetime import datetime +from io import BytesIO from typing import Optional, Union import matplotlib.pyplot as plt @@ -8,6 +9,7 @@ from dateutil.relativedelta import relativedelta from discord import ( Embed, + File, Forbidden, Guild, HTTPException, @@ -358,7 +360,7 @@ async def status_change(bot: Bot, poll: Poll): await embed_message.edit(embed=embed) -def show_results(poll: Poll): +def show_results(poll: Poll) -> tuple[Embed, File]: data: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options] if not any(data): raise CommandError(t.error.no_votes) @@ -369,8 +371,17 @@ def show_results(poll: Poll): plt.pie(data_np, autopct="%1.1f%%", startangle=0) - plt.legend(bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data]) - plt.show() + plt.legend(bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple]) + buf = BytesIO() + plt.savefig(buf, format="png", transparent=True) + buf.seek(0) + + file = File(filename="poll_result.png", fp=buf) + + embed = Embed(title="Poll results", description="Top 10 votes on the question xyz") + embed.set_image(url="attachment://poll_result.png") + + return embed, file class MySelect(Select): @@ -573,7 +584,9 @@ async def result(self, ctx: Context, message: Message): if not poll: raise CommandError(t.error.not_poll) - show_results(poll) + embed, file = show_results(poll) + + await send_long_embed(ctx, embed=embed, file=file) @poll.command(name="activate", aliases=["a"]) @docs(t.commands.poll.activate) From eb03b6ec6cf9c59767244f2428df54c36b87a37e Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Jun 2022 17:46:40 +0200 Subject: [PATCH 61/95] Improved embed --- general/polls/cog.py | 6 +++++- general/polls/translations/en.yml | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index e546ca750..6b445b4f8 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -378,7 +378,11 @@ def show_results(poll: Poll) -> tuple[Embed, File]: file = File(filename="poll_result.png", fp=buf) - embed = Embed(title="Poll results", description="Top 10 votes on the question xyz") + embed = Embed( + title=t.results.results, + description=t.results.desc(10 if len(data_tuple) >= 10 else len(data_tuple)), + color=Colors.Polls, + ) embed.set_image(url="attachment://poll_result.png") return embed, file diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 20bef3b34..a18f80c19 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -176,6 +176,10 @@ wizard: --> Creates an anonymous, 6 hours long poll with 4 select choices for every user +results: + results: "Poll results:" + desc: "Top {} votes on the poll: `{}`" + poll: Poll team_poll: Team Poll poll_voted: "Vote was added to the poll" From 4a53d81cb17571a2951d4c8a06b9c4a6d7ea4ece Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Jun 2022 18:11:49 +0200 Subject: [PATCH 62/95] Improved pie-chart --- general/polls/cog.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 6b445b4f8..a8fb1e981 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -369,8 +369,7 @@ def show_results(poll: Poll) -> tuple[Embed, File]: data_tuple = data_tuple[:10] data_np = np.array([value for _, value in data_tuple]) - plt.pie(data_np, autopct="%1.1f%%", startangle=0) - + plt.pie(data_np, autopct="%1.1f%%", startangle=90, counterclock=False, shadow=True) plt.legend(bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple]) buf = BytesIO() plt.savefig(buf, format="png", transparent=True) @@ -380,7 +379,7 @@ def show_results(poll: Poll) -> tuple[Embed, File]: embed = Embed( title=t.results.results, - description=t.results.desc(10 if len(data_tuple) >= 10 else len(data_tuple)), + description=t.results.desc(10 if len(data_tuple) >= 10 else len(data_tuple), poll.title), color=Colors.Polls, ) embed.set_image(url="attachment://poll_result.png") From 18f41d8f3c238cc21b770d98a2055f7bd6065db4 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Jun 2022 18:19:39 +0200 Subject: [PATCH 63/95] Fixed plt error, recognizing the last chart --- general/polls/cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index a8fb1e981..9ab823c43 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -373,6 +373,7 @@ def show_results(poll: Poll) -> tuple[Embed, File]: plt.legend(bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple]) buf = BytesIO() plt.savefig(buf, format="png", transparent=True) + plt.clf() buf.seek(0) file = File(filename="poll_result.png", fp=buf) From 581495b099a3eeeb039d60ba623f7da04113276d Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Jun 2022 21:56:10 +0200 Subject: [PATCH 64/95] Improved pie-chard (donut) --- general/polls/cog.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 9ab823c43..dd13aac37 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -364,23 +364,35 @@ def show_results(poll: Poll) -> tuple[Embed, File]: data: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options] if not any(data): raise CommandError(t.error.no_votes) - data_tuple: list[tuple[int, float]] = [(i + 1, num) for i, num in enumerate(data) if num] + data_tuple: list[tuple[int | str, float]] = [(i + 1, num) for i, num in enumerate(data) if num] data_tuple.sort(key=lambda x: x[1]) - data_tuple = data_tuple[:10] + data_tuple, other = data_tuple[:7], data_tuple[7:] + rest: tuple[str, float] = ("Other", sum([i for _, i in other])) + data_tuple.append(rest) data_np = np.array([value for _, value in data_tuple]) - plt.pie(data_np, autopct="%1.1f%%", startangle=90, counterclock=False, shadow=True) - plt.legend(bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple]) - buf = BytesIO() - plt.savefig(buf, format="png", transparent=True) - plt.clf() - buf.seek(0) + cc = plt.cycler("color", plt.cm.nipy_spectral(np.linspace(0.1, 0.9, len(data_np)))) + explode = [len(data_tuple) / 30 for _ in data_tuple] + with plt.style.context({"axes.prop_cycle": cc}): + fig1, ax1 = plt.subplots() + ax1.axis("equal") + pie, *_ = ax1.pie( + data_np, autopct="%1.1f%%", startangle=90, counterclock=False, shadow=True, pctdistance=0.8, explode=explode + ) + plt.setp(pie, width=0.5) + plt.legend( + bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple] + ) + buf = BytesIO() + fig1.savefig(buf, format="png", transparent=True) + plt.clf() + buf.seek(0) file = File(filename="poll_result.png", fp=buf) embed = Embed( title=t.results.results, - description=t.results.desc(10 if len(data_tuple) >= 10 else len(data_tuple), poll.title), + description=t.results.desc(7 if len(data_tuple) >= 7 else len(data_tuple) - 1, poll.title), color=Colors.Polls, ) embed.set_image(url="attachment://poll_result.png") From 35bebe9bce3691b88373a094b424ce936e2aa893 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Jun 2022 22:10:52 +0200 Subject: [PATCH 65/95] Changed donut-color --- general/polls/cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index dd13aac37..eadd2cfb5 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -371,7 +371,7 @@ def show_results(poll: Poll) -> tuple[Embed, File]: data_tuple.append(rest) data_np = np.array([value for _, value in data_tuple]) - cc = plt.cycler("color", plt.cm.nipy_spectral(np.linspace(0.1, 0.9, len(data_np)))) + cc = plt.cycler("color", plt.cm.bwr(np.linspace(0.1, 0.9, len(data_np)))) explode = [len(data_tuple) / 30 for _ in data_tuple] with plt.style.context({"axes.prop_cycle": cc}): fig1, ax1 = plt.subplots() From bf05bd7e2728c85587b70eede4aa7b819b8ac5d7 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 30 Jun 2022 22:21:05 +0200 Subject: [PATCH 66/95] Changed donut-color --- general/polls/cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index eadd2cfb5..a9ea57952 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -371,7 +371,7 @@ def show_results(poll: Poll) -> tuple[Embed, File]: data_tuple.append(rest) data_np = np.array([value for _, value in data_tuple]) - cc = plt.cycler("color", plt.cm.bwr(np.linspace(0.1, 0.9, len(data_np)))) + cc = plt.cycler("color", plt.cm.bwr(np.linspace(0.9, 0.2, len(data_np)))) explode = [len(data_tuple) / 30 for _ in data_tuple] with plt.style.context({"axes.prop_cycle": cc}): fig1, ax1 = plt.subplots() From fdebc0fad6cfcd18d4b503cab47dab4735fc3b91 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 1 Jul 2022 17:57:44 +0200 Subject: [PATCH 67/95] Changed donut, now showing up to 25 pieces --- general/polls/cog.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index a9ea57952..7d1f88b4b 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -366,25 +366,23 @@ def show_results(poll: Poll) -> tuple[Embed, File]: raise CommandError(t.error.no_votes) data_tuple: list[tuple[int | str, float]] = [(i + 1, num) for i, num in enumerate(data) if num] data_tuple.sort(key=lambda x: x[1]) - data_tuple, other = data_tuple[:7], data_tuple[7:] - rest: tuple[str, float] = ("Other", sum([i for _, i in other])) - data_tuple.append(rest) data_np = np.array([value for _, value in data_tuple]) - cc = plt.cycler("color", plt.cm.bwr(np.linspace(0.9, 0.2, len(data_np)))) - explode = [len(data_tuple) / 30 for _ in data_tuple] + cc = plt.cycler("color", plt.cm.Spectral(np.linspace(1, 0, len(data_np)))) + explode = [len(data_tuple) / 40 for _ in data_tuple] with plt.style.context({"axes.prop_cycle": cc}): fig1, ax1 = plt.subplots() ax1.axis("equal") pie, *_ = ax1.pie( - data_np, autopct="%1.1f%%", startangle=90, counterclock=False, shadow=True, pctdistance=0.8, explode=explode + data_np, autopct="%1.1f%%", startangle=90, counterclock=False, pctdistance=0.8, explode=explode ) plt.setp(pie, width=0.5) plt.legend( bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple] ) buf = BytesIO() - fig1.savefig(buf, format="png", transparent=True) + fig1.set_size_inches(11.1, 6.3) + fig1.savefig(buf, format="png", transparent=True, dpi=300) plt.clf() buf.seek(0) From 2d07c6829d33741ba297c8a86919ee590853b42a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 1 Jul 2022 18:11:57 +0200 Subject: [PATCH 68/95] Added title into picture --- general/polls/cog.py | 7 ++----- general/polls/translations/en.yml | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 7d1f88b4b..c7dd658b5 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -380,6 +380,7 @@ def show_results(poll: Poll) -> tuple[Embed, File]: plt.legend( bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple] ) + plt.title(poll.title, fontdict={"fontsize": 20, "color": "#FFFFFF"}) buf = BytesIO() fig1.set_size_inches(11.1, 6.3) fig1.savefig(buf, format="png", transparent=True, dpi=300) @@ -388,11 +389,7 @@ def show_results(poll: Poll) -> tuple[Embed, File]: file = File(filename="poll_result.png", fp=buf) - embed = Embed( - title=t.results.results, - description=t.results.desc(7 if len(data_tuple) >= 7 else len(data_tuple) - 1, poll.title), - color=Colors.Polls, - ) + embed = Embed(title=t.results.results, color=Colors.Polls) embed.set_image(url="attachment://poll_result.png") return embed, file diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index a18f80c19..55cdc900e 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -178,7 +178,6 @@ wizard: results: results: "Poll results:" - desc: "Top {} votes on the poll: `{}`" poll: Poll team_poll: Team Poll From 07d1ccd117cc6a9ca19619eeb63d9e34a3fa3d4a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 1 Jul 2022 22:05:50 +0200 Subject: [PATCH 69/95] Changed style of donut --- general/polls/cog.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index c7dd658b5..7421a9298 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -360,7 +360,9 @@ async def status_change(bot: Bot, poll: Poll): await embed_message.edit(embed=embed) -def show_results(poll: Poll) -> tuple[Embed, File]: +def show_results( + poll: Poll, +) -> tuple[Embed, File]: # style is good for now, if you don't like it, change it by yourself data: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options] if not any(data): raise CommandError(t.error.no_votes) @@ -368,22 +370,20 @@ def show_results(poll: Poll) -> tuple[Embed, File]: data_tuple.sort(key=lambda x: x[1]) data_np = np.array([value for _, value in data_tuple]) - cc = plt.cycler("color", plt.cm.Spectral(np.linspace(1, 0, len(data_np)))) - explode = [len(data_tuple) / 40 for _ in data_tuple] + cc = plt.cycler("color", plt.cm.rainbow(np.linspace(1, 0, len(data_np)))) + explode = [len(data_tuple) / 50 for _ in data_tuple] with plt.style.context({"axes.prop_cycle": cc}): fig1, ax1 = plt.subplots() ax1.axis("equal") - pie, *_ = ax1.pie( - data_np, autopct="%1.1f%%", startangle=90, counterclock=False, pctdistance=0.8, explode=explode - ) + pie, *_ = ax1.pie(data_np, autopct="%1.1f%%", startangle=90, pctdistance=0.8, explode=explode) plt.setp(pie, width=0.5) plt.legend( bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple] ) - plt.title(poll.title, fontdict={"fontsize": 20, "color": "#FFFFFF"}) + plt.title(poll.title, fontdict={"fontsize": 20, "color": "#FFFFFF"}, pad=50) buf = BytesIO() - fig1.set_size_inches(11.1, 6.3) - fig1.savefig(buf, format="png", transparent=True, dpi=300) + fig1.set_size_inches(11.1, 8) + fig1.savefig(buf, format="png", transparent=True, dpi=400) plt.clf() buf.seek(0) From 2668ad3320f4c4193d9c51c70396fc454b828c28 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 1 Jul 2022 23:39:50 +0200 Subject: [PATCH 70/95] Removed get_percentage function + removed the percentage from options --- general/polls/cog.py | 21 +++++++-------------- general/polls/translations/en.yml | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 7421a9298..a182d33cd 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -84,13 +84,6 @@ def create_select_view(select_obj: Select, timeout: float = None) -> View: return view -def get_percentage(poll: Poll) -> list[tuple[float, float]]: - """returns the amount of votes and the percentage of an option""" - values: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options] - - return [(float(value), float(round(((value / sum(values)) * 100), 2))) for value in values] - - def build_wizard(skip: bool = False) -> Embed: """creates a help embed for setting up advanced polls""" if skip: @@ -160,8 +153,8 @@ async def send_poll( if len({option.emoji for option in options}) < len(options): raise CommandError(t.option_duplicated) - for option in options: - embed.add_field(name=t.option.field.name(0, 0), value=str(option), inline=False) + for i, option in enumerate(options): + embed.add_field(name=t.option.field.name(i + 1), value=str(option), inline=False) if field: embed.add_field(name=field[0], value=field[1], inline=False) @@ -203,7 +196,6 @@ async def send_poll( async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed: """edits the poll embed, updating the votes and percentages""" - calc = get_percentage(poll) for index, field in enumerate(embed.fields): if field.name == tg.status: missing.sort(key=lambda m: str(m).lower()) @@ -215,9 +207,7 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1), ) else: - weight: float | int = calc[index][0] if not calc[index][0].is_integer() else int(calc[index][0]) - percentage: float | int = calc[index][1] if not calc[index][1].is_integer() else int(calc[index][1]) - embed.set_field_at(index, name=t.option.field.name(weight, percentage), value=field.value, inline=False) + embed.set_field_at(index, name=t.option.field.name(index + 1), value=field.value, inline=False) embed.set_footer(text=t.footer(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) return embed @@ -378,7 +368,10 @@ def show_results( pie, *_ = ax1.pie(data_np, autopct="%1.1f%%", startangle=90, pctdistance=0.8, explode=explode) plt.setp(pie, width=0.5) plt.legend( - bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[str(i) for i, _ in data_tuple] + bbox_to_anchor=(1.1, 1.1), + loc="upper right", + borderaxespad=0, + labels=[f"Option: {i}" for i, _ in data_tuple], ) plt.title(poll.title, fontdict={"fontsize": 20, "color": "#FFFFFF"}, pad=50) buf = BytesIO() diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 55cdc900e..abb514acc 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -141,7 +141,7 @@ yes_no: option: field: - name: "**Votes: {} ({}%)**" + name: "Option {}:" skip: message: skip From 29f9671d0e1ae487e98bbe43ea15d53bafa92628 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 2 Jul 2022 11:56:25 +0200 Subject: [PATCH 71/95] Added check if poll is still running before showing the result --- general/polls/cog.py | 4 ++++ general/polls/translations/en.yml | 1 + 2 files changed, 5 insertions(+) diff --git a/general/polls/cog.py b/general/polls/cog.py index a182d33cd..21ae31319 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -585,6 +585,10 @@ async def voted(self, ctx: Context, message: Message): @docs(t.commands.poll.result) async def result(self, ctx: Context, message: Message): poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + if poll.status == PollStatus.ACTIVE and not ( + poll.owner_id == ctx.author.id or await PollsPermission.manage.check_permissions(ctx.author) + ): + raise CommandError(t.error.still_active) if not poll: raise CommandError(t.error.not_poll) diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index abb514acc..fc9c1a56a 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -41,6 +41,7 @@ error: no_polls: No current {} {}-polls. no_votes: No votes on this poll poll_cant_be_used: Poll can't be used because it's {} + still active: "This poll is still open, please wait until it's closed!" cant_pin: title: Error description: Can't pin any more messages in {} From 893ea4d3e612e07fde794413e0de7d61d29b579e Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 2 Jul 2022 12:03:41 +0200 Subject: [PATCH 72/95] Sorted imports From 95f9a02955d9bfd4cd0f3a813f01e4121c7a0fbe Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 2 Jul 2022 12:18:12 +0200 Subject: [PATCH 73/95] Adeed TODO's + fixed settings for team_yn --- general/polls/cog.py | 21 +++++++++++++-------- general/polls/settings.py | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 21ae31319..7f70b9bb3 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -37,7 +37,7 @@ from .colors import Colors from .models import Option, Poll, PollStatus, PollType, PollVote, RoleWeight, sync_redis from .permissions import PollsPermission -from .settings import PollsDefaultSettings +from .settings import PollsDefaultSettings, PollsTeamsSettings from ...contributor import Contributor from ...pubsub import send_alert, send_to_changelog @@ -761,18 +761,18 @@ async def team(self, ctx: Context): if not ctx.subcommand_passed: raise UserInputError - @team.group(name="settings", aliases=["s"]) + @team.group(name="settings", aliases=["s"]) # TODO: function for team-poll settings @PollsPermission.read.check @docs(t.commands.poll.team.settings.settings) async def tp_settings(self, ctx: Context): pass - @team.command(name="unpin", aliases=["u"]) + @team.command(name="unpin", aliases=["u"]) # TODO: function for unpinning polls @docs(t.commands.poll.team.unpin) async def unpin(self, ctx: Context, message: Message): pass - @team.command(name="new", aliases=["n"]) + @team.command(name="new", aliases=["n"]) # TODO: new team-polls @docs(t.commands.poll.team.new) async def team_new(self, ctx: Context, *, args: str): pass @@ -784,6 +784,11 @@ async def list(self, ctx: Context): await send_long_embed(ctx, embed=embed, paginate=True) + @team.command(name="ignore", aliases=["i"]) # TODO: ignore user from being pinged by team-polls + @docs(t.commands.poll.team.ignore) + async def ignore(self, ctx: Context, member: Member): + pass + @poll.command(name="quick", usage=t.usage.poll, aliases=["q"]) @docs(t.commands.poll.quick) async def quick(self, ctx: Context, *, args: str): @@ -912,7 +917,7 @@ async def team_yesno(self, ctx: Context, *, text: str): max_choices=1, poll_args=options, field=field, - deadline=await PollsDefaultSettings.max_duration.get() * 24, + deadline=await PollsTeamsSettings.duration.get() * 24, ) await Poll.create( message_id=message.id, @@ -921,13 +926,13 @@ async def team_yesno(self, ctx: Context, *, text: str): channel=message.channel.id, owner=ctx.author.id, title=question, - end=await PollsDefaultSettings.max_duration.get() * 24, - anonymous=False, + end=await PollsTeamsSettings.duration.get() * 24, + anonymous=False, # TODO: make optional depending on settings can_delete=False, options=parsed_options, poll_type=PollType.TEAM, interaction=interaction.id, - fair=True, + fair=True, # TODO: only fair if no weight is listed for team-polls max_choices=1, thread=thread_id, ) diff --git a/general/polls/settings.py b/general/polls/settings.py index a49b38079..194a55a44 100644 --- a/general/polls/settings.py +++ b/general/polls/settings.py @@ -11,4 +11,4 @@ class PollsDefaultSettings(Settings): class PollsTeamsSettings(Settings): - duration = 0 + duration = 1 # days after which all missing team-members should be pinged, if not excluded From 80b3c8be6ac6826d5caac56b9995f96843acbfd1 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 2 Jul 2022 16:15:26 +0200 Subject: [PATCH 74/95] Added ignore command for members on team-polls --- general/polls/cog.py | 43 ++++++++++++++++++++++++------- general/polls/models.py | 22 ++++++++++++++++ general/polls/translations/en.yml | 11 ++++++++ 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 7f70b9bb3..068e9021c 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -35,7 +35,7 @@ from PyDrocsid.util import is_teamler from .colors import Colors -from .models import Option, Poll, PollStatus, PollType, PollVote, RoleWeight, sync_redis +from .models import IgnoredUser, Option, Poll, PollStatus, PollType, PollVote, RoleWeight, sync_redis from .permissions import PollsPermission from .settings import PollsDefaultSettings, PollsTeamsSettings from ...contributor import Contributor @@ -238,10 +238,12 @@ async def notify_missing_staff(bot: Bot, poll: Poll): await thread.send(embed=Embed(title=t.error.no_teamlers, color=Colors.error)) return + ignored_ids: list[int] = [i.id for i in await IgnoredUser.get(poll.guild_id)] user_ids: set[int] = set() for option in poll.options: for vote in option.votes: - user_ids.add(vote.user_id) + if vote.user_id not in ignored_ids: + user_ids.add(vote.user_id) missing: list[Member] | None = [teamler for teamler in teamlers if teamler.id not in user_ids] missing.sort(key=lambda m: str(m).lower()) @@ -765,9 +767,37 @@ async def team(self, ctx: Context): @PollsPermission.read.check @docs(t.commands.poll.team.settings.settings) async def tp_settings(self, ctx: Context): - pass + if ctx.subcommand_passed is not None: + if ctx.invoked_subcommand is None: + raise UserInputError + return + + ignored: list[IgnoredUser] = await IgnoredUser.get(ctx.guild.id) + ignore = "None" if not ignored else " ".join(f"<@{i.member_id}>" for i in ignored) + + embed = Embed(title=t.team_settings.title, color=Colors.Polls) + embed.add_field(name=t.team_settings.ignore.name, value=ignore) + + await send_long_embed(ctx, embed=embed) + + @tp_settings.command(name="ignore", aliases=["i"]) + @PollsPermission.write.check + @docs(t.commands.poll.team.settings.ignore) + async def ignore(self, ctx: Context, member: Member): + user: IgnoredUser = await db.get(IgnoredUser, member_id=member.id, guild_id=ctx.guild.id) + + if user: + await user.remove() + desc = t.ignore.removed(member.id) + else: + await IgnoredUser.create(ctx.guild.id, member.id) + desc = t.ignore.added(member.id) + + await send_to_changelog(ctx.guild, desc) + await add_reactions(ctx.message, "white_check_mark") @team.command(name="unpin", aliases=["u"]) # TODO: function for unpinning polls + @PollsPermission.manage.check @docs(t.commands.poll.team.unpin) async def unpin(self, ctx: Context, message: Message): pass @@ -784,11 +814,6 @@ async def list(self, ctx: Context): await send_long_embed(ctx, embed=embed, paginate=True) - @team.command(name="ignore", aliases=["i"]) # TODO: ignore user from being pinged by team-polls - @docs(t.commands.poll.team.ignore) - async def ignore(self, ctx: Context, member: Member): - pass - @poll.command(name="quick", usage=t.usage.poll, aliases=["q"]) @docs(t.commands.poll.quick) async def quick(self, ctx: Context, *, args: str): @@ -898,7 +923,7 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt except Forbidden: pass - @team.command(name="yes_no", aliases=["yn"]) + @commands.command(name="team_yes_no", aliases=["tyn"]) @PollsPermission.team_poll.check @guild_only() @docs(t.commands.team_yes_no) diff --git a/general/polls/models.py b/general/polls/models.py index 2c54f76bd..11304946a 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -192,3 +192,25 @@ async def get_highest(user_roles: list[Role]) -> float: weight = _weight return weight + + +class IgnoredUser(Base): + __tablename__ = "ignored_by_poll_ping" + + id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True) + guild_id: Union[Column, int] = Column(BigInteger) + member_id: Union[Column, int] = Column(BigInteger, unique=True) + timestamp: Union[Column, datetime] = Column(UTCDateTime) + + @staticmethod + async def create(guild_id: int, member_id: int): + ignored_user = IgnoredUser(guild_id=guild_id, member_id=member_id) + await db.add(ignored_user) + return ignored_user + + async def remove(self) -> None: + await db.delete(self) + + @staticmethod + async def get(guild_id: int) -> list[IgnoredUser]: + return await db.all(filter_by(IgnoredUser, guild_id=guild_id)) diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index fc9c1a56a..952e99819 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -24,6 +24,7 @@ commands: list: show all open team-polls settings: settings: manage team-poll settings + ignore: ignore a member from being pinged, can be reverted by executing again yes_no: add thumbs-up/down emotes on a message team_yes_no: starts a yes/no poll and shows, which teamler has not voted yet. @@ -180,6 +181,16 @@ wizard: results: results: "Poll results:" +ignore: + title: Ignore + removed: "<@{}> was removed from the list of ignored member" + added: "<@{}> was added on the list of ignored member" + +team_settings: + title: Team poll settings + ignore: + name: Ignored Members + poll: Poll team_poll: Team Poll poll_voted: "Vote was added to the poll" From 6ec4b6b976730827acc30cdf3718561a8f699342 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 2 Jul 2022 17:26:15 +0200 Subject: [PATCH 75/95] Rewrote en.yml for better reading --- general/polls/cog.py | 108 +++++++++-------- general/polls/translations/en.yml | 195 ++++++++++++++---------------- 2 files changed, 143 insertions(+), 160 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 068e9021c..b1b898ec5 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -56,7 +56,7 @@ class PollOption: async def init(self, ctx: Context, line: str, number: int): if not line: - raise CommandError(t.empty_option) + raise CommandError(t.error.empty_option) emoji_candidate, *option = line.split() option = " ".join(option) @@ -87,10 +87,10 @@ def create_select_view(select_obj: Select, timeout: float = None) -> View: def build_wizard(skip: bool = False) -> Embed: """creates a help embed for setting up advanced polls""" if skip: - return Embed(title=t.skip.title, description=t.skip.description, color=Colors.Polls) + return Embed(title=t.wizard.skip.skipped.title, description=t.wizard.skip.skipped.desc, color=Colors.Polls) - embed = Embed(title=t.wizard.title, description=t.wizard.description, color=Colors.Polls) - embed.add_field(name=t.wizard.arg, value=t.wizard.args, inline=False) + embed = Embed(title=t.wizard.title, description=t.wizard.desc, color=Colors.Polls) + embed.add_field(name=t.wizard.args.name, value=t.wizard.args.value, inline=False) embed.add_field(name=t.wizard.example.name, value=t.wizard.example.value, inline=False) embed.add_field(name=t.wizard.skip.name, value=t.wizard.skip.value, inline=False) @@ -133,38 +133,38 @@ async def send_poll( question, *options = [line.replace("\x00", "\n") for line in poll_args.replace("\\\n", "\x00").split("\n") if line] if not options: - raise CommandError(t.missing_options) + raise CommandError(t.error.missing_options) if len(options) > MAX_OPTIONS: - raise CommandError(t.too_many_options(MAX_OPTIONS)) + raise CommandError(t.error.too_many_options(MAX_OPTIONS)) if field and len(options) >= MAX_OPTIONS: - raise CommandError(t.too_many_options(MAX_OPTIONS - 1)) + raise CommandError(t.error.too_many_options(MAX_OPTIONS - 1)) options = [await PollOption().init(ctx, line, i) for i, line in enumerate(options)] if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options): - raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE)) + raise CommandError(t.error.option_too_long(EmbedLimits.FIELD_VALUE)) embed = Embed(title=title, description=question, color=Colors.Polls, timestamp=utcnow()) embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url) if deadline: - embed.set_footer(text=t.footer(calc_end_time(deadline).strftime("%Y-%m-%d %H:%M"))) + embed.set_footer(text=t.poll.footer.default(calc_end_time(deadline).strftime("%Y-%m-%d %H:%M"))) if len({option.emoji for option in options}) < len(options): - raise CommandError(t.option_duplicated) + raise CommandError(t.error.option_duplicated) for i, option in enumerate(options): - embed.add_field(name=t.option.field.name(i + 1), value=str(option), inline=False) + embed.add_field(name=t.error.option.field.name(i + 1), value=str(option), inline=False) if field: embed.add_field(name=field[0], value=field[1], inline=False) if not max_choices or isinstance(max_choices, str): - place = t.select.place + place = t.poll.select.place max_value = len(options) else: options_amount = len(options) if max_choices >= len(options) else max_choices - place: str = t.select.placeholder(cnt=options_amount) + place: str = t.poll.select.placeholder(cnt=options_amount) max_value = options_amount msg = await ctx.send(embed=embed) @@ -173,14 +173,16 @@ async def send_poll( placeholder=place, max_values=max_value, options=[ - SelectOption(label=t.select.label(index + 1), emoji=option.emoji, description=option.option) + SelectOption(label=t.poll.select.label(index + 1), emoji=option.emoji, description=option.option) for index, option in enumerate(options) ], ) view_msg = await ctx.send(view=create_select_view(select_obj=select_obj)) thread = await msg.create_thread(name=question) - parsed_options: list[tuple[str, str]] = [(obj.emoji, t.select.label(ix)) for ix, obj in enumerate(options, start=1)] + parsed_options: list[tuple[str, str]] = [ + (obj.emoji, t.poll.select.label(ix)) for ix, obj in enumerate(options, start=1) + ] try: await msg.pin() @@ -204,11 +206,11 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None embed.set_field_at( index, name=field.name, - value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1), + value=t.error.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1), ) else: - embed.set_field_at(index, name=t.option.field.name(index + 1), value=field.value, inline=False) - embed.set_footer(text=t.footer(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) + embed.set_field_at(index, name=t.poll.option(index + 1), value=field.value, inline=False) + embed.set_footer(text=t.default.footer(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) return embed @@ -249,7 +251,7 @@ async def notify_missing_staff(bot: Bot, poll: Poll): missing.sort(key=lambda m: str(m).lower()) desc = " ".join(f"<@{user}>" for user in missing) - await thread.send(t.team_poll_missing(desc)) + await thread.send(t.error.team_poll_missing(desc)) async def handle_deleted_messages(bot, message_id: int): @@ -299,7 +301,7 @@ async def close_poll(bot, poll: Poll): await interaction_message.delete() embed = embed_message.embeds[0] - embed.set_footer(text=t.footer_closed) + embed.set_footer(text=t.poll.footer.closed) await embed_message.edit(embed=embed) await embed_message.unpin() @@ -340,12 +342,12 @@ async def status_change(bot: Bot, poll: Poll): embed = embed_message.embeds[0] if poll.status == PollStatus.ACTIVE: poll.status = PollStatus.PAUSED - embed.set_footer(text=t.footer_paused) + embed.set_footer(text=t.poll.footer.paused) embed.colour = Colors.grey await embed_message.unpin() else: poll.status = PollStatus.ACTIVE - embed.set_footer(text=t.footer(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) + embed.set_footer(text=t.poll.footer.default(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) embed.colour = Colors.Polls await embed_message.pin() @@ -384,7 +386,7 @@ def show_results( file = File(filename="poll_result.png", fp=buf) - embed = Embed(title=t.results.results, color=Colors.Polls) + embed = Embed(title=t.poll.results, color=Colors.Polls) embed.set_image(url="attachment://poll_result.png") return embed, file @@ -439,7 +441,7 @@ async def callback(self, interaction): await interaction.response.send_message(content=t.error.no_teamlers, ephemeral=True) return if user not in teamlers: - await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True) + await interaction.response.send_message(content=t.error.teampoll_forbidden, ephemeral=True) return user_ids: set[int] = set() @@ -452,7 +454,7 @@ async def callback(self, interaction): embed = await edit_poll_embed(embed, poll, missing) await message.edit(embed=embed) - await interaction.response.send_message(content=t.poll_voted, ephemeral=True) + await interaction.response.send_message(content=t.poll.voted, ephemeral=True) class PollsCog(Cog, name="Polls"): @@ -474,11 +476,11 @@ async def on_ready(self): if await check_poll_time(poll): select_obj = MySelect( custom_id=str(poll.message_id), - placeholder=t.select.placeholder(cnt=poll.max_choices), + placeholder=t.poll.select.placeholder(cnt=poll.max_choices), max_values=poll.max_choices, options=[ SelectOption( - label=t.select.label(option.field_position + 1), + label=t.poll.select.label(option.field_position + 1), emoji=option.emote, description=option.option, ) @@ -541,7 +543,7 @@ async def delete(self, ctx: Context, message: Message): elif not poll.can_delete and not poll.owner_id == ctx.author.id: raise PermissionError # if delete is False, only the owner can delete it - if not await Confirmation().run(ctx, t.delete.confirm_text): + if not await Confirmation().run(ctx, t.texts.delete.confirm): return await message.delete() @@ -578,8 +580,8 @@ async def voted(self, ctx: Context, message: Message): description = "" for key, value in users.items(): - description += t.voted.row(key, value) - embed = Embed(title=t.voted.title, description=description, color=Colors.Polls) + description += t.texts.voted.row(key, value) + embed = Embed(title=t.texts.voted.title, description=description, color=Colors.Polls) await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True) @@ -608,10 +610,10 @@ async def activate(self, ctx: Context, message: Message): raise PermissionError if poll.status == PollStatus.ACTIVE: - raise CommandError(t.poll_status_not_changed(poll.status.value)) + raise CommandError(t.error.poll_status_not_changed(poll.status.value)) await status_change(self.bot, poll) - await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed(poll.status.value))) + await send_long_embed(ctx, embed=Embed(title=t.poll.status_changed(poll.status.value))) @poll.command(name="pause", aliases=["p", "deactivate", "disable"]) @docs(t.commands.poll.paused) @@ -623,10 +625,10 @@ async def pause(self, ctx: Context, message: Message): raise PermissionError if poll.status == PollStatus.PAUSED: - raise CommandError(t.poll_status_not_changed(poll.status.value)) + raise CommandError(t.error.poll_status_not_changed(poll.status.value)) await status_change(self.bot, poll) - await send_long_embed(ctx, embed=Embed(title=t.poll_status_changed(poll.status.value))) + await send_long_embed(ctx, embed=Embed(title=t.poll.status_changed(poll.status.value))) @poll.group(name="settings", aliases=["s"]) @PollsPermission.read.check @@ -683,13 +685,13 @@ async def roles_weights(self, ctx: Context, role: Role, weight: float | None = N if element and weight: element.weight = weight - msg: str = t.role_weight.set(role.id, weight) + msg: str = t.texts.role_weight.set(role.id, weight) elif weight and not element: await RoleWeight.create(ctx.guild.id, role.id, weight, PollType.STANDARD) - msg: str = t.role_weight.set(role.id, weight) + msg: str = t.texts.role_weight.set(role.id, weight) else: await element.remove() - msg: str = t.role_weight.reset(role.id) + msg: str = t.texts.role_weight.reset(role.id) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) @@ -700,9 +702,9 @@ async def roles_weights(self, ctx: Context, role: Role, weight: float | None = N async def duration(self, ctx: Context, hours: int | None = None): if not hours: hours = 0 - msg: str = t.duration.reset() + msg: str = t.texts.duration.reset() else: - msg: str = t.duration.set(cnt=hours) + msg: str = t.texts.duration.set(cnt=hours) await PollsDefaultSettings.duration.set(hours) await add_reactions(ctx.message, "white_check_mark") @@ -713,7 +715,7 @@ async def duration(self, ctx: Context, hours: int | None = None): @docs(t.commands.poll.settings.max_duration) async def max_duration(self, ctx: Context, days: int | None = None): days = days or 7 - msg: str = t.max_duration.set(cnt=days) + msg: str = t.texts.max_duration.set(cnt=days) await PollsDefaultSettings.max_duration.set(days) await add_reactions(ctx.message, "white_check_mark") @@ -725,9 +727,9 @@ async def max_duration(self, ctx: Context, days: int | None = None): async def votes(self, ctx: Context, votes: int | None = None): if not votes: votes = 0 - msg: str = t.votes.reset + msg: str = t.texts.votes.reset else: - msg: str = t.votes.set(cnt=votes) + msg: str = t.texts.votes.set(cnt=votes) if not 0 < votes < MAX_OPTIONS: votes = 0 @@ -740,7 +742,7 @@ async def votes(self, ctx: Context, votes: int | None = None): @PollsPermission.write.check @docs(t.commands.poll.settings.anonymous) async def anonymous(self, ctx: Context, status: bool): - msg: str = t.anonymous.is_on if status else t.anonymous.is_off + msg: str = t.texts.anonymous.is_on if status else t.texts.anonymous.is_off await PollsDefaultSettings.anonymous.set(status) await add_reactions(ctx.message, "white_check_mark") @@ -750,7 +752,7 @@ async def anonymous(self, ctx: Context, status: bool): @PollsPermission.write.check @docs(t.commands.poll.settings.fair) async def fair(self, ctx: Context, status: bool): - msg: str = t.fair.is_on if status else t.fair.is_off + msg: str = t.texts.fair.is_on if status else t.texts.fair.is_off await PollsDefaultSettings.fair.set(status) await add_reactions(ctx.message, "white_check_mark") @@ -788,10 +790,10 @@ async def ignore(self, ctx: Context, member: Member): if user: await user.remove() - desc = t.ignore.removed(member.id) + desc = t.texts.ignore.removed(member.id) else: await IgnoredUser.create(ctx.guild.id, member.id) - desc = t.ignore.added(member.id) + desc = t.texts.ignore.added(member.id) await send_to_changelog(ctx.guild, desc) await add_reactions(ctx.message, "white_check_mark") @@ -820,7 +822,7 @@ async def quick(self, ctx: Context, *, args: str): deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24 max_choices = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS message, interaction, parsed_options, question, thread_id = await send_poll( - ctx=ctx, title=t.poll, poll_args=args, max_choices=max_choices, deadline=deadline + ctx=ctx, title=t.poll.standard, poll_args=args, max_choices=max_choices, deadline=deadline ) await Poll.create( @@ -850,7 +852,7 @@ async def new(self, ctx: Context, *, options: str): mess: Message = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author, timeout=60.0) args = mess.content - if args.lower() == t.skip.message: + if args.lower() == t.wizard.skip.message: await wizard.edit(embed=build_wizard(True), delete_after=5.0) else: await wizard.delete(delay=5.0) @@ -879,7 +881,7 @@ async def new(self, ctx: Context, *, options: str): """ message, interaction, parsed_options, question, thread_id = await send_poll( - ctx=ctx, title=t.poll, poll_args=options, max_choices=choices, deadline=deadline + ctx=ctx, title=t.poll.standard, poll_args=options, max_choices=choices, deadline=deadline ) await ctx.message.delete() @@ -909,13 +911,13 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt message = ctx.message if message.author != ctx.author and not await is_teamler(ctx.author): - raise CommandError(t.foreign_message) + raise CommandError(t.error.foreign_message) try: await message.add_reaction(name_to_emoji["thumbsup"]) await message.add_reaction(name_to_emoji["thumbsdown"]) except Forbidden: - raise CommandError(t.could_not_add_reactions(message.channel.mention)) + raise CommandError(t.error.could_not_add_reactions(message.channel.mention)) if message != ctx.message: try: @@ -934,11 +936,11 @@ async def team_yesno(self, ctx: Context, *, text: str): missing.sort(key=lambda m: str(m).lower()) *teamlers, last = (x.mention for x in missing) teamlers: list[str] - field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) + field = (tg.status, t.error.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) message, interaction, parsed_options, question, thread_id = await send_poll( ctx=ctx, - title=t.team_poll, + title=t.poll.team_poll, max_choices=1, poll_args=options, field=field, diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 952e99819..a54f9f69d 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -36,13 +36,28 @@ permissions: error: weight_too_small: "Weight cant be lower than `0.1`" + empty_option: Empty option cant_set_weight: Can't set weight! not_poll: Mesage doesn't contains a poll no_teamlers: No user with team-role found! + missing_options: Missing options + too_many_options: You specified too many options. The maximum amount is {}. + option_too_long: Options are limited to {} characters. no_polls: No current {} {}-polls. no_votes: No votes on this poll poll_cant_be_used: Poll can't be used because it's {} - still active: "This poll is still open, please wait until it's closed!" + still_active: "This poll is still open, please wait until it's closed!" + poll_status_not_changed: Poll is already {}! + teampoll_missing: "{} please vote on the poll!" + teampoll_forbidden: You are not allowed to use a team poll! + option_duplicated: You may not use the same emoji twice! + team_role_not_set: Team role is not set. + team_role_no_members: The team role has no members. + teampoll_all_voted: "All teamlers voted :white_check_mark:" + foreign_message: Can't add reactions on messages that aren't yours + teamlers_missing: + one: "{last} hasn't voted yet." + many: "{teamlers} and {last} haven't voted yet." cant_pin: title: Error description: Can't pin any more messages in {} @@ -79,53 +94,6 @@ polls: title: "List of {} {} polls" row: "\n[`{}`]({}) by <@{}> until {}" -role_weight: - set: "Set vote weight for <@&{}> to `{}`" - reset: "Vote weight has been reset for <@&{}>" - -weight_everyone: - set: "Set vote weight for the default role to `{}`" - reset: Vote weight for the default role has been reset - -duration: - set: - one: "Set default duration for poll to {cnt} hour" - many: "Set default duration for poll to {cnt} hours" - reset: "Set the default duration for polls to unlimited" - -max_duration: - set: - one: "Set maximum duration for a poll to {cnt} day" - many: "Set maximum duration for a poll to {cnt} days" - -votes: - set: - one: "Set default votes for a poll to {cnt} vote" - many: "Set default votes for a poll to {cnt} votes" - reset: "Set the default votes for polls to unlimited" - -voted: - title: Votes - row: "\n <@{}> -> Options: {}" - -anonymous: - is_on: made default poll votes anonymous - is_off: made default poll votes visible - -fair: - is_on: made default poll votes fair - is_off: made default poll votes based on roles - -select: - place: Select Options - placeholder: - one: "Select an option!" - many: "Select up to {cnt} options!" - label: "Option {}." - -delete: - confirm_text: Are you sure that you want to delete this poll? - usage: poll: | @@ -137,40 +105,33 @@ yes_no: against: "No" abstention: "Abstention" option_string: "{}\n:thumbsup: Yes\n:thumbsdown: No\n:zzz: Abstention" - count: - one: "{cnt} vote ({}%)" - many: "{cnt} votes ({}%)" - -option: - field: - name: "Option {}:" - -skip: - message: skip - title: Skipped - description: Skipped poll wizard -> default poll created! wizard: title: Poll wizard - description: Set arguments for an advanced poll + desc: Set arguments for an advanced poll skip: + message: skip name: Skip setup value: To skip the setup type `skip` - arg: Arguments - args: | - ``` - --deadline DEADLINE, -D DEADLINE - time when the poll should be closed [Default: server settings] - - --anonymous {True,False}, -A {True,False} - people can see who voted or not [Default: server settings] - - --choices CHOICES, -C CHOICES - the amount of votes someone can set [Default: multiple choices] - - --fair {True,False}, -F {True,False} - all roles have the same vote weight [Default: server settings] - ``` + skipped: + title: Skipped + desc: Skipped poll wizard -> default poll created! + args: + name: Arguments + value: | + ``` + --deadline DEADLINE, -D DEADLINE + time when the poll should be closed [Default: server settings] + + --anonymous {True,False}, -A {True,False} + people can see who voted or not [Default: server settings] + + --choices CHOICES, -C CHOICES + the amount of votes someone can set [Default: multiple choices] + + --fair {True,False}, -F {True,False} + all roles have the same vote weight [Default: server settings] + ``` example: name: Example value: | @@ -178,39 +139,59 @@ wizard: --> Creates an anonymous, 6 hours long poll with 4 select choices for every user -results: - results: "Poll results:" - -ignore: - title: Ignore - removed: "<@{}> was removed from the list of ignored member" - added: "<@{}> was added on the list of ignored member" - team_settings: title: Team poll settings ignore: name: Ignored Members -poll: Poll -team_poll: Team Poll -poll_voted: "Vote was added to the poll" -poll_status_not_changed: Poll is already {}! -poll_status_changed: Poll is now {}! -team_poll_missing: "{} please vote on the poll!" -team_yn_poll_forbidden: You are not allowed to use a team poll! -vote_explanation: Vote using the reactions below! -too_many_options: You specified too many options. The maximum amount is {}. -option_too_long: Options are limited to {} characters. -missing_options: Missing options -option_duplicated: You may not use the same emoji twice! -empty_option: Empty option -team_role_not_set: Team role is not set. -team_role_no_members: The team role has no members. -teampoll_all_voted: "All teamlers voted :white_check_mark:" -teamlers_missing: - one: "{last} hasn't voted yet." - many: "{teamlers} and {last} haven't voted yet." -footer: Ends at {} UTC -footer_closed: Closed -footer_paused: Paused -foreign_message: "You are not allowed to add yes/no reactions to foreign messages!" +poll: + standard: Poll + team_poll: Team Poll + voted: "Vote was added to the poll" + footer: + default: Ends at {} UTC + closed: Closed + paused: Paused + select: + place: Select Options + placeholder: + one: "Select an option!" + many: "Select up to {cnt} options!" + label: "Option {}." + option: "Option {}:" + results: "Poll results:" + status_changed: Poll is now {}! + +texts: + delete: + confirm: Are you sure that you want to delete this poll? + voted: + title: Votes + row: "\n <@{}> -> Options: {}" + role_weight: + set: "Set vote weight for <@&{}> to `{}`" + reset: "Vote weight has been reset for <@&{}>" + duration: + set: + one: "Set default duration for poll to {cnt} hour" + many: "Set default duration for poll to {cnt} hours" + reset: "Set the default duration for polls to unlimited" + max_duration: + set: + one: "Set maximum duration for a poll to {cnt} day" + many: "Set maximum duration for a poll to {cnt} days" + votes: + set: + one: "Set default votes for a poll to {cnt} vote" + many: "Set default votes for a poll to {cnt} votes" + reset: "Set the default votes for polls to unlimited" + anonymous: + is_on: made default poll votes anonymous + is_off: made default poll votes visible + fair: + is_on: made default poll votes fair + is_off: made default poll votes based on roles + ignore: + title: Ignore + removed: "<@{}> was removed from the list of ignored member" + added: "<@{}> was added on the list of ignored member" From 39acf210f54e1069c8da0a620457c67e3a8937a0 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:26:30 +0200 Subject: [PATCH 76/95] Added more TODO's --- general/polls/cog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index b1b898ec5..276e90a7e 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -235,7 +235,7 @@ async def notify_missing_staff(bot: Bot, poll: Poll): if not thread: return try: - teamlers: set[Member] = await get_staff(bot.get_guild(poll.guild_id), ["team"]) + teamlers: set[Member] = await get_staff(bot.get_guild(poll.guild_id), ["team"]) # TODO: kann in libary sein except CommandError: await thread.send(embed=Embed(title=t.error.no_teamlers, color=Colors.error)) return @@ -245,7 +245,7 @@ async def notify_missing_staff(bot: Bot, poll: Poll): for option in poll.options: for vote in option.votes: if vote.user_id not in ignored_ids: - user_ids.add(vote.user_id) + user_ids.add(vote.user_id) # TODO: nur die pingen die noch nicht abgestimmt haben missing: list[Member] | None = [teamler for teamler in teamlers if teamler.id not in user_ids] missing.sort(key=lambda m: str(m).lower()) @@ -363,7 +363,7 @@ def show_results( data_tuple: list[tuple[int | str, float]] = [(i + 1, num) for i, num in enumerate(data) if num] data_tuple.sort(key=lambda x: x[1]) data_np = np.array([value for _, value in data_tuple]) - + # TODO: erste zehn zeigen der frage packen cc = plt.cycler("color", plt.cm.rainbow(np.linspace(1, 0, len(data_np)))) explode = [len(data_tuple) / 50 for _ in data_tuple] with plt.style.context({"axes.prop_cycle": cc}): @@ -585,7 +585,7 @@ async def voted(self, ctx: Context, message: Message): await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True) - @poll.command(name="results", aliases=["res"]) + @poll.command(name="results", aliases=["res"]) # TODO: wenn man True setzt alles, sonst nur 10 @docs(t.commands.poll.result) async def result(self, ctx: Context, message: Message): poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) @@ -798,7 +798,7 @@ async def ignore(self, ctx: Context, member: Member): await send_to_changelog(ctx.guild, desc) await add_reactions(ctx.message, "white_check_mark") - @team.command(name="unpin", aliases=["u"]) # TODO: function for unpinning polls + @team.command(name="unpin", aliases=["u"]) # TODO: Accepted or dismiss poll commands for team-polls @PollsPermission.manage.check @docs(t.commands.poll.team.unpin) async def unpin(self, ctx: Context, message: Message): From 7d40d9790c160d58e00e143cbd0b83e8ec98c430 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sun, 17 Jul 2022 23:40:32 +0200 Subject: [PATCH 77/95] Reworked poll-sending --- general/polls/cog.py | 155 +++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 86 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 276e90a7e..ede2b65ba 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -46,7 +46,6 @@ t = t.polls MAX_OPTIONS = 25 # Discord select menu limit - DEFAULT_EMOJIS = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase] @@ -119,12 +118,16 @@ def calc_end_time(duration: Optional[float]) -> Optional[datetime]: async def send_poll( ctx: Context, + bot: Bot, title: str, poll_args: str, max_choices: int = None, - field: Optional[tuple[str, str]] = None, + team_poll: bool = False, deadline: Optional[int] = None, -) -> tuple[Message, Message, list[tuple[str, str]], str, int]: + anonymous: bool = False, + can_delete: bool = False, + fair: bool = False, +): """sends a poll embed + view message containing the select field""" if not max_choices: @@ -136,7 +139,7 @@ async def send_poll( raise CommandError(t.error.missing_options) if len(options) > MAX_OPTIONS: raise CommandError(t.error.too_many_options(MAX_OPTIONS)) - if field and len(options) >= MAX_OPTIONS: + if team_poll and len(options) >= MAX_OPTIONS: raise CommandError(t.error.too_many_options(MAX_OPTIONS - 1)) options = [await PollOption().init(ctx, line, i) for i, line in enumerate(options)] @@ -154,9 +157,17 @@ async def send_poll( raise CommandError(t.error.option_duplicated) for i, option in enumerate(options): - embed.add_field(name=t.error.option.field.name(i + 1), value=str(option), inline=False) + embed.add_field(name=t.poll.option(i + 1), value=str(option), inline=False) - if field: + poll_type: PollType = PollType.STANDARD + if team_poll: + missing = list(await get_staff(bot.guilds[0], ["team"])) + missing.sort(key=lambda m: str(m).lower()) + *teamlers, last = (x.mention for x in missing) + teamlers: list[str] + field = (tg.status, t.error.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) + + poll_type = poll_type.TEAM embed.add_field(name=field[0], value=field[1], inline=False) if not max_choices or isinstance(max_choices, str): @@ -193,7 +204,24 @@ async def send_poll( color=Colors.error, ) await send_alert(ctx.guild, embed) - return msg, view_msg, parsed_options, question, thread.id + + await Poll.create( + message_id=msg.id, + message_url=msg.jump_url, + guild_id=ctx.guild.id, + channel=msg.channel.id, + owner=ctx.author.id, + title=question, + end=deadline, + anonymous=anonymous, + can_delete=can_delete, + options=parsed_options, + poll_type=poll_type, + interaction=view_msg.id, + fair=fair, + max_choices=1, + thread=thread.id, + ) async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed: @@ -210,7 +238,7 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None ) else: embed.set_field_at(index, name=t.poll.option(index + 1), value=field.value, inline=False) - embed.set_footer(text=t.default.footer(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) + embed.set_footer(text=t.poll.footer.default(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) return embed @@ -355,15 +383,18 @@ async def status_change(bot: Bot, poll: Poll): def show_results( - poll: Poll, + poll: Poll, show_all: bool = False ) -> tuple[Embed, File]: # style is good for now, if you don't like it, change it by yourself - data: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options] + data: list[tuple[float, str]] = [ + (sum([vote.vote_weight for vote in option.votes]), option.option) for option in poll.options + ] if not any(data): raise CommandError(t.error.no_votes) - data_tuple: list[tuple[int | str, float]] = [(i + 1, num) for i, num in enumerate(data) if num] + data_tuple: list[tuple[str, float]] = [(text[:10], num) for num, text in data if num] data_tuple.sort(key=lambda x: x[1]) + if not show_all: + data_tuple = data_tuple[:10] data_np = np.array([value for _, value in data_tuple]) - # TODO: erste zehn zeigen der frage packen cc = plt.cycler("color", plt.cm.rainbow(np.linspace(1, 0, len(data_np)))) explode = [len(data_tuple) / 50 for _ in data_tuple] with plt.style.context({"axes.prop_cycle": cc}): @@ -372,10 +403,7 @@ def show_results( pie, *_ = ax1.pie(data_np, autopct="%1.1f%%", startangle=90, pctdistance=0.8, explode=explode) plt.setp(pie, width=0.5) plt.legend( - bbox_to_anchor=(1.1, 1.1), - loc="upper right", - borderaxespad=0, - labels=[f"Option: {i}" for i, _ in data_tuple], + bbox_to_anchor=(1.1, 1.1), loc="upper right", borderaxespad=0, labels=[f"{i}" for i, _ in data_tuple] ) plt.title(poll.title, fontdict={"fontsize": 20, "color": "#FFFFFF"}, pad=50) buf = BytesIO() @@ -585,9 +613,9 @@ async def voted(self, ctx: Context, message: Message): await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True) - @poll.command(name="results", aliases=["res"]) # TODO: wenn man True setzt alles, sonst nur 10 + @poll.command(name="results", aliases=["res"]) @docs(t.commands.poll.result) - async def result(self, ctx: Context, message: Message): + async def result(self, ctx: Context, message: Message, show_all: bool = False): poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) if poll.status == PollStatus.ACTIVE and not ( poll.owner_id == ctx.author.id or await PollsPermission.manage.check_permissions(ctx.author) @@ -596,7 +624,7 @@ async def result(self, ctx: Context, message: Message): if not poll: raise CommandError(t.error.not_poll) - embed, file = show_results(poll) + embed, file = show_results(poll, show_all) await send_long_embed(ctx, embed=embed, file=file) @@ -819,28 +847,17 @@ async def list(self, ctx: Context): @poll.command(name="quick", usage=t.usage.poll, aliases=["q"]) @docs(t.commands.poll.quick) async def quick(self, ctx: Context, *, args: str): - deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24 - max_choices = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS - message, interaction, parsed_options, question, thread_id = await send_poll( - ctx=ctx, title=t.poll.standard, poll_args=args, max_choices=max_choices, deadline=deadline - ) - await Poll.create( - message_id=message.id, - message_url=message.jump_url, - guild_id=ctx.guild.id, - channel=message.channel.id, - owner=ctx.author.id, - title=question, - end=deadline, + await send_poll( + bot=self.bot, + ctx=ctx, + title=t.poll.standard, + poll_args=args, + max_choices=await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS, + deadline=await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24, anonymous=await PollsDefaultSettings.anonymous.get(), - can_delete=True, - options=parsed_options, - poll_type=PollType.STANDARD, - interaction=interaction.id, fair=await PollsDefaultSettings.fair.get(), - max_choices=max_choices, - thread=thread_id, + can_delete=True, ) await ctx.message.delete() @@ -866,10 +883,7 @@ async def new(self, ctx: Context, *, options: str): if isinstance(deadline, int): deadline = deadline or max_deadline if deadline <= max_deadline else max_deadline else: - deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24 - anonymous: bool = parsed.anonymous - choices: int = parsed.choices - can_delete, fair = True, parsed.fair + deadline = await PollsDefaultSettings.duration.get() or max_deadline """ # Excluded code, need to be put into team-polls (new function) if poll_type == PollType.TEAM: can_delete, fair = False, True @@ -880,29 +894,19 @@ async def new(self, ctx: Context, *, options: str): field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) """ - message, interaction, parsed_options, question, thread_id = await send_poll( - ctx=ctx, title=t.poll.standard, poll_args=options, max_choices=choices, deadline=deadline + await send_poll( + bot=self.bot, + ctx=ctx, + title=t.poll.standard, + poll_args=options, + max_choices=parsed.choices, + deadline=deadline, + anonymous=parsed.anonymous, + fair=parsed.fair, + can_delete=True, ) await ctx.message.delete() - await Poll.create( - message_id=message.id, - message_url=message.jump_url, - guild_id=ctx.guild.id, - channel=message.channel.id, - owner=ctx.author.id, - title=question, - end=deadline, - anonymous=anonymous, - can_delete=can_delete, - options=parsed_options, - poll_type=PollType.STANDARD, - interaction=interaction.id, - fair=fair, - max_choices=choices, - thread=thread_id, - ) - @commands.command(aliases=["yn"]) @guild_only() @docs(t.commands.yes_no) @@ -932,34 +936,13 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt async def team_yesno(self, ctx: Context, *, text: str): options = t.yes_no.option_string(text) - missing = list(await get_staff(self.bot.guilds[0], ["team"])) - missing.sort(key=lambda m: str(m).lower()) - *teamlers, last = (x.mention for x in missing) - teamlers: list[str] - field = (tg.status, t.error.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) - - message, interaction, parsed_options, question, thread_id = await send_poll( + await send_poll( ctx=ctx, + bot=self.bot, title=t.poll.team_poll, max_choices=1, poll_args=options, - field=field, + team_poll=True, deadline=await PollsTeamsSettings.duration.get() * 24, - ) - await Poll.create( - message_id=message.id, - message_url=message.jump_url, - guild_id=ctx.guild.id, - channel=message.channel.id, - owner=ctx.author.id, - title=question, - end=await PollsTeamsSettings.duration.get() * 24, - anonymous=False, # TODO: make optional depending on settings can_delete=False, - options=parsed_options, - poll_type=PollType.TEAM, - interaction=interaction.id, - fair=True, # TODO: only fair if no weight is listed for team-polls - max_choices=1, - thread=thread_id, ) From 119bd12ac509bf6273493906bb301f0a4a4c5cfa Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sun, 24 Jul 2022 18:47:23 +0200 Subject: [PATCH 78/95] Reworked pause/unpause + fixed duration of polls --- general/polls/cog.py | 69 +++++++++++++++---------------- general/polls/models.py | 19 ++++++--- general/polls/settings.py | 2 +- general/polls/translations/en.yml | 2 +- 4 files changed, 48 insertions(+), 44 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index ede2b65ba..b9140788e 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -37,7 +37,7 @@ from .colors import Colors from .models import IgnoredUser, Option, Poll, PollStatus, PollType, PollVote, RoleWeight, sync_redis from .permissions import PollsPermission -from .settings import PollsDefaultSettings, PollsTeamsSettings +from .settings import PollsDefaultSettings, PollsTeamSettings from ...contributor import Contributor from ...pubsub import send_alert, send_to_changelog @@ -99,7 +99,7 @@ def build_wizard(skip: bool = False) -> Embed: async def get_parser() -> ArgumentParser: """creates a parser object with options for advanced polls""" parser = ArgumentParser() - parser.add_argument("--deadline", "-D", default=await PollsDefaultSettings.duration.get(), type=int) + parser.add_argument("--deadline", "-D", default=await PollsDefaultSettings.duration.get() * 60 * 60, type=int) parser.add_argument( "--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool, choices=[True, False] ) @@ -113,12 +113,11 @@ async def get_parser() -> ArgumentParser: def calc_end_time(duration: Optional[float]) -> Optional[datetime]: """returns the time when a poll should be closed""" - return utcnow() + relativedelta(hours=int(duration)) if duration else None + return utcnow() + relativedelta(seconds=int(duration)) if duration else None async def send_poll( ctx: Context, - bot: Bot, title: str, poll_args: str, max_choices: int = None, @@ -161,7 +160,7 @@ async def send_poll( poll_type: PollType = PollType.STANDARD if team_poll: - missing = list(await get_staff(bot.guilds[0], ["team"])) + missing = list(await get_staff(ctx.guild, ["team"])) missing.sort(key=lambda m: str(m).lower()) *teamlers, last = (x.mention for x in missing) teamlers: list[str] @@ -191,8 +190,8 @@ async def send_poll( view_msg = await ctx.send(view=create_select_view(select_obj=select_obj)) thread = await msg.create_thread(name=question) - parsed_options: list[tuple[str, str]] = [ - (obj.emoji, t.poll.select.label(ix)) for ix, obj in enumerate(options, start=1) + parsed_options: list[tuple[str, str, str]] = [ + (obj.emoji, obj.option, t.poll.select.label(ix)) for ix, obj in enumerate(options, start=1) ] try: @@ -219,7 +218,7 @@ async def send_poll( poll_type=poll_type, interaction=view_msg.id, fair=fair, - max_choices=1, + max_choices=max_choices, thread=thread.id, ) @@ -263,7 +262,7 @@ async def notify_missing_staff(bot: Bot, poll: Poll): if not thread: return try: - teamlers: set[Member] = await get_staff(bot.get_guild(poll.guild_id), ["team"]) # TODO: kann in libary sein + teamlers: set[Member] = await get_staff(bot.get_guild(poll.guild_id), ["team"]) except CommandError: await thread.send(embed=Embed(title=t.error.no_teamlers, color=Colors.error)) return @@ -273,7 +272,7 @@ async def notify_missing_staff(bot: Bot, poll: Poll): for option in poll.options: for vote in option.votes: if vote.user_id not in ignored_ids: - user_ids.add(vote.user_id) # TODO: nur die pingen die noch nicht abgestimmt haben + user_ids.add(vote.user_id) missing: list[Member] | None = [teamler for teamler in teamlers if teamler.id not in user_ids] missing.sort(key=lambda m: str(m).lower()) @@ -307,11 +306,18 @@ async def handle_deleted_messages(bot, message_id: int): async def check_poll_time(poll: Poll) -> bool: """checks if a poll has ended""" + + # removes all invalid polls if not poll.end_time and not poll.poll_type == PollType.TEAM: await poll.remove() return False - elif poll.timestamp + relativedelta(seconds=poll.end_time) < utcnow() and poll.status != PollStatus.ACTIVE: + # paused or closed polls + if poll.status != PollStatus.ACTIVE: + return False + + # poll still running + if poll.last_time_state_change + relativedelta(seconds=poll.end_time) < utcnow(): return False return True @@ -335,6 +341,7 @@ async def close_poll(bot, poll: Poll): await embed_message.unpin() poll.status = PollStatus.CLOSED + poll.last_time_state_change = utcnow() async def get_poll_list_embed(ctx: Context, poll_type: PollType, state: PollStatus) -> Embed: @@ -342,9 +349,12 @@ async def get_poll_list_embed(ctx: Context, poll_type: PollType, state: PollStat polls: list[Poll] = await db.all(filter_by(Poll, status=state, guild_id=ctx.guild.id, poll_type=poll_type)) for poll in polls: - description += t.polls.row( - poll.title, poll.message_url, poll.owner_id, format_dt(calc_end_time(poll.end_time), style="R") + time = ( + f'until {format_dt(calc_end_time(poll.end_time), style="R")}' + if poll.status == PollStatus.ACTIVE + else poll.status ) + description += t.polls.row(poll.title, poll.message_url, poll.owner_id, time) if polls and description: embed: Embed = Embed( @@ -379,6 +389,7 @@ async def status_change(bot: Bot, poll: Poll): embed.colour = Colors.Polls await embed_message.pin() + poll.last_time_state_change = utcnow() await embed_message.edit(embed=embed) @@ -556,6 +567,8 @@ async def poll_list(self, ctx: Context, active: bool = True): await send_long_embed(ctx, embed=embed, paginate=True) + # TODO: close command for polls + @poll.command(name="delete", aliases=["del"]) @docs(t.commands.poll.delete) async def delete(self, ctx: Context, message: Message): @@ -672,9 +685,7 @@ async def settings(self, ctx: Context): max_time: int = await PollsDefaultSettings.max_duration.get() embed.add_field( name=t.poll_config.duration.name, - value=t.poll_config.duration.time(cnt=time) - if not time <= 0 - else t.poll_config.duration.time(cnt=max_time * 24), + value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.time(cnt=max_time), inline=False, ) embed.add_field( @@ -847,14 +858,13 @@ async def list(self, ctx: Context): @poll.command(name="quick", usage=t.usage.poll, aliases=["q"]) @docs(t.commands.poll.quick) async def quick(self, ctx: Context, *, args: str): - await send_poll( - bot=self.bot, ctx=ctx, title=t.poll.standard, poll_args=args, max_choices=await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS, - deadline=await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24, + deadline=await PollsDefaultSettings.duration.get() * 60 * 60 + or await PollsDefaultSettings.max_duration.get() * 60 * 60 * 24, anonymous=await PollsDefaultSettings.anonymous.get(), fair=await PollsDefaultSettings.fair.get(), can_delete=True, @@ -878,24 +888,14 @@ async def new(self, ctx: Context, *, options: str): parser = await get_parser() parsed: Namespace = parser.parse_known_args(args.split())[0] - max_deadline = await PollsDefaultSettings.max_duration.get() * 24 + max_deadline = await PollsDefaultSettings.max_duration.get() * 60 * 60 * 24 deadline: Union[list[str, str], int] = parsed.deadline if isinstance(deadline, int): deadline = deadline or max_deadline if deadline <= max_deadline else max_deadline else: - deadline = await PollsDefaultSettings.duration.get() or max_deadline - """ # Excluded code, need to be put into team-polls (new function) - if poll_type == PollType.TEAM: - can_delete, fair = False, True - missing = list(await get_staff(self.bot.guilds[0], ["team"])) - missing.sort(key=lambda m: str(m).lower()) - *teamlers, last = (x.mention for x in missing) - teamlers: list[str] - field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)) - """ + deadline = await PollsDefaultSettings.duration.get() * 60 * 60 or max_deadline await send_poll( - bot=self.bot, ctx=ctx, title=t.poll.standard, poll_args=options, @@ -934,15 +934,12 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt @guild_only() @docs(t.commands.team_yes_no) async def team_yesno(self, ctx: Context, *, text: str): - options = t.yes_no.option_string(text) - await send_poll( ctx=ctx, - bot=self.bot, title=t.poll.team_poll, max_choices=1, - poll_args=options, + poll_args=t.yes_no.option_string(text), team_poll=True, - deadline=await PollsTeamsSettings.duration.get() * 24, + deadline=await PollsTeamSettings.duration.get() * 60 * 60 * 24, can_delete=False, ) diff --git a/general/polls/models.py b/general/polls/models.py index 11304946a..c0c66fe2f 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -65,6 +65,7 @@ class Poll(Base): can_delete: Union[Column, bool] = Column(Boolean) fair: Union[Column, bool] = Column(Boolean) status: Union[Column, PollStatus] = Column(Enum(PollStatus)) + last_time_state_change: Union[Column, datetime] = Column(UTCDateTime) max_choices: Union[Column, int] = Column(BigInteger) @staticmethod @@ -75,7 +76,7 @@ async def create( channel: int, owner: int, title: str, - options: list[tuple[str, str]], + options: list[tuple[str, str, str]], end: Optional[int], anonymous: bool, can_delete: bool, @@ -101,12 +102,17 @@ async def create( thread_id=thread, fair=fair, status=PollStatus.ACTIVE, + last_time_state_change=utcnow(), max_choices=max_choices, ) for position, poll_option in enumerate(options): row.options.append( await Option.create( - poll=message_id, emote=poll_option[0], option_text=poll_option[1], field_position=position + poll=message_id, + emote=poll_option[0], + text=poll_option[1], + option=poll_option[2], + field_position=position, ) ) @@ -124,13 +130,14 @@ class Option(Base): poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll.message_id")) votes: list[PollVote] = relationship("PollVote", back_populates="option", cascade="all, delete") poll: Poll = relationship("Poll", back_populates="options") - emote: Union[Column, str] = Column(Text(30)) - option: Union[Column, str] = Column(Text(250)) + emote: Union[Column, str] = Column(Text(32)) + option: Union[Column, str] = Column(Text(20)) + text: Union[Column, str] = Column(Text(1024)) field_position: Union[Column, int] = Column(BigInteger) @staticmethod - async def create(poll: int, emote: str, option_text: str, field_position: int) -> Option: - options = Option(poll_id=poll, emote=emote, option=option_text, field_position=field_position) + async def create(poll: int, emote: str, option: str, text: str, field_position: int) -> Option: + options = Option(poll_id=poll, emote=emote, option=option, text=text, field_position=field_position) await db.add(options) return options diff --git a/general/polls/settings.py b/general/polls/settings.py index 194a55a44..a7ec5047a 100644 --- a/general/polls/settings.py +++ b/general/polls/settings.py @@ -10,5 +10,5 @@ class PollsDefaultSettings(Settings): fair = False -class PollsTeamsSettings(Settings): +class PollsTeamSettings(Settings): duration = 1 # days after which all missing team-members should be pinged, if not excluded diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index a54f9f69d..f9e08b389 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -92,7 +92,7 @@ poll_config: polls: title: "List of {} {} polls" - row: "\n[`{}`]({}) by <@{}> until {}" + row: "\n[`{}`]({}) by <@{}> ({})" usage: poll: | From 966a3e3a6e7d9b6c5654cee2d1354318243a9f91 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 25 Jul 2022 18:39:07 +0200 Subject: [PATCH 79/95] Improved editing of embeds, now only for team-polls --- general/polls/cog.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index b9140788e..7cfa935d7 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -224,7 +224,7 @@ async def send_poll( async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed: - """edits the poll embed, updating the votes and percentages""" + """edits the poll embed, updating the votes from team-members""" for index, field in enumerate(embed.fields): if field.name == tg.status: missing.sort(key=lambda m: str(m).lower()) @@ -235,9 +235,7 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None name=field.name, value=t.error.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1), ) - else: - embed.set_field_at(index, name=t.poll.option(index + 1), value=field.value, inline=False) - embed.set_footer(text=t.poll.footer.default(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) + embed.set_footer(text=t.poll.footer.default(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) return embed @@ -452,7 +450,6 @@ async def callback(self, interaction): return new_options: list[Option] = [option for option in poll.options if option.option in selected_options] - missing: list[Member] | None = None opt: Option for opt in poll.options: @@ -490,8 +487,8 @@ async def callback(self, interaction): missing: list[Member] | None = [teamler for teamler in teamlers if teamler.id not in user_ids] missing.sort(key=lambda m: str(m).lower()) + embed = await edit_poll_embed(embed, poll, missing) - embed = await edit_poll_embed(embed, poll, missing) await message.edit(embed=embed) await interaction.response.send_message(content=t.poll.voted, ephemeral=True) From 46cb7c2a0d80c83b584b8ca866dce34981ac50ec Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 25 Jul 2022 20:17:16 +0200 Subject: [PATCH 80/95] Added command for accepting/rejecting team polls + addedoptional permissions --- general/polls/cog.py | 92 +++++++++++++++++++------------ general/polls/translations/en.yml | 6 +- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 7cfa935d7..1fd5eb3b3 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -26,7 +26,7 @@ from discord.utils import format_dt, utcnow from PyDrocsid.cog import Cog -from PyDrocsid.command import Confirmation, add_reactions, docs +from PyDrocsid.command import Confirmation, add_reactions, docs, optional_permissions from PyDrocsid.database import db, db_wrapper, filter_by from PyDrocsid.embeds import EmbedLimits, send_long_embed from PyDrocsid.emojis import emoji_to_name, name_to_emoji @@ -227,15 +227,17 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None """edits the poll embed, updating the votes from team-members""" for index, field in enumerate(embed.fields): if field.name == tg.status: - missing.sort(key=lambda m: str(m).lower()) - *teamlers, last = (x.mention for x in missing) - teamlers: list[str] - embed.set_field_at( - index, - name=field.name, - value=t.error.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1), - ) - embed.set_footer(text=t.poll.footer.default(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) + if missing: + missing.sort(key=lambda m: str(m).lower()) + *teamlers, last = (x.mention for x in missing) + teamlers: list[str] + text = t.error.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1) + else: + text = t.poll.all_voted + + embed.set_field_at(index, name=field.name, value=text) + break + embed.set_footer(text=t.poll.footer.default(calc_end_time(poll.end_time).strftime("%Y-%m-%d %H:%M"))) return embed @@ -288,6 +290,8 @@ async def handle_deleted_messages(bot, message_id: int): return poll = deleted_embed or deleted_interaction + if poll.status == PollStatus.CLOSED or poll.poll_type == PollType.TEAM: + return channel = await bot.fetch_channel(poll.channel_id) try: if deleted_interaction: @@ -331,6 +335,9 @@ async def close_poll(bot, poll: Poll): poll.status = PollStatus.CLOSED return + poll.status = PollStatus.CLOSED + poll.last_time_state_change = utcnow() + await interaction_message.delete() embed = embed_message.embeds[0] embed.set_footer(text=t.poll.footer.closed) @@ -338,9 +345,6 @@ async def close_poll(bot, poll: Poll): await embed_message.edit(embed=embed) await embed_message.unpin() - poll.status = PollStatus.CLOSED - poll.last_time_state_change = utcnow() - async def get_poll_list_embed(ctx: Context, poll_type: PollType, state: PollStatus) -> Embed: description = "" @@ -486,7 +490,6 @@ async def callback(self, interaction): user_ids.add(vote.user_id) missing: list[Member] | None = [teamler for teamler in teamlers if teamler.id not in user_ids] - missing.sort(key=lambda m: str(m).lower()) embed = await edit_poll_embed(embed, poll, missing) await message.edit(embed=embed) @@ -546,7 +549,7 @@ async def poll_loop(self): if not await check_poll_time(poll) and poll.poll_type == PollType.STANDARD: await close_poll(self.bot, poll) elif not await check_poll_time(poll) and poll.poll_type == PollType.TEAM: - poll.end_time = poll.end_time + relativedelta(days=1) + poll.end_time = poll.end_time + relativedelta(days=1).seconds await notify_missing_staff(self.bot, poll) @commands.group(name="poll", aliases=["vote"]) @@ -567,16 +570,13 @@ async def poll_list(self, ctx: Context, active: bool = True): # TODO: close command for polls @poll.command(name="delete", aliases=["del"]) + @optional_permissions(PollsPermission.manage) @docs(t.commands.poll.delete) async def delete(self, ctx: Context, message: Message): poll: Poll = await db.get(Poll, message_id=message.id) if not poll: raise CommandError(t.error.not_poll) - if ( - poll.can_delete - and not await PollsPermission.manage.check_permissions(ctx.author) - and not poll.owner_id == ctx.author.id - ): + if poll.can_delete and not poll.owner_id == ctx.author.id: raise PermissionError elif not poll.can_delete and not poll.owner_id == ctx.author.id: raise PermissionError # if delete is False, only the owner can delete it @@ -595,17 +595,14 @@ async def delete(self, ctx: Context, message: Message): await add_reactions(ctx.message, "white_check_mark") @poll.command(name="voted", aliases=["v"]) + @optional_permissions(PollsPermission.manage) @docs(t.commands.poll.voted) async def voted(self, ctx: Context, message: Message): poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) author = ctx.author if not poll: raise CommandError(t.error.not_poll) - if ( - poll.anonymous - and not await PollsPermission.manage.check_permissions(author) - and not poll.owner_id == author.id - ): + if poll.anonymous and not poll.owner_id == author.id: raise PermissionError users = {} @@ -624,12 +621,11 @@ async def voted(self, ctx: Context, message: Message): await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True) @poll.command(name="results", aliases=["res"]) + @optional_permissions(PollsPermission.manage) @docs(t.commands.poll.result) async def result(self, ctx: Context, message: Message, show_all: bool = False): poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) - if poll.status == PollStatus.ACTIVE and not ( - poll.owner_id == ctx.author.id or await PollsPermission.manage.check_permissions(ctx.author) - ): + if poll.status == PollStatus.ACTIVE and not poll.owner_id == ctx.author.id: raise CommandError(t.error.still_active) if not poll: raise CommandError(t.error.not_poll) @@ -639,12 +635,13 @@ async def result(self, ctx: Context, message: Message, show_all: bool = False): await send_long_embed(ctx, embed=embed, file=file) @poll.command(name="activate", aliases=["a"]) + @optional_permissions(PollsPermission.manage) @docs(t.commands.poll.activate) async def activate(self, ctx: Context, message: Message): poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) if not poll: raise CommandError(t.error.not_poll) - if not ctx.author.id == poll.owner_id and not await PollsPermission.manage.check_permissions(ctx.author): + if not ctx.author.id == poll.owner_id: raise PermissionError if poll.status == PollStatus.ACTIVE: @@ -654,12 +651,13 @@ async def activate(self, ctx: Context, message: Message): await send_long_embed(ctx, embed=Embed(title=t.poll.status_changed(poll.status.value))) @poll.command(name="pause", aliases=["p", "deactivate", "disable"]) + @optional_permissions(PollsPermission.manage) @docs(t.commands.poll.paused) async def pause(self, ctx: Context, message: Message): poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) if not poll: raise CommandError(t.error.not_poll) - if not ctx.author.id == poll.owner_id and not await PollsPermission.manage.check_permissions(ctx.author): + if not ctx.author.id == poll.owner_id: raise PermissionError if poll.status == PollStatus.PAUSED: @@ -834,11 +832,37 @@ async def ignore(self, ctx: Context, member: Member): await send_to_changelog(ctx.guild, desc) await add_reactions(ctx.message, "white_check_mark") - @team.command(name="unpin", aliases=["u"]) # TODO: Accepted or dismiss poll commands for team-polls + @team.command(name="conclude", aliases=["c"]) # TODO: Accepted or dismiss poll commands for team-polls @PollsPermission.manage.check - @docs(t.commands.poll.team.unpin) - async def unpin(self, ctx: Context, message: Message): - pass + @docs(t.commands.poll.team.conclude) + async def conclude(self, ctx: Context, message: Message, accepted: bool): + poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + if not poll or poll.poll_type != PollType.TEAM or poll.status == PollStatus.CLOSED or not message.embeds: + raise CommandError(t.error.not_poll) + + embed: Embed = message.embeds[0] if message.embeds else None + thread = self.bot.get_channel(poll.thread_id) + + for index, field in enumerate(embed.fields): + if field.name == tg.status: + if accepted: + embed.colour = Colors.green + text = t.texts.conclude.accepted + else: + embed.colour = Colors.red + text = t.texts.conclude.rejected + + embed.set_field_at(index, name=field.name, value=text(ctx.author.mention)) + await message.edit(embed=embed) + + res = show_results(poll, True) + if thread: + await thread.send(embed=res[0], file=res[1]) + await thread.archive(True) + else: + await ctx.send(embed=res[0], file=res[1]) + + await close_poll(self.bot, poll) @team.command(name="new", aliases=["n"]) # TODO: new team-polls @docs(t.commands.poll.team.new) diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index f9e08b389..5fa25e422 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -19,7 +19,7 @@ commands: fair: manage if role weights impact on default polls team: team: create and manage team-polls - unpin: unpin a team-poll + conclude: close a team-poll by accepting or rejecting new: create a new team-poll list: show all open team-polls settings: @@ -161,6 +161,7 @@ poll: option: "Option {}:" results: "Poll results:" status_changed: Poll is now {}! + all_voted: ":white_check_mark: All team-members have voted!" texts: delete: @@ -195,3 +196,6 @@ texts: title: Ignore removed: "<@{}> was removed from the list of ignored member" added: "<@{}> was added on the list of ignored member" + conclude: + accepted: "Poll was accepted by {}" + rejected: "Poll was rejected by {}" From 4dba95f276955707ffd9633d614b1b39fd479757 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Tue, 26 Jul 2022 15:51:41 +0200 Subject: [PATCH 81/95] Added close command for polls + fixed show_results-function --- general/polls/cog.py | 39 ++++++++++++++++++++++++------- general/polls/translations/en.yml | 2 ++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 1fd5eb3b3..ef738c8e5 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -329,22 +329,32 @@ async def close_poll(bot, poll: Poll): """deletes the interaction message and edits the footer of the poll embed""" try: channel = await bot.fetch_channel(poll.channel_id) + thread = await bot.fetch_channel(poll.thread_id) embed_message = await channel.fetch_message(poll.message_id) interaction_message = await channel.fetch_message(poll.interaction_message_id) except NotFound: poll.status = PollStatus.CLOSED return - poll.status = PollStatus.CLOSED - poll.last_time_state_change = utcnow() - - await interaction_message.delete() embed = embed_message.embeds[0] + embed.colour = Colors.purple embed.set_footer(text=t.poll.footer.closed) await embed_message.edit(embed=embed) await embed_message.unpin() + try: + res = show_results(poll, True) + await thread.send(embed=res[0], file=res[1]) + except CommandError: + pass + await thread.archive(True) + + poll.status = PollStatus.CLOSED + poll.last_time_state_change = utcnow() + await db.commit() + await interaction_message.delete() + async def get_poll_list_embed(ctx: Context, poll_type: PollType, state: PollStatus) -> Embed: description = "" @@ -399,9 +409,9 @@ def show_results( poll: Poll, show_all: bool = False ) -> tuple[Embed, File]: # style is good for now, if you don't like it, change it by yourself data: list[tuple[float, str]] = [ - (sum([vote.vote_weight for vote in option.votes]), option.option) for option in poll.options + (sum([vote.vote_weight for vote in option.votes]), option.text) for option in poll.options ] - if not any(data): + if not any(True for x in data if x[0] != 0): raise CommandError(t.error.no_votes) data_tuple: list[tuple[str, float]] = [(text[:10], num) for num, text in data if num] data_tuple.sort(key=lambda x: x[1]) @@ -567,7 +577,20 @@ async def poll_list(self, ctx: Context, active: bool = True): await send_long_embed(ctx, embed=embed, paginate=True) - # TODO: close command for polls + @poll.command(name="close", aliases=["c"]) + @optional_permissions(PollsPermission.manage) + @docs(t.commands.poll.close) + async def close(self, ctx: Context, message: Message): + poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + if not poll: + raise CommandError(t.error.not_poll) + if poll.status == PollStatus.CLOSED: + raise CommandError(t.error.poll_closed) + if poll.owner_id != ctx.author.id: + raise CommandError(tg.not_allowed) + + await close_poll(self.bot, poll) + await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) @poll.command(name="delete", aliases=["del"]) @optional_permissions(PollsPermission.manage) @@ -832,7 +855,7 @@ async def ignore(self, ctx: Context, member: Member): await send_to_changelog(ctx.guild, desc) await add_reactions(ctx.message, "white_check_mark") - @team.command(name="conclude", aliases=["c"]) # TODO: Accepted or dismiss poll commands for team-polls + @team.command(name="conclude", aliases=["c"]) @PollsPermission.manage.check @docs(t.commands.poll.team.conclude) async def conclude(self, ctx: Context, message: Message, accepted: bool): diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 5fa25e422..49cfae85c 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -3,6 +3,7 @@ commands: poll: poll commands quick: small poll with default options new: advanced poll with more options + close: close a poll without deleting it delete: delete polls activate: activate a paused poll paused: pause an active poll @@ -61,6 +62,7 @@ error: cant_pin: title: Error description: Can't pin any more messages in {} + poll_closed: Poll is closed poll_config: title: Default poll configuration From ccafbac2c32a1928c21c8016cd9c80e8d2482464 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:19:08 +0200 Subject: [PATCH 82/95] Fixed PermissionErrors --- general/polls/cog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index ef738c8e5..61bb58d20 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -600,9 +600,9 @@ async def delete(self, ctx: Context, message: Message): if not poll: raise CommandError(t.error.not_poll) if poll.can_delete and not poll.owner_id == ctx.author.id: - raise PermissionError + raise CommandError(tg.not_allowed) elif not poll.can_delete and not poll.owner_id == ctx.author.id: - raise PermissionError # if delete is False, only the owner can delete it + raise CommandError(tg.not_allowed) # if delete is False, only the owner can delete it if not await Confirmation().run(ctx, t.texts.delete.confirm): return @@ -626,7 +626,7 @@ async def voted(self, ctx: Context, message: Message): if not poll: raise CommandError(t.error.not_poll) if poll.anonymous and not poll.owner_id == author.id: - raise PermissionError + raise CommandError(tg.not_allowed) users = {} for option in poll.options: @@ -665,7 +665,7 @@ async def activate(self, ctx: Context, message: Message): if not poll: raise CommandError(t.error.not_poll) if not ctx.author.id == poll.owner_id: - raise PermissionError + raise CommandError(tg.not_allowed) if poll.status == PollStatus.ACTIVE: raise CommandError(t.error.poll_status_not_changed(poll.status.value)) @@ -681,7 +681,7 @@ async def pause(self, ctx: Context, message: Message): if not poll: raise CommandError(t.error.not_poll) if not ctx.author.id == poll.owner_id: - raise PermissionError + raise CommandError(tg.not_allowed) if poll.status == PollStatus.PAUSED: raise CommandError(t.error.poll_status_not_changed(poll.status.value)) From 140056407fc62713b7858f96861296850a73d267 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 29 Jul 2022 18:41:10 +0200 Subject: [PATCH 83/95] Improved codestyle --- general/polls/cog.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 61bb58d20..7257844d7 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -408,12 +408,12 @@ async def status_change(bot: Bot, poll: Poll): def show_results( poll: Poll, show_all: bool = False ) -> tuple[Embed, File]: # style is good for now, if you don't like it, change it by yourself - data: list[tuple[float, str]] = [ - (sum([vote.vote_weight for vote in option.votes]), option.text) for option in poll.options + data: list[tuple[str, float | int]] = [ + (option.text, weight) for option in poll.options if (weight := sum(vote.vote_weight for vote in option.votes)) ] - if not any(True for x in data if x[0] != 0): + if not data: raise CommandError(t.error.no_votes) - data_tuple: list[tuple[str, float]] = [(text[:10], num) for num, text in data if num] + data_tuple: list[tuple[str, float]] = [(text[:10], num) for text, num in data if num] data_tuple.sort(key=lambda x: x[1]) if not show_all: data_tuple = data_tuple[:10] @@ -522,22 +522,24 @@ async def on_ready(self): await sync_redis() polls: list[Poll] = await db.all(filter_by(Poll, (Poll.options, Option.votes), status=PollStatus.ACTIVE)) for poll in polls: - if await check_poll_time(poll): - select_obj = MySelect( - custom_id=str(poll.message_id), - placeholder=t.poll.select.placeholder(cnt=poll.max_choices), - max_values=poll.max_choices, - options=[ - SelectOption( - label=t.poll.select.label(option.field_position + 1), - emoji=option.emote, - description=option.option, - ) - for option in poll.options - ], - ) - - self.bot.add_view(view=create_select_view(select_obj), message_id=poll.interaction_message_id) + if not await check_poll_time(poll): + continue + + select_obj = MySelect( + custom_id=str(poll.message_id), + placeholder=t.poll.select.placeholder(cnt=poll.max_choices), + max_values=poll.max_choices, + options=[ + SelectOption( + label=t.poll.select.label(option.field_position + 1), + emoji=option.emote, + description=option.option, + ) + for option in poll.options + ], + ) + + self.bot.add_view(view=create_select_view(select_obj), message_id=poll.interaction_message_id) try: self.poll_loop.start() From 5c4a14ac81ea3dbe3d38ba51d9d03a601a81107b Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 29 Jul 2022 19:25:36 +0200 Subject: [PATCH 84/95] Renamed Settings into shorter names --- general/polls/cog.py | 54 ++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 7257844d7..af4155bce 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -37,7 +37,8 @@ from .colors import Colors from .models import IgnoredUser, Option, Poll, PollStatus, PollType, PollVote, RoleWeight, sync_redis from .permissions import PollsPermission -from .settings import PollsDefaultSettings, PollsTeamSettings +from .settings import PollsDefaultSettings as PdS +from .settings import PollsTeamSettings as PtS from ...contributor import Contributor from ...pubsub import send_alert, send_to_changelog @@ -99,14 +100,10 @@ def build_wizard(skip: bool = False) -> Embed: async def get_parser() -> ArgumentParser: """creates a parser object with options for advanced polls""" parser = ArgumentParser() - parser.add_argument("--deadline", "-D", default=await PollsDefaultSettings.duration.get() * 60 * 60, type=int) - parser.add_argument( - "--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool, choices=[True, False] - ) - parser.add_argument( - "--choices", "-C", default=await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS, type=int - ) - parser.add_argument("--fair", "-F", default=await PollsDefaultSettings.fair.get(), type=bool, choices=[True, False]) + parser.add_argument("--deadline", "-D", default=await PdS.duration.get() * 60 * 60, type=int) + parser.add_argument("--anonymous", "-A", default=await PdS.anonymous.get(), type=bool, choices=[True, False]) + parser.add_argument("--choices", "-C", default=await PdS.max_choices.get() or MAX_OPTIONS, type=int) + parser.add_argument("--fair", "-F", default=await PdS.fair.get(), type=bool, choices=[True, False]) return parser @@ -472,7 +469,7 @@ async def callback(self, interaction): await vote.remove() opt.votes.remove(vote) - ev_pover = await PollsDefaultSettings.everyone_power.get() + ev_pover = await PdS.everyone_power.get() if poll.fair: user_weight: float = ev_pover else: @@ -701,8 +698,8 @@ async def settings(self, ctx: Context): return embed = Embed(title=t.poll_config.title, color=Colors.Polls) - time: int = await PollsDefaultSettings.duration.get() - max_time: int = await PollsDefaultSettings.max_duration.get() + time: int = await PdS.duration.get() + max_time: int = await PdS.max_duration.get() embed.add_field( name=t.poll_config.duration.name, value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.time(cnt=max_time), @@ -711,18 +708,18 @@ async def settings(self, ctx: Context): embed.add_field( name=t.poll_config.max_duration.name, value=t.poll_config.max_duration.time(cnt=max_time), inline=False ) - choice: int = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS + choice: int = await PdS.max_choices.get() or MAX_OPTIONS embed.add_field( name=t.poll_config.choices.name, value=t.poll_config.choices.amount(cnt=choice) if not choice <= 0 else t.poll_config.choices.unlimited, inline=False, ) - anonymous: bool = await PollsDefaultSettings.anonymous.get() + anonymous: bool = await PdS.anonymous.get() embed.add_field(name=t.poll_config.anonymous.name, value=str(anonymous), inline=False) - fair: bool = await PollsDefaultSettings.fair.get() + fair: bool = await PdS.fair.get() embed.add_field(name=t.poll_config.fair.name, value=str(fair), inline=False) roles = await RoleWeight.get(ctx.guild.id, PollType.STANDARD) - everyone: int = await PollsDefaultSettings.everyone_power.get() + everyone: int = await PdS.everyone_power.get() base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone) if roles: base += "".join(t.poll_config.roles.row(role.role_id, role.weight) for role in roles) @@ -765,7 +762,7 @@ async def duration(self, ctx: Context, hours: int | None = None): else: msg: str = t.texts.duration.set(cnt=hours) - await PollsDefaultSettings.duration.set(hours) + await PdS.duration.set(hours) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) @@ -776,7 +773,7 @@ async def max_duration(self, ctx: Context, days: int | None = None): days = days or 7 msg: str = t.texts.max_duration.set(cnt=days) - await PollsDefaultSettings.max_duration.set(days) + await PdS.max_duration.set(days) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) @@ -793,7 +790,7 @@ async def votes(self, ctx: Context, votes: int | None = None): if not 0 < votes < MAX_OPTIONS: votes = 0 - await PollsDefaultSettings.max_choices.set(votes) + await PdS.max_choices.set(votes) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) @@ -803,7 +800,7 @@ async def votes(self, ctx: Context, votes: int | None = None): async def anonymous(self, ctx: Context, status: bool): msg: str = t.texts.anonymous.is_on if status else t.texts.anonymous.is_off - await PollsDefaultSettings.anonymous.set(status) + await PdS.anonymous.set(status) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) @@ -813,7 +810,7 @@ async def anonymous(self, ctx: Context, status: bool): async def fair(self, ctx: Context, status: bool): msg: str = t.texts.fair.is_on if status else t.texts.fair.is_off - await PollsDefaultSettings.fair.set(status) + await PdS.fair.set(status) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) @@ -908,11 +905,10 @@ async def quick(self, ctx: Context, *, args: str): ctx=ctx, title=t.poll.standard, poll_args=args, - max_choices=await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS, - deadline=await PollsDefaultSettings.duration.get() * 60 * 60 - or await PollsDefaultSettings.max_duration.get() * 60 * 60 * 24, - anonymous=await PollsDefaultSettings.anonymous.get(), - fair=await PollsDefaultSettings.fair.get(), + max_choices=await PdS.max_choices.get() or MAX_OPTIONS, + deadline=await PdS.duration.get() * 60 * 60 or await PdS.max_duration.get() * 60 * 60 * 24, + anonymous=await PdS.anonymous.get(), + fair=await PdS.fair.get(), can_delete=True, ) @@ -934,12 +930,12 @@ async def new(self, ctx: Context, *, options: str): parser = await get_parser() parsed: Namespace = parser.parse_known_args(args.split())[0] - max_deadline = await PollsDefaultSettings.max_duration.get() * 60 * 60 * 24 + max_deadline = await PdS.max_duration.get() * 60 * 60 * 24 deadline: Union[list[str, str], int] = parsed.deadline if isinstance(deadline, int): deadline = deadline or max_deadline if deadline <= max_deadline else max_deadline else: - deadline = await PollsDefaultSettings.duration.get() * 60 * 60 or max_deadline + deadline = await PdS.duration.get() * 60 * 60 or max_deadline await send_poll( ctx=ctx, @@ -986,6 +982,6 @@ async def team_yesno(self, ctx: Context, *, text: str): max_choices=1, poll_args=t.yes_no.option_string(text), team_poll=True, - deadline=await PollsTeamSettings.duration.get() * 60 * 60 * 24, + deadline=await PtS.duration.get() * 60 * 60 * 24, can_delete=False, ) From 682a3192da7f07f6c8ebb178097c3137295bc81f Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 29 Jul 2022 22:43:51 +0200 Subject: [PATCH 85/95] Removed most default settings (wont be used anyways) --- general/polls/cog.py | 102 +++--------------------------- general/polls/models.py | 17 +++-- general/polls/settings.py | 3 - general/polls/translations/en.yml | 44 +++---------- 4 files changed, 25 insertions(+), 141 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index af4155bce..0b0d9d119 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -17,7 +17,6 @@ Message, NotFound, RawMessageDeleteEvent, - Role, SelectOption, ) from discord.ext import commands, tasks @@ -100,10 +99,11 @@ def build_wizard(skip: bool = False) -> Embed: async def get_parser() -> ArgumentParser: """creates a parser object with options for advanced polls""" parser = ArgumentParser() - parser.add_argument("--deadline", "-D", default=await PdS.duration.get() * 60 * 60, type=int) - parser.add_argument("--anonymous", "-A", default=await PdS.anonymous.get(), type=bool, choices=[True, False]) - parser.add_argument("--choices", "-C", default=await PdS.max_choices.get() or MAX_OPTIONS, type=int) - parser.add_argument("--fair", "-F", default=await PdS.fair.get(), type=bool, choices=[True, False]) + parser.add_argument("--deadline", "-D", default=0, type=int) + parser.add_argument("--anonymous", "-A", default=False, type=bool, choices=[True, False]) + parser.add_argument("--choices", "-C", default=MAX_OPTIONS, type=int) + parser.add_argument("--roles", "-R", default="0", type=str) + parser.add_argument("--weights", "-W", default="0", type=str) return parser @@ -122,7 +122,6 @@ async def send_poll( deadline: Optional[int] = None, anonymous: bool = False, can_delete: bool = False, - fair: bool = False, ): """sends a poll embed + view message containing the select field""" @@ -214,7 +213,6 @@ async def send_poll( options=parsed_options, poll_type=poll_type, interaction=view_msg.id, - fair=fair, max_choices=max_choices, thread=thread.id, ) @@ -703,55 +701,13 @@ async def settings(self, ctx: Context): embed.add_field( name=t.poll_config.duration.name, value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.time(cnt=max_time), - inline=False, ) - embed.add_field( - name=t.poll_config.max_duration.name, value=t.poll_config.max_duration.time(cnt=max_time), inline=False - ) - choice: int = await PdS.max_choices.get() or MAX_OPTIONS - embed.add_field( - name=t.poll_config.choices.name, - value=t.poll_config.choices.amount(cnt=choice) if not choice <= 0 else t.poll_config.choices.unlimited, - inline=False, - ) - anonymous: bool = await PdS.anonymous.get() - embed.add_field(name=t.poll_config.anonymous.name, value=str(anonymous), inline=False) - fair: bool = await PdS.fair.get() - embed.add_field(name=t.poll_config.fair.name, value=str(fair), inline=False) - roles = await RoleWeight.get(ctx.guild.id, PollType.STANDARD) - everyone: int = await PdS.everyone_power.get() - base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone) - if roles: - base += "".join(t.poll_config.roles.row(role.role_id, role.weight) for role in roles) - embed.add_field(name=t.poll_config.roles.name, value=base, inline=False) + embed.add_field(name=t.poll_config.max_duration.name, value=t.poll_config.max_duration.time(cnt=max_time)) + base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, await PdS.everyone_power.get()) + embed.add_field(name=t.poll_config.roles.name, value=base) await send_long_embed(ctx, embed, paginate=False) - @settings.command(name="roles_weights", aliases=["rw"]) - @PollsPermission.write.check - @docs(t.commands.poll.settings.roles_weights) - async def roles_weights(self, ctx: Context, role: Role, weight: float | None = None): - element = await db.get(RoleWeight, role_id=role.id) - - if not weight and not element: - raise CommandError(t.error.cant_set_weight) - - if weight and weight < 0.1: - raise CommandError(t.error.weight_too_small) - - if element and weight: - element.weight = weight - msg: str = t.texts.role_weight.set(role.id, weight) - elif weight and not element: - await RoleWeight.create(ctx.guild.id, role.id, weight, PollType.STANDARD) - msg: str = t.texts.role_weight.set(role.id, weight) - else: - await element.remove() - msg: str = t.texts.role_weight.reset(role.id) - - await add_reactions(ctx.message, "white_check_mark") - await send_to_changelog(ctx.guild, msg) - @settings.command(name="duration", aliases=["d"]) @PollsPermission.write.check @docs(t.commands.poll.settings.duration) @@ -777,43 +733,6 @@ async def max_duration(self, ctx: Context, days: int | None = None): await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, msg) - @settings.command(name="votes", aliases=["v", "choices", "c"]) - @PollsPermission.write.check - @docs(t.commands.poll.settings.votes) - async def votes(self, ctx: Context, votes: int | None = None): - if not votes: - votes = 0 - msg: str = t.texts.votes.reset - else: - msg: str = t.texts.votes.set(cnt=votes) - - if not 0 < votes < MAX_OPTIONS: - votes = 0 - - await PdS.max_choices.set(votes) - await add_reactions(ctx.message, "white_check_mark") - await send_to_changelog(ctx.guild, msg) - - @settings.command(name="anonymous", aliases=["a"]) - @PollsPermission.write.check - @docs(t.commands.poll.settings.anonymous) - async def anonymous(self, ctx: Context, status: bool): - msg: str = t.texts.anonymous.is_on if status else t.texts.anonymous.is_off - - await PdS.anonymous.set(status) - await add_reactions(ctx.message, "white_check_mark") - await send_to_changelog(ctx.guild, msg) - - @settings.command(name="fair", aliases=["f"]) - @PollsPermission.write.check - @docs(t.commands.poll.settings.fair) - async def fair(self, ctx: Context, status: bool): - msg: str = t.texts.fair.is_on if status else t.texts.fair.is_off - - await PdS.fair.set(status) - await add_reactions(ctx.message, "white_check_mark") - await send_to_changelog(ctx.guild, msg) - @poll.group(name="team", aliases=["t"]) @PollsPermission.team_poll.check @docs(t.commands.poll.team.team) @@ -905,10 +824,8 @@ async def quick(self, ctx: Context, *, args: str): ctx=ctx, title=t.poll.standard, poll_args=args, - max_choices=await PdS.max_choices.get() or MAX_OPTIONS, + max_choices=MAX_OPTIONS, deadline=await PdS.duration.get() * 60 * 60 or await PdS.max_duration.get() * 60 * 60 * 24, - anonymous=await PdS.anonymous.get(), - fair=await PdS.fair.get(), can_delete=True, ) @@ -944,7 +861,6 @@ async def new(self, ctx: Context, *, options: str): max_choices=parsed.choices, deadline=deadline, anonymous=parsed.anonymous, - fair=parsed.fair, can_delete=True, ) await ctx.message.delete() diff --git a/general/polls/models.py b/general/polls/models.py index c0c66fe2f..6e5ebe2b4 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -49,6 +49,7 @@ class Poll(Base): id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True) options: list[Option] = relationship("Option", back_populates="poll", cascade="all, delete") + roles: list[RoleWeight] = relationship("RoleWeight", back_populates="poll", cascade="all, delete") message_id: Union[Column, int] = Column(BigInteger, unique=True) message_url: Union[Column, str] = Column(Text(256)) @@ -83,8 +84,8 @@ async def create( poll_type: enum.Enum, interaction: int, thread: int, - fair: bool, max_choices: int, + roles: list[tuple[int, float]] | None = None, ) -> Poll: row = Poll( message_id=message_id, @@ -100,7 +101,6 @@ async def create( can_delete=can_delete, interaction_message_id=interaction, thread_id=thread, - fair=fair, status=PollStatus.ACTIVE, last_time_state_change=utcnow(), max_choices=max_choices, @@ -115,6 +115,8 @@ async def create( field_position=position, ) ) + for role_id, weight in roles or []: + row.roles.append(await RoleWeight.create(message_id, role_id, weight)) await db.add(row) return row @@ -166,17 +168,14 @@ class RoleWeight(Base): __tablename__ = "role_weight" id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True) - guild_id: Union[Column, int] = Column(BigInteger) role_id: Union[Column, int] = Column(BigInteger, unique=True) weight: Union[Column, float] = Column(Float) - poll_type: Union[Column, PollType] = Column(Enum(PollType)) - timestamp: Union[Column, datetime] = Column(UTCDateTime) + poll: Poll = relationship("Poll", back_populates="roles") + poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll.message_id")) @staticmethod - async def create(guild_id: int, role: int, weight: float, poll_type: PollType) -> RoleWeight: - role_weight = RoleWeight( - guild_id=guild_id, role_id=role, weight=weight, timestamp=utcnow(), poll_type=poll_type - ) + async def create(poll_id: int, role: int, weight: float) -> RoleWeight: + role_weight = RoleWeight(poll_id=poll_id, role_id=role, weight=weight) await db.add(role_weight) await sync_redis() return role_weight diff --git a/general/polls/settings.py b/general/polls/settings.py index a7ec5047a..7e55d9a11 100644 --- a/general/polls/settings.py +++ b/general/polls/settings.py @@ -4,10 +4,7 @@ class PollsDefaultSettings(Settings): duration = 0 # 0 for max_duration duration (duration in hours) max_duration = 7 # max duration (duration in days) - max_choices = 0 # 0 for unlimited choices everyone_power = 1.0 - anonymous = False - fair = False class PollsTeamSettings(Settings): diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 49cfae85c..6a591aba3 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -67,7 +67,7 @@ error: poll_config: title: Default poll configuration duration: - name: "**Duration**" + name: "**Default Duration**" time: one: "{cnt} hour" many: "{cnt} hours" @@ -77,20 +77,9 @@ poll_config: time: one: "{cnt} days" many: "{cnt} days" - choices: - name: "**Choices per user**" - amount: - one: "{cnt} choice per user" - many: "{cnt} choices per user" - unlimited: unlimited - anonymous: - name: "**Anonymous**" - fair: - name: "**Fair polls**" roles: name: "**Role Weights**" ev_row: "{} -> `{}x`" - row: "\n<@&{}> -> `{}x`" polls: title: "List of {} {} polls" @@ -121,23 +110,17 @@ wizard: args: name: Arguments value: | - ``` - --deadline DEADLINE, -D DEADLINE - time when the poll should be closed [Default: server settings] - - --anonymous {True,False}, -A {True,False} - people can see who voted or not [Default: server settings] - - --choices CHOICES, -C CHOICES - the amount of votes someone can set [Default: multiple choices] - - --fair {True,False}, -F {True,False} - all roles have the same vote weight [Default: server settings] + ```py + --deadline DEADLINE, -D DEADLINE |time in hours| 0 for server default + --anonymous {True,False}, -A {True,False} |public who voted| True or False + --choices CHOICES, -C CHOICES |amount of choices per user| 0 for multiple choice + --roles ROLES, -R ROLES |which roles can participate| ("role1, role2, role3, ...") "0" for all + --weights WEIGHTS, -W WEIGHTS |role weights| ("role1: weight, role2: weight, ...") "0" for server default / "1" for fair ``` example: name: Example value: | - `--duration 6 --choices 4 -A True` + `--deadline 6 --choices 4 -A True` --> Creates an anonymous, 6 hours long poll with 4 select choices for every user @@ -183,17 +166,6 @@ texts: set: one: "Set maximum duration for a poll to {cnt} day" many: "Set maximum duration for a poll to {cnt} days" - votes: - set: - one: "Set default votes for a poll to {cnt} vote" - many: "Set default votes for a poll to {cnt} votes" - reset: "Set the default votes for polls to unlimited" - anonymous: - is_on: made default poll votes anonymous - is_off: made default poll votes visible - fair: - is_on: made default poll votes fair - is_off: made default poll votes based on roles ignore: title: Ignore removed: "<@{}> was removed from the list of ignored member" From afb5d50561e7c28638ba66124959985c43c57fa2 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 29 Jul 2022 23:06:32 +0200 Subject: [PATCH 86/95] Formatted code, fixed typing, cleared translation --- general/polls/cog.py | 40 ++++++++++++++++++------------- general/polls/translations/en.yml | 12 ++++++---- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 0b0d9d119..a4f71afd9 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -2,7 +2,6 @@ from argparse import ArgumentParser, Namespace from datetime import datetime from io import BytesIO -from typing import Optional, Union import matplotlib.pyplot as plt import numpy as np @@ -108,7 +107,7 @@ async def get_parser() -> ArgumentParser: return parser -def calc_end_time(duration: Optional[float]) -> Optional[datetime]: +def calc_end_time(duration: float | None) -> datetime | None: """returns the time when a poll should be closed""" return utcnow() + relativedelta(seconds=int(duration)) if duration else None @@ -119,9 +118,11 @@ async def send_poll( poll_args: str, max_choices: int = None, team_poll: bool = False, - deadline: Optional[int] = None, + deadline: int | None = None, anonymous: bool = False, can_delete: bool = False, + allowed_roles: list[int] | None = None, + weights: list[tuple[int, float]] | None = None, ): """sends a poll embed + view message containing the select field""" @@ -134,25 +135,38 @@ async def send_poll( raise CommandError(t.error.missing_options) if len(options) > MAX_OPTIONS: raise CommandError(t.error.too_many_options(MAX_OPTIONS)) - if team_poll and len(options) >= MAX_OPTIONS: - raise CommandError(t.error.too_many_options(MAX_OPTIONS - 1)) options = [await PollOption().init(ctx, line, i) for i, line in enumerate(options)] if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options): raise CommandError(t.error.option_too_long(EmbedLimits.FIELD_VALUE)) + if not max_choices or isinstance(max_choices, str): + place = t.poll.select.place + max_value = len(options) + else: + options_amount = len(options) if max_choices >= len(options) else max_choices + place: str = t.poll.select.placeholder(cnt=options_amount) + max_value = options_amount + embed = Embed(title=title, description=question, color=Colors.Polls, timestamp=utcnow()) embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url) + embed.add_field(name=t.poll.choices.name, value=str(max_value)) + if allowed_roles: + embed.add_field(name=t.poll.roles.name, value=" ".join([f"<@&{role}>" for role in allowed_roles])) + if weights: + embed.add_field( + name=t.poll.weights.name, value=" ".join([f"<@&{role}>: `{weight}`" for role, weight in weights]) + ) + if deadline: embed.set_footer(text=t.poll.footer.default(calc_end_time(deadline).strftime("%Y-%m-%d %H:%M"))) if len({option.emoji for option in options}) < len(options): raise CommandError(t.error.option_duplicated) - for i, option in enumerate(options): - embed.add_field(name=t.poll.option(i + 1), value=str(option), inline=False) + embed.add_field(name=t.poll.option, value="\n".join([str(opt) for opt in options]), inline=False) poll_type: PollType = PollType.STANDARD if team_poll: @@ -165,14 +179,6 @@ async def send_poll( poll_type = poll_type.TEAM embed.add_field(name=field[0], value=field[1], inline=False) - if not max_choices or isinstance(max_choices, str): - place = t.poll.select.place - max_value = len(options) - else: - options_amount = len(options) if max_choices >= len(options) else max_choices - place: str = t.poll.select.placeholder(cnt=options_amount) - max_value = options_amount - msg = await ctx.send(embed=embed) select_obj = MySelect( custom_id=str(msg.id), @@ -848,7 +854,7 @@ async def new(self, ctx: Context, *, options: str): parsed: Namespace = parser.parse_known_args(args.split())[0] max_deadline = await PdS.max_duration.get() * 60 * 60 * 24 - deadline: Union[list[str, str], int] = parsed.deadline + deadline: list[str, str] | int = parsed.deadline if isinstance(deadline, int): deadline = deadline or max_deadline if deadline <= max_deadline else max_deadline else: @@ -868,7 +874,7 @@ async def new(self, ctx: Context, *, options: str): @commands.command(aliases=["yn"]) @guild_only() @docs(t.commands.yes_no) - async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Optional[str] = None): + async def yesno(self, ctx: Context, message: Message | None = None, text: str | None = None): if message is None or message.guild is None or text: message = ctx.message diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 6a591aba3..83cfc2d59 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -12,12 +12,8 @@ commands: list: show the active polls settings: settings: poll settings - roles_weights: manage weight for certain roles duration: set the default hours a poll should be open max_duration: set the maximum duration a poll can be opened - votes: set the default amount of votes a user can have on polls - anonymous: set if user can see who voted on a poll - fair: manage if role weights impact on default polls team: team: create and manage team-polls conclude: close a team-poll by accepting or rejecting @@ -143,7 +139,13 @@ poll: one: "Select an option!" many: "Select up to {cnt} options!" label: "Option {}." - option: "Option {}:" + option: "Options:" + choices: + name: "Choices per user:" + roles: + name: "Allowed roles:" + weights: + name: "Weights for roles" results: "Poll results:" status_changed: Poll is now {}! all_voted: ":white_check_mark: All team-members have voted!" From cd95762b651a36f2cb84dc64411e459574da2ce7 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 1 Aug 2022 20:17:04 +0200 Subject: [PATCH 87/95] improved argparse + added option for weights and allowed roles --- general/polls/cog.py | 90 ++++++++++++++++++++++++------- general/polls/models.py | 17 ++++-- general/polls/settings.py | 1 - general/polls/translations/en.yml | 6 --- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index a4f71afd9..4ff03def5 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -1,5 +1,6 @@ +import shlex import string -from argparse import ArgumentParser, Namespace +from argparse import ArgumentParser from datetime import datetime from io import BytesIO @@ -16,10 +17,21 @@ Message, NotFound, RawMessageDeleteEvent, + Role, SelectOption, ) from discord.ext import commands, tasks -from discord.ext.commands import Bot, CommandError, Context, EmojiConverter, EmojiNotFound, UserInputError, guild_only +from discord.ext.commands import ( + BadArgument, + Bot, + CommandError, + Context, + EmojiConverter, + EmojiNotFound, + RoleConverter, + UserInputError, + guild_only, +) from discord.ui import Select, View from discord.utils import format_dt, utcnow @@ -74,6 +86,42 @@ def __str__(self): return f"{self.emoji} {self.option}" if self.option else self.emoji +class RoleParser: + def __init__(self, ctx: Context): + self.ctx = ctx + + async def parse_roles(self, raw: str) -> list[int]: + out: list[int] = [] + parsed = raw.replace(" ", "").split(",") + for role_str in parsed: + try: + role: Role = await RoleConverter().convert(self.ctx, role_str) + out.append(role.id) + except BadArgument: + continue + + return out + + async def parse_weights(self, raw: str) -> list[tuple[int, float]]: + out: list[tuple[int, float]] = [] + parsed = raw.replace(" ", "").split(",") + for role_str in parsed: + spl = role_str.split(":") + if not len(spl) == 2: + continue + try: + weight = float(spl[1]) + except ValueError: + continue + try: + role: Role = await RoleConverter().convert(self.ctx, spl[0]) + out.append((role.id, weight)) + except BadArgument: + continue + + return out + + def create_select_view(select_obj: Select, timeout: float = None) -> View: """returns a view object""" view = View(timeout=timeout) @@ -101,8 +149,8 @@ async def get_parser() -> ArgumentParser: parser.add_argument("--deadline", "-D", default=0, type=int) parser.add_argument("--anonymous", "-A", default=False, type=bool, choices=[True, False]) parser.add_argument("--choices", "-C", default=MAX_OPTIONS, type=int) - parser.add_argument("--roles", "-R", default="0", type=str) - parser.add_argument("--weights", "-W", default="0", type=str) + parser.add_argument("--roles", "-R", default="none", type=str) + parser.add_argument("--weights", "-W", default="none", type=str) return parser @@ -221,6 +269,7 @@ async def send_poll( interaction=view_msg.id, max_choices=max_choices, thread=thread.id, + weights=weights, ) @@ -448,7 +497,7 @@ class MySelect(Select): """adds a method for handling interactions with the select menu""" @db_wrapper - async def callback(self, interaction): + async def callback(self, interaction): # TODO: needs to check for weights and allowed roles user = interaction.user selected_options: list = self.values message: Message = await interaction.channel.fetch_message(interaction.custom_id) @@ -473,7 +522,7 @@ async def callback(self, interaction): await vote.remove() opt.votes.remove(vote) - ev_pover = await PdS.everyone_power.get() + ev_pover = 1 if poll.fair: user_weight: float = ev_pover else: @@ -650,7 +699,7 @@ async def voted(self, ctx: Context, message: Message): @optional_permissions(PollsPermission.manage) @docs(t.commands.poll.result) async def result(self, ctx: Context, message: Message, show_all: bool = False): - poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + poll: Poll = await db.get(Poll, (Poll.options, Poll.roles, Option.votes), message_id=message.id) if poll.status == PollStatus.ACTIVE and not poll.owner_id == ctx.author.id: raise CommandError(t.error.still_active) if not poll: @@ -709,10 +758,8 @@ async def settings(self, ctx: Context): value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.time(cnt=max_time), ) embed.add_field(name=t.poll_config.max_duration.name, value=t.poll_config.max_duration.time(cnt=max_time)) - base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, await PdS.everyone_power.get()) - embed.add_field(name=t.poll_config.roles.name, value=base) - await send_long_embed(ctx, embed, paginate=False) + await send_long_embed(ctx, embed) @settings.command(name="duration", aliases=["d"]) @PollsPermission.write.check @@ -783,7 +830,7 @@ async def ignore(self, ctx: Context, member: Member): @PollsPermission.manage.check @docs(t.commands.poll.team.conclude) async def conclude(self, ctx: Context, message: Message, accepted: bool): - poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id) + poll: Poll = await db.get(Poll, (Poll.options, Poll.roles, Option.votes), message_id=message.id) if not poll or poll.poll_type != PollType.TEAM or poll.status == PollStatus.CLOSED or not message.embeds: raise CommandError(t.error.not_poll) @@ -841,7 +888,7 @@ async def quick(self, ctx: Context, *, args: str): @docs(t.commands.poll.new) async def new(self, ctx: Context, *, options: str): wizard = await ctx.send(embed=build_wizard()) - mess: Message = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author, timeout=60.0) + mess: Message = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author, timeout=120.0) args = mess.content if args.lower() == t.wizard.skip.message: @@ -851,14 +898,17 @@ async def new(self, ctx: Context, *, options: str): await mess.delete() parser = await get_parser() - parsed: Namespace = parser.parse_known_args(args.split())[0] + parsed, _ = parser.parse_known_args(shlex.split(args)) - max_deadline = await PdS.max_duration.get() * 60 * 60 * 24 - deadline: list[str, str] | int = parsed.deadline - if isinstance(deadline, int): - deadline = deadline or max_deadline if deadline <= max_deadline else max_deadline - else: - deadline = await PdS.duration.get() * 60 * 60 or max_deadline + max_deadline = await PdS.max_duration.get() * 24 + deadline: int = parsed.deadline + if deadline == 0: + deadline = await PdS.duration.get() + deadline = max_deadline if not deadline or deadline > max_deadline else deadline + deadline *= 3600 + + roles = await RoleParser(ctx).parse_roles(parsed.roles) + weights = await RoleParser(ctx).parse_weights(parsed.weights) await send_poll( ctx=ctx, @@ -868,6 +918,8 @@ async def new(self, ctx: Context, *, options: str): deadline=deadline, anonymous=parsed.anonymous, can_delete=True, + allowed_roles=roles, + weights=weights, ) await ctx.message.delete() diff --git a/general/polls/models.py b/general/polls/models.py index 6e5ebe2b4..2c01eb7e5 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -68,6 +68,7 @@ class Poll(Base): status: Union[Column, PollStatus] = Column(Enum(PollStatus)) last_time_state_change: Union[Column, datetime] = Column(UTCDateTime) max_choices: Union[Column, int] = Column(BigInteger) + limited: Union[Column, bool] = Column(Boolean) @staticmethod async def create( @@ -85,7 +86,8 @@ async def create( interaction: int, thread: int, max_choices: int, - roles: list[tuple[int, float]] | None = None, + allowed_roles: list[int] | None = None, + weights: list[tuple[int, float]] | None = None, ) -> Poll: row = Poll( message_id=message_id, @@ -104,6 +106,8 @@ async def create( status=PollStatus.ACTIVE, last_time_state_change=utcnow(), max_choices=max_choices, + limited=bool(allowed_roles), + fair=bool(weights), ) for position, poll_option in enumerate(options): row.options.append( @@ -115,8 +119,15 @@ async def create( field_position=position, ) ) - for role_id, weight in roles or []: - row.roles.append(await RoleWeight.create(message_id, role_id, weight)) + + if not weights: + for role_id in allowed_roles or []: + row.roles.append(await RoleWeight.create(message_id, role_id, 1.0)) + else: + for role_id, weight in weights: + if allowed_roles and role_id not in allowed_roles: + continue + row.roles.append(await RoleWeight.create(message_id, role_id, weight)) await db.add(row) return row diff --git a/general/polls/settings.py b/general/polls/settings.py index 7e55d9a11..5fe00116c 100644 --- a/general/polls/settings.py +++ b/general/polls/settings.py @@ -4,7 +4,6 @@ class PollsDefaultSettings(Settings): duration = 0 # 0 for max_duration duration (duration in hours) max_duration = 7 # max duration (duration in days) - everyone_power = 1.0 class PollsTeamSettings(Settings): diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml index 83cfc2d59..2fe53127f 100644 --- a/general/polls/translations/en.yml +++ b/general/polls/translations/en.yml @@ -73,9 +73,6 @@ poll_config: time: one: "{cnt} days" many: "{cnt} days" - roles: - name: "**Role Weights**" - ev_row: "{} -> `{}x`" polls: title: "List of {} {} polls" @@ -156,9 +153,6 @@ texts: voted: title: Votes row: "\n <@{}> -> Options: {}" - role_weight: - set: "Set vote weight for <@&{}> to `{}`" - reset: "Vote weight has been reset for <@&{}>" duration: set: one: "Set default duration for poll to {cnt} hour" From 402fdb1d0cab7cf26f99430f4bb2d8694e845216 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 1 Aug 2022 20:40:25 +0200 Subject: [PATCH 88/95] Fixed permissions for poll delete command --- general/polls/cog.py | 4 +--- general/polls/models.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 4ff03def5..5b6603f7f 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -651,10 +651,8 @@ async def delete(self, ctx: Context, message: Message): poll: Poll = await db.get(Poll, message_id=message.id) if not poll: raise CommandError(t.error.not_poll) - if poll.can_delete and not poll.owner_id == ctx.author.id: + if not poll.owner_id == ctx.author.id and not PollsPermission.manage.check(ctx): raise CommandError(tg.not_allowed) - elif not poll.can_delete and not poll.owner_id == ctx.author.id: - raise CommandError(tg.not_allowed) # if delete is False, only the owner can delete it if not await Confirmation().run(ctx, t.texts.delete.confirm): return diff --git a/general/polls/models.py b/general/polls/models.py index 2c01eb7e5..409b1f545 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -63,7 +63,7 @@ class Poll(Base): poll_type: Union[Column, PollType] = Column(Enum(PollType)) end_time: Union[Column, int] = Column(BigInteger) anonymous: Union[Column, bool] = Column(Boolean) - can_delete: Union[Column, bool] = Column(Boolean) + can_delete: Union[Column, bool] = Column(Boolean) # TODO: nicht nötig fair: Union[Column, bool] = Column(Boolean) status: Union[Column, PollStatus] = Column(Enum(PollStatus)) last_time_state_change: Union[Column, datetime] = Column(UTCDateTime) From aa4aa321cad09fe0f255d29893fc384f2682e177 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 1 Aug 2022 20:45:27 +0200 Subject: [PATCH 89/95] removed can_delete for polls --- general/polls/cog.py | 5 ----- general/polls/models.py | 3 --- 2 files changed, 8 deletions(-) diff --git a/general/polls/cog.py b/general/polls/cog.py index 5b6603f7f..46f368d7e 100644 --- a/general/polls/cog.py +++ b/general/polls/cog.py @@ -168,7 +168,6 @@ async def send_poll( team_poll: bool = False, deadline: int | None = None, anonymous: bool = False, - can_delete: bool = False, allowed_roles: list[int] | None = None, weights: list[tuple[int, float]] | None = None, ): @@ -263,7 +262,6 @@ async def send_poll( title=question, end=deadline, anonymous=anonymous, - can_delete=can_delete, options=parsed_options, poll_type=poll_type, interaction=view_msg.id, @@ -877,7 +875,6 @@ async def quick(self, ctx: Context, *, args: str): poll_args=args, max_choices=MAX_OPTIONS, deadline=await PdS.duration.get() * 60 * 60 or await PdS.max_duration.get() * 60 * 60 * 24, - can_delete=True, ) await ctx.message.delete() @@ -915,7 +912,6 @@ async def new(self, ctx: Context, *, options: str): max_choices=parsed.choices, deadline=deadline, anonymous=parsed.anonymous, - can_delete=True, allowed_roles=roles, weights=weights, ) @@ -955,5 +951,4 @@ async def team_yesno(self, ctx: Context, *, text: str): poll_args=t.yes_no.option_string(text), team_poll=True, deadline=await PtS.duration.get() * 60 * 60 * 24, - can_delete=False, ) diff --git a/general/polls/models.py b/general/polls/models.py index 409b1f545..d376c036c 100644 --- a/general/polls/models.py +++ b/general/polls/models.py @@ -63,7 +63,6 @@ class Poll(Base): poll_type: Union[Column, PollType] = Column(Enum(PollType)) end_time: Union[Column, int] = Column(BigInteger) anonymous: Union[Column, bool] = Column(Boolean) - can_delete: Union[Column, bool] = Column(Boolean) # TODO: nicht nötig fair: Union[Column, bool] = Column(Boolean) status: Union[Column, PollStatus] = Column(Enum(PollStatus)) last_time_state_change: Union[Column, datetime] = Column(UTCDateTime) @@ -81,7 +80,6 @@ async def create( options: list[tuple[str, str, str]], end: Optional[int], anonymous: bool, - can_delete: bool, poll_type: enum.Enum, interaction: int, thread: int, @@ -100,7 +98,6 @@ async def create( poll_type=poll_type, end_time=end, anonymous=anonymous, - can_delete=can_delete, interaction_message_id=interaction, thread_id=thread, status=PollStatus.ACTIVE, From 302bb6829cd2ff7c749cb93d2160104df3154601 Mon Sep 17 00:00:00 2001 From: Tristan <45330667+Tristan-H11@users.noreply.github.com> Date: Mon, 1 Aug 2022 22:31:36 +0200 Subject: [PATCH 90/95] =?UTF-8?q?Akzeptanzkriterien=20f=C3=BCr=20einen=20b?= =?UTF-8?q?esseren=20Abgleich=20mit=20dem=20Code=20erstellt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- general/polls/Akzeptanzkriterien.txt | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 general/polls/Akzeptanzkriterien.txt diff --git a/general/polls/Akzeptanzkriterien.txt b/general/polls/Akzeptanzkriterien.txt new file mode 100644 index 000000000..49ae81f4d --- /dev/null +++ b/general/polls/Akzeptanzkriterien.txt @@ -0,0 +1,62 @@ +## Grundsätzliches +- Ein Poll bleibt x Tage lang aktiv (exklusive Pausen). Die Dauer wird bei Erstellung des Polls festgelegt. +- Stimmen sind gewichtet (default: 1) + - Das Gewicht ist an die Rolle gebunden und rollenspezifisch am expliziten Poll konfigurierbar. +- Der Poll kann auf bestimmte Rollen beschränkt werden. Andere können dann nicht teilnehmen +- Im Poll können Nutzer `1..n` Optionen auswählen. (Default: unlimited) +- Der Poll kann anonym sein. (Ergebnisse anonym) +- Ein Poll kann per Befehl aktiviert und deaktiviert werden. Während dieser Zeit stoppt die "Laufzeit" +- Ein aktiver Poll ist angepinnt +- Es wird für jeden Poll ein Thread erstellt +- Es wird ein Select Menü erstellt (Abstimmfunktion) +- Nur Yes-No Polls laufen über Reaktionen und werden nicht geloggt. +- Es gibt Team- und Standard-"Yes-Nos" + - Team: + - `.tyn ` + - Ein regulärer Team-Poll mit einer Stimme und den Voreinstellungen: "Dafür", "Dagegen", "Enthalten" + - Standard-Yes-No: + - `.yn ` +- Abgelaufene Polls bleiben deaktiviert und können nicht reaktiviert werden. +- Nach Ende des Polls wird ein Kuchendiagramm der Stimmverteilung als Embed gesendet. +- Sobald ein Teil des vom Bot erstellten Polls gelöscht wird, wird der gesamte Poll gelöscht. (Embed oder Select-Menü) +- Es gibt einen List-Command mit Filter-Parameter folgender Möglichkeiten: Aktiv, Inaktiv +- Der Poll-Owner und User mit einer festgelegten Rolle können auf Befehl die Stimmen nicht-anonym einsehen. + +## Parameter der Polls +``` +--deadline DEADLINE, -D DEADLINE |time in hours| 0 for server default +--anonymous {True,False}, -A {True,False} |public who voted| True or False +--choices CHOICES, -C CHOICES |amount of choices per user| 0 for multiple choice +--roles ROLES, -R ROLES |which roles can participate| ("role1, role2, role3, ...") "0" for all +--weights WEIGHTS, -W WEIGHTS |role weights| ("role1: weight, role2: weight, ...") "0" for server default / "1" for fair +``` + +### Poll mit Default-Settings erstellen: +``` +.poll quick \n +