Skip to content

Commit ea94bd1

Browse files
committed
added discord bot
1 parent c4577ea commit ea94bd1

File tree

8 files changed

+322
-0
lines changed

8 files changed

+322
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
26+
# Python
27+
28+
__pycache__
29+
.venv
30+
.env

bot.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cd discord_bot
2+
python -m venv .venv
3+
source ./venv/Scripts/activate
4+
pip install -r requirements.txt
5+
python index.py

discord_bot/cogs/commands.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import discord
2+
from discord.ext import commands
3+
from discord import app_commands
4+
from util.state import StateBot
5+
from util.embeds import Embeds
6+
from util.views import JudgeView
7+
from random import shuffle
8+
from datetime import date, timedelta
9+
10+
11+
class Commands(commands.Cog):
12+
def __init__(self, bot):
13+
self.bot: StateBot = bot
14+
self.TOTAL_SUBMISSIONS = 2
15+
16+
def get_judge(self) -> list[str, str]:
17+
"""Returns the judge for the week
18+
19+
Returns:
20+
list[str, str]: the name of the judge, then their id
21+
"""
22+
NAMES = ["Souren", "Kevin", "Ken", "Artom", "Andrew"]
23+
IDS = [
24+
"290550631591182336",
25+
"183383346569543681",
26+
"290630197458501633",
27+
"161921661346643968",
28+
"196069761082327041",
29+
]
30+
start_monday: date = date(2025, 6, 9)
31+
nearest_monday = date.today() + timedelta((7 - date.today().weekday()) % 7)
32+
day_delta = nearest_monday - start_monday
33+
week_delta = day_delta.days // 7
34+
return [NAMES[week_delta % len(NAMES)], IDS[week_delta % len(IDS)]]
35+
36+
@app_commands.command(name="submit", description="submit a song to music monday")
37+
@app_commands.describe(song="The spotify link for the song")
38+
async def submit_song(self, interaction: discord.Interaction, song: str):
39+
if song.find("spotify") == -1:
40+
return await interaction.response.send_message(
41+
embed=Embeds.error(
42+
"Link did not contain the word 'spotify', are you sure you submitted a spotify link?"
43+
),
44+
ephemeral=True,
45+
)
46+
47+
self.bot.state.add_submission(interaction.user.id, song)
48+
49+
if self.TOTAL_SUBMISSIONS == len(self.bot.state.get_all_submissions()):
50+
await interaction.channel.send(
51+
content=f"<@{self.get_judge()[1]}>, all songs have been submitted, you can now judge with `/submissions`"
52+
)
53+
54+
return await interaction.response.send_message(
55+
embed=Embeds.emphasis(title="Submission received", body="Thank you!")
56+
)
57+
58+
@app_commands.command(
59+
name="submissions",
60+
description="lists submissions blind if 4 have been given, otherwise errors",
61+
)
62+
async def list_submissions(self, interaction: discord.Interaction):
63+
subs = self.bot.state.get_all_submissions()
64+
if len(subs) < self.TOTAL_SUBMISSIONS:
65+
return await interaction.response.send_message(
66+
embed=Embeds.error(
67+
f"We aren't at {self.TOTAL_SUBMISSIONS} submissions yet! So far we have {len(subs)}"
68+
),
69+
ephemeral=True,
70+
)
71+
72+
word_map = ["one", "two", "three", "four"]
73+
74+
# randomize the order of the songs
75+
userIds = list(subs.keys())
76+
shuffle(userIds)
77+
78+
for [idx, userId] in enumerate(userIds):
79+
await interaction.channel.send(content=f":{word_map[idx]}: {subs[userId]}")
80+
return await interaction.response.send_message(
81+
content="Judge, please vote for your favourite this song this week!",
82+
view=JudgeView(subs, userIds),
83+
)
84+
85+
@app_commands.command(name="reveal", description="Reveals who submitted what song")
86+
async def reveal_submissions(self, interaction: discord.Interaction):
87+
subs = self.bot.state.get_all_submissions()
88+
for userId in subs:
89+
await interaction.channel.send(f"<@{userId}> {subs[userId]}")
90+
await interaction.channel.send(
91+
embed=Embeds.info(
92+
title="Here is who sent each song",
93+
body="Congrats on another successful music monday",
94+
)
95+
)
96+
97+
## debug commands
98+
99+
@app_commands.command(
100+
name="clearall", description="clears ALL submissions (debug command)"
101+
)
102+
async def clearall_debug(self, interaction: discord.Interaction):
103+
return await interaction.response.send_message(
104+
embed=Embeds.info(title="Removed all submissions", body=":wave:")
105+
)
106+
107+
@app_commands.command(
108+
name="whojudge", description="Who is the judge this week (debugcommand)"
109+
)
110+
async def whojudge_debug(self, interaction: discord.Interaction):
111+
return await interaction.response.send_message(
112+
embed=Embeds.info(
113+
title="The next judge is", body=f"# {self.get_judge()[0]}!"
114+
)
115+
)
116+
117+
118+
async def setup(bot):
119+
await bot.add_cog(Commands(bot))

