Compare commits

...
Author SHA1 Message Date
imcompiling 3089da7282 Sort members by display name then non members.
build_develop / build (push) Successful in 1m16s
build_latest / build (push) Successful in 29s
build_release / build (push) Successful in 21s
change how view message looks.
2024-05-11 23:51:44 -04:00
imcompiling 24ae0fe21f Allow multiple quarters to be selected from search string, as long as they match the start with.
build_develop / build (push) Successful in 49s
2024-05-11 13:34:30 -04:00
imcompiling ad3da6f2c6 Hosts don't get participation points. 2024-05-11 13:31:39 -04:00
imcompiling 5057e9a844 Change pandas version.
build_develop / build (push) Successful in 50s
2024-05-10 08:49:11 -04:00
imcompiling fea590bf9a Add handling for bad quarter name.
build_develop / build (push) Successful in 48s
2024-05-09 23:33:23 -04:00
imcompiling 058bdfef50 Add auto complete for event points 2024-05-09 23:29:28 -04:00
imcompiling 257a7eccd7 Add view points command. 2024-05-09 23:27:06 -04:00
imcompiling 443609f5ba Fix small bugs with getting cycle name and recording events. 2024-05-09 20:57:36 -04:00
imcompiling fde17edf09 Have to make sure things exist before making list of them.
Don't delete the embed before done using it.
2024-05-09 20:53:48 -04:00
imcompiling 084c9df65c Record points in the command. 2024-05-09 20:34:16 -04:00
imcompiling 4f9524a4c1 Slight database change, add util functions for adding points to users. 2024-05-09 20:25:01 -04:00
imcompiling 1495fb7ee6 Updated all docustrings in record.py. 2024-05-09 17:13:59 -04:00
imcompiling fa312eec87 Change database to handle non-discord participants. 2024-05-09 17:00:20 -04:00
imcompiling eb3cf2b77f Clear submittion view after submitted.
build_develop / build (push) Successful in 36s
2024-05-09 16:52:44 -04:00
imcompiling c08cd84bed Cleaned up embed and event record message. 2024-05-09 16:51:07 -04:00
imcompiling 905314cfb5 Create custom user select to edit the embed with selected people. 2024-05-09 16:37:45 -04:00
imcompiling bd3debf025 Change up recording process. use a modal to edit text values. 2024-05-09 16:26:20 -04:00
imcompiling 3a3c4b7995 Remove hard coded guild id. 2024-05-08 21:43:37 -04:00
imcompiling f465117435 Added basic event recording. No points being tracked yet.
build_develop / build (push) Successful in 34s
2024-05-08 21:34:18 -04:00
imcompiling 0777b71578 More filling out of the record command. The UserSelect seems to work for what I want.
build_develop / build (push) Successful in 39s
2024-05-08 17:13:32 -04:00
imcompiling a2afdc0295 Create record command class. Update configs for recording events. 2024-05-08 16:40:16 -04:00
imcompiling 3b6bef3ff0 fix incorrect description. 2024-05-08 16:39:49 -04:00
imcompiling a963171b16 change get election cycle name, add new function to get the current cycle for quarter name usage. 2024-05-08 16:20:46 -04:00
imcompiling e1787f68d5 Add new table to database 2024-05-08 16:20:19 -04:00
imcompiling a8697b0187 Bandaid fix so HC votes can be viewed.
build_latest / build (push) Successful in 1m39s
build_release / build (push) Successful in 48s
build_develop / build (push) Successful in 52s
2024-01-09 20:04:49 -05:00
imcompiling 8c8dc2c638 Merge branch '44-sort-candidates'
build_latest / build (push) Successful in 47s
build_release / build (push) Successful in 51s
2024-01-03 21:47:59 -05:00
imcompiling 54933b427e Sort nominees in voting views by display name.
build_develop / build (push) Successful in 1m27s
2024-01-03 21:46:54 -05:00
imcompiling 5d05006a23 Prevent unneeded exceptions.
build_latest / build (push) Successful in 1m23s
build_develop / build (push) Successful in 1m7s
2024-01-01 14:11:07 -05:00
imcompiling 2596b9fa7b Merge branch '47-incorrect-election-cycle'
build_latest / build (push) Successful in 44s
build_release / build (push) Successful in 45s
2024-01-01 13:32:41 -05:00
imcompiling 8c654612a9 Ensure that rolback months are properly year-ed.
build_develop / build (push) Successful in 1m27s
2024-01-01 13:32:17 -05:00
imcompiling e836ead399 Force crash on no election cycle. -1 was causing last item in config array to be used.
build_develop / build (push) Successful in 1m38s
2024-01-01 13:23:29 -05:00
imcompiling fa691ced5d Space on the wrong side of the quote.
build_latest / build (push) Successful in 1m33s
build_release / build (push) Successful in 45s
2023-12-27 09:50:01 -05:00
imcompiling 81f4c7741b Merge branch '40-remove-presence'
build_latest / build (push) Successful in 1m29s
build_release / build (push) Successful in 45s
2023-12-23 14:48:23 -05:00
imcompiling 2cde9f7109 Fix bug preventing remove from working.
build_develop / build (push) Successful in 1m40s
Remove guild ids.
2023-12-23 14:48:12 -05:00
imcompiling 2632bb883f Filled out remove presence some more. Not currently working.
build_develop / build (push) Successful in 1m25s
2023-12-22 22:05:07 -05:00
imcompiling 2f75f0cdcb Finish up presence auto complete function. 2023-12-22 21:56:29 -05:00
imcompiling deb405f02b Might not have worked without list cast. 2023-12-22 21:35:52 -05:00
imcompiling ee7cbaf2f6 Merge branch '39-view-presences'
build_latest / build (push) Successful in 1m26s
2023-12-22 21:33:45 -05:00
imcompiling 7ba7d3563c Rework logic to use less loops.
build_develop / build (push) Successful in 1m28s
2023-12-22 21:33:34 -05:00
imcompiling 39746123f1 Add beginning of remove presence. 2023-12-22 21:23:29 -05:00
imcompiling c69adb8374 Limit items sent back on autocomplete (small bug) 2023-12-22 21:23:18 -05:00
imcompiling f722a6a87b Do not want guild ids 2023-12-22 16:36:31 -05:00
imcompiling b3550bbf55 Merge commit '0a18d819afecade9a9812df4c9056ba19bb4d211'
build_develop / build (push) Successful in 1m25s
2023-12-22 16:32:42 -05:00
imcompiling 0a18d819af Add command to view presences. 2023-12-22 16:32:32 -05:00
imcompiling 2bd4fcb136 Format change for presence request.
build_latest / build (push) Successful in 1m37s
build_release / build (push) Successful in 45s
build_develop / build (push) Successful in 52s
2023-12-21 19:23:13 -05:00
imcompiling 2a91274d7f Need to use the and keyword. 2023-12-21 19:22:48 -05:00
imcompiling 4fb4927fb8 Merge branch '36-bot-presence'
build_latest / build (push) Successful in 43s
build_develop / build (push) Successful in 45s
2023-12-20 21:47:07 -05:00
imcompiling fbd8419ccf Add the add presence request.
build_develop / build (push) Successful in 1m33s
2023-12-20 21:45:20 -05:00
imcompiling 09ff620e51 Add loop for changing presence. 2023-12-20 21:38:52 -05:00
imcompiling 9c64e574fe Add new table to record bot presences. 2023-12-20 21:02:04 -05:00
imcompiling 011b6551b0 highcouncil election message update.
build_latest / build (push) Successful in 1m31s
build_release / build (push) Successful in 45s
build_develop / build (push) Successful in 56s
2023-12-20 09:27:18 -05:00
18 changed files with 813 additions and 50 deletions
+1 -2
View File
@@ -7,7 +7,6 @@ From {highcouncil_election_announce} to {highcouncil_election_start}, you will b
Clan Leader
Discord Server Manager
Each member of the CCE can cast one for each category by using the emotes: <:Rank_Owner:479008325137072155> for Clan Leader and <:Rank_Deputy:479008295319896064> for Discord Server Manager.
If needed, the icon 🤷 can be used to abstain to vote in any or both categories and it should be applied to all candidates of that category.
Each member of the CCE can vote once in each category (Discord Server Manager and Clan Leader). Alternatively, you may choose to intentionally abstain.
Votes can be cast until {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}.
+3 -2
View File
@@ -239,7 +239,8 @@ class ElectionCycle:
)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_votes_str(self.config), view=VoteView(self.config, self.guild)
build_votes_str(self.config, self.guild),
view=VoteView(self.config, self.guild),
)
with Session(engine) as session:
@@ -324,7 +325,7 @@ class ElectionCycle:
)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_highcouncil_votes_str(self.config),
build_highcouncil_votes_str(self.config, self.guild),
view=VoteHighcouncilView(self.config, self.guild),
)
+9 -3
View File
@@ -82,7 +82,7 @@ class VoteSelect(nextcord.ui.StringSelect):
session.execute(stmt)
session.commit()
vote_message_str = build_highcouncil_votes_str(self.config)
vote_message_str = build_highcouncil_votes_str(self.config, interaction.guild)
vote_msg: nextcord.Message = await interaction.channel.fetch_message(
vote_msg_id
)
@@ -159,7 +159,10 @@ class VoteHighcouncilView(nextcord.ui.View):
else f"{self.guild.get_member(nominee).display_name}",
value=str(nominee),
)
for nominee in session.scalars(stmt)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name,
)
]
options.append(
SelectOption(
@@ -232,7 +235,10 @@ class VoteHighcouncilView(nextcord.ui.View):
else f"{self.guild.get_member(nominee).display_name}",
value=str(nominee),
)
for nominee in session.scalars(stmt)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name,
)
]
options.append(
SelectOption(
+5 -2
View File
@@ -64,7 +64,7 @@ class VoteSelect(nextcord.ui.StringSelect):
session.execute(stmt)
session.commit()
vote_message_str = build_votes_str(self.config)
vote_message_str = build_votes_str(self.config, interaction.guild)
vote_msg: nextcord.Message = await interaction.channel.fetch_message(
vote_msg_id
)
@@ -135,7 +135,10 @@ class VoteView(nextcord.ui.View):
else f"{self.guild.get_member(nominee).display_name}",
value=str(nominee),
)
for nominee in session.scalars(stmt)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name,
)
]
options.append(
SelectOption(
+5 -1
View File
@@ -4,6 +4,8 @@ Cog loader. Adds all command cogs to the bot.
from nextcord.ext import commands
from cogs.commands.record import RecordCog
from cogs.commands.remove import RemoveCog
from cogs.commands.request import RequestCog
from cogs.commands.set import setCog
from cogs.commands.test import TestCog
@@ -17,7 +19,9 @@ def setup(bot: commands.Bot):
Adds all command classes to bot.
"""
bot.add_cog(RecordCog(bot))
bot.add_cog(RemoveCog())
bot.add_cog(RequestCog(bot))
bot.add_cog(TestCog(bot))
bot.add_cog(setCog())
bot.add_cog(ViewCog())
bot.add_cog(setCog())
+218
View File
@@ -0,0 +1,218 @@
"""
Contained commands and views for record commands.
"""
import nextcord
from discord import TextInputStyle
from nextcord.ext import commands
from config import DakinyConfig
from utils import record_event_host, record_event_participation
class RecordCog(commands.Cog, name="Record Commands"):
"""
Class holds record commands.
"""
def __init__(self, bot) -> None:
"""
Default constructor for RecordCog class. Does nothing.
"""
super().__init__()
self.bot: commands.Bot = bot
@nextcord.slash_command()
async def record(self, interaction: nextcord.Interaction):
"""
Parent slash command for record. Never called.
"""
@record.subcommand(description="Record an event and its participants.")
async def event(
self,
interaction: nextcord.Interaction,
name: str = nextcord.SlashOption(
required=True, description="Name of the event that was hosted."
),
host: nextcord.Member = nextcord.SlashOption(
required=False,
description="An alternative host if you are recording event for someone else.",
),
):
"""
This command creates the base event recording embed and sends a message with it and the EventView
for gathering more information on the event.
Args:
interaction (nextcord.Interaction): interaction with this command.
name (str): Name of the event.
host (nextcord.Member, optional): Alternative host to user using command.
"""
# check role
if (
interaction.guild.get_role(self.bot.config.params.event_host_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.bot.config.params.event_host_role_id}> role to record an event.",
ephemeral=True,
)
return
if host is None:
host = interaction.user
embed: nextcord.Embed = nextcord.Embed(title=name, description="")
embed.add_field(name="Participants", value="")
embed.add_field(name="Non-Discord Participants", value="")
embed.add_field(name="Hosted by", value=f"<@{host.id}>", inline=False)
await interaction.response.send_message(
"Fill out event details and select all members who participated!",
view=EventView(name, self.bot.config, host.id),
ephemeral=True,
embed=embed,
)
class EventView(nextcord.ui.View):
"""
Class handles gathering information on an event. Has buttons to edit the information, cancel, and submit.
Also contains a UserSelect object to pick discord members.
"""
def __init__(self, name: str, config: DakinyConfig, host_id: int):
super().__init__()
self.name: str = name
self.config: DakinyConfig = config
self.host_id: int = host_id
self.event_participants = ParticipantSelect()
self.add_item(self.event_participants)
@nextcord.ui.button(label="Cancel", style=nextcord.ButtonStyle.red, row=1)
async def cancel(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
"""
Cancels event submission. Removes embed and view.
"""
await interaction.response.edit_message(
"Canceled", ephemeral=True, embed=None, view=None
)
self.stop()
@nextcord.ui.button(label="Edit", style=nextcord.ButtonStyle.blurple, row=1)
async def edit(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
"""
Calls modal to update the text input information of the event.
"""
await interaction.response.send_modal(
ParticipantModal(interaction.message.embeds[0])
)
@nextcord.ui.button(label="Confirm", style=nextcord.ButtonStyle.green, row=1)
async def confirm(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
"""
Submits the event embed to the event channel. Removes view and embed from recording message.
Sends point information to database.
"""
discord_users = self.event_participants.values.members
if discord_users:
discord_users_list: list[str] = [
str(user.id) for user in discord_users if user.id is not self.host_id
]
record_event_participation(self.config, discord_users_list, True)
non_discord_users = interaction.message.embeds[0].fields[1].value
if non_discord_users:
non_discord_users_list: list[str] = non_discord_users.split("\n")
record_event_participation(self.config, non_discord_users_list, False)
record_event_host(self.config, self.host_id)
event_channel: nextcord.TextChannel = interaction.guild.get_channel(
self.config.params.event_post_channel_id
)
await event_channel.send(embed=interaction.message.embeds[0])
await interaction.response.edit_message(
content="Event has been submitted.", view=None, embed=None
)
self.stop()
class ParticipantModal(nextcord.ui.Modal):
"""
This modal contains all the text edits for an event. You can change the name, description, and add the usernames
of participants who are not in the discord.
"""
# TODO turn this embed into a dict
def __init__(self, embed):
super().__init__(
"Edit the event.",
timeout=5 * 60,
)
self.embed: nextcord.Embed = embed
self.name: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=True,
label="Event Name",
default_value=self.embed.title,
)
self.description: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=False,
label="Event Description",
default_value=self.embed.description,
style=TextInputStyle.paragraph,
placeholder="Add a description if desired.",
max_length=1000,
)
self.participants: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=False,
label="Non-Discord Usernames",
style=TextInputStyle.paragraph,
placeholder="Please separate usernames with a new line.",
default_value=self.embed.fields[1].value,
)
self.add_item(self.name)
self.add_item(self.description)
self.add_item(self.participants)
async def callback(self, interaction: nextcord.Interaction):
self.embed.title = self.name.value
self.embed.description = self.description.value
self.embed.set_field_at(
index=1, name="Non-Discord Participants", value=self.participants.value
)
await interaction.response.edit_message(embed=self.embed)
class ParticipantSelect(nextcord.ui.UserSelect):
"""
A selection drop down for discord members. Updates the embed with all users selected.
"""
def __init__(self) -> None:
super().__init__(
min_values=1,
max_values=25,
placeholder="Select all participants",
row=0,
)
async def callback(self, interaction: nextcord.Interaction):
embed: nextcord.Embed = interaction.message.embeds[0]
participants: str = "\n".join(
[f"<@{participant.id}>" for participant in self.values.members]
)
embed.set_field_at(0, name="Participants", value=participants)
await interaction.response.edit_message(embed=embed)
+86
View File
@@ -0,0 +1,86 @@
"""
Commands for removing information in the database.
"""
import nextcord
from nextcord.ext import commands
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import Presences, engine
class RemoveCog(commands.Cog, name="Remove Commands"):
"""
Holds the view commands.
"""
def __init__(self) -> None:
"""
Default constructor for RemoveCog class. Does nothing.
"""
@nextcord.slash_command(
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def remove(self, interaction: nextcord.Interaction):
"""
Parent slash command for set. Never called.
"""
@remove.subcommand(description="Remove a presence from the database.")
async def presence(
self,
interaction: nextcord.Interaction,
presence_str: str = nextcord.SlashOption(
description="The presence to remove", required=True
),
):
"""
Deletes the given presence id from the database.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
presence (str): Is SlashOption. The presence to remove. Is the ID of the presence.
"""
presence_id = int(presence_str)
with Session(engine) as session:
stmt = delete(Presences).where(Presences.Id == presence_id)
session.execute(stmt)
session.commit()
await interaction.response.send_message("Presence deleted.", ephemeral=True)
@presence.on_autocomplete("presence_str")
async def presence_autocomplete(
self, interaction: nextcord.Interaction, presence_str: str
):
"""
Collects all presences that start with the presence string.
Sends autocomplete dict {Presence Name, Presence Id}
Args:
interaction (nextcord.Interaction): Interaction with command.
presence_str (str): String to run autocomplete on.
"""
with Session(engine) as session:
stmt = select(Presences)
presences_dict: "dict[str,str]" = {
pres.Presence: str(pres.Id) for pres in session.scalars(stmt)
}
# if empty compare string
if not presence_str:
await interaction.response.send_autocomplete(
dict(list(presences_dict.items())[:25])
)
return
autofilled_presences: "dict[str,str]" = {
pres: pres_id
for pres, pres_id in presences_dict.items()
if presence_str.lower() in pres.lower()
}
await interaction.response.send_autocomplete(autofilled_presences[:25])
+109 -10
View File
@@ -4,8 +4,10 @@ Contains commands and views for request commands.
import nextcord
from nextcord.ext import commands
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import Presences, engine
class RequestCog(commands.Cog, name="Request Commands"):
@@ -22,15 +24,49 @@ class RequestCog(commands.Cog, name="Request Commands"):
@nextcord.slash_command()
async def request(self, interaction: nextcord.Interaction):
"""
Parent slash command for help. Never called.
Parent slash command for request. Never called.
"""
@request.subcommand(
description="Request to add a presence to the bot's presence list."
)
async def add_presence(
self,
interaction: nextcord.Interaction,
presence: str = nextcord.SlashOption(
required=True,
min_length=1,
max_length=100,
description="The presence you want to add.",
),
):
"""
Sends a message to admins to request to add a presence.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
presence (str): The presence to add.
"""
config: DakinyConfig = self.bot.config
content = f'<@{interaction.user.id}> would like to add presence "{presence}."'
view = PresenceChange(presence)
await self.bot.get_channel(config.params.admin_channel_id).send(
content=content, view=view
)
await interaction.response.send_message(
"Request sent to admins.", ephemeral=True
)
@request.subcommand(description="Request to change your Dakiny nickname")
async def change_nickname(
self,
interaction: nextcord.Interaction,
new_nickname: str = nextcord.SlashOption(
required=True, min_length=2, max_length=32, description="Your requested new nickname."
required=True,
min_length=2,
max_length=32,
description="Your requested new nickname.",
),
):
"""
@@ -44,8 +80,12 @@ class RequestCog(commands.Cog, name="Request Commands"):
config: DakinyConfig = self.bot.config
content = f'<@{interaction.user.id}> would like to change their username to "{new_nickname}."'
view = UsernameRequest(interaction.user.id, new_nickname)
await self.bot.get_channel(config.params.admin_channel_id).send(content=content, view=view)
await interaction.response.send_message("Request sent to admins.", ephemeral=True)
await self.bot.get_channel(config.params.admin_channel_id).send(
content=content, view=view
)
await interaction.response.send_message(
"Request sent to admins.", ephemeral=True
)
class UsernameRequest(nextcord.ui.View):
@@ -53,13 +93,19 @@ class UsernameRequest(nextcord.ui.View):
View provides approve and deny buttons and action for a username change request.
"""
def __init__(self, user_id: int, new_nickname: str, *, timeout: float | None = 180, auto_defer: bool = True):
super().__init__(timeout=timeout, auto_defer=auto_defer)
def __init__(
self,
user_id: int,
new_nickname: str,
):
super().__init__(timeout=None, auto_defer=True)
self.user_id = user_id
self.new_nickname = new_nickname
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
async def approve(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
"""
Approve button for a nickname change request. Changes the target's nickname to self.new_nickname.
Catches an error if the target as a higher role than the bot.
@@ -73,7 +119,10 @@ class UsernameRequest(nextcord.ui.View):
if user is not None:
try:
await user.edit(nick=self.new_nickname)
content = interaction.message.content + f"\nApproved by <@{interaction.user.id}>."
content = (
interaction.message.content
+ f"\nApproved by <@{interaction.user.id}>."
)
await interaction.response.edit_message(content=content, view=None)
except nextcord.errors.Forbidden:
await interaction.response.send_message(
@@ -84,10 +133,13 @@ class UsernameRequest(nextcord.ui.View):
interaction.message.content
+ "\n\nFailed. User has too high permissions for bot to change nickname."
)
await interaction.followup.edit_message(interaction.message.id, content=content, view=None)
await interaction.followup.edit_message(
interaction.message.id, content=content, view=None
)
else:
await interaction.response.edit_message(
content="An error occured while changing the user's nickname.", view=None
content="An error occured while changing the user's nickname.",
view=None,
)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
@@ -101,3 +153,50 @@ class UsernameRequest(nextcord.ui.View):
"""
content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None)
class PresenceChange(nextcord.ui.View):
"""
View provides approve and deny buttons and action for a username change request.
"""
def __init__(
self,
presence: str,
):
super().__init__(timeout=None, auto_defer=True)
self.presence = presence
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
"""
Approve button for a presence add request. Adds the presence to the database.
Removes the view from the interaction message.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
with Session(engine) as session:
session.add(Presences(Presence=self.presence))
session.commit()
content = (
interaction.message.content + f"\nApproved by <@{interaction.user.id}>."
)
await interaction.response.edit_message(content=content, view=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
"""
Deny button for a presence add request. Just removes the view and logs who denied the request.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None)
+8 -8
View File
@@ -7,7 +7,7 @@ from nextcord.ext import application_checks, commands
from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from utils import get_current_cycle
from utils import get_current_election_cycle
def is_me():
@@ -50,7 +50,7 @@ class TestCog(commands.Cog, name="Test commands."):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
@@ -67,7 +67,7 @@ class TestCog(commands.Cog, name="Test commands."):
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
@@ -84,7 +84,7 @@ class TestCog(commands.Cog, name="Test commands."):
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
@@ -101,7 +101,7 @@ class TestCog(commands.Cog, name="Test commands."):
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
@@ -112,7 +112,7 @@ class TestCog(commands.Cog, name="Test commands."):
async def election_reminder(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
@@ -123,7 +123,7 @@ class TestCog(commands.Cog, name="Test commands."):
async def daily(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
@@ -134,7 +134,7 @@ class TestCog(commands.Cog, name="Test commands."):
async def term_start(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
+144 -3
View File
@@ -3,11 +3,17 @@ Commands for viewing information in the database.
"""
import nextcord
import pandas as pd
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import ElectionInformation, Votes, engine
from database_setup.sql_data_classes import (
ParticipationPoints,
Presences,
Votes,
engine,
)
class ViewCog(commands.Cog, name="View Commands"):
@@ -26,6 +32,113 @@ class ViewCog(commands.Cog, name="View Commands"):
Parent slash command for set. Never called.
"""
@view.subcommand(description="Show event points.")
async def event_points(
self,
interaction: nextcord.Interaction,
quarter_name: str = nextcord.SlashOption(
description="The quarter to view",
required=False,
),
):
# just in case
await interaction.response.defer(ephemeral=True)
with Session(engine) as session:
if quarter_name:
df = pd.read_sql_query(
sql=session.query(ParticipationPoints)
.where(ParticipationPoints.Cycle.startswith(quarter_name))
.statement,
con=engine,
)
else:
df = pd.read_sql_query(
sql=session.query(ParticipationPoints).statement,
con=engine,
)
if df.empty:
await interaction.followup.send(
f'Quarter "{quarter_name}" is invalid.', ephemeral=True
)
return
cycles = df.groupby("Cycle")
content = ""
for cycle in cycles.groups:
content += f"**{cycle}**\n"
unsorted_all = cycles.get_group(cycle).groupby("DiscordMember")
members = unsorted_all.get_group(True)
non_members = unsorted_all.get_group(False)
sorted_members = members.sort_values(
by="UserId",
key=lambda user_ids: (
[
interaction.guild.get_member(int(id)).display_name
for id in user_ids
]
),
)
sorted_non_members = non_members.sort_values(by="UserId")
sorted_all = pd.concat(
[sorted_members, sorted_non_members], ignore_index=True, sort=False
)
for row in sorted_all.itertuples():
userid = row.UserId
points = row.Points
discord_member = row.DiscordMember
content += (
f"<@{userid}>: {points}\n"
if discord_member
else f"{userid}: {points}\n"
)
content += "\n"
if len(content) > 2000:
await interaction.followup.send(
"Message length is too long, please select search to limit results."
)
return
await interaction.followup.send(content=content, ephemeral=True)
@view.subcommand(description="Show all possible presence choices.")
async def presences(
self,
interaction: nextcord.Interaction,
search: str = nextcord.SlashOption(
description="A word or string to search for in the presences.",
required=False,
),
):
"""
Gathers all presences from the database and responds with a message showing them.
Args:
interaction (nextcord.Integration): User's interaction with the command.
"""
# might take a while depending on number of presences
await interaction.response.defer(ephemeral=True)
with Session(engine) as session:
stmt = select(Presences.Presence)
presences = session.scalars(stmt)
if search:
message = "\n".join(
[
presence
for presence in presences
if search.lower() in presence.lower()
]
)
else:
message = "\n".join(list(presences))
await interaction.followup.send(content=message, ephemeral=True)
@view.subcommand(
description="Shows all voters and who they voted for in a given election."
)
@@ -80,12 +193,12 @@ class ViewCog(commands.Cog, name="View Commands"):
"""
with Session(engine) as session:
stmt = select(ElectionInformation.ElectionCycle)
stmt = select(Votes.ElectionCycle).distinct()
elections = session.scalars(stmt)
# if empty compare string
if not election_name:
await interaction.response.send_autocomplete(elections)
await interaction.response.send_autocomplete(list(elections)[:25])
return
autofilled_elections: "list[str]" = [
@@ -94,3 +207,31 @@ class ViewCog(commands.Cog, name="View Commands"):
if election.lower().startswith(election_name.lower())
]
await interaction.response.send_autocomplete(autofilled_elections[:25])
@event_points.on_autocomplete("quarter_name")
async def event_points_autocomplete(
self, interaction: nextcord.Interaction, quarter_name: str
):
"""
Collects all elections that start with the election string.
Args:
interaction (nextcord.Interaction): Interaction with command.
quarter_name (str): String to run autocomplete on.
"""
with Session(engine) as session:
stmt = select(ParticipationPoints.Cycle).distinct()
quarters = session.scalars(stmt)
# if empty compare string
if not quarter_name:
await interaction.response.send_autocomplete(list(quarters)[:25])
return
autofilled_quarters: "list[str]" = [
quarter
for quarter in quarters
if quarter.lower().startswith(quarter_name.lower())
]
await interaction.response.send_autocomplete(autofilled_quarters[:25])
+3 -1
View File
@@ -4,7 +4,8 @@ Initializing code for all loops.
from nextcord.ext import commands
from .daily_loop import DailyLoop
from cogs.loops.bot_presence import BotPresence
from cogs.loops.daily_loop import DailyLoop
def setup(bot: commands.Bot):
@@ -12,4 +13,5 @@ def setup(bot: commands.Bot):
Adds functions to bot.
"""
bot.add_cog(BotPresence(bot))
bot.add_cog(DailyLoop(bot))
+48
View File
@@ -0,0 +1,48 @@
"""
Functions for controlling the bot's presence. Also contains the loop.
"""
import nextcord
from nextcord.ext import commands, tasks
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import Presences, engine
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(
self, guild: nextcord.Guild
): # pylint: disable=unused-argument
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self):
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop.
self.periodic_change_presence.start() # pylint: disable=no-member
@tasks.loop(minutes=5)
async def periodic_change_presence(self):
"""Loops every 5 minutes and changes presence."""
await self.change_presence()
async def change_presence(self):
"""Randomly chooses a presence and updates bot's presence."""
with Session(engine) as session:
presence = session.scalar(
select(Presences.Presence).order_by(func.random())
)
await self.bot.change_presence(
activity=nextcord.CustomActivity(name="Custom Status", state=presence)
)
+12 -4
View File
@@ -15,7 +15,7 @@ from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from dakiny_bot import birthday_list
from database_setup.sql_data_classes import UserBirthday, engine
from utils import get_current_cycle
from utils import get_current_election_cycle
class DailyLoop(commands.Cog, name="Daily Loop"):
@@ -77,7 +77,11 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
"""
config: DakinyConfig = self.bot.config
cycle = get_current_cycle(config)
cycle = get_current_election_cycle(config)
# prevent exception from using cycle as an Int in an array
if cycle is None:
return None
return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id))
@@ -107,10 +111,14 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
today = datetime.today()
guild: nextcord.Guild = self.bot.get_guild(self.bot.config.params.guild_id)
announce_channel: nextcord.TextChannel = guild.get_channel(self.bot.config.params.general_channel_id)
announce_channel: nextcord.TextChannel = guild.get_channel(
self.bot.config.params.general_channel_id
)
cakedays: list[nextcord.Member] = [
human for human in guild.humans if human.joined_at.month == today.month and human.joined_at.day == today.day
human
for human in guild.humans
if human.joined_at.month == today.month and human.joined_at.day == today.day
]
for member in cakedays:
+5
View File
@@ -1,6 +1,7 @@
"""
File contains classes defining the Hydra configuraton structure.
"""
from dataclasses import dataclass
@@ -17,6 +18,10 @@ class Params:
clan_info_channel_id: int
clan_member_role_id: int
discord_token: str
event_host_points: int
event_host_role_id: int
event_participate_points: int
event_post_channel_id: int
general_channel_id: int
guild_id: int
high_council_role_id: int
+13
View File
@@ -33,4 +33,17 @@ CREATE TABLE UserBirthday (
UserId bigint unsigned not null,
Birthday varchar(5) not null,
PRIMARY KEY (UserId)
);
CREATE TABLE Presences (
Id integer PRIMARY KEY,
Presence varchar(100) not null
);
CREATE TABLE ParticipationPoints (
Cycle varchar(30) not null,
UserId varchar(32) not null,
DiscordMember boolean not null,
Points int unsigned DEFAULT 0,
PRIMARY KEY (Cycle, UserId)
);
+3
View File
@@ -1,6 +1,7 @@
"""
Simple file to set up the sqlalchemy connection to database.
"""
from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base
@@ -15,3 +16,5 @@ ElectionInformation = Base.classes.ElectionInformation
CouncilMembers = Base.classes.CouncilMembers
Votes = Base.classes.Votes
UserBirthday = Base.classes.UserBirthday
Presences = Base.classes.Presences
ParticipationPoints = Base.classes.ParticipationPoints
+2 -1
View File
@@ -2,4 +2,5 @@ nextcord==2.4.2
python-dotenv==1.0.0
pytz==2022.7.1
hydra-core==1.3.2
sqlalchemy<2.0
sqlalchemy<2.0
pandas==1.5.2
+139 -13
View File
@@ -4,11 +4,17 @@ File contains util functions.
from datetime import datetime
import nextcord
from sqlalchemy import select
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, Votes, engine
from database_setup.sql_data_classes import (
CouncilMembers,
ParticipationPoints,
Votes,
engine,
)
NOMINEE_SPLITTER = "========================================\n\n"
@@ -28,9 +34,12 @@ def get_datetime(day: str) -> datetime:
day = int(day.split("/")[1])
# ensure that year rolls over if current month is December and the next month is less than 12.
if datetime.today().month == 12 & month < 12:
if datetime.today().month == 12 and month < 12:
year += 1
if datetime.today().month == 1 and month > 1:
year -= 1
return datetime(year, month, day)
@@ -44,12 +53,27 @@ def get_election_cycle_name(config: DakinyConfig) -> str:
Returns:
str: the name of the election cycle
"""
cycle = get_current_election_cycle(config)
current_cycle = config.election_cycles[cycle]
return f"vote-{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
def get_cycle_name(config: DakinyConfig) -> str:
"""
Builds the name of the current cycle/quarter
Args:
config (DakinyConfig): the config used to know the election cycle.
Returns:
str: the name of the cycle.
"""
cycle = get_current_cycle(config)
current_cycle = config.election_cycles[cycle]
return (
f"vote-{get_datetime(current_cycle.election_start).year}-{current_cycle.name}"
)
return f"{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
def get_dm_election_name(config: DakinyConfig) -> str:
@@ -80,7 +104,7 @@ def get_cl_election_name(config: DakinyConfig) -> str:
return f"{get_election_cycle_name(config)}-cl"
def get_current_cycle(config: DakinyConfig) -> int:
def get_current_election_cycle(config: DakinyConfig) -> int:
"""
Returns index for active election. An election is considered active between the election start and the term start.
@@ -88,7 +112,7 @@ def get_current_cycle(config: DakinyConfig) -> int:
config (DakinyConfig): Config object to use
Returns:
int: cycle index for active election. -1 for no active cycle
int: cycle index for active election. None for no active cycle
"""
today = datetime.today()
@@ -100,15 +124,38 @@ def get_current_cycle(config: DakinyConfig) -> int:
):
return cycle
return -1
return None
def build_votes_str(config: DakinyConfig) -> str:
def get_current_cycle(config: DakinyConfig) -> int:
"""
Returns the index for the current cycle/quarter. Defined as between term start and end. Shouldn't return none
Args:
config (DakinyConfig): Config object to use
Returns:
int: cycle index for current cycle.
"""
today = datetime.today()
for cycle, election in enumerate(config.election_cycles):
if (
today.date() >= get_datetime(election.term_start).date()
and today.date() <= get_datetime(election.term_end).date()
):
return cycle
return None
def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
"""
Creates a string that contains all nominees and their votes to be sent as a discord message.
Args:
config (DakinyConfig): Daking config object
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
Returns:
str: String that contains all nominees and their votes.
@@ -121,7 +168,10 @@ def build_votes_str(config: DakinyConfig) -> str:
CouncilMembers.ElectionCycle == get_election_cycle_name(config)
)
for nominee in session.scalars(stmt):
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
votes[nominee.MemberId] = 0
stmt = select(Votes).where(
@@ -138,7 +188,7 @@ def build_votes_str(config: DakinyConfig) -> str:
return votes_str
def build_highcouncil_votes_str(config: DakinyConfig) -> str:
def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
"""
Creates a string that contains all high council nominees and their votes to be sent as a discord message.
@@ -147,6 +197,7 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str:
Returns:
str: String that contains all nominees and their votes.
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
"""
cl_votes: dict[int, int] = {}
@@ -159,7 +210,10 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str:
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
)
for nominee in session.scalars(stmt):
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
cl_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_cl_election_name(config))
@@ -174,7 +228,10 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str:
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
)
for nominee in session.scalars(stmt):
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
dm_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_dm_election_name(config))
@@ -262,3 +319,72 @@ def decorate_dm_nomination(user_id: int, speech: str) -> str:
msg += speech
return msg
def record_event_participation(config: DakinyConfig, users: list[str], is_member: bool):
"""
Adds participation points to all users listed. Checks if they don't exist in order to make a new entry, otherwise
adds points to the existing value.
Args:
config (DakinyConfig): Dakiny config for getting points
users (list[int]): users that participated and need points.
is_member (bool): whether the users are a discord member or not
"""
cycle = get_cycle_name(config)
participation_point_value = config.params.event_participate_points
with Session(engine) as session:
for user in users:
stmt = (
select(ParticipationPoints)
.where(ParticipationPoints.UserId == user)
.where(ParticipationPoints.Cycle == cycle)
)
points = session.scalar(stmt)
if points is None:
new_points = ParticipationPoints(
Cycle=cycle,
UserId=user,
DiscordMember=is_member,
Points=participation_point_value,
)
session.add(new_points)
else:
points.Points += participation_point_value
session.commit()
def record_event_host(config: DakinyConfig, user: int):
"""
Adds host points to the given user. Checks if they don't exist in order to make a new entry, otherwise adds points
to the existing value.
Args:
config (DakinyConfig): Dakiny config for getting points
user (int): Id of user that hosted.
"""
cycle = get_cycle_name(config)
host_point_value = config.params.event_host_points
with Session(engine) as session:
stmt = (
select(ParticipationPoints)
.where(ParticipationPoints.UserId == str(user))
.where(ParticipationPoints.Cycle == cycle)
)
points = session.scalar(stmt)
if points is None:
new_points = ParticipationPoints(
Cycle=cycle,
UserId=str(user),
DiscordMember=True,
Points=host_point_value,
)
session.add(new_points)
else:
points.Points += host_point_value
session.commit()