Merge pull request #105 from stijndcl/pycord-port

Port to Pycord & add newly supported features
pull/106/head
Stijn De Clercq 2022-02-11 23:21:56 +01:00 committed by GitHub
commit eafc1a8674
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1233 additions and 776 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ files/ufora_notifications.json
__pycache__
.env
/venv/
.pytest_cache

View File

@ -1,82 +0,0 @@
from discord.ext import ipc
from functions.database import custom_commands
import json
from quart import Quart, jsonify, request
from quart_cors import cors
from time import time
app = Quart(__name__)
# TODO allow_origin=re.compile(r"http://localhost:.*")
# needs higher Python & Quart version
app = cors(app, allow_origin="*")
app.config.from_object(__name__)
ipc_client = ipc.Client(secret_key="SOME_SECRET_KEY")
@app.route("/ping", methods=["GET"])
async def ping():
"""
Send a ping request, monitors bot latency and endpoint time
"""
latency = await ipc_client.request("get_bot_latency")
return jsonify({"bot_latency": latency, "response_sent": time()})
@app.route("/dm", methods=["POST"])
async def send_dm():
"""
Send a DM to the given user
"""
data = json.loads((await request.body).decode('UTF-8'))
dm = await ipc_client.request(
"send_dm",
user=int(data["userid"]),
message=data.get("message")
)
return jsonify({"response": dm})
@app.route("/custom", methods=["GET"])
async def get_all_custom_commands():
"""
Return a list of all custom commands in the bot
"""
commands = custom_commands.get_all()
return jsonify(commands)
@app.route("/custom/<command_id>")
async def get_custom_command(command_id):
try:
command_id = int(command_id)
except ValueError:
# Id is not an int
return unprocessable_entity("Parameter id was not a valid integer.")
command = custom_commands.get_by_id(command_id)
if command is None:
return page_not_found("")
return jsonify(command)
@app.errorhandler(404)
def page_not_found(e):
return jsonify({"error": "No resource could be found matching the given URL."}), 404
@app.errorhandler(422)
def unprocessable_entity(e):
return jsonify({"error": e}), 422
if __name__ == "__main__":
app.run()

View File

@ -0,0 +1,32 @@
import discord
from discord import ApplicationContext
from discord.ext import commands
from discord.commands import message_command
from startup.didier import Didier
class SchoolCM(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@message_command(name="Pin")
async def _pin_cm(self, ctx: ApplicationContext, message: discord.Message):
# In case people abuse, check if they're blacklisted
blacklist = []
if ctx.user.id in blacklist:
return await ctx.respond(":angry:", ephemeral=True)
if message.is_system():
return await ctx.respond("Dus jij wil system messages pinnen?\nMag niet.", ephemeral=True)
await message.pin(reason=f"Didier Pin door {ctx.user.display_name}")
await ctx.respond("📌", ephemeral=True)
def setup(client: Didier):
# client.add_cog(SchoolCM(client))
# TODO wait for bug to be fixed in lib then uncomment this
# when used in dm, tries to create a DM with the bot?
pass

View File

@ -1,11 +1,10 @@
from dislash import SlashInteraction
from discord import Interaction
from data import constants
from data.snipe import Snipe, Action, should_snipe
import datetime
import discord
from discord.ext import commands
from dislash.application_commands.errors import InteractionCheckFailure
from functions import checks, easterEggResponses, stringFormatters
from functions.database import stats, muttn, custom_commands, commands as command_stats
import pytz
@ -79,6 +78,12 @@ class Events(commands.Cog):
# Earn XP & Message count
stats.sentMessage(message)
@commands.Cog.listener()
async def on_thread_join(self, thread: discord.Thread):
# Join threads automatically
if thread.me is None:
await thread.join()
@commands.Cog.listener()
async def on_command(self, ctx):
"""
@ -104,6 +109,7 @@ class Events(commands.Cog):
# Don't handle commands that have their own custom error handler
if hasattr(ctx.command, 'on_error'):
return
# Someone just mentioned Didier without calling a real command,
# don't care about this error
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure, commands.TooManyArguments, commands.ExpectedClosingQuoteError), ):
@ -115,32 +121,37 @@ class Events(commands.Cog):
await ctx.send("Geen message gevonden die overeenkomt met het opgegeven argument.")
elif isinstance(err, (commands.ChannelNotFound, commands.ChannelNotReadable)):
await ctx.send("Geen channel gevonden dat overeenkomt met het opgegeven argument.")
elif isinstance(err, commands.ThreadNotFound):
await ctx.reply("Thread niet gevonden.", mention_author=False)
# Someone forgot an argument or passed an invalid argument
elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument, commands.UnexpectedQuoteError)):
await ctx.send("Controleer je argumenten.")
await ctx.reply("Controleer je argumenten.", mention_author=False)
else:
usage = stringFormatters.format_command_usage(ctx)
await self.sendErrorEmbed(err, "Command", usage)
@commands.Cog.listener()
async def on_slash_command(self, interaction: SlashInteraction):
async def on_interaction(self, interaction: Interaction):
"""
Function called whenever someone uses a slash command
"""
if not interaction.is_command():
return
print(stringFormatters.format_slash_command_usage(interaction))
command_stats.invoked(command_stats.InvocationType.SlashCommand)
@commands.Cog.listener()
async def on_slash_command_error(self, interaction, err):
async def on_application_command_error(self, ctx: discord.ApplicationContext, err):
# Debugging Didier shouldn't spam the error logs
if self.client.user.id != int(constants.didierId):
raise err
if isinstance(err, InteractionCheckFailure):
return await interaction.reply("Je hebt geen toegang tot dit commando.", ephemeral=True)
if isinstance(err, commands.CheckFailure):
return await ctx.respond("Je hebt geen toegang tot dit commando.", ephemeral=True)
usage = stringFormatters.format_slash_command_usage(interaction)
usage = stringFormatters.format_slash_command_usage(ctx.interaction)
await self.sendErrorEmbed(err, "Slash Command", usage)
@commands.Cog.listener()

View File

@ -1,15 +1,16 @@
from data.embeds.xkcd import XKCDEmbed
from data.menus import paginatedLeaderboard
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, stringFormatters
from functions.database import memes, trump, dadjoke
from functions.memes import generate
import json
import random
import requests
from discord.ext import commands
from data.embeds.xkcd import XKCDEmbed
from data.menus.memes import MemesList
from decorators import help
from enums.help_categories import Category
from functions import checks
from functions.database import memes, trump, dadjoke
from functions.memes import generate
class Fun(commands.Cog):
@ -98,17 +99,10 @@ class Fun(commands.Cog):
if result is None:
return await ctx.send("Deze meme staat niet in de database.")
# Convert to list to support item assignment
fields = list(fields)
generated = generate(result, fields)
# If the request was successful, remove the message calling it
if generated["success"]:
await self.utilsCog.removeMessage(ctx.message)
# Send the meme's url or the error message
await ctx.send(generated["message"])
await ctx.reply(generated["message"], mention_author=False)
@commands.command(name="Memes")
@commands.check(checks.allowedChannels)
@ -118,15 +112,7 @@ class Fun(commands.Cog):
Command that shows a list of memes in the database.
:param ctx: Discord Context
"""
memeList = memes.getAllMemes()
# Turn the list into a list of [Name: fields]
memeList = [": ".join([stringFormatters.title_case(meme[1]),
str(meme[2])]) for meme in sorted(memeList, key=lambda x: x[1])]
pages = paginatedLeaderboard.Pages(source=paginatedLeaderboard.Source(memeList, "Memes", discord.Colour.blue()),
clear_reactions_after=True)
await pages.start(ctx)
return await MemesList(ctx=ctx).send()
@commands.command(name="Pjoke")
@help.Category(category=Category.Fun)

View File

@ -1,5 +1,6 @@
from data import constants
import discord
from discord.commands import SlashCommand
from discord.ext import commands
from enums.help_categories import categories, getCategory, Category
import json
@ -49,7 +50,7 @@ class HelpCommand(commands.MinimalHelpCommand):
return await self.send_bot_help(self.get_bot_mapping())
# Turn dic to lowercase to allow proper name searching
all_commands = dict((k.lower(), v) for k, v in bot.all_commands.items())
all_commands = dict((k.lower(), v) for k, v in bot.all_commands.items() if not isinstance(v, SlashCommand))
if spl[0].lower() not in all_commands:
return await self.send_error_message(await self.command_not_found(spl[0]))

View File

@ -1,26 +0,0 @@
from discord.ext import commands, ipc
class IPC(commands.Cog):
def __init__(self, client):
self.client = client
@ipc.server.route()
async def send_dm(self, data):
print("got here")
user = self.client.get_user(data.user)
await user.send(data.message)
print("sent")
return True
@ipc.server.route()
async def get_bot_latency(self, data):
"""
Get Didier's latency
"""
return self.client.latency * 1000
def setup(client):
client.add_cog(IPC(client))

View File

@ -1,16 +1,12 @@
from data.menus import paginatedLeaderboard
from decorators import help
import discord
from discord.ext import commands
from data.menus import leaderboards
from decorators import help
from enums.help_categories import Category
from enums.numbers import Numbers
from functions import checks, xp
from functions.database import currency, stats, poke, muttn
import math
import requests
from functions import checks
# TODO some sort of general leaderboard because all of them are the same
class Leaderboards(commands.Cog):
def __init__(self, client):
@ -18,7 +14,7 @@ class Leaderboards(commands.Cog):
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
def cog_check(self, _):
return not self.client.locked
@commands.group(name="Leaderboard", aliases=["Lb", "Leaderboards"], case_insensitive=True, usage="[Categorie]*",
@ -34,174 +30,47 @@ class Leaderboards(commands.Cog):
@leaderboard.command(name="Dinks", aliases=["Cash"], hidden=True)
async def dinks(self, ctx):
entries = currency.getAllRows()
platDinks = currency.getAllPlatDinks()
# Take platinum dinks into account
for i, user in enumerate(entries):
if str(user[0]) in platDinks:
# Tuples don't support assignment, cast to list
user = list(user)
user[1] += platDinks[str(user[0])] * Numbers.q.value
entries[i] = user
boardTop = []
for i, user in enumerate(sorted(entries, key=lambda x: (float(x[1]) + float(x[3])), reverse=True)):
if i == 0 and float(user[1]) + float(user[3]) == 0.0:
return await self.emptyLeaderboard(ctx, "Dinks Leaderboard", "Er zijn nog geen personen met Didier Dinks.")
elif float(user[1]) + float(user[3]) > 0.0:
# Get the username in this guild
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,})**".format(name, math.floor(float(user[1]) + float(user[3]))))
else:
boardTop.append("{} ({:,})".format(name, math.floor(float(user[1]) + float(user[3]))))
await self.startPaginated(ctx, boardTop, "Dinks Leaderboard")
lb = leaderboards.DinksLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Corona", hidden=True)
async def corona(self, ctx):
result = requests.get("http://corona.lmao.ninja/v2/countries").json()
result.sort(key=lambda x: int(x["cases"]), reverse=True)
board = []
for land in result:
if land["country"] == "Belgium":
board.append("**{} ({:,})**".format(land["country"], land["cases"]))
else:
board.append("{} ({:,})".format(land["country"], land["cases"]))
await self.startPaginated(ctx, board, "Corona Leaderboard", discord.Colour.red())
lb = leaderboards.CoronaLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True)
async def bitcoin(self, ctx):
users = currency.getAllRows()
boardTop = []
for i, user in enumerate(sorted(users, key=lambda x: x[8], reverse=True)):
# Don't create an empty leaderboard
if i == 0 and float(user[8]) == 0.0:
return await self.emptyLeaderboard(ctx, "Bitcoin Leaderboard", "Er zijn nog geen personen met Bitcoins.")
elif float(user[8]) > 0.0:
# Only add people with more than 0
# Get the username in this guild
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,})**".format(name, round(user[8], 8)))
else:
boardTop.append("{} ({:,})".format(name, round(user[8], 8)))
await self.startPaginated(ctx, boardTop, "Bitcoin Leaderboard")
lb = leaderboards.BitcoinLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Rob", hidden=True)
async def rob(self, ctx):
users = list(stats.getAllRows())
boardTop = []
for i, user in enumerate(sorted(users, key=lambda x: x[4], reverse=True)):
# Don't create an empty leaderboard
if i == 0 and float(user[4]) == 0.0:
return await self.emptyLeaderboard(ctx, "Rob Leaderboard", "Er heeft nog niemand Didier Dinks gestolen.")
elif float(user[4]) > 0.0:
# Only add people with more than 0
# Get the username in this guild
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,})**".format(name, math.floor(float(user[4]))))
else:
boardTop.append("{} ({:,})".format(name, math.floor(float(user[4]))))
await self.startPaginated(ctx, boardTop, "Rob Leaderboard")
lb = leaderboards.RobLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Poke", hidden=True)
async def poke(self, ctx):
s = stats.getAllRows()
blacklist = poke.getAllBlacklistedUsers()
boardTop = []
for i, user in enumerate(sorted(s, key=lambda x: x[1], reverse=True)):
if i == 0 and int(user[1]) == 0:
return await self.emptyLeaderboard(ctx, "Poke Leaderboard", "Er is nog niemand getikt.")
elif int(user[1]) == 0:
break
# Don't include blacklisted users
elif str(user[0]) not in blacklist:
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,})**".format(name, round(int(user[1]))))
else:
boardTop.append("{} ({:,})".format(name, round(int(user[1]))))
await self.startPaginated(ctx, boardTop, "Poke Leaderboard")
lb = leaderboards.PokeLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Xp", aliases=["Level"], hidden=True)
async def xp(self, ctx):
s = stats.getAllRows()
boardTop = []
for i, user in enumerate(sorted(s, key=lambda x: x[12], reverse=True)):
if int(user[12]) == 0:
break
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} (Level {:,} | {:,} XP)**".format(name,
xp.calculate_level(round(int(user[12]))),
round(int(user[12]))))
else:
boardTop.append("{} (Level {:,} | {:,} XP)".format(name,
xp.calculate_level(round(int(user[12]))),
round(int(user[12]))))
await self.startPaginated(ctx, boardTop, "XP Leaderboard")
lb = leaderboards.XPLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Messages", aliases=["Mc", "Mess"], hidden=True)
async def messages(self, ctx):
s = stats.getAllRows()
boardTop = []
message_count = stats.getTotalMessageCount()
for i, user in enumerate(sorted(s, key=lambda x: x[11], reverse=True)):
if int(user[11]) == 0:
break
perc = round(int(user[11]) * 100 / message_count, 2)
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,} | {}%)**".format(name, round(int(user[11])), perc))
else:
boardTop.append("{} ({:,} | {}%)".format(name, round(int(user[11])), perc))
await self.startPaginated(ctx, boardTop, "Messages Leaderboard")
lb = leaderboards.MessageLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True)
async def muttn(self, ctx):
users = muttn.getAllRows()
boardTop = []
for i, user in enumerate(sorted(users, key=lambda x: x[1], reverse=True)):
if i == 0 and int(user[1]) == 0:
return await self.emptyLeaderboard(ctx, "Muttn Leaderboard", "Der zittn nog geen muttns in de server.")
if float(user[1]) == 0:
break
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({})%**".format(name, round(float(user[1]), 2)))
else:
boardTop.append("{} ({}%)".format(name, round(float(user[1]), 2)))
await self.startPaginated(ctx, boardTop, "Muttn Leaderboard")
lb = leaderboards.MuttnLeaderboard(ctx=ctx)
await lb.send()
async def callLeaderboard(self, name, ctx):
await [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0](ctx)
async def startPaginated(self, ctx, source, name, colour=discord.Colour.blue()):
pages = paginatedLeaderboard.Pages(source=paginatedLeaderboard.Source(source, name, colour),
clear_reactions_after=True)
await pages.start(ctx)
async def emptyLeaderboard(self, ctx, name, message, colour=discord.Colour.blue()):
embed = discord.Embed(colour=colour)
embed.set_author(name=name)
embed.description = message
await ctx.send(embed=embed)
command = [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0]
await command(ctx)
def setup(client):

