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__ __pycache__
.env .env
/venv/ /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 import constants
from data.snipe import Snipe, Action, should_snipe from data.snipe import Snipe, Action, should_snipe
import datetime import datetime
import discord import discord
from discord.ext import commands from discord.ext import commands
from dislash.application_commands.errors import InteractionCheckFailure
from functions import checks, easterEggResponses, stringFormatters from functions import checks, easterEggResponses, stringFormatters
from functions.database import stats, muttn, custom_commands, commands as command_stats from functions.database import stats, muttn, custom_commands, commands as command_stats
import pytz import pytz
@ -79,6 +78,12 @@ class Events(commands.Cog):
# Earn XP & Message count # Earn XP & Message count
stats.sentMessage(message) 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() @commands.Cog.listener()
async def on_command(self, ctx): 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 # Don't handle commands that have their own custom error handler
if hasattr(ctx.command, 'on_error'): if hasattr(ctx.command, 'on_error'):
return return
# Someone just mentioned Didier without calling a real command, # Someone just mentioned Didier without calling a real command,
# don't care about this error # don't care about this error
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure, commands.TooManyArguments, commands.ExpectedClosingQuoteError), ): 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.") await ctx.send("Geen message gevonden die overeenkomt met het opgegeven argument.")
elif isinstance(err, (commands.ChannelNotFound, commands.ChannelNotReadable)): elif isinstance(err, (commands.ChannelNotFound, commands.ChannelNotReadable)):
await ctx.send("Geen channel gevonden dat overeenkomt met het opgegeven argument.") 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 # Someone forgot an argument or passed an invalid argument
elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument, commands.UnexpectedQuoteError)): 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: else:
usage = stringFormatters.format_command_usage(ctx) usage = stringFormatters.format_command_usage(ctx)
await self.sendErrorEmbed(err, "Command", usage) await self.sendErrorEmbed(err, "Command", usage)
@commands.Cog.listener() @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 Function called whenever someone uses a slash command
""" """
if not interaction.is_command():
return
print(stringFormatters.format_slash_command_usage(interaction)) print(stringFormatters.format_slash_command_usage(interaction))
command_stats.invoked(command_stats.InvocationType.SlashCommand) command_stats.invoked(command_stats.InvocationType.SlashCommand)
@commands.Cog.listener() @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 # Debugging Didier shouldn't spam the error logs
if self.client.user.id != int(constants.didierId): if self.client.user.id != int(constants.didierId):
raise err raise err
if isinstance(err, InteractionCheckFailure): if isinstance(err, commands.CheckFailure):
return await interaction.reply("Je hebt geen toegang tot dit commando.", ephemeral=True) 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) await self.sendErrorEmbed(err, "Slash Command", usage)
@commands.Cog.listener() @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 json
import random import random
import requests 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): class Fun(commands.Cog):
@ -98,17 +99,10 @@ class Fun(commands.Cog):
if result is None: if result is None:
return await ctx.send("Deze meme staat niet in de database.") 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) 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 # 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.command(name="Memes")
@commands.check(checks.allowedChannels) @commands.check(checks.allowedChannels)
@ -118,15 +112,7 @@ class Fun(commands.Cog):
Command that shows a list of memes in the database. Command that shows a list of memes in the database.
:param ctx: Discord Context :param ctx: Discord Context
""" """
memeList = memes.getAllMemes() return await MemesList(ctx=ctx).send()
# 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)
@commands.command(name="Pjoke") @commands.command(name="Pjoke")
@help.Category(category=Category.Fun) @help.Category(category=Category.Fun)

View File

