From f85896620a5f6eadc5d9efa73147466f5616c4b3 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Wed, 23 Jun 2021 18:42:26 +0100 Subject: [PATCH 001/101] fresh start --- Dockerfile | 6 + Procfile | 1 - bot.py | 483 -------------------------------------- courses_by_degree.json | 1 - degrees.json | 97 -------- docker-compose.yml | 13 + embeds.json | 36 --- self_roles.json | 41 ---- tools/courses_scrapper.py | 65 ----- version | 1 - 10 files changed, 19 insertions(+), 725 deletions(-) create mode 100644 Dockerfile delete mode 100644 Procfile delete mode 100644 bot.py delete mode 100644 courses_by_degree.json delete mode 100644 degrees.json create mode 100644 docker-compose.yml delete mode 100644 embeds.json delete mode 100644 self_roles.json delete mode 100644 tools/courses_scrapper.py delete mode 100644 version diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2cabcd3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.9-slim-buster +WORKDIR /app +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt +COPY ./src . +CMD [ "python3", "bot.py"] diff --git a/Procfile b/Procfile deleted file mode 100644 index 00a0ef3..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -worker: python bot.py \ No newline at end of file diff --git a/bot.py b/bot.py deleted file mode 100644 index fe4928d..0000000 --- a/bot.py +++ /dev/null @@ -1,483 +0,0 @@ -from discord import Embed, PermissionOverwrite, Intents, DiscordException -from discord.ext import commands -from discord.utils import get -import os -import json -import asyncio -import time -from asyncio import sleep - -# Carregar versão do bot -with open('version', 'r') as file: - version_number = file.read().replace('\n', '') - print("Version {}".format(version_number)) - -# Informação dos cursos -# "display": conteúdo da mensagem utilizada no curso -# "name": nome da role do curso (tem de estar presente no "display") -# "tagus": é um curso do Tagus Park? -# "msg_id": indice da mensagem que foi enviada -with open('degrees.json', 'r', encoding='utf-8') as file: - degrees = json.load(file) - -# O formato de embed pode ser gerado com ferramentas online e umas pequenas alterações. -# De momento o parsing deste json apenas suporta: -# - title -# - description -# - color -# - fields com name e value (a propriedade inline é ignorada) -# Caso se adicionem embeds mais complexos no json será necessário alterar parse_embed -with open('embeds.json', 'r', encoding='utf-8') as file: - embeds = json.load(file) - -with open("self_roles.json", "r", encoding="utf-8") as file: - self_roles = json.load(file) - -with open('courses_by_degree.json', 'r', encoding='utf-8') as file: - courses_by_degree = json.load(file) - -intents = Intents.default() -intents.typing = False -intents.presences = True -intents.members = True -bot = commands.Bot(command_prefix='$', intents=intents) - -# embed: key do embed no embed.json a que se pretende aceder -def parse_embed(embed): - if embed not in embeds: - print('Warning: the key {} isn\'t in embed.json'.format(embed)) - return parse_embed('error') - - ret = Embed( - title=embeds[embed]['title'], - description=embeds[embed]['description'], - color=embeds[embed]['color'] - ) - - for field in embeds[embed]['fields']: - ret.add_field( - value=field['value'].replace('$veterano', roles["veterano"].mention).replace( - '$turista', roles["turista"].mention), - name=field['name'], - inline=False - ) - - ret.set_thumbnail( - url='https://upload.wikimedia.org/wikipedia/pt/e/ed/IST_Logo.png') - - return ret - -async def rebuild_self_roles(): - global channels - await channels["self-roles"].purge() - - for group in self_roles["groups"]: - await channels["self-roles"].send(self_roles["groups"][group]["msg"]) - for role in self_roles["groups"][group]["roles"]: - msg = await channels["self-roles"].send(self_roles["roles"][role]) - await msg.add_reaction('☑️') - self_role_msg_ids[role] = msg.id - - -async def rebuild_role_pickers(): - global roles - global channels - await channels["escolhe-o-teu-curso"].purge() - - await channels["escolhe-o-teu-curso"].send(embed=parse_embed('welcome-pt')) - await channels["escolhe-o-teu-curso"].send(embed=parse_embed('welcome-en')) - - for i in range(0, len(degrees)): - msg = await channels["escolhe-o-teu-curso"].send("`{}`".format(degrees[i]["display"])) - await msg.add_reaction('☑️') - degrees[i]["msg_id"] = msg.id - - - -# Events - -@bot.event -async def on_ready(): - print('Bot iniciado com o utilizador {0.user}'.format(bot)) - - - if len(bot.guilds) != 1: - print('O bot tem de estar em exatamente um guild, no entanto está em {} guilds'.format( - len(bot.guilds))) - exit(-1) - - global guild - guild = bot.guilds[0] - - if len(guild.text_channels) < 2: - print('O guild tem de ter pelo menos dois canais de texto') - exit(-1) - - global self_roles - - global roles - - global self_roles - global self_role_ids - global self_role_msg_ids - - global channels - global categories - global courses_category - roles, channels, categories, self_role_ids, self_role_msg_ids = {}, {}, {}, {}, {} - - channels["escolhe-o-teu-curso"] = get(guild.text_channels, name="escolhe-o-teu-curso") - - channels["entradas"] = get(guild.text_channels, name="entradas") - - channels["self-roles"] = get(guild.text_channels, name="self-roles") - channels["no-context"] = get(guild.text_channels, name="no-context") - courses_category = get(guild.categories, name="Cadeiras") - - roles["turista"] = get(guild.roles, name="TurISTa") - roles["aluno"] = get(guild.roles, name="Aluno/a") - roles["veterano"] = get(guild.roles, name="Veterano/a") - roles["tagus"] = get(guild.roles, name="Tagus Park") - roles["alameda"] = get(guild.roles, name="Alameda") - roles["admin"] = get(guild.roles, name="Admin") - roles["admin_plus"] = get(guild.roles, name="Admin+") - - - will_exit = False - for channel in channels: - if channels[channel] is None: - - print(f"O guild tem de ter um channel '#{channel}'") - will_exit = True - - for role in roles: - if roles[role] is None: - print(f"O guild tem de ter uma role '{role}'") - will_exit = True - - #Self roles - for self_role in self_roles["roles"]: - self_role_ids[self_role] = get(guild.roles, name=self_role) - if self_role_ids[self_role] is None: - print(f"O guild tem de ter uma self_role '{self_role}'") - will_exit = True - - if courses_category is None: - print('O guild tem de ter uma categoria "Cadeiras".') - will_exit = True - - - # Associar cada curso a uma role - for i in range(0, len(degrees)): - degrees[i]["role"] = None - for role in guild.roles: - if role.name == degrees[i]["name"]: - degrees[i]["role"] = role - break - if degrees[i]["role"] is None: - print("A role com o nome {} nao existe".format(degrees[i]["name"])) - will_exit = True - - if will_exit: - exit(-1) - - # Verificar se as mensagens de entrada já estão no canal certo - found_count = 0 - roles_messages = await channels["escolhe-o-teu-curso"].history().flatten() - for msg in roles_messages: - for degree in degrees: - if degree["display"] in msg.content and msg.author.bot: - degree["msg_id"] = msg.id - found_count += 1 - break - - - - # Se não estiverem todas, apaga todas as mensagens do canal e escreve de novo - if found_count != len(degrees): - await rebuild_role_pickers() - - #Verificar se as mensagens do #self-roles já existem - self_roles_messages = await channels["self-roles"].history().flatten() - self_roles_found_count = 0 - for self_role in self_roles["roles"]: - self_role_msg = self_roles["roles"][self_role] - for msg in self_roles_messages: - if self_role_msg == msg.content: - self_role_msg_ids[self_role] = msg.id - self_roles_found_count +=1 - if self_roles_found_count != len(self_roles["roles"]): - await rebuild_self_roles() - -#Only allow media messages on #no-context -@bot.event -async def on_message(msg): - global channels - if channels["no-context"] != msg.channel or msg.author.bot: - await bot.process_commands(msg) - return - if not((msg.attachments or "https://" in msg.content) or roles["admin"] in msg.author.roles ): - bot_msg = await msg.channel.send(f"{msg.author.mention} não podes enviar mensagens sem imagens/links aqui.\n Este canal é para meter screenshots do que os vossos colegas dizem no discord e que provavelmente não quereriam quoted sem contexto.") - await msg.delete() - await sleep(60) - await bot_msg.delete() - return - await bot.process_commands(msg) - return - -#Temporary command to clean the channel -@bot.command(pass_context=True) -async def clean_no_context(ctx): - if not roles["admin_plus"] in ctx.author.roles: - await handle_no_permissions(ctx) - return - await ctx.channel.send("A limpar o histórico do #no-context...") - global channels - msgs = await channels["no-context"].history(limit=None).flatten() - for msg in msgs: - if not(msg.attachments or "https://" in msg.content): - await msg.delete() - await ctx.channel.send(f"{ctx.author.mention} Feito.") - -@bot.event -async def on_member_join(member): - await channels["entradas"].send( - 'Bem vind@ {}! Vai ao canal {} para escolheres o teu curso e a {} para escolheres outras roles.'.format( - member.mention, channels["escolhe-o-teu-curso"].mention, channels["self-roles"].mention)) - -@bot.event -async def on_raw_reaction_add(payload): - member = guild.get_member(payload.user_id) - if member.bot: - return - - - allowed_channel_ids = [channels["escolhe-o-teu-curso"].id,channels["self-roles"].id] - if payload.channel_id not in allowed_channel_ids or payload.emoji.name != '☑️': - return - - - for degree in degrees: - if degree["msg_id"] == payload.message_id: - # Verificar se o membro já tem qualquer outra role de curso - for degree_2 in degrees: - if degree == degree_2: - continue - if degree_2["role"] in member.roles: - msg = await channels["escolhe-o-teu-curso"].fetch_message(payload.message_id) - await msg.remove_reaction('☑️', member) - return - print("Role do curso {} adicionada ao membro {}".format(degree["name"], member)) - await member.remove_roles(roles["turista"]) - await member.add_roles(degree["role"], roles["aluno"]) - if degree["tagus"]: - await member.add_roles(roles["tagus"]) - else: - await member.add_roles(roles["alameda"]) - - for role in self_role_msg_ids: - if payload.message_id == self_role_msg_ids[role]: - await member.add_roles(self_role_ids[role]) -@bot.event -async def on_raw_reaction_remove(payload): - member = guild.get_member(payload.user_id) - - if member.bot: - return - - allowed_channel_ids = [channels["escolhe-o-teu-curso"].id,channels["self-roles"].id] - - if payload.channel_id not in allowed_channel_ids or payload.emoji.name != '☑️': - return - - - for degree in degrees: - if degree["msg_id"] == payload.message_id: - if degree["role"] in member.roles: - if degree["tagus"]: - await member.remove_roles(roles["tagus"]) - else: - await member.remove_roles(roles["alameda"]) - await member.remove_roles(degree["role"], roles["aluno"]) - await member.add_roles(roles["turista"]) - print("Role do curso {} removida do membro {}".format(degree["name"], member)) - return - #Check for self roles - for role in self_role_msg_ids: - if payload.message_id == self_role_msg_ids[role]: - await member.remove_roles(self_role_ids[role]) - -def get_channel_name(name): - name = name.lower() - name = name.replace(" ", "-") - #Non-exhaustive list of disallowed characters in text channel names, might be missing a few - disallowed_characters = "|\\!\"#$%&/()=?'" - for char in disallowed_characters: - if char in name: - name = name.replace(char, "") - name = name.replace("---","-").replace("--","-") - name += "-vc" - return name - -@bot.event -async def on_voice_state_update(user,vc_before,vc_after): - global guild - #remove permissions from previous channel first - if vc_before.channel != None: - #Skip non join/leave/switch vc channel events - if vc_before.channel == vc_after.channel: - return - - vc_txt_before = get_channel_name(vc_before.channel.name) - - channel = get(vc_before.channel.category.text_channels, name=vc_txt_before) - #Txt Channel might not exist the first few times - if channel != None: - if len(vc_before.channel.members) == 0: - await channel.delete() - else: - await channel.set_permissions(user, read_messages=False) - - if vc_after.channel != None: - vc_txt_after = get_channel_name(vc_after.channel.name) - channel = get(vc_after.channel.category.text_channels, name=vc_txt_after) - if channel == None: - overwrites = { - guild.default_role: PermissionOverwrite(read_messages=False), - user: PermissionOverwrite(read_messages=True) - } - channel = await vc_after.channel.category.create_text_channel( - name=vc_txt_after, overwrites=overwrites) - else: - await channel.set_permissions(user, read_messages=True) - - -async def handle_no_permissions(ctx): - msg = await ctx.channel.send(f'{ctx.author.mention} is not in the sudoers file. This incident will be reported.') - await sleep(60) - await ctx.message.delete() - await msg.delete() - - -# Comandos -@bot.command(pass_context=True) -async def reset_admin(ctx): - if not roles["admin"] in ctx.author.roles: - await handle_no_permissions(ctx) - return - for member in guild.members: - if roles["admin_plus"] in member.roles: - await member.remove_roles(roles["admin_plus"]) - await ctx.message.add_reaction('✅') - await sleep(60) - await ctx.message.delete() - -@bot.command(pass_context=True) -async def version(ctx): - msg = await ctx.message.channel.send("{}".format(version_number)) - await sleep(60) - await msg.delete() - await ctx.message.delete() - -@bot.command(pass_context=True) -async def sudo(ctx): - if not roles["admin"] in ctx.author.roles: - await handle_no_permissions(ctx) - return - await ctx.message.add_reaction('✅') - if roles["admin_plus"] not in ctx.author.roles: - await ctx.author.add_roles(roles["admin_plus"]) - else: - await ctx.author.remove_roles(roles["admin_plus"]) - - await sleep(15) - await ctx.message.delete() - -@bot.command(pass_context=True) -async def refresh(ctx): - if not roles["admin"] in ctx.author.roles: - await handle_no_permissions(ctx) - return - await ctx.message.add_reaction('✅') - await rebuild_role_pickers() - await rebuild_self_roles() - msg = await ctx.message.channel.send(f'{ctx.author.mention} Feito') - - await sleep(60) - await msg.delete() - await ctx.message.delete() - -async def count_msgs(ctx, channel): - global leaderboard - try: - msg_count = 0 - async for msg in channel.history(limit=None): - #Filtrar mensagens com caracteres ZWSP - if not "\u200b" in msg.content: - msg_count += 1 - if msg.author.id in leaderboard: - leaderboard[msg.author.id] += len(msg.content) - else: - leaderboard[msg.author.id] = len(msg.content) - except DiscordException as e: - print(f"Exception while doing leaderboard: {e}") - pass - finally: - await ctx.message.channel.send(f"Canal {channel.name} lido, {msg_count} mensagens") - - -@bot.command(pass_context=True) -async def make_leaderboard(ctx): - global leaderboard - if not roles["admin"] in ctx.author.roles: - await handle_no_permissions(ctx) - return - await ctx.message.add_reaction('✅') - # Este comando cria uma leaderboard com os utilizadores que mais falaram no servidor. - visible_user_count = 50 - leaderboard = {} - - start_time = time.time() - - await asyncio.gather(*[count_msgs(ctx, channel) for channel in guild.text_channels]) - leaderboard_msg = "```" - - for user_id in sorted(leaderboard, key=leaderboard.get, reverse=True)[:visible_user_count]: - leaderboard_msg += '{} - {}\n'.format(guild.get_member(user_id), leaderboard[user_id]) - if len(leaderboard_msg) > 500: - leaderboard_msg += "```" - await ctx.message.channel.send('{}'.format(leaderboard_msg)) - leaderboard_msg = "```" - leaderboard_msg += "```" - if len(leaderboard_msg) > 6: - await ctx.message.channel.send('{}'.format(leaderboard_msg)) - - duration = time.time() - start_time - await ctx.channel.send(f"{ctx.author.mention} Concluído em {duration} segundos") - - -@bot.command(pass_context=True) -async def rebuild_course_channels(ctx): - if not roles["admin"] in ctx.author.roles: - await handle_no_permissions(ctx) - return - await ctx.message.add_reaction('✅') - for course in courses_by_degree: - permissions = { - guild.default_role: PermissionOverwrite(read_messages=False) - } - for degree in courses_by_degree[course]['degrees']: - degree_obj = next( - (item for item in degrees if item["name"] == degree), None) - if degree_obj is not None: - permissions[degree_obj["role"]] = PermissionOverwrite( - read_messages=True) - - course_channel = get(courses_category.text_channels, - name=course.lower()) - if course_channel is None: - await courses_category.create_text_channel(course.lower(), overwrites=permissions, topic=courses_by_degree[course]['name']) - else: - await course_channel.edit(overwrites=permissions, topic=courses_by_degree[course]['name']) - -bot.run(os.environ['DISCORD_TOKEN']) diff --git a/courses_by_degree.json b/courses_by_degree.json deleted file mode 100644 index 9edab80..0000000 --- a/courses_by_degree.json +++ /dev/null @@ -1 +0,0 @@ -{"acom": {"degrees": ["LERC", "LEE", "MEEC"], "name": "Arquitectura de Computadores"}, "al": {"degrees": ["LMAC", "LERC", "LEAN", "LEGM", "LEE", "LEGI", "LEIC-A", "LEIC-T", "MEBiom", "MEC", "MEEC", "MEAer", "MEBiol", "MEAmbi", "MEQ", "MEM", "MEMec", "MEFT"], "name": "Álgebra Linear"}, "bbm": {"degrees": ["MEBiol", "MEAmbi", "MEQ"], "name": "Bioquímica e Biologia Molecular"}, "be": {"degrees": ["MEBiom"], "name": "Bio-Electricidade"}, "ccdi": {"degrees": ["LMAC"], "name": "Complementos de Cálculo Diferencial e Integral"}, "cdi-i": {"degrees": ["LMAC", "LERC", "LEAN", "LEGM", "LEE", "LEGI", "LEIC-A", "LEIC-T", "MEBiom", "MEC", "MEEC", "MEAer", "MEBiol", "MEAmbi", "MEQ", "MEM", "MEMec", "MEFT"], "name": "Cálculo Diferencial e Integral I"}, "cdi-ii": {"degrees": ["LMAC", "LERC", "LEAN", "LEGM", "LEE", "LEGI", "LEIC-A", "LEIC-T", "MEBiom", "MEC", "MEEC", "MEAer", "MEBiol", "MEAmbi", "MEQ", "MEM", "MEMec", "MEFT"], "name": "Cálculo Diferencial e Integral II"}, "cmat": {"degrees": ["LEAN", "LEGI", "MEAer", "MEMec"], "name": "Ciência de Materiais"}, "da-ii": {"degrees": ["MA"], "name": "Desenho Arquitectónico II"}, "dac": {"degrees": ["MEC"], "name": "Desenho Assistido por Computador"}, "dase": {"degrees": ["MEC"], "name": "Desafios Ambientais e da Sustentabilidade em Engenharia"}, "dcn": {"degrees": ["LEAN"], "name": "Desenho de Construção Naval"}, "des": {"degrees": ["LEGM"], "name": "Desenho"}, "dmg": {"degrees": ["LEE", "LEGI"], "name": "Desenho e Modelação Geométrica"}, "dmg-ii": {"degrees": ["MEMec"], "name": "Desenho e Modelação Geométrica II"}, "eoeg": {"degrees": ["LEGM"], "name": "Expressão Oral e Escrita-Geológica"}, "est": {"degrees": ["LEGM", "MA"], "name": "Estática"}, "gambi": {"degrees": ["MEAmbi"], "name": "Geologia Ambiental"}, "geol": {"degrees": ["LEGM"], "name": "Geologia"}, "ges": {"degrees": ["LEAN", "MEEC", "MEAer", "MEMec"], "name": "Gestão"}, "hacm": {"degrees": ["MA"], "name": "História da Arquitectura Clássica e Medieval"}, "iaed": {"degrees": ["LERC", "LEIC-A", "LEIC-T"], "name": "Introdução aos Algoritmos e Estruturas de Dados"}, "ialg": {"degrees": ["LMAC"], "name": "Introdução à Álgebra"}, "iem": {"degrees": ["MEM"], "name": "Introdução à Engenharia de Materiais"}, "ieti": {"degrees": ["LERC"], "name": "Introdução à Engenharia de Telecomunicações e Informática"}, "io": {"degrees": ["LMAC"], "name": "Introdução à Optimização"}, "lem": {"degrees": ["MEM"], "name": "Laboratórios de Engenharia de Materiais"}, "lp": {"degrees": ["LEIC-A", "LEIC-T"], "name": "Lógica para Programação"}, "lq-ii": {"degrees": ["MEBiol", "MEQ"], "name": "Laboratórios de Química II"}, "m-ii": {"degrees": ["MA"], "name": "Matemática II"}, "md": {"degrees": ["LEIC-A", "LEIC-T"], "name": "Matemática Discreta"}, "mec-i": {"degrees": ["MEC"], "name": "Mecânica I"}, "mgeo": {"degrees": ["MEC"], "name": "Mineralogia e Geologia"}, "micr": {"degrees": ["LEGI"], "name": "Microeconomia"}, "mo": {"degrees": ["LMAC", "LERC", "LEAN", "LEGM", "LEE", "LEGI", "MEBiom", "MEEC", "MEAer", "MEBiol", "MEAmbi", "MEQ", "MEM", "MEMec"], "name": "Mecânica e Ondas"}, "o": {"degrees": ["MEFT"], "name": "Laboratório de Oficinas"}, "oond": {"degrees": ["MEFT"], "name": "Oscilações e Ondas"}, "ppes": {"degrees": ["MEMec"], "name": "Portfólio Pessoal"}, "pro": {"degrees": ["LEE", "MEEC"], "name": "Programação"}, "q-ii": {"degrees": ["MEBiol", "MEQ"], "name": "Química II"}, "qo": {"degrees": ["MEAmbi", "MEM"], "name": "Química Orgânica"}, "qo-i": {"degrees": ["MEBiol", "MEQ"], "name": "Química Orgânica I"}, "qui": {"degrees": ["MEBiom", "MEFT"], "name": "Química"}, "sd": {"degrees": ["MEAer", "MEFT"], "name": "Sistemas Digitais"}, "tcfe": {"degrees": ["MEBiom"], "name": "Teoria dos Circuitos e Fundamentos de Electrónica"}, "tem": {"degrees": ["MEC"], "name": "Termodinâmica e Estrutura da Matéria"}} \ No newline at end of file diff --git a/degrees.json b/degrees.json deleted file mode 100644 index 658f655..0000000 --- a/degrees.json +++ /dev/null @@ -1,97 +0,0 @@ -[ - { - "display": "(MA) Arquitetura", - "name": "MA", - "tagus": false - }, - { - "display": "(MEAer) Engenharia Aeroespacial", - "name": "MEAer", - "tagus": false - }, - { - "display": "(MEBiol) Engenharia Biológica", - "name": "MEBiol", - "tagus": false - }, - { - "display": "(MEBiom) Engenharia Biomédica", - "name": "MEBiom", - "tagus": false - }, - { - "display": "(MEC) Engenharia Civil", - "name": "MEC", - "tagus": false - }, - { - "display": "(MEEC) Engenharia Eletrotécnica e de Computadores", - "name": "MEEC", - "tagus": false - }, - { - "display": "(LEE) Engenharia Eletrónica", - "name": "LEE", - "tagus": true - }, - { - "display": "(MEFT) Engenharia Física Tecnológica", - "name": "MEFT", - "tagus": false - }, - { - "display": "(LEGM) Engenharia Geológica e de Minas", - "name": "LEGM", - "tagus": false - }, - { - "display": "(LEIC-A) Engenharia Informática e de Computadores", - "name": "LEIC-A", - "tagus": false - }, - { - "display": "(LEIC-T) Engenharia Informática e de Computadores", - "name": "LEIC-T", - "tagus": true - }, - { - "display": "(MEMec) Engenharia Mecânica", - "name": "MEMec", - "tagus": false - }, - { - "display": "(LENO) Engenharia Naval e Oceânica", - "name": "LENO", - "tagus": false - }, - { - "display": "(MEQ) Engenharia Química", - "name": "MEQ", - "tagus": false - }, - { - "display": "(MEM) Engenharia de Materiais", - "name": "MEM", - "tagus": false - }, - { - "display": "(LETI) Engenharia de Telecomunicações e Informática", - "name": "LETI", - "tagus": true - }, - { - "display": "(MEAmbi) Engenharia do Ambiente", - "name": "MEAmbi", - "tagus": false - }, - { - "display": "(LEGI) Engenharia e Gestão Industrial", - "name": "LEGI", - "tagus": true - }, - { - "display": "(LMAC) Matemática Aplicada e Computação", - "name": "LMAC", - "tagus": false - } -] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7471816 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + ist-discord-bot: + #image: ist-bot-team/ist-discord-bot:v2.0.0 + build: . + volumes: + - type: bind + source: ./data + target: /app/data + environment: + DISCORD_TOKEN: abc + restart: unless-stopped diff --git a/embeds.json b/embeds.json deleted file mode 100644 index 3cefa99..0000000 --- a/embeds.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "error": { - "title": "Error", - "description": "This shouldn't happen, if you see this please ping a @Mod" - }, - "welcome-pt": { - "title": "Bem vind@", - "description": "Esta é uma comunidade de Caloiros (e Veteranos) do IST 2020, onde podes conhecer os teus colegas, partilhar apontamentos e conviver!", - "color": 7506394, - "fields": [ - { - "name": "Escolhe um curso", - "value": "Reage à mensagem neste canal com o nome do curso que frequentas no IST.\nSe não estudares no IST és convidado a permanecer neste servidor como 'turista' (nesse caso não precisas de escolher um curso)." - }, - { - "name": "Não sou caloiro", - "value": "Este servidor não é exclusivo para caloiros, por isso também tens lugar aqui! Reage à ultima mensagem para ficares com a role de veterano." - } - ] - }, - "welcome-en": { - "title": "Welcome", - "description": "This is an IST 2020's community for Freshmen (and Veterans) where you'll get to know your colleagues, share notes and hang out!", - "color": 7658098, - "fields": [ - { - "name": "Pick a degree", - "value": "React to the message in this channel with the name of the degree you're on in IST.\nIf you don't study on IST you are invited to stay on this server as a 'tourist' (in that case you don't need to pick a degree)" - }, - { - "name": "I'm not a Freshman", - "value": "This server is not exclusive to freshmen, so you still have a place here! React to the last message to get the veteran role." - } - ] - } -} diff --git a/self_roles.json b/self_roles.json deleted file mode 100644 index 627176c..0000000 --- a/self_roles.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "groups": { - "geral": { - "msg": "Roles Gerais", - "roles": [ - "Veterano/a", - "TurISTa" - ] - }, - "eventos": { - "msg": "Eventos do Servidor", - "roles": [ - "Hora do Porco" - ] - }, - "leic-a": { - "msg": "Roles de LEIC-A", - "roles": [ - "LEIC-A CDI-I", - "LEIC-A CDI-II", - "LEIC-A IAED", - "LEIC-A LP", - "LEIC-A MD", - "LEIC-A Eventos", - "Resumos de LEIC" - ] - } - }, - "roles": { - "Veterano/a": "`Não é o meu primeiro ano no IST`", - "TurISTa": "`Não sou do IST`", - "Hora do Porco": "`Hora do Porco`:pig:", - "LEIC-A CDI-I": "`Receber pings de anúncios de CDI-I`", - "LEIC-A CDI-II": "`Receber pings de anúncios de CDI-II`", - "LEIC-A IAED": "`Receber pings de anúncios de IAED`", - "LEIC-A LP": "`Receber pings de anúncios de LP`", - "LEIC-A MD": "`Receber pings de anúncios de MD`", - "Resumos de LEIC": "`Receber pings sobre resumos de LEIC-A`", - "LEIC-A Eventos": "`Receber pings sobre eventos de LEIC-A`" - } -} \ No newline at end of file diff --git a/tools/courses_scrapper.py b/tools/courses_scrapper.py deleted file mode 100644 index 6989ebd..0000000 --- a/tools/courses_scrapper.py +++ /dev/null @@ -1,65 +0,0 @@ -import requests -import re -import json - -# Intervalo [since,until[ de pares (Ano,Semestre) para dar scrapping -# Provavelmente nada robusto mas há que ter fé -since = [1, 2] -until = [2, 1] - - -def scrape_degree(degree): - html = requests.get( - 'https://fenix.tecnico.ulisboa.pt/cursos/{}/curriculo'.format( - degree)).text - start = html.find('

Ano {}, Semestre {}

'.format(*since)) - end = html.find('

Ano {}, Semestre {}

'.format(*until)) - courses = re.findall(r'>(.*)\s<\/a>', html[start:end]) - return courses - - -course_acronym_map = {} - - -def add_all_courses_from_degree(degree_id): - global course_acronym_map - courses = requests.get( - 'https://fenix.tecnico.ulisboa.pt/api/fenix/v1/degrees/{}/courses?academicTerm=2020/2021' - .format(degree_id)).json() - for course in courses: - if course['name'] not in course_acronym_map: - acronym = re.match(".*?([A-Za-z]+)\d*", - course['acronym']).groups()[0].lower() - trailingRomanNumeral = re.search(" (III|II|I|IV|V|VIII|VII|VI|)$", - course['name']) - if trailingRomanNumeral: - acronym += '-{}'.format( - trailingRomanNumeral.groups()[0].lower()) - course_acronym_map[course['name']] = acronym - - -all_degrees = requests.get( - 'https://fenix.tecnico.ulisboa.pt/api/fenix/v1/degrees?academicTerm=2020/2021' -).json() -degrees = list( - filter( - lambda degree: degree['type'] in - ['Licenciatura Bolonha', 'Mestrado Integrado'], all_degrees)) - -degree_courses = {} -for degree in degrees: - add_all_courses_from_degree(degree['id']) - - courses = scrape_degree(degree['acronym'].lower()) - - for course in courses: - acronym = course_acronym_map[course] - if acronym not in degree_courses: - degree_courses[acronym] = {'name': course, 'degrees': []} - degree_courses[acronym]['degrees'].append(degree['acronym']) - -with open('courses_by_degree.json', 'wb') as file: - file.write( - json.dumps(degree_courses, ensure_ascii=False, - sort_keys=True).encode('utf-8')) - file.close() \ No newline at end of file diff --git a/version b/version deleted file mode 100644 index 920a139..0000000 --- a/version +++ /dev/null @@ -1 +0,0 @@ -43 From 49b57b828e47f73271d3feb17c01d87299a02b81 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Wed, 30 Jun 2021 00:05:35 +0100 Subject: [PATCH 002/101] move from python to node with yarn --- .gitignore | 3 ++ Dockerfile | 9 ++--- package.json | 11 ++++++ yarn.lock | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 package.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index 4c49bd7..01e0337 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ +node_modules/ +.vscode .env + diff --git a/Dockerfile b/Dockerfile index 2cabcd3..359ca68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ -FROM python:3.9-slim-buster +FROM node:lts-alpine3.13 WORKDIR /app -COPY requirements.txt requirements.txt -RUN pip3 install -r requirements.txt +COPY package.json . +COPY yarn.lock . +RUN yarn COPY ./src . -CMD [ "python3", "bot.py"] +CMD [ "yarn", "start"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..c58921c --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "ist-discord-bot", + "version": "2.0.0", + "main": "index.js", + "repository": "git@github.com:ist-bot-team/ist-discord-bot", + "author": "IST Bot Team", + "license": "MIT", + "dependencies": { + "discord.js": "^12.5.3" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..04fddf9 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,97 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@discordjs/collection@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.1.6.tgz#9e9a7637f4e4e0688fd8b2b5c63133c91607682c" + integrity sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ== + +"@discordjs/form-data@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@discordjs/form-data/-/form-data-3.0.1.tgz#5c9e6be992e2e57d0dfa0e39979a850225fb4697" + integrity sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +discord.js@^12.5.3: + version "12.5.3" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-12.5.3.tgz#56820d473c24320871df9ea0bbc6b462f21cf85c" + integrity sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw== + dependencies: + "@discordjs/collection" "^0.1.6" + "@discordjs/form-data" "^3.0.1" + abort-controller "^3.0.0" + node-fetch "^2.6.1" + prism-media "^1.2.9" + setimmediate "^1.0.5" + tweetnacl "^1.0.3" + ws "^7.4.4" + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +mime-db@1.48.0: + version "1.48.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== + +mime-types@^2.1.12: + version "2.1.31" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" + integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== + dependencies: + mime-db "1.48.0" + +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + +prism-media@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.3.1.tgz#418acd2b122bedea2e834056d678f9a5ad2943ae" + integrity sha512-nyYAa3KB4qteJIqdguKmwxTJgy55xxUtkJ3uRnOvO5jO+frci+9zpRXw6QZVcfDeva3S654fU9+26P2OSTzjHw== + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +ws@^7.4.4: + version "7.5.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.1.tgz#44fc000d87edb1d9c53e51fbc69a0ac1f6871d66" + integrity sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow== From 0d8855e6379ec6ed0f6c443deb88aad532596f81 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Wed, 30 Jun 2021 16:58:01 +0100 Subject: [PATCH 003/101] initial setup --- .eslintrc.json | 18 + .gitignore | 4 +- .husky/.gitignore | 1 + .husky/pre-commit | 4 + .prettierignore | 2 + .prettierrc.json | 4 + Dockerfile | 2 +- LICENSE | 2 +- README.md | 2 +- docker-compose.yml | 22 +- package.json | 47 +- requirements.txt | 2 - src/bot.ts | 3 + tsconfig.json | 14 + yarn.lock | 1345 ++++++++++++++++++++++++++++++++++++++++++++ 15 files changed, 1446 insertions(+), 26 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .husky/.gitignore create mode 100755 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc.json delete mode 100644 requirements.txt create mode 100644 src/bot.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..cea703e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "env": { + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": {} +} diff --git a/.gitignore b/.gitignore index 01e0337..464e87c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ -.vscode +dist/ +data/ .env +.vscode diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..8dc2e72 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "useTabs": true +} diff --git a/Dockerfile b/Dockerfile index 359ca68..29df99d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,5 +3,5 @@ WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn -COPY ./src . +COPY ./dist ./dist CMD [ "yarn", "start"] diff --git a/LICENSE b/LICENSE index 351a667..d62651a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 IST Bot Team +Copyright (c) 2021 IST Bot Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9ed6b86..5c32293 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# ist-discord-bot \ No newline at end of file +# ist-discord-bot diff --git a/docker-compose.yml b/docker-compose.yml index 7471816..f18d914 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,13 @@ -version: '3.8' +version: "3.8" services: - ist-discord-bot: - #image: ist-bot-team/ist-discord-bot:v2.0.0 - build: . - volumes: - - type: bind - source: ./data - target: /app/data - environment: - DISCORD_TOKEN: abc - restart: unless-stopped + ist-discord-bot: + #image: ist-bot-team/ist-discord-bot:v2.0.0 + build: . + volumes: + - type: bind + source: ./data + target: /app/data + environment: + DISCORD_TOKEN: abc + restart: unless-stopped diff --git a/package.json b/package.json index c58921c..8a9c795 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,40 @@ { - "name": "ist-discord-bot", - "version": "2.0.0", - "main": "index.js", - "repository": "git@github.com:ist-bot-team/ist-discord-bot", - "author": "IST Bot Team", - "license": "MIT", - "dependencies": { - "discord.js": "^12.5.3" - } + "name": "ist-discord-bot", + "version": "2.0.0", + "description": "Discord bot to manage the IST Hub server.", + "keywords": [ + "discord", + "bot", + "ist-hub", + "ist", + "tecnico" + ], + "homepage": "https://discord.leic.pt", + "main": "dist/bot.js", + "repository": "git@github.com:ist-bot-team/ist-discord-bot", + "author": "IST Bot Team", + "license": "MIT", + "dependencies": { + "discord.js": "^12.5.3" + }, + "devDependencies": { + "@tsconfig/node14": "^1.0.1", + "@types/node": "^15.12.5", + "@typescript-eslint/eslint-plugin": "^4.28.1", + "@typescript-eslint/parser": "^4.28.1", + "eslint": "^7.29.0", + "eslint-config-prettier": "^8.3.0", + "husky": "^6.0.0", + "lint-staged": "^11.0.0", + "prettier": "^2.3.2", + "typescript": "^4.3.4" + }, + "scripts": { + "prepare": "husky install", + "build": "tsc", + "start": "node ." + }, + "lint-staged": { + "**/*": "prettier --write --ignore-unknown" + } } diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 330141b..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -discord.py==1.7.3 -asyncio==3.4.3 diff --git a/src/bot.ts b/src/bot.ts new file mode 100644 index 0000000..4ac8761 --- /dev/null +++ b/src/bot.ts @@ -0,0 +1,3 @@ +console.log("Hi!"); + +while (true) {} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..68f764b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@tsconfig/node14/tsconfig.json", + + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "removeComments": true, + "sourceMap": true, + "strict": true + }, + + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/yarn.lock b/yarn.lock index 04fddf9..960ba0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,34 @@ # yarn lockfile v1 +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/code-frame@^7.0.0": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" + integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== + dependencies: + "@babel/highlight" "^7.14.5" + +"@babel/helper-validator-identifier@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" + integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== + +"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" + integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@discordjs/collection@^0.1.6": version "0.1.6" resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.1.6.tgz#9e9a7637f4e4e0688fd8b2b5c63133c91607682c" @@ -16,6 +44,131 @@ combined-stream "^1.0.8" mime-types "^2.1.12" +"@eslint/eslintrc@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.2.tgz#f63d0ef06f5c0c57d76c4ab5f63d3835c51b0179" + integrity sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz#94c23db18ee4653e129abd26fb06f870ac9e1ee2" + integrity sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@tsconfig/node14@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@types/json-schema@^7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + +"@types/node@^15.12.5": + version "15.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.5.tgz#9a78318a45d75c9523d2396131bd3cca54b2d185" + integrity sha512-se3yX7UHv5Bscf8f1ERKvQOD6sTyycH3hdaoozvaLxgUiY5lIGEeH37AD0G0Qi9kPqihPn0HOfd2yaIEN9VwEg== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@typescript-eslint/eslint-plugin@^4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz#c045e440196ae45464e08e20c38aff5c3a825947" + integrity sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ== + dependencies: + "@typescript-eslint/experimental-utils" "4.28.1" + "@typescript-eslint/scope-manager" "4.28.1" + debug "^4.3.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.1.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz#3869489dcca3c18523c46018b8996e15948dbadc" + integrity sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q== + dependencies: + "@types/json-schema" "^7.0.7" + "@typescript-eslint/scope-manager" "4.28.1" + "@typescript-eslint/types" "4.28.1" + "@typescript-eslint/typescript-estree" "4.28.1" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/parser@^4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.1.tgz#5181b81658414f47291452c15bf6cd44a32f85bd" + integrity sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg== + dependencies: + "@typescript-eslint/scope-manager" "4.28.1" + "@typescript-eslint/types" "4.28.1" + "@typescript-eslint/typescript-estree" "4.28.1" + debug "^4.3.1" + +"@typescript-eslint/scope-manager@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz#fd3c20627cdc12933f6d98b386940d8d0ce8a991" + integrity sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA== + dependencies: + "@typescript-eslint/types" "4.28.1" + "@typescript-eslint/visitor-keys" "4.28.1" + +"@typescript-eslint/types@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.1.tgz#d0f2ecbef3684634db357b9bbfc97b94b828f83f" + integrity sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg== + +"@typescript-eslint/typescript-estree@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz#af882ae41740d1f268e38b4d0fad21e7e8d86a81" + integrity sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ== + dependencies: + "@typescript-eslint/types" "4.28.1" + "@typescript-eslint/visitor-keys" "4.28.1" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/visitor-keys@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz#162a515ee255f18a6068edc26df793cdc1ec9157" + integrity sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg== + dependencies: + "@typescript-eslint/types" "4.28.1" + eslint-visitor-keys "^2.0.0" + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -23,11 +176,188 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +acorn-jsx@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + +acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.6.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.0.tgz#60cc45d9c46a477d80d92c48076d972c342e5720" + integrity sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -35,11 +365,65 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + +deep-is@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + discord.js@^12.5.3: version "12.5.3" resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-12.5.3.tgz#56820d473c24320871df9ea0bbc6b462f21cf85c" @@ -54,11 +438,556 @@ discord.js@^12.5.3: tweetnacl "^1.0.3" ws "^7.4.4" +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enquirer@^2.3.5, enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" + integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint@^7.29.0: + version "7.29.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0" + integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA== + dependencies: + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.1.2" + globals "^13.6.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.9" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1: + version "3.2.6" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.6.tgz#434dd9529845176ea049acc9343e8282765c6e1a" + integrity sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fastq@^1.6.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.6.0, globals@^13.9.0: + version "13.9.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb" + integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.3: + version "11.0.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +husky@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" + integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +lint-staged@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" + integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + dedent "^0.7.0" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + mime-db@1.48.0: version "1.48.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" @@ -71,27 +1000,443 @@ mime-types@^2.1.12: dependencies: mime-db "1.48.0" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" + integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== + prism-media@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.3.1.tgz#418acd2b122bedea2e834056d678f9a5ad2943ae" integrity sha512-nyYAa3KB4qteJIqdguKmwxTJgy55xxUtkJ3uRnOvO5jO+frci+9zpRXw6QZVcfDeva3S654fU9+26P2OSTzjHw== +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +regexpp@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + +semver@^7.2.1, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +table@^6.0.9: + version "6.7.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" + integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== + dependencies: + ajv "^8.0.1" + lodash.clonedeep "^4.5.0" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tslib@^1.8.1, tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + tweetnacl@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typescript@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc" + integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + ws@^7.4.4: version "7.5.1" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.1.tgz#44fc000d87edb1d9c53e51fbc69a0ac1f6871d66" integrity sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== From bcee4e2fe262f2a24397621a05b15b1be6890cc6 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Wed, 30 Jun 2021 22:00:26 +0100 Subject: [PATCH 004/101] remove docker-compose.yml from version control --- .gitignore | 2 +- README.md | 36 +++++++++++++++++++++++++++++++++++- docker-compose.yml | 13 ------------- 3 files changed, 36 insertions(+), 15 deletions(-) delete mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index 464e87c..2d92b43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ node_modules/ dist/ data/ -.env +docker-compose.yml .vscode diff --git a/README.md b/README.md index 5c32293..797b1b4 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ -# ist-discord-bot +# IST Discord Bot + +Discord bot to manage the IST Hub server -- join [here](https://discord.leic.pt). + +### Running + +1. Create a `docker-compose.yml` file as below: + +```yaml +version: "3.8" + +services: + ist-discord-bot: + image: ist-bot-team/ist-discord-bot:v2.0.0 + volumes: + - type: bind + source: ./data + target: /app/data + environment: + DISCORD_TOKEN: PLACE_BOT_TOKEN_HERE + restart: unless-stopped +``` + +2. Run `docker-compose up -d --build` +3. That's it! + +_You can also use `docker-compose down`, `docker-compose up`, `docker-compose restart` and `docker-compose logs`._ + +### Adding to a Server + +Replacing `CLIENT_ID` with the application's public ID, access the following link: + +``` +https://discord.com/oauth2/authorize?client_id=CLIENT_ID&scope=bot+applications.commands&permissions=8 +``` diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index f18d914..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: "3.8" - -services: - ist-discord-bot: - #image: ist-bot-team/ist-discord-bot:v2.0.0 - build: . - volumes: - - type: bind - source: ./data - target: /app/data - environment: - DISCORD_TOKEN: abc - restart: unless-stopped From 2efcc8d661269c37e1bb4faf053b05ea9ddb12df Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sun, 8 Aug 2021 11:34:56 +0100 Subject: [PATCH 005/101] upgrade do discord.js v13 --- package.json | 2 +- yarn.lock | 152 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 8a9c795..c6436be 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "author": "IST Bot Team", "license": "MIT", "dependencies": { - "discord.js": "^12.5.3" + "discord.js": "^13.0.1" }, "devDependencies": { "@tsconfig/node14": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index 960ba0f..315936c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,10 +30,21 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@discordjs/collection@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.1.6.tgz#9e9a7637f4e4e0688fd8b2b5c63133c91607682c" - integrity sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ== +"@discordjs/builders@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-0.4.0.tgz#bb41573ce4824aa9194a53b52c29c5219b610010" + integrity sha512-EiwLltKph6TSaPJIzJYdzNc1PnA2ZNaaE0t0ODg3ghnpVHqfgd0YX9/srsleYHW2cw1sfIq+kbM+h0etf7GWLA== + dependencies: + "@sindresorhus/is" "^4.0.1" + discord-api-types "^0.22.0" + ow "^0.27.0" + ts-mixer "^6.0.0" + tslib "^2.3.0" + +"@discordjs/collection@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.2.1.tgz#ea4bc7b41b7b7b6daa82e439141222ec95c469b2" + integrity sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog== "@discordjs/form-data@^3.0.1": version "3.0.1" @@ -80,6 +91,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@sapphire/async-queue@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.1.4.tgz#ae431310917a8880961cebe8e59df6ffa40f2957" + integrity sha512-fFrlF/uWpGOX5djw5Mu2Hnnrunao75WGey0sP0J3jnhmrJ5TAPzHYOmytD5iN/+pMxS+f+u/gezqHa9tPhRHEA== + +"@sindresorhus/is@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.1.tgz#d26729db850fa327b7cacc5522252194404226f5" + integrity sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g== + "@tsconfig/node14@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" @@ -90,6 +111,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== +"@types/node@*": + version "16.4.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.13.tgz#7dfd9c14661edc65cccd43a29eb454174642370d" + integrity sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg== + "@types/node@^15.12.5": version "15.12.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.5.tgz#9a78318a45d75c9523d2396131bd3cca54b2d185" @@ -100,6 +126,13 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/ws@^7.4.7": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^4.28.1": version "4.28.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz#c045e440196ae45464e08e20c38aff5c3a825947" @@ -169,13 +202,6 @@ "@typescript-eslint/types" "4.28.1" eslint-visitor-keys "^2.0.0" -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" @@ -287,7 +313,7 @@ braces@^3.0.1: dependencies: fill-range "^7.0.1" -callsites@^3.0.0: +callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== @@ -424,19 +450,24 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -discord.js@^12.5.3: - version "12.5.3" - resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-12.5.3.tgz#56820d473c24320871df9ea0bbc6b462f21cf85c" - integrity sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw== +discord-api-types@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.22.0.tgz#34dc57fe8e016e5eaac5e393646cd42a7e1ccc2a" + integrity sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg== + +discord.js@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.0.1.tgz#58f009706d1d7587fe9ff6c6c781e4540ae085f9" + integrity sha512-pEODCFfxypBnGEYpSgjkn1jt70raCS1um7Zp0AXEfW1DcR29wISzQ/WeWdnjP5KTXGi0LTtkRiUjOsMgSoukxA== dependencies: - "@discordjs/collection" "^0.1.6" + "@discordjs/builders" "^0.4.0" + "@discordjs/collection" "^0.2.1" "@discordjs/form-data" "^3.0.1" - abort-controller "^3.0.0" + "@sapphire/async-queue" "^1.1.4" + "@types/ws" "^7.4.7" + discord-api-types "^0.22.0" node-fetch "^2.6.1" - prism-media "^1.2.9" - setimmediate "^1.0.5" - tweetnacl "^1.0.3" - ws "^7.4.4" + ws "^7.5.1" doctrine@^3.0.0: version "3.0.0" @@ -445,6 +476,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -599,11 +637,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -830,6 +863,11 @@ is-obj@^1.0.1: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" @@ -935,6 +973,11 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -1065,6 +1108,18 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ow@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/ow/-/ow-0.27.0.tgz#d44da088e8184fa11de64b5813206f9f86ab68d0" + integrity sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ== + dependencies: + "@sindresorhus/is" "^4.0.1" + callsites "^3.1.0" + dot-prop "^6.0.1" + lodash.isequal "^4.5.0" + type-fest "^1.2.1" + vali-date "^1.0.0" + p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -1126,11 +1181,6 @@ prettier@^2.3.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== -prism-media@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.3.1.tgz#418acd2b122bedea2e834056d678f9a5ad2943ae" - integrity sha512-nyYAa3KB4qteJIqdguKmwxTJgy55xxUtkJ3uRnOvO5jO+frci+9zpRXw6QZVcfDeva3S654fU9+26P2OSTzjHw== - progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -1207,11 +1257,6 @@ semver@^7.2.1, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1340,11 +1385,21 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +ts-mixer@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.0.tgz#4e631d3a36e3fa9521b973b132e8353bc7267f9f" + integrity sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ== + tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -1352,11 +1407,6 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tweetnacl@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" - integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -1374,6 +1424,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + typescript@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc" @@ -1391,6 +1446,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +vali-date@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6" + integrity sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY= + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -1426,10 +1486,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^7.4.4: - version "7.5.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.1.tgz#44fc000d87edb1d9c53e51fbc69a0ac1f6871d66" - integrity sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow== +ws@^7.5.1: + version "7.5.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" + integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== yallist@^4.0.0: version "4.0.0" From 82411361f7b0ca4ecb1562cb021fad0da745cc4a Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sun, 8 Aug 2021 11:35:30 +0100 Subject: [PATCH 006/101] upgrade node to version compatible with discord.js@v13 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 29df99d..dfa340a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts-alpine3.13 +FROM node:16.6.1-alpine3.14 WORKDIR /app COPY package.json . COPY yarn.lock . From 58761494cc192539415d350306c764b039ddce6d Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sun, 8 Aug 2021 19:42:57 +0100 Subject: [PATCH 007/101] basic skeleton --- src/bot.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 4ac8761..4919191 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,3 +1,13 @@ -console.log("Hi!"); +import { Client, Intents } from "discord.js"; -while (true) {} +const { DISCORD_TOKEN } = process.env; + +const client = new Client({ + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], +}); + +client.once("ready", () => { + console.log("Ready!"); +}); + +client.login(DISCORD_TOKEN); From 7ff45bf3af2a14e20b18181c4521c310a930457a Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 12:14:43 +0100 Subject: [PATCH 008/101] ignore unused vars starting with _ --- .eslintrc.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index cea703e..00449c2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,5 +14,10 @@ "sourceType": "module" }, "plugins": ["@typescript-eslint"], - "rules": {} + "rules": { + "@typescript-eslint/no-unused-vars": [ + "error", + { "varsIgnorePattern": "^_" } + ] + } } From eb91842e130170a28407e80f5e9b793280e24438 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 12:14:55 +0100 Subject: [PATCH 009/101] useless maintenance mode --- src/bot.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/bot.ts b/src/bot.ts index 4919191..535f420 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,4 @@ -import { Client, Intents } from "discord.js"; +import { Client, Intents, TextChannel } from "discord.js"; const { DISCORD_TOKEN } = process.env; @@ -6,8 +6,38 @@ const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], }); +let maintenanceStatus: { enabled: boolean; reason?: string }; +const maintenanceCommands = {}; + client.once("ready", () => { console.log("Ready!"); + + maintenanceStatus = { enabled: true, reason: "i said so" }; //updateMaintenanceStatus(); + + if (maintenanceStatus.enabled) { + client.user?.setPresence({ + status: "idle", + activities: [{ name: "🔧 Maintenance Mode" }], + }); + for (const [_id, guild] of client.guilds.cache) { + const channel = + guild.systemChannel ?? + (guild.channels.cache + .filter((c) => c.isText()) + .first() as TextChannel); + channel?.send( + `Activating **MAINTENANCE MODE** because \`${ + maintenanceStatus.reason + }\`. + - Slash commands are not available; + - Prefix is \`#\`; + - Bot owners and users with a role called (exactly) \`Admin\` may interact; + - Commands: ${Object.keys(maintenanceCommands) + .map((k) => "`" + k + "`") + .join(", ")}` + ); + } + } }); client.login(DISCORD_TOKEN); From 852346c8b3bd139e00438fa1cb6cdfbce25807cc Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 12:37:05 +0100 Subject: [PATCH 010/101] add codeowners --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..8ca1c02 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @RafDevX \ No newline at end of file From 6b360dd42f7e97aafa44d3a9510ed7b9fb84eb1d Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sat, 14 Aug 2021 12:46:01 +0100 Subject: [PATCH 011/101] Add attendance poll module (#50) --- README.md | 1 + package.json | 4 +- src/bot.ts | 82 ++++++++++++---------- src/modules/attendance.d.ts | 13 ++++ src/modules/attendance.ts | 136 ++++++++++++++++++++++++++++++++++++ src/storage.d.ts | 5 ++ src/storage.ts | 25 +++++++ yarn.lock | 31 ++++++++ 8 files changed, 260 insertions(+), 37 deletions(-) create mode 100644 src/modules/attendance.d.ts create mode 100644 src/modules/attendance.ts create mode 100644 src/storage.d.ts create mode 100644 src/storage.ts diff --git a/README.md b/README.md index 797b1b4..73d5b36 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ services: target: /app/data environment: DISCORD_TOKEN: PLACE_BOT_TOKEN_HERE + TZ: Europe/Lisbon # default timezone for crontab and other date related stuff restart: unless-stopped ``` diff --git a/package.json b/package.json index c6436be..7bc65b0 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "author": "IST Bot Team", "license": "MIT", "dependencies": { - "discord.js": "^13.0.1" + "discord.js": "^13.0.1", + "node-cron": "^3.0.0" }, "devDependencies": { "@tsconfig/node14": "^1.0.1", "@types/node": "^15.12.5", + "@types/node-cron": "^2.0.4", "@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/parser": "^4.28.1", "eslint": "^7.29.0", diff --git a/src/bot.ts b/src/bot.ts index 535f420..a2a1fb5 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,43 +1,53 @@ -import { Client, Intents, TextChannel } from "discord.js"; +import { ScheduledAttendancePoll } from "./modules/attendance.d"; +import * as attendance from "./modules/attendance"; +import * as Discord from "discord.js"; +import * as storage from "./storage"; -const { DISCORD_TOKEN } = process.env; +const client = new Discord.Client({ + intents: [ + Discord.Intents.FLAGS.GUILDS, + Discord.Intents.FLAGS.GUILD_MESSAGES, + ], +}); + +const buttonHandlers: Record< + string, + (interaction: Discord.ButtonInteraction) => Promise +> = { + attendance: attendance.handleAttendanceButton, +}; + +client.on("ready", async () => { + await attendance.scheduleAttendancePolls( + client, + storage + .getAttendancePolls() + .filter((poll) => poll.type === "scheduled") + .map((poll) => poll as ScheduledAttendancePoll) + ); -const client = new Client({ - intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + console.log(`Logged in as ${client.user?.tag}!`); }); -let maintenanceStatus: { enabled: boolean; reason?: string }; -const maintenanceCommands = {}; - -client.once("ready", () => { - console.log("Ready!"); - - maintenanceStatus = { enabled: true, reason: "i said so" }; //updateMaintenanceStatus(); - - if (maintenanceStatus.enabled) { - client.user?.setPresence({ - status: "idle", - activities: [{ name: "🔧 Maintenance Mode" }], - }); - for (const [_id, guild] of client.guilds.cache) { - const channel = - guild.systemChannel ?? - (guild.channels.cache - .filter((c) => c.isText()) - .first() as TextChannel); - channel?.send( - `Activating **MAINTENANCE MODE** because \`${ - maintenanceStatus.reason - }\`. - - Slash commands are not available; - - Prefix is \`#\`; - - Bot owners and users with a role called (exactly) \`Admin\` may interact; - - Commands: ${Object.keys(maintenanceCommands) - .map((k) => "`" + k + "`") - .join(", ")}` - ); - } +client.on("interactionCreate", async (interaction: Discord.Interaction) => { + if (interaction.isButton()) { + const msgCompInteraction = interaction as Discord.ButtonInteraction; + const prefix = msgCompInteraction.customId.split(":")[0]; + + await buttonHandlers[prefix]?.(msgCompInteraction); } }); -client.login(DISCORD_TOKEN); +const loadBot = async (): Promise => { + await storage.loadStorage(); + + client.login(process.env.DISCORD_TOKEN); +}; + +if (process.env.DISCORD_TOKEN) { + loadBot(); +} else { + console.error( + "Discord token not set. Please set the DISCORD_TOKEN environment variable" + ); +} diff --git a/src/modules/attendance.d.ts b/src/modules/attendance.d.ts new file mode 100644 index 0000000..a4e4f8d --- /dev/null +++ b/src/modules/attendance.d.ts @@ -0,0 +1,13 @@ +export interface AttendancePoll { + id: string; // identifier used to keep track of embed on pinned messages + type: AttendanceType; + title: string; +} + +export interface ScheduledAttendancePoll extends AttendancePoll { + type: "scheduled"; + cron: string; // cron schedule + channelId: string; // channel to post the poll +} + +export type AttendanceType = "single" | "scheduled"; diff --git a/src/modules/attendance.ts b/src/modules/attendance.ts new file mode 100644 index 0000000..7bc3bd5 --- /dev/null +++ b/src/modules/attendance.ts @@ -0,0 +1,136 @@ +import { AttendancePoll, ScheduledAttendancePoll } from "./attendance.d"; +import { + ButtonInteraction, + Client, + Message, + MessageButton, + MessageEmbed, + Snowflake, + TextChannel, +} from "discord.js"; +import * as cron from "node-cron"; + +const ATTENDANCE_POLL_MSG = "Attendance Poll"; +const ATTENDANCE_POLL_BUTTONS = [ + new MessageButton() + .setLabel("Yes") + .setStyle("SUCCESS") + .setCustomId("attendance:yes"), + new MessageButton() + .setLabel("No") + .setStyle("DANGER") + .setCustomId("attendance:no"), + new MessageButton() + .setLabel("Clear") + .setStyle("SECONDARY") + .setCustomId("attendance:clear"), +]; +const ATTENDANCE_NO_ONE = "*No one*"; + +export const handleAttendanceButton = async ( + interaction: ButtonInteraction +): Promise => { + const action = interaction.customId.split(":")[1]; + let fieldIndex = -1; + + const oldEmbeds = interaction.message.embeds; + + if (action === "yes") { + fieldIndex = 0; + } else if (action === "no") { + fieldIndex = 1; + } + + const newEmbed = getNewEmbed( + oldEmbeds[0] as MessageEmbed, + fieldIndex, + interaction.user.id + ); + + (interaction.message as Message).edit({ + content: ATTENDANCE_POLL_MSG, + embeds: [newEmbed], + components: [ATTENDANCE_POLL_BUTTONS], + }); + + await interaction.reply({ content: "Response recorded!", ephemeral: true }); +}; + +export const getNewEmbed = ( + oldEmbed: MessageEmbed, + fieldIndex: number, + userId: Snowflake +): MessageEmbed => { + oldEmbed.fields?.map((field, i) => { + field.value = + field.value + .split("\n") + .filter( + (user) => + user !== ATTENDANCE_NO_ONE && user !== `<@${userId}>` + ) + .join("\n") || (fieldIndex === i ? "" : ATTENDANCE_NO_ONE); + }); + if (oldEmbed.fields[fieldIndex]) + oldEmbed.fields[ + fieldIndex + ].value = `${oldEmbed.fields[fieldIndex]?.value}\n<@${userId}>`; + + return oldEmbed; +}; + +export const sendAttendanceEmbed = async ( + embed: AttendancePoll, + channel: TextChannel +): Promise => { + const pinnedMessages = await channel.messages.fetchPinned(); + await Promise.all( + pinnedMessages.map((msg) => { + if ( + msg.embeds?.some( + (msgEmbed) => msgEmbed.footer?.text === embed.id + ) + ) + return msg.unpin(); + }) + ); + + const message = await channel.send({ + content: ATTENDANCE_POLL_MSG, + embeds: [ + new MessageEmbed() + .setTitle(embed.title) + .addField("Attending", ATTENDANCE_NO_ONE, true) + .addField("Not Attending", ATTENDANCE_NO_ONE, true) + .setFooter(embed.id) + .setTimestamp(), + ], + components: [ATTENDANCE_POLL_BUTTONS], + }); + + await message.pin(); +}; + +export const scheduleAttendancePolls = async ( + client: Client, + polls: ScheduledAttendancePoll[] +): Promise => { + await Promise.all( + polls.map(async (poll) => { + const channel = await client.channels.fetch( + poll.channelId as Snowflake + ); + + if (!channel) { + console.error( + `Couldn't fetch channel ${poll.channelId} for attendance poll ${poll.id}` + ); + return; + } + + cron.schedule(poll.cron, () => + sendAttendanceEmbed(poll, channel as TextChannel) + ); + }) + ); +}; diff --git a/src/storage.d.ts b/src/storage.d.ts new file mode 100644 index 0000000..8b4afd7 --- /dev/null +++ b/src/storage.d.ts @@ -0,0 +1,5 @@ +import { AttendancePoll } from "./modules/attendance.d"; + +export interface Storage { + attendance: AttendancePoll[]; +} diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..3113748 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,25 @@ +import { AttendancePoll } from "./modules/attendance.d"; +import { promises as fs } from "fs"; +import * as path from "path"; +import { Storage } from "./storage.d"; + +const dataFolder = path.resolve("./data"); +const attendanceFile = path.join(dataFolder, "attendance.json"); + +const storage: Storage = { + attendance: [], +}; + +export const loadStorage = async (): Promise => { + try { + storage.attendance = JSON.parse( + await fs.readFile(attendanceFile, "utf-8") + ); + } catch (e) { + console.log( + `Couldn't load attendance.json from data folder, ignoring it.` + ); + } +}; + +export const getAttendancePolls = (): AttendancePoll[] => storage.attendance; diff --git a/yarn.lock b/yarn.lock index 315936c..e57ad75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -111,6 +111,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== +"@types/node-cron@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-2.0.4.tgz#6d467440762e7d3539890d477b33670c020c458f" + integrity sha512-vXzgDRWCZpuut5wJVZtluEnkNhzGojYlyMch2c4kMj7H74L8xTLytVlgQzj+/17wfcjs49aJDFBDglFSGt7GeA== + dependencies: + "@types/tz-offset" "*" + "@types/node@*": version "16.4.13" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.13.tgz#7dfd9c14661edc65cccd43a29eb454174642370d" @@ -126,6 +133,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/tz-offset@*": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@types/tz-offset/-/tz-offset-0.0.0.tgz#d58f1cebd794148d245420f8f0660305d320e565" + integrity sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ== + "@types/ws@^7.4.7": version "7.4.7" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" @@ -1055,6 +1067,18 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +moment-timezone@^0.5.31: + version "0.5.33" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" + integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -1065,6 +1089,13 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +node-cron@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" + integrity sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA== + dependencies: + moment-timezone "^0.5.31" + node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" From bb11ab29c99c0369daa2e7f0e4e5f7d19efa508f Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 12:47:57 +0100 Subject: [PATCH 012/101] remove codeowners --- CODEOWNERS | 1 - 1 file changed, 1 deletion(-) delete mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 8ca1c02..0000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @RafDevX \ No newline at end of file From e10d41420548e65766075a495a2dc11c5e2513b3 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 13:17:26 +0100 Subject: [PATCH 013/101] small tweak --- src/bot.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index a2a1fb5..4ce952e 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -3,6 +3,8 @@ import * as attendance from "./modules/attendance"; import * as Discord from "discord.js"; import * as storage from "./storage"; +const { DISCORD_TOKEN } = process.env; + const client = new Discord.Client({ intents: [ Discord.Intents.FLAGS.GUILDS, @@ -10,10 +12,9 @@ const client = new Discord.Client({ ], }); -const buttonHandlers: Record< - string, - (interaction: Discord.ButtonInteraction) => Promise -> = { +const buttonHandlers: { + [prefix: string]: (interaction: Discord.ButtonInteraction) => Promise; +} = { attendance: attendance.handleAttendanceButton, }; @@ -41,10 +42,10 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { const loadBot = async (): Promise => { await storage.loadStorage(); - client.login(process.env.DISCORD_TOKEN); + client.login(DISCORD_TOKEN); }; -if (process.env.DISCORD_TOKEN) { +if (DISCORD_TOKEN) { loadBot(); } else { console.error( From 5caf26799f90b0dce87782e77af97f53788513cd Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 13:19:14 +0100 Subject: [PATCH 014/101] install better-sqlite3 --- package.json | 1 + yarn.lock | 461 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 459 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7bc65b0..f2bd93e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "IST Bot Team", "license": "MIT", "dependencies": { + "better-sqlite3": "^7.4.3", "discord.js": "^13.0.1", "node-cron": "^3.0.0" }, diff --git a/yarn.lock b/yarn.lock index e57ad75..03ece61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -264,6 +264,16 @@ ansi-escapes@^4.3.0: dependencies: type-fest "^0.21.3" +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" @@ -283,6 +293,19 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -310,6 +333,36 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +better-sqlite3@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-7.4.3.tgz#8e45a4164bf4b4e128d97375023550f780550997" + integrity sha512-07bKjClZg/f4KMVRkzWtoIvazVPcF1gsvVKVIXlxwleC2DxuIhnra3KCMlUT1rFeRYXXckot2a46UciF2d9KLw== + dependencies: + bindings "^1.5.0" + prebuild-install "^6.0.1" + tar "^6.1.0" + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -325,6 +378,14 @@ braces@^3.0.1: dependencies: fill-range "^7.0.1" +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -347,6 +408,16 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -367,6 +438,11 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -413,6 +489,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + cosmiconfig@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" @@ -440,11 +526,23 @@ debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -455,6 +553,16 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -500,6 +608,13 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -664,6 +779,11 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -704,6 +824,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -724,6 +849,18 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -734,6 +871,20 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -744,6 +895,11 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -792,6 +948,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -802,6 +963,11 @@ husky@^6.0.0: resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -838,11 +1004,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -853,6 +1024,18 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -895,6 +1078,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1060,6 +1248,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -1067,6 +1260,36 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimist@^1.2.0, minimist@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + moment-timezone@^0.5.31: version "0.5.33" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" @@ -1084,11 +1307,23 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +node-abi@^2.21.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.0.tgz#8be53bf3e7945a34eea10e0fc9a5982776cf550b" + integrity sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg== + dependencies: + semver "^5.4.1" + node-cron@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" @@ -1113,7 +1348,27 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -once@^1.3.0: +npmlog@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -1202,6 +1457,25 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +prebuild-install@^6.0.1: + version "6.1.4" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" + integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^2.21.0" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -1212,11 +1486,24 @@ prettier@^2.3.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -1227,6 +1514,38 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + regexpp@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -1276,11 +1595,26 @@ rxjs@^6.6.7: dependencies: tslib "^1.9.0" +safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= +semver@^5.4.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@^7.2.1, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" @@ -1288,6 +1622,11 @@ semver@^7.2.1, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1300,11 +1639,25 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -1338,6 +1691,23 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" @@ -1347,6 +1717,20 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -1356,6 +1740,20 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -1373,6 +1771,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -1399,6 +1802,39 @@ table@^6.0.9: string-width "^4.2.0" strip-ansi "^6.0.0" +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@^6.1.0: + version "6.1.8" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.8.tgz#4fc50cfe56511c538ce15b71e05eebe66530cbd4" + integrity sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -1438,6 +1874,13 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -1472,6 +1915,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -1489,6 +1937,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" From 2ea7e8a27fd5c23f9e9a3fc03de46ff6e9b84aef Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 20:09:10 +0100 Subject: [PATCH 015/101] start storage abstraction --- package.json | 4 +- src/storage.d.ts | 5 --- src/storage.ts | 96 +++++++++++++++++++++++++++++++++++++++--------- yarn.lock | 37 +++++++++++++++++++ 4 files changed, 118 insertions(+), 24 deletions(-) delete mode 100644 src/storage.d.ts diff --git a/package.json b/package.json index f2bd93e..d78fd37 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ "author": "IST Bot Team", "license": "MIT", "dependencies": { + "@types/better-sqlite3": "^5.4.3", "better-sqlite3": "^7.4.3", "discord.js": "^13.0.1", - "node-cron": "^3.0.0" + "node-cron": "^3.0.0", + "path": "^0.12.7" }, "devDependencies": { "@tsconfig/node14": "^1.0.1", diff --git a/src/storage.d.ts b/src/storage.d.ts deleted file mode 100644 index 8b4afd7..0000000 --- a/src/storage.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { AttendancePoll } from "./modules/attendance.d"; - -export interface Storage { - attendance: AttendancePoll[]; -} diff --git a/src/storage.ts b/src/storage.ts index 3113748..8005f63 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,25 +1,85 @@ -import { AttendancePoll } from "./modules/attendance.d"; -import { promises as fs } from "fs"; -import * as path from "path"; -import { Storage } from "./storage.d"; +import path from "path"; +import BS3 from "better-sqlite3"; -const dataFolder = path.resolve("./data"); -const attendanceFile = path.join(dataFolder, "attendance.json"); +export default class Storage { + // represents a database + private db: BS3.Database; + readonly: boolean; + constructor(file: string) { + this.db = new BS3(path.resolve(file)); + this.readonly = this.db.readonly; + } + getUnit(uname: string, shape: { [col: string]: string }): StorageUnit { + this.statement( + `CREATE TABLE IF NOT EXISTS ${uname} ( + ${Object.entries(shape) + .map((k, v) => k + " " + v) + .join(", ")} + );` + ).run(); -const storage: Storage = { - attendance: [], -}; + return new ( + Object.keys(shape).length === 2 && shape.key && shape.value + ? KVStorageUnit + : StorageUnit + )(uname, this); + } + statement(str: string): BS3.Statement { + // do NOT use this directly! + return this.db.prepare(str); + } + encodeValue(v: string | number | boolean | null | undefined): string { + if (v === null || v === undefined) { + return "NULL"; + } else if (typeof v === "string") { + return `"${v.replace('"', '\\"')}"`; + } else { + return v.toString(); + } + } +} -export const loadStorage = async (): Promise => { - try { - storage.attendance = JSON.parse( - await fs.readFile(attendanceFile, "utf-8") +class StorageUnit { + // represents a table + name: string; + storage: Storage; + constructor(uname: string, storage: Storage) { + this.name = uname; + this.storage = storage; + } + select( + cols: string[] | string, + where: string | { [col: string]: number | string } + ) { + // TODO: allow more complex objects with more operators + return this.storage.statement( + `SELECT ${typeof cols === "string" ? cols : cols.join(", ")} FROM ${ + this.name + } WHERE ${ + typeof where === "string" + ? where + : Object.entries(where).map( + ([c, v]) => c + " == " + this.storage.encodeValue(v) + ) + };` ); - } catch (e) { - console.log( - `Couldn't load attendance.json from data folder, ignoring it.` + } + replace(values: { [col: string]: number | string }) { + this.storage.statement( + `REPLACE INTO ${this.name} (${Object.keys(values).join( + ", " + )}) VALUES (${Object.values(values) + .map(this.storage.encodeValue) + .join(", ")})` ); } -}; +} -export const getAttendancePolls = (): AttendancePoll[] => storage.attendance; +class KVStorageUnit extends StorageUnit { + getValue(key: string, def: number | string) { + return this.select("value", { key }).pluck().get() ?? def; + } + setValue(key: string, value: number | string) { + return this.replace({ key, value }); + } +} diff --git a/yarn.lock b/yarn.lock index 03ece61..21951dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,6 +106,18 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== +"@types/better-sqlite3@^5.4.3": + version "5.4.3" + resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-5.4.3.tgz#4f1142c02db111627d9c5792f8aa349fcf46d100" + integrity sha512-d4T8Htgz3sQL3u5oVwkWipZLBYUooKEA4fhU9Sp4F6VDIhifQo1NR/IDtnAIID0Y9IXV3TQnNhv6S+m8TnkEdg== + dependencies: + "@types/integer" "*" + +"@types/integer@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/integer/-/integer-4.0.1.tgz#022f2c77a899e383e6d3dd374142416c22a5b9df" + integrity sha512-QQojPymFcV1hrvWXA1h0pP9RmFBFNuWikZcUEjjVsS19IyKO+jqOX24lp2ZHF4A21EmkosJhJDX7CLG67F2s7A== + "@types/json-schema@^7.0.7": version "7.0.7" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" @@ -1009,6 +1021,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -1445,6 +1462,14 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= + dependencies: + process "^0.11.1" + util "^0.10.3" + picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" @@ -1491,6 +1516,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.11.1: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -1920,6 +1950,13 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" From 40d990e7f6955b7d77147a26baa1a40feb68a553 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 21:08:53 +0100 Subject: [PATCH 016/101] use storage abstraction --- src/bot.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 4ce952e..5b689ea 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,9 +1,23 @@ import { ScheduledAttendancePoll } from "./modules/attendance.d"; import * as attendance from "./modules/attendance"; import * as Discord from "discord.js"; -import * as storage from "./storage"; +import Storage from "./storage"; -const { DISCORD_TOKEN } = process.env; +for (const ev of ["DISCORD_TOKEN", "DB_PATH"]) { + if (process.env[ev] === undefined) { + throw new Error(`Missing environment variable; please set ${ev}!`); + } +} +const { DISCORD_TOKEN, DB_PATH } = process.env; + +const storage = new Storage(DB_PATH as string); +const attendanceUnit = storage.getUnit("attendancePolls", { + id: "TEXT PRIMARY_KEY", + type: "TEXT", + title: "TEXT", + cron: "TEXT", + channelId: "TEXT", +}); const client = new Discord.Client({ intents: [ @@ -21,10 +35,10 @@ const buttonHandlers: { client.on("ready", async () => { await attendance.scheduleAttendancePolls( client, - storage - .getAttendancePolls() - .filter((poll) => poll.type === "scheduled") - .map((poll) => poll as ScheduledAttendancePoll) + attendanceUnit + .select("*", { type: "scheduled" }) + .all() + .map((p) => p as ScheduledAttendancePoll) ); console.log(`Logged in as ${client.user?.tag}!`); @@ -39,9 +53,7 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { } }); -const loadBot = async (): Promise => { - await storage.loadStorage(); - +const loadBot = async () => { client.login(DISCORD_TOKEN); }; From ff189d9c5b8ef6b132a36c0e860f3cb9d0bc671d Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 21:50:22 +0100 Subject: [PATCH 017/101] fix attendance --- src/modules/attendance.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/modules/attendance.ts b/src/modules/attendance.ts index 7bc3bd5..d07532b 100644 --- a/src/modules/attendance.ts +++ b/src/modules/attendance.ts @@ -3,6 +3,7 @@ import { ButtonInteraction, Client, Message, + MessageActionRow, MessageButton, MessageEmbed, Snowflake, @@ -11,7 +12,8 @@ import { import * as cron from "node-cron"; const ATTENDANCE_POLL_MSG = "Attendance Poll"; -const ATTENDANCE_POLL_BUTTONS = [ +const ATTENDANCE_POLL_ACTION_ROW = new MessageActionRow(); +ATTENDANCE_POLL_ACTION_ROW.addComponents([ new MessageButton() .setLabel("Yes") .setStyle("SUCCESS") @@ -24,7 +26,7 @@ const ATTENDANCE_POLL_BUTTONS = [ .setLabel("Clear") .setStyle("SECONDARY") .setCustomId("attendance:clear"), -]; +]); const ATTENDANCE_NO_ONE = "*No one*"; export const handleAttendanceButton = async ( @@ -50,7 +52,7 @@ export const handleAttendanceButton = async ( (interaction.message as Message).edit({ content: ATTENDANCE_POLL_MSG, embeds: [newEmbed], - components: [ATTENDANCE_POLL_BUTTONS], + components: [ATTENDANCE_POLL_ACTION_ROW], }); await interaction.reply({ content: "Response recorded!", ephemeral: true }); @@ -105,7 +107,7 @@ export const sendAttendanceEmbed = async ( .setFooter(embed.id) .setTimestamp(), ], - components: [ATTENDANCE_POLL_BUTTONS], + components: [ATTENDANCE_POLL_ACTION_ROW], }); await message.pin(); From de03c9c8f29eef503c242cefc201fe1032b09cdf Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 21:53:03 +0100 Subject: [PATCH 018/101] install deps to build better-sqlite3 --- Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index dfa340a..58be894 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,12 @@ FROM node:16.6.1-alpine3.14 + +# needed to build better-sqlite3 +RUN apk add --update --no-cache python3 make gcc libc-dev g++ +RUN ln -sf python3 /usr/bin/python + WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn COPY ./dist ./dist -CMD [ "yarn", "start"] +CMD [ "yarn", "start" ] From b90731571b8e6b562d2087aae593021e8d387414 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 23:04:23 +0100 Subject: [PATCH 019/101] fix storage bugs --- README.md | 3 ++- src/bot.ts | 44 ++++++++++++++++++++++++++++++++++---------- src/storage.ts | 35 +++++++++++++++++++---------------- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 73d5b36..a5ca52e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ version: "3.8" services: ist-discord-bot: - image: ist-bot-team/ist-discord-bot:v2.0.0 + image: ist-bot-team/ist-discord-bot:v2.0.0 # or 'build: .' if working locally volumes: - type: bind source: ./data @@ -19,6 +19,7 @@ services: environment: DISCORD_TOKEN: PLACE_BOT_TOKEN_HERE TZ: Europe/Lisbon # default timezone for crontab and other date related stuff + DB_PATH: ./data/bot.db restart: unless-stopped ``` diff --git a/src/bot.ts b/src/bot.ts index 5b689ea..c5837a8 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,7 +1,9 @@ -import { ScheduledAttendancePoll } from "./modules/attendance.d"; +import { performance } from "perf_hooks"; +import Discord from "discord.js"; +import Storage, { KVStorageUnit } from "./storage"; + import * as attendance from "./modules/attendance"; -import * as Discord from "discord.js"; -import Storage from "./storage"; +import { ScheduledAttendancePoll } from "./modules/attendance.d"; for (const ev of ["DISCORD_TOKEN", "DB_PATH"]) { if (process.env[ev] === undefined) { @@ -11,6 +13,10 @@ for (const ev of ["DISCORD_TOKEN", "DB_PATH"]) { const { DISCORD_TOKEN, DB_PATH } = process.env; const storage = new Storage(DB_PATH as string); +const configUnit = storage.getUnit("config", { + key: "TEXT PRIMARY_KEY", + value: "TEXT", +}) as KVStorageUnit; const attendanceUnit = storage.getUnit("attendancePolls", { id: "TEXT PRIMARY_KEY", type: "TEXT", @@ -32,16 +38,34 @@ const buttonHandlers: { attendance: attendance.handleAttendanceButton, }; +// TODO: move this somewhere else +const timeFunction = async (fun: () => Promise) => { + const t0 = performance.now(); + await fun(); + const t1 = performance.now(); + return Math.round((t1 - t0 + Number.EPSILON) * 100) / 100; +}; + client.on("ready", async () => { - await attendance.scheduleAttendancePolls( - client, - attendanceUnit - .select("*", { type: "scheduled" }) - .all() - .map((p) => p as ScheduledAttendancePoll) + console.log(`Logged in as ${client.user?.tag}!`); + + const delta = await timeFunction( + async () => + await attendance.scheduleAttendancePolls( + client, + attendanceUnit + .select("*", { type: "scheduled" }) + .all() + .map((p) => p as ScheduledAttendancePoll) + ) ); + console.log(`All attendance polls scheduled (delta=${delta}ms)`); - console.log(`Logged in as ${client.user?.tag}!`); + console.log("Before setting:", configUnit.getValue("dummy")); + + configUnit.setValue("dummy", 7); + + console.log("After setting:", configUnit.getValue("dummy")); }); client.on("interactionCreate", async (interaction: Discord.Interaction) => { diff --git a/src/storage.ts b/src/storage.ts index 8005f63..896cd03 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -13,7 +13,7 @@ export default class Storage { this.statement( `CREATE TABLE IF NOT EXISTS ${uname} ( ${Object.entries(shape) - .map((k, v) => k + " " + v) + .map(([k, v]) => k + " " + v) .join(", ")} );` ).run(); @@ -25,21 +25,22 @@ export default class Storage { )(uname, this); } statement(str: string): BS3.Statement { - // do NOT use this directly! + // do NOT use this directly from outside code! + console.info("Running SQL:", str); return this.db.prepare(str); } encodeValue(v: string | number | boolean | null | undefined): string { if (v === null || v === undefined) { return "NULL"; } else if (typeof v === "string") { - return `"${v.replace('"', '\\"')}"`; + return `'${v.replace("'", "\\'")}'`; } else { return v.toString(); } } } -class StorageUnit { +export class StorageUnit { // represents a table name: string; storage: Storage; @@ -50,7 +51,7 @@ class StorageUnit { select( cols: string[] | string, where: string | { [col: string]: number | string } - ) { + ): BS3.Statement { // TODO: allow more complex objects with more operators return this.storage.statement( `SELECT ${typeof cols === "string" ? cols : cols.join(", ")} FROM ${ @@ -64,22 +65,24 @@ class StorageUnit { };` ); } - replace(values: { [col: string]: number | string }) { - this.storage.statement( - `REPLACE INTO ${this.name} (${Object.keys(values).join( - ", " - )}) VALUES (${Object.values(values) - .map(this.storage.encodeValue) - .join(", ")})` - ); + replace(values: { [col: string]: number | string }): void { + this.storage + .statement( + `REPLACE INTO ${this.name} (${Object.keys(values).join( + ", " + )}) VALUES (${Object.values(values) + .map(this.storage.encodeValue) + .join(", ")})` + ) + .run(); } } -class KVStorageUnit extends StorageUnit { - getValue(key: string, def: number | string) { +export class KVStorageUnit extends StorageUnit { + getValue(key: string, def?: number | string): number | string { return this.select("value", { key }).pluck().get() ?? def; } - setValue(key: string, value: number | string) { + setValue(key: string, value: number | string): void { return this.replace({ key, value }); } } From b4e6fa09db467f67c5d5d448e05e970d1606d292 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sat, 14 Aug 2021 23:06:06 +0100 Subject: [PATCH 020/101] remove extraneous token checking --- src/bot.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index c5837a8..e14e9c7 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -77,14 +77,4 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { } }); -const loadBot = async () => { - client.login(DISCORD_TOKEN); -}; - -if (DISCORD_TOKEN) { - loadBot(); -} else { - console.error( - "Discord token not set. Please set the DISCORD_TOKEN environment variable" - ); -} +client.login(DISCORD_TOKEN); From b12c89b311672d5367a371c803a3a52f2c2409ff Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sun, 15 Aug 2021 23:12:36 +0100 Subject: [PATCH 021/101] implement prisma --- Dockerfile | 1 + package.json | 5 ++ src/bot.ts | 64 +++++++++----- src/modules/attendance.d.ts | 13 --- src/modules/attendance.ts | 7 +- .../20210815204238_init/migration.sql | 14 +++ src/prisma/migrations/migration_lock.toml | 3 + src/prisma/schema.prisma | 25 ++++++ src/storage.ts | 88 ------------------- yarn.lock | 24 +++++ 10 files changed, 117 insertions(+), 127 deletions(-) delete mode 100644 src/modules/attendance.d.ts create mode 100644 src/prisma/migrations/20210815204238_init/migration.sql create mode 100644 src/prisma/migrations/migration_lock.toml create mode 100644 src/prisma/schema.prisma delete mode 100644 src/storage.ts diff --git a/Dockerfile b/Dockerfile index 58be894..2e25ee5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,5 +8,6 @@ WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn +RUN npx prism generate COPY ./dist ./dist CMD [ "yarn", "start" ] diff --git a/package.json b/package.json index d78fd37..efb1486 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "IST Bot Team", "license": "MIT", "dependencies": { + "@prisma/client": "^2.29.1", "@types/better-sqlite3": "^5.4.3", "better-sqlite3": "^7.4.3", "discord.js": "^13.0.1", @@ -32,6 +33,7 @@ "husky": "^6.0.0", "lint-staged": "^11.0.0", "prettier": "^2.3.2", + "prisma": "^2.29.1", "typescript": "^4.3.4" }, "scripts": { @@ -41,5 +43,8 @@ }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" + }, + "prisma": { + "schema": "src/prisma/schema.prisma" } } diff --git a/src/bot.ts b/src/bot.ts index e14e9c7..8ea6601 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,29 +1,17 @@ import { performance } from "perf_hooks"; import Discord from "discord.js"; -import Storage, { KVStorageUnit } from "./storage"; +import { PrismaClient } from "@prisma/client"; import * as attendance from "./modules/attendance"; -import { ScheduledAttendancePoll } from "./modules/attendance.d"; -for (const ev of ["DISCORD_TOKEN", "DB_PATH"]) { +for (const ev of ["DISCORD_TOKEN"]) { if (process.env[ev] === undefined) { throw new Error(`Missing environment variable; please set ${ev}!`); } } -const { DISCORD_TOKEN, DB_PATH } = process.env; +const { DISCORD_TOKEN } = process.env; -const storage = new Storage(DB_PATH as string); -const configUnit = storage.getUnit("config", { - key: "TEXT PRIMARY_KEY", - value: "TEXT", -}) as KVStorageUnit; -const attendanceUnit = storage.getUnit("attendancePolls", { - id: "TEXT PRIMARY_KEY", - type: "TEXT", - title: "TEXT", - cron: "TEXT", - channelId: "TEXT", -}); +const prisma = new PrismaClient(); const client = new Discord.Client({ intents: [ @@ -53,19 +41,49 @@ client.on("ready", async () => { async () => await attendance.scheduleAttendancePolls( client, - attendanceUnit - .select("*", { type: "scheduled" }) - .all() - .map((p) => p as ScheduledAttendancePoll) + await prisma.attendancePoll.findMany({ + where: { + type: "scheduled", + }, + }) ) ); console.log(`All attendance polls scheduled (delta=${delta}ms)`); - console.log("Before setting:", configUnit.getValue("dummy")); + console.log( + "Before setting:", + ( + await prisma.config.findUnique({ + where: { + key: "dummy", + }, + }) + )?.value + ); - configUnit.setValue("dummy", 7); + await prisma.config.upsert({ + where: { + key: "dummy", + }, + update: { + value: "7", + }, + create: { + key: "dummy", + value: "6", + }, + }); - console.log("After setting:", configUnit.getValue("dummy")); + console.log( + "After setting:", + ( + await prisma.config.findUnique({ + where: { + key: "dummy", + }, + }) + )?.value + ); }); client.on("interactionCreate", async (interaction: Discord.Interaction) => { diff --git a/src/modules/attendance.d.ts b/src/modules/attendance.d.ts deleted file mode 100644 index a4e4f8d..0000000 --- a/src/modules/attendance.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface AttendancePoll { - id: string; // identifier used to keep track of embed on pinned messages - type: AttendanceType; - title: string; -} - -export interface ScheduledAttendancePoll extends AttendancePoll { - type: "scheduled"; - cron: string; // cron schedule - channelId: string; // channel to post the poll -} - -export type AttendanceType = "single" | "scheduled"; diff --git a/src/modules/attendance.ts b/src/modules/attendance.ts index d07532b..f667e3d 100644 --- a/src/modules/attendance.ts +++ b/src/modules/attendance.ts @@ -1,4 +1,3 @@ -import { AttendancePoll, ScheduledAttendancePoll } from "./attendance.d"; import { ButtonInteraction, Client, @@ -9,7 +8,9 @@ import { Snowflake, TextChannel, } from "discord.js"; -import * as cron from "node-cron"; +import cron from "node-cron"; + +import { AttendancePoll } from "@prisma/client"; const ATTENDANCE_POLL_MSG = "Attendance Poll"; const ATTENDANCE_POLL_ACTION_ROW = new MessageActionRow(); @@ -115,7 +116,7 @@ export const sendAttendanceEmbed = async ( export const scheduleAttendancePolls = async ( client: Client, - polls: ScheduledAttendancePoll[] + polls: AttendancePoll[] ): Promise => { await Promise.all( polls.map(async (poll) => { diff --git a/src/prisma/migrations/20210815204238_init/migration.sql b/src/prisma/migrations/20210815204238_init/migration.sql new file mode 100644 index 0000000..023f2aa --- /dev/null +++ b/src/prisma/migrations/20210815204238_init/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "config" ( + "key" TEXT NOT NULL PRIMARY KEY, + "value" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "attendance_polls" ( + "id" TEXT NOT NULL PRIMARY KEY, + "type" TEXT NOT NULL, + "title" TEXT NOT NULL, + "cron" TEXT NOT NULL, + "channel_id" TEXT NOT NULL +); diff --git a/src/prisma/migrations/migration_lock.toml b/src/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/src/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma new file mode 100644 index 0000000..a12b5f4 --- /dev/null +++ b/src/prisma/schema.prisma @@ -0,0 +1,25 @@ +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model Config { + key String @id + value String + + @@map("config") +} + +model AttendancePoll { + id String @id /// identifier used to keep track of embed on pinned messages + type String + title String + cron String /// cron schedule + channelId String @map("channel_id") /// channel where to post the poll + + @@map("attendance_polls") +} diff --git a/src/storage.ts b/src/storage.ts deleted file mode 100644 index 896cd03..0000000 --- a/src/storage.ts +++ /dev/null @@ -1,88 +0,0 @@ -import path from "path"; -import BS3 from "better-sqlite3"; - -export default class Storage { - // represents a database - private db: BS3.Database; - readonly: boolean; - constructor(file: string) { - this.db = new BS3(path.resolve(file)); - this.readonly = this.db.readonly; - } - getUnit(uname: string, shape: { [col: string]: string }): StorageUnit { - this.statement( - `CREATE TABLE IF NOT EXISTS ${uname} ( - ${Object.entries(shape) - .map(([k, v]) => k + " " + v) - .join(", ")} - );` - ).run(); - - return new ( - Object.keys(shape).length === 2 && shape.key && shape.value - ? KVStorageUnit - : StorageUnit - )(uname, this); - } - statement(str: string): BS3.Statement { - // do NOT use this directly from outside code! - console.info("Running SQL:", str); - return this.db.prepare(str); - } - encodeValue(v: string | number | boolean | null | undefined): string { - if (v === null || v === undefined) { - return "NULL"; - } else if (typeof v === "string") { - return `'${v.replace("'", "\\'")}'`; - } else { - return v.toString(); - } - } -} - -export class StorageUnit { - // represents a table - name: string; - storage: Storage; - constructor(uname: string, storage: Storage) { - this.name = uname; - this.storage = storage; - } - select( - cols: string[] | string, - where: string | { [col: string]: number | string } - ): BS3.Statement { - // TODO: allow more complex objects with more operators - return this.storage.statement( - `SELECT ${typeof cols === "string" ? cols : cols.join(", ")} FROM ${ - this.name - } WHERE ${ - typeof where === "string" - ? where - : Object.entries(where).map( - ([c, v]) => c + " == " + this.storage.encodeValue(v) - ) - };` - ); - } - replace(values: { [col: string]: number | string }): void { - this.storage - .statement( - `REPLACE INTO ${this.name} (${Object.keys(values).join( - ", " - )}) VALUES (${Object.values(values) - .map(this.storage.encodeValue) - .join(", ")})` - ) - .run(); - } -} - -export class KVStorageUnit extends StorageUnit { - getValue(key: string, def?: number | string): number | string { - return this.select("value", { key }).pluck().get() ?? def; - } - setValue(key: string, value: number | string): void { - return this.replace({ key, value }); - } -} diff --git a/yarn.lock b/yarn.lock index 21951dd..1037c38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -91,6 +91,23 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@prisma/client@^2.29.1": + version "2.29.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.29.1.tgz#a7b91c9644800de4e00b2f7c3789ff4bae42b3d6" + integrity sha512-GhieSvHGPIV5IwRYIkJ4FrGSNfX18lPhFtlyVWxhvX0ocdy8oTnjNZVTFgGxB6qVmJIUpH1HsckAzIoAX689IA== + dependencies: + "@prisma/engines-version" "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" + +"@prisma/engines-version@2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a": + version "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a.tgz#96db92f09714d4dd2a5e21054c28bd1c0820fa77" + integrity sha512-BU1DNNDhdzqjHtycpUzDrU8+jf6ZY+fbXvCV/rbqG+0JifljlIo4vbkHDMg97gBi1Do8pTLZGlTH16FlniKgAg== + +"@prisma/engines@2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a": + version "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a.tgz#0a44a6dcbee7e0a2850ea086675a8a4f4d627f9d" + integrity sha512-cgEoGK3dmKZkMp/sRbL8TsuVS50rHXYBHk2NY18DPUGr5//4ICno46EjzlayqAFVak8J6RtWZEs+8tE8j8frAQ== + "@sapphire/async-queue@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.1.4.tgz#ae431310917a8880961cebe8e59df6ffa40f2957" @@ -1511,6 +1528,13 @@ prettier@^2.3.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== +prisma@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.29.1.tgz#7845f55c7f09955b01f973c6a4b1f330cb212e7d" + integrity sha512-fRGh90+z0m3Jw3D6KBE6wyVCRR0w6M6QD93jh+em8IOQycmC48zB8hho8zeri3J9//C0k8fkDeQrRLJUosXROw== + dependencies: + "@prisma/engines" "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" From 588cbd9483cda8332656abc233d5d32003636eea Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sun, 15 Aug 2021 23:43:48 +0100 Subject: [PATCH 022/101] tweaks to allow prisma to generate client from within docker --- Dockerfile | 3 ++- package.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2e25ee5..24ad31e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn -RUN npx prism generate +COPY ./src/prisma/schema.prisma . +RUN yarn run prisma-gen COPY ./dist ./dist CMD [ "yarn", "start" ] diff --git a/package.json b/package.json index efb1486..56d7892 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "scripts": { "prepare": "husky install", "build": "tsc", - "start": "node ." + "start": "node .", + "prisma-gen": "node_modules/.bin/prisma generate --schema=./schema.prisma" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" From c0b2621ad6b00821c56cd3c7cd993e695896a7a0 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Sun, 15 Aug 2021 23:48:32 +0100 Subject: [PATCH 023/101] remove dependency on better-sqlite3 --- Dockerfile | 5 - package.json | 1 - yarn.lock | 461 +-------------------------------------------------- 3 files changed, 3 insertions(+), 464 deletions(-) diff --git a/Dockerfile b/Dockerfile index 24ad31e..cd9ec7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,4 @@ FROM node:16.6.1-alpine3.14 - -# needed to build better-sqlite3 -RUN apk add --update --no-cache python3 make gcc libc-dev g++ -RUN ln -sf python3 /usr/bin/python - WORKDIR /app COPY package.json . COPY yarn.lock . diff --git a/package.json b/package.json index 56d7892..f75ee07 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "dependencies": { "@prisma/client": "^2.29.1", "@types/better-sqlite3": "^5.4.3", - "better-sqlite3": "^7.4.3", "discord.js": "^13.0.1", "node-cron": "^3.0.0", "path": "^0.12.7" diff --git a/yarn.lock b/yarn.lock index 1037c38..420bfd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -293,16 +293,6 @@ ansi-escapes@^4.3.0: dependencies: type-fest "^0.21.3" -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" @@ -322,19 +312,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -362,36 +339,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -better-sqlite3@^7.4.3: - version "7.4.3" - resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-7.4.3.tgz#8e45a4164bf4b4e128d97375023550f780550997" - integrity sha512-07bKjClZg/f4KMVRkzWtoIvazVPcF1gsvVKVIXlxwleC2DxuIhnra3KCMlUT1rFeRYXXckot2a46UciF2d9KLw== - dependencies: - bindings "^1.5.0" - prebuild-install "^6.0.1" - tar "^6.1.0" - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -407,14 +354,6 @@ braces@^3.0.1: dependencies: fill-range "^7.0.1" -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -437,16 +376,6 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -467,11 +396,6 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -518,16 +442,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - cosmiconfig@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" @@ -555,23 +469,11 @@ debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== - dependencies: - mimic-response "^2.0.0" - dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -582,16 +484,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -637,13 +529,6 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -808,11 +693,6 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -853,11 +733,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -878,18 +753,6 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -900,20 +763,6 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -924,11 +773,6 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" - integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= - glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -977,11 +821,6 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -992,11 +831,6 @@ husky@^6.0.0: resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -1033,7 +867,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1043,11 +877,6 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -1058,18 +887,6 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -1112,11 +929,6 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1282,11 +1094,6 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== - minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -1294,36 +1101,6 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.3: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minipass@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== - dependencies: - yallist "^4.0.0" - -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - moment-timezone@^0.5.31: version "0.5.33" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" @@ -1341,23 +1118,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -node-abi@^2.21.0: - version "2.30.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.0.tgz#8be53bf3e7945a34eea10e0fc9a5982776cf550b" - integrity sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg== - dependencies: - semver "^5.4.1" - node-cron@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" @@ -1382,27 +1147,7 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npmlog@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -1499,25 +1244,6 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" -prebuild-install@^6.0.1: - version "6.1.4" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" - integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== - dependencies: - detect-libc "^1.0.3" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^2.21.0" - npmlog "^4.0.1" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^3.0.3" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -1535,11 +1261,6 @@ prisma@^2.29.1: dependencies: "@prisma/engines" "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - process@^0.11.1: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -1550,14 +1271,6 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -1568,38 +1281,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - regexpp@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -1649,26 +1330,11 @@ rxjs@^6.6.7: dependencies: tslib "^1.9.0" -safe-buffer@^5.0.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= -semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - semver@^7.2.1, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" @@ -1676,11 +1342,6 @@ semver@^7.2.1, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1693,25 +1354,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" - integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== - dependencies: - decompress-response "^4.2.0" - once "^1.3.1" - simple-concat "^1.0.0" - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -1745,23 +1392,6 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" @@ -1771,20 +1401,6 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -1794,20 +1410,6 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -1825,11 +1427,6 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -1856,39 +1453,6 @@ table@^6.0.9: string-width "^4.2.0" strip-ansi "^6.0.0" -tar-fs@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.1.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^6.1.0: - version "6.1.8" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.8.tgz#4fc50cfe56511c538ce15b71e05eebe66530cbd4" - integrity sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -1928,13 +1492,6 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -1969,11 +1526,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - util@^0.10.3: version "0.10.4" resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" @@ -1998,13 +1550,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" From c5c5a4234d025220f24db3faccb9f4dbe5ab2a12 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Mon, 16 Aug 2021 00:29:17 +0100 Subject: [PATCH 024/101] deploy prisma migrations from dockerfile --- Dockerfile | 1 + package.json | 3 ++- src/prisma/schema.prisma | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index cd9ec7c..7162a51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,5 +5,6 @@ COPY yarn.lock . RUN yarn COPY ./src/prisma/schema.prisma . RUN yarn run prisma-gen +RUN yarn run prisma-mig COPY ./dist ./dist CMD [ "yarn", "start" ] diff --git a/package.json b/package.json index f75ee07..769835c 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "prepare": "husky install", "build": "tsc", "start": "node .", - "prisma-gen": "node_modules/.bin/prisma generate --schema=./schema.prisma" + "prisma-gen": "node_modules/.bin/prisma generate --schema=./schema.prisma", + "prisma-mig": "node_modules/.bin/prisma migrate deploy --schema=./schema.prisma" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index a12b5f4..f8ea879 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -1,6 +1,6 @@ datasource db { provider = "sqlite" - url = env("DATABASE_URL") + url = "file:./data/bot.db" } generator client { From 0a8f0a5c823f0f740860b8d5e1864a16a92e214e Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Mon, 16 Aug 2021 00:29:25 +0100 Subject: [PATCH 025/101] update readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a5ca52e..7ee1dac 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,13 @@ services: environment: DISCORD_TOKEN: PLACE_BOT_TOKEN_HERE TZ: Europe/Lisbon # default timezone for crontab and other date related stuff - DB_PATH: ./data/bot.db restart: unless-stopped ``` 2. Run `docker-compose up -d --build` 3. That's it! -_You can also use `docker-compose down`, `docker-compose up`, `docker-compose restart` and `docker-compose logs`._ +_You can also use `docker-compose down`, `docker-compose up`, `docker-compose restart` and `docker-compose logs [-f]`._ ### Adding to a Server From bda0bf6e86a5b73ade882f8b3b3f530b0ce61feb Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Mon, 16 Aug 2021 00:30:18 +0100 Subject: [PATCH 026/101] run migrations before generating prisma client --- Dockerfile | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7162a51..8dfc56f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ COPY package.json . COPY yarn.lock . RUN yarn COPY ./src/prisma/schema.prisma . -RUN yarn run prisma-gen RUN yarn run prisma-mig +RUN yarn run prisma-gen COPY ./dist ./dist CMD [ "yarn", "start" ] diff --git a/package.json b/package.json index 769835c..981a7d8 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "prepare": "husky install", "build": "tsc", "start": "node .", - "prisma-gen": "node_modules/.bin/prisma generate --schema=./schema.prisma", - "prisma-mig": "node_modules/.bin/prisma migrate deploy --schema=./schema.prisma" + "prisma-mig": "node_modules/.bin/prisma migrate deploy --schema=./schema.prisma", + "prisma-gen": "node_modules/.bin/prisma generate --schema=./schema.prisma" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" From f31dfffae048cfd6cb41a2279d50519f9817eb80 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Mon, 16 Aug 2021 22:53:59 +0100 Subject: [PATCH 027/101] move prisma migrate and generate to dockerfile --- Dockerfile | 4 ++-- package.json | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8dfc56f..f7d6dbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ COPY package.json . COPY yarn.lock . RUN yarn COPY ./src/prisma/schema.prisma . -RUN yarn run prisma-mig -RUN yarn run prisma-gen +RUN yarn run prisma migrate deploy --schema=./schema.prisma +RUN yarn run prisma generate --schema=./schema.prisma COPY ./dist ./dist CMD [ "yarn", "start" ] diff --git a/package.json b/package.json index 981a7d8..67355d3 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,7 @@ "scripts": { "prepare": "husky install", "build": "tsc", - "start": "node .", - "prisma-mig": "node_modules/.bin/prisma migrate deploy --schema=./schema.prisma", - "prisma-gen": "node_modules/.bin/prisma generate --schema=./schema.prisma" + "start": "node ." }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" From 34c9a602d0d89bb42c22e62500695b773c3a5dad Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Mon, 16 Aug 2021 23:20:53 +0100 Subject: [PATCH 028/101] extract utils module --- src/modules/utils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/modules/utils.ts diff --git a/src/modules/utils.ts b/src/modules/utils.ts new file mode 100644 index 0000000..5e70ac7 --- /dev/null +++ b/src/modules/utils.ts @@ -0,0 +1,10 @@ +// Misc. utilities + +import { performance } from "perf_hooks"; + +export async function timeFunction(fun: () => Promise): Promise { + const t0 = performance.now(); + await fun(); + const t1 = performance.now(); + return Math.round((t1 - t0 + Number.EPSILON) * 100) / 100; +} From e9961ecea3b213718f5bbacac09e50ed426e01b1 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Mon, 16 Aug 2021 23:21:48 +0100 Subject: [PATCH 029/101] re-organize startup chores --- src/bot.ts | 66 +++++++++++++++--------------------------------------- 1 file changed, 18 insertions(+), 48 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 8ea6601..14a6d1f 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -2,6 +2,7 @@ import { performance } from "perf_hooks"; import Discord from "discord.js"; import { PrismaClient } from "@prisma/client"; +import * as utils from "./modules/utils"; import * as attendance from "./modules/attendance"; for (const ev of ["DISCORD_TOKEN"]) { @@ -26,19 +27,10 @@ const buttonHandlers: { attendance: attendance.handleAttendanceButton, }; -// TODO: move this somewhere else -const timeFunction = async (fun: () => Promise) => { - const t0 = performance.now(); - await fun(); - const t1 = performance.now(); - return Math.round((t1 - t0 + Number.EPSILON) * 100) / 100; -}; - -client.on("ready", async () => { - console.log(`Logged in as ${client.user?.tag}!`); - - const delta = await timeFunction( - async () => +const startupChores = [ + { + summary: "Schedule attendance polls", + fn: async () => await attendance.scheduleAttendancePolls( client, await prisma.attendancePoll.findMany({ @@ -46,44 +38,22 @@ client.on("ready", async () => { type: "scheduled", }, }) - ) - ); - console.log(`All attendance polls scheduled (delta=${delta}ms)`); + ), + complete: "All attendance polls scheduled", + }, +]; - console.log( - "Before setting:", - ( - await prisma.config.findUnique({ - where: { - key: "dummy", - }, - }) - )?.value - ); +client.on("ready", async () => { + console.log(`Logged in as ${client.user?.tag}!`); - await prisma.config.upsert({ - where: { - key: "dummy", - }, - update: { - value: "7", - }, - create: { - key: "dummy", - value: "6", - }, - }); + console.log("Duty before self: starting chores..."); - console.log( - "After setting:", - ( - await prisma.config.findUnique({ - where: { - key: "dummy", - }, - }) - )?.value - ); + for (const [i, chore] of startupChores.entries()) { + const delta = await utils.timeFunction(chore.fn); + console.log( + `[${i + 1}/${startupChores.length}] ${chore.complete} (${delta}ms)` + ); + } }); client.on("interactionCreate", async (interaction: Discord.Interaction) => { From 5b3b884e8f7809208811d5aa6c6c55675051f9de Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Mon, 16 Aug 2021 23:22:19 +0100 Subject: [PATCH 030/101] add short description at the beginning of files --- src/bot.ts | 3 ++- src/modules/attendance.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bot.ts b/src/bot.ts index 14a6d1f..c9f3179 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,5 @@ -import { performance } from "perf_hooks"; +// Main controller + import Discord from "discord.js"; import { PrismaClient } from "@prisma/client"; diff --git a/src/modules/attendance.ts b/src/modules/attendance.ts index f667e3d..09a3ed0 100644 --- a/src/modules/attendance.ts +++ b/src/modules/attendance.ts @@ -1,3 +1,5 @@ +// Handler for attendance polls + import { ButtonInteraction, Client, From 6ea0dee9b8155cf6ed2d2929bc92748ed72f8a71 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 11:55:02 +0100 Subject: [PATCH 031/101] Role Selection (#52) --- Dockerfile | 2 + README.md | 10 +- package.json | 5 +- src/bot.d.ts | 29 + src/bot.ts | 132 ++- src/modules/attendance.ts | 2 + src/modules/populate.ts | 85 ++ src/modules/roleSelection.ts | 963 ++++++++++++++++++ src/modules/utils.ts | 17 + .../migration.sql | 15 + .../migration.sql | 2 + .../migration.sql | 21 + .../migration.sql | 22 + .../migration.sql | 2 + .../migration.sql | 15 + src/prisma/schema.prisma | 30 +- yarn.lock | 86 +- 17 files changed, 1401 insertions(+), 37 deletions(-) create mode 100644 src/bot.d.ts create mode 100644 src/modules/populate.ts create mode 100644 src/modules/roleSelection.ts create mode 100644 src/prisma/migrations/20210821102445_role_selection/migration.sql create mode 100644 src/prisma/migrations/20210822093133_add_emoji_to_menu_opt/migration.sql create mode 100644 src/prisma/migrations/20210822215809_add_channel_id_and_allow_multiselect_role_groups/migration.sql create mode 100644 src/prisma/migrations/20210822221619_add_message_to_role_groups/migration.sql create mode 100644 src/prisma/migrations/20210823191157_add_messageid_to_role_groups/migration.sql create mode 100644 src/prisma/migrations/20210904233150_cascade_role_group_option/migration.sql diff --git a/Dockerfile b/Dockerfile index f7d6dbf..ff044c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ FROM node:16.6.1-alpine3.14 +ARG DATABASE_URL +ENV DATABASE_URL ${DATABASE_URL} WORKDIR /app COPY package.json . COPY yarn.lock . diff --git a/README.md b/README.md index 7ee1dac..5f27850 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,21 @@ version: "3.8" services: ist-discord-bot: - image: ist-bot-team/ist-discord-bot:v2.0.0 # or 'build: .' if working locally + ## EITHER: + image: ist-bot-team/ist-discord-bot:v2.0.0 + ## OR: + build: + context: . + args: + DATABASE_URL: file:./data/bot.db + ## END; volumes: - type: bind source: ./data target: /app/data environment: DISCORD_TOKEN: PLACE_BOT_TOKEN_HERE + GUILD_ID: PLACE_MAIN_GUILD_ID_HERE # or "GLOBAL" to use in multiple guilds (1hr roll-out time) TZ: Europe/Lisbon # default timezone for crontab and other date related stuff restart: unless-stopped ``` diff --git a/package.json b/package.json index 67355d3..f99e30a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "author": "IST Bot Team", "license": "MIT", "dependencies": { - "@prisma/client": "^2.29.1", + "@discordjs/rest": "^0.1.0-canary.0", + "@prisma/client": "^2.30.3", "@types/better-sqlite3": "^5.4.3", "discord.js": "^13.0.1", "node-cron": "^3.0.0", @@ -32,7 +33,7 @@ "husky": "^6.0.0", "lint-staged": "^11.0.0", "prettier": "^2.3.2", - "prisma": "^2.29.1", + "prisma": "^2.30.3", "typescript": "^4.3.4" }, "scripts": { diff --git a/src/bot.d.ts b/src/bot.d.ts new file mode 100644 index 0000000..a1762ad --- /dev/null +++ b/src/bot.d.ts @@ -0,0 +1,29 @@ +// General types + +import { PrismaClient } from "@prisma/client"; +import * as Discord from "discord.js"; +import { SlashCommandBuilder } from "@discordjs/builders"; + +export type InteractionHandler = ( + interaction: T, + prisma: PrismaClient, + client: Discord.Client +) => Promise; + +export type InteractionHandlers = { + [prefix: string]: InteractionHandler; +}; + +export interface CommandDescriptor { + command: string; + builder: SlashCommandBuilder; + handler: InteractionHandler; +} + +export type CommandProvider = () => CommandDescriptor[]; + +export interface Chore { + summary: string; + fn: () => Promise; + complete: string; +} diff --git a/src/bot.ts b/src/bot.ts index c9f3179..0469c14 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,17 +1,26 @@ // Main controller import Discord from "discord.js"; +import { + RESTPostAPIApplicationCommandsJSONBody, + Routes, +} from "discord-api-types/v9"; +import { REST } from "@discordjs/rest"; import { PrismaClient } from "@prisma/client"; +import { InteractionHandlers, CommandProvider, Chore } from "./bot.d"; + import * as utils from "./modules/utils"; import * as attendance from "./modules/attendance"; +import * as roleSelection from "./modules/roleSelection"; +import * as populate from "./modules/populate"; -for (const ev of ["DISCORD_TOKEN"]) { +for (const ev of ["DISCORD_TOKEN", "GUILD_ID"]) { if (process.env[ev] === undefined) { throw new Error(`Missing environment variable; please set ${ev}!`); } } -const { DISCORD_TOKEN } = process.env; +const { DISCORD_TOKEN, GUILD_ID } = process.env; const prisma = new PrismaClient(); @@ -22,13 +31,24 @@ const client = new Discord.Client({ ], }); -const buttonHandlers: { - [prefix: string]: (interaction: Discord.ButtonInteraction) => Promise; -} = { +const commandProviders: CommandProvider[] = [ + roleSelection.provideCommands, + misc.provideCommands, +]; + +const commandHandlers: InteractionHandlers = {}; +// will be dynamically loaded + +const buttonHandlers: InteractionHandlers = { attendance: attendance.handleAttendanceButton, + roleSelection: roleSelection.handleRoleSelectionButton, +}; + +const menuHandlers: InteractionHandlers = { + roleSelection: roleSelection.handleRoleSelectionMenu, }; -const startupChores = [ +const startupChores: Chore[] = [ { summary: "Schedule attendance polls", fn: async () => @@ -42,6 +62,55 @@ const startupChores = [ ), complete: "All attendance polls scheduled", }, + { + summary: "Populate database with mock/default/test data", + fn: async () => { + await populate.populateDatabase(prisma); + }, + complete: "Database fully populated with mock/default/test data", + }, + { + summary: "Send role selection messages", + fn: async () => { + await roleSelection.sendRoleSelectionMessages(client, prisma); + }, + complete: "Role selection messages deployed", + }, + { + summary: "Register slash commands", + fn: async () => { + const commands: RESTPostAPIApplicationCommandsJSONBody[] = []; + for (const provider of commandProviders) { + for (const descriptor of provider()) { + commands.push(descriptor.builder.toJSON()); + commandHandlers[descriptor.command] = descriptor.handler; + } + } + + const rest = new REST({ version: "9" }).setToken( + DISCORD_TOKEN as string + ); + + // TODO: use built-in slash commands permissions + + const useGlobalCommands = + GUILD_ID?.toLocaleLowerCase() === "global"; + await rest.put( + Routes.applicationCommands(client?.user?.id as string), + { body: useGlobalCommands ? commands : [] } + ); + if (!useGlobalCommands) { + await rest.put( + Routes.applicationGuildCommands( + client?.user?.id as string, + GUILD_ID as string + ), + { body: commands } + ); + } + }, + complete: "All slash commands registered", + }, ]; client.on("ready", async () => { @@ -50,19 +119,54 @@ client.on("ready", async () => { console.log("Duty before self: starting chores..."); for (const [i, chore] of startupChores.entries()) { - const delta = await utils.timeFunction(chore.fn); - console.log( - `[${i + 1}/${startupChores.length}] ${chore.complete} (${delta}ms)` - ); + const delta = await utils + .timeFunction(chore.fn) + .catch((e) => console.error("Chore error:", chore.summary, "-", e)); + if (delta) { + console.log( + `[${i + 1}/${startupChores.length}] ${ + chore.complete + } (${delta}ms)` + ); + } } + + console.log("Ready!"); }); client.on("interactionCreate", async (interaction: Discord.Interaction) => { - if (interaction.isButton()) { - const msgCompInteraction = interaction as Discord.ButtonInteraction; - const prefix = msgCompInteraction.customId.split(":")[0]; + try { + if (interaction.isMessageComponent()) { + const prefix = interaction.customId.split(":")[0]; + + // TODO: consider moving `await interaction.deferReply({ ephemeral: true });` here + + if (interaction.isButton()) { + await buttonHandlers[prefix]?.( + interaction as Discord.ButtonInteraction, + prisma, + client + ); + } else if (interaction.isSelectMenu()) { + await menuHandlers[prefix]?.( + interaction as Discord.SelectMenuInteraction, + prisma, + client + ); + } + } else if (interaction.isCommand()) { + await interaction.deferReply({ ephemeral: true }); + + // TODO: permissions!! - await buttonHandlers[prefix]?.(msgCompInteraction); + await commandHandlers[interaction.commandName]?.( + interaction, + prisma, + client + ); + } + } catch (e) { + console.error("Problem handling interaction: " + e.message); } }); diff --git a/src/modules/attendance.ts b/src/modules/attendance.ts index 09a3ed0..e588769 100644 --- a/src/modules/attendance.ts +++ b/src/modules/attendance.ts @@ -35,6 +35,8 @@ const ATTENDANCE_NO_ONE = "*No one*"; export const handleAttendanceButton = async ( interaction: ButtonInteraction ): Promise => { + await interaction.deferReply({ ephemeral: true }); + const action = interaction.customId.split(":")[1]; let fieldIndex = -1; diff --git a/src/modules/populate.ts b/src/modules/populate.ts new file mode 100644 index 0000000..7deee28 --- /dev/null +++ b/src/modules/populate.ts @@ -0,0 +1,85 @@ +// Populate the database with mock/test/default values + +import { PrismaClient } from "@prisma/client"; + +const populators = { + degree_selection: async (prisma: PrismaClient) => { + await prisma.roleGroup.create({ + data: { + id: "degree", + mode: "menu", + placeholder: "Escolhe o teu curso", + message: + "Olá <@97446650548588544>!\n\n||(isto é uma mensagem)||", + channelId: "859896451270574082", + options: { + create: [ + { + label: "LEIC-A", + description: + "Licenciatura em Engenharia Informática e de Computadores - Alameda", + value: "876961096253206542", + emoji: "💻", + }, + { + label: "LEIC-T", + description: + "Licenciatura em Engenharia Informática e de Computadores - Taguspark", + value: "876961212590587914", + emoji: "🇹", + }, + { + label: "LEFT", + description: + "Licenciatura em Engenharia Física e Tecnológica", + value: "876961271667372073", + emoji: "⚛️", + }, + ], + }, + }, + }); + }, + tourist: async (prisma: PrismaClient) => { + const configs = { + channel_id: "859896451270574082", + message_id: null, + message: "If you're a ~~terrible person~~ tourIST click below", + label: "I'm not in IST", + role_id: "881601009300959282", + exclusive_role_groups: "degree,year", + }; + + const items = []; + for (const [key, value] of Object.entries(configs)) { + const fqkey = `tourist:${key}`; + if (value) { + items.push( + prisma.config.upsert({ + where: { key: fqkey }, + create: { key: fqkey, value }, + update: { value }, + }) + ); + } + } + await prisma.$transaction(items); + }, +}; + +export async function populateDatabase(prisma: PrismaClient): Promise { + for (const [key, fn] of Object.entries(populators)) { + const fqkey = `populated:${key}`; + if ( + (await prisma.config.findFirst({ where: { key: fqkey } })) + ?.value !== "yes" + ) { + await fn(prisma); + await prisma.config.upsert({ + where: { key: fqkey }, + create: { key: fqkey, value: "yes" }, + update: { value: "yes" }, + }); + } + } +} diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts new file mode 100644 index 0000000..6906020 --- /dev/null +++ b/src/modules/roleSelection.ts @@ -0,0 +1,963 @@ +// Handler for role selection + +import { PrismaClient } from "@prisma/client"; +import Discord from "discord.js"; +import { getConfigFactory } from "./utils"; +import * as Builders from "@discordjs/builders"; +import { CommandDescriptor } from "../bot.d"; + +const MAX_COMPONENTS_PER_ROW = 5; +const MAX_ROWS_PER_MESSAGE = 5; + +const TOURIST_GROUP_ID = "__tourist"; // must be unique in database +const TOURIST_BUTTON_STYLE = "SECONDARY"; + +// TODO: load from fénix into database + +export async function sendRoleSelectionMessages( + client: Discord.Client, + prisma: PrismaClient, + editExisting = false +): Promise { + const groups = await prisma.roleGroup.findMany({ + include: { options: true }, + }); + + const getTouristConfig = getConfigFactory(prisma, "tourist"); + + try { + groups.push({ + id: TOURIST_GROUP_ID, + mode: "buttons", + placeholder: "N/A", + message: + (await getTouristConfig("message")) ?? + "Press if you're not from IST", + channelId: + (await getTouristConfig("channel_id", true)) ?? "missing", + messageId: (await getTouristConfig("message_id")) ?? null, + minValues: null, + maxValues: null, + options: [ + { + label: (await getTouristConfig("label")) ?? "I'm a TourIST", + description: TOURIST_BUTTON_STYLE, + value: + (await getTouristConfig("role_id", true)) ?? "missing", + emoji: null, + roleGroupId: TOURIST_GROUP_ID, + }, + ], + }); + } catch (e) { + console.error(`Failed to inject tourist group: ${e.message}`); + } + + for (const group of groups) { + try { + const channel = client.channels.cache.find( + (c) => c.id === group.channelId + ); + if (channel === undefined || !channel.isText()) { + throw new Error("Could not find channel"); + } + + let message; + + if ( + group.messageId === null || + !(message = await channel.messages + .fetch(group.messageId as string) + .catch(() => null)) || + editExisting + ) { + let components; + + if (group.mode === "menu") { + components = [ + new Discord.MessageActionRow().addComponents( + new Discord.MessageSelectMenu() + .setCustomId(`roleSelection:${group.id}`) + .setPlaceholder(group.placeholder) + .setMinValues(group.minValues ?? 1) + .setMaxValues(group.maxValues ?? 1) + .addOptions( + group.options as Discord.MessageSelectOptionData[] + ) + ), + ]; + } else if (group.mode === "buttons") { + const rows: Discord.MessageButton[][] = []; + let curRow: Discord.MessageButton[] = []; + + for (const opt of group.options) { + if ( + curRow.length && + curRow.length % MAX_COMPONENTS_PER_ROW === 0 + ) { + rows.push(curRow); + curRow = []; + } + + const btn = new Discord.MessageButton() + .setCustomId( + `roleSelection:${group.id}:${opt.value}` + ) + .setLabel(opt.label) + .setStyle( + ([ + "PRIMARY", + "SECONDARY", + "SUCCESS", + "DANGER", + "LINK", + ].includes(opt.description) + ? opt.description + : "PRIMARY") as Discord.MessageButtonStyleResolvable + ); + if (opt.emoji !== null) { + btn.setEmoji(opt.emoji); + } + curRow.push(btn); + } + + if (curRow.length) { + rows.push(curRow); + } + if (rows.length > MAX_ROWS_PER_MESSAGE) { + throw new Error( + `Too many buttons (${ + (rows.length - 1) * MAX_COMPONENTS_PER_ROW + + rows[rows.length - 1].length + } > ${ + MAX_COMPONENTS_PER_ROW * MAX_ROWS_PER_MESSAGE + })` + ); + } + components = rows.map((r) => + new Discord.MessageActionRow().addComponents(r) + ); + } else { + throw new Error(`Unknown mode '${group.mode}'`); + } + + if (components) { + const args = { + content: group.message, + components, + }; + const msg = + message && editExisting + ? await message.edit(args) + : await (channel as Discord.TextChannel).send(args); + + if (group.id === TOURIST_GROUP_ID) { + const key = "tourist:message_id"; + await prisma.config.upsert({ + where: { key }, + create: { key, value: msg.id }, + update: { value: msg.id }, + }); + } else { + await prisma.roleGroup.update({ + where: { id: group.id }, + data: { messageId: msg.id }, + }); + } + } + } + } catch (e) { + console.error( + `Could not send role selection message for group ${group.id} because: ${e.message}` + ); + } + } +} + +async function handleRoleSelection( + groupId: string, + roles: Discord.GuildMemberRoleManager, + roleToAdd: string, + prisma: PrismaClient +) { + const touristExclusive = ( + (await getConfigFactory(prisma, "tourist")("exclusive_role_groups")) ?? + "degree,year" + ).split(","); + + const groupRoles = + groupId === TOURIST_GROUP_ID + ? [ + roleToAdd, + ...( + await prisma.roleGroup.findMany({ + where: { + OR: touristExclusive.map((e) => ({ id: e })), + }, + include: { options: true }, + }) + ).flatMap((g) => g.options.map((o) => o.value)), + ] + : ( + ( + await prisma.roleGroup.findFirst({ + where: { id: groupId }, + include: { options: true }, + }) + )?.options.map((o) => o.value) ?? [] + ).concat( + [ + touristExclusive.includes(groupId) + ? ( + await prisma.config.findFirst({ + where: { key: "tourist:role_id" }, + }) + )?.value + : undefined, + ].filter((e) => e !== undefined) as string[] + ); + + try { + if (groupRoles.includes(roleToAdd)) { + const rolesToSet = [roleToAdd]; + for (const id of roles.cache.keys()) { + if (!groupRoles.includes(id)) { + rolesToSet.push(id); + } + } + await roles.set(rolesToSet); + return true; + } else { + throw new Error("Role not in group"); + } + } catch (e) { + return false; + } +} + +export async function handleRoleSelectionMenu( + interaction: Discord.SelectMenuInteraction, + prisma: PrismaClient +): Promise { + await interaction.deferReply({ ephemeral: true }); + + const groupId = interaction.customId.split(":")[1]; + const roles = interaction.member?.roles as Discord.GuildMemberRoleManager; + const roleToAdd = interaction.values[0]; // FIXME: this won't work for multiselects! + + if (await handleRoleSelection(groupId, roles, roleToAdd, prisma)) { + await interaction.editReply("✅ Role applied."); + } else { + await interaction.editReply("❌ Failed to apply role."); + } +} +// FIXME: these two (v & ^) are a bit humid +export async function handleRoleSelectionButton( + interaction: Discord.ButtonInteraction, + prisma: PrismaClient +): Promise { + await interaction.deferReply({ ephemeral: true }); + + const sp = interaction.customId.split(":"); + const roles = interaction.member?.roles as Discord.GuildMemberRoleManager; + + if (await handleRoleSelection(sp[1], roles, sp[2], prisma)) { + await interaction.editReply("✅ Role applied."); + } else { + await interaction.editReply("❌ Failed to apply role."); + } +} + +export function provideCommands(): CommandDescriptor[] { + const cmd = new Builders.SlashCommandBuilder() + .setName("role-selection") + .setDescription("Manage the role selection module"); + cmd.addSubcommandGroup( + new Builders.SlashCommandSubcommandGroupBuilder() + .setName("group") + .setDescription("Manage role selection groups") + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("create") + .setDescription("Create a new role selection group") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("id") + .setDescription("Unique identifier, snake_case") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("mode") + .setDescription("How users may choose roles") + .setRequired(true) + .addChoice("Selection Menu", "menu") + .addChoice("Buttons", "buttons") + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("placeholder") + .setDescription( + "Shown in select menus when no options are selected; ignored otherwise" + ) + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("message") + .setDescription("Message sent with menu/buttons") + .setRequired(true) + ) + // FIXME: no multiselect support yet, so not asking for minValues/maxValues + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription( + "Channel where to send role selection message" + ) + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("delete") + .setDescription( + "Delete a role selection group (this action cannot be reverted!)" + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("id") + .setDescription( + "Unique identifier of the role to delete" + ) + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("confirm-id") + .setDescription("Type id again to confirm") + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("edit") + .setDescription( + "Change a setting for an existing role selection group" + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("id") + .setDescription( + "Unique identifier of the role group" + ) + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("name") + .setDescription("Property to set") + .setRequired(true) + .addChoice("Mode", "mode") + .addChoice("Placeholder", "placeholder") + .addChoice("Message", "message") + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("value") + .setDescription("New value to set") + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("move") + .setDescription("Move role group to a new channel") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("id") + .setDescription( + "Unique identifier of the role group" + ) + .setRequired(true) + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription("New channel to send message to") + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("view") + .setDescription("Get information for a role group") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("id") + .setDescription( + "Unique identifier of the role group" + ) + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("list") + .setDescription("List all existing role selection groups") + ) + ); + cmd.addSubcommandGroup( + new Builders.SlashCommandSubcommandGroupBuilder() + .setName("options") + .setDescription("Manage options for a role selection group") + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("add") + .setDescription( + "Add a new option to a role selection group" + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("group-id") + .setDescription( + "Unique identifier of the role group" + ) + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("label") + .setDescription("New option's name") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("description") + .setDescription( + "For menus, shown to the user below the label. For buttons, specifies the button style (in caps)" + ) + .setRequired(true) + ) + .addRoleOption( + new Builders.SlashCommandRoleOption() + .setName("role") + .setDescription("Role corresponding to this option") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("emoji") + .setDescription( + "Single emote to show next to label" + ) + .setRequired(false) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("remove") + .setDescription("Remove an option from a role group") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("group-id") + .setDescription( + "Unique identifier of the role group" + ) + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("label") + .setDescription( + "Label corresponding to the option to remove" + ) + .setRequired(true) + ) + ) + ); + cmd.addSubcommandGroup( + new Builders.SlashCommandSubcommandGroupBuilder() + .setName("apply") + .setDescription( + "Apply changes made to role groups and respective options" + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("send-messages") + .setDescription("Send messages for all role groups") + .addBooleanOption( + new Builders.SlashCommandBooleanOption() + .setName("edit-existing") + .setDescription( + "Whether to edit existing messages, updating them. Otherwise, you may delete some before running this" + ) + .setRequired(true) + ) + ) + ); + return [ + { command: "role-selection", builder: cmd, handler: handleCommand }, + ]; +} + +async function createGroup( + prisma: PrismaClient, + id: string, + mode: string, + placeholder: string, + message: string, + channel: Discord.GuildChannel +): Promise<[boolean, string]> { + try { + if (!id.match(/^[a-z0-9_]+$/)) { + return [false, "Invalid id: must be snake_case"]; + } + + const goodModes = ["buttons", "menu"]; + if (!goodModes.includes(mode)) { + return [ + false, + "Invalid mode: may only be one of " + goodModes.join("/"), + ]; + } + + message = message.replace("\\n", "\n"); + + if (!channel.isText()) { + return [false, "Invalid channel: must be a text channel"]; + } + + return [ + true, + ( + await prisma.roleGroup.create({ + data: { + id, + mode, + placeholder, + message, + channelId: channel.id, + }, + }) + ).id, + ]; + } catch (e) { + return [false, "Something went wrong"]; + } +} + +async function deleteGroup( + prisma: PrismaClient, + id: string, + confirm: string +): Promise { + try { + if (id !== confirm) { + return "Confirmation failed"; + } + + if (!id.match(/^[a-z0-9_]+$/)) { + return "Invalid id: must be snake_case"; + } + + await prisma.roleGroup.delete({ where: { id } }); + + return true; + } catch (e) { + return "Something went wrong; possibly, no role group was found with that ID"; + } +} + +async function editGroup( + prisma: PrismaClient, + id: string, + name: string, + value: string +): Promise { + try { + if (!id.match(/^[a-z0-9_]+$/)) { + return "Invalid id: must be snake_case"; + } + + const goodNames = ["mode", "placeholder", "message"]; + if (!goodNames.includes(name)) { + return "Invalid name: must be one of " + goodNames.join("/"); + } + + const goodModes = ["menu", "buttons"]; + if (name === "mode" && !goodModes.includes(value)) { + return "Invalid mode: must be one of " + goodModes.join("/"); + } + + await prisma.roleGroup.update({ + where: { id }, + data: { [name]: value }, + }); + + return true; + } catch (e) { + return "Something went wrong; possibly, no role group was found with that ID"; + } +} + +async function moveGroup( + prisma: PrismaClient, + id: string, + channel: Discord.GuildChannel +): Promise { + try { + if (!id.match(/^[a-z0-9_]+$/)) { + return "Invalid id: must be snake_case"; + } + + if (!channel.isText()) { + return "Invalid channel: must be a text channel"; + } + + await prisma.roleGroup.update({ + where: { id }, + data: { channelId: channel.id }, + }); + + return true; + } catch (e) { + return "Something went wrong; possibly, no role group was found with that ID"; + } +} + +async function viewGroup( + prisma: PrismaClient, + id: string, + guildId: string +): Promise { + try { + const group = await prisma.roleGroup.findFirst({ + where: { id }, + include: { options: true }, + }); + if (group) { + const embed = new Discord.MessageEmbed().setTitle( + "Role Group Information" + ).setDescription(`**ID**: ${group.id} + **Mode:** ${group.mode} + **Placeholder:** ${group.placeholder} + **Message:** ${group.message} + **Channel:** <#${group.channelId}> + **Message:** ${ + group.messageId + ? "[Here](https://discord.com/channels/" + + guildId + + "/" + + group.channelId + + "/" + + group.messageId + + " 'Message Link')" + : "NONE" + }`); + + for (const opt of group.options) { + embed.addField( + (opt.emoji ? opt.emoji + " " : "") + opt.label, + opt.value + " - " + opt.description, + true + ); + } + + return { + embeds: [embed], + }; + } else { + return `No group was found with ID \`${id}\``; + } + } catch (e) { + return "Something went wrong"; + } +} + +async function addOption( + prisma: PrismaClient, + groupId: string, + label: string, + description: string, + role: Discord.Role, + emoji: string | null +): Promise { + try { + if (!groupId.match(/^[a-z0-9_]+$/)) { + return "Invalid group id: must be snake_case"; + } + + const group = await prisma.roleGroup.findFirst({ + where: { id: groupId }, + }); + + if (group === null) { + return "No group was found with that ID"; + } + + await prisma.roleGroupOption.create({ + data: { + label, + description, + value: role.id, + emoji, + roleGroupId: group.id, + }, + }); + + return true; + } catch (e) { + return "Something went wrong; possibly, that role is already associated with an option"; + } +} + +async function removeOption( + prisma: PrismaClient, + groupId: string, + label: string +): Promise { + try { + if (!groupId.match(/^[a-z0-9_]+$/)) { + return "Invalid group id: must be snake_case"; + } + + const group = await prisma.roleGroup.findFirst({ + where: { id: groupId }, + }); + + if (group === null) { + return "No group was found with that ID"; + } + + const possible = await prisma.roleGroupOption.findMany({ + where: { + label, + roleGroupId: group.id, + }, + }); + + if (possible.length < 1) { + return "No such option was found"; + } else if (possible.length > 1) { + return "Several options have this label; in the future a command to remove an option by its associated role (unique) may be added"; + } + + await prisma.roleGroupOption.delete({ + where: { value: possible[0].value }, + }); + + return true; + } catch (e) { + return "Something went wrong"; + } +} + +// TODO: dry this a bit +export async function handleCommand( + interaction: Discord.CommandInteraction, + prisma: PrismaClient, + client: Discord.Client +): Promise { + const subCommandGroup = interaction.options.getSubcommandGroup(); + const subCommand = interaction.options.getSubcommand(); + switch (subCommandGroup) { + case "group": + switch (subCommand) { + case "create": { + const [succ, res] = await createGroup( + prisma, + interaction.options.getString("id", true), + interaction.options.getString("mode", true), + interaction.options.getString("placeholder", true), + interaction.options.getString("message", true), + interaction.options.getChannel( + "channel", + true + ) as Discord.GuildChannel + // FIXME: I've no clue what a APIInteractionDataResolvedChannel is + // so I'm ignoring the possibility of it even existing + ); + if (succ) { + await interaction.editReply( + `✅ Role selection group \`${res}\` successfully created.` + ); + } else { + await interaction.editReply( + `❌ Could not create group because: **${res}**` + ); + } + break; + } + case "delete": { + const id = interaction.options.getString("id", true); + const res = await deleteGroup( + prisma, + id, + interaction.options.getString("confirm-id", true) + ); + if (res === true) { + await interaction.editReply( + `✅ Role selection group \`${id}\` successfully deleted.` + ); + } else { + await interaction.editReply( + `❌ Could not delete group because: **${res}**` + ); + } + break; + } + case "edit": { + const id = interaction.options.getString("id", true); + const res = await editGroup( + prisma, + id, + interaction.options.getString("name", true), + interaction.options.getString("value", true) + ); + if (res === true) { + await interaction.editReply( + `✅ Role selection group \`${id}\` successfully edited.` + ); + } else { + await interaction.editReply( + `❌ Could not edit group because: **${res}**` + ); + } + break; + } + case "move": { + const id = interaction.options.getString("id", true); + const channel = interaction.options.getChannel( + "channel", + true + ) as Discord.GuildChannel; + const res = await moveGroup(prisma, id, channel); + if (res === true) { + await interaction.editReply( + `✅ Role selection group \`${id}\` successfully moved to <#${channel.id}>.` + ); + } else { + await interaction.editReply( + `❌ Could not move group because: **${res}**` + ); + } + break; + } + case "view": { + await interaction.editReply( + await viewGroup( + prisma, + interaction.options.getString("id", true), + interaction.guildId as string + ) + ); + break; + } + case "list": { + try { + await interaction.editReply({ + embeds: [ + new Discord.MessageEmbed() + .setTitle("Role Selection Groups") + .setDescription( + "All available role groups are listed below with their `mode` field." + ) + .addFields( + ( + await prisma.roleGroup.findMany() + ).map((g) => ({ + name: g.id, + value: g.mode, + inline: true, + })) + ), + ], + }); + } catch (e) { + console.error( + "Could not list role groups because: ", + e.message + ); + await interaction.editReply( + "❌ Failed to list role groups." + ); + } + break; + } + } + break; + case "options": + switch (subCommand) { + case "add": { + const res = await addOption( + prisma, + interaction.options.getString("group-id", true), + interaction.options.getString("label", true), + interaction.options.getString("description", true), + interaction.options.getRole( + "role", + true + ) as Discord.Role, + interaction.options.getString("emoji", false) + ); + if (res === true) { + await interaction.editReply( + "✅ Option successfully added." + ); + } else { + await interaction.editReply( + `❌ Could not add option because: **${res}**` + ); + } + break; + } + case "remove": { + const res = await removeOption( + prisma, + interaction.options.getString("group-id", true), + interaction.options.getString("label", true) + ); + if (res === true) { + await interaction.editReply( + "✅ Option successfully removed." + ); + } else { + await interaction.editReply( + `❌ Could not remove option because: **${res}**` + ); + } + break; + } + } + break; + case "apply": + switch (subCommand) { + case "send-messages": { + try { + await sendRoleSelectionMessages( + client, + prisma, + interaction.options.getBoolean( + "edit-existing", + true + ) + ); + + await interaction.editReply( + "✅ Role selection messages successfully sent." + ); + } catch (e) { + console.error( + "Could not send role selection messages:", + e.message + ); + await interaction.editReply( + "❌ Failed to send role selection messages." + ); + } + break; + } + } + } +} diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 5e70ac7..01c8f85 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -2,9 +2,26 @@ import { performance } from "perf_hooks"; +import { PrismaClient } from "@prisma/client"; + export async function timeFunction(fun: () => Promise): Promise { const t0 = performance.now(); await fun(); const t1 = performance.now(); return Math.round((t1 - t0 + Number.EPSILON) * 100) / 100; } + +export function getConfigFactory( + prisma: PrismaClient, + scope: string +): (key: string, throwIfMissing?: boolean) => Promise { + return async (key: string, throwIfMissing?: boolean) => { + const result = ( + await prisma.config.findFirst({ where: { key: `${scope}:${key}` } }) + )?.value; + if (throwIfMissing && result === undefined) { + throw new Error(`Missing config "${scope}:${key}"`); + } + return result; + }; +} diff --git a/src/prisma/migrations/20210821102445_role_selection/migration.sql b/src/prisma/migrations/20210821102445_role_selection/migration.sql new file mode 100644 index 0000000..ff0b658 --- /dev/null +++ b/src/prisma/migrations/20210821102445_role_selection/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "role_groups" ( + "id" TEXT NOT NULL PRIMARY KEY, + "mode" TEXT NOT NULL, + "placeholder" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "role_group_options" ( + "label" TEXT NOT NULL, + "description" TEXT NOT NULL, + "value" TEXT NOT NULL PRIMARY KEY, + "role_group_id" TEXT, + FOREIGN KEY ("role_group_id") REFERENCES "role_groups" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); diff --git a/src/prisma/migrations/20210822093133_add_emoji_to_menu_opt/migration.sql b/src/prisma/migrations/20210822093133_add_emoji_to_menu_opt/migration.sql new file mode 100644 index 0000000..5f4a6d2 --- /dev/null +++ b/src/prisma/migrations/20210822093133_add_emoji_to_menu_opt/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "role_group_options" ADD COLUMN "emoji" TEXT; diff --git a/src/prisma/migrations/20210822215809_add_channel_id_and_allow_multiselect_role_groups/migration.sql b/src/prisma/migrations/20210822215809_add_channel_id_and_allow_multiselect_role_groups/migration.sql new file mode 100644 index 0000000..05e0f56 --- /dev/null +++ b/src/prisma/migrations/20210822215809_add_channel_id_and_allow_multiselect_role_groups/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - Added the required column `channel_id` to the `role_groups` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_role_groups" ( + "id" TEXT NOT NULL PRIMARY KEY, + "mode" TEXT NOT NULL, + "placeholder" TEXT NOT NULL, + "min_values" INTEGER, + "max_values" INTEGER, + "channel_id" TEXT NOT NULL +); +INSERT INTO "new_role_groups" ("id", "mode", "placeholder") SELECT "id", "mode", "placeholder" FROM "role_groups"; +DROP TABLE "role_groups"; +ALTER TABLE "new_role_groups" RENAME TO "role_groups"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/src/prisma/migrations/20210822221619_add_message_to_role_groups/migration.sql b/src/prisma/migrations/20210822221619_add_message_to_role_groups/migration.sql new file mode 100644 index 0000000..6c9a989 --- /dev/null +++ b/src/prisma/migrations/20210822221619_add_message_to_role_groups/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - Added the required column `message` to the `role_groups` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_role_groups" ( + "id" TEXT NOT NULL PRIMARY KEY, + "mode" TEXT NOT NULL, + "placeholder" TEXT NOT NULL, + "message" TEXT NOT NULL, + "min_values" INTEGER, + "max_values" INTEGER, + "channel_id" TEXT NOT NULL +); +INSERT INTO "new_role_groups" ("channel_id", "id", "max_values", "min_values", "mode", "placeholder") SELECT "channel_id", "id", "max_values", "min_values", "mode", "placeholder" FROM "role_groups"; +DROP TABLE "role_groups"; +ALTER TABLE "new_role_groups" RENAME TO "role_groups"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/src/prisma/migrations/20210823191157_add_messageid_to_role_groups/migration.sql b/src/prisma/migrations/20210823191157_add_messageid_to_role_groups/migration.sql new file mode 100644 index 0000000..df1b6b5 --- /dev/null +++ b/src/prisma/migrations/20210823191157_add_messageid_to_role_groups/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "role_groups" ADD COLUMN "message_id" TEXT; diff --git a/src/prisma/migrations/20210904233150_cascade_role_group_option/migration.sql b/src/prisma/migrations/20210904233150_cascade_role_group_option/migration.sql new file mode 100644 index 0000000..4ceb559 --- /dev/null +++ b/src/prisma/migrations/20210904233150_cascade_role_group_option/migration.sql @@ -0,0 +1,15 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_role_group_options" ( + "label" TEXT NOT NULL, + "description" TEXT NOT NULL, + "value" TEXT NOT NULL PRIMARY KEY, + "emoji" TEXT, + "role_group_id" TEXT, + FOREIGN KEY ("role_group_id") REFERENCES "role_groups" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_role_group_options" ("description", "emoji", "label", "role_group_id", "value") SELECT "description", "emoji", "label", "role_group_id", "value" FROM "role_group_options"; +DROP TABLE "role_group_options"; +ALTER TABLE "new_role_group_options" RENAME TO "role_group_options"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index f8ea879..c56b775 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -1,10 +1,11 @@ datasource db { provider = "sqlite" - url = "file:./data/bot.db" + url = env("DATABASE_URL") } generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] } model Config { @@ -23,3 +24,28 @@ model AttendancePoll { @@map("attendance_polls") } + +model RoleGroup { + id String @id + mode String + placeholder String + message String + minValues Int? @map("min_values") + maxValues Int? @map("max_values") + channelId String @map("channel_id") + messageId String? @map("message_id") + options RoleGroupOption[] + + @@map("role_groups") +} + +model RoleGroupOption { + label String + description String + value String @id + emoji String? + RoleGroup RoleGroup? @relation(fields: [roleGroupId], references: [id], onDelete: Cascade) + roleGroupId String? @map("role_group_id") + + @@map("role_group_options") +} diff --git a/yarn.lock b/yarn.lock index 420bfd3..71429ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,6 +41,11 @@ ts-mixer "^6.0.0" tslib "^2.3.0" +"@discordjs/collection@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.1.6.tgz#9e9a7637f4e4e0688fd8b2b5c63133c91607682c" + integrity sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ== + "@discordjs/collection@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.2.1.tgz#ea4bc7b41b7b7b6daa82e439141222ec95c469b2" @@ -55,6 +60,20 @@ combined-stream "^1.0.8" mime-types "^2.1.12" +"@discordjs/rest@^0.1.0-canary.0": + version "0.1.0-canary.0" + resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-0.1.0-canary.0.tgz#666f9a1a0c1f2f5a09a3a79f77aeddaeafbcbcc1" + integrity sha512-d+s//ISYVV+e0w/926wMEeO7vju+Pn11x1JM4tcmVMCHSDgpi6pnFCNAXF1TEdnDcy7xf9tq5cf2pQkb/7ySTQ== + dependencies: + "@discordjs/collection" "^0.1.6" + "@sapphire/async-queue" "^1.1.4" + "@sapphire/snowflake" "^1.3.5" + abort-controller "^3.0.0" + discord-api-types "^0.18.1" + form-data "^4.0.0" + node-fetch "^2.6.1" + tslib "^2.3.0" + "@eslint/eslintrc@^0.4.2": version "0.4.2" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.2.tgz#f63d0ef06f5c0c57d76c4ab5f63d3835c51b0179" @@ -91,28 +110,33 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@prisma/client@^2.29.1": - version "2.29.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.29.1.tgz#a7b91c9644800de4e00b2f7c3789ff4bae42b3d6" - integrity sha512-GhieSvHGPIV5IwRYIkJ4FrGSNfX18lPhFtlyVWxhvX0ocdy8oTnjNZVTFgGxB6qVmJIUpH1HsckAzIoAX689IA== +"@prisma/client@^2.30.3": + version "2.30.3" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.30.3.tgz#49c1015e2cec26a44b20c62eb2fd738cb0bb043b" + integrity sha512-Ey2miZ+Hne12We3rA8XrlPoAF0iuKEhw5IK2nropaelSt0Ju3b2qSz9Qt50a/1Mx3+7yRSu/iSXt8y9TUMl/Yw== dependencies: - "@prisma/engines-version" "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" + "@prisma/engines-version" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" -"@prisma/engines-version@2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a": - version "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a.tgz#96db92f09714d4dd2a5e21054c28bd1c0820fa77" - integrity sha512-BU1DNNDhdzqjHtycpUzDrU8+jf6ZY+fbXvCV/rbqG+0JifljlIo4vbkHDMg97gBi1Do8pTLZGlTH16FlniKgAg== +"@prisma/engines-version@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20": + version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#d5ef55c92beeba56e52bba12b703af0bfd30530d" + integrity sha512-/iDRgaoSQC77WN2oDsOM8dn61fykm6tnZUAClY+6p+XJbOEgZ9gy4CKuKTBgrjSGDVjtQ/S2KGcYd3Ring8xaw== -"@prisma/engines@2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a": - version "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a.tgz#0a44a6dcbee7e0a2850ea086675a8a4f4d627f9d" - integrity sha512-cgEoGK3dmKZkMp/sRbL8TsuVS50rHXYBHk2NY18DPUGr5//4ICno46EjzlayqAFVak8J6RtWZEs+8tE8j8frAQ== +"@prisma/engines@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20": + version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#2df768aa7c9f84acaa1f35c970417822233a9fb1" + integrity sha512-WPnA/IUrxDihrRhdP6+8KAVSwsc0zsh8ioPYsLJjOhzVhwpRbuFH2tJDRIAbc+qFh+BbTIZbeyBYt8fpNXaYQQ== "@sapphire/async-queue@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.1.4.tgz#ae431310917a8880961cebe8e59df6ffa40f2957" integrity sha512-fFrlF/uWpGOX5djw5Mu2Hnnrunao75WGey0sP0J3jnhmrJ5TAPzHYOmytD5iN/+pMxS+f+u/gezqHa9tPhRHEA== +"@sapphire/snowflake@^1.3.5": + version "1.3.6" + resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-1.3.6.tgz#166e8c5c08d01c861edd7e2edc80b5739741715f" + integrity sha512-QnzuLp+p9D7agynVub/zqlDVriDza9y3STArBhNiNBUgIX8+GL5FpQxstRfw1jDr5jkZUjcuKYAHxjIuXKdJAg== + "@sindresorhus/is@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.1.tgz#d26729db850fa327b7cacc5522252194404226f5" @@ -243,6 +267,13 @@ "@typescript-eslint/types" "4.28.1" eslint-visitor-keys "^2.0.0" +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" @@ -491,6 +522,11 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +discord-api-types@^0.18.1: + version "0.18.1" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.18.1.tgz#5d08ed1263236be9c21a22065d0e6b51f790f492" + integrity sha512-hNC38R9ZF4uaujaZQtQfm5CdQO58uhdkoHQAVvMfIL0LgOSZeW575W8H6upngQOuoxWd8tiRII3LLJm9zuQKYg== + discord-api-types@^0.22.0: version "0.22.0" resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.22.0.tgz#34dc57fe8e016e5eaac5e393646cd42a7e1ccc2a" @@ -678,6 +714,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -753,6 +794,15 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1254,12 +1304,12 @@ prettier@^2.3.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== -prisma@^2.29.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.29.1.tgz#7845f55c7f09955b01f973c6a4b1f330cb212e7d" - integrity sha512-fRGh90+z0m3Jw3D6KBE6wyVCRR0w6M6QD93jh+em8IOQycmC48zB8hho8zeri3J9//C0k8fkDeQrRLJUosXROw== +prisma@^2.30.3: + version "2.30.3" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.30.3.tgz#e4a770e1f52151e72c1c5be0aa2e75222a0135c4" + integrity sha512-48qYba2BIyUmXuosBZs0g3kYGrxKvo4VkSHYOuLlDdDirmKyvoY2hCYMUYHSx3f++8ovfgs+MX5KmNlP+iAZrQ== dependencies: - "@prisma/engines" "2.29.0-34.1be4cd60b89afa04b192acb1ef47758a39810f3a" + "@prisma/engines" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" process@^0.11.1: version "0.11.10" From e9fd8126be73fb77dfdc0801f87724c452f766e2 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 12:04:18 +0100 Subject: [PATCH 032/101] Misc commands (#56) Co-authored-by: Diogo Correia --- src/bot.ts | 14 ++++++++- src/modules/misc.ts | 68 ++++++++++++++++++++++++++++++++++++++++++++ src/modules/utils.ts | 23 +++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/modules/misc.ts diff --git a/src/bot.ts b/src/bot.ts index 0469c14..e22b377 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -13,6 +13,7 @@ import { InteractionHandlers, CommandProvider, Chore } from "./bot.d"; import * as utils from "./modules/utils"; import * as attendance from "./modules/attendance"; import * as roleSelection from "./modules/roleSelection"; +import * as misc from "./modules/misc"; import * as populate from "./modules/populate"; for (const ev of ["DISCORD_TOKEN", "GUILD_ID"]) { @@ -157,7 +158,18 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { } else if (interaction.isCommand()) { await interaction.deferReply({ ephemeral: true }); - // TODO: permissions!! + const perms: Discord.Permissions | undefined = ( + interaction.member as Discord.GuildMember + )?.permissions; + if ( + !( + perms && + perms.has(Discord.Permissions.FLAGS.MANAGE_GUILD, true) + ) + ) { + await interaction.editReply("Permission denied."); + return; + } await commandHandlers[interaction.commandName]?.( interaction, diff --git a/src/modules/misc.ts b/src/modules/misc.ts new file mode 100644 index 0000000..eadc1fa --- /dev/null +++ b/src/modules/misc.ts @@ -0,0 +1,68 @@ +// Misc. logic that doesn't fit elsewhere + +import * as fs from "fs"; + +import * as Discord from "discord.js"; +import * as Builders from "@discordjs/builders"; + +import { PrismaClient } from "@prisma/client"; + +import { CommandDescriptor } from "../bot.d"; +import * as utils from "./utils"; + +export function provideCommands(): CommandDescriptor[] { + return [ + { + command: "about", + builder: new Builders.SlashCommandBuilder() + .setName("about") + .setDescription("Show general and version information"), + handler: handleAboutCommand, + }, + ]; +} + +export async function handleAboutCommand( + interaction: Discord.CommandInteraction, + _prisma: PrismaClient, + client: Discord.Client +): Promise { + let pkg: Record; + try { + pkg = JSON.parse( + fs.readFileSync(__dirname + "/../../package.json").toString() + ); + } catch (e) { + pkg = {}; + } + const pvar = (v: string, fallback = "[unknown]") => pkg[v] ?? fallback; + + // cannot easily import so reading it directly + await interaction.editReply({ + embeds: [ + new Discord.MessageEmbed() + .setTitle("IST Discord Bot") + .setURL(pvar("homepage", "https://discord.leic.pt")) + .setAuthor(pvar("author")) + .setDescription( + `**Description:** ${pvar("description")} + **Version:** ${pvar("version")} + **License:** ${pvar("license")} + **Authors:**` + ) + .addFields( + [ + ["Rafael Oliveira", "RafDevX"], + ["Diogo Correia", "diogotcorreia"], + ].map((a) => ({ + name: a[0], + value: "[GitHub](https://github.com/" + a[1] + ")", + inline: true, + })) + ) + .setFooter( + "Uptime: " + utils.durationString(client.uptime ?? 0) + ), + ], + }); +} diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 01c8f85..490b2cc 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -25,3 +25,26 @@ export function getConfigFactory( return result; }; } + +export function divmod(a: number, b: number): [number, number] { + return [Math.floor(a / b), a % b]; +} + +export function durationString(time: number): string { + time = Math.round(time / 1000); // ms -> secs + let tmp; + const strs: string[] = [], + seps: { [label: string]: number } = { + day: 24 * 60 * 60, + hour: 60 * 60, + min: 60, + sec: 1, + }; + for (const [label, secs] of Object.entries(seps)) { + [tmp, time] = divmod(time, secs); + if (tmp) { + strs.push(tmp + " " + label + (tmp === 1 ? "" : "s")); + } + } + return strs.join(", "); +} From a7d4e666fe35d01176b074f566fe0404f448b35a Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 12:14:06 +0100 Subject: [PATCH 033/101] Sudo Module (#57) Co-authored-by: Diogo Correia --- README.md | 2 ++ package.json | 1 + src/bot.ts | 4 ++- src/modules/sudo.ts | 72 +++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 16 ++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/modules/sudo.ts diff --git a/README.md b/README.md index 5f27850..f99fefd 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ services: environment: DISCORD_TOKEN: PLACE_BOT_TOKEN_HERE GUILD_ID: PLACE_MAIN_GUILD_ID_HERE # or "GLOBAL" to use in multiple guilds (1hr roll-out time) + ADMIN_ID: PLACE_ADMIN_ROLE_ID_HERE + ADMIN_PLUS_ID: PLACE_ADMIN_PLUS_ROLE_ID_HERE TZ: Europe/Lisbon # default timezone for crontab and other date related stuff restart: unless-stopped ``` diff --git a/package.json b/package.json index f99e30a..837efda 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "IST Bot Team", "license": "MIT", "dependencies": { + "@discordjs/builders": "^0.6.0", "@discordjs/rest": "^0.1.0-canary.0", "@prisma/client": "^2.30.3", "@types/better-sqlite3": "^5.4.3", diff --git a/src/bot.ts b/src/bot.ts index e22b377..218dfda 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -13,10 +13,11 @@ import { InteractionHandlers, CommandProvider, Chore } from "./bot.d"; import * as utils from "./modules/utils"; import * as attendance from "./modules/attendance"; import * as roleSelection from "./modules/roleSelection"; +import * as sudo from "./modules/sudo"; import * as misc from "./modules/misc"; import * as populate from "./modules/populate"; -for (const ev of ["DISCORD_TOKEN", "GUILD_ID"]) { +for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { if (process.env[ev] === undefined) { throw new Error(`Missing environment variable; please set ${ev}!`); } @@ -34,6 +35,7 @@ const client = new Discord.Client({ const commandProviders: CommandProvider[] = [ roleSelection.provideCommands, + sudo.provideCommands, misc.provideCommands, ]; diff --git a/src/modules/sudo.ts b/src/modules/sudo.ts new file mode 100644 index 0000000..5f19bab --- /dev/null +++ b/src/modules/sudo.ts @@ -0,0 +1,72 @@ +// Controller for sudo features +// (allows admins to not be overwhelmed by having all permissions at all times) + +import * as Discord from "discord.js"; +import * as Builders from "@discordjs/builders"; + +import { CommandDescriptor } from "../bot.d"; + +export function provideCommands(): CommandDescriptor[] { + return [ + { + command: "sudo", + builder: new Builders.SlashCommandBuilder() + .setName("sudo") + .setDescription("Toggle enhanced administrator permissions"), + handler: handleSudoCommand, + }, + { + command: "reset-admin", + builder: new Builders.SlashCommandBuilder() + .setName("reset-admin") + .setDescription( + "Remove enhanced administrator permissions from everyone" + ), + handler: handleResetAdminCommand, + }, + ]; +} + +export async function handleSudoCommand( + interaction: Discord.CommandInteraction +): Promise { + try { + const roles = interaction.member + ?.roles as Discord.GuildMemberRoleManager; + const apId = process.env.ADMIN_PLUS_ID as string; + + if (roles.cache.has(apId)) { + await roles.remove(apId); + await interaction.editReply( + "✅ Successfully removed `Admin+` role." + ); + } else { + await roles.add(apId); + await interaction.editReply("✅ Successfully added `Admin+` role."); + } + } catch (e) { + await interaction.editReply("❌ Failed to toggle `Admin+` role."); + } +} + +export async function handleResetAdminCommand( + interaction: Discord.CommandInteraction +): Promise { + try { + const role = await ( + await interaction.guild?.fetch() + )?.roles.cache.get(process.env.ADMIN_PLUS_ID as string); + + if (!role) { + await interaction.editReply( + "❌ Could not locate the `Admin+` role" + ); + } + + role?.members.forEach((member) => member.roles.remove(role)); + + await interaction.editReply("✅ Successfully reset the `Admin+` role."); + } catch (e) { + await interaction.editReply("❌ Failed to reset the `Admin+` role."); + } +} diff --git a/yarn.lock b/yarn.lock index 71429ca..80f66a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,6 +41,17 @@ ts-mixer "^6.0.0" tslib "^2.3.0" +"@discordjs/builders@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-0.6.0.tgz#4724d18990a97d84d0250eba5b50991b71a450a5" + integrity sha512-mH3Gx61LKk2CD05laCI9K5wp+a3NyASHDUGx83DGJFkqJlRlSV5WMJNY6RS37A5SjqDtGMF4wVR9jzFaqShe6Q== + dependencies: + "@sindresorhus/is" "^4.0.1" + discord-api-types "^0.22.0" + ow "^0.27.0" + ts-mixer "^6.0.0" + tslib "^2.3.1" + "@discordjs/collection@^0.1.6": version "0.1.6" resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.1.6.tgz#9e9a7637f4e4e0688fd8b2b5c63133c91607682c" @@ -1535,6 +1546,11 @@ tslib@^2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From 6ab6fb50adfafe145144d5fb6e1cab366864973b Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 20:36:46 +0100 Subject: [PATCH 034/101] Use native permissions for slash commands (#58) Co-authored-by: Diogo Correia --- src/bot.d.ts | 1 + src/bot.ts | 102 +++++++++++++++++++++++++++++++++++++------- src/modules/misc.ts | 2 + 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/src/bot.d.ts b/src/bot.d.ts index a1762ad..2a4b501 100644 --- a/src/bot.d.ts +++ b/src/bot.d.ts @@ -18,6 +18,7 @@ export interface CommandDescriptor { command: string; builder: SlashCommandBuilder; handler: InteractionHandler; + permission?: CommandPermission; } export type CommandProvider = () => CommandDescriptor[]; diff --git a/src/bot.ts b/src/bot.ts index 218dfda..eaef7ea 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -24,6 +24,16 @@ for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { } const { DISCORD_TOKEN, GUILD_ID } = process.env; +export enum CommandPermission { + Public, + Admin, + ServerOwner, +} +// this cannot be in bot.d.ts since declaration files are not copied to dist/ +// and enums are needed at runtime + +const DEFAULT_COMMAND_PERMISSION: CommandPermission = CommandPermission.Admin; + const prisma = new PrismaClient(); const client = new Discord.Client({ @@ -39,8 +49,9 @@ const commandProviders: CommandProvider[] = [ misc.provideCommands, ]; +const commandPermissions: { [command: string]: CommandPermission } = {}; const commandHandlers: InteractionHandlers = {}; -// will be dynamically loaded +// two above will be dynamically loaded const buttonHandlers: InteractionHandlers = { attendance: attendance.handleAttendanceButton, @@ -85,8 +96,19 @@ const startupChores: Chore[] = [ const commands: RESTPostAPIApplicationCommandsJSONBody[] = []; for (const provider of commandProviders) { for (const descriptor of provider()) { - commands.push(descriptor.builder.toJSON()); + commands.push( + descriptor.builder + .setDefaultPermission( + descriptor.permission === + CommandPermission.Public + ) + .toJSON() + ); commandHandlers[descriptor.command] = descriptor.handler; + if (descriptor.permission !== undefined) { + commandPermissions[descriptor.command] = + descriptor.permission; + } } } @@ -94,8 +116,6 @@ const startupChores: Chore[] = [ DISCORD_TOKEN as string ); - // TODO: use built-in slash commands permissions - const useGlobalCommands = GUILD_ID?.toLocaleLowerCase() === "global"; await rest.put( @@ -114,6 +134,55 @@ const startupChores: Chore[] = [ }, complete: "All slash commands registered", }, + { + summary: "Update guild slash command permissions", + fn: async () => { + const guild = await client.guilds.cache.get(GUILD_ID as string); + const commands = guild?.commands; + + const fetched = await commands?.fetch(); + + if (fetched) { + await commands?.permissions.set({ + fullPermissions: fetched.map((c) => { + let commandSpecificPermission: + | Discord.ApplicationCommandPermissionData + | undefined; + const perm = + commandPermissions[c.name] ?? + DEFAULT_COMMAND_PERMISSION; + switch (perm) { + case CommandPermission.Admin: + commandSpecificPermission = { + id: process.env.ADMIN_ID as string, + type: "ROLE", + permission: true, + }; + break; + case CommandPermission.ServerOwner: { + const owner = guild?.ownerId; + if (owner) { + commandSpecificPermission = { + id: owner, + type: "USER", + permission: true, + }; + } + break; + } + } + return { + id: c.id, + permissions: commandSpecificPermission + ? [commandSpecificPermission] + : [], + }; + }), + }); + } + }, + complete: "All slash command permissions overwritten", + }, ]; client.on("ready", async () => { @@ -160,17 +229,20 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { } else if (interaction.isCommand()) { await interaction.deferReply({ ephemeral: true }); - const perms: Discord.Permissions | undefined = ( - interaction.member as Discord.GuildMember - )?.permissions; - if ( - !( - perms && - perms.has(Discord.Permissions.FLAGS.MANAGE_GUILD, true) - ) - ) { - await interaction.editReply("Permission denied."); - return; + if (!interaction.command?.guildId) { + // global commands + const perms: Discord.Permissions | undefined = ( + interaction.member as Discord.GuildMember + )?.permissions; + if ( + !( + perms && + perms.has(Discord.Permissions.FLAGS.MANAGE_GUILD, true) + ) + ) { + await interaction.editReply("Permission denied."); + return; + } } await commandHandlers[interaction.commandName]?.( diff --git a/src/modules/misc.ts b/src/modules/misc.ts index eadc1fa..7804d41 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -8,6 +8,7 @@ import * as Builders from "@discordjs/builders"; import { PrismaClient } from "@prisma/client"; import { CommandDescriptor } from "../bot.d"; +import { CommandPermission } from "../bot"; import * as utils from "./utils"; export function provideCommands(): CommandDescriptor[] { @@ -18,6 +19,7 @@ export function provideCommands(): CommandDescriptor[] { .setName("about") .setDescription("Show general and version information"), handler: handleAboutCommand, + permission: CommandPermission.Public, }, ]; } From b3da17aec50fc3d370f3778143eb6a9997503aa3 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 20:47:05 +0100 Subject: [PATCH 035/101] Gallery channels (#59) Co-authored-by: Diogo Correia --- src/bot.ts | 6 + src/modules/galleryChannels.ts | 241 +++++++++++++++++++++++++++++++++ src/modules/utils.ts | 12 ++ 3 files changed, 259 insertions(+) create mode 100644 src/modules/galleryChannels.ts diff --git a/src/bot.ts b/src/bot.ts index eaef7ea..d6070a8 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -15,6 +15,7 @@ import * as attendance from "./modules/attendance"; import * as roleSelection from "./modules/roleSelection"; import * as sudo from "./modules/sudo"; import * as misc from "./modules/misc"; +import * as galleryChannels from "./modules/galleryChannels"; import * as populate from "./modules/populate"; for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { @@ -47,6 +48,7 @@ const commandProviders: CommandProvider[] = [ roleSelection.provideCommands, sudo.provideCommands, misc.provideCommands, + galleryChannels.provideCommands, ]; const commandPermissions: { [command: string]: CommandPermission } = {}; @@ -256,4 +258,8 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { } }); +client.on("messageCreate", async (message) => { + await galleryChannels.handleMessage(message, prisma); +}); + client.login(DISCORD_TOKEN); diff --git a/src/modules/galleryChannels.ts b/src/modules/galleryChannels.ts new file mode 100644 index 0000000..4e6ffe6 --- /dev/null +++ b/src/modules/galleryChannels.ts @@ -0,0 +1,241 @@ +// Controller for channels where only images may be sent + +import { PrismaClient } from "@prisma/client"; +import * as Discord from "discord.js"; +import * as Builders from "@discordjs/builders"; + +import { CommandDescriptor } from "../bot.d"; +import * as utils from "./utils"; + +export async function handleMessage( + message: Discord.Message, + prisma: PrismaClient, + galleries?: Discord.Snowflake[] +): Promise { + if ( + message.author.bot || + message.member?.roles.cache.has(process.env.ADMIN_ID as string) + ) { + return false; + } + + // TODO: cache this vv + if (galleries === undefined) { + galleries = await utils.fetchGalleries(prisma); + } + + if (galleries.includes(message.channelId)) { + if ( + !message.attachments.size && + !message.content.startsWith("https://") + ) { + const sermon = await message.reply({ + content: `This is a gallery channel, ${message.author}, so only images may be sent here.`, + allowedMentions: { users: [message.author.id] }, + failIfNotExists: false, + }); + await message.delete().catch(() => undefined); + setTimeout( + async () => await sermon.delete().catch(() => undefined), + 5000 + ); + return true; + } + } + + return false; +} + +export async function parseExistingMessages( + prisma: PrismaClient, + channels: (Discord.TextChannel | Discord.ThreadChannel)[] +): Promise { + let count = 0; + for (const channel of channels) { + try { + const messages = await channel.messages.fetch({ limit: 100 }); + + for (const [_id, message] of messages) { + try { + if (await handleMessage(message, prisma, [channel.id])) { + count++; + } + } catch (e) { + // do nothing + } + } + } catch (e) { + // do nothing + } + } + return count; +} + +export function provideCommands(): CommandDescriptor[] { + const cmd = new Builders.SlashCommandBuilder() + .setName("gallery-channels") + .setDescription("Controller for the gallery-channels module"); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("add") + .setDescription("Add a new gallery channel") + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription("Existing messages will be preserved") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("remove") + .setDescription("Remove a gallery channel") + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription("New messages will no longer be moderated") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("list") + .setDescription("List existing gallery channels") + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("clean") + .setDescription( + "Clean (an) existing gallery channel(s), 100 messages/14 days" + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription( + "If specified, only this channel will be cleaned" + ) + .setRequired(false) + ) + ); + return [ + { + builder: cmd, + command: "gallery-channels", + handler: handleCommand, + }, + ]; +} + +async function updateGalleries( + prisma: PrismaClient, + newVal: Discord.Snowflake[] +) { + const value = newVal.join(","); + await prisma.config.upsert({ + where: { key: "gallery_channels" }, + update: { value }, + create: { + key: "gallery_channels", + value, + }, + }); +} + +export async function handleCommand( + interaction: Discord.CommandInteraction, + prisma: PrismaClient, + client: Discord.Client +): Promise { + const galleries = await utils.fetchGalleries(prisma); + + switch (interaction.options.getSubcommand()) { + case "add": { + try { + const channel = interaction.options.getChannel( + "channel", + true + ).id; + + if (galleries.includes(channel)) { + await interaction.editReply( + "❌ Channel is already a gallery." + ); + } else { + galleries.push(channel); + await updateGalleries(prisma, galleries); + await interaction.editReply( + "✅ Gallery successfully added." + ); + } + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + break; + } + case "remove": { + try { + const channel = interaction.options.getChannel( + "channel", + true + ).id; + + if (!galleries.includes(channel)) { + await interaction.editReply("❌ Channel is not a gallery."); + } else { + await updateGalleries( + prisma, + galleries.filter((c) => c != channel) + ); + await interaction.editReply( + "✅ Gallery successfully removed." + ); + } + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + break; + } + case "list": + interaction.editReply( + "**Gallery Channels**\n\n" + + (galleries.length + ? galleries.map((c) => `- <#${c}>`).join("\n") + : "*None*") + ); + break; + case "clean": { + try { + const channel = interaction.options.getChannel( + "channel", + false + ) as Discord.GuildChannel | null; + let channels: (Discord.TextChannel | Discord.ThreadChannel)[] = + []; + if ( + channel === null || + !(channel.isText() || channel.isThread()) + ) { + for (const c of galleries) { + const cf = await client.channels.fetch(c); + if (cf && (cf.isText() || cf.isThread())) { + channels.push( + cf as + | Discord.TextChannel + | Discord.ThreadChannel + ); + } + } + } else { + channels = [ + channel as Discord.TextChannel | Discord.ThreadChannel, + ]; + } + const n = await parseExistingMessages(prisma, channels); + await interaction.editReply(`✅ Cleaned \`${n}\` messages.`); + break; + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + } + } +} diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 490b2cc..a5b525f 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -3,6 +3,7 @@ import { performance } from "perf_hooks"; import { PrismaClient } from "@prisma/client"; +import * as Discord from "discord.js"; export async function timeFunction(fun: () => Promise): Promise { const t0 = performance.now(); @@ -48,3 +49,14 @@ export function durationString(time: number): string { } return strs.join(", "); } + +export async function fetchGalleries( + prisma: PrismaClient +): Promise { + return ( + (await prisma.config.findFirst({ where: { key: "gallery_channels" } })) + ?.value ?? "" + ) + .split(",") + .filter((c) => c.length); +} From d680e1d7de883237b225dff72a615fb0d26d39e0 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 20:53:55 +0100 Subject: [PATCH 036/101] Infer command name (#60) Co-authored-by: Diogo Correia --- src/bot.d.ts | 1 - src/bot.ts | 6 +++--- src/modules/galleryChannels.ts | 1 - src/modules/misc.ts | 1 - src/modules/roleSelection.ts | 4 +--- src/modules/sudo.ts | 2 -- 6 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/bot.d.ts b/src/bot.d.ts index 2a4b501..e8a283d 100644 --- a/src/bot.d.ts +++ b/src/bot.d.ts @@ -15,7 +15,6 @@ export type InteractionHandlers = { }; export interface CommandDescriptor { - command: string; builder: SlashCommandBuilder; handler: InteractionHandler; permission?: CommandPermission; diff --git a/src/bot.ts b/src/bot.ts index d6070a8..413b59b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -98,6 +98,7 @@ const startupChores: Chore[] = [ const commands: RESTPostAPIApplicationCommandsJSONBody[] = []; for (const provider of commandProviders) { for (const descriptor of provider()) { + const name = descriptor.builder.name; commands.push( descriptor.builder .setDefaultPermission( @@ -106,10 +107,9 @@ const startupChores: Chore[] = [ ) .toJSON() ); - commandHandlers[descriptor.command] = descriptor.handler; + commandHandlers[name] = descriptor.handler; if (descriptor.permission !== undefined) { - commandPermissions[descriptor.command] = - descriptor.permission; + commandPermissions[name] = descriptor.permission; } } } diff --git a/src/modules/galleryChannels.ts b/src/modules/galleryChannels.ts index 4e6ffe6..236347f 100644 --- a/src/modules/galleryChannels.ts +++ b/src/modules/galleryChannels.ts @@ -120,7 +120,6 @@ export function provideCommands(): CommandDescriptor[] { return [ { builder: cmd, - command: "gallery-channels", handler: handleCommand, }, ]; diff --git a/src/modules/misc.ts b/src/modules/misc.ts index 7804d41..19c8692 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -14,7 +14,6 @@ import * as utils from "./utils"; export function provideCommands(): CommandDescriptor[] { return [ { - command: "about", builder: new Builders.SlashCommandBuilder() .setName("about") .setDescription("Show general and version information"), diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 6906020..66095cc 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -496,9 +496,7 @@ export function provideCommands(): CommandDescriptor[] { ) ) ); - return [ - { command: "role-selection", builder: cmd, handler: handleCommand }, - ]; + return [{ builder: cmd, handler: handleCommand }]; } async function createGroup( diff --git a/src/modules/sudo.ts b/src/modules/sudo.ts index 5f19bab..f6116f2 100644 --- a/src/modules/sudo.ts +++ b/src/modules/sudo.ts @@ -9,14 +9,12 @@ import { CommandDescriptor } from "../bot.d"; export function provideCommands(): CommandDescriptor[] { return [ { - command: "sudo", builder: new Builders.SlashCommandBuilder() .setName("sudo") .setDescription("Toggle enhanced administrator permissions"), handler: handleSudoCommand, }, { - command: "reset-admin", builder: new Builders.SlashCommandBuilder() .setName("reset-admin") .setDescription( From 8318e9f72a2a66b7e6236ea50e1d6ba529c6a979 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 20:59:46 +0100 Subject: [PATCH 037/101] TourIST config commands (#61) Co-authored-by: Diogo Correia --- src/modules/roleSelection.ts | 176 ++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 1 deletion(-) diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 66095cc..7c53f14 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -2,8 +2,9 @@ import { PrismaClient } from "@prisma/client"; import Discord from "discord.js"; -import { getConfigFactory } from "./utils"; import * as Builders from "@discordjs/builders"; + +import { getConfigFactory } from "./utils"; import { CommandDescriptor } from "../bot.d"; const MAX_COMPONENTS_PER_ROW = 5; @@ -496,6 +497,59 @@ export function provideCommands(): CommandDescriptor[] { ) ) ); + cmd.addSubcommandGroup( + new Builders.SlashCommandSubcommandGroupBuilder() + .setName("tourist") + .setDescription("Settings for the special TourIST group") + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-info") + .setDescription("Change the message or button label") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("field") + .setDescription("Which field to change") + .setRequired(true) + .addChoice("Message", "message") + .addChoice("Label", "label") + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("value") + .setDescription("Value to change to") + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("move") + .setDescription("Set the channel where the message is sent") + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription("Channel where to move to") + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-role") + .setDescription("Set the TourIST role") + .addRoleOption( + new Builders.SlashCommandRoleOption() + .setName("role") + .setDescription("TourIST role") + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("info") + .setDescription( + "Get all information relative to the TourIST role" + ) + ) + ); return [{ builder: cmd, handler: handleCommand }]; } @@ -957,5 +1011,125 @@ export async function handleCommand( break; } } + break; + case "tourist": + switch (subCommand) { + case "set-info": + try { + const name = interaction.options.getString( + "name", + true + ); + const value = interaction.options.getString( + "value", + true + ); + + if (!["message", "label"].includes(name)) { + await interaction.editReply("❌ Invalid name."); + } + + const fqkey = `tourist:${name}`; + + await prisma.config.upsert({ + where: { key: fqkey }, + update: { value }, + create: { key: fqkey, value }, + }); + + await interaction.editReply( + `✅ Successfully set TourIST ${name}.` + ); + } catch (e) { + await interaction.editReply( + "❌ Failed to set TourIST info." + ); + } + break; + case "move": + try { + const channel = interaction.options.getChannel( + "channel", + true + ) as Discord.GuildChannel; + + if (!channel.isText() && !channel.isThread()) { + await interaction.editReply("❌ Invalid channel."); + } + + const fqkey = `tourist:channel_id`; + + await prisma.config.upsert({ + where: { key: fqkey }, + update: { value: channel.id }, + create: { key: fqkey, value: channel.id }, + }); + + await interaction.editReply( + `✅ Successfully set TourIST channel.` + ); + } catch (e) { + await interaction.editReply( + "❌ Failed to set TourIST channel." + ); + } + break; + case "set-role": + try { + const role = interaction.options.getRole( + "role", + true + ) as Discord.Role; + + const fqkey = `tourist:role_id`; + + await prisma.config.upsert({ + where: { key: fqkey }, + update: { value: role.id }, + create: { key: fqkey, value: role.id }, + }); + + await interaction.editReply( + `✅ Successfully set TourIST role.` + ); + } catch (e) { + await interaction.editReply( + "❌ Failed to set TourIST role." + ); + } + break; + case "info": + try { + const getConfig = getConfigFactory(prisma, "tourist"); + const message = + (await getConfig("message")) ?? "[UNSET]"; + const label = (await getConfig("label")) ?? "[UNSET]"; + const channel = await getConfig("channel_id"); + const role = await getConfig("role_id"); + const msgId = await getConfig("message_id"); + + const embed = new Discord.MessageEmbed() + .setTitle("TourIST Information") + .addField("Message", message) + .addField("Label", label) + .addField( + "Channel", + channel ? `<#${channel}>` : "[UNSET]" + ) + .addField("Role", role ? `<@&${role}>` : "[UNSET]") + .addField( + "Location", + channel && msgId + ? `[Here](https://discord.com/channels/${process.env.GUILD_ID}/${channel}/${msgId})` + : "[UNSET]" + ); + await interaction.editReply({ embeds: [embed] }); + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + + break; + } + break; } } From 953b07e70e627cd2861f6998a67ba147c7f0a00b Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 21:01:06 +0100 Subject: [PATCH 038/101] Update README (#62) Co-authored-by: Diogo Correia --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f99fefd..1f8de80 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,10 @@ services: restart: unless-stopped ``` -2. Run `docker-compose up -d --build` -3. That's it! +2. Create a folder named `data` for Docker to store things in +3. Run `yarn` and `yarn run build` if you're using the source code +4. Run `docker-compose up -d --build` +5. That's it! _You can also use `docker-compose down`, `docker-compose up`, `docker-compose restart` and `docker-compose logs [-f]`._ From 8eb794644ce9622510b17f6d0e42538d4fc22833 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 6 Sep 2021 21:06:22 +0100 Subject: [PATCH 039/101] Voice threads (#63) Co-authored-by: Diogo Correia --- src/bot.ts | 24 +++++ src/modules/voiceThreads.ts | 207 ++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 src/modules/voiceThreads.ts diff --git a/src/bot.ts b/src/bot.ts index 413b59b..0d4f458 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -16,6 +16,7 @@ import * as roleSelection from "./modules/roleSelection"; import * as sudo from "./modules/sudo"; import * as misc from "./modules/misc"; import * as galleryChannels from "./modules/galleryChannels"; +import * as voiceThreads from "./modules/voiceThreads"; import * as populate from "./modules/populate"; for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { @@ -41,6 +42,7 @@ const client = new Discord.Client({ intents: [ Discord.Intents.FLAGS.GUILDS, Discord.Intents.FLAGS.GUILD_MESSAGES, + Discord.Intents.FLAGS.GUILD_VOICE_STATES, ], }); @@ -49,6 +51,7 @@ const commandProviders: CommandProvider[] = [ sudo.provideCommands, misc.provideCommands, galleryChannels.provideCommands, + voiceThreads.provideCommands, ]; const commandPermissions: { [command: string]: CommandPermission } = {}; @@ -262,4 +265,25 @@ client.on("messageCreate", async (message) => { await galleryChannels.handleMessage(message, prisma); }); +client.on("voiceStateUpdate", async (oldState, newState) => { + if (oldState.channelId === newState.channelId) { + return; + } + if (oldState.channelId !== null) { + try { + await voiceThreads.handleVoiceLeave(oldState, prisma); + } catch (e) { + console.error("Someone left a VC, GONE WRONG!!!:", e.message); + } + } + + if (newState.channelId !== null) { + try { + await voiceThreads.handleVoiceJoin(newState, prisma); + } catch (e) { + console.error("Someone joined a VC, GONE WRONG!!!:", e.message); + } + } +}); + client.login(DISCORD_TOKEN); diff --git a/src/modules/voiceThreads.ts b/src/modules/voiceThreads.ts new file mode 100644 index 0000000..00a48ba --- /dev/null +++ b/src/modules/voiceThreads.ts @@ -0,0 +1,207 @@ +// Controller for vc-chat threads + +import { PrismaClient } from "@prisma/client"; +import * as Discord from "discord.js"; +import * as Builders from "@discordjs/builders"; + +import { CommandDescriptor } from "../bot.d"; + +const VC_CHAT_KEY = "voice_threads:vc_chat"; + +export function provideCommands(): CommandDescriptor[] { + const cmd = new Builders.SlashCommandBuilder() + .setName("voice-threads") + .setDescription("Manage voice threads"); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("get-vc-chat") + .setDescription( + "Shows the currently set VC Chat under which threads will be created" + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-vc-chat") + .setDescription( + "Set under which channel voice threads will be created" + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription("New vc-chat TEXT channel") + .setRequired(true) + ) + ); + return [{ builder: cmd, handler: handleCommand }]; +} + +export async function handleCommand( + interaction: Discord.CommandInteraction, + prisma: PrismaClient +): Promise { + switch (interaction.options.getSubcommand()) { + case "get-vc-chat": { + try { + const channel = ( + await prisma.config.findFirst({ + where: { key: VC_CHAT_KEY }, + }) + )?.value; + + if (channel === undefined) { + await interaction.editReply( + "❌ No channel is currently set as vc-chat." + ); + } else { + await interaction.editReply( + `Current vc-chat: <#${channel}>` + ); + } + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + break; + } + case "set-vc-chat": { + try { + const channel = interaction.options.getChannel( + "channel", + true + ) as Discord.GuildChannel; + + if (!channel.isText()) { + await interaction.editReply( + "❌ Channel must be a text channel." + ); + } else { + await prisma.config.upsert({ + where: { key: VC_CHAT_KEY }, + update: { value: channel.id }, + create: { + key: VC_CHAT_KEY, + value: channel.id, + }, + }); + await interaction.editReply( + `✅ Successfully set vc-chat to <#${channel.id}>.` + ); + } + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + break; + } + } +} + +async function fetchVCChat( + prisma: PrismaClient, + guild: Discord.Guild +): Promise { + const vcChatId = ( + await prisma.config.findFirst({ where: { key: VC_CHAT_KEY } }) + )?.value; + if (vcChatId === undefined) { + return null; + } + + return (await guild.channels.fetch(vcChatId)) as Discord.TextChannel | null; +} + +function normalizeVCName(name: string): string { + return ( + name + .normalize("NFD") + .replace(/\p{Diacritic}/gu, "") + .toLowerCase() + .replace(/\s/g, "-") + .replace(/[^a-z0-9-]/g, "") + .replace(/-{2,}/g, "-") + "-chat" + ); +} + +export async function handleVoiceJoin( + newState: Discord.VoiceState, + prisma: PrismaClient +): Promise { + if (newState.channel === null || newState.member === null) { + return; + } + + const vcChat = await fetchVCChat(prisma, newState.guild); + if (vcChat === null) { + return; + } + + const threadName = normalizeVCName(newState.channel.name); + const threadType = newState.guild.features.includes("PRIVATE_THREADS") + ? "GUILD_PRIVATE_THREAD" + : "GUILD_PUBLIC_THREAD"; + + const threads = (await vcChat.threads.fetch()).threads; + const td = threads.filter( + (t) => t.type === threadType && t.name === threadName + ); + + const reason = `${newState.member.user.tag} joined voice channel ${newState.channel.name}`; + + if (!td.size) { + const newThread = await vcChat.threads.create({ + name: threadName, + autoArchiveDuration: 60, + type: threadType, + reason, + }); + td.set(newThread.id, newThread); + } + // can't do td[0] so need to iterate + for (const [_id, thread] of td) { + await thread.members.add( + newState.member as Discord.GuildMember, + reason + ); + } +} + +export async function handleVoiceLeave( + oldState: Discord.VoiceState, + prisma: PrismaClient +): Promise { + if (oldState.channel === null || oldState.member === null) { + return; + } + + const vcChat = await fetchVCChat(prisma, oldState.guild); + if (vcChat === null) { + return; + } + + const threadName = normalizeVCName(oldState.channel.name); + const threadType = oldState.guild.features.includes("PRIVATE_THREADS") + ? "GUILD_PRIVATE_THREAD" + : "GUILD_PUBLIC_THREAD"; + + const threads = (await vcChat.threads.fetch()).threads; + const td = threads.filter( + (t) => t.type === threadType && t.name === threadName + ); + + const removeThread = + oldState.channel.members.filter( + (m) => m.id !== oldState.member?.id && !m.user.bot + ).size <= 0; + + // can't do td[0] so need to iterate + for (const [_id, thread] of td) { + if (removeThread) { + await thread.delete( + `Everyone left voice channel ${oldState.channel.name}` + ); + } else { + await thread.members.remove( + (oldState.member as Discord.GuildMember).id, + `${oldState.member.user.tag} left voice channel ${oldState.channel.name}` + ); + } + } +} From 80e5f7bfcb84d0dd2ce25a5e71217b7b5c226d6a Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Mon, 6 Sep 2021 21:13:12 +0100 Subject: [PATCH 040/101] remove populate module --- src/bot.ts | 8 ---- src/modules/populate.ts | 85 ----------------------------------------- 2 files changed, 93 deletions(-) delete mode 100644 src/modules/populate.ts diff --git a/src/bot.ts b/src/bot.ts index 0d4f458..0f2c037 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -17,7 +17,6 @@ import * as sudo from "./modules/sudo"; import * as misc from "./modules/misc"; import * as galleryChannels from "./modules/galleryChannels"; import * as voiceThreads from "./modules/voiceThreads"; -import * as populate from "./modules/populate"; for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { if (process.env[ev] === undefined) { @@ -81,13 +80,6 @@ const startupChores: Chore[] = [ ), complete: "All attendance polls scheduled", }, - { - summary: "Populate database with mock/default/test data", - fn: async () => { - await populate.populateDatabase(prisma); - }, - complete: "Database fully populated with mock/default/test data", - }, { summary: "Send role selection messages", fn: async () => { diff --git a/src/modules/populate.ts b/src/modules/populate.ts deleted file mode 100644 index 7deee28..0000000 --- a/src/modules/populate.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Populate the database with mock/test/default values - -import { PrismaClient } from "@prisma/client"; - -const populators = { - degree_selection: async (prisma: PrismaClient) => { - await prisma.roleGroup.create({ - data: { - id: "degree", - mode: "menu", - placeholder: "Escolhe o teu curso", - message: - "Olá <@97446650548588544>!\n\n||(isto é uma mensagem)||", - channelId: "859896451270574082", - options: { - create: [ - { - label: "LEIC-A", - description: - "Licenciatura em Engenharia Informática e de Computadores - Alameda", - value: "876961096253206542", - emoji: "💻", - }, - { - label: "LEIC-T", - description: - "Licenciatura em Engenharia Informática e de Computadores - Taguspark", - value: "876961212590587914", - emoji: "🇹", - }, - { - label: "LEFT", - description: - "Licenciatura em Engenharia Física e Tecnológica", - value: "876961271667372073", - emoji: "⚛️", - }, - ], - }, - }, - }); - }, - tourist: async (prisma: PrismaClient) => { - const configs = { - channel_id: "859896451270574082", - message_id: null, - message: "If you're a ~~terrible person~~ tourIST click below", - label: "I'm not in IST", - role_id: "881601009300959282", - exclusive_role_groups: "degree,year", - }; - - const items = []; - for (const [key, value] of Object.entries(configs)) { - const fqkey = `tourist:${key}`; - if (value) { - items.push( - prisma.config.upsert({ - where: { key: fqkey }, - create: { key: fqkey, value }, - update: { value }, - }) - ); - } - } - await prisma.$transaction(items); - }, -}; - -export async function populateDatabase(prisma: PrismaClient): Promise { - for (const [key, fn] of Object.entries(populators)) { - const fqkey = `populated:${key}`; - if ( - (await prisma.config.findFirst({ where: { key: fqkey } })) - ?.value !== "yes" - ) { - await fn(prisma); - await prisma.config.upsert({ - where: { key: fqkey }, - create: { key: fqkey, value: "yes" }, - update: { value: "yes" }, - }); - } - } -} From 0626c4c90d2991e69897f63045400a04853765aa Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Tue, 7 Sep 2021 01:17:52 +0100 Subject: [PATCH 041/101] fix dockerfile so migrations can run --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index ff044c4..b26fada 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,8 @@ WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn -COPY ./src/prisma/schema.prisma . -RUN yarn run prisma migrate deploy --schema=./schema.prisma -RUN yarn run prisma generate --schema=./schema.prisma +COPY ./src/prisma ./prisma +RUN yarn run prisma migrate deploy --schema=./prisma/schema.prisma +RUN yarn run prisma generate --schema=./prisma/schema.prisma COPY ./dist ./dist CMD [ "yarn", "start" ] From 40261c2c522edaa579ec2de28ee1321d60922518 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Tue, 7 Sep 2021 16:56:40 +0100 Subject: [PATCH 042/101] fix \n replace on role selection --- src/modules/roleSelection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 7c53f14..b03f297 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -574,7 +574,7 @@ async function createGroup( ]; } - message = message.replace("\\n", "\n"); + message = message.replace(/\\n/g, "\n"); if (!channel.isText()) { return [false, "Invalid channel: must be a text channel"]; From 6b75708b15d1603fb35e39160589a76c6bdc9788 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Tue, 7 Sep 2021 17:02:58 +0100 Subject: [PATCH 043/101] allow \n in tourist info --- src/modules/roleSelection.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index b03f297..8c2bb30 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -1020,10 +1020,9 @@ export async function handleCommand( "name", true ); - const value = interaction.options.getString( - "value", - true - ); + const value = interaction.options + .getString("value", true) + .replace(/\\n/g, "\n"); if (!["message", "label"].includes(name)) { await interaction.editReply("❌ Invalid name."); From 5bbf1c6b3cadc30196424427ec9bd9b558920304 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 7 Sep 2021 20:09:23 +0100 Subject: [PATCH 044/101] Fix wrong interaction options key on role-selection tourist set-info --- src/modules/roleSelection.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 8c2bb30..354351b 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -1016,19 +1016,19 @@ export async function handleCommand( switch (subCommand) { case "set-info": try { - const name = interaction.options.getString( - "name", + const field = interaction.options.getString( + "field", true ); const value = interaction.options .getString("value", true) .replace(/\\n/g, "\n"); - if (!["message", "label"].includes(name)) { + if (!["message", "label"].includes(field)) { await interaction.editReply("❌ Invalid name."); } - const fqkey = `tourist:${name}`; + const fqkey = `tourist:${field}`; await prisma.config.upsert({ where: { key: fqkey }, @@ -1037,7 +1037,7 @@ export async function handleCommand( }); await interaction.editReply( - `✅ Successfully set TourIST ${name}.` + `✅ Successfully set TourIST ${field}.` ); } catch (e) { await interaction.editReply( From ab107dbbf295cca5b61af169f310db021dd247d4 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Tue, 7 Sep 2021 20:10:55 +0100 Subject: [PATCH 045/101] Make docker work (#65) Co-authored-by: Diogo Correia --- Dockerfile | 21 +++++++++++++++------ README.md | 2 +- package.json | 3 ++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index b26fada..d172375 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,21 @@ -FROM node:16.6.1-alpine3.14 +FROM node:16.6.1-alpine3.14 as ts-compiler ARG DATABASE_URL ENV DATABASE_URL ${DATABASE_URL} WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn -COPY ./src/prisma ./prisma -RUN yarn run prisma migrate deploy --schema=./prisma/schema.prisma -RUN yarn run prisma generate --schema=./prisma/schema.prisma -COPY ./dist ./dist -CMD [ "yarn", "start" ] +COPY ./ ./ +RUN yarn build + +FROM node:16.6.1-alpine3.14 +ARG DATABASE_URL +ENV DATABASE_URL ${DATABASE_URL} +WORKDIR /app +COPY --from=ts-compiler /app/package.json . +COPY --from=ts-compiler /app/yarn.lock . +COPY --from=ts-compiler /app/src/prisma ./src/prisma +COPY --from=ts-compiler /app/dist ./dist +RUN yarn install --prod --frozen-lockfile +RUN yarn run prisma generate +CMD [ "yarn", "start:docker" ] diff --git a/README.md b/README.md index 1f8de80..0453856 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ services: build: context: . args: - DATABASE_URL: file:./data/bot.db + DATABASE_URL: file:/app/data/bot.db ## END; volumes: - type: bind diff --git a/package.json b/package.json index 837efda..4d8a6f0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "scripts": { "prepare": "husky install", "build": "tsc", - "start": "node ." + "start": "node .", + "start:docker": "prisma migrate deploy && node ." }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" From 9bdaa3370fd73290c4822fe125b4aab7bee545ab Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Tue, 7 Sep 2021 20:13:09 +0100 Subject: [PATCH 046/101] Control attendance polls with commands (#64) --- src/bot.d.ts | 3 +- src/bot.ts | 11 +- src/modules/attendance.ts | 208 ++++++++++++++++++++++++++++++++- src/modules/galleryChannels.ts | 5 +- src/modules/misc.ts | 9 +- src/modules/roleSelection.ts | 7 +- 6 files changed, 218 insertions(+), 25 deletions(-) diff --git a/src/bot.d.ts b/src/bot.d.ts index e8a283d..57b320d 100644 --- a/src/bot.d.ts +++ b/src/bot.d.ts @@ -6,8 +6,7 @@ import { SlashCommandBuilder } from "@discordjs/builders"; export type InteractionHandler = ( interaction: T, - prisma: PrismaClient, - client: Discord.Client + prisma: PrismaClient ) => Promise; export type InteractionHandlers = { diff --git a/src/bot.ts b/src/bot.ts index 0f2c037..fef3dbd 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -46,6 +46,7 @@ const client = new Discord.Client({ }); const commandProviders: CommandProvider[] = [ + attendance.provideCommands, roleSelection.provideCommands, sudo.provideCommands, misc.provideCommands, @@ -72,6 +73,7 @@ const startupChores: Chore[] = [ fn: async () => await attendance.scheduleAttendancePolls( client, + prisma, await prisma.attendancePoll.findMany({ where: { type: "scheduled", @@ -213,14 +215,12 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { if (interaction.isButton()) { await buttonHandlers[prefix]?.( interaction as Discord.ButtonInteraction, - prisma, - client + prisma ); } else if (interaction.isSelectMenu()) { await menuHandlers[prefix]?.( interaction as Discord.SelectMenuInteraction, - prisma, - client + prisma ); } } else if (interaction.isCommand()) { @@ -244,8 +244,7 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { await commandHandlers[interaction.commandName]?.( interaction, - prisma, - client + prisma ); } } catch (e) { diff --git a/src/modules/attendance.ts b/src/modules/attendance.ts index e588769..3f914a1 100644 --- a/src/modules/attendance.ts +++ b/src/modules/attendance.ts @@ -3,6 +3,7 @@ import { ButtonInteraction, Client, + CommandInteraction, Message, MessageActionRow, MessageButton, @@ -10,9 +11,11 @@ import { Snowflake, TextChannel, } from "discord.js"; +import * as Builders from "@discordjs/builders"; import cron from "node-cron"; -import { AttendancePoll } from "@prisma/client"; +import { PrismaClient, AttendancePoll } from "@prisma/client"; +import { CommandDescriptor } from "../bot.d"; const ATTENDANCE_POLL_MSG = "Attendance Poll"; const ATTENDANCE_POLL_ACTION_ROW = new MessageActionRow(); @@ -120,6 +123,7 @@ export const sendAttendanceEmbed = async ( export const scheduleAttendancePolls = async ( client: Client, + prisma: PrismaClient, polls: AttendancePoll[] ): Promise => { await Promise.all( @@ -135,9 +139,205 @@ export const scheduleAttendancePolls = async ( return; } - cron.schedule(poll.cron, () => - sendAttendanceEmbed(poll, channel as TextChannel) - ); + cron.schedule(poll.cron, async () => { + try { + // make sure it wasn't deleted / edited in the meantime + const p = await prisma.attendancePoll.findFirst({ + where: { id: poll.id }, + }); + if (p !== null) { + await sendAttendanceEmbed(p, channel as TextChannel); + } + } catch (e) { + console.error( + "Could not verify (& send) attendance poll:", + e.message + ); + } + }); }) ); }; + +export function provideCommands(): CommandDescriptor[] { + const cmd = new Builders.SlashCommandBuilder() + .setName("attendance") + .setDescription("Manage attendance polls"); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("add") + .setDescription("Create a new scheduled attendance poll") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("id") + .setDescription("Unique identifier") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("title") + .setDescription("Attendance poll title") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("cron") + .setDescription( + "Cron schedule string; BE VERY CAREFUL THIS IS CORRECT!" + ) + .setRequired(true) + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription("Where polls will be sent") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("remove") + .setDescription("Remove an existing attendance poll") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("id") + .setDescription("Unique identifier") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("list") + .setDescription("List existing attendance polls") + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("info") + .setDescription("Get information for an existing attendance poll") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("id") + .setDescription("Unique identifier") + .setRequired(true) + ) + ); + return [{ builder: cmd, handler: handleCommand }]; +} + +export async function handleCommand( + interaction: CommandInteraction, + prisma: PrismaClient +): Promise { + switch (interaction.options.getSubcommand()) { + case "add": { + try { + const id = interaction.options.getString("id", true); + const title = interaction.options.getString("title", true); + const cron = interaction.options.getString("cron", true); + const channel = interaction.options.getChannel("channel", true); + + // TODO: don't take this at face value + // ^ how important is this? in principle admins won't mess up + + const poll = await prisma.attendancePoll.create({ + data: { + id, + type: "scheduled", + title, + cron, + channelId: channel.id, + }, + }); + + await scheduleAttendancePolls(interaction.client, prisma, [ + poll, + ]); + + await interaction.editReply( + "✅ Successfully added and scheduled." + ); + } catch (e) { + await interaction.editReply( + "❌ Something went wrong, maybe the ID already exists?" + ); + } + + break; + } + case "remove": { + try { + const id = interaction.options.getString("id", true); + + await prisma.attendancePoll.delete({ where: { id } }); + + await interaction.editReply("✅ Successfully removed."); + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + + break; + } + case "list": { + try { + const polls = await prisma.attendancePoll.findMany(); + await interaction.editReply({ + embeds: [ + new MessageEmbed() + .setTitle("Attendance Polls") + .setDescription( + polls.length + ? "Below is a list of all attendance polls with their title and ID" + : "No attendance polls found" + ) + .addFields( + polls.map((p) => ({ + name: p.title, + value: p.id, + inline: true, + })) + ), + ], + }); + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + + break; + } + case "info": { + try { + const id = interaction.options.getString("id", true); + + const poll = await prisma.attendancePoll.findFirst({ + where: { id }, + }); + + if (poll === null) { + await interaction.editReply( + "❌ No poll found with that ID." + ); + } else { + await interaction.editReply({ + embeds: [ + new MessageEmbed() + .setTitle("Attendance Poll Information") + .addField("ID", poll.id, true) + .addField("Type", poll.type, true) + .addField("Title", poll.title, true) + .addField("Cron Schedule", poll.cron, true) + .addField( + "Channel", + `<#${poll.channelId}>`, + true + ), + ], + }); + } + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } + + break; + } + } +} diff --git a/src/modules/galleryChannels.ts b/src/modules/galleryChannels.ts index 236347f..4c3fb5b 100644 --- a/src/modules/galleryChannels.ts +++ b/src/modules/galleryChannels.ts @@ -142,8 +142,7 @@ async function updateGalleries( export async function handleCommand( interaction: Discord.CommandInteraction, - prisma: PrismaClient, - client: Discord.Client + prisma: PrismaClient ): Promise { const galleries = await utils.fetchGalleries(prisma); @@ -215,7 +214,7 @@ export async function handleCommand( !(channel.isText() || channel.isThread()) ) { for (const c of galleries) { - const cf = await client.channels.fetch(c); + const cf = await interaction.client.channels.fetch(c); if (cf && (cf.isText() || cf.isThread())) { channels.push( cf as diff --git a/src/modules/misc.ts b/src/modules/misc.ts index 19c8692..9a40a09 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -5,8 +5,6 @@ import * as fs from "fs"; import * as Discord from "discord.js"; import * as Builders from "@discordjs/builders"; -import { PrismaClient } from "@prisma/client"; - import { CommandDescriptor } from "../bot.d"; import { CommandPermission } from "../bot"; import * as utils from "./utils"; @@ -24,9 +22,7 @@ export function provideCommands(): CommandDescriptor[] { } export async function handleAboutCommand( - interaction: Discord.CommandInteraction, - _prisma: PrismaClient, - client: Discord.Client + interaction: Discord.CommandInteraction ): Promise { let pkg: Record; try { @@ -62,7 +58,8 @@ export async function handleAboutCommand( })) ) .setFooter( - "Uptime: " + utils.durationString(client.uptime ?? 0) + "Uptime: " + + utils.durationString(interaction.client.uptime ?? 0) ), ], }); diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 354351b..cc47dc8 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -808,8 +808,7 @@ async function removeOption( // TODO: dry this a bit export async function handleCommand( interaction: Discord.CommandInteraction, - prisma: PrismaClient, - client: Discord.Client + prisma: PrismaClient ): Promise { const subCommandGroup = interaction.options.getSubcommandGroup(); const subCommand = interaction.options.getSubcommand(); @@ -988,7 +987,7 @@ export async function handleCommand( case "send-messages": { try { await sendRoleSelectionMessages( - client, + interaction.client, prisma, interaction.options.getBoolean( "edit-existing", @@ -1089,7 +1088,7 @@ export async function handleCommand( }); await interaction.editReply( - `✅ Successfully set TourIST role.` + "✅ Successfully set TourIST role." ); } catch (e) { await interaction.editReply( From fd04d24bbad37889d40f222103c50f187eb4e61a Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Wed, 8 Sep 2021 00:03:06 +0100 Subject: [PATCH 047/101] Fix docker image not building from scratch --- .dockerignore | 4 ++++ Dockerfile | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ee6d215 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules/ +data/ +dist/ +.husky/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d172375..b96ee02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,9 @@ ENV DATABASE_URL ${DATABASE_URL} WORKDIR /app COPY package.json . COPY yarn.lock . -RUN yarn +RUN yarn install --frozen-lockfile COPY ./ ./ +RUN yarn run prisma generate RUN yarn build FROM node:16.6.1-alpine3.14 From de11af14fc415f151870fb91ad62030d3d272325 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Wed, 8 Sep 2021 00:09:43 +0100 Subject: [PATCH 048/101] Add docker image publish github action --- .github/workflows/publish-docker-image.yml | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/publish-docker-image.yml diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml new file mode 100644 index 0000000..37539af --- /dev/null +++ b/.github/workflows/publish-docker-image.yml @@ -0,0 +1,53 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Create and publish a Docker image + +on: + release: + types: [published] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From dee1366fd04e0b05f8f7ae2aaad193a72acfd9e0 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Wed, 8 Sep 2021 13:47:42 +0100 Subject: [PATCH 049/101] Fix attendance response reply not being sent --- src/modules/attendance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/attendance.ts b/src/modules/attendance.ts index 3f914a1..032c74d 100644 --- a/src/modules/attendance.ts +++ b/src/modules/attendance.ts @@ -63,7 +63,7 @@ export const handleAttendanceButton = async ( components: [ATTENDANCE_POLL_ACTION_ROW], }); - await interaction.reply({ content: "Response recorded!", ephemeral: true }); + await interaction.editReply("Response recorded!"); }; export const getNewEmbed = ( From b9a893487576c0657cbd826807c5bc6c22bb7d57 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Wed, 8 Sep 2021 13:55:19 +0100 Subject: [PATCH 050/101] Bump version to v2.0.1 --- README.md | 13 +++++++++---- package.json | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0453856..a829e78 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # IST Discord Bot +![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/ist-bot-team/ist-discord-bot?label=version) +[![Discord](https://img.shields.io/discord/759576132227694642?label=discord&logo=discord)](https://discord.leic.pt) +![GitHub](https://img.shields.io/github/license/ist-bot-team/ist-discord-bot) + Discord bot to manage the IST Hub server -- join [here](https://discord.leic.pt). ### Running +#### Production + 1. Create a `docker-compose.yml` file as below: ```yaml @@ -12,7 +18,7 @@ version: "3.8" services: ist-discord-bot: ## EITHER: - image: ist-bot-team/ist-discord-bot:v2.0.0 + image: ghcr.io/ist-bot-team/ist-discord-bot:2 ## OR: build: context: . @@ -33,9 +39,8 @@ services: ``` 2. Create a folder named `data` for Docker to store things in -3. Run `yarn` and `yarn run build` if you're using the source code -4. Run `docker-compose up -d --build` -5. That's it! +3. Run `docker-compose up -d --build` +4. That's it! _You can also use `docker-compose down`, `docker-compose up`, `docker-compose restart` and `docker-compose logs [-f]`._ diff --git a/package.json b/package.json index 4d8a6f0..af7b74d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.0.0", + "version": "2.0.1", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From de87eb40d319154a8a534f8d962958ee4de1a649 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Thu, 9 Sep 2021 13:39:16 +0100 Subject: [PATCH 051/101] Welcome module (#67) --- src/bot.ts | 19 ++++- src/modules/roleSelection.ts | 12 ++- src/modules/welcome.ts | 140 +++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 src/modules/welcome.ts diff --git a/src/bot.ts b/src/bot.ts index fef3dbd..93b6b1e 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -17,6 +17,7 @@ import * as sudo from "./modules/sudo"; import * as misc from "./modules/misc"; import * as galleryChannels from "./modules/galleryChannels"; import * as voiceThreads from "./modules/voiceThreads"; +import * as welcome from "./modules/welcome"; for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { if (process.env[ev] === undefined) { @@ -42,6 +43,7 @@ const client = new Discord.Client({ Discord.Intents.FLAGS.GUILDS, Discord.Intents.FLAGS.GUILD_MESSAGES, Discord.Intents.FLAGS.GUILD_VOICE_STATES, + Discord.Intents.FLAGS.GUILD_MEMBERS, // THIS IS A PRIVILEGED INTENT! MANUAL ACTION REQUIRED TO ENABLE! ], }); @@ -52,6 +54,7 @@ const commandProviders: CommandProvider[] = [ misc.provideCommands, galleryChannels.provideCommands, voiceThreads.provideCommands, + welcome.provideCommands, ]; const commandPermissions: { [command: string]: CommandPermission } = {}; @@ -248,7 +251,7 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { ); } } catch (e) { - console.error("Problem handling interaction: " + e.message); + console.error("Problem handling interaction: " + (e as Error).message); } }); @@ -264,7 +267,10 @@ client.on("voiceStateUpdate", async (oldState, newState) => { try { await voiceThreads.handleVoiceLeave(oldState, prisma); } catch (e) { - console.error("Someone left a VC, GONE WRONG!!!:", e.message); + console.error( + "Someone left a VC, GONE WRONG!!!:", + (e as Error).message + ); } } @@ -272,9 +278,16 @@ client.on("voiceStateUpdate", async (oldState, newState) => { try { await voiceThreads.handleVoiceJoin(newState, prisma); } catch (e) { - console.error("Someone joined a VC, GONE WRONG!!!:", e.message); + console.error( + "Someone joined a VC, GONE WRONG!!!:", + (e as Error).message + ); } } }); +client.on("guildMemberAdd", async (member) => { + await welcome.handleGuildJoin(member, prisma); +}); + client.login(DISCORD_TOKEN); diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index cc47dc8..26ea621 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -51,7 +51,9 @@ export async function sendRoleSelectionMessages( ], }); } catch (e) { - console.error(`Failed to inject tourist group: ${e.message}`); + console.error( + `Failed to inject tourist group: ${(e as Error).message}` + ); } for (const group of groups) { @@ -169,7 +171,9 @@ export async function sendRoleSelectionMessages( } } catch (e) { console.error( - `Could not send role selection message for group ${group.id} because: ${e.message}` + `Could not send role selection message for group ${ + group.id + } because: ${(e as Error).message}` ); } } @@ -928,7 +932,7 @@ export async function handleCommand( } catch (e) { console.error( "Could not list role groups because: ", - e.message + (e as Error).message ); await interaction.editReply( "❌ Failed to list role groups." @@ -1001,7 +1005,7 @@ export async function handleCommand( } catch (e) { console.error( "Could not send role selection messages:", - e.message + (e as Error).message ); await interaction.editReply( "❌ Failed to send role selection messages." diff --git a/src/modules/welcome.ts b/src/modules/welcome.ts new file mode 100644 index 0000000..1a5cd4c --- /dev/null +++ b/src/modules/welcome.ts @@ -0,0 +1,140 @@ +// Send welcome message to new users + +import { PrismaClient } from "@prisma/client"; +import * as Discord from "discord.js"; +import * as Builders from "@discordjs/builders"; + +import { CommandDescriptor } from "../bot.d"; + +async function getWelcomeChannel( + prisma: PrismaClient, + client: Discord.Client +): Promise { + const id = ( + await prisma.config.findFirst({ + where: { key: "welcome:channel_id" }, + }) + )?.value; + if (id === undefined) { + return null; + } + return (await client.channels.fetch(id)) as Discord.TextChannel | null; +} + +export async function handleGuildJoin( + member: Discord.GuildMember, + prisma: PrismaClient +): Promise { + const channel = await getWelcomeChannel(prisma, member.client); + const message = ( + await prisma.config.findFirst({ where: { key: "welcome:message" } }) + )?.value; + if (channel !== null && message != undefined) { + channel.send(message.replace(/\$USER/g, member.user.toString())); + } +} + +export function provideCommands(): CommandDescriptor[] { + const cmd = new Builders.SlashCommandBuilder() + .setName("welcome") + .setDescription("Send a welcome message when someone joins the server"); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-channel") + .setDescription("Set where messages will be sent") + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription("Where welcome messages will be sent") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-message") + .setDescription("Set which message will be sent when someone joins") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("message") + .setDescription( + "Message that will be sent; use $USER to tag the new member" + ) + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("view") + .setDescription("View welcome message settings") + ); + return [{ builder: cmd, handler: handleCommand }]; +} + +export async function handleCommand( + interaction: Discord.CommandInteraction, + prisma: PrismaClient +): Promise { + try { + switch (interaction.options.getSubcommand()) { + case "set-channel": { + const channel = interaction.options.getChannel( + "channel", + true + ) as Discord.GuildChannel; + if (!channel.isText()) { + await interaction.editReply( + "❌ Channel must be a text channel." + ); + } else { + await prisma.config.upsert({ + where: { key: "welcome:channel_id" }, + update: { value: channel.id }, + create: { + key: "welcome:channel_id", + value: channel.id, + }, + }); + await interaction.editReply( + `✅ Welcome channel successfully set as <#${channel.id}>.` + ); + } + break; + } + case "set-message": { + const message = interaction.options.getString("message", true); + const value = message.replace(/\\n/g, "\n"); + await prisma.config.upsert({ + where: { key: "welcome:message" }, + update: { value }, + create: { key: "welcome:message", value }, + }); + await interaction.editReply( + `✅ Welcome message successfully set.` + ); + break; + } + case "view": { + const getSetting = async (s: string) => + ( + await prisma.config.findFirst({ + where: { key: `welcome:${s}` }, + }) + )?.value; + const message = (await getSetting("message")) ?? "[NONE]"; + const channelId = await getSetting("channel"); + const channel = channelId ? `<#${channelId}>` : "[NONE]"; + await interaction.editReply({ + embeds: [ + new Discord.MessageEmbed() + .setTitle("Welcome Message Settings") + .setDescription(`**Message:** ${message}`) + .addField("Channel", channel, true), + ], + }); + break; + } + } + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } +} From 1b472ef38a540b8af5b8b176d6fd86e6041ef160 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 9 Sep 2021 13:42:58 +0100 Subject: [PATCH 052/101] Bump version to v2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af7b74d..d4a93a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.0.1", + "version": "2.1.0", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From 6e4dfa9c8cf2ded2e5410257cb0d44afc68d9482 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:30:03 +0100 Subject: [PATCH 053/101] Multiselect support (#68) --- src/modules/roleSelection.ts | 136 ++++++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 9 deletions(-) diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 26ea621..ff8be4c 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -77,6 +77,14 @@ export async function sendRoleSelectionMessages( let components; if (group.mode === "menu") { + if (group.options.length < (group.maxValues ?? 1)) { + throw new Error( + `Requires at least ${ + group.maxValues ?? 1 + } options, but got ${group.options.length}` + ); + } + components = [ new Discord.MessageActionRow().addComponents( new Discord.MessageSelectMenu() @@ -182,18 +190,18 @@ export async function sendRoleSelectionMessages( async function handleRoleSelection( groupId: string, roles: Discord.GuildMemberRoleManager, - roleToAdd: string, + selectedRoles: string[], prisma: PrismaClient ) { const touristExclusive = ( (await getConfigFactory(prisma, "tourist")("exclusive_role_groups")) ?? "degree,year" - ).split(","); + ).split(","); // TODO: allow changing this config with commands const groupRoles = groupId === TOURIST_GROUP_ID ? [ - roleToAdd, + selectedRoles, ...( await prisma.roleGroup.findMany({ where: { @@ -223,8 +231,8 @@ async function handleRoleSelection( ); try { - if (groupRoles.includes(roleToAdd)) { - const rolesToSet = [roleToAdd]; + if (selectedRoles.every((r) => groupRoles.includes(r))) { + const rolesToSet = [...selectedRoles]; for (const id of roles.cache.keys()) { if (!groupRoles.includes(id)) { rolesToSet.push(id); @@ -248,9 +256,8 @@ export async function handleRoleSelectionMenu( const groupId = interaction.customId.split(":")[1]; const roles = interaction.member?.roles as Discord.GuildMemberRoleManager; - const roleToAdd = interaction.values[0]; // FIXME: this won't work for multiselects! - if (await handleRoleSelection(groupId, roles, roleToAdd, prisma)) { + if (await handleRoleSelection(groupId, roles, interaction.values, prisma)) { await interaction.editReply("✅ Role applied."); } else { await interaction.editReply("❌ Failed to apply role."); @@ -266,7 +273,7 @@ export async function handleRoleSelectionButton( const sp = interaction.customId.split(":"); const roles = interaction.member?.roles as Discord.GuildMemberRoleManager; - if (await handleRoleSelection(sp[1], roles, sp[2], prisma)) { + if (await handleRoleSelection(sp[1], roles, [sp[2]], prisma)) { await interaction.editReply("✅ Role applied."); } else { await interaction.editReply("❌ Failed to apply role."); @@ -313,7 +320,6 @@ export function provideCommands(): CommandDescriptor[] { .setDescription("Message sent with menu/buttons") .setRequired(true) ) - // FIXME: no multiselect support yet, so not asking for minValues/maxValues .addChannelOption( new Builders.SlashCommandChannelOption() .setName("channel") @@ -322,6 +328,22 @@ export function provideCommands(): CommandDescriptor[] { ) .setRequired(true) ) + .addIntegerOption( + new Builders.SlashCommandIntegerOption() + .setName("min") + .setDescription( + "At least how many options must be selected; default 1" + ) + .setRequired(false) + ) + .addIntegerOption( + new Builders.SlashCommandIntegerOption() + .setName("max") + .setDescription( + "At most how many options may be selected; default 1" + ) + .setRequired(false) + ) ) .addSubcommand( new Builders.SlashCommandSubcommandBuilder() @@ -374,6 +396,29 @@ export function provideCommands(): CommandDescriptor[] { .setRequired(true) ) ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-num") + .setDescription( + "Set how many options may be selected at once" + ) + .addIntegerOption( + new Builders.SlashCommandIntegerOption() + .setName("min") + .setDescription( + "At least how many options must be selected" + ) + .setRequired(true) + ) + .addIntegerOption( + new Builders.SlashCommandIntegerOption() + .setName("max") + .setDescription( + "At most how many options may be selected" + ) + .setRequired(true) + ) + ) .addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("move") @@ -563,11 +608,15 @@ async function createGroup( mode: string, placeholder: string, message: string, + minValues: number, + maxValues: number, channel: Discord.GuildChannel ): Promise<[boolean, string]> { try { if (!id.match(/^[a-z0-9_]+$/)) { return [false, "Invalid id: must be snake_case"]; + } else if (id === TOURIST_GROUP_ID) { + return [false, "Tourist group ID must be unique in the database"]; } const goodModes = ["buttons", "menu"]; @@ -580,6 +629,19 @@ async function createGroup( message = message.replace(/\\n/g, "\n"); + if ( + minValues < 0 || + minValues > 25 || + maxValues < 0 || + maxValues > 25 || + minValues > maxValues + ) { + return [ + false, + "Minimum and maximum number of options must be between 0 and 25 and min<=max", + ]; + } + if (!channel.isText()) { return [false, "Invalid channel: must be a text channel"]; } @@ -593,6 +655,8 @@ async function createGroup( mode, placeholder, message, + minValues, + maxValues, channelId: channel.id, }, }) @@ -657,6 +721,38 @@ async function editGroup( } } +async function setNumGroup( + prisma: PrismaClient, + id: string, + minValues: number, + maxValues: number +): Promise { + try { + if (!id.match(/^[a-z0-9_]+$/)) { + return "Invalid id: must be snake_case"; + } + + if ( + minValues < 0 || + minValues > 25 || + maxValues < 0 || + maxValues > 25 || + minValues > maxValues + ) { + return "Minimum and maximum number of options must be between 0 and 25 and min<=max"; + } + + await prisma.roleGroup.update({ + where: { id }, + data: { minValues, maxValues }, + }); + + return true; + } catch (e) { + return "Something went wrong; possibly, no role group was found with that ID"; + } +} + async function moveGroup( prisma: PrismaClient, id: string, @@ -699,6 +795,7 @@ async function viewGroup( **Mode:** ${group.mode} **Placeholder:** ${group.placeholder} **Message:** ${group.message} + **Value Constraints:** Min ${group.minValues}, Max ${group.maxValues} **Channel:** <#${group.channelId}> **Message:** ${ group.messageId @@ -826,6 +923,8 @@ export async function handleCommand( interaction.options.getString("mode", true), interaction.options.getString("placeholder", true), interaction.options.getString("message", true), + interaction.options.getInteger("min", false) ?? 1, + interaction.options.getInteger("max", false) ?? 1, interaction.options.getChannel( "channel", true @@ -881,6 +980,25 @@ export async function handleCommand( } break; } + case "set-num": { + const id = interaction.options.getString("id", true); + const res = await setNumGroup( + prisma, + id, + interaction.options.getInteger("min", true), + interaction.options.getInteger("max", true) + ); + if (res === true) { + await interaction.editReply( + `✅ Role selection group \`${id}\` successfully edited.` + ); + } else { + await interaction.editReply( + `❌ Could not edit group because: **${res}**` + ); + } + break; + } case "move": { const id = interaction.options.getString("id", true); const channel = interaction.options.getChannel( From 60bb3c03ec4c76457406fab135c56b260a9de324 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Thu, 16 Sep 2021 21:40:56 +0100 Subject: [PATCH 054/101] Important necessary commands (#69) Co-authored-by: Diogo Correia --- src/modules/misc.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++ src/modules/sudo.ts | 30 ++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/modules/misc.ts b/src/modules/misc.ts index 9a40a09..d3764b4 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -10,6 +10,29 @@ import { CommandPermission } from "../bot"; import * as utils from "./utils"; export function provideCommands(): CommandDescriptor[] { + const say = new Builders.SlashCommandBuilder() + .setName("say") + .setDescription("Sends a message to a channel"); + say.addStringOption( + new Builders.SlashCommandStringOption() + .setName("message") + .setDescription("What message to send") + .setRequired(true) + ); + say.addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("channel") + .setDescription("Where to send the message; defaults to current") + .setRequired(false) + ); + say.addBooleanOption( + new Builders.SlashCommandBooleanOption() + .setName("allow-mentions") + .setDescription( + "Whether to allow mentions in the message; defaults to false" + ) + .setRequired(false) + ); return [ { builder: new Builders.SlashCommandBuilder() @@ -18,6 +41,10 @@ export function provideCommands(): CommandDescriptor[] { handler: handleAboutCommand, permission: CommandPermission.Public, }, + { + builder: say, + handler: handleSayCommand, + }, ]; } @@ -64,3 +91,27 @@ export async function handleAboutCommand( ], }); } + +export async function handleSayCommand( + interaction: Discord.CommandInteraction +): Promise { + try { + const channel = (interaction.options.getChannel("channel", false) || + interaction.channel) as Discord.GuildChannel | null; + const message = interaction.options.getString("message", true); + const allowMentions = + interaction.options.getBoolean("allow-mentions", false) ?? false; + + if (channel && channel.isText()) { + await channel.send({ + content: message.replace(/\\n/g, "\n"), + allowedMentions: allowMentions ? undefined : { parse: [] }, + }); + await interaction.editReply("✅ Successfully sent message."); + return; + } + throw new Error("???"); + } catch (e) { + await interaction.editReply("❌ Something went wrong."); + } +} diff --git a/src/modules/sudo.ts b/src/modules/sudo.ts index f6116f2..4a576ca 100644 --- a/src/modules/sudo.ts +++ b/src/modules/sudo.ts @@ -7,11 +7,20 @@ import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; export function provideCommands(): CommandDescriptor[] { + const sudo = new Builders.SlashCommandBuilder() + .setName("sudo") + .setDescription("Toggle enhanced administrator permissions"); + sudo.addUserOption( + new Builders.SlashCommandUserOption() + .setName("target") + .setDescription( + "Must have permissions to run sudo themselves; defaults to self" + ) + .setRequired(false) + ); return [ { - builder: new Builders.SlashCommandBuilder() - .setName("sudo") - .setDescription("Toggle enhanced administrator permissions"), + builder: sudo, handler: handleSudoCommand, }, { @@ -29,10 +38,23 @@ export async function handleSudoCommand( interaction: Discord.CommandInteraction ): Promise { try { - const roles = interaction.member + const target = interaction.options.getMember( + "target", + false + ) as Discord.GuildMember | null; + + const roles = (target ?? interaction.member) ?.roles as Discord.GuildMemberRoleManager; + const aId = process.env.ADMIN_ID as string; const apId = process.env.ADMIN_PLUS_ID as string; + if (!roles.cache.has(aId) && !roles.cache.has(apId)) { + await interaction.editReply( + "❌ User does not have administrator permissions." + ); + return; + } + if (roles.cache.has(apId)) { await roles.remove(apId); await interaction.editReply( From b0534119550293d7eb8ac7eaf8e15cc7da4f7396 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 16 Sep 2021 21:47:28 +0100 Subject: [PATCH 055/101] Bump version to v2.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4a93a4..6e0b17d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.1.0", + "version": "2.2.0", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From 81a011802a57c94a23aa648f4c70a99d858f45ed Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Sat, 18 Sep 2021 15:43:45 +0100 Subject: [PATCH 056/101] Say logs (#70) --- src/modules/misc.ts | 49 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/modules/misc.ts b/src/modules/misc.ts index d3764b4..473ef84 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -8,6 +8,7 @@ import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; import { CommandPermission } from "../bot"; import * as utils from "./utils"; +import { SlashCommandBuilder } from "@discordjs/builders"; export function provideCommands(): CommandDescriptor[] { const say = new Builders.SlashCommandBuilder() @@ -33,6 +34,15 @@ export function provideCommands(): CommandDescriptor[] { ) .setRequired(false) ); + const whoSaid = new SlashCommandBuilder() + .setName("who-said") + .setDescription("Shows who ordered the bot to say something"); + whoSaid.addStringOption( + new Builders.SlashCommandStringOption() + .setName("message-id") + .setDescription("Message ID in question") + .setRequired(true) + ); return [ { builder: new Builders.SlashCommandBuilder() @@ -45,6 +55,10 @@ export function provideCommands(): CommandDescriptor[] { builder: say, handler: handleSayCommand, }, + { + builder: whoSaid, + handler: handleWhoSaidCommand, + }, ]; } @@ -92,6 +106,8 @@ export async function handleAboutCommand( }); } +const sayLogs: Discord.Collection = new Discord.Collection(); + export async function handleSayCommand( interaction: Discord.CommandInteraction ): Promise { @@ -103,10 +119,21 @@ export async function handleSayCommand( interaction.options.getBoolean("allow-mentions", false) ?? false; if (channel && channel.isText()) { - await channel.send({ + const msg = await channel.send({ content: message.replace(/\\n/g, "\n"), allowedMentions: allowMentions ? undefined : { parse: [] }, }); + const uid = interaction.member?.user.id; + if (uid) { + sayLogs.set(msg.id, uid); + console.log( + `User ${ + interaction.member?.user.username + } said «${message}» (w/${ + allowMentions ? "" : "o" + } mentions)` + ); + } await interaction.editReply("✅ Successfully sent message."); return; } @@ -115,3 +142,23 @@ export async function handleSayCommand( await interaction.editReply("❌ Something went wrong."); } } + +export async function handleWhoSaidCommand( + interaction: Discord.CommandInteraction +): Promise { + try { + const messageId = interaction.options.getString("message-id", true); + const split = messageId.split("-"); + const who = sayLogs.get(split[split.length - 1]); + + if (who) { + await interaction.editReply(`<@${who}> said it!`); + } else { + await interaction.editReply("I don't know who said it..."); + } + } catch (e) { + await interaction.editReply( + "❌ Something went wrong, maybe wrong message ID?" + ); + } +} From 993abbf9bb74af52ef826e6e46104d9c95f1cc10 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Sat, 18 Sep 2021 17:02:01 +0100 Subject: [PATCH 057/101] Dev Dockerfile (#73) --- .npmrc | 1 + Dockerfile.dev | 11 +++++++++++ README.md | 1 + package.json | 4 ++++ 4 files changed, 17 insertions(+) create mode 100644 .npmrc create mode 100644 Dockerfile.dev diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4fd0219 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..f38808b --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,11 @@ +FROM node:16.6.1-alpine3.14 +ARG DATABASE_URL +ENV DATABASE_URL ${DATABASE_URL} +WORKDIR /app +COPY package.json . +COPY yarn.lock . +RUN yarn install --frozen-lockfile +COPY ./ ./ +RUN yarn run prisma generate +RUN yarn build +CMD [ "yarn", "start:docker" ] diff --git a/README.md b/README.md index a829e78..421c998 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ services: ## OR: build: context: . + dockerfile: Dockerfile.dev args: DATABASE_URL: file:/app/data/bot.db ## END; diff --git a/package.json b/package.json index 6e0b17d..934ba0e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,10 @@ "repository": "git@github.com:ist-bot-team/ist-discord-bot", "author": "IST Bot Team", "license": "MIT", + "engines": { + "node": ">=16.6.1", + "npm": ">=7.23.0" + }, "dependencies": { "@discordjs/builders": "^0.6.0", "@discordjs/rest": "^0.1.0-canary.0", From 3fa116b96fde5a63ec33c5d555bf3e5b5f3cff1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Fonseca?= Date: Sat, 18 Sep 2021 17:22:53 +0100 Subject: [PATCH 058/101] Ignore shell.nix --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2d92b43..d51f73d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ data/ docker-compose.yml .vscode + +shell.nix From cf8d9bcba73704d5b5c6f16494ade0aafae54d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Fonseca?= Date: Wed, 22 Sep 2021 13:28:52 +0100 Subject: [PATCH 059/101] Generalize to any kind of polls (#76) --- src/bot.ts | 23 +-- src/modules/{attendance.ts => polls.ts} | 163 +++++++++++------- .../migration.sql | 3 + .../migration.sql | 14 ++ src/prisma/schema.prisma | 6 +- 5 files changed, 125 insertions(+), 84 deletions(-) rename src/modules/{attendance.ts => polls.ts} (65%) create mode 100644 src/prisma/migrations/20210919131102_rename_attendance_polls_to_polls/migration.sql create mode 100644 src/prisma/migrations/20210919145235_make_poll_schedule_optional/migration.sql diff --git a/src/bot.ts b/src/bot.ts index 93b6b1e..f00c45b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -11,7 +11,7 @@ import { PrismaClient } from "@prisma/client"; import { InteractionHandlers, CommandProvider, Chore } from "./bot.d"; import * as utils from "./modules/utils"; -import * as attendance from "./modules/attendance"; +import * as polls from "./modules/polls"; import * as roleSelection from "./modules/roleSelection"; import * as sudo from "./modules/sudo"; import * as misc from "./modules/misc"; @@ -48,7 +48,7 @@ const client = new Discord.Client({ }); const commandProviders: CommandProvider[] = [ - attendance.provideCommands, + polls.provideCommands, roleSelection.provideCommands, sudo.provideCommands, misc.provideCommands, @@ -62,7 +62,7 @@ const commandHandlers: InteractionHandlers = {}; // two above will be dynamically loaded const buttonHandlers: InteractionHandlers = { - attendance: attendance.handleAttendanceButton, + polls: polls.handlePollButton, roleSelection: roleSelection.handleRoleSelectionButton, }; @@ -72,18 +72,11 @@ const menuHandlers: InteractionHandlers = { const startupChores: Chore[] = [ { - summary: "Schedule attendance polls", - fn: async () => - await attendance.scheduleAttendancePolls( - client, - prisma, - await prisma.attendancePoll.findMany({ - where: { - type: "scheduled", - }, - }) - ), - complete: "All attendance polls scheduled", + summary: "Schedule polls", + fn: async () => { + await polls.scheduleAllScheduledPolls(client, prisma); + }, + complete: "All polls scheduled", }, { summary: "Send role selection messages", diff --git a/src/modules/attendance.ts b/src/modules/polls.ts similarity index 65% rename from src/modules/attendance.ts rename to src/modules/polls.ts index 032c74d..8969e25 100644 --- a/src/modules/attendance.ts +++ b/src/modules/polls.ts @@ -1,4 +1,4 @@ -// Handler for attendance polls +// Handler for polls import { ButtonInteraction, @@ -14,28 +14,27 @@ import { import * as Builders from "@discordjs/builders"; import cron from "node-cron"; -import { PrismaClient, AttendancePoll } from "@prisma/client"; +import { PrismaClient, Poll } from "@prisma/client"; import { CommandDescriptor } from "../bot.d"; -const ATTENDANCE_POLL_MSG = "Attendance Poll"; -const ATTENDANCE_POLL_ACTION_ROW = new MessageActionRow(); -ATTENDANCE_POLL_ACTION_ROW.addComponents([ +const POLL_ACTION_ROW = new MessageActionRow(); +POLL_ACTION_ROW.addComponents([ new MessageButton() .setLabel("Yes") .setStyle("SUCCESS") - .setCustomId("attendance:yes"), + .setCustomId("polls:yes"), new MessageButton() .setLabel("No") .setStyle("DANGER") - .setCustomId("attendance:no"), + .setCustomId("polls:no"), new MessageButton() .setLabel("Clear") .setStyle("SECONDARY") - .setCustomId("attendance:clear"), + .setCustomId("polls:clear"), ]); -const ATTENDANCE_NO_ONE = "*No one*"; +const POLL_NO_ONE = "*No one*"; -export const handleAttendanceButton = async ( +export const handlePollButton = async ( interaction: ButtonInteraction ): Promise => { await interaction.deferReply({ ephemeral: true }); @@ -51,22 +50,21 @@ export const handleAttendanceButton = async ( fieldIndex = 1; } - const newEmbed = getNewEmbed( + const newEmbed = getNewPollEmbed( oldEmbeds[0] as MessageEmbed, fieldIndex, interaction.user.id ); (interaction.message as Message).edit({ - content: ATTENDANCE_POLL_MSG, embeds: [newEmbed], - components: [ATTENDANCE_POLL_ACTION_ROW], + components: [POLL_ACTION_ROW], }); await interaction.editReply("Response recorded!"); }; -export const getNewEmbed = ( +export const getNewPollEmbed = ( oldEmbed: MessageEmbed, fieldIndex: number, userId: Snowflake @@ -76,10 +74,9 @@ export const getNewEmbed = ( field.value .split("\n") .filter( - (user) => - user !== ATTENDANCE_NO_ONE && user !== `<@${userId}>` + (user) => user !== POLL_NO_ONE && user !== `<@${userId}>` ) - .join("\n") || (fieldIndex === i ? "" : ATTENDANCE_NO_ONE); + .join("\n") || (fieldIndex === i ? "" : POLL_NO_ONE); }); if (oldEmbed.fields[fieldIndex]) oldEmbed.fields[ @@ -89,8 +86,8 @@ export const getNewEmbed = ( return oldEmbed; }; -export const sendAttendanceEmbed = async ( - embed: AttendancePoll, +export const unpinPoll = async ( + poll: Poll, channel: TextChannel ): Promise => { const pinnedMessages = await channel.messages.fetchPinned(); @@ -98,33 +95,39 @@ export const sendAttendanceEmbed = async ( pinnedMessages.map((msg) => { if ( msg.embeds?.some( - (msgEmbed) => msgEmbed.footer?.text === embed.id + (msgEmbed) => msgEmbed.footer?.text === poll.id ) ) return msg.unpin(); }) ); +}; + +export const sendPollEmbed = async ( + poll: Poll, + channel: TextChannel +): Promise => { + await unpinPoll(poll, channel); const message = await channel.send({ - content: ATTENDANCE_POLL_MSG, embeds: [ new MessageEmbed() - .setTitle(embed.title) - .addField("Attending", ATTENDANCE_NO_ONE, true) - .addField("Not Attending", ATTENDANCE_NO_ONE, true) - .setFooter(embed.id) + .setTitle(poll.title) + .addField("Yes", POLL_NO_ONE, true) + .addField("No", POLL_NO_ONE, true) + .setFooter(poll.id) .setTimestamp(), ], - components: [ATTENDANCE_POLL_ACTION_ROW], + components: [POLL_ACTION_ROW], }); await message.pin(); }; -export const scheduleAttendancePolls = async ( +export const schedulePolls = async ( client: Client, prisma: PrismaClient, - polls: AttendancePoll[] + polls: (Poll & { cron: string })[] ): Promise => { await Promise.all( polls.map(async (poll) => { @@ -134,7 +137,7 @@ export const scheduleAttendancePolls = async ( if (!channel) { console.error( - `Couldn't fetch channel ${poll.channelId} for attendance poll ${poll.id}` + `Couldn't fetch channel ${poll.channelId} for poll ${poll.id}` ); return; } @@ -142,16 +145,16 @@ export const scheduleAttendancePolls = async ( cron.schedule(poll.cron, async () => { try { // make sure it wasn't deleted / edited in the meantime - const p = await prisma.attendancePoll.findFirst({ + const p = await prisma.poll.findFirst({ where: { id: poll.id }, }); if (p !== null) { - await sendAttendanceEmbed(p, channel as TextChannel); + await sendPollEmbed(p, channel as TextChannel); } } catch (e) { console.error( - "Could not verify (& send) attendance poll:", - e.message + "Could not verify (& send) poll:", + (e as Error).message ); } }); @@ -159,14 +162,29 @@ export const scheduleAttendancePolls = async ( ); }; +export const scheduleAllScheduledPolls = async ( + client: Client, + prisma: PrismaClient +): Promise => { + await schedulePolls( + client, + prisma, + (await prisma.poll.findMany({ + where: { + type: "scheduled", + }, + })) as (Poll & { cron: string })[] + ); +}; + export function provideCommands(): CommandDescriptor[] { const cmd = new Builders.SlashCommandBuilder() - .setName("attendance") - .setDescription("Manage attendance polls"); + .setName("poll") + .setDescription("Manage polls"); cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("add") - .setDescription("Create a new scheduled attendance poll") + .setDescription("Create a new poll") .addStringOption( new Builders.SlashCommandStringOption() .setName("id") @@ -176,15 +194,7 @@ export function provideCommands(): CommandDescriptor[] { .addStringOption( new Builders.SlashCommandStringOption() .setName("title") - .setDescription("Attendance poll title") - .setRequired(true) - ) - .addStringOption( - new Builders.SlashCommandStringOption() - .setName("cron") - .setDescription( - "Cron schedule string; BE VERY CAREFUL THIS IS CORRECT!" - ) + .setDescription("Poll title") .setRequired(true) ) .addChannelOption( @@ -193,11 +203,19 @@ export function provideCommands(): CommandDescriptor[] { .setDescription("Where polls will be sent") .setRequired(true) ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("schedule") + .setDescription( + "Cron schedule string; BE VERY CAREFUL THIS IS CORRECT! If none, send one-shot poll." + ) + .setRequired(false) + ) ); cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("remove") - .setDescription("Remove an existing attendance poll") + .setDescription("Remove an existing poll") .addStringOption( new Builders.SlashCommandStringOption() .setName("id") @@ -208,12 +226,12 @@ export function provideCommands(): CommandDescriptor[] { cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("list") - .setDescription("List existing attendance polls") + .setDescription("List existing polls") ); cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("info") - .setDescription("Get information for an existing attendance poll") + .setDescription("Get information for an existing poll") .addStringOption( new Builders.SlashCommandStringOption() .setName("id") @@ -233,28 +251,29 @@ export async function handleCommand( try { const id = interaction.options.getString("id", true); const title = interaction.options.getString("title", true); - const cron = interaction.options.getString("cron", true); const channel = interaction.options.getChannel("channel", true); + const cron = interaction.options.getString("schedule", false); - // TODO: don't take this at face value - // ^ how important is this? in principle admins won't mess up - - const poll = await prisma.attendancePoll.create({ + const poll = await prisma.poll.create({ data: { id, - type: "scheduled", + type: cron ? "scheduled" : "one-shot", title, cron, channelId: channel.id, }, }); - await scheduleAttendancePolls(interaction.client, prisma, [ - poll, - ]); + if (cron) { + await schedulePolls(interaction.client, prisma, [ + poll as Poll & { cron: string }, + ]); + } else { + await sendPollEmbed(poll, channel as TextChannel); + } await interaction.editReply( - "✅ Successfully added and scheduled." + `✅ Successfully added${cron ? " and scheduled" : ""}.` ); } catch (e) { await interaction.editReply( @@ -268,7 +287,15 @@ export async function handleCommand( try { const id = interaction.options.getString("id", true); - await prisma.attendancePoll.delete({ where: { id } }); + const p = (await prisma.poll.findFirst({ + where: { id }, + })) as Poll; + const channel = (await interaction.client.channels.fetch( + p.channelId as Snowflake + )) as TextChannel; + await unpinPoll(p, channel); + + await prisma.poll.delete({ where: { id } }); await interaction.editReply("✅ Successfully removed."); } catch (e) { @@ -279,15 +306,15 @@ export async function handleCommand( } case "list": { try { - const polls = await prisma.attendancePoll.findMany(); + const polls = await prisma.poll.findMany(); await interaction.editReply({ embeds: [ new MessageEmbed() - .setTitle("Attendance Polls") + .setTitle("Polls") .setDescription( polls.length - ? "Below is a list of all attendance polls with their title and ID" - : "No attendance polls found" + ? "Below is a list of all polls with their title and ID" + : "No polls found" ) .addFields( polls.map((p) => ({ @@ -308,7 +335,7 @@ export async function handleCommand( try { const id = interaction.options.getString("id", true); - const poll = await prisma.attendancePoll.findFirst({ + const poll = await prisma.poll.findFirst({ where: { id }, }); @@ -320,11 +347,15 @@ export async function handleCommand( await interaction.editReply({ embeds: [ new MessageEmbed() - .setTitle("Attendance Poll Information") + .setTitle("Poll Information") .addField("ID", poll.id, true) .addField("Type", poll.type, true) .addField("Title", poll.title, true) - .addField("Cron Schedule", poll.cron, true) + .addField( + "Schedule", + poll.cron ? poll.cron : "N/A", + true + ) .addField( "Channel", `<#${poll.channelId}>`, diff --git a/src/prisma/migrations/20210919131102_rename_attendance_polls_to_polls/migration.sql b/src/prisma/migrations/20210919131102_rename_attendance_polls_to_polls/migration.sql new file mode 100644 index 0000000..f9585f1 --- /dev/null +++ b/src/prisma/migrations/20210919131102_rename_attendance_polls_to_polls/migration.sql @@ -0,0 +1,3 @@ +PRAGMA foreign_keys=off; +ALTER TABLE "attendance_polls" RENAME TO "polls"; +PRAGMA foreign_keys=on; diff --git a/src/prisma/migrations/20210919145235_make_poll_schedule_optional/migration.sql b/src/prisma/migrations/20210919145235_make_poll_schedule_optional/migration.sql new file mode 100644 index 0000000..e03b92f --- /dev/null +++ b/src/prisma/migrations/20210919145235_make_poll_schedule_optional/migration.sql @@ -0,0 +1,14 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_polls" ( + "id" TEXT NOT NULL PRIMARY KEY, + "type" TEXT NOT NULL, + "title" TEXT NOT NULL, + "cron" TEXT, + "channel_id" TEXT NOT NULL +); +INSERT INTO "new_polls" ("channel_id", "cron", "id", "title", "type") SELECT "channel_id", "cron", "id", "title", "type" FROM "polls"; +DROP TABLE "polls"; +ALTER TABLE "new_polls" RENAME TO "polls"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index c56b775..1dc6022 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -15,14 +15,14 @@ model Config { @@map("config") } -model AttendancePoll { +model Poll { id String @id /// identifier used to keep track of embed on pinned messages type String title String - cron String /// cron schedule + cron String? /// cron schedule channelId String @map("channel_id") /// channel where to post the poll - @@map("attendance_polls") + @@map("polls") } model RoleGroup { From d5931e6826bfda9dd5a83f02e3da7e7c37ea9554 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Wed, 22 Sep 2021 13:33:11 +0100 Subject: [PATCH 060/101] Add leaderboard module (#71) --- src/bot.ts | 2 + src/modules/galleryChannels.ts | 6 +- src/modules/leaderboard.ts | 271 ++++++++++++++++++ src/modules/utils.ts | 39 ++- .../migration.sql | 5 + src/prisma/schema.prisma | 7 + 6 files changed, 323 insertions(+), 7 deletions(-) create mode 100644 src/modules/leaderboard.ts create mode 100644 src/prisma/migrations/20210919001905_add_leaderboard_entries/migration.sql diff --git a/src/bot.ts b/src/bot.ts index f00c45b..227f312 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -18,6 +18,7 @@ import * as misc from "./modules/misc"; import * as galleryChannels from "./modules/galleryChannels"; import * as voiceThreads from "./modules/voiceThreads"; import * as welcome from "./modules/welcome"; +import * as leaderboard from "./modules/leaderboard"; for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { if (process.env[ev] === undefined) { @@ -55,6 +56,7 @@ const commandProviders: CommandProvider[] = [ galleryChannels.provideCommands, voiceThreads.provideCommands, welcome.provideCommands, + leaderboard.provideCommands, ]; const commandPermissions: { [command: string]: CommandPermission } = {}; diff --git a/src/modules/galleryChannels.ts b/src/modules/galleryChannels.ts index 4c3fb5b..c17e745 100644 --- a/src/modules/galleryChannels.ts +++ b/src/modules/galleryChannels.ts @@ -53,7 +53,7 @@ export async function parseExistingMessages( let count = 0; for (const channel of channels) { try { - const messages = await channel.messages.fetch({ limit: 100 }); + const messages = await utils.fetchAllChannelMessages(channel); for (const [_id, message] of messages) { try { @@ -105,9 +105,7 @@ export function provideCommands(): CommandDescriptor[] { cmd.addSubcommand( new Builders.SlashCommandSubcommandBuilder() .setName("clean") - .setDescription( - "Clean (an) existing gallery channel(s), 100 messages/14 days" - ) + .setDescription("Clean (an) existing gallery channel(s)") .addChannelOption( new Builders.SlashCommandChannelOption() .setName("channel") diff --git a/src/modules/leaderboard.ts b/src/modules/leaderboard.ts new file mode 100644 index 0000000..a635b47 --- /dev/null +++ b/src/modules/leaderboard.ts @@ -0,0 +1,271 @@ +// Guild leaderboard of users sorted by characters sent + +import { PrismaClient, LeaderboardEntry } from "@prisma/client"; + +import * as Discord from "discord.js"; +import * as Builders from "@discordjs/builders"; + +import { CommandDescriptor } from "../bot.d"; + +import * as utils from "./utils"; + +const MAX_PEOPLE = 50; + +type UsersCharacterCount = Discord.Collection; + +export async function getUsersCharacterCount( + guild: Discord.Guild, + onlyCountAfter?: Date, + cacheDate?: Date +): Promise< + [ + UsersCharacterCount, + number, + number, + Discord.Channel[], + UsersCharacterCount + ] +> { + const chars: UsersCharacterCount = new Discord.Collection(); + const addToCache: UsersCharacterCount = new Discord.Collection(); + const failed: (Discord.TextChannel | Discord.ThreadChannel)[] = []; + let msgCount = 0; + + const channels = Array.from( + (await guild.channels.fetch()).filter( + (channel) => channel.isText() || channel.isThread() + ) + ).map((o) => o[1]) as (Discord.TextChannel | Discord.ThreadChannel)[]; + const promises = channels.map(async (channel) => { + const messages = await utils.fetchAllChannelMessages(channel); + for (const [_id, msg] of messages) { + if (!msg.deleted && msg.author && !msg.author.bot) { + if ( + onlyCountAfter === undefined || + msg.createdAt > onlyCountAfter + ) { + chars.set( + msg.author.id, + (chars.get(msg.author.id) ?? 0) + msg.content.length + ); + msgCount++; + + if (cacheDate && msg.createdAt <= cacheDate) { + addToCache.set( + msg.author.id, + (addToCache.get(msg.author.id) ?? 0) + + msg.content.length + ); + } + } else { + break; // counting on you to be ordered, discord! + } + } + } + }); + + (await Promise.allSettled(promises)).map((res, i) => { + if (res.status === "rejected") { + failed.push(channels[i]); + } + }); + + return [chars, channels.length, msgCount, failed, addToCache]; +} + +export async function sendLeaderboard( + sendChannel: Discord.TextChannel | Discord.ThreadChannel, + period: string, + prisma: PrismaClient +): Promise<[number, number, Discord.Channel[], number, Discord.Snowflake]> { + const now = new Date().getTime(); + const day = 1000 * 60 * 60 * 24; + + const cacheStamp = ((i) => (i !== undefined ? new Date(i) : i))( + ( + await prisma.config.findFirst({ + where: { key: "leaderboard:cache_stamp" }, + }) + )?.value + ); + const cache = cacheStamp ? await prisma.leaderboardEntry.findMany() : []; + + const cacheDate = new Date(now - 45 * day); // cache messages older than 45 days + + const [chars, channels, msgs, failed, addToCache] = + await getUsersCharacterCount( + sendChannel.guild, + period === "month" + ? new Date(now - 30 * day) + : period === "week" + ? new Date(now - 7 * day) + : cacheStamp + ? new Date(cacheStamp) + : undefined, + cacheDate + ); + + const cacheMap: { [uid: string]: LeaderboardEntry } = {}; + + for (const entry of cache) { + cacheMap[entry.userId] = entry; + + chars.set( + entry.userId, + (chars.get(entry.userId) ?? 0) + entry.characterCount + ); + } + + if (!failed.length) { + for (const [uid, count] of addToCache) { + const entry = cacheMap[uid]; + if (entry) { + entry.characterCount += count; + } else { + cache.push({ userId: uid, characterCount: count }); + } + } + + await prisma.$transaction([ + prisma.config.upsert({ + where: { key: "leaderboard:cache_stamp" }, + update: { value: cacheDate.toISOString() }, + create: { + key: "leaderboard:cache_stamp", + value: cacheDate.toISOString(), + }, + }), + ...cache.map((entry) => + prisma.leaderboardEntry.upsert({ + where: { userId: entry.userId }, + update: { characterCount: entry.characterCount }, + create: { + userId: entry.userId, + characterCount: entry.characterCount, + }, + }) + ), + ]); + } + + chars.sort((v1, v2, k1, k2) => v2 - v1 || parseInt(k1) - parseInt(k2)); + + const lines = []; + + for (const [uid, cs] of chars) { + if (lines.length > MAX_PEOPLE) break; + lines.push( + `\`#${(lines.length + 1) + .toString() + .padStart( + Math.ceil(Math.log10(MAX_PEOPLE)), + "0" + )}\` <@${uid}> (${cs})` + ); + } + + lines.unshift( + "**__LEADERBOARD__** *(by character count)* `[" + + period.toUpperCase() + + "]`" + ); + + const sent = await sendChannel.send({ + content: lines.join("\n"), + allowedMentions: { parse: [] }, + }); + + return [ + channels, + msgs, + failed, + failed.length ? 0 : addToCache.reduce((a, b) => a + b, 0), + sent.id, + ]; +} + +export function provideCommands(): CommandDescriptor[] { + const cmd = new Builders.SlashCommandBuilder() + .setName("leaderboard") + .setDescription( + "Manage leaderboard that sorts server members by characters sent" + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("send") + .setDescription("Calculate and send a new leaderboard") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("time-period") + .setDescription( + "How recent do messages have to be to be considered; defaults to all" + ) + .setRequired(false) + .addChoice("All messages", "all") + .addChoice("Last 30 days", "month") + .addChoice("Last 7 days", "week") + ) + ); + return [ + { + builder: cmd, + handler: handleCommand, + }, + ]; +} + +export async function handleCommand( + interaction: Discord.CommandInteraction, + prisma: PrismaClient +): Promise { + switch (interaction.options.getSubcommand()) { + case "send": { + try { + if (!interaction.channel) { + throw new Error("Channel must exist"); + } + // TODO: if one is already being sent, don't allow another user to run command + const period = + interaction.options.getString("time-period", false) ?? + "all"; + await interaction.editReply( + `Will send a leaderboard (period: ${period}) to ${interaction.channel}, but calculations are necessary.\nThis may take a while...` + ); + + const [delta, [channels, msgs, failed, cached, msgId]] = + (await utils.timeFunction( + async () => + await sendLeaderboard( + interaction.channel as + | Discord.TextChannel + | Discord.ThreadChannel, + period, + prisma + ) + )) as [ + number, + utils.ThenArg> + ]; + await interaction.editReply(`✅ Sent [here](https://discord.com/channels/${ + interaction.guildId as string + }/${interaction.channelId as string}/${msgId}) + +Took ${delta}ms, combed through ${channels} channels and ${msgs} messages. +${ + failed.length + ? "❌ Failed to go through the following channels: " + + failed.map((c) => "<#" + c.id + ">").join(", ") + + "; as such, did not cache anything" + : "Did not fail to go through any channel; cached " + + cached + + " characters" +}`); + } catch (e) { + await interaction + .editReply("❌ Something went wrong.") + .catch(() => console.error("Leaderboard took too long :(")); + } + break; + } + } +} diff --git a/src/modules/utils.ts b/src/modules/utils.ts index a5b525f..fdc7055 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -5,11 +5,21 @@ import { performance } from "perf_hooks"; import { PrismaClient } from "@prisma/client"; import * as Discord from "discord.js"; -export async function timeFunction(fun: () => Promise): Promise { +// ThenArgRecursive from https://stackoverflow.com/a/49889856 +export type ThenArg = T extends PromiseLike ? ThenArg : T; + +export async function timeFunction( + fun: () => Promise +): Promise { const t0 = performance.now(); - await fun(); + const res = await fun(); const t1 = performance.now(); - return Math.round((t1 - t0 + Number.EPSILON) * 100) / 100; + const delta = Math.round((t1 - t0 + Number.EPSILON) * 100) / 100; + if (res === undefined) { + return delta; + } else { + return [delta, res]; + } } export function getConfigFactory( @@ -60,3 +70,26 @@ export async function fetchGalleries( .split(",") .filter((c) => c.length); } + +export type MessageCollection = Discord.Collection; + +export async function fetchAllChannelMessages( + channel: Discord.TextChannel | Discord.ThreadChannel, + after?: Date +): Promise { + const messages = new Discord.Collection(); + let fetched: MessageCollection | undefined; + + do { + fetched = await channel.messages.fetch({ + limit: 100, + before: fetched ? fetched.last()?.id : undefined, + }); + fetched.map((msg, id) => messages.set(id, msg)); + } while ( + fetched.size >= 100 && + (fetched.last()?.createdAt ?? 1) > (after ?? 0) + ); + + return messages; +} diff --git a/src/prisma/migrations/20210919001905_add_leaderboard_entries/migration.sql b/src/prisma/migrations/20210919001905_add_leaderboard_entries/migration.sql new file mode 100644 index 0000000..0053072 --- /dev/null +++ b/src/prisma/migrations/20210919001905_add_leaderboard_entries/migration.sql @@ -0,0 +1,5 @@ +-- CreateTable +CREATE TABLE "leaderboard_entries" ( + "user_id" TEXT NOT NULL PRIMARY KEY, + "character_count" INTEGER NOT NULL +); diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 1dc6022..43e4a52 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -49,3 +49,10 @@ model RoleGroupOption { @@map("role_group_options") } + +model LeaderboardEntry { + userId String @id @map("user_id") + characterCount Int @map("character_count") + + @@map("leaderboard_entries") +} From a8de06736388cd6a90e4e3d382f5915a2c1607e7 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Wed, 22 Sep 2021 13:36:22 +0100 Subject: [PATCH 061/101] Bump version to v2.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 934ba0e..91efadd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.2.0", + "version": "2.3.0", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From a8ea2b602ea7cc1adf8a14bfb0c1ae9e67cd96bc Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Thu, 30 Sep 2021 09:17:37 +0100 Subject: [PATCH 062/101] Upgrade to Prisma V3 (#77) * upgrade to prisma v3 * add yarn to package.json engines --- package.json | 7 ++++--- src/prisma/schema.prisma | 3 +-- yarn.lock | 36 ++++++++++++++++++------------------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 91efadd..9bb4933 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,13 @@ "license": "MIT", "engines": { "node": ">=16.6.1", - "npm": ">=7.23.0" + "npm": "^7.23.0", + "yarn": "^1.22.10" }, "dependencies": { "@discordjs/builders": "^0.6.0", "@discordjs/rest": "^0.1.0-canary.0", - "@prisma/client": "^2.30.3", + "@prisma/client": "^3.0.0", "@types/better-sqlite3": "^5.4.3", "discord.js": "^13.0.1", "node-cron": "^3.0.0", @@ -38,7 +39,7 @@ "husky": "^6.0.0", "lint-staged": "^11.0.0", "prettier": "^2.3.2", - "prisma": "^2.30.3", + "prisma": "^3.0.0", "typescript": "^4.3.4" }, "scripts": { diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 43e4a52..2e4918b 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -4,8 +4,7 @@ datasource db { } generator client { - provider = "prisma-client-js" - previewFeatures = ["referentialActions"] + provider = "prisma-client-js" } model Config { diff --git a/yarn.lock b/yarn.lock index 80f66a5..ca6dabf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,22 +121,22 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@prisma/client@^2.30.3": - version "2.30.3" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.30.3.tgz#49c1015e2cec26a44b20c62eb2fd738cb0bb043b" - integrity sha512-Ey2miZ+Hne12We3rA8XrlPoAF0iuKEhw5IK2nropaelSt0Ju3b2qSz9Qt50a/1Mx3+7yRSu/iSXt8y9TUMl/Yw== +"@prisma/client@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.0.2.tgz#f04d9b252f3d0c6918df43ad228eac27d03f6db1" + integrity sha512-6SrDYY2Yr5AmYpVB3XAXFqfzxKMdDTemXR7FmfXthnxWhQHoBwRLNZ3B3GyI/MmWa5tr+kaaGDJjp1LU0vuYvQ== dependencies: - "@prisma/engines-version" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" + "@prisma/engines-version" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" -"@prisma/engines-version@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20": - version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#d5ef55c92beeba56e52bba12b703af0bfd30530d" - integrity sha512-/iDRgaoSQC77WN2oDsOM8dn61fykm6tnZUAClY+6p+XJbOEgZ9gy4CKuKTBgrjSGDVjtQ/S2KGcYd3Ring8xaw== +"@prisma/engines-version@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db": + version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#c45323e420f47dd950b22c873bdcf38f75e65779" + integrity sha512-iArSApZZImVmT9oC/rGOjzvpG2AOqlIeqYcVnop9poA3FxD4zfVPbNPH9DTgOWhc06OkBHujJZeAcsNddVabIQ== -"@prisma/engines@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20": - version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#2df768aa7c9f84acaa1f35c970417822233a9fb1" - integrity sha512-WPnA/IUrxDihrRhdP6+8KAVSwsc0zsh8ioPYsLJjOhzVhwpRbuFH2tJDRIAbc+qFh+BbTIZbeyBYt8fpNXaYQQ== +"@prisma/engines@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db": + version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621" + integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg== "@sapphire/async-queue@^1.1.4": version "1.1.4" @@ -1315,12 +1315,12 @@ prettier@^2.3.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== -prisma@^2.30.3: - version "2.30.3" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.30.3.tgz#e4a770e1f52151e72c1c5be0aa2e75222a0135c4" - integrity sha512-48qYba2BIyUmXuosBZs0g3kYGrxKvo4VkSHYOuLlDdDirmKyvoY2hCYMUYHSx3f++8ovfgs+MX5KmNlP+iAZrQ== +prisma@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.0.2.tgz#e86cb6abf4a815c7ac97b9d0ed383f01c253ce34" + integrity sha512-TyOCbtWGDVdWvsM1RhUzJXoGClXGalHhyYWIc5eizSF8T1ScGiOa34asBUdTnXOUBFSErbsqMNw40DHAteBm1A== dependencies: - "@prisma/engines" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" + "@prisma/engines" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" process@^0.11.1: version "0.11.10" From 891bdadb10a948890f07015c095b0b1a0dc63b0f Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Sun, 3 Oct 2021 16:30:05 +0100 Subject: [PATCH 063/101] Degree Features (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Luís Fonseca Co-authored-by: Diogo Correia --- README.md | 10 + package.json | 9 +- src/bot.ts | 4 + src/modules/courses.d.ts | 5 + src/modules/courses.ts | 585 ++++++++++++++++ src/modules/degrees.ts | 651 ++++++++++++++++++ src/modules/fenix.d.ts | 25 + src/modules/fenix.ts | 126 ++++ src/modules/galleryChannels.ts | 26 +- src/modules/leaderboard.ts | 22 +- src/modules/misc.ts | 8 +- src/modules/polls.ts | 25 +- src/modules/roleSelection.ts | 270 +++++--- src/modules/sudo.ts | 23 +- src/modules/utils.d.ts | 8 + src/modules/utils.ts | 20 +- src/modules/voiceThreads.ts | 16 +- src/modules/welcome.ts | 10 +- .../migration.sql | 79 +++ src/prisma/schema.prisma | 56 +- yarn.lock | 131 +++- 21 files changed, 1957 insertions(+), 152 deletions(-) create mode 100644 src/modules/courses.d.ts create mode 100644 src/modules/courses.ts create mode 100644 src/modules/degrees.ts create mode 100644 src/modules/fenix.d.ts create mode 100644 src/modules/fenix.ts create mode 100644 src/modules/utils.d.ts create mode 100644 src/prisma/migrations/20211003144607_add_degree_course_features/migration.sql diff --git a/README.md b/README.md index 421c998..fb096af 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,13 @@ Replacing `CLIENT_ID` with the application's public ID, access the following lin ``` https://discord.com/oauth2/authorize?client_id=CLIENT_ID&scope=bot+applications.commands&permissions=8 ``` + +### Development + +If you're looking at the source code, you should probably run + +```sh +npx prisma generate +``` + +first so you can have typings. diff --git a/package.json b/package.json index 9bb4933..678a47c 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,16 @@ "license": "MIT", "engines": { "node": ">=16.6.1", - "npm": "^7.23.0", - "yarn": "^1.22.10" + "npm": ">=7.23.0", + "yarn": ">=1.22.0" }, "dependencies": { "@discordjs/builders": "^0.6.0", "@discordjs/rest": "^0.1.0-canary.0", "@prisma/client": "^3.0.0", "@types/better-sqlite3": "^5.4.3", + "axios": "^0.21.4", + "cheerio": "^1.0.0-rc.10", "discord.js": "^13.0.1", "node-cron": "^3.0.0", "path": "^0.12.7" @@ -46,7 +48,8 @@ "prepare": "husky install", "build": "tsc", "start": "node .", - "start:docker": "prisma migrate deploy && node ." + "start:docker": "prisma migrate deploy && node .", + "prisma:generate": "prisma generate" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" diff --git a/src/bot.ts b/src/bot.ts index 227f312..aa58244 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -19,6 +19,8 @@ import * as galleryChannels from "./modules/galleryChannels"; import * as voiceThreads from "./modules/voiceThreads"; import * as welcome from "./modules/welcome"; import * as leaderboard from "./modules/leaderboard"; +import * as degrees from "./modules/degrees"; +import * as courses from "./modules/courses"; for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { if (process.env[ev] === undefined) { @@ -57,6 +59,8 @@ const commandProviders: CommandProvider[] = [ voiceThreads.provideCommands, welcome.provideCommands, leaderboard.provideCommands, + degrees.provideCommands, + courses.provideCommands, ]; const commandPermissions: { [command: string]: CommandPermission } = {}; diff --git a/src/modules/courses.d.ts b/src/modules/courses.d.ts new file mode 100644 index 0000000..49f7da7 --- /dev/null +++ b/src/modules/courses.d.ts @@ -0,0 +1,5 @@ +// Course typings + +import * as Discord from "discord.js"; + +export type OrphanChannel = Discord.GuildChannel; diff --git a/src/modules/courses.ts b/src/modules/courses.ts new file mode 100644 index 0000000..75859fc --- /dev/null +++ b/src/modules/courses.ts @@ -0,0 +1,585 @@ +import { CommandDescriptor } from "./../bot.d"; +// Controller for everything courses + +import { + PrismaClient, + RoleGroup, + RoleGroupOption, + DegreeCourse, + Course, +} from "@prisma/client"; + +import * as Discord from "discord.js"; +import * as Builders from "@discordjs/builders"; + +import * as fenix from "./fenix"; +import * as utils from "./utils"; +import { OrphanChannel } from "./courses.d"; + +export function provideCommands(): CommandDescriptor[] { + const cmd = new Builders.SlashCommandBuilder() + .setName("courses") + .setDescription("Manage courses"); + + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("refresh-channels") + .setDescription("Refresh course channels") + ); + cmd.addSubcommand( + [...new Array(5)].reduce( + (builder, _, i) => + builder.addChannelOption( + new Builders.SlashCommandChannelOption() + .setName(`category${i + 1}`) + .setDescription(`Category ${i + 1}`) + .setRequired(i === 0) + ), + new Builders.SlashCommandSubcommandBuilder() + .setName("set-categories") + .setDescription( + "Set which categories the course channels should be created in" + ) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("toggle-channel-visibility") + .setDescription( + "Show or hide a course channel (and role) to remove clutter. This will delete course channel and role" + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("The display acroynm of the course") + .setRequired(true) + ) + .addBooleanOption( + new Builders.SlashCommandBooleanOption() + .setName("delete_role") + .setDescription( + "If hiding channel, delete the course role as well (true by default)" + ) + .setRequired(false) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("rename") + .setDescription("Set display acronym of course") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("old_acronym") + .setDescription("The acronym of the course to be renamed") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("new_acronym") + .setDescription( + "The acronym to show on channel name and role (e.g. CDI-I)" + ) + .setRequired(true) + ) + ); + + return [{ builder: cmd, handler: handleCommand }]; +} + +export async function handleCommand( + interaction: Discord.CommandInteraction, + prisma: PrismaClient +): Promise { + if (!interaction.guild) return; + + switch (interaction.options.getSubcommand()) { + case "refresh-channels": { + try { + const orphanChannels = await refreshCourses( + prisma, + interaction.guild + ); + + await interaction.editReply( + utils.CheckMarkEmoji + + "Sucessfully updated course channels." + + (orphanChannels.length + ? `\nConsider deleting the following ${orphanChannels.length} orphan channel(s):\n` + + orphanChannels + .map((c) => `- <#${c.id}>`) + .join("\n") + : "") + ); + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "set-categories": { + try { + const categories = [...Array(5)] + .map((_, i) => + interaction.options.getChannel( + `category${i + 1}`, + i === 0 + ) + ) + .filter((v) => !!v && v.type === "GUILD_CATEGORY"); + + if (categories.length === 0) { + await interaction.editReply( + utils.XEmoji + "No category channels provided" + ); + } + + await prisma.config.upsert({ + where: { key: "courses:category_channels" }, + update: { + value: categories.map((c) => c?.id || "").join(","), + }, + create: { + key: "courses:category_channels", + value: categories.map((c) => c?.id || "").join(","), + }, + }); + + await interaction.editReply( + utils.CheckMarkEmoji + + "Sucessfully updated course categories.\n" + + categories.map((c) => `- <#${c?.id}>`).join("\n") + ); + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "toggle-channel-visibility": { + try { + const courseAcronym = interaction.options.getString( + "acronym", + true + ); + const deleteRole = + interaction.options.getBoolean("delete_role", false) ?? + true; + + const course = await prisma.course.findUnique({ + where: { displayAcronym: courseAcronym }, + }); + if (!course) { + await interaction.editReply( + utils.XEmoji + `Course \`${courseAcronym}\` not found!` + ); + return; + } + + const exists = !course.hideChannel; + await prisma.course.update({ + where: { displayAcronym: courseAcronym }, + data: { hideChannel: exists }, + }); + + if (exists) { + await prisma.course.update({ + where: { displayAcronym: courseAcronym }, + data: { roleId: null, channelId: null }, + }); + try { + const courseChannel = + await interaction.guild.channels.fetch( + course.channelId || "" + ); + if (courseChannel && courseChannel.deletable) { + courseChannel.delete(); + } + if (deleteRole) { + const courseRole = + await interaction.guild.roles.fetch( + course.roleId || "" + ); + if (courseRole) { + courseRole.delete( + `${interaction.user.tag} has hidden course` + ); + } + } + } catch (e) { + await interaction.editReply( + utils.XEmoji + + "Could not delete channel and/or role, but settings were updated correctly. Please delete the channel/role manually." + ); + return; + } + await interaction.editReply( + utils.CheckMarkEmoji + + `Course \`${course.name}\` is now hidden` + ); + } else { + await refreshCourses(prisma, interaction.guild); + await interaction.editReply( + utils.CheckMarkEmoji + + `Course \`${course.name}\` is now being shown` + ); + } + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + break; + } + case "rename": { + try { + const oldAcronym = interaction.options.getString( + "old_acronym", + true + ); + const newAcronym = interaction.options.getString( + "new_acronym", + true + ); + + const course = await prisma.course.findUnique({ + where: { displayAcronym: oldAcronym }, + }); + if (!course) { + await interaction.editReply( + utils.XEmoji + `Course \`${oldAcronym}\` not found!` + ); + return; + } + + await prisma.course.update({ + where: { displayAcronym: oldAcronym }, + data: { displayAcronym: newAcronym }, + }); + + try { + const courseChannel = + await interaction.guild.channels.fetch( + course.channelId || "" + ); + const roleChannel = await interaction.guild.roles.fetch( + course.roleId || "" + ); + if (courseChannel) { + courseChannel.edit( + { + name: newAcronym.toLowerCase(), + topic: `${course.name} - ${newAcronym}`, + }, + `Course rename by ${interaction.user.tag}` + ); + } + if (roleChannel) { + roleChannel.setName( + newAcronym, + `Course rename by ${interaction.user.tag}` + ); + } + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + + "Failed to rename channel and/or role, but renamed course on database. Please rename channel and/or role manully." + ); + } + + await interaction.editReply( + utils.CheckMarkEmoji + "Course renamed succesfully!" + ); + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + + "Something went wrong. Maybe there is already another course with the desired acronym?" + ); + } + + break; + } + } +} + +export async function importCoursesFromDegree( + prisma: PrismaClient, + degreeId: string +): Promise { + const degreeCourses = await fenix.getDegreeCourses(degreeId); + + await Promise.all( + degreeCourses.map(async (course) => { + const globalCourse = await prisma.course.findUnique({ + where: { acronym: course.acronym }, + }); + + if (!globalCourse) { + // Create global course since it doesn't exist + + await prisma.course.create({ + data: { + acronym: course.acronym, + displayAcronym: course.acronym, + name: course.name, + }, + }); + } + + await prisma.degreeCourse.create({ + data: { + id: `${degreeId}-${course.acronym}`, + degreeFenixId: degreeId, + courseAcronym: course.acronym, + year: course.year, + semester: course.semester, + }, + }); + }) + ); +} + +export async function refreshCourses( + prisma: PrismaClient, + guild: Discord.Guild +): Promise { + const reason = "Course channels refresh"; + + const categoriesId = ( + ( + await prisma.config.findUnique({ + where: { key: "courses:category_channels" }, + }) + )?.value || "" + ) + .split(",") + .filter((v) => v !== ""); + + const categoriesChannels: Discord.CategoryChannel[] = ( + await Promise.all( + categoriesId.map( + async (id) => + (await guild.channels.fetch(id)) as Discord.CategoryChannel + ) + ) + ).filter((channel) => channel?.type === "GUILD_CATEGORY"); + if (!categoriesChannels.length) { + throw new Error("No category channels configured"); + } + + const getNextFreeCategory = () => + categoriesChannels.find((v) => v.children.size < 50); + + // Get courses with associated degrees over tier 2 + const courses = await prisma.course.findMany({ + where: { + perDegreeImplementations: { + some: { + degree: { + tier: { + gte: 2, + }, + }, + }, + }, + hideChannel: false, + }, + }); + const channels = new Discord.Collection< + string, + Discord.GuildChannel + >().concat(...categoriesChannels.map((v) => v.children)); + const roles = await guild.roles.fetch(); + + await courses.reduce(async (prevPromise, course) => { + await prevPromise; + + let courseRole = + roles.get(course.roleId || "") || + roles.find((v) => v.name === course.displayAcronym); + let courseChannel = + channels.get(course.channelId || "") || + channels.find( + (v) => v.name === course.displayAcronym.toLowerCase() + ); + + if (!courseRole) { + courseRole = await guild.roles.create({ + name: course.displayAcronym, + mentionable: false, + reason, + }); + } else if (courseRole.name !== course.acronym) { + await courseRole.setName(course.acronym, reason); + } + if (course.roleId !== courseRole.id) { + await prisma.course.update({ + where: { acronym: course.acronym }, + data: { roleId: courseRole.id }, + }); + } + + const courseChannelTopic = `${course.name} - ${course.displayAcronym}`; + if (!courseChannel) { + courseChannel = await guild.channels.create( + course.displayAcronym.toLowerCase(), + { + type: "GUILD_TEXT", + topic: courseChannelTopic, + parent: getNextFreeCategory(), + reason, + permissionOverwrites: [ + { + id: guild.roles.everyone.id, + deny: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + }, + { + id: courseRole, + allow: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + }, + ], + } + ); + } else { + if (courseChannel.name !== course.displayAcronym.toLowerCase()) { + await courseChannel.setName( + course.displayAcronym.toLowerCase() + ); + } + if ( + courseChannel.type === "GUILD_TEXT" && + (courseChannel as Discord.TextChannel).topic !== + courseChannelTopic + ) { + await (courseChannel as Discord.TextChannel).setTopic( + courseChannelTopic, + reason + ); + } + if ( + !courseChannel + .permissionsFor(courseRole) + .has(Discord.Permissions.FLAGS.VIEW_CHANNEL) + ) { + await courseChannel.edit({ + permissionOverwrites: [ + { + id: guild.roles.everyone.id, + deny: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + }, + { + id: courseRole, + allow: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + }, + ], + }); + } + } + if (course.channelId !== courseChannel.id) { + await prisma.course.update({ + where: { acronym: course.acronym }, + data: { channelId: courseChannel.id }, + }); + } + + channels.delete(courseChannel.id); + }, Promise.resolve()); + + return channels.map((v) => v); +} + +export async function generateRoleSelectionGroupsForCourseSelectionChannel( + prisma: PrismaClient, + channelId: Discord.Snowflake +): Promise<(RoleGroup & { options: RoleGroupOption[] })[]> { + const courses = await prisma.degreeCourse.findMany({ + where: { + degree: { + courseSelectionChannelId: channelId, + }, + course: { + hideChannel: false, + roleId: { not: null }, + }, + }, + include: { course: true }, + }); + courses.sort((a, b) => + a.course.displayAcronym.localeCompare(b.course.displayAcronym) + ); + + const byYear: Record = {}; + for (const course of courses) { + if (byYear[course.year] === undefined) { + byYear[course.year] = []; + } + byYear[course.year].push(course); + } + + return await Promise.all( + Object.keys(byYear) + .sort() + .map(async (year) => { + const yearCourses = utils.removeDuplicatesFromArray( + byYear[year], + (v) => v.course.roleId + ); + const groupId = `__dc-${channelId}-${year}`; + + return { + id: groupId, + mode: "menu", + placeholder: `Escolhe cadeiras de ${year}º ano`, + message: `Para acederes aos respetivos canais e receberes anúncios, escolhe em que cadeiras de ${year}º ano tens interesse.`, + minValues: 0, + maxValues: -1, + channelId, + messageId: + ( + await prisma.courseRoleSelectionMessage.findFirst({ + where: { injectedRoleGroupId: groupId }, + }) + )?.messageId ?? null, + options: yearCourses.map((c) => ({ + label: c.course.displayAcronym, + description: `${c.course.name} (${c.semester}º Semestre)`, + value: c.course.roleId as string, + emoji: null, + roleGroupId: groupId, + })), + }; + }) + ); +} + +export async function getRoleSelectionGroupsForInjection( + prisma: PrismaClient +): Promise< + ReturnType +> { + const channelIds = ( + await prisma.degree.findMany({ + where: { + courseSelectionChannelId: { not: null }, + }, + }) + ).map((d) => d.courseSelectionChannelId as Discord.Snowflake); + + const uniqueChannelIds = utils.removeDuplicatesFromArray(channelIds); + + return [ + ...(await Promise.all( + uniqueChannelIds.map((id) => + generateRoleSelectionGroupsForCourseSelectionChannel(prisma, id) + ) + )), + ].flat(); +} diff --git a/src/modules/degrees.ts b/src/modules/degrees.ts new file mode 100644 index 0000000..83fa6d4 --- /dev/null +++ b/src/modules/degrees.ts @@ -0,0 +1,651 @@ +import { PrismaClient } from "@prisma/client"; + +import * as Discord from "discord.js"; +import * as Builders from "@discordjs/builders"; + +import { CommandDescriptor } from "../bot.d"; +import * as utils from "./utils"; +import * as fenix from "./fenix"; +import * as courses from "./courses"; +import { OrphanChannel } from "./courses.d"; + +const tierChoices = [ + "None", + "Degree channels (Text & VC)", + "Course channels (& course selection channel)", + "Announcements channel", +].map( + (desc, i) => + [`${i}: ${i > 1 ? i - 1 + " + " : ""}${desc}`, i.toString()] as [ + name: string, + value: string + ] +); + +export function provideCommands(): CommandDescriptor[] { + const cmd = new Builders.SlashCommandBuilder() + .setName("degrees") + .setDescription("Manage degrees"); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("create") + .setDescription("Create a new degree") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("Degree acronym") + .setRequired(true) + ) + .addRoleOption( + new Builders.SlashCommandRoleOption() + .setName("role") + .setDescription("Degree role") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("tier") + .setDescription("Degree tier within the server") + .setRequired(true) + .addChoices(tierChoices) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("fenix-acronym") + .setDescription( + "Acronym used by Fénix, if different than normal acronym" + ) + .setRequired(false) + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("degree-text-channel") + .setDescription("Use an existing degree text channel") + .setRequired(false) + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("degree-voice-channel") + .setDescription("Use an existing degree voice channel") + .setRequired(false) + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("course-selection-channel") + .setDescription("Use an existing course selection channel") + .setRequired(false) + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("announcements-channel") + .setDescription("Use an existing announcements channel") + .setRequired(false) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("list") + .setDescription("List all degrees") + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("view") + .setDescription("Show information relative to a degree") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("The acronym of the degree to be removed") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("delete") + .setDescription( + "Remove a degree (no channels/roles will be deleted)" + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("The acronym of the degree to be removed") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("rename") + .setDescription("Rename an existing degree") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("Course acronym") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("new-name") + .setDescription("What to change the name to") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-role") + .setDescription("Set the role associated with an existing degree") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("The degree's acronym") + .setRequired(true) + ) + .addRoleOption( + new Builders.SlashCommandRoleOption() + .setName("new-role") + .setDescription("What role to set") + .setRequired(true) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-tier") + .setDescription("Set the tier associated with an existing degree") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("The degree's acronym") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("new-tier") + .setDescription("What tier to set") + .setRequired(true) + .addChoices(tierChoices) + ) + ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set-channel") + .setDescription("Set a channel associated with an existing degree") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("The degree's acronym") + .setRequired(true) + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("channel-type") + .setDescription("What type channel to set") + .setRequired(true) + .addChoice("Degree Text", "degree-text") + .addChoice("Degree Voice", "degree-voice") + .addChoice("Announcements", "announcements") + .addChoice("Course Selection", "course-selection") + ) + .addChannelOption( + new Builders.SlashCommandChannelOption() + .setName("new-channel") + .setDescription("New channel to set") + .setRequired(true) + ) + ); + return [{ builder: cmd, handler: handleCommand }]; +} + +export async function createDegree( + prisma: PrismaClient, + guild: Discord.Guild, + acronym: string, + role: Discord.Role, + tier: number, + fenixAcronym: string | null, + degreeTextChannel: Discord.GuildChannel | null, + degreeVoiceChannel: Discord.GuildChannel | null, + courseSelectionChannel: Discord.GuildChannel | null, + announcementsChannel: Discord.GuildChannel | null +): Promise { + // snowflakes are orphan channels; FIXME: change to course.OrphanChannel[] + if (!tierChoices.map((arr) => arr[1]).includes(tier.toString())) { + return "Invalid tier"; + } + + if (!fenixAcronym) fenixAcronym = acronym; + + const degrees = await fenix.getDegrees(); + const shortDegree = degrees.find( + (d) => d.acronym.toLowerCase() === fenixAcronym?.toLowerCase() + ); + + if (shortDegree === undefined) { + return "Could not find degree; try setting a fenix-acronym"; + } + + const reason = "Adding new degree " + acronym; + + if (tier >= 1) { + const catName = role.name.toUpperCase(); + const category = + ( + degreeTextChannel ?? + degreeVoiceChannel ?? + courseSelectionChannel ?? + announcementsChannel + )?.parent ?? + ((await guild.channels.fetch()) + .filter( + (c) => c.type === "GUILD_CATEGORY" && c.name === catName + ) + .first() as Discord.CategoryChannel | undefined) ?? + (await guild.channels.create(catName, { + type: "GUILD_CATEGORY", + permissionOverwrites: [ + { + id: guild.roles.everyone.id, + deny: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + }, + { + id: role.id, + allow: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + }, + ], + reason, + })); + if (!degreeTextChannel) { + degreeTextChannel = await guild.channels.create( + acronym.toLowerCase(), + { + type: "GUILD_TEXT", + topic: shortDegree.name, + parent: category, + reason, + } + ); + await degreeTextChannel.lockPermissions(); + } + if (!degreeVoiceChannel) { + degreeVoiceChannel = await guild.channels.create( + acronym.toUpperCase(), + { + type: "GUILD_VOICE", + parent: category, + reason, + } + ); + await degreeVoiceChannel.lockPermissions(); + } + + const restricted = [ + { + id: guild.roles.everyone, + deny: [ + Discord.Permissions.FLAGS.VIEW_CHANNEL, + Discord.Permissions.FLAGS.SEND_MESSAGES, + ], + }, + { + id: role.id, + allow: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + }, + ]; + + if (!courseSelectionChannel) { + courseSelectionChannel = await guild.channels.create( + acronym.toLowerCase() + "-cadeiras", + { + type: "GUILD_TEXT", + topic: "Selecionar cadeiras", + parent: category, + reason, + permissionOverwrites: restricted, + } + ); + } + + if (tier >= 3) { + if (!announcementsChannel) { + const announcer = (await guild.roles.fetch()) + .filter((r) => r.name === "Announcer") + .first(); + announcementsChannel = await guild.channels.create( + acronym.toLowerCase() + "-announcements", + { + type: "GUILD_TEXT", + topic: shortDegree.name + " Announcements", + parent: category, + reason, + permissionOverwrites: announcer + ? restricted.concat({ + id: announcer.id, + allow: [ + Discord.Permissions.FLAGS.SEND_MESSAGES, + ], + }) + : restricted, + } + ); + } + } + } + + await prisma.degree.create({ + data: { + fenixId: shortDegree.id, + acronym, + name: shortDegree.name, + roleId: role.id, + tier, + degreeTextChannelId: degreeTextChannel + ? degreeTextChannel.id + : null, + degreeVoiceChannelId: degreeVoiceChannel + ? degreeVoiceChannel.id + : null, + announcementsChannelId: announcementsChannel + ? announcementsChannel.id + : null, + courseSelectionChannelId: courseSelectionChannel + ? courseSelectionChannel.id + : null, + }, + }); + + await courses.importCoursesFromDegree(prisma, shortDegree.id); + + return await courses.refreshCourses(prisma, guild); +} + +export async function handleCommand( + interaction: Discord.CommandInteraction, + prisma: PrismaClient +): Promise { + if (!interaction.guild) return; + + switch (interaction.options.getSubcommand()) { + case "create": { + try { + const result = await createDegree( + prisma, + interaction.guild, + interaction.options.getString("acronym", true), + interaction.options.getRole("role", true) as Discord.Role, + parseInt(interaction.options.getString("tier", true)), + interaction.options.getString("fenix-acronym", false), + interaction.options.getChannel( + "degree-text-channel", + false + ) as Discord.GuildChannel | null, + interaction.options.getChannel( + "degree-voice-channel", + false + ) as Discord.GuildChannel | null, + interaction.options.getChannel( + "course-selection-channel", + false + ) as Discord.GuildChannel | null, + interaction.options.getChannel( + "announcements-channel", + false + ) as Discord.GuildChannel | null + ); + if (typeof result === "string") { + await interaction.editReply(utils.XEmoji + result); + } else { + await interaction.editReply( + utils.CheckMarkEmoji + + "Sucessfully created degree." + + (result.length + ? `\nConsider deleting the following ${result.length} orphan channel(s):\n` + + result.map((c) => `- <#${c.id}>`).join("\n") + : "") + ); + } + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "list": { + try { + const degrees = await prisma.degree.findMany(); + + await interaction.editReply({ + embeds: [ + new Discord.MessageEmbed() + .setTitle("All Degrees") + .setDescription( + "Below are all known degrees, by acronym and Fénix ID" + ) + .addFields( + degrees.map((d) => ({ + name: d.acronym, + value: d.fenixId, + inline: true, + })) + ), + ], + }); + } catch (e) { + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "view": { + try { + const acronym = interaction.options.getString("acronym", true); + + const degree = await prisma.degree.findFirst({ + where: { acronym }, + }); + + if (degree === null) { + await interaction.editReply( + utils.XEmoji + "Could not find degree" + ); + } else { + await interaction.editReply({ + embeds: [ + new Discord.MessageEmbed() + .setTitle("Degree Information") + .setDescription( + "Below are all the available details on this degree." + ) + .addField("Acronym", degree.acronym, true) + .addField("Name", degree.name, true) + .addField("Fénix ID", degree.fenixId, true) + .addField("Tier", degree.tier.toString(), true) + .addField("Role", `<@&${degree.roleId}>`, true) + .addField( + "Text Channel", + degree.degreeTextChannelId + ? `<#${degree.degreeTextChannelId}>` + : "[NOT SET]", + true + ) + .addField( + "Voice Channel", + degree.degreeVoiceChannelId ?? "[NOT SET]", + true + ) + .addField( + "Announcements Channel", + degree.announcementsChannelId + ? `<#${degree.announcementsChannelId}>` + : "[NOT SET]", + true + ) + .addField( + "Course Selection Channel", + degree.courseSelectionChannelId + ? `<#${degree.courseSelectionChannelId}>` + : "[NOT SET", + true + ), + ], + }); + } + } catch (e) { + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "delete": { + try { + const acronym = interaction.options.getString("acronym", true); + + await prisma.degree.delete({ where: { acronym } }); + + await interaction.editReply( + utils.CheckMarkEmoji + "Successfully removed." + ); + } catch (e) { + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "rename": { + try { + const acronym = interaction.options.getString("acronym", true); + const newName = interaction.options.getString("new-name", true); + + await prisma.degree.update({ + where: { acronym }, + data: { name: newName }, + }); + + await interaction.editReply( + utils.CheckMarkEmoji + + `Successfully renamed ${acronym} to ${newName}` + ); + } catch (e) { + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "set-role": { + try { + const acronym = interaction.options.getString("acronym", true); + const newRole = interaction.options.getRole("new-role", true); + + await prisma.degree.update({ + where: { acronym }, + data: { roleId: newRole.id }, + }); + + await interaction.editReply( + utils.CheckMarkEmoji + + `Successfully set role of ${acronym} to <@&${newRole.id}>` + ); + } catch (e) { + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "set-tier": { + try { + const acronym = interaction.options.getString("acronym", true); + const newTier = interaction.options.getString("new-tier", true); + const numTier = parseInt(newTier); + + if (isNaN(numTier) || tierChoices[numTier] === undefined) { + await interaction.editReply(utils.XEmoji + "Invalid tier"); + return; + } + + await prisma.degree.update({ + where: { acronym }, + data: { tier: numTier }, + }); + + await interaction.editReply( + utils.CheckMarkEmoji + + `Successfully set tier of ${acronym} to ${newTier}` + ); + } catch (e) { + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + case "set-channel": { + try { + const acronym = interaction.options.getString("acronym", true); + const channelType = interaction.options.getString( + "channel-type", + true + ); + const newChannel = interaction.options.getChannel( + "new-channel", + true + ) as Discord.GuildChannel; + + const key = { + ["degree-text"]: "degreeTextChannelId", + ["degree-voice"]: "degreeVoiceChannelId", + ["announcements"]: "announcementsChannelId", + ["course-selection"]: "courseSelectionChannelId", + }[channelType]; + + if (key === undefined) { + await interaction.editReply( + utils.XEmoji + "Invalid channel type" + ); + return; + } + + if (channelType === "degreeVoice" && !newChannel.isVoice()) { + await interaction.editReply( + utils.XEmoji + "Must be a voice channel" + ); + return; + } else if ( + channelType !== "degreeVoice" && + newChannel.isVoice() + ) { + await interaction.editReply( + utils.XEmoji + "Must not be a voice channel" + ); + return; + } + + await prisma.degree.update({ + where: { acronym }, + data: { [key]: newChannel.id }, + }); + + await interaction.editReply( + utils.CheckMarkEmoji + + `Successfully set ${channelType} of ${acronym} to <@#${newChannel.id}>` + ); + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + + break; + } + } +} diff --git a/src/modules/fenix.d.ts b/src/modules/fenix.d.ts new file mode 100644 index 0000000..c33b4c9 --- /dev/null +++ b/src/modules/fenix.d.ts @@ -0,0 +1,25 @@ +// Typings for Fénix API interactions + +export interface AboutInfo { + institutionName: string; + institutionUrl: string; + rssFeeds: { description: string; url: string }[]; + currentAcademicTerm: string; + languages: string[]; + language: string; + rss: { [key: string]: string }; +} + +export interface ShortDegree { + id: string; + name: string; + acronym: string; + academicTerms: string[]; +} + +export interface FenixDegreeCourse { + acronym: string; + name: string; + year: number; + semester: number; +} diff --git a/src/modules/fenix.ts b/src/modules/fenix.ts new file mode 100644 index 0000000..7fd62c1 --- /dev/null +++ b/src/modules/fenix.ts @@ -0,0 +1,126 @@ +// Handle everything that uses Fénix APIs + +import axios from "axios"; +import cheerio from "cheerio"; + +import * as FenixTypings from "./fenix.d"; +import * as utils from "./utils"; + +export const axiosClient = axios.create({ + baseURL: "https://fenix.tecnico.ulisboa.pt", + params: { + lang: "pt-PT", + }, +}); + +export async function callEndpoint(endpoint: string): Promise { + try { + return (await axiosClient.get(endpoint)).data; + } catch (e) { + console.error( + `Fénix broke while calling endpoint '${endpoint}': ${ + (e as Error).message + }` + ); + throw e; // propagate + } +} + +export async function getAboutInfo(): Promise { + return (await callEndpoint( + "/api/fenix/v1/about" + )) as FenixTypings.AboutInfo; +} + +export async function getCurrentAcademicTerm(): Promise<{ + year: string; + semester: number; +}> { + const match = (await getAboutInfo()).currentAcademicTerm.match( + /(\d+)º \w+ (\d+\/\d+)/ + ); + if (match === null) { + throw new Error("Could not get academic term!"); + } else { + return { year: match[2], semester: parseInt(match[1]) }; + } +} + +export async function getDegrees(): Promise { + const curYear = (await getCurrentAcademicTerm()).year; + const degrees = (await callEndpoint( + "/api/fenix/v1/degrees/all" + )) as FenixTypings.ShortDegree[]; + return degrees.filter((d) => d.academicTerms.includes(curYear)); +} + +export async function getDegreeCourses( + degreeId: string +): Promise { + const degrees = await getDegrees(); + const shortDegree = degrees.find((d) => d.id.toLowerCase() === degreeId); + + if (shortDegree === undefined) { + throw new Error(`Could not find degree with ID ${degreeId}`); + } + + const degreeAcronym = shortDegree.acronym.toLowerCase(); + + const curriculumHtml = (await callEndpoint( + `/cursos/${degreeAcronym}/curriculo` + )) as string; + + const $ = cheerio.load(curriculumHtml); + + const courses = await Promise.all( + $("#bySemesters .row") + .map(function () { + const hrefEl = $(".row div:first a", this).first(); + const courseName = hrefEl.text()?.trim() || ""; + const courseLink = hrefEl.attr("href")?.trim() || ""; + const yearSemester = $(".row div div", null, this) + .first() + .text() + ?.trim(); + const year = + parseInt(yearSemester.match(/Ano (\d)/)?.[1] || "0", 10) || + 0; + const semester = + parseInt( + yearSemester.match(/Sem\. (\d)/)?.[1] || "0", + 10 + ) || + (parseInt(yearSemester.match(/P (\d)/)?.[1] || "0", 10) <= 2 + ? 1 + : 2) || + 0; + + return { + name: courseName, + acronym: courseLink, + year, + semester, + }; + }) + .toArray() + .filter(({ name }) => !name.startsWith("HASS ")) + .map(async (course: FenixTypings.FenixDegreeCourse) => { + const coursePageHtml = (await callEndpoint( + course.acronym + )) as string; + const acronym = + cheerio + .load(coursePageHtml)("#content-block h1 small") + .first() + .text() + ?.trim() || course.name; + + return { ...course, acronym }; + }) + ); + + return utils.removeDuplicatesFromArray( + courses, + (courses) => courses.acronym + ); +} diff --git a/src/modules/galleryChannels.ts b/src/modules/galleryChannels.ts index c17e745..c73f9b9 100644 --- a/src/modules/galleryChannels.ts +++ b/src/modules/galleryChannels.ts @@ -154,17 +154,19 @@ export async function handleCommand( if (galleries.includes(channel)) { await interaction.editReply( - "❌ Channel is already a gallery." + utils.XEmoji + "Channel is already a gallery." ); } else { galleries.push(channel); await updateGalleries(prisma, galleries); await interaction.editReply( - "✅ Gallery successfully added." + utils.CheckMarkEmoji + "Gallery successfully added." ); } } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } break; } @@ -176,18 +178,22 @@ export async function handleCommand( ).id; if (!galleries.includes(channel)) { - await interaction.editReply("❌ Channel is not a gallery."); + await interaction.editReply( + utils.XEmoji + "Channel is not a gallery." + ); } else { await updateGalleries( prisma, galleries.filter((c) => c != channel) ); await interaction.editReply( - "✅ Gallery successfully removed." + utils.CheckMarkEmoji + "Gallery successfully removed." ); } } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } break; } @@ -227,10 +233,14 @@ export async function handleCommand( ]; } const n = await parseExistingMessages(prisma, channels); - await interaction.editReply(`✅ Cleaned \`${n}\` messages.`); + await interaction.editReply( + utils.CheckMarkEmoji + `Cleaned \`${n}\` messages.` + ); break; } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } } } diff --git a/src/modules/leaderboard.ts b/src/modules/leaderboard.ts index a635b47..bd02681 100644 --- a/src/modules/leaderboard.ts +++ b/src/modules/leaderboard.ts @@ -8,6 +8,7 @@ import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; import * as utils from "./utils"; +import { ThenArg } from "./utils.d"; const MAX_PEOPLE = 50; @@ -242,27 +243,28 @@ export async function handleCommand( period, prisma ) - )) as [ - number, - utils.ThenArg> - ]; - await interaction.editReply(`✅ Sent [here](https://discord.com/channels/${ - interaction.guildId as string - }/${interaction.channelId as string}/${msgId}) + )) as [number, ThenArg>]; + await interaction.editReply( + utils.CheckMarkEmoji + + `Sent [here](https://discord.com/channels/${ + interaction.guildId as string + }/${interaction.channelId as string}/${msgId}) Took ${delta}ms, combed through ${channels} channels and ${msgs} messages. ${ failed.length - ? "❌ Failed to go through the following channels: " + + ? utils.XEmoji + + "Failed to go through the following channels: " + failed.map((c) => "<#" + c.id + ">").join(", ") + "; as such, did not cache anything" : "Did not fail to go through any channel; cached " + cached + " characters" -}`); +}` + ); } catch (e) { await interaction - .editReply("❌ Something went wrong.") + .editReply(utils.XEmoji + "Something went wrong.") .catch(() => console.error("Leaderboard took too long :(")); } break; diff --git a/src/modules/misc.ts b/src/modules/misc.ts index 473ef84..68df680 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -134,12 +134,14 @@ export async function handleSayCommand( } mentions)` ); } - await interaction.editReply("✅ Successfully sent message."); + await interaction.editReply( + utils.CheckMarkEmoji + "Successfully sent message." + ); return; } throw new Error("???"); } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply(utils.XEmoji + "Something went wrong."); } } @@ -158,7 +160,7 @@ export async function handleWhoSaidCommand( } } catch (e) { await interaction.editReply( - "❌ Something went wrong, maybe wrong message ID?" + utils.XEmoji + "Something went wrong, maybe wrong message ID?" ); } } diff --git a/src/modules/polls.ts b/src/modules/polls.ts index 8969e25..9db7e91 100644 --- a/src/modules/polls.ts +++ b/src/modules/polls.ts @@ -16,6 +16,7 @@ import cron from "node-cron"; import { PrismaClient, Poll } from "@prisma/client"; import { CommandDescriptor } from "../bot.d"; +import * as utils from "./utils"; const POLL_ACTION_ROW = new MessageActionRow(); POLL_ACTION_ROW.addComponents([ @@ -273,11 +274,13 @@ export async function handleCommand( } await interaction.editReply( - `✅ Successfully added${cron ? " and scheduled" : ""}.` + utils.CheckMarkEmoji + + `Successfully added${cron ? " and scheduled" : ""}.` ); } catch (e) { await interaction.editReply( - "❌ Something went wrong, maybe the ID already exists?" + utils.XEmoji + + "Something went wrong, maybe the ID already exists?" ); } @@ -297,9 +300,13 @@ export async function handleCommand( await prisma.poll.delete({ where: { id } }); - await interaction.editReply("✅ Successfully removed."); + await interaction.editReply( + utils.CheckMarkEmoji + "Successfully removed." + ); } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } break; @@ -326,7 +333,9 @@ export async function handleCommand( ], }); } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } break; @@ -341,7 +350,7 @@ export async function handleCommand( if (poll === null) { await interaction.editReply( - "❌ No poll found with that ID." + utils.XEmoji + "No poll found with that ID." ); } else { await interaction.editReply({ @@ -365,7 +374,9 @@ export async function handleCommand( }); } } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } break; diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index ff8be4c..97eaa3d 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -1,18 +1,33 @@ // Handler for role selection -import { PrismaClient } from "@prisma/client"; +import { PrismaClient, RoleGroup, RoleGroupOption } from "@prisma/client"; import Discord from "discord.js"; import * as Builders from "@discordjs/builders"; import { getConfigFactory } from "./utils"; import { CommandDescriptor } from "../bot.d"; +import * as utils from "./utils"; +import * as courses from "./courses"; const MAX_COMPONENTS_PER_ROW = 5; const MAX_ROWS_PER_MESSAGE = 5; +// * IMPORTANT: all injected group IDs must start with __ to prevent collisions! + const TOURIST_GROUP_ID = "__tourist"; // must be unique in database const TOURIST_BUTTON_STYLE = "SECONDARY"; +const injectedGroups: { + // FIXME: find a better way to do this, without global vars + [groupId: string]: RoleGroup & { + options: RoleGroupOption[]; + onSendCallback?: ( + prisma: PrismaClient, + messageId: Discord.Snowflake + ) => Promise; + }; +} = {}; + // TODO: load from fénix into database export async function sendRoleSelectionMessages( @@ -24,37 +39,7 @@ export async function sendRoleSelectionMessages( include: { options: true }, }); - const getTouristConfig = getConfigFactory(prisma, "tourist"); - - try { - groups.push({ - id: TOURIST_GROUP_ID, - mode: "buttons", - placeholder: "N/A", - message: - (await getTouristConfig("message")) ?? - "Press if you're not from IST", - channelId: - (await getTouristConfig("channel_id", true)) ?? "missing", - messageId: (await getTouristConfig("message_id")) ?? null, - minValues: null, - maxValues: null, - options: [ - { - label: (await getTouristConfig("label")) ?? "I'm a TourIST", - description: TOURIST_BUTTON_STYLE, - value: - (await getTouristConfig("role_id", true)) ?? "missing", - emoji: null, - roleGroupId: TOURIST_GROUP_ID, - }, - ], - }); - } catch (e) { - console.error( - `Failed to inject tourist group: ${(e as Error).message}` - ); - } + await injectGroups(prisma, groups); for (const group of groups) { try { @@ -77,7 +62,10 @@ export async function sendRoleSelectionMessages( let components; if (group.mode === "menu") { - if (group.options.length < (group.maxValues ?? 1)) { + if ( + (group.maxValues ?? 1) > 0 && + group.options.length < (group.maxValues ?? 1) + ) { throw new Error( `Requires at least ${ group.maxValues ?? 1 @@ -91,7 +79,11 @@ export async function sendRoleSelectionMessages( .setCustomId(`roleSelection:${group.id}`) .setPlaceholder(group.placeholder) .setMinValues(group.minValues ?? 1) - .setMaxValues(group.maxValues ?? 1) + .setMaxValues( + ((v) => (v < 0 ? group.options.length : v))( + group.maxValues ?? 1 + ) + ) .addOptions( group.options as Discord.MessageSelectOptionData[] ) @@ -162,13 +154,11 @@ export async function sendRoleSelectionMessages( ? await message.edit(args) : await (channel as Discord.TextChannel).send(args); - if (group.id === TOURIST_GROUP_ID) { - const key = "tourist:message_id"; - await prisma.config.upsert({ - where: { key }, - create: { key, value: msg.id }, - update: { value: msg.id }, - }); + if (group.id.startsWith("__")) { + await injectedGroups[group.id]?.onSendCallback?.( + prisma, + msg.id + ); } else { await prisma.roleGroup.update({ where: { id: group.id }, @@ -187,6 +177,80 @@ export async function sendRoleSelectionMessages( } } +async function injectGroups( + prisma: PrismaClient, + groups: (RoleGroup & { options: RoleGroupOption[] })[] +) { + try { + await injectTouristGroup(prisma, groups); + (await courses.getRoleSelectionGroupsForInjection(prisma)).map( + (g) => + groups.push(g) && + (injectedGroups[g.id] = { + ...g, + onSendCallback: async (prisma, messageId) => { + await prisma.courseRoleSelectionMessage.upsert({ + where: { injectedRoleGroupId: g.id }, + update: { messageId }, + create: { injectedRoleGroupId: g.id, messageId }, + }); + }, + }) + ); + } catch (e) { + await console.error(`Failed to inject groups: ${e}`); + } +} + +async function injectTouristGroup( + prisma: PrismaClient, + groups: (RoleGroup & { options: RoleGroupOption[] })[] +) { + const getTouristConfig = getConfigFactory(prisma, "tourist"); + + try { + const group = { + id: TOURIST_GROUP_ID, + mode: "buttons", + placeholder: "N/A", + message: + (await getTouristConfig("message")) ?? + "Press if you're not from IST", + channelId: + (await getTouristConfig("channel_id", true)) ?? "missing", + messageId: (await getTouristConfig("message_id")) ?? null, + minValues: null, + maxValues: null, + options: [ + { + label: (await getTouristConfig("label")) ?? "I'm a TourIST", + description: TOURIST_BUTTON_STYLE, + value: + (await getTouristConfig("role_id", true)) ?? "missing", + emoji: null, + roleGroupId: TOURIST_GROUP_ID, + }, + ], + }; + groups.push(group); + injectedGroups[TOURIST_GROUP_ID] = { + ...group, + onSendCallback: async (prisma, messageId) => { + const key = "tourist:message_id"; + await prisma.config.upsert({ + where: { key }, + create: { key, value: messageId }, + update: { value: messageId }, + }); + }, + }; + } catch (e) { + console.error( + `Failed to inject tourist group: ${(e as Error).message}` + ); + } +} + async function handleRoleSelection( groupId: string, roles: Discord.GuildMemberRoleManager, @@ -213,10 +277,10 @@ async function handleRoleSelection( ] : ( ( - await prisma.roleGroup.findFirst({ + (await prisma.roleGroup.findFirst({ where: { id: groupId }, include: { options: true }, - }) + })) ?? injectedGroups[groupId] )?.options.map((o) => o.value) ?? [] ).concat( [ @@ -258,9 +322,9 @@ export async function handleRoleSelectionMenu( const roles = interaction.member?.roles as Discord.GuildMemberRoleManager; if (await handleRoleSelection(groupId, roles, interaction.values, prisma)) { - await interaction.editReply("✅ Role applied."); + await interaction.editReply(utils.CheckMarkEmoji + "Role applied."); } else { - await interaction.editReply("❌ Failed to apply role."); + await interaction.editReply(utils.XEmoji + "Failed to apply role."); } } // FIXME: these two (v & ^) are a bit humid @@ -274,9 +338,9 @@ export async function handleRoleSelectionButton( const roles = interaction.member?.roles as Discord.GuildMemberRoleManager; if (await handleRoleSelection(sp[1], roles, [sp[2]], prisma)) { - await interaction.editReply("✅ Role applied."); + await interaction.editReply(utils.CheckMarkEmoji + "Role applied."); } else { - await interaction.editReply("❌ Failed to apply role."); + await interaction.editReply(utils.XEmoji + "Failed to apply role."); } } @@ -340,7 +404,7 @@ export function provideCommands(): CommandDescriptor[] { new Builders.SlashCommandIntegerOption() .setName("max") .setDescription( - "At most how many options may be selected; default 1" + "At most how many options may be selected; default 1; negative = all" ) .setRequired(false) ) @@ -414,7 +478,7 @@ export function provideCommands(): CommandDescriptor[] { new Builders.SlashCommandIntegerOption() .setName("max") .setDescription( - "At most how many options may be selected" + "At most how many options may be selected; negative = all" ) .setRequired(true) ) @@ -602,6 +666,15 @@ export function provideCommands(): CommandDescriptor[] { return [{ builder: cmd, handler: handleCommand }]; } +function validMinAndMaxValues(minValues: number, maxValues: number): boolean { + return ( + minValues > 0 && + minValues <= 25 && + maxValues <= 25 && + (maxValues < 0 ? true : minValues <= maxValues) + ); +} + async function createGroup( prisma: PrismaClient, id: string, @@ -615,8 +688,11 @@ async function createGroup( try { if (!id.match(/^[a-z0-9_]+$/)) { return [false, "Invalid id: must be snake_case"]; - } else if (id === TOURIST_GROUP_ID) { - return [false, "Tourist group ID must be unique in the database"]; + } else if (id.startsWith("__")) { + return [ + false, + "IDs starting with double underscore are reserved for injected groups", + ]; } const goodModes = ["buttons", "menu"]; @@ -629,13 +705,7 @@ async function createGroup( message = message.replace(/\\n/g, "\n"); - if ( - minValues < 0 || - minValues > 25 || - maxValues < 0 || - maxValues > 25 || - minValues > maxValues - ) { + if (!validMinAndMaxValues(minValues, maxValues)) { return [ false, "Minimum and maximum number of options must be between 0 and 25 and min<=max", @@ -732,13 +802,7 @@ async function setNumGroup( return "Invalid id: must be snake_case"; } - if ( - minValues < 0 || - minValues > 25 || - maxValues < 0 || - maxValues > 25 || - minValues > maxValues - ) { + if (!validMinAndMaxValues(minValues, maxValues)) { return "Minimum and maximum number of options must be between 0 and 25 and min<=max"; } @@ -934,11 +998,13 @@ export async function handleCommand( ); if (succ) { await interaction.editReply( - `✅ Role selection group \`${res}\` successfully created.` + utils.CheckMarkEmoji + + `Role selection group \`${res}\` successfully created.` ); } else { await interaction.editReply( - `❌ Could not create group because: **${res}**` + utils.XEmoji + + `Could not create group because: **${res}**` ); } break; @@ -952,11 +1018,13 @@ export async function handleCommand( ); if (res === true) { await interaction.editReply( - `✅ Role selection group \`${id}\` successfully deleted.` + utils.CheckMarkEmoji + + `Role selection group \`${id}\` successfully deleted.` ); } else { await interaction.editReply( - `❌ Could not delete group because: **${res}**` + utils.XEmoji + + `Could not delete group because: **${res}**` ); } break; @@ -971,11 +1039,13 @@ export async function handleCommand( ); if (res === true) { await interaction.editReply( - `✅ Role selection group \`${id}\` successfully edited.` + utils.CheckMarkEmoji + + `Role selection group \`${id}\` successfully edited.` ); } else { await interaction.editReply( - `❌ Could not edit group because: **${res}**` + utils.XEmoji + + `Could not edit group because: **${res}**` ); } break; @@ -990,11 +1060,13 @@ export async function handleCommand( ); if (res === true) { await interaction.editReply( - `✅ Role selection group \`${id}\` successfully edited.` + utils.CheckMarkEmoji + + `Role selection group \`${id}\` successfully edited.` ); } else { await interaction.editReply( - `❌ Could not edit group because: **${res}**` + utils.XEmoji + + `Could not edit group because: **${res}**` ); } break; @@ -1008,11 +1080,13 @@ export async function handleCommand( const res = await moveGroup(prisma, id, channel); if (res === true) { await interaction.editReply( - `✅ Role selection group \`${id}\` successfully moved to <#${channel.id}>.` + utils.CheckMarkEmoji + + `Role selection group \`${id}\` successfully moved to <#${channel.id}>.` ); } else { await interaction.editReply( - `❌ Could not move group because: **${res}**` + utils.XEmoji + + `Could not move group because: **${res}**` ); } break; @@ -1053,7 +1127,7 @@ export async function handleCommand( (e as Error).message ); await interaction.editReply( - "❌ Failed to list role groups." + utils.XEmoji + "Failed to list role groups." ); } break; @@ -1076,11 +1150,12 @@ export async function handleCommand( ); if (res === true) { await interaction.editReply( - "✅ Option successfully added." + utils.CheckMarkEmoji + "Option successfully added." ); } else { await interaction.editReply( - `❌ Could not add option because: **${res}**` + utils.XEmoji + + `Could not add option because: **${res}**` ); } break; @@ -1093,11 +1168,13 @@ export async function handleCommand( ); if (res === true) { await interaction.editReply( - "✅ Option successfully removed." + utils.CheckMarkEmoji + + "Option successfully removed." ); } else { await interaction.editReply( - `❌ Could not remove option because: **${res}**` + utils.XEmoji + + `Could not remove option because: **${res}**` ); } break; @@ -1118,7 +1195,8 @@ export async function handleCommand( ); await interaction.editReply( - "✅ Role selection messages successfully sent." + utils.CheckMarkEmoji + + "Role selection messages successfully sent." ); } catch (e) { console.error( @@ -1126,7 +1204,8 @@ export async function handleCommand( (e as Error).message ); await interaction.editReply( - "❌ Failed to send role selection messages." + utils.XEmoji + + "Failed to send role selection messages." ); } break; @@ -1146,7 +1225,9 @@ export async function handleCommand( .replace(/\\n/g, "\n"); if (!["message", "label"].includes(field)) { - await interaction.editReply("❌ Invalid name."); + await interaction.editReply( + utils.XEmoji + "Invalid name." + ); } const fqkey = `tourist:${field}`; @@ -1158,11 +1239,12 @@ export async function handleCommand( }); await interaction.editReply( - `✅ Successfully set TourIST ${field}.` + utils.CheckMarkEmoji + + `Successfully set TourIST ${field}.` ); } catch (e) { await interaction.editReply( - "❌ Failed to set TourIST info." + utils.XEmoji + "Failed to set TourIST info." ); } break; @@ -1174,7 +1256,9 @@ export async function handleCommand( ) as Discord.GuildChannel; if (!channel.isText() && !channel.isThread()) { - await interaction.editReply("❌ Invalid channel."); + await interaction.editReply( + utils.XEmoji + "Invalid channel." + ); } const fqkey = `tourist:channel_id`; @@ -1186,11 +1270,12 @@ export async function handleCommand( }); await interaction.editReply( - `✅ Successfully set TourIST channel.` + utils.CheckMarkEmoji + + `Successfully set TourIST channel.` ); } catch (e) { await interaction.editReply( - "❌ Failed to set TourIST channel." + utils.XEmoji + "Failed to set TourIST channel." ); } break; @@ -1210,11 +1295,12 @@ export async function handleCommand( }); await interaction.editReply( - "✅ Successfully set TourIST role." + utils.CheckMarkEmoji + + "Successfully set TourIST role." ); } catch (e) { await interaction.editReply( - "❌ Failed to set TourIST role." + utils.XEmoji + "Failed to set TourIST role." ); } break; @@ -1245,7 +1331,9 @@ export async function handleCommand( ); await interaction.editReply({ embeds: [embed] }); } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } break; diff --git a/src/modules/sudo.ts b/src/modules/sudo.ts index 4a576ca..371b3b6 100644 --- a/src/modules/sudo.ts +++ b/src/modules/sudo.ts @@ -5,6 +5,7 @@ import * as Discord from "discord.js"; import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; +import * as utils from "./utils"; export function provideCommands(): CommandDescriptor[] { const sudo = new Builders.SlashCommandBuilder() @@ -50,7 +51,7 @@ export async function handleSudoCommand( if (!roles.cache.has(aId) && !roles.cache.has(apId)) { await interaction.editReply( - "❌ User does not have administrator permissions." + utils.XEmoji + "User does not have administrator permissions." ); return; } @@ -58,14 +59,18 @@ export async function handleSudoCommand( if (roles.cache.has(apId)) { await roles.remove(apId); await interaction.editReply( - "✅ Successfully removed `Admin+` role." + utils.CheckMarkEmoji + "Successfully removed `Admin+` role." ); } else { await roles.add(apId); - await interaction.editReply("✅ Successfully added `Admin+` role."); + await interaction.editReply( + utils.CheckMarkEmoji + "Successfully added `Admin+` role." + ); } } catch (e) { - await interaction.editReply("❌ Failed to toggle `Admin+` role."); + await interaction.editReply( + utils.XEmoji + "Failed to toggle `Admin+` role." + ); } } @@ -79,14 +84,18 @@ export async function handleResetAdminCommand( if (!role) { await interaction.editReply( - "❌ Could not locate the `Admin+` role" + utils.XEmoji + "Could not locate the `Admin+` role" ); } role?.members.forEach((member) => member.roles.remove(role)); - await interaction.editReply("✅ Successfully reset the `Admin+` role."); + await interaction.editReply( + utils.CheckMarkEmoji + "Successfully reset the `Admin+` role." + ); } catch (e) { - await interaction.editReply("❌ Failed to reset the `Admin+` role."); + await interaction.editReply( + utils.XEmoji + "Failed to reset the `Admin+` role." + ); } } diff --git a/src/modules/utils.d.ts b/src/modules/utils.d.ts new file mode 100644 index 0000000..54add17 --- /dev/null +++ b/src/modules/utils.d.ts @@ -0,0 +1,8 @@ +// Utility typings + +import * as Discord from "discord.js"; + +export type MessageCollection = Discord.Collection; + +// ThenArgRecursive from https://stackoverflow.com/a/49889856 +export type ThenArg = T extends PromiseLike ? ThenArg : T; diff --git a/src/modules/utils.ts b/src/modules/utils.ts index fdc7055..652dcfb 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -5,8 +5,10 @@ import { performance } from "perf_hooks"; import { PrismaClient } from "@prisma/client"; import * as Discord from "discord.js"; -// ThenArgRecursive from https://stackoverflow.com/a/49889856 -export type ThenArg = T extends PromiseLike ? ThenArg : T; +import { MessageCollection } from "./utils.d"; + +export const XEmoji = "❌ "; +export const CheckMarkEmoji = "✅ "; export async function timeFunction( fun: () => Promise @@ -71,8 +73,6 @@ export async function fetchGalleries( .filter((c) => c.length); } -export type MessageCollection = Discord.Collection; - export async function fetchAllChannelMessages( channel: Discord.TextChannel | Discord.ThreadChannel, after?: Date @@ -93,3 +93,15 @@ export async function fetchAllChannelMessages( return messages; } + +export function removeDuplicatesFromArray( + array: T[], + getKey?: (item: T) => unknown +): T[] { + if (!getKey) getKey = (v) => v; + + return array.filter( + (value, i) => + !array.some((v, j) => j < i && getKey?.(value) === getKey?.(v)) + ); +} diff --git a/src/modules/voiceThreads.ts b/src/modules/voiceThreads.ts index 00a48ba..e3ffa1d 100644 --- a/src/modules/voiceThreads.ts +++ b/src/modules/voiceThreads.ts @@ -5,6 +5,7 @@ import * as Discord from "discord.js"; import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; +import * as utils from "./utils"; const VC_CHAT_KEY = "voice_threads:vc_chat"; @@ -50,7 +51,7 @@ export async function handleCommand( if (channel === undefined) { await interaction.editReply( - "❌ No channel is currently set as vc-chat." + utils.XEmoji + "No channel is currently set as vc-chat." ); } else { await interaction.editReply( @@ -58,7 +59,9 @@ export async function handleCommand( ); } } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } break; } @@ -71,7 +74,7 @@ export async function handleCommand( if (!channel.isText()) { await interaction.editReply( - "❌ Channel must be a text channel." + utils.XEmoji + "Channel must be a text channel." ); } else { await prisma.config.upsert({ @@ -83,11 +86,14 @@ export async function handleCommand( }, }); await interaction.editReply( - `✅ Successfully set vc-chat to <#${channel.id}>.` + utils.CheckMarkEmoji + + `Successfully set vc-chat to <#${channel.id}>.` ); } } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); } break; } diff --git a/src/modules/welcome.ts b/src/modules/welcome.ts index 1a5cd4c..bbeeb81 100644 --- a/src/modules/welcome.ts +++ b/src/modules/welcome.ts @@ -5,6 +5,7 @@ import * as Discord from "discord.js"; import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; +import * as utils from "./utils"; async function getWelcomeChannel( prisma: PrismaClient, @@ -83,7 +84,7 @@ export async function handleCommand( ) as Discord.GuildChannel; if (!channel.isText()) { await interaction.editReply( - "❌ Channel must be a text channel." + utils.XEmoji + "Channel must be a text channel." ); } else { await prisma.config.upsert({ @@ -95,7 +96,8 @@ export async function handleCommand( }, }); await interaction.editReply( - `✅ Welcome channel successfully set as <#${channel.id}>.` + utils.CheckMarkEmoji + + `Welcome channel successfully set as <#${channel.id}>.` ); } break; @@ -109,7 +111,7 @@ export async function handleCommand( create: { key: "welcome:message", value }, }); await interaction.editReply( - `✅ Welcome message successfully set.` + utils.CheckMarkEmoji + `Welcome message successfully set.` ); break; } @@ -135,6 +137,6 @@ export async function handleCommand( } } } catch (e) { - await interaction.editReply("❌ Something went wrong."); + await interaction.editReply(utils.XEmoji + "Something went wrong."); } } diff --git a/src/prisma/migrations/20211003144607_add_degree_course_features/migration.sql b/src/prisma/migrations/20211003144607_add_degree_course_features/migration.sql new file mode 100644 index 0000000..10cc297 --- /dev/null +++ b/src/prisma/migrations/20211003144607_add_degree_course_features/migration.sql @@ -0,0 +1,79 @@ +/* + Warnings: + + - Made the column `role_group_id` on table `role_group_options` required. This step will fail if there are existing NULL values in that column. + +*/ +-- CreateTable +CREATE TABLE "degrees" ( + "fenix_id" TEXT NOT NULL PRIMARY KEY, + "acronym" TEXT NOT NULL, + "name" TEXT NOT NULL, + "role_id" TEXT NOT NULL, + "tier" INTEGER NOT NULL, + "degree_text_channel_id" TEXT, + "degree_voice_channel_id" TEXT, + "announcements_channel_id" TEXT, + "course_selection_channel_id" TEXT +); + +-- CreateTable +CREATE TABLE "courses" ( + "acronym" TEXT NOT NULL PRIMARY KEY, + "display_acronym" TEXT NOT NULL, + "name" TEXT NOT NULL, + "channel_id" TEXT, + "role_id" TEXT, + "hideChannel" BOOLEAN NOT NULL DEFAULT false +); + +-- CreateTable +CREATE TABLE "degree_courses" ( + "id" TEXT NOT NULL PRIMARY KEY, + "degree_fenix_id" TEXT NOT NULL, + "course_acronym" TEXT NOT NULL, + "year" INTEGER NOT NULL, + "semester" INTEGER NOT NULL, + CONSTRAINT "degree_courses_degree_fenix_id_fkey" FOREIGN KEY ("degree_fenix_id") REFERENCES "degrees" ("fenix_id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "degree_courses_course_acronym_fkey" FOREIGN KEY ("course_acronym") REFERENCES "courses" ("acronym") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "course_role_selection_messages" ( + "injected_role_group_id" TEXT NOT NULL PRIMARY KEY, + "message_id" TEXT NOT NULL +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_role_group_options" ( + "label" TEXT NOT NULL, + "description" TEXT NOT NULL, + "value" TEXT NOT NULL PRIMARY KEY, + "emoji" TEXT, + "role_group_id" TEXT NOT NULL, + CONSTRAINT "role_group_options_role_group_id_fkey" FOREIGN KEY ("role_group_id") REFERENCES "role_groups" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_role_group_options" ("description", "emoji", "label", "role_group_id", "value") SELECT "description", "emoji", "label", "role_group_id", "value" FROM "role_group_options"; +DROP TABLE "role_group_options"; +ALTER TABLE "new_role_group_options" RENAME TO "role_group_options"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; + +-- CreateIndex +CREATE UNIQUE INDEX "degrees_acronym_key" ON "degrees"("acronym"); + +-- CreateIndex +CREATE UNIQUE INDEX "degrees_name_key" ON "degrees"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "courses_display_acronym_key" ON "courses"("display_acronym"); + +-- CreateIndex +CREATE UNIQUE INDEX "courses_name_key" ON "courses"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "courses_channel_id_key" ON "courses"("channel_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "courses_role_id_key" ON "courses"("role_id"); diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 2e4918b..bee1bea 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -15,11 +15,11 @@ model Config { } model Poll { - id String @id /// identifier used to keep track of embed on pinned messages + id String @id /// identifier used to keep track of embed on pinned messages type String title String cron String? /// cron schedule - channelId String @map("channel_id") /// channel where to post the poll + channelId String @map("channel_id") /// channel where to post the poll @@map("polls") } @@ -41,14 +41,60 @@ model RoleGroup { model RoleGroupOption { label String description String - value String @id + value String @id emoji String? - RoleGroup RoleGroup? @relation(fields: [roleGroupId], references: [id], onDelete: Cascade) - roleGroupId String? @map("role_group_id") + RoleGroup RoleGroup @relation(fields: [roleGroupId], references: [id], onDelete: Cascade) + roleGroupId String @map("role_group_id") @@map("role_group_options") } +model Degree { + fenixId String @id @map("fenix_id") + acronym String @unique /// do not use this with fenix api! leti != lerc + name String @unique + roleId String @map("role_id") /// not unique, e.g. [lm]eic = leic + meic + tier Int /// 0 = none, 1 = degree channels (text + VC), 2 = +course channels, 3 = +announcements channel + degreeTextChannelId String? @map("degree_text_channel_id") + degreeVoiceChannelId String? @map("degree_voice_channel_id") + announcementsChannelId String? @map("announcements_channel_id") + courseSelectionChannelId String? @map("course_selection_channel_id") + courses DegreeCourse[] + + @@map("degrees") +} + +model Course { + acronym String @id /// let's hope this is unique + displayAcronym String @unique @map("display_acronym") // e.g., fp > fprog + name String @unique + channelId String? @unique @map("channel_id") + roleId String? @unique @map("role_id") + hideChannel Boolean @default(false) + perDegreeImplementations DegreeCourse[] + + @@map("courses") +} + +model DegreeCourse { + id String @id /// due to course groupings using the same fenixId, this should be `${degreeAcronym}-${fenixId}` + degree Degree @relation(fields: [degreeFenixId], references: [fenixId], onDelete: Cascade) + degreeFenixId String @map("degree_fenix_id") + course Course @relation(fields: [courseAcronym], references: [acronym], onDelete: Cascade) + courseAcronym String @map("course_acronym") + year Int /// 1, 2, 3; not 2021, 2022, 2023 + semester Int /// 1 or 2 + + @@map("degree_courses") +} + +model CourseRoleSelectionMessage { + injectedRoleGroupId String @id @map("injected_role_group_id") + messageId String @map("message_id") + + @@map("course_role_selection_messages") +} + model LeaderboardEntry { userId String @id @map("user_id") characterCount Int @map("character_count") diff --git a/yarn.lock b/yarn.lock index ca6dabf..ebf42d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -376,11 +376,23 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -418,6 +430,30 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" +cheerio-select@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823" + integrity sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg== + dependencies: + css-select "^4.1.3" + css-what "^5.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + domutils "^2.7.0" + +cheerio@^1.0.0-rc.10: + version "1.0.0-rc.10" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" + integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== + dependencies: + cheerio-select "^1.5.0" + dom-serializer "^1.3.2" + domhandler "^4.2.0" + htmlparser2 "^6.1.0" + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter "^6.0.1" + tslib "^2.2.0" + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -504,6 +540,22 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-select@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" + integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA== + dependencies: + boolbase "^1.0.0" + css-what "^5.0.0" + domhandler "^4.2.0" + domutils "^2.6.0" + nth-check "^2.0.0" + +css-what@^5.0.0, css-what@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" + integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== + debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -564,6 +616,36 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^1.0.1, dom-serializer@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^4.0.0, domhandler@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f" + integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + dot-prop@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" @@ -583,6 +665,11 @@ enquirer@^2.3.5, enquirer@^2.3.6: dependencies: ansi-colors "^4.1.1" +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -805,6 +892,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== +follow-redirects@^1.14.0: + version "1.14.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" + integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -882,6 +974,16 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -1208,6 +1310,13 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +nth-check@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== + dependencies: + boolbase "^1.0.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1270,6 +1379,18 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse5-htmlparser2-tree-adapter@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + +parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -1541,16 +1662,16 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.2.0, tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + tslib@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== -tslib@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From b2c4239025699574e71f5e421adbe860d0f53312 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 3 Oct 2021 16:32:08 +0100 Subject: [PATCH 064/101] Add RSS feed announcements module (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafael Oliveira Co-authored-by: Luís Fonseca --- package.json | 5 +- src/bot.ts | 8 ++ src/modules/courses.ts | 109 +++++++++++++++++- src/modules/degrees.ts | 41 +++++++ src/modules/fenix.d.ts | 12 ++ src/modules/fenix.ts | 50 +++++++- src/modules/rss.ts | 103 +++++++++++++++++ src/modules/utils.ts | 8 ++ .../migration.sql | 19 +++ src/prisma/schema.prisma | 17 +-- yarn.lock | 45 +++++++- 11 files changed, 401 insertions(+), 16 deletions(-) create mode 100644 src/modules/rss.ts create mode 100644 src/prisma/migrations/20211003151727_rss_announcements/migration.sql diff --git a/package.json b/package.json index 678a47c..9f89ecb 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,15 @@ "cheerio": "^1.0.0-rc.10", "discord.js": "^13.0.1", "node-cron": "^3.0.0", - "path": "^0.12.7" + "path": "^0.12.7", + "rss-parser": "^3.12.0", + "turndown": "^7.1.1" }, "devDependencies": { "@tsconfig/node14": "^1.0.1", "@types/node": "^15.12.5", "@types/node-cron": "^2.0.4", + "@types/turndown": "^5.0.1", "@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/parser": "^4.28.1", "eslint": "^7.29.0", diff --git a/src/bot.ts b/src/bot.ts index aa58244..78f0e83 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -21,6 +21,7 @@ import * as welcome from "./modules/welcome"; import * as leaderboard from "./modules/leaderboard"; import * as degrees from "./modules/degrees"; import * as courses from "./modules/courses"; +import * as rss from "./modules/rss"; for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { if (process.env[ev] === undefined) { @@ -184,6 +185,13 @@ const startupChores: Chore[] = [ }, complete: "All slash command permissions overwritten", }, + { + summary: "Start RSS cron job", + fn: async () => { + rss.scheduleRSSFeedJob(prisma, client); + }, + complete: "Finished starting RSS cron job", + }, ]; client.on("ready", async () => { diff --git a/src/modules/courses.ts b/src/modules/courses.ts index 75859fc..ee2f552 100644 --- a/src/modules/courses.ts +++ b/src/modules/courses.ts @@ -82,6 +82,31 @@ export function provideCommands(): CommandDescriptor[] { .setRequired(true) ) ); + cmd.addSubcommandGroup( + new Builders.SlashCommandSubcommandGroupBuilder() + .setName("academic_year") + .setDescription("Manage the current academic year") + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("set") + .setDescription( + "Set the current academic year (e.g. 2020-2021)" + ) + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("academic_year") + .setDescription( + "The current academic year (e.g. 2020-2021)" + ) + .setRequired(true) + ) + ) + .addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("get") + .setDescription("Get the current academic year") + ) + ); return [{ builder: cmd, handler: handleCommand }]; } @@ -92,7 +117,10 @@ export async function handleCommand( ): Promise { if (!interaction.guild) return; - switch (interaction.options.getSubcommand()) { + switch ( + interaction.options.getSubcommandGroup(false) || + interaction.options.getSubcommand() + ) { case "refresh-channels": { try { const orphanChannels = await refreshCourses( @@ -307,14 +335,87 @@ export async function handleCommand( break; } + case "academic_year": { + const subcommand = interaction.options.getSubcommand(); + switch (subcommand) { + case "get": { + try { + const academicYear = ( + await prisma.config.findUnique({ + where: { key: "academic_year" }, + }) + )?.value; + + if (academicYear) { + await interaction.editReply( + `The current academic year is \`${academicYear}\`` + ); + } else { + await interaction.editReply( + `No academic year is currently set` + ); + } + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + break; + } + case "set": { + try { + const academicYear = interaction.options.getString( + "academic_year", + true + ); + + await prisma.config.upsert({ + where: { key: "academic_year" }, + update: { value: academicYear }, + create: { + key: "academic_year", + value: academicYear, + }, + }); + + await interaction.editReply( + `The current academic year has been set to \`${academicYear}\`` + ); + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + break; + } + } + break; + } } } export async function importCoursesFromDegree( prisma: PrismaClient, - degreeId: string + degreeId: string, + force = false ): Promise { - const degreeCourses = await fenix.getDegreeCourses(degreeId); + const academicYear = ( + await prisma.config.findUnique({ where: { key: "academic_year" } }) + )?.value; + + if (!academicYear) { + throw Error("Academic year is not defined"); + } + + const degreeCourses = await fenix.getDegreeCourses(degreeId, academicYear); + + if (force) { + await prisma.degreeCourse.deleteMany({ + where: { degreeFenixId: degreeId }, + }); + } await Promise.all( degreeCourses.map(async (course) => { @@ -341,6 +442,8 @@ export async function importCoursesFromDegree( courseAcronym: course.acronym, year: course.year, semester: course.semester, + announcementsFeedUrl: course.announcementsFeedUrl, + color: utils.generateHexCode(), }, }); }) diff --git a/src/modules/degrees.ts b/src/modules/degrees.ts index 83fa6d4..7c3e895 100644 --- a/src/modules/degrees.ts +++ b/src/modules/degrees.ts @@ -190,6 +190,17 @@ export function provideCommands(): CommandDescriptor[] { .setRequired(true) ) ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("refresh-courses") + .setDescription("Refresh degree courses from Fenix") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("The acronym of the degree to refresh") + .setRequired(true) + ) + ); return [{ builder: cmd, handler: handleCommand }]; } @@ -647,5 +658,35 @@ export async function handleCommand( break; } + case "refresh-courses": { + try { + const acronym = interaction.options.getString("acronym", true); + + const degree = await prisma.degree.findUnique({ + where: { acronym }, + }); + if (!degree) { + await interaction.editReply( + utils.XEmoji + `Degree \`${acronym}\` not found!` + ); + return; + } + + await courses.importCoursesFromDegree( + prisma, + degree.fenixId, + true + ); + + await interaction.editReply( + utils.CheckMarkEmoji + + `Degree \`${acronym}\`'s courses have been refreshed!` + ); + } catch (e) { + await interaction.editReply( + utils.XEmoji + "Something went wrong." + ); + } + } } } diff --git a/src/modules/fenix.d.ts b/src/modules/fenix.d.ts index c33b4c9..a0e4490 100644 --- a/src/modules/fenix.d.ts +++ b/src/modules/fenix.d.ts @@ -22,4 +22,16 @@ export interface FenixDegreeCourse { name: string; year: number; semester: number; + announcementsFeedUrl?: string; +} + +export interface RSSFeedItem { + pubDate: string; +} + +export interface RSSCourseAnnouncement extends RSSFeedItem { + title: string; + content: string; + link: string; + author: string; } diff --git a/src/modules/fenix.ts b/src/modules/fenix.ts index 7fd62c1..fc7ee95 100644 --- a/src/modules/fenix.ts +++ b/src/modules/fenix.ts @@ -2,10 +2,13 @@ import axios from "axios"; import cheerio from "cheerio"; +import RSSParser from "rss-parser"; import * as FenixTypings from "./fenix.d"; import * as utils from "./utils"; +const parser = new RSSParser(); + export const axiosClient = axios.create({ baseURL: "https://fenix.tecnico.ulisboa.pt", params: { @@ -55,7 +58,8 @@ export async function getDegrees(): Promise { } export async function getDegreeCourses( - degreeId: string + degreeId: string, + academicYear: string ): Promise { const degrees = await getDegrees(); const shortDegree = degrees.find((d) => d.id.toLowerCase() === degreeId); @@ -108,14 +112,34 @@ export async function getDegreeCourses( const coursePageHtml = (await callEndpoint( course.acronym )) as string; + const $coursePage = cheerio.load(coursePageHtml); const acronym = - cheerio - .load(coursePageHtml)("#content-block h1 small") + $coursePage("#content-block h1 small") .first() .text() ?.trim() || course.name; - return { ...course, acronym }; + const executionCourseUrl = $coursePage("#content-block a") + .map((_, linkNode) => { + const executionCourseLink = $coursePage(linkNode) + .attr("href") + ?.match(/\/disciplinas\/\w+\/([\w-]+)\/[\w-]+/); + if ( + !executionCourseLink || + executionCourseLink[1] !== academicYear + ) + return null; + + return executionCourseLink[0]; + }) + .toArray() + .find((v) => !!v); + + const rssUrl = + executionCourseUrl && + `${executionCourseUrl}/rss/announcement`; + + return { ...course, acronym, announcementsFeedUrl: rssUrl }; }) ); @@ -124,3 +148,21 @@ export async function getDegreeCourses( (courses) => courses.acronym ); } + +export async function getRSSFeed( + url: string, + after: Date +): Promise { + const data = await callEndpoint(url); + const json = await parser.parseString(data as string); + + let item: T[] = (json?.items || []) as T[]; + if (!Array.isArray(item)) item = [item]; + + return item + .filter((v) => new Date(v.pubDate) > after) + .sort( + (a, b) => + new Date(a.pubDate).getTime() - new Date(b.pubDate).getTime() + ); +} diff --git a/src/modules/rss.ts b/src/modules/rss.ts new file mode 100644 index 0000000..23ebf99 --- /dev/null +++ b/src/modules/rss.ts @@ -0,0 +1,103 @@ +import Discord from "discord.js"; +import { PrismaClient } from "@prisma/client"; +import cron from "node-cron"; +import TurndownService from "turndown"; + +import * as FenixAPI from "./fenix"; +import * as FenixTypings from "./fenix.d"; + +const turndownService = new TurndownService(); + +export function scheduleRSSFeedJob( + prisma: PrismaClient, + client: Discord.Client +): void { + cron.schedule("*/2 * * * *", runRSSFeedJob(prisma, client)); +} + +export function runRSSFeedJob( + prisma: PrismaClient, + client: Discord.Client +): () => Promise { + return async function () { + const degreeCourse = await prisma.degreeCourse.findMany({ + select: { + id: true, + announcementsFeedUrl: true, + feedLastUpdated: true, + color: true, + degree: true, + course: true, + }, + }); + + degreeCourse + .filter((course) => !!course.announcementsFeedUrl) + .forEach(async (course) => { + const degreeChannel = await client.channels.fetch( + course.degree.degreeTextChannelId || "" + ); + if (!degreeChannel?.isText()) return; + + const announcements = + await FenixAPI.getRSSFeed( + course.announcementsFeedUrl || "", + course.feedLastUpdated + ); + + await announcements.reduce( + async (prevPromise, announcement) => { + await prevPromise; + + await degreeChannel.send({ + content: `Novo anúncio de ${course.course.name}${ + course.course.roleId + ? ` <@&${course.course.roleId}>` + : `` + }`, + embeds: [ + { + title: announcement.title?.substring( + 0, + 256 + ), + description: turndownService + .turndown( + announcement.content || + "Não foi possível obter o conteúdo deste anúncio" + ) + .substring(0, 2048), + url: announcement.link, + color: parseInt( + (course.color || "#00a0e4").substring( + 1 + ), + 16 + ), + author: { + name: + announcement.author.match( + /\((.+)\)/ + )?.[1] || announcement.author, + }, + footer: { + text: course.course.name, + }, + timestamp: new Date(announcement.pubDate), + }, + ], + }); + }, + Promise.resolve() + ); + + if (announcements.length > 0) { + const date = new Date(announcements.pop()?.pubDate || "."); + await prisma.degreeCourse.update({ + where: { id: course.id }, + data: { feedLastUpdated: date }, + }); + } + }); + }; +} diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 652dcfb..a9aa684 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -105,3 +105,11 @@ export function removeDuplicatesFromArray( !array.some((v, j) => j < i && getKey?.(value) === getKey?.(v)) ); } + +export function generateHexCode(): string { + let randomHexCode = "#"; + while (randomHexCode.length < 7) { + randomHexCode += Math.floor(Math.random() * 15).toString(16); + } + return randomHexCode; +} diff --git a/src/prisma/migrations/20211003151727_rss_announcements/migration.sql b/src/prisma/migrations/20211003151727_rss_announcements/migration.sql new file mode 100644 index 0000000..21d1341 --- /dev/null +++ b/src/prisma/migrations/20211003151727_rss_announcements/migration.sql @@ -0,0 +1,19 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_degree_courses" ( + "id" TEXT NOT NULL PRIMARY KEY, + "degree_fenix_id" TEXT NOT NULL, + "course_acronym" TEXT NOT NULL, + "year" INTEGER NOT NULL, + "semester" INTEGER NOT NULL, + "announcements_feed_url" TEXT, + "feed_last_updated" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "color" TEXT, + CONSTRAINT "degree_courses_degree_fenix_id_fkey" FOREIGN KEY ("degree_fenix_id") REFERENCES "degrees" ("fenix_id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "degree_courses_course_acronym_fkey" FOREIGN KEY ("course_acronym") REFERENCES "courses" ("acronym") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_degree_courses" ("course_acronym", "degree_fenix_id", "id", "semester", "year") SELECT "course_acronym", "degree_fenix_id", "id", "semester", "year" FROM "degree_courses"; +DROP TABLE "degree_courses"; +ALTER TABLE "new_degree_courses" RENAME TO "degree_courses"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index bee1bea..56eadf6 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -77,13 +77,16 @@ model Course { } model DegreeCourse { - id String @id /// due to course groupings using the same fenixId, this should be `${degreeAcronym}-${fenixId}` - degree Degree @relation(fields: [degreeFenixId], references: [fenixId], onDelete: Cascade) - degreeFenixId String @map("degree_fenix_id") - course Course @relation(fields: [courseAcronym], references: [acronym], onDelete: Cascade) - courseAcronym String @map("course_acronym") - year Int /// 1, 2, 3; not 2021, 2022, 2023 - semester Int /// 1 or 2 + id String @id /// due to course groupings using the same fenixId, this should be `${degreeAcronym}-${fenixId}` + degree Degree @relation(fields: [degreeFenixId], references: [fenixId], onDelete: Cascade) + degreeFenixId String @map("degree_fenix_id") + course Course @relation(fields: [courseAcronym], references: [acronym], onDelete: Cascade) + courseAcronym String @map("course_acronym") + year Int /// 1, 2, 3; not 2021, 2022, 2023 + semester Int /// 1 or 2 + announcementsFeedUrl String? @map("announcements_feed_url") + feedLastUpdated DateTime @default(now()) @map("feed_last_updated") + color String? @@map("degree_courses") } diff --git a/yarn.lock b/yarn.lock index ebf42d6..d142829 100644 --- a/yarn.lock +++ b/yarn.lock @@ -197,6 +197,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/turndown@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.1.tgz#fcda7b02cda4c9d445be1440036df20f335b9387" + integrity sha512-N8Ad4e3oJxh9n9BiZx9cbe/0M3kqDpOTm2wzj13wdDUxDPjfjloWIJaquZzWE1cYTAHpjOH3rcTnXQdpEfS/SQ== + "@types/tz-offset@*": version "0.0.0" resolved "https://registry.yarnpkg.com/@types/tz-offset/-/tz-offset-0.0.0.tgz#d58f1cebd794148d245420f8f0660305d320e565" @@ -637,6 +642,11 @@ domhandler@^4.0.0, domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" +domino@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe" + integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ== + domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" @@ -665,7 +675,7 @@ enquirer@^2.3.5, enquirer@^2.3.6: dependencies: ansi-colors "^4.1.1" -entities@^2.0.0: +entities@^2.0.0, entities@^2.0.3: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== @@ -1498,6 +1508,14 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rss-parser@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/rss-parser/-/rss-parser-3.12.0.tgz#b8888699ea46304a74363fbd8144671b2997984c" + integrity sha512-aqD3E8iavcCdkhVxNDIdg1nkBI17jgqF+9OqPS1orwNaOgySdpvq6B+DoONLhzjzwV8mWg37sb60e4bmLK117A== + dependencies: + entities "^2.0.3" + xml2js "^0.4.19" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -1512,6 +1530,11 @@ rxjs@^6.6.7: dependencies: tslib "^1.9.0" +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" @@ -1679,6 +1702,13 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +turndown@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.1.1.tgz#96992f2d9b40a1a03d3ea61ad31b5a5c751ef77f" + integrity sha512-BEkXaWH7Wh7e9bd2QumhfAXk5g34+6QUmmWx+0q6ThaVOLuLUqsnkq35HQ5SBHSaxjSfSM7US5o4lhJNH7B9MA== + dependencies: + domino "^2.1.6" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -1770,6 +1800,19 @@ ws@^7.5.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== +xml2js@^0.4.19: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From 78427e873dd47bb53e9a4a7d0ef1724f27f4068a Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 3 Oct 2021 16:34:11 +0100 Subject: [PATCH 065/101] Add @luishfonseca to contributors list --- src/modules/misc.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/misc.ts b/src/modules/misc.ts index 68df680..0e63413 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -92,6 +92,7 @@ export async function handleAboutCommand( [ ["Rafael Oliveira", "RafDevX"], ["Diogo Correia", "diogotcorreia"], + ["Luís Fonseca", "luishfonseca"], ].map((a) => ({ name: a[0], value: "[GitHub](https://github.com/" + a[1] + ")", From d651fd5b56a9c371ea0adb2cedd1037ef7a232c2 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 3 Oct 2021 16:35:19 +0100 Subject: [PATCH 066/101] Bump version to v2.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f89ecb..ae75ec9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.3.0", + "version": "2.4.0", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From a24c82fd378ea0943b6420c9f1f1a9d8ca7292e8 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 3 Oct 2021 18:47:37 +0100 Subject: [PATCH 067/101] Fix course role name not respecting displayAcronym --- src/modules/courses.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/courses.ts b/src/modules/courses.ts index ee2f552..7428d8d 100644 --- a/src/modules/courses.ts +++ b/src/modules/courses.ts @@ -520,8 +520,8 @@ export async function refreshCourses( mentionable: false, reason, }); - } else if (courseRole.name !== course.acronym) { - await courseRole.setName(course.acronym, reason); + } else if (courseRole.name !== course.displayAcronym) { + await courseRole.setName(course.displayAcronym, reason); } if (course.roleId !== courseRole.id) { await prisma.course.update({ From dd04e3a034d7d60cfa517d8267ed95d6f88adbf3 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 3 Oct 2021 18:47:55 +0100 Subject: [PATCH 068/101] Bump version to v2.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae75ec9..2f3af74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.4.0", + "version": "2.4.1", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From bea8f670ba3d2accd2826f8a55854efebd5c623e Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Sun, 3 Oct 2021 19:41:21 +0100 Subject: [PATCH 069/101] Add which degrees need course command (#81) --- src/modules/courses.ts | 75 ++++++++++++++++++++++++++++++++++++------ src/modules/degrees.ts | 4 +-- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/modules/courses.ts b/src/modules/courses.ts index 7428d8d..c86df12 100644 --- a/src/modules/courses.ts +++ b/src/modules/courses.ts @@ -56,7 +56,7 @@ export function provideCommands(): CommandDescriptor[] { ) .addBooleanOption( new Builders.SlashCommandBooleanOption() - .setName("delete_role") + .setName("delete-role") .setDescription( "If hiding channel, delete the course role as well (true by default)" ) @@ -69,22 +69,33 @@ export function provideCommands(): CommandDescriptor[] { .setDescription("Set display acronym of course") .addStringOption( new Builders.SlashCommandStringOption() - .setName("old_acronym") + .setName("old-acronym") .setDescription("The acronym of the course to be renamed") .setRequired(true) ) .addStringOption( new Builders.SlashCommandStringOption() - .setName("new_acronym") + .setName("new-acronym") .setDescription( "The acronym to show on channel name and role (e.g. CDI-I)" ) .setRequired(true) ) ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("list-degrees-with-course") + .setDescription("Show which degrees have a specific course") + .addStringOption( + new Builders.SlashCommandStringOption() + .setName("acronym") + .setDescription("The acronym of the course in question") + .setRequired(true) + ) + ); cmd.addSubcommandGroup( new Builders.SlashCommandSubcommandGroupBuilder() - .setName("academic_year") + .setName("academic-year") .setDescription("Manage the current academic year") .addSubcommand( new Builders.SlashCommandSubcommandBuilder() @@ -94,7 +105,7 @@ export function provideCommands(): CommandDescriptor[] { ) .addStringOption( new Builders.SlashCommandStringOption() - .setName("academic_year") + .setName("academic-year") .setDescription( "The current academic year (e.g. 2020-2021)" ) @@ -196,7 +207,7 @@ export async function handleCommand( true ); const deleteRole = - interaction.options.getBoolean("delete_role", false) ?? + interaction.options.getBoolean("delete-role", false) ?? true; const course = await prisma.course.findUnique({ @@ -268,11 +279,11 @@ export async function handleCommand( case "rename": { try { const oldAcronym = interaction.options.getString( - "old_acronym", + "old-acronym", true ); const newAcronym = interaction.options.getString( - "new_acronym", + "new-acronym", true ); @@ -335,7 +346,51 @@ export async function handleCommand( break; } - case "academic_year": { + case "list-degrees-with-course": { + try { + const acronym = interaction.options.getString("acronym", true); + const degrees = await prisma.degree.findMany({ + where: { + courses: { + some: { + course: { + OR: { + acronym: acronym, + displayAcronym: acronym, + }, + }, + }, + }, + }, + }); + + await interaction.editReply({ + embeds: [ + new Discord.MessageEmbed() + .setTitle("Degrees with Course") + .setDescription( + `Below are all degrees that need course \`${acronym}\`, as well as whether they have a tier high enough to justify having a channel for said course.` + ) + .addFields( + degrees.map((d) => ({ + name: d.acronym, + value: `Tier ${d.tier} ${ + d.tier >= 2 ? "✅" : "❌" + }`, + inline: true, + })) + ), + ], + }); + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + "Something went wrong" + ); + } + break; + } + case "academic-year": { const subcommand = interaction.options.getSubcommand(); switch (subcommand) { case "get": { @@ -366,7 +421,7 @@ export async function handleCommand( case "set": { try { const academicYear = interaction.options.getString( - "academic_year", + "academic-year", true ); diff --git a/src/modules/degrees.ts b/src/modules/degrees.ts index 7c3e895..d5bd690 100644 --- a/src/modules/degrees.ts +++ b/src/modules/degrees.ts @@ -429,12 +429,12 @@ export async function handleCommand( new Discord.MessageEmbed() .setTitle("All Degrees") .setDescription( - "Below are all known degrees, by acronym and Fénix ID" + "Below are all known degrees, by acronym, Fénix ID and tier" ) .addFields( degrees.map((d) => ({ name: d.acronym, - value: d.fenixId, + value: `${d.fenixId} (Tier ${d.tier})`, inline: true, })) ), From 2f07e7622e74c60dad33c253a2a86c7f61f3b6b0 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 3 Oct 2021 19:41:44 +0100 Subject: [PATCH 070/101] Bump version to v2.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f3af74..de955dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.4.1", + "version": "2.4.2", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From d2230bc8fbc114a5105887a5898ae5336849b669 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Sun, 10 Oct 2021 23:19:02 +0100 Subject: [PATCH 071/101] Improve leaderboard (#82) Ignore whitespace. Add clear cache command --- src/modules/leaderboard.ts | 50 +++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/modules/leaderboard.ts b/src/modules/leaderboard.ts index bd02681..59613b6 100644 --- a/src/modules/leaderboard.ts +++ b/src/modules/leaderboard.ts @@ -47,7 +47,11 @@ export async function getUsersCharacterCount( ) { chars.set( msg.author.id, - (chars.get(msg.author.id) ?? 0) + msg.content.length + (chars.get(msg.author.id) ?? 0) + + msg.content.replace( + /[^\p{Letter}\p{Number}\p{Punctuation}]/gu, + "" + ).length ); msgCount++; @@ -149,19 +153,33 @@ export async function sendLeaderboard( ]); } - chars.sort((v1, v2, k1, k2) => v2 - v1 || parseInt(k1) - parseInt(k2)); + chars.sort((v1, v2, k1, k2) => + v1 !== v2 ? v2 - v1 : parseInt(k2) - parseInt(k1) + ); const lines = []; for (const [uid, cs] of chars) { if (lines.length > MAX_PEOPLE) break; + let label = uid; + try { + try { + const member = await sendChannel.guild.members.fetch(uid); + label = member.toString(); + } catch (e) { + const user = await sendChannel.client.users.fetch(uid); + label = user.tag; + } + } catch (e) { + // do nothing + } lines.push( `\`#${(lines.length + 1) .toString() .padStart( Math.ceil(Math.log10(MAX_PEOPLE)), "0" - )}\` <@${uid}> (${cs})` + )}\` ${label} (${cs})` ); } @@ -207,6 +225,11 @@ export function provideCommands(): CommandDescriptor[] { .addChoice("Last 7 days", "week") ) ); + cmd.addSubcommand( + new Builders.SlashCommandSubcommandBuilder() + .setName("clear-cache") + .setDescription("Clear existing message cache") + ); return [ { builder: cmd, @@ -269,5 +292,26 @@ ${ } break; } + case "clear-cache": { + try { + await prisma.leaderboardEntry.deleteMany(); // CAREFUL! deletes everything! + const stamp = ( + await prisma.config.delete({ + where: { key: "leaderboard:cache_stamp" }, + }) + ).value; + await interaction.editReply( + utils.CheckMarkEmoji + + `Successfully reset cache; last stamp was ${stamp}` + ); + } catch (e) { + console.error(e); + await interaction.editReply( + utils.XEmoji + + "Something went wrong, maybe there was no cache?" + ); + } + break; + } } } From 66bfb36c354da8856975259d90598f9c71571c2f Mon Sep 17 00:00:00 2001 From: Diogo Cardoso Date: Sun, 10 Oct 2021 23:19:17 +0100 Subject: [PATCH 072/101] Add just-ask command (#84) --- src/modules/misc.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/modules/misc.ts b/src/modules/misc.ts index 0e63413..b48ed2b 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -59,6 +59,13 @@ export function provideCommands(): CommandDescriptor[] { builder: whoSaid, handler: handleWhoSaidCommand, }, + { + builder: new Builders.SlashCommandBuilder() + .setName("just-ask") + .setDescription("Send 'Don´t ask to ask' website"), + handler: handleJustAskCommand, + permission: CommandPermission.Public, + }, ]; } @@ -165,3 +172,15 @@ export async function handleWhoSaidCommand( ); } } + +export async function handleJustAskCommand( + interaction: Discord.CommandInteraction +): Promise { + try { + await interaction.channel?.send("https://dontasktoask.com/"); + await interaction.editReply(utils.CheckMarkEmoji + "Sent"); + } catch (e) { + console.error(e); + await interaction.editReply(utils.XEmoji + "Something went wrong."); + } +} From d586aa07398815a532423c5b6f07af9a9b5581db Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Sun, 10 Oct 2021 23:21:32 +0100 Subject: [PATCH 073/101] Simple command logs (#83) --- README.md | 1 + src/bot.ts | 45 ++++++++++++++++++++++++++++++++++++++++++-- src/modules/utils.ts | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fb096af..18cf77e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ services: GUILD_ID: PLACE_MAIN_GUILD_ID_HERE # or "GLOBAL" to use in multiple guilds (1hr roll-out time) ADMIN_ID: PLACE_ADMIN_ROLE_ID_HERE ADMIN_PLUS_ID: PLACE_ADMIN_PLUS_ROLE_ID_HERE + COMMAND_LOGS_CHANNEL_ID: PLACE_LOGGING_CHANNEL_ID_HERE TZ: Europe/Lisbon # default timezone for crontab and other date related stuff restart: unless-stopped ``` diff --git a/src/bot.ts b/src/bot.ts index 78f0e83..3d47d5b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -23,12 +23,18 @@ import * as degrees from "./modules/degrees"; import * as courses from "./modules/courses"; import * as rss from "./modules/rss"; -for (const ev of ["DISCORD_TOKEN", "GUILD_ID", "ADMIN_ID", "ADMIN_PLUS_ID"]) { +for (const ev of [ + "DISCORD_TOKEN", + "GUILD_ID", + "ADMIN_ID", + "ADMIN_PLUS_ID", + "COMMAND_LOGS_CHANNEL_ID", +]) { if (process.env[ev] === undefined) { throw new Error(`Missing environment variable; please set ${ev}!`); } } -const { DISCORD_TOKEN, GUILD_ID } = process.env; +const { DISCORD_TOKEN, GUILD_ID, COMMAND_LOGS_CHANNEL_ID } = process.env; export enum CommandPermission { Public, @@ -77,6 +83,8 @@ const menuHandlers: InteractionHandlers = { roleSelection: roleSelection.handleRoleSelectionMenu, }; +let commandLogsChannel: Discord.TextBasedChannels | undefined; + const startupChores: Chore[] = [ { summary: "Schedule polls", @@ -192,6 +200,25 @@ const startupChores: Chore[] = [ }, complete: "Finished starting RSS cron job", }, + { + summary: "Find command logging channel", + fn: async () => { + try { + const c = await client.channels.fetch( + COMMAND_LOGS_CHANNEL_ID ?? "" + ); + if (!c?.isText()) { + throw new Error("Wrong type"); + } else { + commandLogsChannel = + (await c.fetch()) as typeof commandLogsChannel; + } + } catch (e) { + throw new Error("Failed to find channel"); + } + }, + complete: "Successfully set command logging channel", + }, ]; client.on("ready", async () => { @@ -252,6 +279,20 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { } } + try { + const str = utils.stringifyCommand(interaction); + console.log(str); + await commandLogsChannel?.send({ + content: str, + allowedMentions: { parse: [] }, + }); + } catch (e) { + // do nothing + } + // TODO: show a X or CheckMark emoji before `str` to indicate whether + // TODO: the command was successful; that ruins the simplicity of the + // TODO: statement below vvvv, though :( + await commandHandlers[interaction.commandName]?.( interaction, prisma diff --git a/src/modules/utils.ts b/src/modules/utils.ts index a9aa684..e278aa9 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -113,3 +113,38 @@ export function generateHexCode(): string { } return randomHexCode; } + +export function stringifyCommand( + interaction: Discord.CommandInteraction +): string { + const subcommandGroup = interaction.options.getSubcommandGroup(false); + const subcommand = interaction.options.getSubcommand(false); + + const options: string[] = []; + + const extractOptions = ( + opt: readonly Discord.CommandInteractionOption[] | undefined + ): readonly Discord.CommandInteractionOption[] => + opt === undefined + ? [] + : opt.length === 1 && + ["SUB_COMMAND", "SUB_COMMAND_GROUP"].includes(opt[0].type) + ? extractOptions(opt[0].options) + : opt; + + for (const opt of extractOptions(interaction.options.data)) { + const specialValue = opt.channel ?? opt.member ?? opt.user ?? opt.role; + options.push( + `${opt.name}: ${opt.value?.toString()}` + + (specialValue ? ` (${specialValue})` : "") + ); + } + + return ( + `[${interaction.user.tag}]: /` + + interaction.commandName + + (subcommandGroup ? " " + subcommandGroup : "") + + (subcommand ? " " + subcommand : "") + + (options.length ? ["", ...options].join("\n-\t") : "none") + ); +} From d9a550587ca4384f67a40ff4e5d6dee8737f2937 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 10 Oct 2021 23:24:51 +0100 Subject: [PATCH 074/101] Bump version to v2.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de955dc..bbcfa58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.4.2", + "version": "2.5.0", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From 5d6781113027ece6e15017540825ca63dc2f6b91 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Mon, 11 Oct 2021 00:33:37 +0100 Subject: [PATCH 075/101] remove debug message --- src/modules/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/utils.ts b/src/modules/utils.ts index e278aa9..f161fe8 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -145,6 +145,6 @@ export function stringifyCommand( interaction.commandName + (subcommandGroup ? " " + subcommandGroup : "") + (subcommand ? " " + subcommand : "") + - (options.length ? ["", ...options].join("\n-\t") : "none") + (options.length ? ["", ...options].join("\n-\t") : "") ); } From 6002e27d7df6b6ba693a3fb80068e57ac3b69c85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 00:44:11 +0100 Subject: [PATCH 076/101] Bump ansi-regex from 5.0.0 to 5.0.1 (#85) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d142829..ba1bea3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -341,9 +341,9 @@ ansi-escapes@^4.3.0: type-fest "^0.21.3" ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.1: version "3.2.1" From 713a18a2788d2608653d2ef02b261d3550a71d21 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 12 Oct 2021 19:30:03 +0100 Subject: [PATCH 077/101] Bump version to v2.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bbcfa58..4664f28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.5.0", + "version": "2.5.1", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From fefd8eda5ed649d3b8bbfcff0ee90ab2e96da02a Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Tue, 9 Nov 2021 22:01:44 +0000 Subject: [PATCH 078/101] Fix leaderboard cache (#86) --- src/modules/leaderboard.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/modules/leaderboard.ts b/src/modules/leaderboard.ts index 59613b6..6b092c2 100644 --- a/src/modules/leaderboard.ts +++ b/src/modules/leaderboard.ts @@ -45,21 +45,20 @@ export async function getUsersCharacterCount( onlyCountAfter === undefined || msg.createdAt > onlyCountAfter ) { + const delta = msg.content.replace( + /[^\p{Letter}\p{Number}\p{Punctuation}]/gu, + "" + ).length; chars.set( msg.author.id, - (chars.get(msg.author.id) ?? 0) + - msg.content.replace( - /[^\p{Letter}\p{Number}\p{Punctuation}]/gu, - "" - ).length + (chars.get(msg.author.id) ?? 0) + delta ); msgCount++; if (cacheDate && msg.createdAt <= cacheDate) { addToCache.set( msg.author.id, - (addToCache.get(msg.author.id) ?? 0) + - msg.content.length + (addToCache.get(msg.author.id) ?? 0) + delta ); } } else { From 5d7022282a19c0b260de184b3de6ac2d424694fd Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 9 Nov 2021 22:02:29 +0000 Subject: [PATCH 079/101] Bump version to v2.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4664f28..c3b6f77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.5.1", + "version": "2.5.2", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From 3532187cf499ac6a6f9fa7f1f3d92163201b8b9e Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Fri, 26 Nov 2021 15:09:25 +0000 Subject: [PATCH 080/101] Fix tourist role selection button throwing error --- src/modules/roleSelection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 97eaa3d..89ccc61 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -265,7 +265,7 @@ async function handleRoleSelection( const groupRoles = groupId === TOURIST_GROUP_ID ? [ - selectedRoles, + ...selectedRoles, ...( await prisma.roleGroup.findMany({ where: { From 2ba701cc64a29159c4eb2cdc89612ab31bee8295 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Fri, 26 Nov 2021 15:10:03 +0000 Subject: [PATCH 081/101] Bump version to v2.5.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c3b6f77..1c2bf3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.5.2", + "version": "2.5.3", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From 481fcf59accd190350a45722148fbac640146e96 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 10 Mar 2022 23:00:27 +0000 Subject: [PATCH 082/101] =?UTF-8?q?Handle=20errors=20while=20fetching=20RS?= =?UTF-8?q?S=20feed=20from=20F=C3=A9nix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/fenix.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/modules/fenix.ts b/src/modules/fenix.ts index fc7ee95..8e734e9 100644 --- a/src/modules/fenix.ts +++ b/src/modules/fenix.ts @@ -153,16 +153,24 @@ export async function getRSSFeed( url: string, after: Date ): Promise { - const data = await callEndpoint(url); - const json = await parser.parseString(data as string); - - let item: T[] = (json?.items || []) as T[]; - if (!Array.isArray(item)) item = [item]; - - return item - .filter((v) => new Date(v.pubDate) > after) - .sort( - (a, b) => - new Date(a.pubDate).getTime() - new Date(b.pubDate).getTime() + try { + const data = await callEndpoint(url); + const json = await parser.parseString(data as string); + + let item: T[] = (json?.items || []) as T[]; + if (!Array.isArray(item)) item = [item]; + + return item + .filter((v) => new Date(v.pubDate) > after) + .sort( + (a, b) => + new Date(a.pubDate).getTime() - + new Date(b.pubDate).getTime() + ); + } catch (e) { + console.error( + `Could not get RSS feed for '${url}': ${(e as Error).message}` ); + return []; + } } From c9f6fe84a4eba2c0cb1cef178b44bc68f867f73d Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 10 Mar 2022 23:00:57 +0000 Subject: [PATCH 083/101] Bump version to v2.5.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c2bf3d..784462b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.5.3", + "version": "2.5.4", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From f0a76850725cbc67cdb9c145bbc27c6887baa44c Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Wed, 20 Apr 2022 08:38:45 +0100 Subject: [PATCH 084/101] Fix typo on Don't Ask to Ask command description (#92) --- src/modules/misc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/misc.ts b/src/modules/misc.ts index b48ed2b..5dde6c0 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -62,7 +62,7 @@ export function provideCommands(): CommandDescriptor[] { { builder: new Builders.SlashCommandBuilder() .setName("just-ask") - .setDescription("Send 'Don´t ask to ask' website"), + .setDescription("Send a link to the \"Don't ask to ask\" website"), handler: handleJustAskCommand, permission: CommandPermission.Public, }, From 0656b3552fde3c21ed64b6e780c23483fa94efbb Mon Sep 17 00:00:00 2001 From: Rafael Oliveira <56204853+RafDevX@users.noreply.github.com> Date: Tue, 13 Sep 2022 18:13:53 +0100 Subject: [PATCH 085/101] Add `migrate-members-with-role` command (#95) --- src/modules/misc.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/modules/misc.ts b/src/modules/misc.ts index 5dde6c0..f1d95f5 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -43,6 +43,27 @@ export function provideCommands(): CommandDescriptor[] { .setDescription("Message ID in question") .setRequired(true) ); + const migrateMembersWithRole = new SlashCommandBuilder() + .setName("migrate-members-with-role") + .setDescription("Gives a role to everyone who has a certain role"); + migrateMembersWithRole.addRoleOption( + new Builders.SlashCommandRoleOption() + .setName("old-role") + .setDescription("The role to migrate members from") + .setRequired(true) + ); + migrateMembersWithRole.addRoleOption( + new Builders.SlashCommandRoleOption() + .setName("new-role") + .setDescription("The role to migrate members to") + .setRequired(true) + ); + migrateMembersWithRole.addBooleanOption( + new Builders.SlashCommandBooleanOption() + .setName("remove-old") + .setDescription("Whether to remove the old role") + .setRequired(false) + ); return [ { builder: new Builders.SlashCommandBuilder() @@ -62,10 +83,16 @@ export function provideCommands(): CommandDescriptor[] { { builder: new Builders.SlashCommandBuilder() .setName("just-ask") - .setDescription("Send a link to the \"Don't ask to ask\" website"), + .setDescription( + 'Send a link to the "Don\'t ask to ask" website' + ), handler: handleJustAskCommand, permission: CommandPermission.Public, }, + { + builder: migrateMembersWithRole, + handler: handleMigrateMembersWithRole, + }, ]; } @@ -184,3 +211,38 @@ export async function handleJustAskCommand( await interaction.editReply(utils.XEmoji + "Something went wrong."); } } + +export async function handleMigrateMembersWithRole( + interaction: Discord.CommandInteraction +): Promise { + try { + const oldRole = interaction.options.getRole( + "old-role", + true + ) as Discord.Role; + const newRole = interaction.options.getRole( + "new-role", + true + ) as Discord.Role; + const removeOld = interaction.options.getBoolean("remove-old", false); + + let count = 0; + oldRole.members.forEach((member) => { + member.roles.add(newRole); + + if (removeOld) { + member.roles.remove(oldRole); + } + + count++; + }); + + await interaction.editReply( + utils.CheckMarkEmoji + + `Migrated ${count} members from ${oldRole} to ${newRole}` + ); + } catch (e) { + console.error(e); + await interaction.editReply(utils.XEmoji + "Something went wrong."); + } +} From bd4ebafbc6c934bd3ba427ff24b0b028656d34a4 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Tue, 13 Sep 2022 18:54:50 +0100 Subject: [PATCH 086/101] Bump version to v2.5.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 784462b..e091467 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.5.4", + "version": "2.5.5", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From ae7f5ae846f8c440d3c2befb0cb28f93753f5ad3 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 15 Sep 2022 14:35:21 +0100 Subject: [PATCH 087/101] Migrate to Discord.js v14 (#96) --- Dockerfile.dev | 4 +- package.json | 20 +- src/bot.d.ts | 2 +- src/bot.ts | 152 ++++---- src/logger.ts | 7 + src/modules/courses.ts | 87 +++-- src/modules/degrees.ts | 220 ++++++----- src/modules/fenix.ts | 11 +- src/modules/galleryChannels.ts | 6 +- src/modules/leaderboard.ts | 21 +- src/modules/misc.ts | 52 +-- src/modules/polls.ts | 97 ++--- src/modules/roleSelection.ts | 148 ++++---- src/modules/rss.ts | 165 +++++---- src/modules/sudo.ts | 9 +- src/modules/utils.ts | 8 +- src/modules/voiceThreads.ts | 13 +- src/modules/welcome.ts | 12 +- src/utils/buttonStyleUtils.ts | 17 + tsconfig.json | 2 +- yarn.lock | 644 ++++++++++++++++++++++----------- 21 files changed, 1013 insertions(+), 684 deletions(-) create mode 100644 src/logger.ts create mode 100644 src/utils/buttonStyleUtils.ts diff --git a/Dockerfile.dev b/Dockerfile.dev index f38808b..820c916 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:16.6.1-alpine3.14 +FROM node:16.17.0-alpine3.16 ARG DATABASE_URL ENV DATABASE_URL ${DATABASE_URL} WORKDIR /app @@ -8,4 +8,4 @@ RUN yarn install --frozen-lockfile COPY ./ ./ RUN yarn run prisma generate RUN yarn build -CMD [ "yarn", "start:docker" ] +CMD [ "yarn", "start:docker:dev" ] diff --git a/package.json b/package.json index e091467..dc4b919 100644 --- a/package.json +++ b/package.json @@ -15,25 +15,25 @@ "author": "IST Bot Team", "license": "MIT", "engines": { - "node": ">=16.6.1", + "node": ">=16.9.0", "npm": ">=7.23.0", "yarn": ">=1.22.0" }, "dependencies": { - "@discordjs/builders": "^0.6.0", - "@discordjs/rest": "^0.1.0-canary.0", + "@discordjs/rest": "^1.1.0", "@prisma/client": "^3.0.0", "@types/better-sqlite3": "^5.4.3", - "axios": "^0.21.4", - "cheerio": "^1.0.0-rc.10", - "discord.js": "^13.0.1", - "node-cron": "^3.0.0", + "axios": "^0.27.2", + "cheerio": "1.0.0-rc.10", + "discord.js": "^14.3.0", + "node-cron": "^3.0.2", "path": "^0.12.7", + "pino": "^8.5.0", "rss-parser": "^3.12.0", "turndown": "^7.1.1" }, "devDependencies": { - "@tsconfig/node14": "^1.0.1", + "@tsconfig/node16": "^1.0.3", "@types/node": "^15.12.5", "@types/node-cron": "^2.0.4", "@types/turndown": "^5.0.1", @@ -43,15 +43,17 @@ "eslint-config-prettier": "^8.3.0", "husky": "^6.0.0", "lint-staged": "^11.0.0", + "pino-pretty": "^9.1.0", "prettier": "^2.3.2", "prisma": "^3.0.0", - "typescript": "^4.3.4" + "typescript": "^4.8.3" }, "scripts": { "prepare": "husky install", "build": "tsc", "start": "node .", "start:docker": "prisma migrate deploy && node .", + "start:docker:dev": "npm run start:docker | pino-pretty -c", "prisma:generate": "prisma generate" }, "lint-staged": { diff --git a/src/bot.d.ts b/src/bot.d.ts index 57b320d..fca24be 100644 --- a/src/bot.d.ts +++ b/src/bot.d.ts @@ -15,7 +15,7 @@ export type InteractionHandlers = { export interface CommandDescriptor { builder: SlashCommandBuilder; - handler: InteractionHandler; + handler: InteractionHandler; permission?: CommandPermission; } diff --git a/src/bot.ts b/src/bot.ts index 3d47d5b..b72dd09 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,10 +1,12 @@ // Main controller -import Discord from "discord.js"; -import { +import Discord, { + ChannelType, + Client, + GatewayIntentBits, RESTPostAPIApplicationCommandsJSONBody, Routes, -} from "discord-api-types/v9"; +} from "discord.js"; import { REST } from "@discordjs/rest"; import { PrismaClient } from "@prisma/client"; @@ -23,6 +25,8 @@ import * as degrees from "./modules/degrees"; import * as courses from "./modules/courses"; import * as rss from "./modules/rss"; +import logger from "./logger"; + for (const ev of [ "DISCORD_TOKEN", "GUILD_ID", @@ -36,24 +40,21 @@ for (const ev of [ } const { DISCORD_TOKEN, GUILD_ID, COMMAND_LOGS_CHANNEL_ID } = process.env; +// this cannot be in bot.d.ts since declaration files are not copied to dist/ +// and enums are needed at runtime export enum CommandPermission { Public, Admin, - ServerOwner, } -// this cannot be in bot.d.ts since declaration files are not copied to dist/ -// and enums are needed at runtime - -const DEFAULT_COMMAND_PERMISSION: CommandPermission = CommandPermission.Admin; const prisma = new PrismaClient(); -const client = new Discord.Client({ +const client = new Client({ intents: [ - Discord.Intents.FLAGS.GUILDS, - Discord.Intents.FLAGS.GUILD_MESSAGES, - Discord.Intents.FLAGS.GUILD_VOICE_STATES, - Discord.Intents.FLAGS.GUILD_MEMBERS, // THIS IS A PRIVILEGED INTENT! MANUAL ACTION REQUIRED TO ENABLE! + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.GuildMembers, // THIS IS A PRIVILEGED INTENT! MANUAL ACTION REQUIRED TO ENABLE! ], }); @@ -71,7 +72,8 @@ const commandProviders: CommandProvider[] = [ ]; const commandPermissions: { [command: string]: CommandPermission } = {}; -const commandHandlers: InteractionHandlers = {}; +const commandHandlers: InteractionHandlers = + {}; // two above will be dynamically loaded const buttonHandlers: InteractionHandlers = { @@ -83,7 +85,7 @@ const menuHandlers: InteractionHandlers = { roleSelection: roleSelection.handleRoleSelectionMenu, }; -let commandLogsChannel: Discord.TextBasedChannels | undefined; +let commandLogsChannel: Discord.TextBasedChannel | undefined; const startupChores: Chore[] = [ { @@ -109,9 +111,14 @@ const startupChores: Chore[] = [ const name = descriptor.builder.name; commands.push( descriptor.builder - .setDefaultPermission( + // bot should only be used on the server + .setDMPermission(false) + // undefined leaves the default (everyone), 0 restricts to admins + .setDefaultMemberPermissions( descriptor.permission === CommandPermission.Public + ? undefined + : 0 ) .toJSON() ); @@ -122,7 +129,7 @@ const startupChores: Chore[] = [ } } - const rest = new REST({ version: "9" }).setToken( + const rest = new REST({ version: "10" }).setToken( DISCORD_TOKEN as string ); @@ -144,6 +151,7 @@ const startupChores: Chore[] = [ }, complete: "All slash commands registered", }, + /* This does not work with new Discord API { summary: "Update guild slash command permissions", fn: async () => { @@ -152,47 +160,51 @@ const startupChores: Chore[] = [ const fetched = await commands?.fetch(); - if (fetched) { - await commands?.permissions.set({ - fullPermissions: fetched.map((c) => { - let commandSpecificPermission: - | Discord.ApplicationCommandPermissionData - | undefined; - const perm = - commandPermissions[c.name] ?? - DEFAULT_COMMAND_PERMISSION; - switch (perm) { - case CommandPermission.Admin: + if (!fetched) { + throw new Error("Failed to fetch commands from guild."); + } + + await Promise.all( + fetched.map(async (c) => { + let commandSpecificPermission: + | Discord.ApplicationCommandPermissions + | undefined; + const perm = + commandPermissions[c.name] ?? + DEFAULT_COMMAND_PERMISSION; + switch (perm) { + case CommandPermission.Admin: + commandSpecificPermission = { + id: process.env.ADMIN_ID as string, + type: ApplicationCommandPermissionType.Role, + permission: true, + }; + break; + case CommandPermission.ServerOwner: { + const owner = guild?.ownerId; + if (owner) { commandSpecificPermission = { - id: process.env.ADMIN_ID as string, - type: "ROLE", + id: owner, + type: ApplicationCommandPermissionType.User, permission: true, }; - break; - case CommandPermission.ServerOwner: { - const owner = guild?.ownerId; - if (owner) { - commandSpecificPermission = { - id: owner, - type: "USER", - permission: true, - }; - } - break; } + break; } - return { - id: c.id, - permissions: commandSpecificPermission - ? [commandSpecificPermission] - : [], - }; - }), - }); - } + } + + if (commandSpecificPermission) { + await commands?.permissions.set({ + token: client.token as string, + command: c.id, + permissions: [commandSpecificPermission], + }); + } + }) + ); }, complete: "All slash command permissions overwritten", - }, + },*/ { summary: "Start RSS cron job", fn: async () => { @@ -207,7 +219,7 @@ const startupChores: Chore[] = [ const c = await client.channels.fetch( COMMAND_LOGS_CHANNEL_ID ?? "" ); - if (!c?.isText()) { + if (c?.type !== ChannelType.GuildText) { throw new Error("Wrong type"); } else { commandLogsChannel = @@ -222,16 +234,22 @@ const startupChores: Chore[] = [ ]; client.on("ready", async () => { - console.log(`Logged in as ${client.user?.tag}!`); + logger.info(`Logged in as ${client.user?.tag}!`); - console.log("Duty before self: starting chores..."); + logger.info("Duty before self: starting chores..."); for (const [i, chore] of startupChores.entries()) { const delta = await utils .timeFunction(chore.fn) - .catch((e) => console.error("Chore error:", chore.summary, "-", e)); + .catch((e) => + logger.error( + e, + "An error occurred while executing chore '%s'", + chore.summary + ) + ); if (delta) { - console.log( + logger.info( `[${i + 1}/${startupChores.length}] ${ chore.complete } (${delta}ms)` @@ -239,7 +257,7 @@ client.on("ready", async () => { } } - console.log("Ready!"); + logger.info("Ready!"); }); client.on("interactionCreate", async (interaction: Discord.Interaction) => { @@ -260,18 +278,18 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { prisma ); } - } else if (interaction.isCommand()) { + } else if (interaction.isChatInputCommand()) { await interaction.deferReply({ ephemeral: true }); if (!interaction.command?.guildId) { // global commands - const perms: Discord.Permissions | undefined = ( + const perms: Discord.PermissionsBitField | undefined = ( interaction.member as Discord.GuildMember )?.permissions; if ( !( perms && - perms.has(Discord.Permissions.FLAGS.MANAGE_GUILD, true) + perms.has(Discord.PermissionFlagsBits.ManageGuild, true) ) ) { await interaction.editReply("Permission denied."); @@ -281,7 +299,7 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { try { const str = utils.stringifyCommand(interaction); - console.log(str); + logger.debug({ interaction }, "Handling command %s", str); await commandLogsChannel?.send({ content: str, allowedMentions: { parse: [] }, @@ -299,7 +317,7 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { ); } } catch (e) { - console.error("Problem handling interaction: " + (e as Error).message); + logger.error(e, "An error occurred while handling an interaction"); } }); @@ -315,10 +333,7 @@ client.on("voiceStateUpdate", async (oldState, newState) => { try { await voiceThreads.handleVoiceLeave(oldState, prisma); } catch (e) { - console.error( - "Someone left a VC, GONE WRONG!!!:", - (e as Error).message - ); + logger.error(e, "Someone left a VC, GONE WRONG!!!:"); } } @@ -326,10 +341,7 @@ client.on("voiceStateUpdate", async (oldState, newState) => { try { await voiceThreads.handleVoiceJoin(newState, prisma); } catch (e) { - console.error( - "Someone joined a VC, GONE WRONG!!!:", - (e as Error).message - ); + logger.error(e, "Someone joined a VC, GONE WRONG!!!:"); } } }); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..41ae6b3 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,7 @@ +import pino from "pino"; + +const logger = pino({ + level: process.env.NODE_ENV === "development" ? "trace" : "info", +}); + +export default logger; diff --git a/src/modules/courses.ts b/src/modules/courses.ts index c86df12..989506a 100644 --- a/src/modules/courses.ts +++ b/src/modules/courses.ts @@ -15,6 +15,7 @@ import * as Builders from "@discordjs/builders"; import * as fenix from "./fenix"; import * as utils from "./utils"; import { OrphanChannel } from "./courses.d"; +import logger from "../logger"; export function provideCommands(): CommandDescriptor[] { const cmd = new Builders.SlashCommandBuilder() @@ -123,7 +124,7 @@ export function provideCommands(): CommandDescriptor[] { } export async function handleCommand( - interaction: Discord.CommandInteraction, + interaction: Discord.ChatInputCommandInteraction, prisma: PrismaClient ): Promise { if (!interaction.guild) return; @@ -150,7 +151,7 @@ export async function handleCommand( : "") ); } catch (e) { - console.error(e); + logger.error(e, "Error while refreshing channels"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -167,7 +168,9 @@ export async function handleCommand( i === 0 ) ) - .filter((v) => !!v && v.type === "GUILD_CATEGORY"); + .filter( + (v) => v?.type === Discord.ChannelType.GuildCategory + ); if (categories.length === 0) { await interaction.editReply( @@ -192,7 +195,7 @@ export async function handleCommand( categories.map((c) => `- <#${c?.id}>`).join("\n") ); } catch (e) { - console.error(e); + logger.error(e, "Error while settings categories"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -269,7 +272,7 @@ export async function handleCommand( ); } } catch (e) { - console.error(e); + logger.error(e, "Error while toggling channel visibility"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -311,13 +314,11 @@ export async function handleCommand( course.roleId || "" ); if (courseChannel) { - courseChannel.edit( - { - name: newAcronym.toLowerCase(), - topic: `${course.name} - ${newAcronym}`, - }, - `Course rename by ${interaction.user.tag}` - ); + courseChannel.edit({ + name: newAcronym.toLowerCase(), + topic: `${course.name} - ${newAcronym}`, + reason: `Course rename by ${interaction.user.tag}`, + }); } if (roleChannel) { roleChannel.setName( @@ -326,7 +327,7 @@ export async function handleCommand( ); } } catch (e) { - console.error(e); + logger.error(e, "Error while renamming course channel"); await interaction.editReply( utils.XEmoji + "Failed to rename channel and/or role, but renamed course on database. Please rename channel and/or role manully." @@ -337,7 +338,7 @@ export async function handleCommand( utils.CheckMarkEmoji + "Course renamed succesfully!" ); } catch (e) { - console.error(e); + logger.error(e, "Error while renamming course channel"); await interaction.editReply( utils.XEmoji + "Something went wrong. Maybe there is already another course with the desired acronym?" @@ -366,7 +367,7 @@ export async function handleCommand( await interaction.editReply({ embeds: [ - new Discord.MessageEmbed() + new Discord.EmbedBuilder() .setTitle("Degrees with Course") .setDescription( `Below are all degrees that need course \`${acronym}\`, as well as whether they have a tier high enough to justify having a channel for said course.` @@ -383,7 +384,7 @@ export async function handleCommand( ], }); } catch (e) { - console.error(e); + logger.error(e, "Error while listing degrees with course"); await interaction.editReply( utils.XEmoji + "Something went wrong" ); @@ -411,7 +412,7 @@ export async function handleCommand( ); } } catch (e) { - console.error(e); + logger.error(e, "Error while getting academic year"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -438,7 +439,7 @@ export async function handleCommand( `The current academic year has been set to \`${academicYear}\`` ); } catch (e) { - console.error(e); + logger.error(e, "Error while setting academic year"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -528,13 +529,13 @@ export async function refreshCourses( (await guild.channels.fetch(id)) as Discord.CategoryChannel ) ) - ).filter((channel) => channel?.type === "GUILD_CATEGORY"); + ).filter((channel) => channel?.type === Discord.ChannelType.GuildCategory); if (!categoriesChannels.length) { throw new Error("No category channels configured"); } const getNextFreeCategory = () => - categoriesChannels.find((v) => v.children.size < 50); + categoriesChannels.find((v) => v.children.cache.size < 50); // Get courses with associated degrees over tier 2 const courses = await prisma.course.findMany({ @@ -554,7 +555,7 @@ export async function refreshCourses( const channels = new Discord.Collection< string, Discord.GuildChannel - >().concat(...categoriesChannels.map((v) => v.children)); + >().concat(...categoriesChannels.map((v) => v.children.cache)); const roles = await guild.roles.fetch(); await courses.reduce(async (prevPromise, course) => { @@ -587,25 +588,23 @@ export async function refreshCourses( const courseChannelTopic = `${course.name} - ${course.displayAcronym}`; if (!courseChannel) { - courseChannel = await guild.channels.create( - course.displayAcronym.toLowerCase(), - { - type: "GUILD_TEXT", - topic: courseChannelTopic, - parent: getNextFreeCategory(), - reason, - permissionOverwrites: [ - { - id: guild.roles.everyone.id, - deny: [Discord.Permissions.FLAGS.VIEW_CHANNEL], - }, - { - id: courseRole, - allow: [Discord.Permissions.FLAGS.VIEW_CHANNEL], - }, - ], - } - ); + courseChannel = await guild.channels.create({ + name: course.displayAcronym.toLowerCase(), + type: Discord.ChannelType.GuildText, + topic: courseChannelTopic, + parent: getNextFreeCategory(), + reason, + permissionOverwrites: [ + { + id: guild.roles.everyone.id, + deny: [Discord.PermissionFlagsBits.ViewChannel], + }, + { + id: courseRole, + allow: [Discord.PermissionFlagsBits.ViewChannel], + }, + ], + }); } else { if (courseChannel.name !== course.displayAcronym.toLowerCase()) { await courseChannel.setName( @@ -613,7 +612,7 @@ export async function refreshCourses( ); } if ( - courseChannel.type === "GUILD_TEXT" && + courseChannel.type === Discord.ChannelType.GuildText && (courseChannel as Discord.TextChannel).topic !== courseChannelTopic ) { @@ -625,17 +624,17 @@ export async function refreshCourses( if ( !courseChannel .permissionsFor(courseRole) - .has(Discord.Permissions.FLAGS.VIEW_CHANNEL) + .has(Discord.PermissionFlagsBits.ViewChannel) ) { await courseChannel.edit({ permissionOverwrites: [ { id: guild.roles.everyone.id, - deny: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + deny: [Discord.PermissionFlagsBits.ViewChannel], }, { id: courseRole, - allow: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + allow: [Discord.PermissionFlagsBits.ViewChannel], }, ], }); diff --git a/src/modules/degrees.ts b/src/modules/degrees.ts index d5bd690..45b33b0 100644 --- a/src/modules/degrees.ts +++ b/src/modules/degrees.ts @@ -8,19 +8,18 @@ import * as utils from "./utils"; import * as fenix from "./fenix"; import * as courses from "./courses"; import { OrphanChannel } from "./courses.d"; +import { ChannelType, PermissionFlagsBits } from "discord.js"; +import logger from "../logger"; const tierChoices = [ "None", "Degree channels (Text & VC)", "Course channels (& course selection channel)", "Announcements channel", -].map( - (desc, i) => - [`${i}: ${i > 1 ? i - 1 + " + " : ""}${desc}`, i.toString()] as [ - name: string, - value: string - ] -); +].map((desc, i) => ({ + name: `${i}: ${i > 1 ? i - 1 + " + " : ""}${desc}`, + value: i.toString(), +})); export function provideCommands(): CommandDescriptor[] { const cmd = new Builders.SlashCommandBuilder() @@ -47,7 +46,7 @@ export function provideCommands(): CommandDescriptor[] { .setName("tier") .setDescription("Degree tier within the server") .setRequired(true) - .addChoices(tierChoices) + .addChoices(...tierChoices) ) .addStringOption( new Builders.SlashCommandStringOption() @@ -160,7 +159,7 @@ export function provideCommands(): CommandDescriptor[] { .setName("new-tier") .setDescription("What tier to set") .setRequired(true) - .addChoices(tierChoices) + .addChoices(...tierChoices) ) ); cmd.addSubcommand( @@ -178,10 +177,16 @@ export function provideCommands(): CommandDescriptor[] { .setName("channel-type") .setDescription("What type channel to set") .setRequired(true) - .addChoice("Degree Text", "degree-text") - .addChoice("Degree Voice", "degree-voice") - .addChoice("Announcements", "announcements") - .addChoice("Course Selection", "course-selection") + .addChoices({ name: "Degree Text", value: "degree-text" }) + .addChoices({ name: "Degree Voice", value: "degree-voice" }) + .addChoices({ + name: "Announcements", + value: "announcements", + }) + .addChoices({ + name: "Course Selection", + value: "course-selection", + }) ) .addChannelOption( new Builders.SlashCommandChannelOption() @@ -217,7 +222,7 @@ export async function createDegree( announcementsChannel: Discord.GuildChannel | null ): Promise { // snowflakes are orphan channels; FIXME: change to course.OrphanChannel[] - if (!tierChoices.map((arr) => arr[1]).includes(tier.toString())) { + if (!tierChoices.map((arr) => arr.value).includes(tier.toString())) { return "Invalid tier"; } @@ -245,44 +250,42 @@ export async function createDegree( )?.parent ?? ((await guild.channels.fetch()) .filter( - (c) => c.type === "GUILD_CATEGORY" && c.name === catName + (c) => + c.type === ChannelType.GuildText && c.name === catName ) .first() as Discord.CategoryChannel | undefined) ?? - (await guild.channels.create(catName, { - type: "GUILD_CATEGORY", + (await guild.channels.create({ + name: catName, + type: ChannelType.GuildCategory, permissionOverwrites: [ { id: guild.roles.everyone.id, - deny: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + deny: [Discord.PermissionFlagsBits.ViewChannel], }, { id: role.id, - allow: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + allow: [Discord.PermissionFlagsBits.ViewChannel], }, ], reason, })); if (!degreeTextChannel) { - degreeTextChannel = await guild.channels.create( - acronym.toLowerCase(), - { - type: "GUILD_TEXT", - topic: shortDegree.name, - parent: category, - reason, - } - ); + degreeTextChannel = await guild.channels.create({ + name: acronym.toLowerCase(), + type: ChannelType.GuildText, + topic: shortDegree.name, + parent: category, + reason, + }); await degreeTextChannel.lockPermissions(); } if (!degreeVoiceChannel) { - degreeVoiceChannel = await guild.channels.create( - acronym.toUpperCase(), - { - type: "GUILD_VOICE", - parent: category, - reason, - } - ); + degreeVoiceChannel = await guild.channels.create({ + name: acronym.toUpperCase(), + type: ChannelType.GuildVoice, + parent: category, + reason, + }); await degreeVoiceChannel.lockPermissions(); } @@ -290,27 +293,25 @@ export async function createDegree( { id: guild.roles.everyone, deny: [ - Discord.Permissions.FLAGS.VIEW_CHANNEL, - Discord.Permissions.FLAGS.SEND_MESSAGES, + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, ], }, { id: role.id, - allow: [Discord.Permissions.FLAGS.VIEW_CHANNEL], + allow: [PermissionFlagsBits.ViewChannel], }, ]; if (!courseSelectionChannel) { - courseSelectionChannel = await guild.channels.create( - acronym.toLowerCase() + "-cadeiras", - { - type: "GUILD_TEXT", - topic: "Selecionar cadeiras", - parent: category, - reason, - permissionOverwrites: restricted, - } - ); + courseSelectionChannel = await guild.channels.create({ + name: acronym.toLowerCase() + "-cadeiras", + type: ChannelType.GuildText, + topic: "Selecionar cadeiras", + parent: category, + reason, + permissionOverwrites: restricted, + }); } if (tier >= 3) { @@ -318,23 +319,21 @@ export async function createDegree( const announcer = (await guild.roles.fetch()) .filter((r) => r.name === "Announcer") .first(); - announcementsChannel = await guild.channels.create( - acronym.toLowerCase() + "-announcements", - { - type: "GUILD_TEXT", - topic: shortDegree.name + " Announcements", - parent: category, - reason, - permissionOverwrites: announcer - ? restricted.concat({ - id: announcer.id, - allow: [ - Discord.Permissions.FLAGS.SEND_MESSAGES, - ], - }) - : restricted, - } - ); + announcementsChannel = await guild.channels.create({ + name: acronym.toLowerCase() + "-announcements", + type: ChannelType.GuildText, + topic: shortDegree.name + " Announcements", + parent: category, + reason, + permissionOverwrites: announcer + ? restricted.concat({ + id: announcer.id, + allow: [ + Discord.PermissionFlagsBits.SendMessages, + ], + }) + : restricted, + }); } } } @@ -367,7 +366,7 @@ export async function createDegree( } export async function handleCommand( - interaction: Discord.CommandInteraction, + interaction: Discord.ChatInputCommandInteraction, prisma: PrismaClient ): Promise { if (!interaction.guild) return; @@ -412,7 +411,7 @@ export async function handleCommand( ); } } catch (e) { - console.error(e); + logger.error(e, "Error while creating degree"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -426,7 +425,7 @@ export async function handleCommand( await interaction.editReply({ embeds: [ - new Discord.MessageEmbed() + new Discord.EmbedBuilder() .setTitle("All Degrees") .setDescription( "Below are all known degrees, by acronym, Fénix ID and tier" @@ -463,42 +462,64 @@ export async function handleCommand( } else { await interaction.editReply({ embeds: [ - new Discord.MessageEmbed() + new Discord.EmbedBuilder() .setTitle("Degree Information") .setDescription( "Below are all the available details on this degree." ) - .addField("Acronym", degree.acronym, true) - .addField("Name", degree.name, true) - .addField("Fénix ID", degree.fenixId, true) - .addField("Tier", degree.tier.toString(), true) - .addField("Role", `<@&${degree.roleId}>`, true) - .addField( - "Text Channel", - degree.degreeTextChannelId + .addFields({ + name: "Acronym", + value: degree.acronym, + inline: true, + }) + .addFields({ + name: "Name", + value: degree.name, + inline: true, + }) + .addFields({ + name: "Fénix ID", + value: degree.fenixId, + inline: true, + }) + .addFields({ + name: "Tier", + value: degree.tier.toString(), + inline: true, + }) + .addFields({ + name: "Role", + value: `<@&${degree.roleId}>`, + inline: true, + }) + .addFields({ + name: "Text Channel", + value: degree.degreeTextChannelId ? `<#${degree.degreeTextChannelId}>` : "[NOT SET]", - true - ) - .addField( - "Voice Channel", - degree.degreeVoiceChannelId ?? "[NOT SET]", - true - ) - .addField( - "Announcements Channel", - degree.announcementsChannelId + inline: true, + }) + .addFields({ + name: "Voice Channel", + value: + degree.degreeVoiceChannelId ?? + "[NOT SET]", + inline: true, + }) + .addFields({ + name: "Announcements Channel", + value: degree.announcementsChannelId ? `<#${degree.announcementsChannelId}>` : "[NOT SET]", - true - ) - .addField( - "Course Selection Channel", - degree.courseSelectionChannelId + inline: true, + }) + .addFields({ + name: "Course Selection Channel", + value: degree.courseSelectionChannelId ? `<#${degree.courseSelectionChannelId}>` : "[NOT SET", - true - ), + inline: true, + }), ], }); } @@ -625,14 +646,17 @@ export async function handleCommand( return; } - if (channelType === "degreeVoice" && !newChannel.isVoice()) { + if ( + channelType === "degreeVoice" && + newChannel.type !== Discord.ChannelType.GuildVoice + ) { await interaction.editReply( utils.XEmoji + "Must be a voice channel" ); return; } else if ( channelType !== "degreeVoice" && - newChannel.isVoice() + newChannel.type === Discord.ChannelType.GuildVoice ) { await interaction.editReply( utils.XEmoji + "Must not be a voice channel" @@ -650,7 +674,7 @@ export async function handleCommand( `Successfully set ${channelType} of ${acronym} to <@#${newChannel.id}>` ); } catch (e) { - console.error(e); + logger.error(e, "Error while setting channel of degree"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); diff --git a/src/modules/fenix.ts b/src/modules/fenix.ts index 8e734e9..ccf3101 100644 --- a/src/modules/fenix.ts +++ b/src/modules/fenix.ts @@ -3,6 +3,7 @@ import axios from "axios"; import cheerio from "cheerio"; import RSSParser from "rss-parser"; +import logger from "../logger"; import * as FenixTypings from "./fenix.d"; import * as utils from "./utils"; @@ -20,11 +21,7 @@ export async function callEndpoint(endpoint: string): Promise { try { return (await axiosClient.get(endpoint)).data; } catch (e) { - console.error( - `Fénix broke while calling endpoint '${endpoint}': ${ - (e as Error).message - }` - ); + logger.error(e, `Fénix broke while calling endpoint '%s'`, endpoint); throw e; // propagate } } @@ -168,9 +165,7 @@ export async function getRSSFeed( new Date(b.pubDate).getTime() ); } catch (e) { - console.error( - `Could not get RSS feed for '${url}': ${(e as Error).message}` - ); + logger.error(e, "Could not get RSS feed for '%s'", url); return []; } } diff --git a/src/modules/galleryChannels.ts b/src/modules/galleryChannels.ts index c73f9b9..cad011e 100644 --- a/src/modules/galleryChannels.ts +++ b/src/modules/galleryChannels.ts @@ -139,7 +139,7 @@ async function updateGalleries( } export async function handleCommand( - interaction: Discord.CommandInteraction, + interaction: Discord.ChatInputCommandInteraction, prisma: PrismaClient ): Promise { const galleries = await utils.fetchGalleries(prisma); @@ -215,11 +215,11 @@ export async function handleCommand( []; if ( channel === null || - !(channel.isText() || channel.isThread()) + !(channel.isTextBased() || channel.isThread()) ) { for (const c of galleries) { const cf = await interaction.client.channels.fetch(c); - if (cf && (cf.isText() || cf.isThread())) { + if (cf && (cf.isTextBased() || cf.isThread())) { channels.push( cf as | Discord.TextChannel diff --git a/src/modules/leaderboard.ts b/src/modules/leaderboard.ts index 6b092c2..ee18704 100644 --- a/src/modules/leaderboard.ts +++ b/src/modules/leaderboard.ts @@ -9,6 +9,7 @@ import { CommandDescriptor } from "../bot.d"; import * as utils from "./utils"; import { ThenArg } from "./utils.d"; +import logger from "../logger"; const MAX_PEOPLE = 50; @@ -23,7 +24,7 @@ export async function getUsersCharacterCount( UsersCharacterCount, number, number, - Discord.Channel[], + Discord.BaseChannel[], UsersCharacterCount ] > { @@ -34,13 +35,13 @@ export async function getUsersCharacterCount( const channels = Array.from( (await guild.channels.fetch()).filter( - (channel) => channel.isText() || channel.isThread() + (channel) => channel.isTextBased() || channel.isThread() ) ).map((o) => o[1]) as (Discord.TextChannel | Discord.ThreadChannel)[]; const promises = channels.map(async (channel) => { const messages = await utils.fetchAllChannelMessages(channel); for (const [_id, msg] of messages) { - if (!msg.deleted && msg.author && !msg.author.bot) { + if (msg.author && !msg.author.bot) { if ( onlyCountAfter === undefined || msg.createdAt > onlyCountAfter @@ -81,7 +82,7 @@ export async function sendLeaderboard( sendChannel: Discord.TextChannel | Discord.ThreadChannel, period: string, prisma: PrismaClient -): Promise<[number, number, Discord.Channel[], number, Discord.Snowflake]> { +): Promise<[number, number, Discord.BaseChannel[], number, Discord.Snowflake]> { const now = new Date().getTime(); const day = 1000 * 60 * 60 * 24; @@ -219,9 +220,9 @@ export function provideCommands(): CommandDescriptor[] { "How recent do messages have to be to be considered; defaults to all" ) .setRequired(false) - .addChoice("All messages", "all") - .addChoice("Last 30 days", "month") - .addChoice("Last 7 days", "week") + .addChoices({ name: "All messages", value: "all" }) + .addChoices({ name: "Last 30 days", value: "month" }) + .addChoices({ name: "Last 7 days", value: "week" }) ) ); cmd.addSubcommand( @@ -238,7 +239,7 @@ export function provideCommands(): CommandDescriptor[] { } export async function handleCommand( - interaction: Discord.CommandInteraction, + interaction: Discord.ChatInputCommandInteraction, prisma: PrismaClient ): Promise { switch (interaction.options.getSubcommand()) { @@ -287,7 +288,7 @@ ${ } catch (e) { await interaction .editReply(utils.XEmoji + "Something went wrong.") - .catch(() => console.error("Leaderboard took too long :(")); + .catch(() => logger.error("Leaderboard took too long :(")); } break; } @@ -304,7 +305,7 @@ ${ `Successfully reset cache; last stamp was ${stamp}` ); } catch (e) { - console.error(e); + logger.error(e, "Error while crearing cache of leaderboard"); await interaction.editReply( utils.XEmoji + "Something went wrong, maybe there was no cache?" diff --git a/src/modules/misc.ts b/src/modules/misc.ts index f1d95f5..8c0714d 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -9,6 +9,7 @@ import { CommandDescriptor } from "../bot.d"; import { CommandPermission } from "../bot"; import * as utils from "./utils"; import { SlashCommandBuilder } from "@discordjs/builders"; +import logger from "../logger"; export function provideCommands(): CommandDescriptor[] { const say = new Builders.SlashCommandBuilder() @@ -112,10 +113,10 @@ export async function handleAboutCommand( // cannot easily import so reading it directly await interaction.editReply({ embeds: [ - new Discord.MessageEmbed() + new Discord.EmbedBuilder() .setTitle("IST Discord Bot") .setURL(pvar("homepage", "https://discord.leic.pt")) - .setAuthor(pvar("author")) + .setAuthor({ name: pvar("author") }) .setDescription( `**Description:** ${pvar("description")} **Version:** ${pvar("version")} @@ -133,10 +134,11 @@ export async function handleAboutCommand( inline: true, })) ) - .setFooter( - "Uptime: " + - utils.durationString(interaction.client.uptime ?? 0) - ), + .setFooter({ + text: + "Uptime: " + + utils.durationString(interaction.client.uptime ?? 0), + }), ], }); } @@ -144,7 +146,7 @@ export async function handleAboutCommand( const sayLogs: Discord.Collection = new Discord.Collection(); export async function handleSayCommand( - interaction: Discord.CommandInteraction + interaction: Discord.ChatInputCommandInteraction ): Promise { try { const channel = (interaction.options.getChannel("channel", false) || @@ -153,7 +155,7 @@ export async function handleSayCommand( const allowMentions = interaction.options.getBoolean("allow-mentions", false) ?? false; - if (channel && channel.isText()) { + if (channel && channel.isTextBased()) { const msg = await channel.send({ content: message.replace(/\\n/g, "\n"), allowedMentions: allowMentions ? undefined : { parse: [] }, @@ -161,12 +163,13 @@ export async function handleSayCommand( const uid = interaction.member?.user.id; if (uid) { sayLogs.set(msg.id, uid); - console.log( - `User ${ - interaction.member?.user.username - } said «${message}» (w/${ - allowMentions ? "" : "o" - } mentions)` + logger.info( + { + user: interaction.member?.user.username, + message, + allowMentions, + }, + "User used /say command" ); } await interaction.editReply( @@ -181,7 +184,7 @@ export async function handleSayCommand( } export async function handleWhoSaidCommand( - interaction: Discord.CommandInteraction + interaction: Discord.ChatInputCommandInteraction ): Promise { try { const messageId = interaction.options.getString("message-id", true); @@ -201,19 +204,19 @@ export async function handleWhoSaidCommand( } export async function handleJustAskCommand( - interaction: Discord.CommandInteraction + interaction: Discord.ChatInputCommandInteraction ): Promise { try { await interaction.channel?.send("https://dontasktoask.com/"); await interaction.editReply(utils.CheckMarkEmoji + "Sent"); } catch (e) { - console.error(e); + logger.error(e, "Error while executing just ask command"); await interaction.editReply(utils.XEmoji + "Something went wrong."); } } export async function handleMigrateMembersWithRole( - interaction: Discord.CommandInteraction + interaction: Discord.ChatInputCommandInteraction ): Promise { try { const oldRole = interaction.options.getRole( @@ -226,23 +229,26 @@ export async function handleMigrateMembersWithRole( ) as Discord.Role; const removeOld = interaction.options.getBoolean("remove-old", false); + // fetch all members, otherwise oldRole.members will be empty + await interaction.guild?.members.fetch(); + let count = 0; - oldRole.members.forEach((member) => { - member.roles.add(newRole); + for (const [_, member] of oldRole.members) { + await member.roles.add(newRole); if (removeOld) { - member.roles.remove(oldRole); + await member.roles.remove(oldRole); } count++; - }); + } await interaction.editReply( utils.CheckMarkEmoji + `Migrated ${count} members from ${oldRole} to ${newRole}` ); } catch (e) { - console.error(e); + logger.error(e, "Error while migrating members to new role"); await interaction.editReply(utils.XEmoji + "Something went wrong."); } } diff --git a/src/modules/polls.ts b/src/modules/polls.ts index 9db7e91..8f9a232 100644 --- a/src/modules/polls.ts +++ b/src/modules/polls.ts @@ -1,13 +1,14 @@ // Handler for polls import { + ActionRowBuilder, + ButtonBuilder, ButtonInteraction, + ButtonStyle, + ChatInputCommandInteraction, Client, - CommandInteraction, - Message, - MessageActionRow, - MessageButton, - MessageEmbed, + Embed, + EmbedBuilder, Snowflake, TextChannel, } from "discord.js"; @@ -17,20 +18,20 @@ import cron from "node-cron"; import { PrismaClient, Poll } from "@prisma/client"; import { CommandDescriptor } from "../bot.d"; import * as utils from "./utils"; +import logger from "../logger"; -const POLL_ACTION_ROW = new MessageActionRow(); -POLL_ACTION_ROW.addComponents([ - new MessageButton() +const POLL_ACTION_ROW = new ActionRowBuilder().addComponents([ + new ButtonBuilder() .setLabel("Yes") - .setStyle("SUCCESS") + .setStyle(ButtonStyle.Success) .setCustomId("polls:yes"), - new MessageButton() + new ButtonBuilder() .setLabel("No") - .setStyle("DANGER") + .setStyle(ButtonStyle.Danger) .setCustomId("polls:no"), - new MessageButton() + new ButtonBuilder() .setLabel("Clear") - .setStyle("SECONDARY") + .setStyle(ButtonStyle.Secondary) .setCustomId("polls:clear"), ]); const POLL_NO_ONE = "*No one*"; @@ -52,12 +53,12 @@ export const handlePollButton = async ( } const newEmbed = getNewPollEmbed( - oldEmbeds[0] as MessageEmbed, + oldEmbeds[0] as Embed, fieldIndex, interaction.user.id ); - (interaction.message as Message).edit({ + interaction.message.edit({ embeds: [newEmbed], components: [POLL_ACTION_ROW], }); @@ -66,10 +67,10 @@ export const handlePollButton = async ( }; export const getNewPollEmbed = ( - oldEmbed: MessageEmbed, + oldEmbed: Embed, fieldIndex: number, userId: Snowflake -): MessageEmbed => { +): Embed => { oldEmbed.fields?.map((field, i) => { field.value = field.value @@ -112,11 +113,11 @@ export const sendPollEmbed = async ( const message = await channel.send({ embeds: [ - new MessageEmbed() + new EmbedBuilder() .setTitle(poll.title) - .addField("Yes", POLL_NO_ONE, true) - .addField("No", POLL_NO_ONE, true) - .setFooter(poll.id) + .addFields({ name: "Yes", value: POLL_NO_ONE, inline: true }) + .addFields({ name: "No", value: POLL_NO_ONE, inline: true }) + .setFooter({ text: poll.id }) .setTimestamp(), ], components: [POLL_ACTION_ROW], @@ -137,8 +138,9 @@ export const schedulePolls = async ( ); if (!channel) { - console.error( - `Couldn't fetch channel ${poll.channelId} for poll ${poll.id}` + logger.error( + { channel: poll.channelId, poll: poll.id }, + "Couldn't fetch channel for poll" ); return; } @@ -153,10 +155,7 @@ export const schedulePolls = async ( await sendPollEmbed(p, channel as TextChannel); } } catch (e) { - console.error( - "Could not verify (& send) poll:", - (e as Error).message - ); + logger.error(e, "Could not verify (& send) poll"); } }); }) @@ -244,7 +243,7 @@ export function provideCommands(): CommandDescriptor[] { } export async function handleCommand( - interaction: CommandInteraction, + interaction: ChatInputCommandInteraction, prisma: PrismaClient ): Promise { switch (interaction.options.getSubcommand()) { @@ -316,7 +315,7 @@ export async function handleCommand( const polls = await prisma.poll.findMany(); await interaction.editReply({ embeds: [ - new MessageEmbed() + new EmbedBuilder() .setTitle("Polls") .setDescription( polls.length @@ -355,21 +354,33 @@ export async function handleCommand( } else { await interaction.editReply({ embeds: [ - new MessageEmbed() + new EmbedBuilder() .setTitle("Poll Information") - .addField("ID", poll.id, true) - .addField("Type", poll.type, true) - .addField("Title", poll.title, true) - .addField( - "Schedule", - poll.cron ? poll.cron : "N/A", - true - ) - .addField( - "Channel", - `<#${poll.channelId}>`, - true - ), + .addFields({ + name: "ID", + value: poll.id, + inline: true, + }) + .addFields({ + name: "Type", + value: poll.type, + inline: true, + }) + .addFields({ + name: "Title", + value: poll.title, + inline: true, + }) + .addFields({ + name: "Schedule", + value: poll.cron ? poll.cron : "N/A", + inline: true, + }) + .addFields({ + name: "Channel", + value: `<#${poll.channelId}>`, + inline: true, + }), ], }); } diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 89ccc61..787233b 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -1,13 +1,19 @@ // Handler for role selection import { PrismaClient, RoleGroup, RoleGroupOption } from "@prisma/client"; -import Discord from "discord.js"; +import Discord, { + ButtonBuilder, + SelectMenuBuilder, + SelectMenuComponentOptionData, +} from "discord.js"; import * as Builders from "@discordjs/builders"; import { getConfigFactory } from "./utils"; import { CommandDescriptor } from "../bot.d"; import * as utils from "./utils"; import * as courses from "./courses"; +import { parseButtonStyle } from "../utils/buttonStyleUtils"; +import logger from "../logger"; const MAX_COMPONENTS_PER_ROW = 5; const MAX_ROWS_PER_MESSAGE = 5; @@ -46,7 +52,7 @@ export async function sendRoleSelectionMessages( const channel = client.channels.cache.find( (c) => c.id === group.channelId ); - if (channel === undefined || !channel.isText()) { + if (channel === undefined || !channel.isTextBased()) { throw new Error("Could not find channel"); } @@ -73,9 +79,18 @@ export async function sendRoleSelectionMessages( ); } + // discord.js does not accept `null` as a value for `emoji` + const options: SelectMenuComponentOptionData[] = + group.options.map((option) => ({ + label: option.label, + description: option.description, + value: option.value, + emoji: option.emoji ?? undefined, + })); + components = [ - new Discord.MessageActionRow().addComponents( - new Discord.MessageSelectMenu() + new Discord.ActionRowBuilder().addComponents( + new Discord.SelectMenuBuilder() .setCustomId(`roleSelection:${group.id}`) .setPlaceholder(group.placeholder) .setMinValues(group.minValues ?? 1) @@ -84,14 +99,12 @@ export async function sendRoleSelectionMessages( group.maxValues ?? 1 ) ) - .addOptions( - group.options as Discord.MessageSelectOptionData[] - ) + .addOptions(options) ), ]; } else if (group.mode === "buttons") { - const rows: Discord.MessageButton[][] = []; - let curRow: Discord.MessageButton[] = []; + const rows: Discord.ButtonBuilder[][] = []; + let curRow: Discord.ButtonBuilder[] = []; for (const opt of group.options) { if ( @@ -102,22 +115,12 @@ export async function sendRoleSelectionMessages( curRow = []; } - const btn = new Discord.MessageButton() + const btn = new Discord.ButtonBuilder() .setCustomId( `roleSelection:${group.id}:${opt.value}` ) .setLabel(opt.label) - .setStyle( - ([ - "PRIMARY", - "SECONDARY", - "SUCCESS", - "DANGER", - "LINK", - ].includes(opt.description) - ? opt.description - : "PRIMARY") as Discord.MessageButtonStyleResolvable - ); + .setStyle(parseButtonStyle(opt.description)); if (opt.emoji !== null) { btn.setEmoji(opt.emoji); } @@ -138,7 +141,9 @@ export async function sendRoleSelectionMessages( ); } components = rows.map((r) => - new Discord.MessageActionRow().addComponents(r) + new Discord.ActionRowBuilder().addComponents( + r + ) ); } else { throw new Error(`Unknown mode '${group.mode}'`); @@ -168,10 +173,10 @@ export async function sendRoleSelectionMessages( } } } catch (e) { - console.error( - `Could not send role selection message for group ${ - group.id - } because: ${(e as Error).message}` + logger.error( + e, + "Could not send role selection message for group %s", + group.id ); } } @@ -198,7 +203,7 @@ async function injectGroups( }) ); } catch (e) { - await console.error(`Failed to inject groups: ${e}`); + logger.error(e, "Failed to inject groups"); } } @@ -245,9 +250,7 @@ async function injectTouristGroup( }, }; } catch (e) { - console.error( - `Failed to inject tourist group: ${(e as Error).message}` - ); + logger.error(e, "Failed to inject tourist group"); } } @@ -367,8 +370,11 @@ export function provideCommands(): CommandDescriptor[] { .setName("mode") .setDescription("How users may choose roles") .setRequired(true) - .addChoice("Selection Menu", "menu") - .addChoice("Buttons", "buttons") + .addChoices({ + name: "Selection Menu", + value: "menu", + }) + .addChoices({ name: "Buttons", value: "buttons" }) ) .addStringOption( new Builders.SlashCommandStringOption() @@ -449,9 +455,12 @@ export function provideCommands(): CommandDescriptor[] { .setName("name") .setDescription("Property to set") .setRequired(true) - .addChoice("Mode", "mode") - .addChoice("Placeholder", "placeholder") - .addChoice("Message", "message") + .addChoices({ name: "Mode", value: "mode" }) + .addChoices({ + name: "Placeholder", + value: "placeholder", + }) + .addChoices({ name: "Message", value: "message" }) ) .addStringOption( new Builders.SlashCommandStringOption() @@ -623,8 +632,8 @@ export function provideCommands(): CommandDescriptor[] { .setName("field") .setDescription("Which field to change") .setRequired(true) - .addChoice("Message", "message") - .addChoice("Label", "label") + .addChoices({ name: "Message", value: "message" }) + .addChoices({ name: "Label", value: "label" }) ) .addStringOption( new Builders.SlashCommandStringOption() @@ -712,7 +721,7 @@ async function createGroup( ]; } - if (!channel.isText()) { + if (!channel.isTextBased()) { return [false, "Invalid channel: must be a text channel"]; } @@ -827,7 +836,7 @@ async function moveGroup( return "Invalid id: must be snake_case"; } - if (!channel.isText()) { + if (!channel.isTextBased()) { return "Invalid channel: must be a text channel"; } @@ -853,7 +862,7 @@ async function viewGroup( include: { options: true }, }); if (group) { - const embed = new Discord.MessageEmbed().setTitle( + const embed = new Discord.EmbedBuilder().setTitle( "Role Group Information" ).setDescription(`**ID**: ${group.id} **Mode:** ${group.mode} @@ -874,11 +883,11 @@ async function viewGroup( }`); for (const opt of group.options) { - embed.addField( - (opt.emoji ? opt.emoji + " " : "") + opt.label, - opt.value + " - " + opt.description, - true - ); + embed.addFields({ + name: (opt.emoji ? opt.emoji + " " : "") + opt.label, + value: opt.value + " - " + opt.description, + inline: true, + }); } return { @@ -972,7 +981,7 @@ async function removeOption( // TODO: dry this a bit export async function handleCommand( - interaction: Discord.CommandInteraction, + interaction: Discord.ChatInputCommandInteraction, prisma: PrismaClient ): Promise { const subCommandGroup = interaction.options.getSubcommandGroup(); @@ -1105,7 +1114,7 @@ export async function handleCommand( try { await interaction.editReply({ embeds: [ - new Discord.MessageEmbed() + new Discord.EmbedBuilder() .setTitle("Role Selection Groups") .setDescription( "All available role groups are listed below with their `mode` field." @@ -1122,10 +1131,7 @@ export async function handleCommand( ], }); } catch (e) { - console.error( - "Could not list role groups because: ", - (e as Error).message - ); + logger.error(e, "Could not list role groups"); await interaction.editReply( utils.XEmoji + "Failed to list role groups." ); @@ -1199,9 +1205,9 @@ export async function handleCommand( "Role selection messages successfully sent." ); } catch (e) { - console.error( - "Could not send role selection messages:", - (e as Error).message + logger.error( + e, + "Could not send role selection messages" ); await interaction.editReply( utils.XEmoji + @@ -1255,7 +1261,7 @@ export async function handleCommand( true ) as Discord.GuildChannel; - if (!channel.isText() && !channel.isThread()) { + if (!channel.isTextBased() && !channel.isThread()) { await interaction.editReply( utils.XEmoji + "Invalid channel." ); @@ -1314,21 +1320,25 @@ export async function handleCommand( const role = await getConfig("role_id"); const msgId = await getConfig("message_id"); - const embed = new Discord.MessageEmbed() + const embed = new Discord.EmbedBuilder() .setTitle("TourIST Information") - .addField("Message", message) - .addField("Label", label) - .addField( - "Channel", - channel ? `<#${channel}>` : "[UNSET]" - ) - .addField("Role", role ? `<@&${role}>` : "[UNSET]") - .addField( - "Location", - channel && msgId - ? `[Here](https://discord.com/channels/${process.env.GUILD_ID}/${channel}/${msgId})` - : "[UNSET]" - ); + .addFields({ name: "Message", value: message }) + .addFields({ name: "Label", value: label }) + .addFields({ + name: "Channel", + value: channel ? `<#${channel}>` : "[UNSET]", + }) + .addFields({ + name: "Role", + value: role ? `<@&${role}>` : "[UNSET]", + }) + .addFields({ + name: "Location", + value: + channel && msgId + ? `[Here](https://discord.com/channels/${process.env.GUILD_ID}/${channel}/${msgId})` + : "[UNSET]", + }); await interaction.editReply({ embeds: [embed] }); } catch (e) { await interaction.editReply( diff --git a/src/modules/rss.ts b/src/modules/rss.ts index 23ebf99..4c7120b 100644 --- a/src/modules/rss.ts +++ b/src/modules/rss.ts @@ -1,10 +1,15 @@ -import Discord from "discord.js"; -import { PrismaClient } from "@prisma/client"; +import Discord, { + EmbedBuilder, + HexColorString, + TextBasedChannel, +} from "discord.js"; +import { Course, Degree, PrismaClient } from "@prisma/client"; import cron from "node-cron"; import TurndownService from "turndown"; import * as FenixAPI from "./fenix"; import * as FenixTypings from "./fenix.d"; +import logger from "../logger"; const turndownService = new TurndownService(); @@ -34,70 +39,102 @@ export function runRSSFeedJob( degreeCourse .filter((course) => !!course.announcementsFeedUrl) .forEach(async (course) => { - const degreeChannel = await client.channels.fetch( - course.degree.degreeTextChannelId || "" - ); - if (!degreeChannel?.isText()) return; - - const announcements = - await FenixAPI.getRSSFeed( - course.announcementsFeedUrl || "", - course.feedLastUpdated + try { + await fetchRSSForCourse(course, prisma, client); + } catch (e) { + logger.error( + { + err: e, + course: course.course.name, + degree: course.degree.name, + }, + "Failed to fetch and send announcements for course" ); - - await announcements.reduce( - async (prevPromise, announcement) => { - await prevPromise; - - await degreeChannel.send({ - content: `Novo anúncio de ${course.course.name}${ - course.course.roleId - ? ` <@&${course.course.roleId}>` - : `` - }`, - embeds: [ - { - title: announcement.title?.substring( - 0, - 256 - ), - description: turndownService - .turndown( - announcement.content || - "Não foi possível obter o conteúdo deste anúncio" - ) - .substring(0, 2048), - url: announcement.link, - color: parseInt( - (course.color || "#00a0e4").substring( - 1 - ), - 16 - ), - author: { - name: - announcement.author.match( - /\((.+)\)/ - )?.[1] || announcement.author, - }, - footer: { - text: course.course.name, - }, - timestamp: new Date(announcement.pubDate), - }, - ], - }); - }, - Promise.resolve() - ); - - if (announcements.length > 0) { - const date = new Date(announcements.pop()?.pubDate || "."); - await prisma.degreeCourse.update({ - where: { id: course.id }, - data: { feedLastUpdated: date }, - }); } }); }; } + +type DegreeCourse = { + degree: Degree; + course: Course; + announcementsFeedUrl: string | null; + feedLastUpdated: Date; + color: string | null; + id: string; +}; + +async function fetchRSSForCourse( + course: DegreeCourse, + prisma: PrismaClient, + client: Discord.Client +): Promise { + const degreeChannel = await client.channels.fetch( + course.degree.degreeTextChannelId || "" + ); + if (!degreeChannel?.isTextBased()) return; + + const announcements = + await FenixAPI.getRSSFeed( + course.announcementsFeedUrl || "", + course.feedLastUpdated + ); + + for (const announcement of announcements) { + await sendAnnouncementMessage(course, degreeChannel, announcement); + } + + if (announcements.length > 0) { + const date = new Date(announcements.pop()?.pubDate || "."); + await prisma.degreeCourse.update({ + where: { id: course.id }, + data: { feedLastUpdated: date }, + }); + } +} + +async function sendAnnouncementMessage( + course: DegreeCourse, + channel: TextBasedChannel, + announcement: FenixTypings.RSSCourseAnnouncement +) { + const roleMention = course.course.roleId + ? ` <@&${course.course.roleId}>` + : ``; + const color = (course.color as HexColorString) || "#00a0e4"; + // get the name of the author inside brackets + // format is "email@tecnico.ulisboa.pt (Name)" + const authorName = + announcement.author.match(/\((.+)\)/)?.[1] || announcement.author; + + const content = + turndownService + .turndown(announcement.content ?? "") + .substring(0, 2048) || + "Não foi possível obter o conteúdo deste anúncio"; + + logger.debug( + { + roleMention, + color, + authorName, + content, + courseName: course.course.name, + }, + "Sending course announcement" + ); + + await channel.send({ + content: `Novo anúncio de ${course.course.name}${roleMention}`, + embeds: [ + new EmbedBuilder() + .setTitle(announcement.title?.substring(0, 256)) + .setDescription(content) + .setURL(announcement.link) + .setColor(color) + .setAuthor({ name: authorName }) + .setFooter({ text: course.course.name }) + .setTimestamp(new Date(announcement.pubDate)), + ], + }); +} diff --git a/src/modules/sudo.ts b/src/modules/sudo.ts index 371b3b6..b072a2b 100644 --- a/src/modules/sudo.ts +++ b/src/modules/sudo.ts @@ -36,13 +36,10 @@ export function provideCommands(): CommandDescriptor[] { } export async function handleSudoCommand( - interaction: Discord.CommandInteraction + interaction: Discord.ChatInputCommandInteraction ): Promise { try { - const target = interaction.options.getMember( - "target", - false - ) as Discord.GuildMember | null; + const target = interaction.options.getMember("target"); const roles = (target ?? interaction.member) ?.roles as Discord.GuildMemberRoleManager; @@ -75,7 +72,7 @@ export async function handleSudoCommand( } export async function handleResetAdminCommand( - interaction: Discord.CommandInteraction + interaction: Discord.ChatInputCommandInteraction ): Promise { try { const role = await ( diff --git a/src/modules/utils.ts b/src/modules/utils.ts index f161fe8..6793194 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -6,6 +6,7 @@ import { PrismaClient } from "@prisma/client"; import * as Discord from "discord.js"; import { MessageCollection } from "./utils.d"; +import { ApplicationCommandOptionType } from "discord.js"; export const XEmoji = "❌ "; export const CheckMarkEmoji = "✅ "; @@ -115,7 +116,7 @@ export function generateHexCode(): string { } export function stringifyCommand( - interaction: Discord.CommandInteraction + interaction: Discord.ChatInputCommandInteraction ): string { const subcommandGroup = interaction.options.getSubcommandGroup(false); const subcommand = interaction.options.getSubcommand(false); @@ -128,7 +129,10 @@ export function stringifyCommand( opt === undefined ? [] : opt.length === 1 && - ["SUB_COMMAND", "SUB_COMMAND_GROUP"].includes(opt[0].type) + [ + ApplicationCommandOptionType.Subcommand, + ApplicationCommandOptionType.SubcommandGroup, + ].includes(opt[0].type) ? extractOptions(opt[0].options) : opt; diff --git a/src/modules/voiceThreads.ts b/src/modules/voiceThreads.ts index e3ffa1d..b508bef 100644 --- a/src/modules/voiceThreads.ts +++ b/src/modules/voiceThreads.ts @@ -1,3 +1,4 @@ +import { ChannelType } from "discord.js"; // Controller for vc-chat threads import { PrismaClient } from "@prisma/client"; @@ -37,7 +38,7 @@ export function provideCommands(): CommandDescriptor[] { } export async function handleCommand( - interaction: Discord.CommandInteraction, + interaction: Discord.ChatInputCommandInteraction, prisma: PrismaClient ): Promise { switch (interaction.options.getSubcommand()) { @@ -72,7 +73,7 @@ export async function handleCommand( true ) as Discord.GuildChannel; - if (!channel.isText()) { + if (!channel.isTextBased()) { await interaction.editReply( utils.XEmoji + "Channel must be a text channel." ); @@ -141,8 +142,8 @@ export async function handleVoiceJoin( const threadName = normalizeVCName(newState.channel.name); const threadType = newState.guild.features.includes("PRIVATE_THREADS") - ? "GUILD_PRIVATE_THREAD" - : "GUILD_PUBLIC_THREAD"; + ? ChannelType.PrivateThread + : ChannelType.PublicThread; const threads = (await vcChat.threads.fetch()).threads; const td = threads.filter( @@ -184,8 +185,8 @@ export async function handleVoiceLeave( const threadName = normalizeVCName(oldState.channel.name); const threadType = oldState.guild.features.includes("PRIVATE_THREADS") - ? "GUILD_PRIVATE_THREAD" - : "GUILD_PUBLIC_THREAD"; + ? ChannelType.PrivateThread + : ChannelType.PublicThread; const threads = (await vcChat.threads.fetch()).threads; const td = threads.filter( diff --git a/src/modules/welcome.ts b/src/modules/welcome.ts index bbeeb81..c08bd4e 100644 --- a/src/modules/welcome.ts +++ b/src/modules/welcome.ts @@ -72,7 +72,7 @@ export function provideCommands(): CommandDescriptor[] { } export async function handleCommand( - interaction: Discord.CommandInteraction, + interaction: Discord.ChatInputCommandInteraction, prisma: PrismaClient ): Promise { try { @@ -82,7 +82,7 @@ export async function handleCommand( "channel", true ) as Discord.GuildChannel; - if (!channel.isText()) { + if (!channel.isTextBased()) { await interaction.editReply( utils.XEmoji + "Channel must be a text channel." ); @@ -127,10 +127,14 @@ export async function handleCommand( const channel = channelId ? `<#${channelId}>` : "[NONE]"; await interaction.editReply({ embeds: [ - new Discord.MessageEmbed() + new Discord.EmbedBuilder() .setTitle("Welcome Message Settings") .setDescription(`**Message:** ${message}`) - .addField("Channel", channel, true), + .addFields({ + name: "Channel", + value: channel, + inline: true, + }), ], }); break; diff --git a/src/utils/buttonStyleUtils.ts b/src/utils/buttonStyleUtils.ts new file mode 100644 index 0000000..cbb3774 --- /dev/null +++ b/src/utils/buttonStyleUtils.ts @@ -0,0 +1,17 @@ +import { ButtonStyle } from "discord.js"; + +export function parseButtonStyle(style: string): ButtonStyle { + switch (style) { + case "PRIMARY": + default: + return ButtonStyle.Primary; + case "SECONDARY": + return ButtonStyle.Secondary; + case "DANGER": + return ButtonStyle.Danger; + case "LINK": + return ButtonStyle.Link; + case "SUCCESS": + return ButtonStyle.Success; + } +} diff --git a/tsconfig.json b/tsconfig.json index 68f764b..0b9ba4b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@tsconfig/node14/tsconfig.json", + "extends": "@tsconfig/node16/tsconfig.json", "compilerOptions": { "rootDir": "src", diff --git a/yarn.lock b/yarn.lock index ba1bea3..6762edb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,60 +30,34 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@discordjs/builders@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-0.4.0.tgz#bb41573ce4824aa9194a53b52c29c5219b610010" - integrity sha512-EiwLltKph6TSaPJIzJYdzNc1PnA2ZNaaE0t0ODg3ghnpVHqfgd0YX9/srsleYHW2cw1sfIq+kbM+h0etf7GWLA== - dependencies: - "@sindresorhus/is" "^4.0.1" - discord-api-types "^0.22.0" - ow "^0.27.0" - ts-mixer "^6.0.0" - tslib "^2.3.0" - -"@discordjs/builders@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-0.6.0.tgz#4724d18990a97d84d0250eba5b50991b71a450a5" - integrity sha512-mH3Gx61LKk2CD05laCI9K5wp+a3NyASHDUGx83DGJFkqJlRlSV5WMJNY6RS37A5SjqDtGMF4wVR9jzFaqShe6Q== - dependencies: - "@sindresorhus/is" "^4.0.1" - discord-api-types "^0.22.0" - ow "^0.27.0" - ts-mixer "^6.0.0" - tslib "^2.3.1" - -"@discordjs/collection@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.1.6.tgz#9e9a7637f4e4e0688fd8b2b5c63133c91607682c" - integrity sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ== - -"@discordjs/collection@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.2.1.tgz#ea4bc7b41b7b7b6daa82e439141222ec95c469b2" - integrity sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog== - -"@discordjs/form-data@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@discordjs/form-data/-/form-data-3.0.1.tgz#5c9e6be992e2e57d0dfa0e39979a850225fb4697" - integrity sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -"@discordjs/rest@^0.1.0-canary.0": - version "0.1.0-canary.0" - resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-0.1.0-canary.0.tgz#666f9a1a0c1f2f5a09a3a79f77aeddaeafbcbcc1" - integrity sha512-d+s//ISYVV+e0w/926wMEeO7vju+Pn11x1JM4tcmVMCHSDgpi6pnFCNAXF1TEdnDcy7xf9tq5cf2pQkb/7ySTQ== +"@discordjs/builders@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.2.0.tgz#4f5059e258c30a26931ae384985ef8c6b371ba05" + integrity sha512-ARy4BUTMU+S0ZI6605NDqfWO+qZqV2d/xfY32z3hVSsd9IaAKJBZ1ILTZLy87oIjW8+gUpQmk9Kt0ZP9bmmd8Q== dependencies: - "@discordjs/collection" "^0.1.6" - "@sapphire/async-queue" "^1.1.4" - "@sapphire/snowflake" "^1.3.5" - abort-controller "^3.0.0" - discord-api-types "^0.18.1" - form-data "^4.0.0" - node-fetch "^2.6.1" - tslib "^2.3.0" + "@sapphire/shapeshift" "^3.5.1" + discord-api-types "^0.37.3" + fast-deep-equal "^3.1.3" + ts-mixer "^6.0.1" + tslib "^2.4.0" + +"@discordjs/collection@^1.0.1", "@discordjs/collection@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.1.0.tgz#5f9f926404fd48ccde86a0d2268f202cbec77833" + integrity sha512-PQ2Bv6pnT7aGPCKWbvvNRww5tYCGpggIQVgpuF9TdDPeR6n6vQYxezXiLVOS9z2B62Dp4c+qepQ15SgJbLYtCQ== + +"@discordjs/rest@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-1.1.0.tgz#5c283571a22b911ca334316245487af40baffe8b" + integrity sha512-yCrthRTQeUyNThQEpCk7bvQJlwQmz6kU0tf3dcWBv2WX3Bncl41x7Wc+v5b5OsIxfNYq38PvVtWircu9jtYZug== + dependencies: + "@discordjs/collection" "^1.0.1" + "@sapphire/async-queue" "^1.5.0" + "@sapphire/snowflake" "^3.2.2" + discord-api-types "^0.37.3" + file-type "^17.1.6" + tslib "^2.4.0" + undici "^5.9.1" "@eslint/eslintrc@^0.4.2": version "0.4.2" @@ -138,25 +112,33 @@ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621" integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg== -"@sapphire/async-queue@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.1.4.tgz#ae431310917a8880961cebe8e59df6ffa40f2957" - integrity sha512-fFrlF/uWpGOX5djw5Mu2Hnnrunao75WGey0sP0J3jnhmrJ5TAPzHYOmytD5iN/+pMxS+f+u/gezqHa9tPhRHEA== +"@sapphire/async-queue@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.0.tgz#2f255a3f186635c4fb5a2381e375d3dfbc5312d8" + integrity sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA== -"@sapphire/snowflake@^1.3.5": - version "1.3.6" - resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-1.3.6.tgz#166e8c5c08d01c861edd7e2edc80b5739741715f" - integrity sha512-QnzuLp+p9D7agynVub/zqlDVriDza9y3STArBhNiNBUgIX8+GL5FpQxstRfw1jDr5jkZUjcuKYAHxjIuXKdJAg== +"@sapphire/shapeshift@^3.5.1": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sapphire/shapeshift/-/shapeshift-3.6.0.tgz#988ff6576162a581a29bc26deb5492f7d1bf419f" + integrity sha512-tu2WLRdo5wotHRvsCkspg3qMiP6ETC3Q1dns1Q5V6zKUki+1itq6AbhMwohF9ZcLoYqg+Y8LkgRRtVxxTQVTBQ== + dependencies: + fast-deep-equal "^3.1.3" + lodash.uniqwith "^4.5.0" -"@sindresorhus/is@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.1.tgz#d26729db850fa327b7cacc5522252194404226f5" - integrity sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g== +"@sapphire/snowflake@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.2.2.tgz#faacdc1b5f7c43145a71eddba917de2b707ef780" + integrity sha512-ula2O0kpSZtX9rKXNeQMrHwNd7E4jPDJYUXmEGTFdMRfyfMw+FPyh04oKMjAiDuOi64bYgVkOV3MjK+loImFhQ== -"@tsconfig/node14@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" - integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== +"@tokenizer/token@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" + integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== + +"@tsconfig/node16@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== "@types/better-sqlite3@^5.4.3": version "5.4.3" @@ -207,10 +189,10 @@ resolved "https://registry.yarnpkg.com/@types/tz-offset/-/tz-offset-0.0.0.tgz#d58f1cebd794148d245420f8f0660305d320e565" integrity sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ== -"@types/ws@^7.4.7": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== +"@types/ws@^8.5.3": + version "8.5.3" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== dependencies: "@types/node" "*" @@ -381,12 +363,18 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -axios@^0.21.4: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== dependencies: - follow-redirects "^1.14.0" + follow-redirects "^1.14.9" + form-data "^4.0.0" balanced-match@^1.0.0: version "1.0.2" @@ -406,6 +394,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -413,7 +408,7 @@ braces@^3.0.1: dependencies: fill-range "^7.0.1" -callsites@^3.0.0, callsites@^3.1.0: +callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== @@ -436,17 +431,17 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: supports-color "^7.1.0" cheerio-select@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823" - integrity sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696" + integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== dependencies: - css-select "^4.1.3" - css-what "^5.0.1" + css-select "^4.3.0" + css-what "^6.0.1" domelementtype "^2.2.0" - domhandler "^4.2.0" - domutils "^2.7.0" + domhandler "^4.3.1" + domutils "^2.8.0" -cheerio@^1.0.0-rc.10: +cheerio@1.0.0-rc.10: version "1.0.0-rc.10" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== @@ -508,6 +503,11 @@ colorette@^1.2.2: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== +colorette@^2.0.7: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -545,21 +545,26 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -css-select@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" - integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA== +css-select@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== dependencies: boolbase "^1.0.0" - css-what "^5.0.0" - domhandler "^4.2.0" - domutils "^2.6.0" - nth-check "^2.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" -css-what@^5.0.0, css-what@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" - integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +dateformat@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: version "4.3.1" @@ -590,29 +595,27 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -discord-api-types@^0.18.1: - version "0.18.1" - resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.18.1.tgz#5d08ed1263236be9c21a22065d0e6b51f790f492" - integrity sha512-hNC38R9ZF4uaujaZQtQfm5CdQO58uhdkoHQAVvMfIL0LgOSZeW575W8H6upngQOuoxWd8tiRII3LLJm9zuQKYg== - -discord-api-types@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.22.0.tgz#34dc57fe8e016e5eaac5e393646cd42a7e1ccc2a" - integrity sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg== - -discord.js@^13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.0.1.tgz#58f009706d1d7587fe9ff6c6c781e4540ae085f9" - integrity sha512-pEODCFfxypBnGEYpSgjkn1jt70raCS1um7Zp0AXEfW1DcR29wISzQ/WeWdnjP5KTXGi0LTtkRiUjOsMgSoukxA== - dependencies: - "@discordjs/builders" "^0.4.0" - "@discordjs/collection" "^0.2.1" - "@discordjs/form-data" "^3.0.1" - "@sapphire/async-queue" "^1.1.4" - "@types/ws" "^7.4.7" - discord-api-types "^0.22.0" - node-fetch "^2.6.1" - ws "^7.5.1" +discord-api-types@^0.37.3: + version "0.37.9" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.9.tgz#3ceba0f047c5239967c50b180fe9d72c55c82464" + integrity sha512-mAJv7EcDDo6YztR3XSIED2IAHJcAlzWFD31Q2cHLURMKPb0CW0TM/r32HhAHO9uHw74N3cviOzJIZfZVB/e/5A== + +discord.js@^14.3.0: + version "14.3.0" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.3.0.tgz#9e43df7e4d2d14b11f3de751236e983159c3d3d0" + integrity sha512-CpIwoAAuELiHSgVKRMzsCADS6ZlJwAZ9RlvcJYdEgS00aW36dSvXyBgE+S3pigkc7G+jU6BEalMUWIJFveqrBQ== + dependencies: + "@discordjs/builders" "^1.2.0" + "@discordjs/collection" "^1.1.0" + "@discordjs/rest" "^1.1.0" + "@sapphire/snowflake" "^3.2.2" + "@types/ws" "^8.5.3" + discord-api-types "^0.37.3" + fast-deep-equal "^3.1.3" + lodash.snakecase "^4.1.1" + tslib "^2.4.0" + undici "^5.9.1" + ws "^8.8.1" doctrine@^3.0.0: version "3.0.0" @@ -622,23 +625,23 @@ doctrine@^3.0.0: esutils "^2.0.2" dom-serializer@^1.0.1, dom-serializer@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== dependencies: domelementtype "^2.0.1" domhandler "^4.2.0" entities "^2.0.0" domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== -domhandler@^4.0.0, domhandler@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f" - integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w== +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== dependencies: domelementtype "^2.2.0" @@ -647,7 +650,7 @@ domino@^2.1.6: resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe" integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ== -domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: +domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== @@ -656,18 +659,18 @@ domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: domelementtype "^2.2.0" domhandler "^4.2.0" -dot-prop@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" - integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== - dependencies: - is-obj "^2.0.0" - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -842,6 +845,11 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +fast-copy@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-2.1.3.tgz#bf6e05ac3cb7a9d66fbf12c51dd4440e9ddd4afb" + integrity sha512-LDzYKNTHhD+XOp8wGMuCkY4eTxFZOOycmpwLBiuF3r3OjOmZnURRD8t2dUAbmKuXGbo/MGggwbSjcBdp8QT0+g== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -868,6 +876,16 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-redact@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" + integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== + +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fastq@^1.6.0: version "1.11.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" @@ -882,6 +900,15 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-type@^17.1.6: + version "17.1.6" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-17.1.6.tgz#18669e0577a4849ef6e73a41f8bdf1ab5ae21023" + integrity sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw== + dependencies: + readable-web-to-node-stream "^3.0.2" + strtok3 "^7.0.0-alpha.9" + token-types "^5.0.0-alpha.2" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -902,10 +929,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== -follow-redirects@^1.14.0: - version "1.14.4" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" - integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== +follow-redirects@^1.14.9: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== form-data@^4.0.0: version "4.0.0" @@ -955,6 +982,17 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^13.6.0, globals@^13.9.0: version "13.9.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb" @@ -984,6 +1022,14 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +help-me@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.1.0.tgz#c105e78ba490d6fcaa61a3d0cd06e0054554efab" + integrity sha512-5HMrkOks2j8Fpu2j5nTLhrBhT7VwHwELpqnSnx802ckofys5MO2SkLpgSz3dgNFHV7IYFX2igm5CM75SmuYidw== + dependencies: + glob "^8.0.0" + readable-stream "^3.6.0" + htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -1004,6 +1050,11 @@ husky@^6.0.0: resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -1040,7 +1091,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1082,11 +1133,6 @@ is-obj@^1.0.1: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" @@ -1107,6 +1153,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1192,21 +1243,26 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.snakecase@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= +lodash.uniqwith@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz#7a0cbf65f43b5928625a9d4d0dc54b18cadc7ef3" + integrity sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q== + log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" @@ -1274,17 +1330,17 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -moment-timezone@^0.5.31: - version "0.5.33" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" - integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== +minimatch@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== dependencies: - moment ">= 2.9.0" + brace-expansion "^2.0.1" -"moment@>= 2.9.0": - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== ms@2.1.2: version "2.1.2" @@ -1296,17 +1352,12 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -node-cron@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" - integrity sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA== +node-cron@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.2.tgz#bb0681342bd2dfb568f28e464031280e7f06bd01" + integrity sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ== dependencies: - moment-timezone "^0.5.31" - -node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + uuid "8.3.2" normalize-path@^3.0.0: version "3.0.0" @@ -1320,14 +1371,19 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -nth-check@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" - integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== dependencies: boolbase "^1.0.0" -once@^1.3.0: +on-exit-leak-free@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" + integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -1353,18 +1409,6 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -ow@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/ow/-/ow-0.27.0.tgz#d44da088e8184fa11de64b5813206f9f86ab68d0" - integrity sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ== - dependencies: - "@sindresorhus/is" "^4.0.1" - callsites "^3.1.0" - dot-prop "^6.0.1" - lodash.isequal "^4.5.0" - type-fest "^1.2.1" - vali-date "^1.0.0" - p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -1424,11 +1468,66 @@ path@^0.12.7: process "^0.11.1" util "^0.10.3" +peek-readable@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.0.0.tgz#7ead2aff25dc40458c60347ea76cfdfd63efdfec" + integrity sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A== + picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" + integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-pretty@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-9.1.0.tgz#162f525944d7eede154db8055cb0ab5e00bde0c4" + integrity sha512-IM6NY9LLo/dVgY7/prJhCh4rAJukafdt0ibxeNOWc2fxKMyTk90SOB9Ao2HfbtShT9QPeP0ePpJktksMhSQMYA== + dependencies: + colorette "^2.0.7" + dateformat "^4.6.3" + fast-copy "^2.1.1" + fast-safe-stringify "^2.1.1" + help-me "^4.0.1" + joycon "^3.1.1" + minimist "^1.2.6" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.0.0" + pump "^3.0.0" + readable-stream "^4.0.0" + secure-json-parse "^2.4.0" + sonic-boom "^3.0.0" + strip-json-comments "^3.1.1" + +pino-std-serializers@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz#4c20928a1bafca122fdc2a7a4a171ca1c5f9c526" + integrity sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ== + +pino@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.5.0.tgz#60943fa2ec0ac4f22b1f8fde199cc2488547261e" + integrity sha512-PuD6sOti8Y+p9zRoNB5dibmfjfM/OU2tEtJFICxw5ulXi1d0qnq/Rt3CsR6aBEAOeyCXP+ZUfiNWW+tt55pNzg== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport v1.0.0 + pino-std-serializers "^6.0.0" + process-warning "^2.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^3.1.0" + thread-stream "^2.0.0" + please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -1453,6 +1552,11 @@ prisma@^3.0.0: dependencies: "@prisma/engines" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" +process-warning@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.0.0.tgz#341dbeaac985b90a04ebcd844d50097c7737b2ee" + integrity sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww== + process@^0.11.1: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -1463,6 +1567,14 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -1473,6 +1585,39 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.1.0.tgz#280d0a29f559d3fb684a277254e02b6f61ae0631" + integrity sha512-sVisi3+P2lJ2t0BPbpK629j8wRW06yKGJUcaLAGXPAUhyUxVJm7VsCTit1PFgT4JHUDMrGNR+ZjSKpzGaRF3zw== + dependencies: + abort-controller "^3.0.0" + +readable-web-to-node-stream@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" + integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw== + dependencies: + readable-stream "^3.6.0" + +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + regexpp@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -1530,11 +1675,26 @@ rxjs@^6.6.7: dependencies: tslib "^1.9.0" +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" + integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== + sax@>=0.6.0: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +secure-json-parse@^2.4.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.5.0.tgz#f929829df2adc7ccfb53703569894d051493a6ac" + integrity sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w== + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" @@ -1587,6 +1747,18 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +sonic-boom@^3.0.0, sonic-boom@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.0.tgz#ce9f2de7557e68be2e52c8df6d9b052e7d348143" + integrity sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA== + dependencies: + atomic-sleep "^1.0.0" + +split2@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" + integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -1606,6 +1778,13 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -1632,6 +1811,14 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strtok3@^7.0.0-alpha.9: + version "7.0.0" + resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-7.0.0.tgz#868c428b4ade64a8fd8fee7364256001c1a4cbe5" + integrity sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ== + dependencies: + "@tokenizer/token" "^0.3.0" + peek-readable "^5.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -1663,6 +1850,13 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thread-stream@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8" + integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ== + dependencies: + real-require "^0.2.0" + through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -1675,25 +1869,28 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -ts-mixer@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.0.tgz#4e631d3a36e3fa9521b973b132e8353bc7267f9f" - integrity sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ== +token-types@^5.0.0-alpha.2: + version "5.0.1" + resolved "https://registry.yarnpkg.com/token-types/-/token-types-5.0.1.tgz#aa9d9e6b23c420a675e55413b180635b86a093b4" + integrity sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg== + dependencies: + "@tokenizer/token" "^0.3.0" + ieee754 "^1.2.1" + +ts-mixer@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.1.tgz#7c2627fb98047eb5f3c7f2fee39d1521d18fe87a" + integrity sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg== tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.2.0, tslib@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -tslib@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" - integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +tslib@^2.2.0, tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== tsutils@^3.21.0: version "3.21.0" @@ -1726,15 +1923,15 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-fest@^1.2.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" - integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== +typescript@^4.8.3: + version "4.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88" + integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig== -typescript@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc" - integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew== +undici@^5.9.1: + version "5.10.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.10.0.tgz#dd9391087a90ccfbd007568db458674232ebf014" + integrity sha512-c8HsD3IbwmjjbLvoZuRI26TZic+TSEe8FPMLLOkN1AfYRhdjnKBU6yL+IwcSCbdZiX4e5t0lfMDLDCqj4Sq70g== uri-js@^4.2.2: version "4.4.1" @@ -1743,6 +1940,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + util@^0.10.3: version "0.10.4" resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" @@ -1750,16 +1952,16 @@ util@^0.10.3: dependencies: inherits "2.0.3" +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -vali-date@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6" - integrity sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY= - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -1795,10 +1997,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^7.5.1: - version "7.5.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" - integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== +ws@^8.8.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" + integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== xml2js@^0.4.19: version "0.4.23" From a0ced12ab26ed728896fed61c1f796c277fa70a7 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 15 Sep 2022 14:39:15 +0100 Subject: [PATCH 088/101] Upgrade dependencies to latest patch version --- yarn.lock | 606 +++++++++++++++++++++++++++--------------------------- 1 file changed, 305 insertions(+), 301 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6762edb..f190f64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,23 +10,23 @@ "@babel/highlight" "^7.10.4" "@babel/code-frame@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" - integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== dependencies: - "@babel/highlight" "^7.14.5" + "@babel/highlight" "^7.18.6" -"@babel/helper-validator-identifier@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" - integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== +"@babel/helper-validator-identifier@^7.18.6": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== -"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" - integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== +"@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== dependencies: - "@babel/helper-validator-identifier" "^7.14.5" + "@babel/helper-validator-identifier" "^7.18.6" chalk "^2.0.0" js-tokens "^4.0.0" @@ -59,10 +59,10 @@ tslib "^2.4.0" undici "^5.9.1" -"@eslint/eslintrc@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.2.tgz#f63d0ef06f5c0c57d76c4ab5f63d3835c51b0179" - integrity sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg== +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -74,6 +74,20 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -88,29 +102,29 @@ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz#94c23db18ee4653e129abd26fb06f870ac9e1ee2" - integrity sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA== + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" "@prisma/client@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.0.2.tgz#f04d9b252f3d0c6918df43ad228eac27d03f6db1" - integrity sha512-6SrDYY2Yr5AmYpVB3XAXFqfzxKMdDTemXR7FmfXthnxWhQHoBwRLNZ3B3GyI/MmWa5tr+kaaGDJjp1LU0vuYvQ== + version "3.15.2" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.15.2.tgz#2181398147afc79bfe0d83c03a88dc45b49bd365" + integrity sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w== dependencies: - "@prisma/engines-version" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" + "@prisma/engines-version" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" -"@prisma/engines-version@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db": - version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#c45323e420f47dd950b22c873bdcf38f75e65779" - integrity sha512-iArSApZZImVmT9oC/rGOjzvpG2AOqlIeqYcVnop9poA3FxD4zfVPbNPH9DTgOWhc06OkBHujJZeAcsNddVabIQ== +"@prisma/engines-version@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e": + version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53" + integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w== -"@prisma/engines@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db": - version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621" - integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg== +"@prisma/engines@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e": + version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#f691893df506b93e3cb1ccc15ec6e5ac64e8e570" + integrity sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg== "@sapphire/async-queue@^1.5.0": version "1.5.0" @@ -153,26 +167,26 @@ integrity sha512-QQojPymFcV1hrvWXA1h0pP9RmFBFNuWikZcUEjjVsS19IyKO+jqOX24lp2ZHF4A21EmkosJhJDX7CLG67F2s7A== "@types/json-schema@^7.0.7": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/node-cron@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-2.0.4.tgz#6d467440762e7d3539890d477b33670c020c458f" - integrity sha512-vXzgDRWCZpuut5wJVZtluEnkNhzGojYlyMch2c4kMj7H74L8xTLytVlgQzj+/17wfcjs49aJDFBDglFSGt7GeA== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-2.0.5.tgz#e244709a86d32453c5a702ced35b53db683fbc8e" + integrity sha512-rQ4kduTmgW11tbtx0/RsoybYHHPu4Vxw5v5ZS5qUKNerlEAI8r8P1F5UUZ2o2HTvzG759sbFxuRuqWxU8zc+EQ== dependencies: "@types/tz-offset" "*" "@types/node@*": - version "16.4.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.13.tgz#7dfd9c14661edc65cccd43a29eb454174642370d" - integrity sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg== + version "18.7.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154" + integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg== "@types/node@^15.12.5": - version "15.12.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.5.tgz#9a78318a45d75c9523d2396131bd3cca54b2d185" - integrity sha512-se3yX7UHv5Bscf8f1ERKvQOD6sTyycH3hdaoozvaLxgUiY5lIGEeH37AD0G0Qi9kPqihPn0HOfd2yaIEN9VwEg== + version "15.14.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" + integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== "@types/parse-json@^4.0.0": version "4.0.0" @@ -197,72 +211,73 @@ "@types/node" "*" "@typescript-eslint/eslint-plugin@^4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz#c045e440196ae45464e08e20c38aff5c3a825947" - integrity sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ== + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" + integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== dependencies: - "@typescript-eslint/experimental-utils" "4.28.1" - "@typescript-eslint/scope-manager" "4.28.1" + "@typescript-eslint/experimental-utils" "4.33.0" + "@typescript-eslint/scope-manager" "4.33.0" debug "^4.3.1" functional-red-black-tree "^1.0.1" + ignore "^5.1.8" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz#3869489dcca3c18523c46018b8996e15948dbadc" - integrity sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q== +"@typescript-eslint/experimental-utils@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" + integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.28.1" - "@typescript-eslint/types" "4.28.1" - "@typescript-eslint/typescript-estree" "4.28.1" + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" "@typescript-eslint/parser@^4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.1.tgz#5181b81658414f47291452c15bf6cd44a32f85bd" - integrity sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg== + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899" + integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA== dependencies: - "@typescript-eslint/scope-manager" "4.28.1" - "@typescript-eslint/types" "4.28.1" - "@typescript-eslint/typescript-estree" "4.28.1" + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" debug "^4.3.1" -"@typescript-eslint/scope-manager@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz#fd3c20627cdc12933f6d98b386940d8d0ce8a991" - integrity sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA== +"@typescript-eslint/scope-manager@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" + integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== dependencies: - "@typescript-eslint/types" "4.28.1" - "@typescript-eslint/visitor-keys" "4.28.1" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" -"@typescript-eslint/types@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.1.tgz#d0f2ecbef3684634db357b9bbfc97b94b828f83f" - integrity sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg== +"@typescript-eslint/types@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" + integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== -"@typescript-eslint/typescript-estree@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz#af882ae41740d1f268e38b4d0fad21e7e8d86a81" - integrity sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ== +"@typescript-eslint/typescript-estree@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" + integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== dependencies: - "@typescript-eslint/types" "4.28.1" - "@typescript-eslint/visitor-keys" "4.28.1" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" debug "^4.3.1" globby "^11.0.3" is-glob "^4.0.1" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz#162a515ee255f18a6068edc26df793cdc1ec9157" - integrity sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg== +"@typescript-eslint/visitor-keys@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" + integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== dependencies: - "@typescript-eslint/types" "4.28.1" + "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" abort-controller@^3.0.0: @@ -273,9 +288,9 @@ abort-controller@^3.0.0: event-target-shim "^5.0.0" acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn@^7.4.0: version "7.4.1" @@ -301,9 +316,9 @@ ajv@^6.10.0, ajv@^6.12.4: uri-js "^4.2.2" ajv@^8.0.1: - version "8.6.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.0.tgz#60cc45d9c46a477d80d92c48076d972c342e5720" - integrity sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ== + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -311,9 +326,9 @@ ajv@^8.0.1: uri-js "^4.2.2" ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^4.3.0: version "4.3.2" @@ -322,7 +337,7 @@ ansi-escapes@^4.3.0: dependencies: type-fest "^0.21.3" -ansi-regex@^5.0.0: +ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -361,7 +376,7 @@ astral-regex@^2.0.0: asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== atomic-sleep@^1.0.0: version "1.0.0" @@ -384,7 +399,7 @@ balanced-match@^1.0.0: boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@^1.1.7: version "1.1.11" @@ -401,7 +416,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.1: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -422,10 +437,10 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" - integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -466,7 +481,7 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-truncate@^2.1.0: +cli-truncate@2.1.0, cli-truncate@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== @@ -491,19 +506,19 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== +colorette@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== -colorette@^2.0.7: +colorette@^2.0.16, colorette@^2.0.7: version "2.0.19" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== @@ -515,20 +530,20 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commander@^8.2.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== +cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== dependencies: "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" @@ -566,27 +581,22 @@ dateformat@^4.6.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== +debug@^4.0.1, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -dedent@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" - integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= - deep-is@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== dir-glob@^3.0.1: version "3.0.1" @@ -596,9 +606,9 @@ dir-glob@^3.0.1: path-type "^4.0.0" discord-api-types@^0.37.3: - version "0.37.9" - resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.9.tgz#3ceba0f047c5239967c50b180fe9d72c55c82464" - integrity sha512-mAJv7EcDDo6YztR3XSIED2IAHJcAlzWFD31Q2cHLURMKPb0CW0TM/r32HhAHO9uHw74N3cviOzJIZfZVB/e/5A== + version "0.37.10" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.10.tgz#d596ed7178f349009e6a9109de5724f11a1de157" + integrity sha512-NvDh2Puc3wZQzQt2zLavlI5ewBnLFjI46/NJmvWIs6OC0JOkq7KcmH4s80X2+22mSQ3wUyge2mxq3cGYRT2noQ== discord.js@^14.3.0: version "14.3.0" @@ -693,7 +703,7 @@ error-ex@^1.3.1: escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^4.0.0: version "4.0.0" @@ -701,9 +711,9 @@ escape-string-regexp@^4.0.0: integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-config-prettier@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== eslint-scope@^5.1.1: version "5.1.1" @@ -738,12 +748,13 @@ eslint-visitor-keys@^2.0.0: integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== eslint@^7.29.0: - version "7.29.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0" - integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA== + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== dependencies: "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.2" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -816,9 +827,9 @@ estraverse@^4.1.1: integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" @@ -830,7 +841,7 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -execa@^5.0.0: +execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -855,10 +866,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.1.1: - version "3.2.6" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.6.tgz#434dd9529845176ea049acc9343e8282765c6e1a" - integrity sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ== +fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -874,7 +885,7 @@ fast-json-stable-stringify@^2.0.0: fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-redact@^3.1.1: version "3.1.2" @@ -887,9 +898,9 @@ fast-safe-stringify@^2.1.1: integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== fastq@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== dependencies: reusify "^1.0.4" @@ -925,9 +936,9 @@ flat-cache@^3.0.4: rimraf "^3.0.2" flatted@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" - integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== follow-redirects@^1.14.9: version "1.15.2" @@ -946,12 +957,12 @@ form-data@^4.0.0: fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" @@ -971,14 +982,14 @@ glob-parent@^5.1.2: is-glob "^4.0.1" glob@^7.1.3: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" @@ -994,28 +1005,28 @@ glob@^8.0.0: once "^1.3.0" globals@^13.6.0, globals@^13.9.0: - version "13.9.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb" - integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA== + version "13.17.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4" + integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== dependencies: type-fest "^0.20.2" globby@^11.0.3: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" slash "^3.0.0" has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" @@ -1060,10 +1071,10 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +ignore@^5.1.8, ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -1076,7 +1087,7 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== indent-string@^4.0.0: version "4.0.0" @@ -1086,7 +1097,7 @@ indent-string@^4.0.0: inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -1099,17 +1110,17 @@ inherits@2, inherits@^2.0.3: inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -1117,9 +1128,9 @@ is-fullwidth-code-point@^3.0.0: integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-glob@^4.0.0, is-glob@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -1131,27 +1142,22 @@ is-number@^7.0.0: is-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" - integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== joycon@^3.1.1: version "3.1.1" @@ -1189,7 +1195,7 @@ json-schema-traverse@^1.0.0: json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== levn@^0.4.1: version "0.4.1" @@ -1200,49 +1206,44 @@ levn@^0.4.1: type-check "~0.4.0" lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== lint-staged@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" - integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== - dependencies: - chalk "^4.1.1" - cli-truncate "^2.1.0" - commander "^7.2.0" - cosmiconfig "^7.0.0" - debug "^4.3.1" - dedent "^0.7.0" + version "11.2.6" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.2.6.tgz#f477b1af0294db054e5937f171679df63baa4c43" + integrity sha512-Vti55pUnpvPE0J9936lKl0ngVeTdSZpEdTNhASbkaWX7J5R9OEifo1INBGQuGW4zmy6OG+TcWPJ3m5yuy5Q8Tg== + dependencies: + cli-truncate "2.1.0" + colorette "^1.4.0" + commander "^8.2.0" + cosmiconfig "^7.0.1" + debug "^4.3.2" enquirer "^2.3.6" - execa "^5.0.0" - listr2 "^3.8.2" - log-symbols "^4.1.0" + execa "^5.1.1" + listr2 "^3.12.2" micromatch "^4.0.4" normalize-path "^3.0.0" please-upgrade-node "^3.2.0" string-argv "0.3.1" - stringify-object "^3.3.0" + stringify-object "3.3.0" + supports-color "8.1.1" -listr2@^3.8.2: - version "3.10.0" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" - integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== +listr2@^3.12.2: + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== dependencies: cli-truncate "^2.1.0" - colorette "^1.2.2" + colorette "^2.0.16" log-update "^4.0.0" p-map "^4.0.0" - rxjs "^6.6.7" + rfdc "^1.3.0" + rxjs "^7.5.1" through "^2.3.8" wrap-ansi "^7.0.0" -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -1256,21 +1257,13 @@ lodash.snakecase@^4.1.1: lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== lodash.uniqwith@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz#7a0cbf65f43b5928625a9d4d0dc54b18cadc7ef3" integrity sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q== -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - log-update@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" @@ -1293,40 +1286,40 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.2" + picomatch "^2.3.1" -mime-db@1.48.0: - version "1.48.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" - integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.12: - version "2.1.31" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" - integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.48.0" + mime-db "1.52.0" mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -1350,7 +1343,7 @@ ms@2.1.2: natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== node-cron@^3.0.2: version "3.0.2" @@ -1386,7 +1379,7 @@ on-exit-leak-free@^2.1.0: once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -1448,7 +1441,7 @@ parse5@^6.0.1: path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" @@ -1463,7 +1456,7 @@ path-type@^4.0.0: path@^0.12.7: version "0.12.7" resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" - integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= + integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== dependencies: process "^0.11.1" util "^0.10.3" @@ -1473,10 +1466,10 @@ peek-readable@^5.0.0: resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.0.0.tgz#7ead2aff25dc40458c60347ea76cfdfd63efdfec" integrity sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A== -picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0: version "1.0.0" @@ -1541,16 +1534,16 @@ prelude-ls@^1.2.1: integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prettier@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" - integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== prisma@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.0.2.tgz#e86cb6abf4a815c7ac97b9d0ed383f01c253ce34" - integrity sha512-TyOCbtWGDVdWvsM1RhUzJXoGClXGalHhyYWIc5eizSF8T1ScGiOa34asBUdTnXOUBFSErbsqMNw40DHAteBm1A== + version "3.15.2" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.15.2.tgz#4ebe32fb284da3ac60c49fbc16c75e56ecf32067" + integrity sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA== dependencies: - "@prisma/engines" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" + "@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" process-warning@^2.0.0: version "2.0.0" @@ -1560,7 +1553,7 @@ process-warning@^2.0.0: process@^0.11.1: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== progress@^2.0.0: version "2.0.3" @@ -1646,6 +1639,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -1668,12 +1666,12 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^6.6.7: - version "6.6.7" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== +rxjs@^7.5.1: + version "7.5.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" + integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== dependencies: - tslib "^1.9.0" + tslib "^2.1.0" safe-buffer@~5.2.0: version "5.2.1" @@ -1698,12 +1696,12 @@ secure-json-parse@^2.4.0: semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== semver@^7.2.1, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" @@ -1720,9 +1718,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== slash@^3.0.0: version "3.0.0" @@ -1762,21 +1760,21 @@ split2@^4.0.0: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== string-argv@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" string_decoder@^1.1.1: version "1.3.0" @@ -1785,7 +1783,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -stringify-object@^3.3.0: +stringify-object@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== @@ -1794,12 +1792,12 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" strip-final-newline@^2.0.0: version "2.0.0" @@ -1819,6 +1817,13 @@ strtok3@^7.0.0-alpha.9: "@tokenizer/token" "^0.3.0" peek-readable "^5.0.0" +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -1834,21 +1839,20 @@ supports-color@^7.1.0: has-flag "^4.0.0" table@^6.0.9: - version "6.7.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" - integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== + version "6.8.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" + integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== dependencies: ajv "^8.0.1" - lodash.clonedeep "^4.5.0" lodash.truncate "^4.4.2" slice-ansi "^4.0.0" - string-width "^4.2.0" - strip-ansi "^6.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== thread-stream@^2.0.0: version "2.2.0" @@ -1860,7 +1864,7 @@ thread-stream@^2.0.0: through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== to-regex-range@^5.0.1: version "5.0.1" @@ -1882,12 +1886,12 @@ ts-mixer@^6.0.1: resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.1.tgz#7c2627fb98047eb5f3c7f2fee39d1521d18fe87a" integrity sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg== -tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.2.0, tslib@^2.4.0: +tslib@^2.1.0, tslib@^2.2.0, tslib@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== @@ -1995,7 +1999,7 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== ws@^8.8.1: version "8.8.1" From 50651dfddf0ddc7cb63c844a537b5dee7a74929a Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 15 Sep 2022 14:39:34 +0100 Subject: [PATCH 089/101] Bump version to v2.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc4b919..d991b60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.5.5", + "version": "2.6.0", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From acc8f5715f9082c8869d0271e6527070c5c89e0a Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 15 Sep 2022 16:38:31 +0100 Subject: [PATCH 090/101] Bump docker node version to 16.17.0 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b96ee02..cdb851a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16.6.1-alpine3.14 as ts-compiler +FROM node:16.17.0-alpine3.16 as ts-compiler ARG DATABASE_URL ENV DATABASE_URL ${DATABASE_URL} WORKDIR /app @@ -9,7 +9,7 @@ COPY ./ ./ RUN yarn run prisma generate RUN yarn build -FROM node:16.6.1-alpine3.14 +FROM node:16.17.0-alpine3.16 ARG DATABASE_URL ENV DATABASE_URL ${DATABASE_URL} WORKDIR /app From 4ce55d3283abf16eae9e4e937c4a740a227aa0ce Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 15 Sep 2022 16:38:51 +0100 Subject: [PATCH 091/101] Bump version to v2.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d991b60..91795ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.6.0", + "version": "2.6.1", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From dd039d1bc0bb9b803959693c27628743e5d3264b Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 19 Sep 2022 20:51:53 +0100 Subject: [PATCH 092/101] Add error logs to all catch blocks --- src/modules/courses.ts | 29 ++++++- src/modules/degrees.ts | 31 ++++++- src/modules/galleryChannels.ts | 14 ++++ src/modules/leaderboard.ts | 8 +- src/modules/misc.ts | 6 ++ src/modules/polls.ts | 6 ++ src/modules/roleSelection.ts | 143 ++++++++++++++++++++++++++------- src/modules/sudo.ts | 3 + src/modules/welcome.ts | 4 + 9 files changed, 210 insertions(+), 34 deletions(-) diff --git a/src/modules/courses.ts b/src/modules/courses.ts index 989506a..de71118 100644 --- a/src/modules/courses.ts +++ b/src/modules/courses.ts @@ -140,6 +140,7 @@ export async function handleCommand( interaction.guild ); + logger.info("Refreshed courses channels"); await interaction.editReply( utils.CheckMarkEmoji + "Sucessfully updated course channels." + @@ -151,7 +152,7 @@ export async function handleCommand( : "") ); } catch (e) { - logger.error(e, "Error while refreshing channels"); + logger.error(e, "Error while refreshing courses channels"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -189,13 +190,17 @@ export async function handleCommand( }, }); + logger.info({ categories }, "Set course channels' categories"); await interaction.editReply( utils.CheckMarkEmoji + "Sucessfully updated course categories.\n" + categories.map((c) => `- <#${c?.id}>`).join("\n") ); } catch (e) { - logger.error(e, "Error while settings categories"); + logger.error( + e, + "Error while setting course channels' categories" + ); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -254,18 +259,27 @@ export async function handleCommand( } } } catch (e) { + logger.error( + e, + "Failed to delete channel and/or role while toggling course's channel visibility" + ); await interaction.editReply( utils.XEmoji + "Could not delete channel and/or role, but settings were updated correctly. Please delete the channel/role manually." ); return; } + logger.info({ course }, "Course channel has been hidden"); await interaction.editReply( utils.CheckMarkEmoji + `Course \`${course.name}\` is now hidden` ); } else { await refreshCourses(prisma, interaction.guild); + logger.info( + { course }, + "Course channel is now being shown" + ); await interaction.editReply( utils.CheckMarkEmoji + `Course \`${course.name}\` is now being shown` @@ -334,6 +348,7 @@ export async function handleCommand( ); } + logger.info({ oldAcronym, newAcronym }, "Renamed course"); await interaction.editReply( utils.CheckMarkEmoji + "Course renamed succesfully!" ); @@ -435,6 +450,10 @@ export async function handleCommand( }, }); + logger.info( + { academicYear }, + "Academic year has been set" + ); await interaction.editReply( `The current academic year has been set to \`${academicYear}\`` ); @@ -467,6 +486,12 @@ export async function importCoursesFromDegree( const degreeCourses = await fenix.getDegreeCourses(degreeId, academicYear); + logger.info( + { degreeId }, + "Got %d courses from Fénix for degree", + degreeCourses.length + ); + if (force) { await prisma.degreeCourse.deleteMany({ where: { degreeFenixId: degreeId }, diff --git a/src/modules/degrees.ts b/src/modules/degrees.ts index 45b33b0..7a2c7cd 100644 --- a/src/modules/degrees.ts +++ b/src/modules/degrees.ts @@ -374,10 +374,11 @@ export async function handleCommand( switch (interaction.options.getSubcommand()) { case "create": { try { + const acronym = interaction.options.getString("acronym", true); const result = await createDegree( prisma, interaction.guild, - interaction.options.getString("acronym", true), + acronym, interaction.options.getRole("role", true) as Discord.Role, parseInt(interaction.options.getString("tier", true)), interaction.options.getString("fenix-acronym", false), @@ -399,8 +400,10 @@ export async function handleCommand( ) as Discord.GuildChannel | null ); if (typeof result === "string") { + logger.error({ result }, "Error while creating degree"); await interaction.editReply(utils.XEmoji + result); } else { + logger.info({ result, acronym }, "Created degree"); await interaction.editReply( utils.CheckMarkEmoji + "Sucessfully created degree." + @@ -440,6 +443,7 @@ export async function handleCommand( ], }); } catch (e) { + logger.error(e, "Error while listing degrees"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -524,6 +528,7 @@ export async function handleCommand( }); } } catch (e) { + logger.error(e, "Error while viewing a degree"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -537,10 +542,12 @@ export async function handleCommand( await prisma.degree.delete({ where: { acronym } }); + logger.info({ acronym }, "Deleted a degree"); await interaction.editReply( utils.CheckMarkEmoji + "Successfully removed." ); } catch (e) { + logger.error(e, "Error while deleting a degree"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -558,11 +565,13 @@ export async function handleCommand( data: { name: newName }, }); + logger.info({ acronym, newName }, "Renamed a degree"); await interaction.editReply( utils.CheckMarkEmoji + `Successfully renamed ${acronym} to ${newName}` ); } catch (e) { + logger.error(e, "Error while renaming a degree"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -580,11 +589,13 @@ export async function handleCommand( data: { roleId: newRole.id }, }); + logger.info({ acronym, newRole }, "Set role of degree"); await interaction.editReply( utils.CheckMarkEmoji + `Successfully set role of ${acronym} to <@&${newRole.id}>` ); } catch (e) { + logger.error(e, "Error while setting role of degree"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -608,11 +619,16 @@ export async function handleCommand( data: { tier: numTier }, }); + logger.info( + { acronym, newTier: numTier }, + "Changed degree tier" + ); await interaction.editReply( utils.CheckMarkEmoji + `Successfully set tier of ${acronym} to ${newTier}` ); } catch (e) { + logger.error(e, "Error while setting tier of degree"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -647,7 +663,7 @@ export async function handleCommand( } if ( - channelType === "degreeVoice" && + channelType === "degree-voice" && newChannel.type !== Discord.ChannelType.GuildVoice ) { await interaction.editReply( @@ -655,7 +671,7 @@ export async function handleCommand( ); return; } else if ( - channelType !== "degreeVoice" && + channelType !== "degree-voice" && newChannel.type === Discord.ChannelType.GuildVoice ) { await interaction.editReply( @@ -669,6 +685,7 @@ export async function handleCommand( data: { [key]: newChannel.id }, }); + logger.info({ acronym, channelType, newChannel }); await interaction.editReply( utils.CheckMarkEmoji + `Successfully set ${channelType} of ${acronym} to <@#${newChannel.id}>` @@ -702,11 +719,19 @@ export async function handleCommand( true ); + logger.info( + { acronym }, + "Refreshed degree's courses from Fénix" + ); await interaction.editReply( utils.CheckMarkEmoji + `Degree \`${acronym}\`'s courses have been refreshed!` ); } catch (e) { + logger.error( + e, + "Failed to refresh degree's courses from Fénix" + ); await interaction.editReply( utils.XEmoji + "Something went wrong." ); diff --git a/src/modules/galleryChannels.ts b/src/modules/galleryChannels.ts index cad011e..5b82136 100644 --- a/src/modules/galleryChannels.ts +++ b/src/modules/galleryChannels.ts @@ -6,6 +6,7 @@ import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; import * as utils from "./utils"; +import logger from "../logger"; export async function handleMessage( message: Discord.Message, @@ -157,6 +158,7 @@ export async function handleCommand( utils.XEmoji + "Channel is already a gallery." ); } else { + logger.info({ channel }, "Added a gallery channel"); galleries.push(channel); await updateGalleries(prisma, galleries); await interaction.editReply( @@ -164,6 +166,7 @@ export async function handleCommand( ); } } catch (e) { + logger.error(e, "Error while adding a gallery channel"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -186,11 +189,13 @@ export async function handleCommand( prisma, galleries.filter((c) => c != channel) ); + logger.info({ channel }, "Removed a gallery channel"); await interaction.editReply( utils.CheckMarkEmoji + "Gallery successfully removed." ); } } catch (e) { + logger.error(e, "Error while removing a gallery channel"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -233,11 +238,20 @@ export async function handleCommand( ]; } const n = await parseExistingMessages(prisma, channels); + logger.info( + { channels }, + "Cleaned %d messages from gallery channels", + n + ); await interaction.editReply( utils.CheckMarkEmoji + `Cleaned \`${n}\` messages.` ); break; } catch (e) { + logger.error( + e, + "Error while cleaning gallery channel messages" + ); await interaction.editReply( utils.XEmoji + "Something went wrong." ); diff --git a/src/modules/leaderboard.ts b/src/modules/leaderboard.ts index ee18704..beaceb9 100644 --- a/src/modules/leaderboard.ts +++ b/src/modules/leaderboard.ts @@ -267,6 +267,10 @@ export async function handleCommand( prisma ) )) as [number, ThenArg>]; + logger.info( + { delta, channels, msgs, failed }, + "Send leaderboard" + ); await interaction.editReply( utils.CheckMarkEmoji + `Sent [here](https://discord.com/channels/${ @@ -286,6 +290,7 @@ ${ }` ); } catch (e) { + logger.error(e, "Error while sending leaderboard"); await interaction .editReply(utils.XEmoji + "Something went wrong.") .catch(() => logger.error("Leaderboard took too long :(")); @@ -300,12 +305,13 @@ ${ where: { key: "leaderboard:cache_stamp" }, }) ).value; + logger.info("Cleared leaderboard cache"); await interaction.editReply( utils.CheckMarkEmoji + `Successfully reset cache; last stamp was ${stamp}` ); } catch (e) { - logger.error(e, "Error while crearing cache of leaderboard"); + logger.error(e, "Error while clearing cache of leaderboard"); await interaction.editReply( utils.XEmoji + "Something went wrong, maybe there was no cache?" diff --git a/src/modules/misc.ts b/src/modules/misc.ts index 8c0714d..caeb421 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -179,6 +179,7 @@ export async function handleSayCommand( } throw new Error("???"); } catch (e) { + logger.error(e, "Failed to send message through /say"); await interaction.editReply(utils.XEmoji + "Something went wrong."); } } @@ -197,6 +198,7 @@ export async function handleWhoSaidCommand( await interaction.editReply("I don't know who said it..."); } } catch (e) { + logger.error(e, "Error while handling Who Said command"); await interaction.editReply( utils.XEmoji + "Something went wrong, maybe wrong message ID?" ); @@ -243,6 +245,10 @@ export async function handleMigrateMembersWithRole( count++; } + logger.info( + { oldRole, newRole, count, removeOld }, + "Migrated members from role to role" + ); await interaction.editReply( utils.CheckMarkEmoji + `Migrated ${count} members from ${oldRole} to ${newRole}` diff --git a/src/modules/polls.ts b/src/modules/polls.ts index 8f9a232..d9ce308 100644 --- a/src/modules/polls.ts +++ b/src/modules/polls.ts @@ -272,11 +272,13 @@ export async function handleCommand( await sendPollEmbed(poll, channel as TextChannel); } + logger.info({ id, title, channel, cron }, "Added poll"); await interaction.editReply( utils.CheckMarkEmoji + `Successfully added${cron ? " and scheduled" : ""}.` ); } catch (e) { + logger.error(e, "Failed to add poll"); await interaction.editReply( utils.XEmoji + "Something went wrong, maybe the ID already exists?" @@ -299,10 +301,12 @@ export async function handleCommand( await prisma.poll.delete({ where: { id } }); + logger.info({ id }, "Removed poll"); await interaction.editReply( utils.CheckMarkEmoji + "Successfully removed." ); } catch (e) { + logger.error(e, "Failed to remove poll"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -332,6 +336,7 @@ export async function handleCommand( ], }); } catch (e) { + logger.error(e, "Failed to list all polls"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); @@ -385,6 +390,7 @@ export async function handleCommand( }); } } catch (e) { + logger.error(e, "Failed to list poll"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); diff --git a/src/modules/roleSelection.ts b/src/modules/roleSelection.ts index 787233b..6869cbc 100644 --- a/src/modules/roleSelection.ts +++ b/src/modules/roleSelection.ts @@ -311,6 +311,10 @@ async function handleRoleSelection( throw new Error("Role not in group"); } } catch (e) { + logger.error( + { groupId, selectedRoles, err: e }, + "Failed to select role" + ); return false; } } @@ -742,6 +746,7 @@ async function createGroup( ).id, ]; } catch (e) { + logger.error(e, "Error while creating role selection group"); return [false, "Something went wrong"]; } } @@ -764,6 +769,7 @@ async function deleteGroup( return true; } catch (e) { + logger.error(e, "Error while deleting role selection group"); return "Something went wrong; possibly, no role group was found with that ID"; } } @@ -796,6 +802,7 @@ async function editGroup( return true; } catch (e) { + logger.error(e, "Error while editing role selection group"); return "Something went wrong; possibly, no role group was found with that ID"; } } @@ -822,6 +829,10 @@ async function setNumGroup( return true; } catch (e) { + logger.error( + e, + "Error while setting maximum/minimum number of options of role selection group" + ); return "Something went wrong; possibly, no role group was found with that ID"; } } @@ -847,6 +858,7 @@ async function moveGroup( return true; } catch (e) { + logger.error(e, "Error while moving role selection group"); return "Something went wrong; possibly, no role group was found with that ID"; } } @@ -897,6 +909,7 @@ async function viewGroup( return `No group was found with ID \`${id}\``; } } catch (e) { + logger.error(e, "Error while viewing role selection group"); return "Something went wrong"; } } @@ -934,6 +947,7 @@ async function addOption( return true; } catch (e) { + logger.error(e, "Error while adding option to role selection group"); return "Something went wrong; possibly, that role is already associated with an option"; } } @@ -975,6 +989,10 @@ async function removeOption( return true; } catch (e) { + logger.error( + e, + "Error while removing option from role selection group" + ); return "Something went wrong"; } } @@ -1006,11 +1024,16 @@ export async function handleCommand( // so I'm ignoring the possibility of it even existing ); if (succ) { + logger.info({ res }, "Created role selection group"); await interaction.editReply( utils.CheckMarkEmoji + `Role selection group \`${res}\` successfully created.` ); } else { + logger.error( + { res }, + "Failed to create role selection group" + ); await interaction.editReply( utils.XEmoji + `Could not create group because: **${res}**` @@ -1026,11 +1049,16 @@ export async function handleCommand( interaction.options.getString("confirm-id", true) ); if (res === true) { + logger.info({ id }, "Deleted role selection group"); await interaction.editReply( utils.CheckMarkEmoji + `Role selection group \`${id}\` successfully deleted.` ); } else { + logger.error( + { res }, + "Failed to delete role selection group" + ); await interaction.editReply( utils.XEmoji + `Could not delete group because: **${res}**` @@ -1040,18 +1068,24 @@ export async function handleCommand( } case "edit": { const id = interaction.options.getString("id", true); - const res = await editGroup( - prisma, - id, - interaction.options.getString("name", true), - interaction.options.getString("value", true) - ); + const name = interaction.options.getString("name", true); + const value = interaction.options.getString("value", true); + const res = await editGroup(prisma, id, name, value); + if (res === true) { + logger.info( + { id, name, value }, + "Edited role selection group" + ); await interaction.editReply( utils.CheckMarkEmoji + `Role selection group \`${id}\` successfully edited.` ); } else { + logger.info( + { id, name, value, res }, + "Error while editing role selection group" + ); await interaction.editReply( utils.XEmoji + `Could not edit group because: **${res}**` @@ -1061,18 +1095,23 @@ export async function handleCommand( } case "set-num": { const id = interaction.options.getString("id", true); - const res = await setNumGroup( - prisma, - id, - interaction.options.getInteger("min", true), - interaction.options.getInteger("max", true) - ); + const min = interaction.options.getInteger("min", true); + const max = interaction.options.getInteger("max", true); + const res = await setNumGroup(prisma, id, min, max); if (res === true) { + logger.info( + { id, min, max }, + "Set minimum/maximum of option selections for role selection group" + ); await interaction.editReply( utils.CheckMarkEmoji + `Role selection group \`${id}\` successfully edited.` ); } else { + logger.info( + { id, min, max, res }, + "Error while setting minimum/maximum of option selections for role selection group" + ); await interaction.editReply( utils.XEmoji + `Could not edit group because: **${res}**` @@ -1088,11 +1127,19 @@ export async function handleCommand( ) as Discord.GuildChannel; const res = await moveGroup(prisma, id, channel); if (res === true) { + logger.info( + { id, channel }, + "Moved role selection group" + ); await interaction.editReply( utils.CheckMarkEmoji + `Role selection group \`${id}\` successfully moved to <#${channel.id}>.` ); } else { + logger.info( + { id, channel, res }, + "Error while moving role selection group" + ); await interaction.editReply( utils.XEmoji + `Could not move group because: **${res}**` @@ -1143,22 +1190,41 @@ export async function handleCommand( case "options": switch (subCommand) { case "add": { + const groupId = interaction.options.getString( + "group-id", + true + ); + const label = interaction.options.getString("label", true); + const description = interaction.options.getString( + "description", + true + ); + const role = interaction.options.getRole( + "role", + true + ) as Discord.Role; + const emoji = interaction.options.getString("emoji", false); const res = await addOption( prisma, - interaction.options.getString("group-id", true), - interaction.options.getString("label", true), - interaction.options.getString("description", true), - interaction.options.getRole( - "role", - true - ) as Discord.Role, - interaction.options.getString("emoji", false) + groupId, + label, + description, + role, + emoji ); if (res === true) { + logger.info( + { groupId, label, description, role, emoji }, + "Added option to role selection group" + ); await interaction.editReply( utils.CheckMarkEmoji + "Option successfully added." ); } else { + logger.error( + { groupId, label, description, role, emoji, res }, + "Error while adding option to role selection group" + ); await interaction.editReply( utils.XEmoji + `Could not add option because: **${res}**` @@ -1167,17 +1233,26 @@ export async function handleCommand( break; } case "remove": { - const res = await removeOption( - prisma, - interaction.options.getString("group-id", true), - interaction.options.getString("label", true) + const groupId = interaction.options.getString( + "group-id", + true ); + const label = interaction.options.getString("label", true); + const res = await removeOption(prisma, groupId, label); if (res === true) { + logger.info( + { groupId, label }, + "Removed option from role selection group" + ); await interaction.editReply( utils.CheckMarkEmoji + "Option successfully removed." ); } else { + logger.error( + { groupId, label, res }, + "Error while removing option from role selection group" + ); await interaction.editReply( utils.XEmoji + `Could not remove option because: **${res}**` @@ -1191,15 +1266,20 @@ export async function handleCommand( switch (subCommand) { case "send-messages": { try { + const editExisting = interaction.options.getBoolean( + "edit-existing", + true + ); await sendRoleSelectionMessages( interaction.client, prisma, - interaction.options.getBoolean( - "edit-existing", - true - ) + editExisting ); + logger.info( + { editExisting }, + "Sent messages for all role selection groups" + ); await interaction.editReply( utils.CheckMarkEmoji + "Role selection messages successfully sent." @@ -1244,11 +1324,13 @@ export async function handleCommand( create: { key: fqkey, value }, }); + logger.info({ field, value }, "Set tourist info"); await interaction.editReply( utils.CheckMarkEmoji + `Successfully set TourIST ${field}.` ); } catch (e) { + logger.error(e, "Failed to set tourist info"); await interaction.editReply( utils.XEmoji + "Failed to set TourIST info." ); @@ -1275,11 +1357,13 @@ export async function handleCommand( create: { key: fqkey, value: channel.id }, }); + logger.info({ channel }, "Set tourist channel"); await interaction.editReply( utils.CheckMarkEmoji + `Successfully set TourIST channel.` ); } catch (e) { + logger.error(e, "Failed to set tourist channel"); await interaction.editReply( utils.XEmoji + "Failed to set TourIST channel." ); @@ -1300,11 +1384,13 @@ export async function handleCommand( create: { key: fqkey, value: role.id }, }); + logger.info({ role }, "Set tourist role"); await interaction.editReply( utils.CheckMarkEmoji + "Successfully set TourIST role." ); } catch (e) { + logger.error(e, "Error while setting tourist role"); await interaction.editReply( utils.XEmoji + "Failed to set TourIST role." ); @@ -1341,6 +1427,7 @@ export async function handleCommand( }); await interaction.editReply({ embeds: [embed] }); } catch (e) { + logger.error(e, "Error while getting tourist info"); await interaction.editReply( utils.XEmoji + "Something went wrong." ); diff --git a/src/modules/sudo.ts b/src/modules/sudo.ts index b072a2b..2fcabb7 100644 --- a/src/modules/sudo.ts +++ b/src/modules/sudo.ts @@ -6,6 +6,7 @@ import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; import * as utils from "./utils"; +import logger from "../logger"; export function provideCommands(): CommandDescriptor[] { const sudo = new Builders.SlashCommandBuilder() @@ -65,6 +66,7 @@ export async function handleSudoCommand( ); } } catch (e) { + logger.error(e, "Failed to toggle sudo for user"); await interaction.editReply( utils.XEmoji + "Failed to toggle `Admin+` role." ); @@ -91,6 +93,7 @@ export async function handleResetAdminCommand( utils.CheckMarkEmoji + "Successfully reset the `Admin+` role." ); } catch (e) { + logger.error(e, "Failed to reset sudo for user"); await interaction.editReply( utils.XEmoji + "Failed to reset the `Admin+` role." ); diff --git a/src/modules/welcome.ts b/src/modules/welcome.ts index c08bd4e..479d2c4 100644 --- a/src/modules/welcome.ts +++ b/src/modules/welcome.ts @@ -6,6 +6,7 @@ import * as Builders from "@discordjs/builders"; import { CommandDescriptor } from "../bot.d"; import * as utils from "./utils"; +import logger from "../logger"; async function getWelcomeChannel( prisma: PrismaClient, @@ -95,6 +96,7 @@ export async function handleCommand( value: channel.id, }, }); + logger.info({ channel }, "Set welcome channel"); await interaction.editReply( utils.CheckMarkEmoji + `Welcome channel successfully set as <#${channel.id}>.` @@ -110,6 +112,7 @@ export async function handleCommand( update: { value }, create: { key: "welcome:message", value }, }); + logger.info({ message }, "Set welcome message"); await interaction.editReply( utils.CheckMarkEmoji + `Welcome message successfully set.` ); @@ -141,6 +144,7 @@ export async function handleCommand( } } } catch (e) { + logger.error(e, "Error while handling welcome command"); await interaction.editReply(utils.XEmoji + "Something went wrong."); } } From 0b5263aaed7566610e2497dbec617820b4e7feb8 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 19 Sep 2022 20:54:15 +0100 Subject: [PATCH 093/101] Fix permission denied on public commands --- src/bot.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bot.ts b/src/bot.ts index b72dd09..0a25673 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -281,7 +281,10 @@ client.on("interactionCreate", async (interaction: Discord.Interaction) => { } else if (interaction.isChatInputCommand()) { await interaction.deferReply({ ephemeral: true }); - if (!interaction.command?.guildId) { + if ( + !interaction.command?.guildId && + interaction.guildId !== GUILD_ID + ) { // global commands const perms: Discord.PermissionsBitField | undefined = ( interaction.member as Discord.GuildMember From 9a92e9192ce6420a84893a875e6e4034202031a6 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 19 Sep 2022 20:55:57 +0100 Subject: [PATCH 094/101] Add Don't Ask to Ask usage logs --- src/modules/misc.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/misc.ts b/src/modules/misc.ts index caeb421..0fd27fc 100644 --- a/src/modules/misc.ts +++ b/src/modules/misc.ts @@ -211,6 +211,7 @@ export async function handleJustAskCommand( try { await interaction.channel?.send("https://dontasktoask.com/"); await interaction.editReply(utils.CheckMarkEmoji + "Sent"); + logger.info({ member: interaction.member }, "Used don't ask to ask"); } catch (e) { logger.error(e, "Error while executing just ask command"); await interaction.editReply(utils.XEmoji + "Something went wrong."); From 86cb24fbed2774a5e6f7e0485f5a69aa2d42aa4c Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 19 Sep 2022 20:57:32 +0100 Subject: [PATCH 095/101] Remove voice chat threads support --- src/bot.ts | 23 ---- src/modules/voiceThreads.ts | 214 ------------------------------------ 2 files changed, 237 deletions(-) delete mode 100644 src/modules/voiceThreads.ts diff --git a/src/bot.ts b/src/bot.ts index 0a25673..2600d0a 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -18,7 +18,6 @@ import * as roleSelection from "./modules/roleSelection"; import * as sudo from "./modules/sudo"; import * as misc from "./modules/misc"; import * as galleryChannels from "./modules/galleryChannels"; -import * as voiceThreads from "./modules/voiceThreads"; import * as welcome from "./modules/welcome"; import * as leaderboard from "./modules/leaderboard"; import * as degrees from "./modules/degrees"; @@ -64,7 +63,6 @@ const commandProviders: CommandProvider[] = [ sudo.provideCommands, misc.provideCommands, galleryChannels.provideCommands, - voiceThreads.provideCommands, welcome.provideCommands, leaderboard.provideCommands, degrees.provideCommands, @@ -328,27 +326,6 @@ client.on("messageCreate", async (message) => { await galleryChannels.handleMessage(message, prisma); }); -client.on("voiceStateUpdate", async (oldState, newState) => { - if (oldState.channelId === newState.channelId) { - return; - } - if (oldState.channelId !== null) { - try { - await voiceThreads.handleVoiceLeave(oldState, prisma); - } catch (e) { - logger.error(e, "Someone left a VC, GONE WRONG!!!:"); - } - } - - if (newState.channelId !== null) { - try { - await voiceThreads.handleVoiceJoin(newState, prisma); - } catch (e) { - logger.error(e, "Someone joined a VC, GONE WRONG!!!:"); - } - } -}); - client.on("guildMemberAdd", async (member) => { await welcome.handleGuildJoin(member, prisma); }); diff --git a/src/modules/voiceThreads.ts b/src/modules/voiceThreads.ts deleted file mode 100644 index b508bef..0000000 --- a/src/modules/voiceThreads.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { ChannelType } from "discord.js"; -// Controller for vc-chat threads - -import { PrismaClient } from "@prisma/client"; -import * as Discord from "discord.js"; -import * as Builders from "@discordjs/builders"; - -import { CommandDescriptor } from "../bot.d"; -import * as utils from "./utils"; - -const VC_CHAT_KEY = "voice_threads:vc_chat"; - -export function provideCommands(): CommandDescriptor[] { - const cmd = new Builders.SlashCommandBuilder() - .setName("voice-threads") - .setDescription("Manage voice threads"); - cmd.addSubcommand( - new Builders.SlashCommandSubcommandBuilder() - .setName("get-vc-chat") - .setDescription( - "Shows the currently set VC Chat under which threads will be created" - ) - ); - cmd.addSubcommand( - new Builders.SlashCommandSubcommandBuilder() - .setName("set-vc-chat") - .setDescription( - "Set under which channel voice threads will be created" - ) - .addChannelOption( - new Builders.SlashCommandChannelOption() - .setName("channel") - .setDescription("New vc-chat TEXT channel") - .setRequired(true) - ) - ); - return [{ builder: cmd, handler: handleCommand }]; -} - -export async function handleCommand( - interaction: Discord.ChatInputCommandInteraction, - prisma: PrismaClient -): Promise { - switch (interaction.options.getSubcommand()) { - case "get-vc-chat": { - try { - const channel = ( - await prisma.config.findFirst({ - where: { key: VC_CHAT_KEY }, - }) - )?.value; - - if (channel === undefined) { - await interaction.editReply( - utils.XEmoji + "No channel is currently set as vc-chat." - ); - } else { - await interaction.editReply( - `Current vc-chat: <#${channel}>` - ); - } - } catch (e) { - await interaction.editReply( - utils.XEmoji + "Something went wrong." - ); - } - break; - } - case "set-vc-chat": { - try { - const channel = interaction.options.getChannel( - "channel", - true - ) as Discord.GuildChannel; - - if (!channel.isTextBased()) { - await interaction.editReply( - utils.XEmoji + "Channel must be a text channel." - ); - } else { - await prisma.config.upsert({ - where: { key: VC_CHAT_KEY }, - update: { value: channel.id }, - create: { - key: VC_CHAT_KEY, - value: channel.id, - }, - }); - await interaction.editReply( - utils.CheckMarkEmoji + - `Successfully set vc-chat to <#${channel.id}>.` - ); - } - } catch (e) { - await interaction.editReply( - utils.XEmoji + "Something went wrong." - ); - } - break; - } - } -} - -async function fetchVCChat( - prisma: PrismaClient, - guild: Discord.Guild -): Promise { - const vcChatId = ( - await prisma.config.findFirst({ where: { key: VC_CHAT_KEY } }) - )?.value; - if (vcChatId === undefined) { - return null; - } - - return (await guild.channels.fetch(vcChatId)) as Discord.TextChannel | null; -} - -function normalizeVCName(name: string): string { - return ( - name - .normalize("NFD") - .replace(/\p{Diacritic}/gu, "") - .toLowerCase() - .replace(/\s/g, "-") - .replace(/[^a-z0-9-]/g, "") - .replace(/-{2,}/g, "-") + "-chat" - ); -} - -export async function handleVoiceJoin( - newState: Discord.VoiceState, - prisma: PrismaClient -): Promise { - if (newState.channel === null || newState.member === null) { - return; - } - - const vcChat = await fetchVCChat(prisma, newState.guild); - if (vcChat === null) { - return; - } - - const threadName = normalizeVCName(newState.channel.name); - const threadType = newState.guild.features.includes("PRIVATE_THREADS") - ? ChannelType.PrivateThread - : ChannelType.PublicThread; - - const threads = (await vcChat.threads.fetch()).threads; - const td = threads.filter( - (t) => t.type === threadType && t.name === threadName - ); - - const reason = `${newState.member.user.tag} joined voice channel ${newState.channel.name}`; - - if (!td.size) { - const newThread = await vcChat.threads.create({ - name: threadName, - autoArchiveDuration: 60, - type: threadType, - reason, - }); - td.set(newThread.id, newThread); - } - // can't do td[0] so need to iterate - for (const [_id, thread] of td) { - await thread.members.add( - newState.member as Discord.GuildMember, - reason - ); - } -} - -export async function handleVoiceLeave( - oldState: Discord.VoiceState, - prisma: PrismaClient -): Promise { - if (oldState.channel === null || oldState.member === null) { - return; - } - - const vcChat = await fetchVCChat(prisma, oldState.guild); - if (vcChat === null) { - return; - } - - const threadName = normalizeVCName(oldState.channel.name); - const threadType = oldState.guild.features.includes("PRIVATE_THREADS") - ? ChannelType.PrivateThread - : ChannelType.PublicThread; - - const threads = (await vcChat.threads.fetch()).threads; - const td = threads.filter( - (t) => t.type === threadType && t.name === threadName - ); - - const removeThread = - oldState.channel.members.filter( - (m) => m.id !== oldState.member?.id && !m.user.bot - ).size <= 0; - - // can't do td[0] so need to iterate - for (const [_id, thread] of td) { - if (removeThread) { - await thread.delete( - `Everyone left voice channel ${oldState.channel.name}` - ); - } else { - await thread.members.remove( - (oldState.member as Discord.GuildMember).id, - `${oldState.member.user.tag} left voice channel ${oldState.channel.name}` - ); - } - } -} From 81c5994d319d942040d974528398952dad131281 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 19 Sep 2022 20:57:59 +0100 Subject: [PATCH 096/101] Bump version to 2.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91795ef..ca44387 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.6.1", + "version": "2.7.0", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From 2d383fc06a2555b422aa0549a92d75abc22e1b2b Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 19 Sep 2022 21:16:00 +0100 Subject: [PATCH 097/101] Make course insertion into database sequential --- src/modules/courses.ts | 50 ++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/modules/courses.ts b/src/modules/courses.ts index de71118..d71ca12 100644 --- a/src/modules/courses.ts +++ b/src/modules/courses.ts @@ -498,37 +498,35 @@ export async function importCoursesFromDegree( }); } - await Promise.all( - degreeCourses.map(async (course) => { - const globalCourse = await prisma.course.findUnique({ - where: { acronym: course.acronym }, - }); - - if (!globalCourse) { - // Create global course since it doesn't exist + for (const course of degreeCourses) { + const globalCourse = await prisma.course.findUnique({ + where: { acronym: course.acronym }, + }); - await prisma.course.create({ - data: { - acronym: course.acronym, - displayAcronym: course.acronym, - name: course.name, - }, - }); - } + if (!globalCourse) { + // Create global course since it doesn't exist - await prisma.degreeCourse.create({ + await prisma.course.create({ data: { - id: `${degreeId}-${course.acronym}`, - degreeFenixId: degreeId, - courseAcronym: course.acronym, - year: course.year, - semester: course.semester, - announcementsFeedUrl: course.announcementsFeedUrl, - color: utils.generateHexCode(), + acronym: course.acronym, + displayAcronym: course.acronym, + name: course.name, }, }); - }) - ); + } + + await prisma.degreeCourse.create({ + data: { + id: `${degreeId}-${course.acronym}`, + degreeFenixId: degreeId, + courseAcronym: course.acronym, + year: course.year, + semester: course.semester, + announcementsFeedUrl: course.announcementsFeedUrl, + color: utils.generateHexCode(), + }, + }); + } } export async function refreshCourses( From 467320475c225eb122464df3e1d5e2a27170794f Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 19 Sep 2022 21:16:27 +0100 Subject: [PATCH 098/101] Bump version to v2.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca44387..8dd7fa8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.7.0", + "version": "2.7.1", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord", From 0597e849d03140ef36eb295db4cc98397556d6d1 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 3 Oct 2022 13:21:53 +0100 Subject: [PATCH 099/101] Fix course RSS link not being imported (#98) --- src/modules/courses.ts | 29 +++++++++++++++++++++++++---- src/modules/degrees.ts | 15 +++++++++++++-- src/modules/fenix.ts | 2 +- src/modules/rss.ts | 1 + 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/modules/courses.ts b/src/modules/courses.ts index d71ca12..ce7f832 100644 --- a/src/modules/courses.ts +++ b/src/modules/courses.ts @@ -493,9 +493,18 @@ export async function importCoursesFromDegree( ); if (force) { - await prisma.degreeCourse.deleteMany({ - where: { degreeFenixId: degreeId }, + const idsToKeep = degreeCourses.map( + (course) => `${degreeId}-${course.acronym}` + ); + const deleteResult = await prisma.degreeCourse.deleteMany({ + where: { degreeFenixId: degreeId, id: { notIn: idsToKeep } }, }); + + logger.info( + { degreeId }, + "Deleted %d orphan courses for degree", + deleteResult.count + ); } for (const course of degreeCourses) { @@ -515,8 +524,10 @@ export async function importCoursesFromDegree( }); } - await prisma.degreeCourse.create({ - data: { + logger.debug({ course, degreeId }, "Upserting degree course"); + + await prisma.degreeCourse.upsert({ + create: { id: `${degreeId}-${course.acronym}`, degreeFenixId: degreeId, courseAcronym: course.acronym, @@ -525,6 +536,16 @@ export async function importCoursesFromDegree( announcementsFeedUrl: course.announcementsFeedUrl, color: utils.generateHexCode(), }, + update: { + degreeFenixId: degreeId, + courseAcronym: course.acronym, + year: course.year, + semester: course.semester, + announcementsFeedUrl: course.announcementsFeedUrl, + }, + where: { + id: `${degreeId}-${course.acronym}`, + }, }); } } diff --git a/src/modules/degrees.ts b/src/modules/degrees.ts index 7a2c7cd..f756001 100644 --- a/src/modules/degrees.ts +++ b/src/modules/degrees.ts @@ -205,6 +205,14 @@ export function provideCommands(): CommandDescriptor[] { .setDescription("The acronym of the degree to refresh") .setRequired(true) ) + .addBooleanOption( + new Builders.SlashCommandBooleanOption() + .setName("delete-orphans") + .setDescription( + "Delete courses that no longer belong to this degree" + ) + .setRequired(false) + ) ); return [{ builder: cmd, handler: handleCommand }]; } @@ -702,6 +710,9 @@ export async function handleCommand( case "refresh-courses": { try { const acronym = interaction.options.getString("acronym", true); + const deleteOrphans = + interaction.options.getBoolean("delete-orphans", false) ?? + true; const degree = await prisma.degree.findUnique({ where: { acronym }, @@ -716,11 +727,11 @@ export async function handleCommand( await courses.importCoursesFromDegree( prisma, degree.fenixId, - true + deleteOrphans ); logger.info( - { acronym }, + { acronym, deleteOrphans }, "Refreshed degree's courses from Fénix" ); await interaction.editReply( diff --git a/src/modules/fenix.ts b/src/modules/fenix.ts index ccf3101..bcf918b 100644 --- a/src/modules/fenix.ts +++ b/src/modules/fenix.ts @@ -120,7 +120,7 @@ export async function getDegreeCourses( .map((_, linkNode) => { const executionCourseLink = $coursePage(linkNode) .attr("href") - ?.match(/\/disciplinas\/\w+\/([\w-]+)\/[\w-]+/); + ?.match(/\/disciplinas\/[\w-]+\/([\w-]+)\/[\w-]+/); if ( !executionCourseLink || executionCourseLink[1] !== academicYear diff --git a/src/modules/rss.ts b/src/modules/rss.ts index 4c7120b..e099320 100644 --- a/src/modules/rss.ts +++ b/src/modules/rss.ts @@ -120,6 +120,7 @@ async function sendAnnouncementMessage( authorName, content, courseName: course.course.name, + channel, }, "Sending course announcement" ); From af2c386472564ed033ce79d430be1a38f3510c45 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 3 Oct 2022 13:32:40 +0100 Subject: [PATCH 100/101] Allow refreshing courses without specifying a degree (#99) --- src/modules/degrees.ts | 59 ++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/src/modules/degrees.ts b/src/modules/degrees.ts index f756001..1ae050c 100644 --- a/src/modules/degrees.ts +++ b/src/modules/degrees.ts @@ -202,8 +202,10 @@ export function provideCommands(): CommandDescriptor[] { .addStringOption( new Builders.SlashCommandStringOption() .setName("acronym") - .setDescription("The acronym of the degree to refresh") - .setRequired(true) + .setDescription( + "The acronym of the degree to refresh. If unset, all degrees will be refreshed" + ) + .setRequired(false) ) .addBooleanOption( new Builders.SlashCommandBooleanOption() @@ -709,39 +711,52 @@ export async function handleCommand( } case "refresh-courses": { try { - const acronym = interaction.options.getString("acronym", true); + const acronym = interaction.options.getString("acronym", false); const deleteOrphans = interaction.options.getBoolean("delete-orphans", false) ?? true; - const degree = await prisma.degree.findUnique({ - where: { acronym }, - }); - if (!degree) { - await interaction.editReply( - utils.XEmoji + `Degree \`${acronym}\` not found!` - ); - return; + let degrees = []; + + if (acronym == null) { + degrees = await prisma.degree.findMany(); + } else { + const degree = await prisma.degree.findUnique({ + where: { acronym }, + }); + if (!degree) { + await interaction.editReply( + utils.XEmoji + `Degree \`${acronym}\` not found!` + ); + return; + } + + degrees = [degree]; } - await courses.importCoursesFromDegree( - prisma, - degree.fenixId, - deleteOrphans - ); + for (const degree of degrees) { + await courses.importCoursesFromDegree( + prisma, + degree.fenixId, + deleteOrphans + ); + + logger.info( + { acronym: degree.acronym, deleteOrphans }, + "Refreshed degree's courses from Fénix" + ); + } - logger.info( - { acronym, deleteOrphans }, - "Refreshed degree's courses from Fénix" - ); await interaction.editReply( utils.CheckMarkEmoji + - `Degree \`${acronym}\`'s courses have been refreshed!` + `Refreshed courses for degree(s): ${degrees + .map((degree) => `\`${degree.acronym}\``) + .join(", ")}` ); } catch (e) { logger.error( e, - "Failed to refresh degree's courses from Fénix" + "Failed to refresh degrees' courses from Fénix" ); await interaction.editReply( utils.XEmoji + "Something went wrong." From c88d7465dc28791702967086aed3a1ca700a2430 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 3 Oct 2022 13:33:14 +0100 Subject: [PATCH 101/101] Bump version to v2.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8dd7fa8..8f356c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ist-discord-bot", - "version": "2.7.1", + "version": "2.7.2", "description": "Discord bot to manage the IST Hub server.", "keywords": [ "discord",