View File

@ -151,7 +151,7 @@ class Oneliners(commands.Cog):
@commands.command(name="Inspire")
@help.Category(Category.Other)
async def inspire(self, ctx):
image = get("http://inspirobot.me/api?generate=true")
image = get("https://inspirobot.me/api?generate=true")
if image.status_code == 200:
await ctx.send(image.text)

View File

@ -1,12 +1,13 @@
import discord
from discord.ext import commands
from data.embeds.snipe import EditSnipe, DeleteSnipe
from data.links import get_link_for
from data.menus import custom_commands
from data.snipe import Action, Snipe
from decorators import help
from enums.help_categories import Category
from functions.database.custom_commands import get_all
from functions.stringFormatters import capitalize
from functions.utils import reply_to_reference
from startup.didier import Didier
@ -14,10 +15,22 @@ class Other(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
# TODO add locked field to Didier instead of client
# # Don't allow any commands to work when locked
# def cog_check(self, ctx):
# return not self.client.locked
# Don't allow any commands to work when locked
def cog_check(self, _):
return not self.client.locked
@commands.command(name="Link", usage="[Naam]")
@help.Category(category=Category.Other)
async def link(self, ctx: commands.Context, name: str):
"""
Send commonly used links
"""
match = get_link_for(name)
if match is None:
return await ctx.reply(f"Geen match gevonden voor \"{name}\".", mention_author=False, delete_after=15)
await reply_to_reference(ctx, content=match)
@commands.command(name="Custom")
@help.Category(category=Category.Didier)
@ -25,10 +38,17 @@ class Other(commands.Cog):
"""
Get a list of all custom commands
"""
all_commands = get_all()
formatted = list(sorted(map(lambda x: capitalize(x["name"]), all_commands)))
src = custom_commands.CommandsList(formatted)
await custom_commands.Pages(source=src, clear_reactions_after=True).start(ctx)
await custom_commands.CommandsList(ctx).send()
@commands.command(name="Join", usage="[Thread]")
@help.Category(category=Category.Didier)
async def join_thread(self, ctx, thread: discord.Thread):
"""
Join threads
"""
if thread.me is None:
await thread.join()
await ctx.message.add_reaction("")
@commands.command(name="Snipe")
@help.Category(category=Category.Other)

View File

@ -1,4 +1,5 @@
from data import schedule
from data.courses import find_course_from_name
from data.embeds.deadlines import Deadlines
from data.embeds.food import Menu
from decorators import help
@ -8,6 +9,7 @@ from enums.help_categories import Category
from functions import config, les
from functions.stringFormatters import capitalize
from functions.timeFormatters import skip_weekends
from functions.utils import reply_to_reference
class School(commands.Cog):
@ -82,9 +84,28 @@ class School(commands.Cog):
if message.is_system():
return await ctx.send("Dus jij wil system messages pinnen?\nMag niet.")
await message.pin(reason="Didier Pin door {}".format(ctx.author.display_name))
await message.pin(reason=f"Didier Pin door {ctx.author.display_name}")
await ctx.message.add_reaction("")
@commands.command(name="Fiche", usage="[Vak]", aliases=["guide", "studiefiche"])
@help.Category(category=Category.School)
async def study_guide(self, ctx, name: str):
"""
Send links to study guides
"""
# Find code corresponding to the search query
course = find_course_from_name(name)
# Code not found
if course is None:
return await ctx.reply(f"Onbekend vak: \"{name}\".", mention_author=False, delete_after=15)
# Get the guide for the current year
year = 2018 + int(config.get("year"))
link = f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}"
return await reply_to_reference(ctx, content=link)
@commands.command(name="Deadlines", aliases=["dl"])
@help.Category(category=Category.School)
async def deadlines(self, ctx):