@ -1,5 +1,6 @@
from data import constants from data import constants
import discord import discord
from discord.commands import SlashCommand
from discord.ext import commands from discord.ext import commands
from enums.help_categories import categories, getCategory, Category from enums.help_categories import categories, getCategory, Category
import json import json
@ -49,7 +50,7 @@ class HelpCommand(commands.MinimalHelpCommand):
return await self.send_bot_help(self.get_bot_mapping()) return await self.send_bot_help(self.get_bot_mapping())
# Turn dic to lowercase to allow proper name searching # 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: if spl[0].lower() not in all_commands:
return await self.send_error_message(await self.command_not_found(spl[0])) 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 import discord
from discord.ext import commands from discord.ext import commands
from data.menus import leaderboards
from decorators import help
from enums.help_categories import Category from enums.help_categories import Category
from enums.numbers import Numbers from functions import checks
from functions import checks, xp
from functions.database import currency, stats, poke, muttn
import math
import requests
# TODO some sort of general leaderboard because all of them are the same
class Leaderboards(commands.Cog): class Leaderboards(commands.Cog):
def __init__(self, client): def __init__(self, client):
@ -18,7 +14,7 @@ class Leaderboards(commands.Cog):
self.utilsCog = self.client.get_cog("Utils") self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked # Don't allow any commands to work when locked
def cog_check(self, ctx): def cog_check(self, _):
return not self.client.locked return not self.client.locked
@commands.group(name="Leaderboard", aliases=["Lb", "Leaderboards"], case_insensitive=True, usage="[Categorie]*", @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) @leaderboard.command(name="Dinks", aliases=["Cash"], hidden=True)
async def dinks(self, ctx): async def dinks(self, ctx):
entries = currency.getAllRows() lb = leaderboards.DinksLeaderboard(ctx=ctx)
platDinks = currency.getAllPlatDinks() await lb.send()
# 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")
@leaderboard.command(name="Corona", hidden=True) @leaderboard.command(name="Corona", hidden=True)
async def corona(self, ctx): async def corona(self, ctx):
result = requests.get("http://corona.lmao.ninja/v2/countries").json() lb = leaderboards.CoronaLeaderboard(ctx=ctx)
result.sort(key=lambda x: int(x["cases"]), reverse=True) await lb.send()
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())
@leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True) @leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True)
async def bitcoin(self, ctx): async def bitcoin(self, ctx):
users = currency.getAllRows() lb = leaderboards.BitcoinLeaderboard(ctx=ctx)
boardTop = [] await lb.send()
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")
@leaderboard.command(name="Rob", hidden=True) @leaderboard.command(name="Rob", hidden=True)
async def rob(self, ctx): async def rob(self, ctx):
users = list(stats.getAllRows()) lb = leaderboards.RobLeaderboard(ctx=ctx)
boardTop = [] await lb.send()
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")
@leaderboard.command(name="Poke", hidden=True) @leaderboard.command(name="Poke", hidden=True)
async def poke(self, ctx): async def poke(self, ctx):
s = stats.getAllRows() lb = leaderboards.PokeLeaderboard(ctx=ctx)
blacklist = poke.getAllBlacklistedUsers() await lb.send()
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")
@leaderboard.command(name="Xp", aliases=["Level"], hidden=True) @leaderboard.command(name="Xp", aliases=["Level"], hidden=True)
async def xp(self, ctx): async def xp(self, ctx):
s = stats.getAllRows() lb = leaderboards.XPLeaderboard(ctx=ctx)
boardTop = [] await lb.send()
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")
@leaderboard.command(name="Messages", aliases=["Mc", "Mess"], hidden=True) @leaderboard.command(name="Messages", aliases=["Mc", "Mess"], hidden=True)
async def messages(self, ctx): async def messages(self, ctx):
s = stats.getAllRows() lb = leaderboards.MessageLeaderboard(ctx=ctx)
boardTop = [] await lb.send()
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")
@leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True) @leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True)
async def muttn(self, ctx): async def muttn(self, ctx):
users = muttn.getAllRows() lb = leaderboards.MuttnLeaderboard(ctx=ctx)
boardTop = [] await lb.send()
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")
async def callLeaderboard(self, name, ctx): async def callLeaderboard(self, name, ctx):
await [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0](ctx) command = [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0]
await command(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)
def setup(client): def setup(client):

View File

@ -151,7 +151,7 @@ class Oneliners(commands.Cog):
@commands.command(name="Inspire") @commands.command(name="Inspire")
@help.Category(Category.Other) @help.Category(Category.Other)
async def inspire(self, ctx): 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: if image.status_code == 200:
await ctx.send(image.text) await ctx.send(image.text)

