From c19335d4269ccdd4ccf56b51593e80c953b733c5 Mon Sep 17 00:00:00 2001 From: tony_sax Date: Sun, 29 Jan 2023 21:28:59 -0600 Subject: [PATCH 1/4] Work from November adding more UI concept stuff. --- star-voting.py | 170 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 15 deletions(-) diff --git a/star-voting.py b/star-voting.py index e301c2b..986825b 100644 --- a/star-voting.py +++ b/star-voting.py @@ -3,13 +3,15 @@ import os import traceback +import requests import discord from discord import TextStyle from discord.ext import commands from dotenv import load_dotenv load_dotenv() -token = os.getenv('TOKEN') +discord_token = os.getenv('DISCORD_TOKEN') +jwt_token = os.getenv('JWT_TOKEN') # The guild in which this slash command will be registered. # It is recommended to have a test guild to separate from your "production" bot @@ -18,12 +20,14 @@ class CandidateScorecardView(discord.ui.View): def __init__(self): + self.score_chosen = -1 super().__init__(timeout=None) @discord.ui.button(label='0', style=discord.ButtonStyle.grey, custom_id='persistent_view:0') async def zero(self, interaction: discord.Interaction, button: discord.ui.Button): # Make sure to update the message with our updated selves self.reset_all_buttons() + self.score_chosen = 0 button.style = discord.ButtonStyle.blurple button.disabled = True await interaction.response.edit_message(view=self) @@ -32,6 +36,7 @@ async def zero(self, interaction: discord.Interaction, button: discord.ui.Button async def one(self, interaction: discord.Interaction, button: discord.ui.Button): # Make sure to update the message with our updated selves self.reset_all_buttons() + self.score_chosen = 1 button.style = discord.ButtonStyle.blurple button.disabled = True await interaction.response.edit_message(view=self) @@ -40,6 +45,7 @@ async def one(self, interaction: discord.Interaction, button: discord.ui.Button) async def two(self, interaction: discord.Interaction, button: discord.ui.Button): # Make sure to update the message with our updated selves self.reset_all_buttons() + self.score_chosen = 2 button.style = discord.ButtonStyle.blurple button.disabled = True await interaction.response.edit_message(view=self) @@ -48,6 +54,7 @@ async def two(self, interaction: discord.Interaction, button: discord.ui.Button) async def three(self, interaction: discord.Interaction, button: discord.ui.Button): # Make sure to update the message with our updated selves self.reset_all_buttons() + self.score_chosen = 3 button.style = discord.ButtonStyle.blurple button.disabled = True await interaction.response.edit_message(view=self) @@ -56,6 +63,7 @@ async def three(self, interaction: discord.Interaction, button: discord.ui.Butto async def four(self, interaction: discord.Interaction, button: discord.ui.Button): # Make sure to update the message with our updated selves self.reset_all_buttons() + self.score_chosen = 4 button.style = discord.ButtonStyle.blurple button.disabled = True await interaction.response.edit_message(view=self) @@ -64,10 +72,20 @@ async def four(self, interaction: discord.Interaction, button: discord.ui.Button async def five(self, interaction: discord.Interaction, button: discord.ui.Button): # Make sure to update the message with our updated selves self.reset_all_buttons() + self.score_chosen = 5 button.style = discord.ButtonStyle.blurple button.disabled = True await interaction.response.edit_message(view=self) + @discord.ui.button(label='Prev', style=discord.ButtonStyle.grey, custom_id='persistent_view:prev') + async def prev(self, interaction: discord.Interaction, button: discord.ui.Button): + # Make sure to update the message with our updated selves + await interaction.response.edit_message(view=self) + + @discord.ui.button(label='Next', style=discord.ButtonStyle.grey, custom_id='persistent_view:next') + async def next(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.edit_message(view=self) + def reset_all_buttons(self): self.zero.style = discord.ButtonStyle.grey self.one.style = discord.ButtonStyle.grey @@ -87,12 +105,12 @@ class SubmitBallotView(discord.ui.View): def __init__(self): super().__init__(timeout=None) - @discord.ui.button(label='Submit', style=discord.ButtonStyle.green, custom_id='persistent_view:green') + @discord.ui.button(label='Submit Ballot 🗳', style=discord.ButtonStyle.green, custom_id='persistent_view:submit_ballot') async def submitballot(self, interaction: discord.Interaction, button: discord.ui.Button): - await interaction.response.send_message("Ballot submitted :)") + await interaction.response.send_message("Ballot submitted to the void :)", ephemeral=True) -class BallotView(discord.ui.View): +class BallotView(discord.ui.View, ): def __init__(self): super().__init__(timeout=None) @@ -103,9 +121,22 @@ def __init__(self): @discord.ui.button(label='Vote', style=discord.ButtonStyle.green, custom_id='persistent_view:vote') async def vote(self, interaction: discord.Interaction, button: discord.ui.Button): - # Make sure to update the message with our updated selves - await interaction.response.send_message("Score these candidates on a 0 to 5 scale.\nCandidate Name", - view=BallotView(), ephemeral=True) + message_sent = await interaction.send("```candidate_name\n```", ephemeral=True) + #await interaction.response.send_message(view=CandidateScorecardView(), ephemeral=True) + #await interaction.followup.send("Complete your ballot by clicking the emoji corresponding to the score you would give the candidate.") + #for candidate in args: + # message_sent = await ctx.send(candidate) + # await message_sent.add_reaction('0️⃣') + # await message_sent.add_reaction('1️⃣') + # await message_sent.add_reaction('2️⃣') + # await message_sent.add_reaction('3️⃣') + # await message_sent.add_reaction('4️⃣') + # await message_sent.add_reaction('5️⃣') + #await interaction.response.send_message("Score these candidates on a 0 to 5 scale.\nCandidate Name", + # view=CandidateScorecardView(), ephemeral=True) + # Webhooks + #await interaction.followup.send("Score these candidates on a 0 to 5 scale.\nCandidate Name", + # view=CandidateScorecardView(), ephemeral=True) class EmbedEdit(discord.ui.View): @@ -365,13 +396,13 @@ def prettify_candidates(candidate: tuple) -> str: @bot.command() @commands.is_owner() -async def star(ctx: commands.Context, *args): +async def new_star_embed(ctx: commands.Context, *args): """Creates an embed for a STAR voting election. - args: - ElectionTitle: Title of the election + Arguments: + ElectionTitle: Title of the election. (e.g. NewElection or "New Election") Days: Number of days the election will last. - Candidates: As many candidates as you want. Using both firstname and lastname requires "Jane Doe" - example: + Candidates: As many candidates as you want. (e.g. JaneDoe or "Jane Doe") + Example: .star "What is the best color?" 5 Blue Red Green Yellow Purple Orange """ # The first argument should be the election title. @@ -383,6 +414,10 @@ async def star(ctx: commands.Context, *args): endDate = endDate.strftime("%A, %B %d, %Y %H:%M:%S") # Example: Friday September 16, 2022 18:10:11 # Set the candidates into their own variable. candidateTuple = args[2:] + # stuff that wasn't working idk + #candidateTuple = () + #for candidate in args[2:]: + # candidateTuple = (candidateTuple + candidate) candidates = str(candidateTuple) # Pretty print the candidates #candidates_prettified = prettify_candidates(candidates) @@ -409,8 +444,8 @@ async def star(ctx: commands.Context, *args): embedVar.add_field(name="🟢 Green Party", value=f"> {candidates}", inline=True) embedVar.add_field(name="🔵 Blue Party", value=f"> {candidates}", inline=True) embedVar.add_field(name="🟣 Purple Party", value=f"> {candidates}", inline=True) - - embedVar.add_field(name="Current Vote Count:", value="#ofvotes", inline=False) + vote_count = 0 + embedVar.add_field(name="Current Vote Count:", value=vote_count, inline=False) # Set the large image that displays. image_simple_ballot = "https://d3n8a8pro7vhmx.cloudfront.net/unifiedprimary/pages/494/attachments/original/1632368538/STAR_Ballot.jpg?1632368538" @@ -434,10 +469,115 @@ async def star(ctx: commands.Context, *args): # ctx.author.guild_avatar displays as None if you don't have a server specific picture. embedVar.set_footer(text=f"Election created by {ctx.author.display_name}") + print("New STAR Embed Created at " + endDate) await ctx.send(embed=embedVar) await ctx.send(view=EmbedEdit(), ephemeral=True) await ctx.send(view=EmbedVote()) +@bot.command() +@commands.is_owner() +async def new_star_election(ctx: commands.Context, *args): + URL = "https://star-vote.herokuapp.com/API/Elections" + election_creator_id = ctx.author.id + print("Election creator id: " + str(election_creator_id)) + election_name = args[0] + days = int(args[1]) + print("Duration of Election: " + args[1] + " Days") + candidates = args[2:] + candidate_list = [] + for candidate in candidates: + print("Candidate: " + candidate) + new_candidate_obj = { "Candidate": { "candidate_name": candidate}} + candidate_list.append(new_candidate_obj) + new_race_obj = { + "Race": + { + "title": election_name, + "voting_method": "STAR", + "num_winners": 1, + "candidates": candidate_list + } + } + race_list = [new_race_obj] + new_election_settings_obj = { + "ElectionSettings": + { + "election_roll_type": "none", + "voter_id_type": "predefined voter ID" + } + } + new_election_obj = { + "Election": + { + "title": election_name, + "owner_id": str(election_creator_id), + "state": "dev testing", + "races": race_list, + "settings": new_election_settings_obj + } + } + + response = requests.post(URL, json = new_election_obj, cookies = {"id_token": jwt_token}) + print("response.url: " + response.url + "\n") + print("response.status_code: " + str(response.status_code) + "\n") + print("response.text: " + response.text + "\n") + print("\n\n") + + await new_star_embed(ctx, election_name, days, candidates) + +@bot.command() +@commands.is_owner() +async def list_star_candidates(ctx: commands.Context, *args): + candidate_scores = dict() + for candidate in args: + message_sent = await ctx.send("```" + candidate + "\n```", ephemeral=True) + candidate_scores[candidate] = 0 + await message_sent.add_reaction('0️⃣') + await message_sent.add_reaction('1️⃣') + await message_sent.add_reaction('2️⃣') + await message_sent.add_reaction('3️⃣') + await message_sent.add_reaction('4️⃣') + await message_sent.add_reaction('5️⃣') + #reaction = await bot.wait_for(['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣'], message_sent, ) + #await ctx.send("You responded with {}".format(reaction.emoji)) + + print("Finished displaying candidates.") + print("Candidate_scores dict:") + for candidate in candidate_scores: + print("\tCandidate: " + candidate + " Score: " + str(candidate_scores[candidate])) + + def checkReaction(reaction, user): + print("User: " + str(user)) + print("ctx.author: " + str(ctx.author)) + #print("Reaction.message: " + str(reaction.message)) + print("Reaction.message.content: " + str(reaction.message.content[3:-4])) + return True + + score = 0 + print("Waiting for reaction now.") + reaction_add, reactor = await bot.wait_for('reaction_add', check=checkReaction) + print("Reaction added to one of the candidates.") + print("Reaction_add.emoji: " + reaction_add.emoji) + #reaction_remove = await bot.wait_for('reaction_remove', check=checkReaction) + if reaction_add.emoji == '0️⃣': + score = 0 + elif reaction_add.emoji == '1️⃣': + score = 1 + elif reaction_add.emoji == '2️⃣': + score = 2 + elif reaction_add.emoji == '3️⃣': + score = 3 + elif reaction_add.emoji == '4️⃣': + score = 4 + elif reaction_add.emoji == '5️⃣': + score = 5 + + candidate_scores[reaction_add.message.content[3:-4]] = score + print("Updated candidate_scores dict:") + for candidate in candidate_scores: + print("\tCandidate: " + candidate + " Score: " + str(candidate_scores[candidate])) + + thing = await ctx.send(view=SubmitBallotView()) @bot.command() @commands.is_owner() @@ -476,4 +616,4 @@ async def results(ctx: commands.Context, *args): f"{str(candidate) :{padding_char}<{candidate_padding_length}}|{' ':{bar_graph_padding_char}<{bar_graph_padding_length}}{' ':{padding_char}>{bar_graph_empty_padding_length}}| {score}") -bot.run(token) +bot.run(discord_token) From 41d982621d8f42cd66a57a510a70d4276c903e23 Mon Sep 17 00:00:00 2001 From: tony_sax Date: Sun, 29 Jan 2023 22:02:52 -0600 Subject: [PATCH 2/4] Made the new_star_election command able to create elections via api request. --- star-voting.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/star-voting.py b/star-voting.py index 986825b..7477466 100644 --- a/star-voting.py +++ b/star-voting.py @@ -11,6 +11,7 @@ load_dotenv() discord_token = os.getenv('DISCORD_TOKEN') +jwt_secret_key = os.getenv('JWT_SECRET_KEY') jwt_token = os.getenv('JWT_TOKEN') # The guild in which this slash command will be registered. @@ -499,11 +500,17 @@ async def new_star_election(ctx: commands.Context, *args): } } race_list = [new_race_obj] + new_authentication_obj = { + "authentication": + { + "email": "true" + } + } new_election_settings_obj = { - "ElectionSettings": + "settings": { - "election_roll_type": "none", - "voter_id_type": "predefined voter ID" + "voter_access": "open", + "voter_authentication": new_authentication_obj } } new_election_obj = { @@ -511,13 +518,15 @@ async def new_star_election(ctx: commands.Context, *args): { "title": election_name, "owner_id": str(election_creator_id), - "state": "dev testing", + "state": "draft", "races": race_list, - "settings": new_election_settings_obj + "settings": new_election_settings_obj, + "auth_key": jwt_secret_key } } - response = requests.post(URL, json = new_election_obj, cookies = {"id_token": jwt_token}) + # need to create a separate token for every user that votes, but those tokens don't need to be stored. + response = requests.post(URL, json = new_election_obj, cookies = {"custom_id_token": jwt_token}) print("response.url: " + response.url + "\n") print("response.status_code: " + str(response.status_code) + "\n") print("response.text: " + response.text + "\n") From eb7648b95c7327df0e9e4ccd98685bc57c3335ab Mon Sep 17 00:00:00 2001 From: tony_sax Date: Sun, 29 Jan 2023 23:06:10 -0600 Subject: [PATCH 3/4] Fixing api json formatting. --- star-voting.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/star-voting.py b/star-voting.py index 7477466..afebbf3 100644 --- a/star-voting.py +++ b/star-voting.py @@ -426,7 +426,7 @@ async def new_star_embed(ctx: commands.Context, *args): # Using block quotes via "> " or ">>> " looks nice so maybe use it for the formatting of values. # Create the instructions for the embed. - star_voting_instructions = "See the image below for instructions \non how a STAR voting election works! " \ + star_voting_instructions = "Click on the image below for instructions \non how a STAR voting election works! " \ "\nThen click on Vote and fill out your ballot!" embedVar = discord.Embed( title=electionTitle, description=star_voting_instructions, @@ -488,30 +488,22 @@ async def new_star_election(ctx: commands.Context, *args): candidate_list = [] for candidate in candidates: print("Candidate: " + candidate) - new_candidate_obj = { "Candidate": { "candidate_name": candidate}} + new_candidate_obj = { "candidate_name": candidate} candidate_list.append(new_candidate_obj) new_race_obj = { - "Race": - { "title": election_name, "voting_method": "STAR", "num_winners": 1, "candidates": candidate_list - } } race_list = [new_race_obj] new_authentication_obj = { - "authentication": - { - "email": "true" - } + "voter_id": False, + "email": True } new_election_settings_obj = { - "settings": - { - "voter_access": "open", - "voter_authentication": new_authentication_obj - } + "voter_access": "open", + "voter_authentication": new_authentication_obj } new_election_obj = { "Election": From da9816e37f6332d26d86647f01930d5bc102bfdb Mon Sep 17 00:00:00 2001 From: tony_sax Date: Mon, 30 Jan 2023 01:15:16 -0600 Subject: [PATCH 4/4] Initial work so that the election creator can change the candidates names. --- star-voting.py | 85 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/star-voting.py b/star-voting.py index afebbf3..a2fff6c 100644 --- a/star-voting.py +++ b/star-voting.py @@ -9,6 +9,8 @@ from discord.ext import commands from dotenv import load_dotenv +import json + load_dotenv() discord_token = os.getenv('DISCORD_TOKEN') jwt_secret_key = os.getenv('JWT_SECRET_KEY') @@ -19,6 +21,8 @@ TEST_GUILD = discord.Object(id=918037457277161492) + + class CandidateScorecardView(discord.ui.View): def __init__(self): self.score_chosen = -1 @@ -160,17 +164,10 @@ async def editenddate(self, interaction: discord.Interaction, button: discord.ui @discord.ui.button(label='Edit Candidates', style=discord.ButtonStyle.blurple, custom_id='persistent_view:editcandidates1') - async def editcandidates1(self, interaction: discord.Interaction, button: discord.ui.Button): - # Need to make sure only the creator can edit the embed. - # if (interaction.user == message.user): - await interaction.response.send_modal(STARVotingCandidateEdit()) - - @discord.ui.button(label='Edit Candidates 2', style=discord.ButtonStyle.blurple, - custom_id='persistent_view:editcandidates2') - async def editcandidates2(self, interaction: discord.Interaction, button: discord.ui.Button): + async def editcandidates(self, interaction: discord.Interaction, button: discord.ui.Button): # Need to make sure only the creator can edit the embed. # if (interaction.user == message.user): - await interaction.response.send_modal(STARVotingCandidateEdit()) + await interaction.response.send_modal(STARVotingCandidateNameEdit()) class STARVotingElectionSetup(discord.ui.Modal, title='New STAR Voting Election'): @@ -232,6 +229,9 @@ class STARVotingDescriptionEdit(discord.ui.Modal, title='Edit the Description'): # default=defaultText, async def on_submit(self, interaction: discord.Interaction): + URL = "https://star-vote.herokuapp.com/API/Elections" + election = requests.get(URL) + response = requests.post(URL, json = new_election_obj, cookies = {"custom_id_token": jwt_token}) await interaction.response.send_message('Starting a new election...', ephemeral=True) async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: @@ -308,6 +308,58 @@ async def on_error(self, interaction: discord.Interaction, error: Exception) -> # Make sure we know what the error actually is traceback.print_tb(error.__traceback__) +class STARVotingCandidateNameEdit(discord.ui.Modal, title='Edit Candidates Names'): + descriptionEditBox = discord.ui.TextInput( + label='Candidates', + placeholder='Harry Potter, Ron Weasley, Hermione Granger, Draco Malfoy, Hedwig', + style=TextStyle.long, + ) + + async def on_submit(self, interaction: discord.Interaction): + election_id = None + # Get the election ID that was saved to the embed by getting the embed from the history of this channel. + messages = [mess async for mess in interaction.message.channel.history(limit=3)] + for message in messages: + for embed in message.embeds: + for field in embed.fields: + if field.name == "Election ID": + election_id = field.value + break + + # Add the election ID to the URL so that we can send the correct get request. + URL = "https://star-vote.herokuapp.com/API/Election/" + URL += str(election_id) + # Get the election from star-vote. + draft_election_response = requests.get(URL, cookies = {"custom_id_token": jwt_token}) + + # Display all the election data. + print("\ndraft_election_response") + print(draft_election_response) + print("\ndraft_election_response.text") + print(draft_election_response.text) + draft_election_json = json.loads(draft_election_response.text) + draft_election_response_election = draft_election_json["election"] + print("\ndraft_election_response_election") + print(draft_election_response_election) + draft_election_response_races = draft_election_response_election["races"] + print("\ndraft_election_response_races") + print(draft_election_response_races) + draft_election_response_race = draft_election_response_races[0] + print("\ndraft_election_response_race") + print(draft_election_response_race) + draft_election_response_candidates = draft_election_response_race["candidates"] + print("\ndraft_election_response_candidates") + print(draft_election_response_candidates) + + # Send a message to the person who edited the candidates with the values they provided. + await interaction.response.send_message(f'Starting a new election... {self.descriptionEditBox}', ephemeral=True) + + async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: + await interaction.response.send_message('Oops! Something went wrong.', ephemeral=True) + + # Make sure we know what the error actually is + traceback.print_tb(error.__traceback__) + class STARVotingBot(commands.Bot): def __init__(self): @@ -400,6 +452,7 @@ def prettify_candidates(candidate: tuple) -> str: async def new_star_embed(ctx: commands.Context, *args): """Creates an embed for a STAR voting election. Arguments: + ElectionID: The ID of the election that STAR Vote generates. ElectionTitle: Title of the election. (e.g. NewElection or "New Election") Days: Number of days the election will last. Candidates: As many candidates as you want. (e.g. JaneDoe or "Jane Doe") @@ -407,14 +460,16 @@ async def new_star_embed(ctx: commands.Context, *args): .star "What is the best color?" 5 Blue Red Green Yellow Purple Orange """ # The first argument should be the election title. - electionTitle = args[0] + electionID = args[0] + # The first argument should be the election title. + electionTitle = args[1] # Get how many days the STAR Voting election will last and set an end date. now = datetime.datetime.now() - days = int(args[1]) + days = int(args[2]) endDate = now + datetime.timedelta(days) endDate = endDate.strftime("%A, %B %d, %Y %H:%M:%S") # Example: Friday September 16, 2022 18:10:11 # Set the candidates into their own variable. - candidateTuple = args[2:] + candidateTuple = args[3:] # stuff that wasn't working idk #candidateTuple = () #for candidate in args[2:]: @@ -447,6 +502,7 @@ async def new_star_embed(ctx: commands.Context, *args): embedVar.add_field(name="🟣 Purple Party", value=f"> {candidates}", inline=True) vote_count = 0 embedVar.add_field(name="Current Vote Count:", value=vote_count, inline=False) + embedVar.add_field(name="Election ID", value=electionID, inline=False) # Set the large image that displays. image_simple_ballot = "https://d3n8a8pro7vhmx.cloudfront.net/unifiedprimary/pages/494/attachments/original/1632368538/STAR_Ballot.jpg?1632368538" @@ -524,7 +580,10 @@ async def new_star_election(ctx: commands.Context, *args): print("response.text: " + response.text + "\n") print("\n\n") - await new_star_embed(ctx, election_name, days, candidates) + response_data = json.loads(response.text) + election_id = response_data["election"]["election_id"] + + await new_star_embed(ctx, election_id, election_name, days, candidates) @bot.command() @commands.is_owner()