View File

@ -1,87 +0,0 @@
import datetime
import json
from discord.ext import commands
from dislash import SlashInteraction, slash_command, Option, OptionType, check
from functions.checks import isMe
from functions.timeFormatters import fromString
from startup.didier import Didier
class DBSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="db")
@check(isMe)
async def _db_slash(self, interaction: SlashInteraction):
pass
@_db_slash.sub_command_group(name="add")
async def _add_slash(self, interaction: SlashInteraction):
pass
@_add_slash.sub_command(
name="deadline",
options=[
Option(
"year",
description="Year (1-based)",
type=OptionType.INTEGER,
required=True
),
Option(
"course",
description="Course (abbreviated)",
type=OptionType.STRING,
required=True
),
Option(
"name",
description="Name of the deadline/project",
type=OptionType.STRING,
required=True
),
Option(
"date",
description="Date (DD/MM)",
type=OptionType.STRING,
required=True
),
Option(
"time",
description="Timestamp (HH:MM or HH:MM:SS)",
type=OptionType.STRING,
required=False
)
]
)
async def _add_deadline_slash(self, interaction: SlashInteraction, year: int, course: str, name: str, date: str, time: str = "00:00:00"):
with open("files/deadlines.json", "r") as f:
deadlines = json.load(f)
date += "/" + str(datetime.datetime.now().year)
# Fix format
if time.count(":") == 1:
time += ":00"
dt = fromString(f"{date} {time}", formatString="%d/%m/%Y %H:%M:%S", tzinfo=None)
# Add year & course if necessary
if str(year) not in deadlines:
deadlines[str(year)] = {}
if course not in deadlines[str(year)]:
deadlines[str(year)][course] = {}
deadlines[str(year)][course][name] = round(dt.timestamp())
with open("files/deadlines.json", "w") as f:
json.dump(deadlines, f)
await interaction.reply("Addition successful", ephemeral=True)
def setup(client: Didier):
client.add_cog(DBSlash(client))

View File

@ -1,5 +1,5 @@
from discord.ext import commands
from dislash import SlashInteraction, slash_command, Option, OptionType
from discord.commands import slash_command, ApplicationContext, Option
from data.embeds.urban_dictionary import Definition
from startup.didier import Didier
@ -9,15 +9,10 @@ class DefineSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="define",
description="Urban Dictionary",
options=[
Option("query", "Search query", OptionType.STRING, required=True)
]
)
async def _define_slash(self, interaction: SlashInteraction, query):
@slash_command(name="define", description="Urban Dictionary")
async def _define_slash(self, ctx: ApplicationContext, query: Option(str, "Search query", required=True)):
embed = Definition(query).to_embed()
await interaction.reply(embed=embed)
await ctx.respond(embed=embed)
def setup(client: Didier):

View File

@ -1,6 +1,6 @@
from discord.ext import commands
from dislash import SlashInteraction, slash_command, Option, OptionType
from functions import config, checks
from discord.commands import Option, SlashCommandGroup, ApplicationContext, permissions
from functions import config
from functions.football import get_matches, get_table, get_jpl_code
from startup.didier import Didier
@ -9,35 +9,29 @@ class FootballSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="jpl", description="Jupiler Pro League commands")
async def _jpl_group(self, interaction: SlashInteraction):
pass
_jpl_group = SlashCommandGroup("jpl", "Jupiler Pro League commands")
@_jpl_group.sub_command(name="matches",
description="Schema voor een bepaalde speeldag",
options=[
Option("day", "Speeldag (default huidige)", OptionType.INTEGER)
]
)
async def _jpl_matches_slash(self, interaction: SlashInteraction, day: int = None):
@_jpl_group.command(name="matches", description="Schema voor een bepaalde speeldag")
async def _jpl_matches_slash(self, ctx: ApplicationContext,
day: Option(int, name="day", description="Speeldag (default huidige)", required=False, default=None)
):
# Default is current day
if day is None:
day = int(config.get("jpl_day"))
await interaction.reply(get_matches(day))
await ctx.respond(get_matches(day))
@_jpl_group.sub_command(name="table", description="Huidige rangschikking")
async def _jpl_table_slash(self, interaction: SlashInteraction):
await interaction.reply(get_table())
@_jpl_group.sub_command(name="update", description="Update de code voor deze competitie (owner-only)")
async def _jpl_update_slash(self, interaction: SlashInteraction):
if not await checks.isMe(interaction):
return await interaction.reply(f"Je hebt geen toegang tot dit commando.")
@_jpl_group.command(name="table", description="Huidige rangschikking")
async def _jpl_table_slash(self, ctx: ApplicationContext):
await ctx.response.defer()
await ctx.send_followup(get_table())
@_jpl_group.command(name="update", description="Update de code voor deze competitie (owner-only)", default_permission=False)
@permissions.is_owner()
async def _jpl_update_slash(self, ctx: ApplicationContext):
code = get_jpl_code()
config.config("jpl", code)
await interaction.reply(f"Done (code: {code})")
await ctx.respond(f"Done (code: {code})")
def setup(client: Didier):

View File

@ -1,28 +1,69 @@
from discord.ext import commands
from dislash import SlashInteraction, slash_command, Option, OptionType
from discord.commands import slash_command, ApplicationContext, Option, AutocompleteContext
from functions.database import memes
from functions.database.memes import getAllMemes
from data.embeds.xkcd import XKCDEmbed
from data.menus.memes import MemesList
from functions.memes import generate
from functions.stringFormatters import title_case
from startup.didier import Didier
all_memes = getAllMemes()
def autocomplete_memes(ctx: AutocompleteContext) -> list[str]:
starting = []
containing = []
val = ctx.value.lower()
# First show matches that start with this word, then matches that contain it
for meme in all_memes:
if meme[1].startswith(val):
starting.append(title_case(meme[1]))
elif val in meme[1]:
containing.append(title_case(meme[1]))
return [*starting, *containing]
class FunSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(
name="xkcd",
description="Zoek xkcd comics",
options=[
Option(
"num",
description="Nummer van de comic (default de comic van vandaag).",
type=OptionType.INTEGER,
required=False
)
]
)
async def _xkcd_slash(self, interaction: SlashInteraction, num: int = None):
return await interaction.reply(embed=XKCDEmbed(num).create())
@slash_command(name="xkcd", description="Zoek xkcd comics")
async def _xkcd_slash(self, ctx: ApplicationContext,
num: Option(int, description="Nummer van de comic (default de comic van vandaag).", required=False, default=None)
):
return await ctx.respond(embed=XKCDEmbed(num).create())
@slash_command(name="memes", description="Lijst van memegen-memes")
async def _memes_slash(self, ctx: ApplicationContext):
return await MemesList(ctx=ctx).respond()
@slash_command(name="memegen", description="Genereer memes")
async def _memegen_slash(self, ctx: ApplicationContext,
meme: Option(str, description="Naam van de template", required=True, autocomplete=autocomplete_memes),
field1: Option(str, required=True),
field2: Option(str, required=False, default=""),
field3: Option(str, required=False, default=""),
field4: Option(str, required=False, default="")):
# Get the meme info that corresponds to this name
result: memes.Meme = memes.getMeme(meme)
# No meme found
if result is None:
return await ctx.respond("Deze meme staat niet in de database.", ephemeral=True)
await ctx.response.defer()
fields = (field1, field2, field3, field4)
generated = generate(result, fields)
# Send generated meme or error message
await ctx.send_followup(generated["message"])
def setup(client: Didier):

View File

@ -1,5 +1,5 @@
from discord.ext import commands
from dislash import slash_command, SlashInteraction, Option, OptionType
from discord.commands import slash_command, ApplicationContext, Option
from functions.scrapers.google import google_search, create_google_embed
from startup.didier import Didier
@ -8,20 +8,15 @@ class GoogleSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="google",
description="Google search",
options=[
Option("query", "Search query", OptionType.STRING, required=True)
]
)
async def _google_slash(self, interaction: SlashInteraction, query: str):
@slash_command(name="google", description="Google search")
async def _google_slash(self, ctx: ApplicationContext, query: Option(str, "Search query")):
result = google_search(query)
if not result.results:
return await interaction.reply("Er ging iets fout (Response {})".format(result.status_code))
return await ctx.respond("Er ging iets fout (Response {})".format(result.status_code))
embed = create_google_embed(result)
await interaction.reply(embed=embed)
await ctx.respond(embed=embed)
def setup(client: Didier):