View File

@ -1,12 +1,13 @@
import discord
from discord.ext import commands from discord.ext import commands
from data.embeds.snipe import EditSnipe, DeleteSnipe from data.embeds.snipe import EditSnipe, DeleteSnipe
from data.links import get_link_for
from data.menus import custom_commands from data.menus import custom_commands
from data.snipe import Action, Snipe from data.snipe import Action, Snipe
from decorators import help from decorators import help
from enums.help_categories import Category from enums.help_categories import Category
from functions.database.custom_commands import get_all from functions.utils import reply_to_reference
from functions.stringFormatters import capitalize
from startup.didier import Didier from startup.didier import Didier
@ -14,10 +15,22 @@ class Other(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client: Didier = client self.client: Didier = client
# TODO add locked field to Didier instead of client # Don't allow any commands to work when locked
# # Don't allow any commands to work when locked def cog_check(self, _):
# def cog_check(self, ctx): return not self.client.locked
# 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") @commands.command(name="Custom")
@help.Category(category=Category.Didier) @help.Category(category=Category.Didier)
@ -25,10 +38,17 @@ class Other(commands.Cog):
""" """
Get a list of all custom commands Get a list of all custom commands
""" """
all_commands = get_all() await custom_commands.CommandsList(ctx).send()
formatted = list(sorted(map(lambda x: capitalize(x["name"]), all_commands)))
src = custom_commands.CommandsList(formatted) @commands.command(name="Join", usage="[Thread]")
await custom_commands.Pages(source=src, clear_reactions_after=True).start(ctx) @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") @commands.command(name="Snipe")
@help.Category(category=Category.Other) @help.Category(category=Category.Other)

View File

@ -1,4 +1,5 @@
from data import schedule from data import schedule
from data.courses import find_course_from_name
from data.embeds.deadlines import Deadlines from data.embeds.deadlines import Deadlines
from data.embeds.food import Menu from data.embeds.food import Menu
from decorators import help from decorators import help
@ -8,6 +9,7 @@ from enums.help_categories import Category
from functions import config, les from functions import config, les
from functions.stringFormatters import capitalize from functions.stringFormatters import capitalize
from functions.timeFormatters import skip_weekends from functions.timeFormatters import skip_weekends
from functions.utils import reply_to_reference
class School(commands.Cog): class School(commands.Cog):
@ -82,9 +84,28 @@ class School(commands.Cog):
if message.is_system(): if message.is_system():
return await ctx.send("Dus jij wil system messages pinnen?\nMag niet.") 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("") 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"]) @commands.command(name="Deadlines", aliases=["dl"])
@help.Category(category=Category.School) @help.Category(category=Category.School)
async def deadlines(self, ctx): 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 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 data.embeds.urban_dictionary import Definition
from startup.didier import Didier from startup.didier import Didier
@ -9,15 +9,10 @@ class DefineSlash(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client: Didier = client self.client: Didier = client
@slash_command(name="define", @slash_command(name="define", description="Urban Dictionary")
description="Urban Dictionary", async def _define_slash(self, ctx: ApplicationContext, query: Option(str, "Search query", required=True)):
options=[
Option("query", "Search query", OptionType.STRING, required=True)
]
)
async def _define_slash(self, interaction: SlashInteraction, query):
embed = Definition(query).to_embed() embed = Definition(query).to_embed()
await interaction.reply(embed=embed) await ctx.respond(embed=embed)
def setup(client: Didier): def setup(client: Didier):

View File

@ -1,6 +1,6 @@
from discord.ext import commands from discord.ext import commands
from dislash import SlashInteraction, slash_command, Option, OptionType from discord.commands import Option, SlashCommandGroup, ApplicationContext, permissions
from functions import config, checks from functions import config
from functions.football import get_matches, get_table, get_jpl_code from functions.football import get_matches, get_table, get_jpl_code
from startup.didier import Didier from startup.didier import Didier
@ -9,35 +9,29 @@ class FootballSlash(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client: Didier = client self.client: Didier = client
@slash_command(name="jpl", description="Jupiler Pro League commands") _jpl_group = SlashCommandGroup("jpl", "Jupiler Pro League commands")
async def _jpl_group(self, interaction: SlashInteraction):
pass
@_jpl_group.sub_command(name="matches", @_jpl_group.command(name="matches", description="Schema voor een bepaalde speeldag")
description="Schema voor een bepaalde speeldag", async def _jpl_matches_slash(self, ctx: ApplicationContext,
options=[ day: Option(int, name="day", description="Speeldag (default huidige)", required=False, default=None)
Option("day", "Speeldag (default huidige)", OptionType.INTEGER) ):
]
)
async def _jpl_matches_slash(self, interaction: SlashInteraction, day: int = None):
# Default is current day # Default is current day
if day is None: if day is None:
day = int(config.get("jpl_day")) 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") @_jpl_group.command(name="table", description="Huidige rangschikking")
async def _jpl_table_slash(self, interaction: SlashInteraction): async def _jpl_table_slash(self, ctx: ApplicationContext):
await interaction.reply(get_table()) await ctx.response.defer()
await ctx.send_followup(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="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() code = get_jpl_code()
config.config("jpl", code) config.config("jpl", code)
await interaction.reply(f"Done (code: {code})") await ctx.respond(f"Done (code: {code})")
def setup(client: Didier): def setup(client: Didier):

View File

@ -1,28 +1,69 @@
from discord.ext import commands 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.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 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): class FunSlash(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client: Didier = client self.client: Didier = client
@slash_command( @slash_command(name="xkcd", description="Zoek xkcd comics")
name="xkcd", async def _xkcd_slash(self, ctx: ApplicationContext,
description="Zoek xkcd comics", num: Option(int, description="Nummer van de comic (default de comic van vandaag).", required=False, default=None)
options=[ ):
Option( return await ctx.respond(embed=XKCDEmbed(num).create())
"num",
description="Nummer van de comic (default de comic van vandaag).", @slash_command(name="memes", description="Lijst van memegen-memes")
type=OptionType.INTEGER, async def _memes_slash(self, ctx: ApplicationContext):
required=False return await MemesList(ctx=ctx).respond()
)
] @slash_command(name="memegen", description="Genereer memes")
) async def _memegen_slash(self, ctx: ApplicationContext,
async def _xkcd_slash(self, interaction: SlashInteraction, num: int = None): meme: Option(str, description="Naam van de template", required=True, autocomplete=autocomplete_memes),
return await interaction.reply(embed=XKCDEmbed(num).create()) 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): def setup(client: Didier):

View File

@ -1,5 +1,5 @@
from discord.ext import commands 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 functions.scrapers.google import google_search, create_google_embed
from startup.didier import Didier from startup.didier import Didier
@ -8,20 +8,15 @@ class GoogleSlash(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client: Didier = client self.client: Didier = client
@slash_command(name="google", @slash_command(name="google", description="Google search")
description="Google search", async def _google_slash(self, ctx: ApplicationContext, query: Option(str, "Search query")):
options=[
Option("query", "Search query", OptionType.STRING, required=True)
]
)
async def _google_slash(self, interaction: SlashInteraction, query: str):
result = google_search(query) result = google_search(query)
if not result.results: 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) embed = create_google_embed(result)
await interaction.reply(embed=embed) await ctx.respond(embed=embed)
def setup(client: Didier): 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 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 import schedule
from data.courses import load_courses, find_course_from_name
from data.embeds.food import Menu from data.embeds.food import Menu
from data.embeds.deadlines import Deadlines from data.embeds.deadlines import Deadlines
from functions import les, config from functions import les, config
@ -10,66 +11,77 @@ from functions.timeFormatters import skip_weekends
from startup.didier import Didier 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): class SchoolSlash(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client: Didier = client self.client: Didier = client
@slash_command( @slash_command(name="eten", description="Menu in de UGent resto's op een bepaalde dag")
name="eten", async def _food_slash(self, ctx: ApplicationContext,
description="Menu in de UGENT resto's op een bepaalde dag", dag: Option(str, description="Dag", required=False, default=None, autocomplete=day_autocomplete)
options=[ ):
Option(
"dag",
description="Dag",
type=OptionType.STRING
)
]
)
async def _food_slash(self, interaction: SlashInteraction, dag: str = None):
embed = Menu(dag).to_embed() embed = Menu(dag).to_embed()
await interaction.reply(embed=embed) await ctx.respond(embed=embed)
@slash_command(name="deadlines", description="Aanstaande deadlines") @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() embed = Deadlines().to_embed()
await interaction.reply(embed=embed) await ctx.respond(embed=embed)
@slash_command( @slash_command(name="les", description="Lessenrooster voor [Dag] (default vandaag)",)
name="les", async def _schedule_slash(self, ctx: ApplicationContext,
description="Lessenrooster voor [Dag] (default vandaag)", dag: Option(str, description="Dag", required=False, default=None, autocomplete=day_autocomplete)
options=[ ):
Option(
"dag",
description="dag",
type=OptionType.STRING,
required=False
)
]
)
async def _schedule_slash(self, interaction: SlashInteraction, day: str = None):
"""It's late and I really don't want to refactor the original right now""" """It's late and I really don't want to refactor the original right now"""
if day is not None: if dag is not None:
day = day.lower() dag = dag.lower()
date = les.find_target_date(day) date = les.find_target_date(dag)
# Person explicitly requested a weekend-day # Person explicitly requested a weekend-day
if day is not None and day.lower() in ("morgen", "overmorgen") and date.weekday() > 4: if dag is not None and dag.lower() in ("morgen", "overmorgen") and date.weekday() > 4:
return await interaction.reply(f"{capitalize(day)} is het weekend.", ephemeral=True) return await ctx.respond(f"{capitalize(dag)} is het weekend.", ephemeral=True)
date = skip_weekends(date) 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: 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 # DM only shows user's own minor
if interaction.guild is None: if ctx.guild is None:
minor_roles = [*schedule.find_minor(self.client, interaction.author.id)] minor_roles = [*schedule.find_minor(self.client, ctx.interaction.user.id)]
return await interaction.reply(embed=s.create_schedule(minor_roles=minor_roles).to_embed()) 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): def setup(client: Didier):

View File

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

View File

@ -1,5 +1,4 @@
from converters.numbers import Abbreviated from converters.numbers import Abbreviated
from data.menus import storePages
from decorators import help from decorators import help
import discord import discord
from discord.ext import commands from discord.ext import commands
@ -22,11 +21,12 @@ class Store(commands.Cog):
@commands.check(checks.allowedChannels) @commands.check(checks.allowedChannels)
@help.Category(Category.Currency) @help.Category(Category.Currency)
async def store(self, ctx): async def store(self, ctx):
entries = store.getAllItems() pass
await storePages.Pages(source=storePages.Source(entries), clear_reactions_after=True).start(ctx) # entries = store.getAllItems()
# await storePages.Pages(source=storePages.Source(entries), clear_reactions_after=True).start(ctx)
@store.command(name="Buy", aliases=["Get"], hidden=True) @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: if amount is None:
return return
@ -56,7 +56,7 @@ class Store(commands.Cog):
)) ))
@store.command(name="Sell", hidden=True) @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: if amount is None:
return return
await self.sell(ctx, itemid, amount) 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 typing import Union
from discord.ext import menus
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): class CommandsList(Paginated):
def __init__(self, data, colour=discord.Colour.blue()): def __init__(self, ctx: Union[ApplicationContext, Context]):
super().__init__(data, per_page=15) all_commands = get_all()
self.colour = colour 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): def format_entry(self, index: int, value: tuple) -> str:
embed = discord.Embed(colour=self.colour) return value[0]
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)

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 import discord
from dotenv import load_dotenv
from functions.prefixes import get_prefix from functions.prefixes import get_prefix
from settings import STATUS_MESSAGE, TOKEN from settings import STATUS_MESSAGE, TOKEN
from startup.didier import Didier from startup.didier import Didier
if __name__ == "__main__": if __name__ == "__main__":
load_dotenv(verbose=True)
# Activities # Activities
activity = discord.Activity(type=discord.ActivityType.playing, name=STATUS_MESSAGE) activity = discord.Activity(type=discord.ActivityType.playing, name=STATUS_MESSAGE)
status = discord.Status.online status = discord.Status.online
@ -16,9 +13,4 @@ if __name__ == "__main__":
intents.members = True intents.members = True
client = Didier(command_prefix=get_prefix, case_insensitive=True, intents=intents, activity=activity, status=status) 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) 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": "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 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.", "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": "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.", "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.", "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": "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 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.", "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.", "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.", "inventory": "Bekijk de items in jouw inventory.",
"invest": "Investeer [Aantal] Didier Dinks in jouw Didier Bank om rente te vergaren.", "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": "Informatie over de Jupiler Pro League.",
"jpl matches": "Bekijk de wedstrijden die gespeeld worden op [Week]. Default naar de huidige speeldag.", "jpl matches": "Bekijk de wedstrijden die gespeeld worden op [Week]. Default naar de huidige speeldag.",
"jpl table": "De huidige stand van het klassement.", "jpl table": "De huidige stand van het klassement.",
"jpl update": "Haalt de nieuwe code voor de competitie van dit jaar op.", "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.", "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.", "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": "Laadt [Cog] in.",
"load all": "Laadt alle cogs in.", "load all": "Laadt alle cogs in.",
"lost": "Bekijk het aantal Didier Dinks die al verloren zijn door gambling.", "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. 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 there's only one field, the user isn't required to use quotes
if meme.fields == 1: if meme.fields == 1:
fields = [" ".join(fields)] fields = [" ".join(fields)]
fields = _applyMeme(meme, fields) fields = _apply_meme(meme, fields)
# List of fields to send to the API # List of fields to send to the API
boxes = [{"text": ""}, {"text": ""}, {"text": ""}, {"text": ""}] boxes = [{"text": ""}, {"text": ""}, {"text": ""}, {"text": ""}]
@ -33,19 +35,22 @@ def generate(meme: Meme, fields):
return {"success": False, "message": "Er is een fout opgetreden."} return {"success": False, "message": "Er is een fout opgetreden."}
# Post meme # Post meme
reply = _postMeme(meme, boxes) reply = _post_meme(meme, boxes)
# Adding a message parameter makes the code in the cog a lot cleaner # Adding a message parameter makes the code in the cog a lot cleaner
if not reply["success"]: if not reply["success"]:
reply["success"] = False
reply["message"] = "Error! Controleer of je de juiste syntax hebt gebruikt. Gebruik het commando " \ reply["message"] = "Error! Controleer of je de juiste syntax hebt gebruikt. Gebruik het commando " \
"\"memes\" voor een lijst aan geaccepteerde meme-namen." "\"memes\" voor een lijst aan geaccepteerde meme-namen."
else: else:
reply["message"] = reply["data"]["url"] reply["message"] = reply["data"]["url"]
reply["success"] = False
return reply return reply
def _postMeme(meme: Meme, boxes): def _post_meme(meme: Meme, boxes):
""" """
Performs API request to generate the meme Performs API request to generate the meme
""" """
@ -65,7 +70,7 @@ def _postMeme(meme: Meme, boxes):
return memeReply return memeReply
def _applyMeme(meme: Meme, fields): def _apply_meme(meme: Meme, fields):
""" """
Some memes are in a special format that only requires Some memes are in a special format that only requires
a few words to be added, or needs the input to be changed. 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. Links certain meme id's to functions that need to be applied first.
""" """
memeDict = { memeDict = {
102156234: _mockingSpongebob, 102156234: mocking_spongebob,
91538330: _xXEverywhere, 91538330: _x_x_everywhere,
252600902: _alwaysHasBeen 252600902: _always_has_been,
167754325: _math_is_math,
206493414: _i_used_the_x_to_destroy_the_x
} }
# Meme needs no special treatment # Meme needs no special treatment
@ -86,17 +93,29 @@ def _applyMeme(meme: Meme, fields):
return memeDict[meme.meme_id](fields) return memeDict[meme.meme_id](fields)
def _mockingSpongebob(fields): def mocking_spongebob(fields):
return list(map(mock, fields)) return list(map(mock, fields))
def _xXEverywhere(fields): def _x_x_everywhere(fields):
word = fields[0] word = fields[0]
return ["{}".format(word), "{} everywhere".format(word)] return ["{}".format(word), "{} everywhere".format(word)]
def _alwaysHasBeen(fields): def _always_has_been(fields):
word = fields[0] 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 import traceback
from discord import Interaction
from discord.ext.commands import Context from discord.ext.commands import Context
from dislash import SlashInteraction
def title_case(string): 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}" 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 # Create a string with the options used
# TODO look into the format used by the lib because it's terrible
options = " ".join(list(map( options = " ".join(list(map(
lambda option: f"{option.name}: \"{option.value}\"", lambda o: f"{o['name']}: \"{o['value']}\"",
interaction.data.options.values() interaction.data.get("options", [])
))) )))
command = f"{interaction.slash_command.name} {options or ''}" command = f"{interaction.data['name']} {options or ''}"
return f"{interaction.author.display_name} in {_format_error_location(interaction)}: /{command}" return f"{interaction.user.display_name} in {_format_error_location(interaction)}: /{command}"
def get_edu_year(index: int) -> str: 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 python-dotenv==0.14.0
beautifulsoup4==4.9.1 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==2.8.5
psycopg2-binary==2.8.5 psycopg2-binary==2.8.5
python-dateutil==2.6.1 python-dateutil==2.6.1
@ -13,12 +13,7 @@ tabulate==0.8.7
yarl==1.4.2 yarl==1.4.2
feedparser==6.0.2 feedparser==6.0.2
googletrans==4.0.0rc1 googletrans==4.0.0rc1
quart==0.15.1
Quart-CORS==0.5.0
attrs~=21.2.0 attrs~=21.2.0
dacite~=1.6.0 dacite~=1.6.0
pytest==6.2.4 pytest==6.2.4
markdownify==0.9.2 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 import os
load_dotenv() load_dotenv(verbose=True)
def _to_bool(value: str) -> bool: def _to_bool(value: str) -> bool:
@ -31,7 +31,6 @@ DB_NAME = os.getenv("DBNAME", "")
# Discord-related # Discord-related
TOKEN = os.getenv("TOKEN", "") 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 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.") STATUS_MESSAGE = os.getenv("STATUSMESSAGE", "with your Didier Dinks.")

