diff --git a/extensions/polls.py b/extensions/polls.py index 56dba8f9b791eb54334fee64e12784877f1e5c87..d0ae1194df504cf5e23b108b345c788c3ef018f3 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 cfe501e5ce09c29080f0feb9cf45d3e6810cb8fc..cbc5aa1fe5cf3dc9b6e21e2261e94f6af8286e5b 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 0000000000000000000000000000000000000000..390eb4e74658210a1d0438da2e8cc65d3c57ecaa --- /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