View File

@ -0,0 +1,41 @@
from discord.ext import commands
from discord.commands import slash_command, ApplicationContext, AutocompleteContext, Option
from requests import get
from data.links import load_all_links, get_link_for
from startup.didier import Didier
links = load_all_links()
def link_autocomplete(ctx: AutocompleteContext) -> list[str]:
return [link for link in links if link.lower().startswith(ctx.value.lower())]
class OtherSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="inspire", description="Genereer quotes via Inspirobot.")
async def _inspire_slash(self, ctx: ApplicationContext):
image = get("https://inspirobot.me/api?generate=true")
if image.status_code == 200:
await ctx.respond(image.text)
else:
await ctx.respond("Uh oh API down.")
@slash_command(name="link", description="Shortcut voor nuttige links.")
async def _link_slash(self, ctx: ApplicationContext,
name: Option(str, description="Naam van de link.", required=True,
autocomplete=link_autocomplete)):
match = get_link_for(name)
if match is None:
return await ctx.respond(f"Geen match gevonden voor \"{name}\".")
return await ctx.respond(match)
def setup(client: Didier):
client.add_cog(OtherSlash(client))

View File

@ -1,7 +1,8 @@
from discord.ext import commands
from dislash import SlashInteraction, slash_command, Option, OptionType
from discord.commands import slash_command, ApplicationContext, Option, AutocompleteContext
from data import schedule
from data.courses import load_courses, find_course_from_name
from data.embeds.food import Menu
from data.embeds.deadlines import Deadlines
from functions import les, config
@ -10,66 +11,77 @@ from functions.timeFormatters import skip_weekends
from startup.didier import Didier
# Preload autocomplete constants to allow for smoother results
courses = load_courses()
days = ["Morgen", "Overmorgen", "Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag"]
def day_autocomplete(ctx: AutocompleteContext) -> list[str]:
return [day for day in days if day.lower().startswith(ctx.value.lower())]
def course_autocomplete(ctx: AutocompleteContext) -> list[str]:
return [course for course in courses if course.lower().startswith(ctx.value.lower())]
class SchoolSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(
name="eten",
description="Menu in de UGENT resto's op een bepaalde dag",
options=[
Option(
"dag",
description="Dag",
type=OptionType.STRING
)
]
)
async def _food_slash(self, interaction: SlashInteraction, dag: str = None):
@slash_command(name="eten", description="Menu in de UGent resto's op een bepaalde dag")
async def _food_slash(self, ctx: ApplicationContext,
dag: Option(str, description="Dag", required=False, default=None, autocomplete=day_autocomplete)
):
embed = Menu(dag).to_embed()
await interaction.reply(embed=embed)
await ctx.respond(embed=embed)
@slash_command(name="deadlines", description="Aanstaande deadlines")
async def _deadlines_slash(self, interaction: SlashInteraction):
async def _deadlines_slash(self, ctx: ApplicationContext):
embed = Deadlines().to_embed()
await interaction.reply(embed=embed)
await ctx.respond(embed=embed)
@slash_command(
name="les",
description="Lessenrooster voor [Dag] (default vandaag)",
options=[
Option(
"dag",
description="dag",
type=OptionType.STRING,
required=False
)
]
)
async def _schedule_slash(self, interaction: SlashInteraction, day: str = None):
@slash_command(name="les", description="Lessenrooster voor [Dag] (default vandaag)",)
async def _schedule_slash(self, ctx: ApplicationContext,
dag: Option(str, description="Dag", required=False, default=None, autocomplete=day_autocomplete)
):
"""It's late and I really don't want to refactor the original right now"""
if day is not None:
day = day.lower()
if dag is not None:
dag = dag.lower()
date = les.find_target_date(day)
date = les.find_target_date(dag)
# Person explicitly requested a weekend-day
if day is not None and day.lower() in ("morgen", "overmorgen") and date.weekday() > 4:
return await interaction.reply(f"{capitalize(day)} is het weekend.", ephemeral=True)
if dag is not None and dag.lower() in ("morgen", "overmorgen") and date.weekday() > 4:
return await ctx.respond(f"{capitalize(dag)} is het weekend.", ephemeral=True)
date = skip_weekends(date)
s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), day is not None)
s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), dag is not None)
if s.semester_over:
return await interaction.reply("Het semester is afgelopen.", ephemeral=True)
return await ctx.respond("Het semester is afgelopen.", ephemeral=True)
# DM only shows user's own minor
if interaction.guild is None:
minor_roles = [*schedule.find_minor(self.client, interaction.author.id)]
return await interaction.reply(embed=s.create_schedule(minor_roles=minor_roles).to_embed())
if ctx.guild is None:
minor_roles = [*schedule.find_minor(self.client, ctx.interaction.user.id)]
return await ctx.respond(embed=s.create_schedule(minor_roles=minor_roles).to_embed())
return await interaction.reply(embed=s.create_schedule().to_embed())
return await ctx.respond(embed=s.create_schedule().to_embed())
@slash_command(name="fiche", description="Zoek de studiefiche voor een vak.")
async def _study_guide_slash(self, ctx: ApplicationContext,
vak: Option(str, description="Naam van het vak. Afkortingen werken ook, maar worden niet geautocompletet.",
required=True, autocomplete=course_autocomplete)):
# Find code corresponding to the search query
course = find_course_from_name(vak, courses)
# Code not found
if course is None:
return await ctx.respond(f"Onbekend vak: \"{vak}\".", ephemeral=True)
# Get the guide for the current year
year = 2018 + int(config.get("year"))
return await ctx.respond(f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}")
def setup(client: Didier):

View File

@ -1,5 +1,5 @@
from discord.ext import commands
from dislash import SlashInteraction, slash_command, Option, OptionType
from discord.commands import slash_command, ApplicationContext, Option
from data.embeds.translate import Translation
from startup.didier import Didier
@ -9,18 +9,14 @@ class TranslateSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(
name="translate",
description="Google Translate",
options=[
Option("text", "Tekst om te vertalen", OptionType.STRING, required=True),
Option("from_lang", "Taal om van te vertalen (default auto-detect)", OptionType.STRING),
Option("to_lang", "Taal om naar te vertalen (default NL)", OptionType.STRING)
]
)
async def _translate_slash(self, interaction: SlashInteraction, text: str, from_lang: str = "auto", to_lang: str = "nl"):
@slash_command(name="translate", description="Google Translate")
async def _translate_slash(self, ctx: ApplicationContext,
text: Option(str, description="Tekst om te vertalen"),
from_lang: Option(str, description="Taal om van te vertalen (default auto-detect)", default="auto"),
to_lang: Option(str, description="Taal om naar te vertalen (default NL)", default="nl")
):
translation = Translation(text=text, fr=from_lang.lower(), to=to_lang.lower())
await interaction.reply(embed=translation.to_embed())
await ctx.respond(embed=translation.to_embed())
def setup(client: Didier):

View File

@ -1,5 +1,4 @@
from converters.numbers import Abbreviated
from data.menus import storePages
from decorators import help
import discord
from discord.ext import commands
@ -22,11 +21,12 @@ class Store(commands.Cog):
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def store(self, ctx):
entries = store.getAllItems()
await storePages.Pages(source=storePages.Source(entries), clear_reactions_after=True).start(ctx)
pass
# entries = store.getAllItems()
# await storePages.Pages(source=storePages.Source(entries), clear_reactions_after=True).start(ctx)
@store.command(name="Buy", aliases=["Get"], hidden=True)
async def storeBuy(self, ctx, item, amount: Abbreviated = 1):
async def store_buy(self, ctx, item, amount: Abbreviated = 1):
if amount is None:
return
@ -56,7 +56,7 @@ class Store(commands.Cog):
))
@store.command(name="Sell", hidden=True)
async def storeSell(self, ctx, itemid, amount: Abbreviated = 1):
async def store_sell(self, ctx, itemid, amount: Abbreviated = 1):
if amount is None:
return
await self.sell(ctx, itemid, amount)

View File