View File

@ -1,8 +1,6 @@
from data.snipe import Snipe from data.snipe import Snipe
from discord.ext import commands, ipc from discord.ext import commands
from dislash import InteractionClient
import os import os
from settings import HOST_IPC, SLASH_TEST_GUILDS
from startup.init_files import check_all from startup.init_files import check_all
from typing import Dict from typing import Dict
@ -11,30 +9,18 @@ class Didier(commands.Bot):
""" """
Main Bot class for Didier Main Bot class for Didier
""" """
# Reference to interactions client
interactions: InteractionClient
# Dict to store the most recent Snipe info per channel # Dict to store the most recent Snipe info per channel
snipe: Dict[int, Snipe] = {} snipe: Dict[int, Snipe] = {}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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 # Cogs that should be loaded before the others
self._preload = ("ipc", "utils", "failedchecks", "events",) self._preload = ("utils", "failedchecks", "events",)
# Remove default help command # Remove default help command
self.remove_command("help") self.remove_command("help")
# Create interactions client
self.interactions = InteractionClient(self, test_guilds=SLASH_TEST_GUILDS)
# Load all extensions # Load all extensions
self.init_extensions() self.init_extensions()
@ -65,9 +51,3 @@ class Didier(commands.Bot):
# Subdirectory # Subdirectory
# Also walrus operator hype # Also walrus operator hype
self._init_directory(new_path) 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")