discord_bot/index.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import discord
2+
import os
3+
from dotenv import load_dotenv
4+
from discord.ext import commands
5+
from util.state import State
6+
7+
load_dotenv()
8+
9+
# placeholder command prefix; We only use slash commands
10+
client = commands.Bot(command_prefix="<>", intents=discord.Intents.all())
11+
12+
# load state
13+
14+
client.state = State()
15+
16+
17+
# load all cogs
18+
async def load():
19+
for f in os.listdir("./cogs"):
20+
if f.endswith(".py"):
21+
await client.load_extension(f"cogs.{f[:-3]}")
22+
23+
24+
@client.event
25+
async def on_ready():
26+
print("Bot is ready")
27+
try:
28+
synched_commands = await client.tree.sync()
29+
print(f"Synched {len(synched_commands)} commands")
30+
except Exception as e:
31+
print("Error synching")
32+
33+
34+
async def setup_bot():
35+
await load()
36+
37+
38+
if __name__ == "__main__":
39+
client.setup_hook = setup_bot
40+
client.run(os.getenv("TOKEN"))

discord_bot/requirements.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
aiohappyeyeballs==2.6.1
2+
aiohttp==3.12.13
3+
aiosignal==1.4.0
4+
attrs==25.3.0
5+
audioop-lts==0.2.1
6+
discord.py==2.5.2
7+
frozenlist==1.7.0
8+
idna==3.10
9+
multidict==6.6.3
10+
propcache==0.3.2
11+
python-dotenv==1.1.1
12+
tzdata==2025.2
13+
yarl==1.20.1

discord_bot/util/embeds.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import discord
2+
3+
4+
class Embeds:
5+
6+
### Generic embeds
7+
8+
@staticmethod
9+
def error(body: str):
10+
return discord.Embed(
11+
colour=discord.Colour.red(), title="Error!", description=body
12+
)
13+
14+
@staticmethod
15+
def info(title: str, body: str, footer: str = ""):
16+
embed = discord.Embed(
17+
colour=discord.Colour.blue(), title=title, description=body
18+
)
19+
if len(footer):
20+
embed.set_footer(text=footer)
21+
return embed
22+
23+
@staticmethod
24+
def megainfo(
25+
title: str,
26+
body: str,
27+
fields: list[tuple[str, str, bool]] = [],
28+
footer: str = "",
29+
img: str = "",
30+
thumbnail: str = "",
31+
author: str = "",
32+
):
33+
embed = discord.Embed(
34+
colour=discord.Colour.orange(),
35+
title=title,
36+
description=body,
37+
)
38+
for field in fields:
39+
embed.add_field(name=field[0], value=field[1], inline=field[2])
40+
41+
if len(footer):
42+
embed.set_footer(text=footer)
43+
if len(img):
44+
embed.set_image(url=img)
45+
if len(thumbnail):
46+
embed.set_thumbnail(url=thumbnail)
47+
if len(author):
48+
embed.set_author(name=author)
49+
return embed
50+
51+
@staticmethod
52+
def emphasis(title: str, body: str, footer: str = ""):
53+
embed = discord.Embed(
54+
color=discord.Colour.pink(), title=title, description=body
55+
)
56+
if len(footer):
57+
embed.set_footer(text=footer)
58+
return embed
59+
60+
### Specific embeds
61+
62+
@staticmethod
63+
def word_not_found(word: str):
64+
return Embeds.error(
65+
f"You aren't tracking __'{word}'__. Please choose a different word."
66+
)

discord_bot/util/state.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from datetime import datetime
2+
from zoneinfo import ZoneInfo
3+
from discord.ext import commands
4+
5+
type submissions_map = dict[str, str]
6+
7+
8+
class State:
9+
"""Main state holder for the bot
10+
11+
No safeguards, assumes the following things:
12+
- All submissions take place over the course of one day
13+
- Music monday takes place from 00:00 - 23:59 in EST
14+
"""
15+
16+
def __init__(self):
17+
self.submission_date = None
18+
self.submissions: submissions_map = dict()
19+
20+
def get_date_today(self):
21+
return datetime.now(ZoneInfo("America/New_York")).date()
22+
23+
def set_date_today(self):
24+
self.submission_date = self.get_date_today()
25+
26+
def add_submission(self, user_id: str, song_link: str):
27+
if self.submission_date == None:
28+
self.set_date_today()
29+
30+
# reset submissions upon a new day
31+
if self.submission_date != self.get_date_today():
32+
self.set_date_today()
33+
self.submissions = dict()
34+
35+
self.submissions[user_id] = song_link
36+
37+
def get_all_submissions(self):
38+
return self.submissions
39+
40+
def clear_all_submissions(self):
41+
self.submissions = dict()
42+
43+
44+
class StateBot(commands.Bot):
45+
state: State

discord_bot/util/views.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import discord
2+
from discord.ext import commands
3+
4+
from util.embeds import Embeds
5+
6+
7+
class JudgeView(discord.ui.View):
8+
def __init__(self, songs: dict[str, str], userIds: list[str]):
9+
super().__init__()
10+
self.songs = songs
11+
for idx, userId in enumerate(userIds):
12+
self.add_item(SongButton(str(idx + 1), userId, songs))
13+
14+
15+
class SongButton(discord.ui.Button):
16+
def __init__(self, label: str, key: str, songs: dict[str, str]):
17+
super().__init__(label=label, style=discord.ButtonStyle.primary)
18+
self.index = key
19+
self.songs = songs
20+
21+
async def callback(self, interaction: discord.Interaction):
22+
await interaction.response.send_message(
23+
embed=Embeds.emphasis(
24+
title="Congratulations to the winner!", body=f"<@{self.index}>"
25+
)
26+
)
27+
await interaction.channel.send(content=f"{self.songs[self.index]}")
28+
self.view.stop()

0 commit comments

Comments
 (0)