@ -1,127 +0,0 @@
from data.menus import paginatedLeaderboard
from decorators import help
import discord
from discord.ext import commands, menus
from enums.help_categories import Category
from functions import checks, timeFormatters
import requests
class Train(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Train", aliases=["Trein"], usage="[Vertrek]* [Bestemming]")
@help.Category(category=Category.School)
async def train(self, ctx, *args):
if not args or len(args) > 2:
await ctx.send("Controleer je argumenten.")
return
destination = args[-1]
departure = args[0] if len(args) > 1 else "Gent Sint-Pieters"
req = requests.get(
"http://api.irail.be/connections/?from={}&to={}&alerts=true&lang=nl&format=json".format(departure,
destination)).json()
if "error" in req:
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Treinen van {} naar {}".format(
self.formatCity(departure), self.formatCity(destination)))
embed.add_field(name="Error", value="Er ging iets fout, probeer het later opnieuw.", inline=False)
await self.sendEmbed(ctx, embed)
return
pages = paginatedLeaderboard.Pages(source=TrainPagination(self.formatConnections(req["connection"]),
self.formatCity(departure),
self.formatCity(destination)),
clear_reactions_after=True)
await pages.start(ctx)
def formatConnections(self, connections):
response = []
for connection in sorted(connections, key=lambda con: con["departure"]["time"]):
conn = {}
if connection["departure"]["canceled"] != "0" or connection["arrival"]["canceled"] != "0":
conn = {"Canceled": "Afgeschaft"}
dep = connection["departure"]
arr = connection["arrival"]
conn["depStation"] = self.formatCity(dep["station"])
conn["depTime"] = self.formatTime(dep["time"])
conn["delay"] = self.formatDelay(dep["delay"])
conn["track"] = dep["platform"]
conn["arrStation"] = self.formatCity(arr["station"])
conn["direction"] = self.formatCity(dep["direction"]["name"])
conn["arrTime"] = self.formatTime(arr["time"])
conn["duration"] = self.formatTime(connection["duration"])
response.append(conn)
return response
def formatTime(self, timestamp):
if int(timestamp) <= 86400:
minutes = int(timestamp) // 60
if minutes < 60:
return str(minutes) + "m"
return "{}h{:02}m".format(minutes // 60, minutes % 60)
else:
return timeFormatters.epochToDate(int(timestamp), "%H:%M")["date"]
def formatDelay(self, seconds):
seconds = int(seconds)
return self.sign(seconds) + self.formatTime(abs(seconds)) if seconds != 0 else ""
def sign(self, number):
return "-" if int(number) < 0 else "+"
def formatCity(self, city):
city = city[0].upper() + city[1:]
arr = []
for i, letter in enumerate(city):
if (i > 0 and (city[i - 1] == " " or city[i - 1] == "-")) or i == 0:
arr.append(letter.upper())
else:
arr.append(letter.lower())
return "".join(arr)
async def sendEmbed(self, ctx, embed):
if await checks.allowedChannels(ctx):
await ctx.send(embed=embed)
else:
await ctx.author.send(embed=embed)
class TrainPagination(menus.ListPageSource):
def __init__(self, data, departure, destination):
super().__init__(data, per_page=3)
self.departure = departure
self.destination = destination
async def format_page(self, menu: menus.MenuPages, entries):
offset = menu.current_page * self.per_page
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Treinen van {} naar {}".format(self.departure, self.destination))
embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages()))
for i, connection in enumerate(entries, start=offset):
afgeschaft = "Canceled" in connection
embed.add_field(name="Van", value=str(connection["depStation"]), inline=True)
embed.add_field(name="Om", value=str(connection["depTime"]), inline=True)
embed.add_field(name="Spoor", value=str(connection["track"]), inline=True)
embed.add_field(name="Richting", value=str(connection["direction"]), inline=True)
embed.add_field(name="Aankomst", value=(str(connection["arrTime"])
if not afgeschaft else "**AFGESCHAFT**"), inline=True)
embed.add_field(name="Vertraging", value=str(connection["delay"]) if connection["delay"] != "" else "0",
inline=True)
# White space
if i - offset < 2:
embed.add_field(name="\u200b", value="\u200b", inline=False)
return embed
def setup(client):
client.add_cog(Train(client))

67
data/courses.py 100644
View File

@ -0,0 +1,67 @@
from dataclasses import dataclass
from typing import Optional
import dacite
import json
from os import path
@dataclass
class Course:
abbreviations: list[str]
code: str
name: str
year: int
alt: Optional[str] = None
def load_courses() -> dict[str, Course]:
"""Create a list of all courses"""
# Allows testing
filepath = path.join(path.dirname(__file__), "..", "files", "courses.json")
with open(filepath, "r") as file:
data = json.load(file)
courses = {}
for course_name in data:
# Add name into the dict to allow flexibility
course_data = data[course_name]
course_data["name"] = course_name
courses[course_name] = dacite.from_dict(data_class=Course, data=course_data)
return courses
def find_course_from_name(name: str, courses: Optional[dict[str, Course]] = None, case_insensitive: bool = True) -> Optional[Course]:
# Allow passing a course dict in to avoid having to create it all the time
if courses is None:
courses = load_courses()
if case_insensitive:
name = name.lower()
def _perhaps_lower(inp: str) -> str:
"""Cast a string to lowercase if necessary"""
if case_insensitive:
return inp.lower()
return inp
# Iterate over all courses to look for a match
for course_name, course in courses.items():
# Check name first
if _perhaps_lower(course_name) == name:
return course
# Then abbreviations
for abbreviation in course.abbreviations:
if _perhaps_lower(abbreviation) == name:
return course
# Finally alternative names
if course.alt is not None and _perhaps_lower(course.alt) == name:
return course
return None

16
data/links.py 100644
View File

@ -0,0 +1,16 @@
import json
from typing import Optional
def load_all_links() -> dict[str, str]:
with open("files/links.json", "r") as file:
return json.load(file)
def get_link_for(name: str) -> Optional[str]:
links = load_all_links()
for link in links:
if link.lower() == name.lower():
return links[link]
return None

View File

@ -1,21 +1,18 @@
import discord
from discord.ext import menus
from typing import Union
from discord import ApplicationContext
from discord.ext.commands import Context
from data.menus.paginated import Paginated
from functions.database.custom_commands import get_all
from functions.stringFormatters import capitalize
class CommandsList(menus.ListPageSource):
def __init__(self, data, colour=discord.Colour.blue()):
super().__init__(data, per_page=15)
self.colour = colour
class CommandsList(Paginated):
def __init__(self, ctx: Union[ApplicationContext, Context]):
all_commands = get_all()
commands_sorted = list(sorted(map(lambda x: (capitalize(x["name"]),), all_commands)))
super().__init__(ctx=ctx, title="Custom Commands", data=commands_sorted, per_page=15)
async def format_page(self, menu: menus.MenuPages, entries):
embed = discord.Embed(colour=self.colour)
embed.set_author(name="Custom Commands")
embed.description = "\n".join(entries)
embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages()))
return embed
class Pages(menus.MenuPages):
def __init__(self, source, clear_reactions_after, timeout=30.0):
super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after)
def format_entry(self, index: int, value: tuple) -> str:
return value[0]

View File

@ -0,0 +1,246 @@
import math
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Union, Optional
import discord
import requests
from discord import ApplicationContext
from discord.ext.commands import Context
from data.menus.paginated import Paginated
from enums.numbers import Numbers
from functions import xp
from functions.database import currency, stats, poke, muttn
from functions.utils import get_display_name
@dataclass
class Leaderboard(Paginated, ABC):
highlight: str = None
colour: discord.Colour = discord.Colour.blue()
fetch_names: bool = True
ignore_non_pos: bool = True
def __post_init__(self):
self.data = self.process_data(self.get_data())
@abstractmethod
def get_data(self) -> list[tuple]:
pass
def process_data(self, entries: list[tuple]) -> Optional[list[tuple]]:
data = []
for i, v in enumerate(sorted(entries, key=self.get_value, reverse=True)):
entry_data = self.get_value(v)
# Leaderboard is empty
if i == 0 and entry_data == 0 and self.ignore_non_pos:
return None
# Ignore entries with no data
if self.ignore_non_pos and entry_data <= 0:
continue
data.append((self.get_key(v), f"{entry_data:,}", entry_data,))
return data
def get_key(self, data: tuple):
return data[0]
def get_value(self, data: tuple):
return data[1]
def _should_highlight(self, data) -> bool:
"""Check if an entry should be highlighted"""
if self.fetch_names:
return data == self.ctx.author.id
return data == self.highlight
def format_entry_data(self, data: tuple) -> str:
return str(data[1])
def format_entry(self, index: int, data: tuple) -> str:
name = data[0]
if self.fetch_names:
name = get_display_name(self.ctx, int(data[0]))
s = f"{index + 1}: {name} ({self.format_entry_data(data)})"
if self._should_highlight(data[0]):
return f"**{s}**"
return s
@property
def empty_description(self) -> str:
return ""
async def empty_leaderboard(self, ctx: Union[ApplicationContext, Context], **kwargs):
embed = discord.Embed(colour=self.colour)
embed.set_author(name=self.title)
embed.description = self.empty_description
if isinstance(ctx, ApplicationContext):
return await ctx.respond(embed=embed)
return await ctx.reply(embed=embed, **kwargs)
async def respond(self, **kwargs) -> discord.Message:
if self.data is None:
return await self.empty_leaderboard(self.ctx, **kwargs)
return await super().respond(**kwargs)
async def send(self, **kwargs) -> discord.Message:
if self.data is None:
return await self.empty_leaderboard(self.ctx, mention_author=False, **kwargs)
return await super().send(mention_author=False, **kwargs)
@dataclass
class BitcoinLeaderboard(Leaderboard):
title: str = field(default="Bitcoin Leaderboard")
def get_data(self) -> list[tuple]:
return currency.getAllRows()
def get_value(self, data: tuple):
return round(float(data[8]), 8)
@property
def empty_description(self) -> str:
return "Er zijn nog geen personen met Bitcoins."
@dataclass
class CoronaLeaderboard(Leaderboard):
colour: discord.Colour = field(default=discord.Colour.red())
fetch_names: bool = field(default=False)
highlight: str = field(default="Belgium")
title: str = field(default="Corona Leaderboard")
def get_data(self) -> list[tuple]:
result = requests.get("https://disease.sh/v3/covid-19/countries").json()
result.sort(key=lambda x: int(x["cases"]), reverse=True)
data = []
for country in result:
data.append((country["country"], f"{country['cases']:,}", country["cases"]))
return data
def get_value(self, data: tuple):
return data[2]
@dataclass
class DinksLeaderboard(Leaderboard):
title: str = field(default="Dinks Leaderboard")
def get_data(self) -> list[tuple]:
entries = currency.getAllRows()
platDinks = currency.getAllPlatDinks()
# Take platinum dinks into account
for i, user in enumerate(entries):
if str(user[0]) in platDinks:
# Tuples don't support assignment, cast to list
user = list(user)
user[1] += platDinks[str(user[0])] * Numbers.q.value
entries[i] = user
return entries
def get_value(self, data: tuple):
return float(data[1]) + float(data[3])
@property
def empty_description(self) -> str:
return "Er zijn nog geen personen met Didier Dinks."
@dataclass
class MessageLeaderboard(Leaderboard):
title: str = field(default="Message Leaderboard")
message_count: int = field(init=False)
def get_data(self) -> list[tuple]:
entries = stats.getAllRows()
self.message_count = stats.getTotalMessageCount()
return entries
def get_value(self, data: tuple):
return round(int(data[11]))
def format_entry_data(self, data: tuple) -> str:
perc = round(data[2] * 100 / self.message_count, 2)
return f"{data[2]:,} | {perc}%"
@dataclass
class MuttnLeaderboard(Leaderboard):
title: str = field(default="Muttn Leaderboard")
def get_data(self) -> list[tuple]:
return muttn.getAllRows()
def get_value(self, data: tuple):
return round(float(data[1]), 2)
def format_entry_data(self, data: tuple) -> str:
return f"{data[2]}%"
def empty_description(self) -> str:
return "Der zittn nog geen muttns in de server."
@dataclass
class PokeLeaderboard(Leaderboard):
title: str = field(default="Poke Leaderboard")
def get_data(self) -> list[tuple]:
data = stats.getAllRows()
blacklist = poke.getAllBlacklistedUsers()
return list(filter(lambda x: x[0] not in blacklist, data))
def get_value(self, data: tuple):
return round(int(data[1]))
@property
def empty_description(self) -> str:
return "Er is nog niemand getikt."
@dataclass
class RobLeaderboard(Leaderboard):
title: str = field(default="Rob Leaderboard")
def get_data(self) -> list[tuple]:
return list(stats.getAllRows())
def get_value(self, data: tuple):
return math.floor(float(data[4]))
@property
def empty_description(self) -> str:
return "Er heeft nog niemand Didier Dinks gestolen."
@dataclass
class XPLeaderboard(Leaderboard):
title: str = field(default="XP Leaderboard")
def get_data(self) -> list[tuple]:
return stats.getAllRows()
def get_value(self, data: tuple):
return round(int(data[12]))
def format_entry_data(self, data: tuple) -> str:
entry = data[2]
return f"Level {xp.calculate_level(entry):,} | {entry:,} XP"

