From 000668799b507ad33f88248e4c618b8551127d55 Mon Sep 17 00:00:00 2001 From: dnns01 <github@dnns01.de> Date: Sat, 18 Feb 2023 23:54:56 +0100 Subject: [PATCH] Enhance polls with anonymous polls, single or multiple choice polls, usage of buttons and dropdown, sorted in descending order. Also polls can be closed and deleted by mods, too. (#210) Co-authored-by: dnns01 <git@dnns01.de> --- extensions/polls.py | 133 +++++++++++++++++++++++++++++++------------- requirements.txt | 2 +- views/poll_view.py | 119 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 41 deletions(-) create mode 100644 views/poll_view.py diff --git a/extensions/polls.py b/extensions/polls.py index 56dba8f..d0ae119 100644 --- a/extensions/polls.py +++ b/extensions/polls.py @@ -1,59 +1,112 @@ -import os +import enum +import json -from discord import Interaction, app_commands +import discord +import emoji +from discord import app_commands, Interaction from discord.ext import commands -from extensions.components.poll.poll import Poll +from views.poll_view import PollView +DEFAULT_CHOICES = ["š¦", "š§", "šØ", "š©", "šŖ", "š«", "š¬", "š", "š®", "šÆ", "š°", "š±", "š²", "š³", "š“", "šµ", "š¶", + "š·", "šø", "š¹"] + +class PollType(enum.Enum): + single_choice = "single" + multiple_choice = "multiple" + + +@app_commands.guild_only() class Polls(commands.GroupCog, name="poll", description="Handle Polls in Channels"): def __init__(self, bot): self.bot = bot - self.poll_sugg_channel = int(os.getenv("DISCORD_POLL_SUGG_CHANNEL")) + self.polls = {} + self.load() + + def load(self): + try: + with open("data/polls.json", "r") as polls_file: + self.polls = json.load(polls_file) + except FileNotFoundError: + pass + + def save(self): + with open("data/polls.json", "w") as polls_file: + json.dump(self.polls, polls_file) @app_commands.command(name="add", description="Erstelle eine Umfrage mit bis zu 20 Antwortmƶglichkeiten.") - @app_commands.describe(question="Welche Frage mƶchtest du stellen?", choice_a="1. Antwortmƶglichkeit", - choice_b="2. Antwortmƶglichkeit", choice_c="3. Antwortmƶglichkeit", - choice_d="4. Antwortmƶglichkeit", choice_e="5. Antwortmƶglichkeit", - choice_f="6. Antwortmƶglichkeit", choice_g="7. Antwortmƶglichkeit", - choice_h="8. Antwortmƶglichkeit", choice_i="9. Antwortmƶglichkeit", - choice_j="10. Antwortmƶglichkeit", choice_k="11. Antwortmƶglichkeit", - choice_l="12. Antwortmƶglichkeit", choice_m="13. Antwortmƶglichkeit", - choice_n="14. Antwortmƶglichkeit", choice_o="15. Antwortmƶglichkeit", - choice_p="16. Antwortmƶglichkeit", choice_q="17. Antwortmƶglichkeit", - choice_r="18. Antwortmƶglichkeit", choice_s="19. Antwortmƶglichkeit", - choice_t="20. Antwortmƶglichkeit") - async def cmd_poll(self, interaction: Interaction, question: str, choice_a: str, choice_b: str, + @app_commands.describe( + type="Umfragetyp, single_choice: nur eine Antwort kann ausgewƤhlt werden, multiple_choice: Mehrere Antwortmƶglichkeiten wƤhlbar.", + anonymous="Bei einer Anonymen Umfrage kann nicht nachgeschaut werden, welcher Teilnehmer wofür abgestimmt hat.", + question="Welche Frage mƶchtest du stellen?", choice_a="1. Antwortmƶglichkeit", + choice_b="2. Antwortmƶglichkeit", choice_c="3. Antwortmƶglichkeit", choice_d="4. Antwortmƶglichkeit", + choice_e="5. Antwortmƶglichkeit", choice_f="6. Antwortmƶglichkeit", choice_g="7. Antwortmƶglichkeit", + choice_h="8. Antwortmƶglichkeit", choice_i="9. Antwortmƶglichkeit", choice_j="10. Antwortmƶglichkeit", + choice_k="11. Antwortmƶglichkeit", choice_l="12. Antwortmƶglichkeit", choice_m="13. Antwortmƶglichkeit", + choice_n="14. Antwortmƶglichkeit", choice_o="15. Antwortmƶglichkeit", choice_p="16. Antwortmƶglichkeit", + choice_q="17. Antwortmƶglichkeit", choice_r="18. Antwortmƶglichkeit", choice_s="19. Antwortmƶglichkeit", + choice_t="20. Antwortmƶglichkeit") + async def cmd_poll(self, interaction: Interaction, type: PollType, anonymous: bool, question: str, choice_a: str, + choice_b: str, choice_c: str = None, choice_d: str = None, choice_e: str = None, choice_f: str = None, choice_g: str = None, choice_h: str = None, choice_i: str = None, choice_j: str = None, choice_k: str = None, choice_l: str = None, choice_m: str = None, choice_n: str = None, choice_o: str = None, choice_p: str = None, choice_q: str = None, choice_r: str = None, choice_s: str = None, choice_t: str = None): + """ Create a new poll """ + choices = [self.parse_choice(index, choice) for index, choice in enumerate( + [choice_a, choice_b, choice_c, choice_d, choice_e, choice_f, choice_g, choice_h, choice_i, choice_j, + choice_k, choice_l, choice_m, choice_n, choice_o, choice_p, choice_q, choice_r, choice_s, choice_t]) if + choice] + await interaction.response.defer() - """ Create poll """ - answers = [choice for choice in - [choice_a, choice_b, choice_c, choice_d, choice_e, choice_f, choice_g, choice_h, choice_i, choice_j, - choice_k, choice_l, choice_m, choice_n, choice_o, choice_p, choice_q, choice_r, choice_s, choice_t] - if choice] - - await Poll(self.bot, question, list(answers), interaction.user.id).send_poll(interaction) - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id: - return - - if payload.emoji.name in ["šļø", "š"]: - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - if len(message.embeds) > 0 and message.embeds[0].title == "Umfrage": - poll = Poll(self.bot, message=message) - if str(payload.user_id) == poll.author: - if payload.emoji.name == "šļø": - await poll.delete_poll() - else: - await poll.close_poll() + poll = {"type": type.value, "anonymous": anonymous, "question": question, "author": interaction.user.id, + "choices": choices, "participants": {}} + await interaction.edit_original_response(embed=self.get_embed(poll), view=PollView(self)) + message = await interaction.original_response() + self.polls[str(message.id)] = poll + self.save() + + def get_embed(self, poll) -> discord.Embed: + embed = discord.Embed(title="Umfrage", description=poll["question"]) + embed.add_field(name="Erstellt von", value=f'<@!{poll["author"]}>', inline=False) + embed.add_field(name="\u200b", value="\u200b", inline=False) + choices = sorted(poll["choices"], key=lambda x: x[2], reverse=True) + + for choice in choices: + name = f'{choice[0]} {choice[1]}' + value = f'{choice[2]}' + + embed.add_field(name=name, value=value, inline=False) + + embed.add_field(name="\u200b", value="\u200b", inline=False) + embed.add_field(name="Anzahl der Teilnehmer an der Umfrage", value=f"{len(poll['participants'])}", inline=False) + + return embed + + def parse_choice(self, idx: int, choice: str): + choice = choice.strip() + index = choice.find(" ") + + if index > -1: + possible_option = choice[:index] + if emoji.is_emoji(possible_option) or possible_option in DEFAULT_CHOICES: + if len(choice[index:].strip()) > 0: + return [possible_option, choice[index:].strip(), 0] + elif len(possible_option) > 1: + if (possible_option[0:2] == "<:" or possible_option[0:3] == "<a:") and possible_option[-1] == ">": + splitted_custom_emoji = possible_option.strip("<a:>").split(":") + if len(splitted_custom_emoji) == 2: + id = splitted_custom_emoji[1] + custom_emoji = self.bot.get_emoji(int(id)) + if custom_emoji and len(choice[index:].strip()) > 0: + return [custom_emoji, choice[index:].strip(), 0] + + return [DEFAULT_CHOICES[idx], choice, 0] async def setup(bot: commands.Bot) -> None: - await bot.add_cog(Polls(bot)) + polls = Polls(bot) + await bot.add_cog(polls) + bot.add_view(PollView(polls)) diff --git a/requirements.txt b/requirements.txt index cfe501e..cbc5aa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ certifi==2022.12.7 cffi==1.15.1 charset-normalizer==2.1.1 discord.py==2.1.0 -emoji==1.2.0 +emoji==2.2.0 frozenlist==1.3.3 idna==3.4 multidict==6.0.4 diff --git a/views/poll_view.py b/views/poll_view.py new file mode 100644 index 0000000..390eb4e --- /dev/null +++ b/views/poll_view.py @@ -0,0 +1,119 @@ +import discord + +import utils + + +async def show_participants(interaction, poll, ephemeral): + msg = f"Teilnehmer der Umfrage `{poll['question']}`:\n" + participant_choices = [[] for _ in range(len(poll["choices"]))] + for participant, choices in poll["participants"].items(): + for choice in choices: + participant_choices[choice].append(participant) + + choices = poll["choices"] + for idx, participants in enumerate(participant_choices): + choice_msg = f"{choices[idx][0]} {choices[idx][1]} ({choices[idx][2]}):" + choice_msg += "<@" if choices[idx][2] > 0 else "" + choice_msg += ">, <@".join(participants) + choice_msg += ">\n" if choices[idx][2] > 0 else "" + if len(msg) + len(choice_msg) >= utils.MAX_MESSAGE_LEN: + await interaction.followup.send(msg, ephemeral=ephemeral) + msg = choice_msg + else: + msg += choice_msg + + await interaction.followup.send(msg, ephemeral=ephemeral) + + +class PollView(discord.ui.View): + def __init__(self, polls): + super().__init__(timeout=None) + self.polls = polls + + @discord.ui.button(label='Abstimmen', style=discord.ButtonStyle.green, custom_id='poll_view:vote', emoji="ā ") + async def vote(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + if poll := self.polls.polls.get(str(interaction.message.id)): + await interaction.followup.send( + f"{poll['question']}\n\n*(Nach der Abstimmung kannst du diese Nachricht verwerfen. Wenn die Abstimmung " + f"nicht funktioniert, bitte verwirf die Nachricht und Klicke erneut auf den Abstimmen Button der " + f"Abstimmung.)*", view=PollChoiceView(poll, interaction.user, interaction.message, self.polls), + ephemeral=True) + + @discord.ui.button(label='Teilnehmer', style=discord.ButtonStyle.blurple, custom_id='poll_view:participants', + emoji="š„") + async def participants(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + if poll := self.polls.polls.get(str(interaction.message.id)): + if poll["anonymous"]: + await interaction.followup.send( + "Diese Umfrage ist anonym. Daher kann ich dir nicht sagen, wer an dieser Umfrage teilgenommen hat.") + else: + await show_participants(interaction, poll, ephemeral=True) + + @discord.ui.button(label='Beenden', style=discord.ButtonStyle.gray, custom_id='poll_view:close', emoji="š") + async def close(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + if poll := self.polls.polls.get(str(interaction.message.id)): + if poll.get("author") == interaction.user.id or utils.is_mod(interaction.user): + if not poll["anonymous"]: + await show_participants(interaction, poll, ephemeral=False) + + del self.polls.polls[str(interaction.message.id)] + self.polls.save() + + await interaction.edit_original_response(view=None) + + @discord.ui.button(label='Lƶschen', style=discord.ButtonStyle.gray, custom_id='poll_view:delete', emoji="š") + async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + if poll := self.polls.polls.get(str(interaction.message.id)): + if poll.get("author") == interaction.user.id or utils.is_mod(interaction.user): + await interaction.followup.send(f"Umfrage {poll.get('question')} gelƶscht.", ephemeral=True) + await interaction.message.delete() + del self.polls[str(interaction.message.id)] + self.polls.save() + + +class PollChoiceView(discord.ui.View): + def __init__(self, poll, user, message, polls): + super().__init__(timeout=None) + self.poll = poll + self.user = user + self.add_item(PollDropdown(poll, user, message, polls)) + + +class PollDropdown(discord.ui.Select): + def __init__(self, poll, user, message, polls): + self.poll = poll + self.user = user + self.message = message + self.polls = polls + participant = self.poll["participants"].get(str(user.id)) + options = [discord.SelectOption(label=choice[1], emoji=choice[0], value=str(idx), + default=self.is_default(participant, idx)) for idx, choice in + enumerate(poll["choices"])] + max_values = 1 if poll["type"] == "single" else len(options) + + super().__init__(placeholder='Gib deine Stimme(n) jetzt ab....', min_values=0, max_values=max_values, + options=options) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer() + self.poll["participants"][str(interaction.user.id)] = [int(value) for value in self.values] + + choices = [0] * len(self.poll["choices"]) + for participant in self.poll["participants"].values(): + for choice in participant: + choices[choice] += 1 + + for idx, choice in enumerate(self.poll["choices"]): + choice[2] = choices[idx] + + await self.message.edit(embed=self.polls.get_embed(self.poll), view=PollView(self.poll)) + self.polls.save() + + def is_default(self, participant, idx): + if participant: + return idx in participant + return False -- GitLab