View File

@ -0,0 +1,26 @@
from dataclasses import dataclass, field
from data.menus.paginated import Paginated
from functions import stringFormatters
from functions.database import memes
@dataclass
class MemesList(Paginated):
title: str = field(default="Memes")
def __post_init__(self):
self.data = self.get_data()
def get_data(self) -> list[tuple]:
data = []
meme_list = memes.getAllMemes()
for meme in sorted(meme_list, key=lambda x: x[1]):
name = stringFormatters.title_case(meme[1])
fields = meme[2]
data.append((name, fields,))
return data
def format_entry(self, index: int, value: tuple) -> str:
return f"{value[0]} ({value[1]})"

View File

@ -0,0 +1,67 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Union
import discord
from discord import ApplicationContext
from discord.ext import pages
from discord.ext.commands import Context
@dataclass
class Paginated(ABC):
"""Abstract class to support paginated menus easily"""
ctx: Union[ApplicationContext, Context]
title: str
data: list[tuple] = None
per_page: int = 10
colour: discord.Colour = discord.Colour.blue()
def create_embed(self, description: str) -> discord.Embed:
embed = discord.Embed(colour=self.colour)
embed.set_author(name=self.title)
embed.description = description
return embed
@abstractmethod
def format_entry(self, index: int, value: tuple) -> str:
pass
def create_pages(self, data: list[tuple]) -> list[discord.Embed]:
# Amount of entries added to this page
added = 0
page_list = []
description = ""
for i, v in enumerate(data):
s = self.format_entry(i, v)
description += s + "\n"
added += 1
# Page full, create an embed & change counters
if added == self.per_page:
embed = self.create_embed(description)
description = ""
added = 0
page_list.append(embed)
# Add final embed if necessary
if added != 0:
embed = self.create_embed(description)
page_list.append(embed)
return page_list
def create_paginator(self) -> pages.Paginator:
return pages.Paginator(pages=self.create_pages(self.data), show_disabled=False, disable_on_timeout=True, timeout=30)
async def respond(self, **kwargs) -> discord.Message:
paginator = self.create_paginator()
return await paginator.respond(self.ctx.interaction, **kwargs)
async def send(self, **kwargs) -> discord.Message:
paginator = self.create_paginator()
return await paginator.send(self.ctx, **kwargs)

View File

@ -1,31 +0,0 @@
import discord
from discord.ext import menus
# https://github.com/Rapptz/discord-ext-menus
class Source(menus.ListPageSource):
def __init__(self, data, name, colour=discord.Colour.blue()):
super().__init__(data, per_page=10)
self.name = name
self.colour = colour
async def format_page(self, menu: menus.MenuPages, entries):
offset = menu.current_page * self.per_page
description = ""
for i, v in enumerate(entries, start=offset):
# Check if the person's name has to be highlighted
if v.startswith("**") and v.endswith("**"):
description += "**"
v = v[2:]
description += "{}: {}\n".format(i + 1, v)
embed = discord.Embed(colour=self.colour)
embed.set_author(name=self.name)
embed.description = description
embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages()))
return embed
class Pages(menus.MenuPages):
def __init__(self, source, clear_reactions_after, timeout=30.0):
super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after)

View File

@ -1,28 +0,0 @@
import discord
from discord.ext import menus
# https://github.com/Rapptz/discord-ext-menus
class Source(menus.ListPageSource):
def __init__(self, data):
super().__init__(data, per_page=10)
self.name = "Didier Store"
self.colour = discord.Colour.blue()
async def format_page(self, menu: menus.MenuPages, entries):
offset = menu.current_page * self.per_page
embed = discord.Embed(colour=self.colour)
embed.set_author(name=self.name)
embed.description = "Heb je een idee voor een item? DM DJ STIJN met je idee!"
embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages()))
for i, v in enumerate(entries, start=offset):
embed.add_field(name="#{} - {}".format(v[0], v[1]), value="{:,} Didier Dinks".format(v[2]))
return embed
class Pages(menus.MenuPages):
def __init__(self, source, clear_reactions_after, timeout=30.0):
super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after)

View File

@ -1,12 +1,9 @@
import discord
from dotenv import load_dotenv
from functions.prefixes import get_prefix
from settings import STATUS_MESSAGE, TOKEN
from startup.didier import Didier
if __name__ == "__main__":
load_dotenv(verbose=True)
# Activities
activity = discord.Activity(type=discord.ActivityType.playing, name=STATUS_MESSAGE)
status = discord.Status.online
@ -16,9 +13,4 @@ if __name__ == "__main__":
intents.members = True
client = Didier(command_prefix=get_prefix, case_insensitive=True, intents=intents, activity=activity, status=status)
# Run IPC server if necessary
if client.ipc is not None:
client.ipc.start()
client.run(TOKEN)

View File

@ -1 +1 @@
{"semester": "1", "year": "3", "years": 3, "jpl": 161733, "jpl_day": 24}
{"semester": "2", "year": "3", "years": 3, "jpl": 161733, "jpl_day": 24}

121
files/courses.json 100644
View File

@ -0,0 +1,121 @@
{
"Algoritmen en Datastructuren 2": {
"abbreviations": ["AD2"],
"code": "C003777",
"year": 2
},
"Algoritmen en Datastructuren 3": {
"abbreviations": ["AD3"],
"code": "C003782",
"year": 3
},
"Artificiële Intelligentie": {
"abbreviations": ["AI"],
"code": "C003756",
"year": 3
},
"Automaten, Berekenbaarheid en Complexiteit": {
"abbreviations": ["ABC"],
"code": "C003785",
"year": 3
},
"Besturingssystemen": {
"abbreviations": ["BS"],
"code": "E019010",
"year": 3
},
"Communicatienetwerken": {
"abbreviations": ["Comnet"],
"code": "E008620",
"year": 2
},
"Computationele Biologie": {
"abbreviations": ["Compbio"],
"code": "C003789",
"year": 3
},
"Computerarchitectuur": {
"abbreviations": ["CA", "Comparch"],
"code": "E034110",
"year": 2
},
"Functioneel Programmeren": {
"abbreviations": ["FP", "Funcprog"],
"code": "C003775",
"year": 2
},
"Informatiebeveiliging": {
"abbreviations": ["Infosec"],
"alt": "Information Security",
"code": "E019400",
"year": 3
},
"Inleiding tot Elektrotechniek": {
"abbreviations": [],
"alt": "Elektrotechniek",
"code": "C003806",
"year": 3
},
"Inleiding tot Telecommunicatie": {
"abbreviations": ["Telecom"],
"code": "C003787",
"year": 3
},
"Logisch Programmeren": {
"abbreviations": ["LP", "Logprog", "Prolog"],
"code": "C003783",
"year": 3
},
"Modelleren en Simuleren": {
"abbreviations": ["Modsim"],
"code": "C003786",
"year": 3
},
"Multimedia": {
"abbreviations": ["MM"],
"code": "C002126",
"year": 2
},
"Parallelle Computersystemen": {
"abbreviations": ["Paracomp", "Parallelle", "PCS"],
"alt": "Parallel Computer Systems",
"code": "E034140",
"year": 3
},
"Statistiek en Probabiliteit": {
"abbreviations": ["Stat","Statistiek", "Statprob"],
"code": "C003778",
"year": 2
},
"Software Engineering Lab 1": {
"abbreviations": ["SEL1"],
"code": "C003780",
"year": 2
},
"Software Engineering Lab 2": {
"abbreviations": ["SEL2"],
"code": "C003784",
"year": 3
},
"Systeemprogrammeren": {
"abbreviations": ["Sysprog"],
"code": "C003776",
"year": 2
},
"Webdevelopment": {
"abbreviations": ["Webdev"],
"code": "C003779",
"year": 2
},
"Wetenschappelijk Rekenen": {
"abbreviations": ["Wetrek"],
"code": "C001521",
"year": 2
},
"Wiskundige Modellering in de Ingenieurswetenschappen": {
"abbreviations": ["Wimo"],
"alt": "Wiskundige Modellering",
"code": "C003788",
"year": 3
}
}

View File

@ -44,24 +44,27 @@
"faq": "Stuurt een lijst van alle FAQ's in [Categorie] naar jouw DM's.\nGeef geen categorie op om een lijst van categorieën te krijgen.\nIndien je een of meerdere personen tagt, wordt de lijst naar hun DM's gestuurd in plaats van de jouwe.",
"faq add": "Voegt een vraag & antwoord toe aan FAQ [Categorie].\nIndien je geen vraag & antwoord opgeeft, maakt het een categorie aan.",
"faq quote": "Stuurt een specifieke entry uit de FAQ in het huidige channel in plaats van de hele lijst naar iemand's DM's.",
"fiche": "Stuurt de URL naar de studiefiche voor [Vak]. Veelgebruikte afkortingen van vakken werken ook.\n\nVoorbeeld: Funcprog, FP, InfoSec, SEL2, ...",
"github": "Stuurt de GitHub link van [Persoon] indien je iemand tagt, zoniet een lijst van GitHubs van mensen uit deze Discord.\nIndien je jouw eigen GitHub hier graag bij zou zetten, gebruik je ``GitHub Add`` of kan ke een DM sturen naar DJ STIJN.",
"github add": "Voegt jouw eigen GitHub toe aan de lijst.\nZowel ``github.com/username`` als ``username`` werken.",
"give": "Geef [Persoon] [Aantal] van jouw eigen Didier Dinks.",
"give": "Geeft [Persoon] [Aantal] van jouw eigen Didier Dinks.",
"google": "Geeft de eerste 10 zoekresultaten voor [Query] op Google.",
"hangman": "Raad [Letter]. Indien je geen letter opgeeft, toont het de status van de huidige Hangman game indien er een bezig is.",
"hangman start": "Start een nieuwe Hangman game indien er nog geen bezig is. Indien je geen woord opgeeft, wordt er een willekeurig woord gekozen.\n**Indien je wel een woord opgeeft, werkt dit enkel in DM.**",
"hangman guess": "Probeer het woord te raden.",
"claim": "Claim [Aantal] Didier Dinks uit je profit.\nIndien je geen aantal opgeeft (of \"all\"), claim je alles, inclusief je investering.",
"inspire": "Generate quotes via [InspiroBot](https://inspirobot.me/).",
"inspire": "Genereer quotes via [InspiroBot](https://inspirobot.me/).",
"inventory": "Bekijk de items in jouw inventory.",
"invest": "Investeer [Aantal] Didier Dinks in jouw Didier Bank om rente te vergaren.",
"join": "Laat Didier [Thread] joinen.",
"jpl": "Informatie over de Jupiler Pro League.",
"jpl matches": "Bekijk de wedstrijden die gespeeld worden op [Week]. Default naar de huidige speeldag.",
"jpl table": "De huidige stand van het klassement.",
"jpl update": "Haalt de nieuwe code voor de competitie van dit jaar op.",
"leaderboard": "Bekijk de Top 10 van [Categorie].\nIndien je geen categorie opgeeft krijg je een lijst van categorieën.",
"les": "Bekijk het lessenrooster voor [Dag].\nIndien je geen dag opgeeft, is dit standaard vandaag.\nLes Morgen/Overmorgen werkt ook.",
"lmgtfy": "Stuur iemand een LMGTFY link wanneer ze je een domme vraag stellen in plaats van het zelf op te zoeken.\nQueries met spaties moeten **niet** tussen aanhalingstekens staan.",
"link": "Stuurt de link die overeenkomt met [Naam].",
"lmgtfy": "Stuurt iemand een LMGTFY link wanneer ze je een domme vraag stellen in plaats van het zelf op te zoeken.\nQueries met spaties moeten **niet** tussen aanhalingstekens staan.",
"load": "Laadt [Cog] in.",
"load all": "Laadt alle cogs in.",
"lost": "Bekijk het aantal Didier Dinks die al verloren zijn door gambling.",

3
files/links.json 100644
View File

@ -0,0 +1,3 @@
{
"DERP": "https://github.ugent.be/DERP"
}

View File

@ -0,0 +1,208 @@
{
"semester_start": [14, 2, 2022],
"semester_end": [20, 2, 2022],
"holidays": [
{
"start_date": [18, 3, 2022, 0, 0, 0],
"end_date": [18, 3, 2022, 23, 59, 59]
},
{
"start_date": [1, 4, 2022, 11, 30, 0],
"end_date": [17, 4, 2022, 23, 59, 59]
},
{
"start_date": [18, 4, 2022, 0, 0, 0],
"end_date": [18, 4, 2022, 23, 59, 59]
}
],
"minors": [
{
"name": "Beveiliging en parallele systemen",
"role": 891744461405687808,
"schedule": [
{
"course": "Informatiebeveiliging",
"online_links": {
"opencast": "https://elosp.ugent.be/opencastplanner/live/GCMobile-ocrec79"
},
"slots": [
{
"online": "opencast",
"location": {
"campus": "Ardoyen",
"building": "iGent 126",
"room": "Auditorium 1"
},
"time": {
"day": "maandag",
"start": 1300,
"end": 1430
}
},
{
"online": "opencast",
"location": {
"campus": "Ardoyen",
"building": "iGent 126",
"room": "Auditorium 1"
},
"time": {
"day": "dinsdag",
"start": 830,
"end": 1130
}
}
]
}
]
},
{
"name": "Elektrotechniek en Telecommunicatie",
"role": 891744390035415111,
"schedule": [
{
"course": "Elektrotechniek",
"slots": [
{
"location": {
"campus": "Sterre",
"building": "S9",
"room": "3.2"
},
"time": {
"day": "dinsdag",
"start": 1300,
"end": 1430
}
},
{
"location": {
"campus": "Sterre",
"building": "S8",
"room": "3.1"
},
"time": {
"day": "woensdag",
"start": 1430,
"end": 1730
}
},
{
"location": {
"campus": "Sterre",
"building": "S9",
"room": "3.2"
},
"time": {
"day": "donderdag",
"start": 1130,
"end": 1300
}
}
]
}
]
}
],
"schedule": [
{
"course": "Automaten, Berekenbaarheid en Complexiteit",
"slots": [
{
"location": {
"campus": "Sterre",
"building": "S25",
"room": "Emmy Noether"
},
"time": {
"day": "dinsdag",
"start": 1430,
"end": 1730
}
},
{
"location": {
"campus": "Sterre",
"building": "S9",
"room": "PC 3.1 Konrad Zuse"
},
"time": {
"day": "donderdag",
"start": 1430,
"end": 1730
}
}
]
},
{
"course": "Computationele Biologie",
"slots": [
{
"location": {
"campus": "Sterre",
"building": "S9",
"room": "3.2"
},
"time": {
"day": "woensdag",
"start": 830,
"end": 1130
}
},
{
"location": {
"campus": "Sterre",
"building": "S9",
"room": "PC 3.1 Konrad Zuse"
},
"time": {
"day": "vrijdag",
"start": 830,
"end": 1130
}
}
]
},
{
"course": "Logisch Programmeren",
"online_links": {
"zoom": "https://ufora.ugent.be/d2l/ext/rp/443368/lti/framedlaunch/556e197e-e87b-4c27-be5d-53adc7a41826"
},
"slots": [
{
"online": "zoom",
"time": {
"day": "maandag",
"start": 1430,
"end": 1730
}
},
{
"online": "zoom",
"time": {
"day": "donderdag",
"start": 830,
"end": 1130
}
}
]
},
{
"course": "Software Engineering Lab 2",
"slots": [
{
"location": {
"campus": "Sterre",
"building": "S8",
"room": "3.1"
},
"time": {
"day": "vrijdag",
"start": 1430,
"end": 1730
}
}
]
}
]
}

View File

@ -10,11 +10,13 @@ def generate(meme: Meme, fields):
"""
Main function that takes a Meme as input & generates an image.
"""
fields = list(fields)
# If there's only one field, the user isn't required to use quotes
if meme.fields == 1:
fields = [" ".join(fields)]
fields = _applyMeme(meme, fields)
fields = _apply_meme(meme, fields)
# List of fields to send to the API
boxes = [{"text": ""}, {"text": ""}, {"text": ""}, {"text": ""}]
@ -33,19 +35,22 @@ def generate(meme: Meme, fields):
return {"success": False, "message": "Er is een fout opgetreden."}
# Post meme
reply = _postMeme(meme, boxes)
reply = _post_meme(meme, boxes)
# Adding a message parameter makes the code in the cog a lot cleaner
if not reply["success"]:
reply["success"] = False
reply["message"] = "Error! Controleer of je de juiste syntax hebt gebruikt. Gebruik het commando " \
"\"memes\" voor een lijst aan geaccepteerde meme-namen."
else:
reply["message"] = reply["data"]["url"]
reply["success"] = False
return reply
def _postMeme(meme: Meme, boxes):
def _post_meme(meme: Meme, boxes):
"""
Performs API request to generate the meme
"""
@ -65,7 +70,7 @@ def _postMeme(meme: Meme, boxes):
return memeReply
def _applyMeme(meme: Meme, fields):
def _apply_meme(meme: Meme, fields):
"""
Some memes are in a special format that only requires
a few words to be added, or needs the input to be changed.
@ -74,9 +79,11 @@ def _applyMeme(meme: Meme, fields):
Links certain meme id's to functions that need to be applied first.
"""
memeDict = {
102156234: _mockingSpongebob,
91538330: _xXEverywhere,
252600902: _alwaysHasBeen
102156234: mocking_spongebob,
91538330: _x_x_everywhere,
252600902: _always_has_been,
167754325: _math_is_math,
206493414: _i_used_the_x_to_destroy_the_x
}
# Meme needs no special treatment
@ -86,17 +93,29 @@ def _applyMeme(meme: Meme, fields):
return memeDict[meme.meme_id](fields)
def _mockingSpongebob(fields):
def mocking_spongebob(fields):
return list(map(mock, fields))
def _xXEverywhere(fields):
def _x_x_everywhere(fields):
word = fields[0]
return ["{}".format(word), "{} everywhere".format(word)]
def _alwaysHasBeen(fields):
def _always_has_been(fields):
word = fields[0]
return ["Wait, it's all {}?".format(word), "Always has been"]
return ["Wait, {}?".format(word), "Always has been"]
def _math_is_math(fields):
word = fields[0].upper()
return ["", f"{word} IS {word}!"]
def _i_used_the_x_to_destroy_the_x(fields):
word = fields[0]
return ["", f"I used the {word} to destroy the {word}"]

View File

@ -1,7 +1,7 @@
import traceback
from discord import Interaction
from discord.ext.commands import Context
from dislash import SlashInteraction
def title_case(string):
@ -42,16 +42,15 @@ def format_command_usage(ctx: Context) -> str:
return f"{ctx.author.display_name} in {_format_error_location(ctx)}: {ctx.message.content}"
def format_slash_command_usage(interaction: SlashInteraction) -> str:
def format_slash_command_usage(interaction: Interaction) -> str:
# Create a string with the options used
# TODO look into the format used by the lib because it's terrible
options = " ".join(list(map(
lambda option: f"{option.name}: \"{option.value}\"",
interaction.data.options.values()
lambda o: f"{o['name']}: \"{o['value']}\"",
interaction.data.get("options", [])
)))
command = f"{interaction.slash_command.name} {options or ''}"
return f"{interaction.author.display_name} in {_format_error_location(interaction)}: /{command}"
command = f"{interaction.data['name']} {options or ''}"
return f"{interaction.user.display_name} in {_format_error_location(interaction)}: /{command}"
def get_edu_year(index: int) -> str:

53
functions/utils.py 100644
View File

@ -0,0 +1,53 @@
from typing import Union, Optional
import discord
from discord import ApplicationContext
from discord.ext.commands import Context
from data import constants
def get_display_name(ctx: Union[ApplicationContext, Context], user_id: int) -> str:
author = ctx.author if isinstance(ctx, Context) else ctx.user
# Check if this is a DM, or the user is not in the guild
if ctx.guild is None or ctx.guild.get_member(user_id) is None:
# User is the author, no need to fetch their name
if user_id == author.id:
return author.display_name
# Get member instance from CoC
COC = ctx.bot.get_guild(int(constants.DeZandbak))
member = COC.get_member(user_id)
if member is not None:
return member.display_name
# Try to fetch the user
user = ctx.bot.get_user(user_id)
if user is not None:
return user.name
# User couldn't be found
return f"[? | {user_id}]"
mem = ctx.guild.get_member(user_id)
return mem.display_name
async def reply_to_reference(ctx: Context, content: Optional[str] = None, embed: Optional[discord.Embed] = None, always_mention=False):
"""Reply to a message
In case the message is a reply to another message, try to reply to that one instead and ping the author
otherwise, reply to the message that invoked the command & only mention the author if necessary
"""
# Message is a reply
if ctx.message.reference is not None:
cached = ctx.message.reference.cached_message
# Reference is not cached anymore: fetch it
if cached is None:
# Message is in the same channel, otherwise no way to reply to it
cached = await ctx.channel.fetch_message(ctx.message.reference.message_id)
return await cached.reply(content, embed=embed, mention_author=cached.author != ctx.author)
return await ctx.reply(content, embed=embed, mention_author=always_mention)

View File

@ -1,8 +1,8 @@
# Beta version of Discord.py fork
py-cord==2.0.0b1
python-dotenv==0.14.0
beautifulsoup4==4.9.1
discord.py==1.7.3
git+https://github.com/Rapptz/discord-ext-menus@master
discord-ext-ipc==2.0.0
psycopg2==2.8.5
psycopg2-binary==2.8.5
python-dateutil==2.6.1
@ -13,12 +13,7 @@ tabulate==0.8.7
yarl==1.4.2
feedparser==6.0.2
googletrans==4.0.0rc1
quart==0.15.1
Quart-CORS==0.5.0
attrs~=21.2.0
dacite~=1.6.0
pytest==6.2.4
markdownify==0.9.2
# Experimental package for slash commands & menus
dislash.py==1.4.9

View File

@ -4,7 +4,7 @@ from dotenv import load_dotenv
import os
load_dotenv()
load_dotenv(verbose=True)
def _to_bool(value: str) -> bool:
@ -31,7 +31,6 @@ DB_NAME = os.getenv("DBNAME", "")
# Discord-related
TOKEN = os.getenv("TOKEN", "")
HOST_IPC = _to_bool(os.getenv("HOSTIPC", "false"))
READY_MESSAGE = os.getenv("READYMESSAGE", "I'M READY I'M READY I'M READY I'M READY") # Yes, this is a Spongebob reference
STATUS_MESSAGE = os.getenv("STATUSMESSAGE", "with your Didier Dinks.")

View File

@ -1,8 +1,6 @@
from data.snipe import Snipe
from discord.ext import commands, ipc
from dislash import InteractionClient
from discord.ext import commands
import os
from settings import HOST_IPC, SLASH_TEST_GUILDS
from startup.init_files import check_all
from typing import Dict
@ -11,30 +9,18 @@ class Didier(commands.Bot):
"""
Main Bot class for Didier
"""
# Reference to interactions client
interactions: InteractionClient
# Dict to store the most recent Snipe info per channel
snipe: Dict[int, Snipe] = {}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._host_ipc = HOST_IPC
# IPC Server
# TODO secret key
self.ipc = ipc.Server(self, secret_key="SOME_SECRET_KEY") if self._host_ipc else None
# Cogs that should be loaded before the others
self._preload = ("ipc", "utils", "failedchecks", "events",)
self._preload = ("utils", "failedchecks", "events",)
# Remove default help command
self.remove_command("help")
# Create interactions client
self.interactions = InteractionClient(self, test_guilds=SLASH_TEST_GUILDS)
# Load all extensions
self.init_extensions()
@ -65,9 +51,3 @@ class Didier(commands.Bot):
# Subdirectory
# Also walrus operator hype
self._init_directory(new_path)
async def on_ipc_ready(self):
print("IPC server is ready.")
async def on_ipc_error(self, endpoint, error):
print(endpoint, "raised", error)

View File

@ -0,0 +1,32 @@
import unittest
from data.courses import load_courses, find_course_from_name
class TestCourses(unittest.TestCase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.courses = load_courses()
def test_find_course(self):
self.assertIsNone(find_course_from_name("garbage", self.courses))
self.assertIsNone(find_course_from_name("garbage"))
# Find by name
webdev = find_course_from_name("Webdevelopment", self.courses)
self.assertIsNotNone(webdev)
self.assertEqual(webdev.code, "C003779")
# Find by abbreviation
infosec = find_course_from_name("infosec", self.courses)
self.assertIsNotNone(infosec)
self.assertEqual(infosec.code, "E019400")
# Case sensitive
not_found = find_course_from_name("ad3", self.courses, case_insensitive=False)
self.assertIsNone(not_found)
# Find by alt name
pcs = find_course_from_name("parallel computer systems", self.courses)
self.assertIsNotNone(pcs)
self.assertEqual(pcs.code, "E034140")