diff --git a/.env.sample b/.env.sample index e5d5f00..84d36a5 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,5 @@ BOT_TOKEN='' GUILD_ID='' GONGO_CHANNEL_ID='' +BOKKEN_JWT='' +API_URL='' diff --git a/bot/client.py b/bot/client.py index 4ced661..8b87005 100644 --- a/bot/client.py +++ b/bot/client.py @@ -1,5 +1,5 @@ import logging -import os +import sys import discord from discord.ext import commands @@ -14,7 +14,7 @@ case_insensitive=True, ) -# Logging setup +# Logging to file logger = logging.getLogger("discord") logger.setLevel(logging.INFO) handler = logging.FileHandler( @@ -25,14 +25,21 @@ ) logger.addHandler(handler) +# Logging to stdout +stdout_handler = logging.StreamHandler(sys.stdout) +stdout_handler.setFormatter( + logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s") +) +logger.addHandler(stdout_handler) + @client.event async def on_ready(): """Client event that run when the program is ready.""" - logger.info("The bot was logged in") + logger.info("The bot was logged in ✨") DailyReport(client).report.start() - logger.info("The task has been loaded") + logger.info("The bot is ready 🚀") @client.command() diff --git a/bot/cogs/belts.py b/bot/cogs/belts.py index e7531c5..8609591 100644 --- a/bot/cogs/belts.py +++ b/bot/cogs/belts.py @@ -1,7 +1,5 @@ -import json +import asyncio import time -from datetime import date -from enum import Enum, unique import discord from discord.ext import commands @@ -9,7 +7,10 @@ from sqlalchemy.orm import sessionmaker from bot.cogs.utils.constants import * +from bot.cogs.utils.file_handler import * from bot.cogs.utils.logs import AttributionLogs, Base, log_attribution +from bot.cogs.utils.ninja import * +from bot.web import * # sqlalchemy setup engine = create_engine("sqlite:///bot/data/daily_logs.db") @@ -21,84 +22,14 @@ session = DBSession() -class FileHandler: - """ - This is a class to handle a json file. - - Attributes: - file (string): The path to the json file being handled. - """ - - file = "bot/data/belts.json" - - def __init__(self: str, belt: str): - """ - The constructor for the FileHandler class. - - Parameters: - color (int): Color code to be displayed in discord embed. - """ - self.belt = belt - self.msg = self.get_info()[0] - self.color = self.get_info()[1] - - def get_info(self) -> tuple: - """ - The function to get the info from the belts.json file. - - Returns: - msg (string): Variable that contains the message of the respective belt. - color (int): Color code to be displayed in discord embed. - """ - with open(self.file) as json_file: - data = json.load(json_file) - msg = f"Subiste para {self.belt} :clap:\n\nPróximos objetivos:" - color = int(data[self.belt]["color"], 16) - for param in data[self.belt]["goals"]: - msg += "\n" + param - - return (msg, color) - - -class Ninja: - """This is a class to get information about a specific ninja.""" - - def __init__(self, guild: discord.Guild, member: discord.Member): - self.guild = guild - self.member = member - self.roles = list(member.roles) - - def current_belt(self): - """This function returns the current belt of the ninja.""" - - highest_belt = None - for role in self.roles: - for belt in Belts: - if belt.name == role.name: - highest_belt = belt - - return highest_belt - - return highest_belt - - def next_belt(self) -> Belts: - """This function returns the next belt of the ninja.""" - - value = self.current_belt().value + 1 if self.current_belt().value < 8 else 8 - - return Belts(value) - - class BeltsAttributions(commands.Cog): - """This is a class to handle the attribution of belts.""" + """This is a class to handle the discord attribution of belt roles.""" def __init__(self, client: commands.Bot): self.client = client @commands.command(name="promove") - @commands.has_any_role( - Roles["ADMIN"].name, Roles["CHAMPION"].name, Roles["MENTOR"].name - ) + @commands.has_any_role(Roles["ADMIN"].name, Roles["CHAMPION"].name) async def promove( self, ctx: discord.ext.commands.Context, user: str, belt: str ) -> None: @@ -109,73 +40,64 @@ async def promove( member = guild.get_member(mentions[0]) ninja = Ninja(guild, member) - if belt == "Branco" and ninja.current_belt() == None: + if belt == ninja.next_belt().name: role = get_role_from_name(guild, belt) - await member.add_roles(guild.get_role(role.id), reason=None, atomic=True) - - # Public message - await ctx.send(f"{user} agora és cinturão {belt} :tada:") + # send request to update ninja belt + ninja_username = member.name + "#" + member.discriminator + status = await update_belt(ninja_username, belt) - # Private message - file_handler = FileHandler(belt) - emoji = translator_to_emoji[belt] - user = member - embed = discord.Embed( - title=f"{emoji} Parabéns, subiste de cinturão :tada:", - description=file_handler.msg, - color=file_handler.color, - ) + if status == 200: + await member.add_roles( + guild.get_role(role.id), reason=None, atomic=True + ) - await user.send(embed=embed) + # Public message + asyncio.create_task(ctx.send(f"{user} agora és cinturão {belt} :tada:")) - # Adding the log to the database - new_log = AttributionLogs( - ninja_id=str(member), - mentor_id=str(ctx.author), - belt_attributed=belt, - timestamp=int(time.time()), - ) + # Private message + asyncio.create_task(self.send_private_message(member, belt)) - session.add(new_log) - session.commit() + # Adding the log to the database + asyncio.create_task(self.log(ctx, member, belt)) + else: + await ctx.reply( + f"Ocorreu um erro ao atualizar o cinturão do ninja {user} no site :(\nPor favor tente mais tarde." + ) elif belt == ninja.current_belt().name: await ctx.reply(f"Esse já é o cinturão do ninja {user}!") - elif belt == ninja.next_belt().name: - role = get_role_from_name(guild, belt) - await member.add_roles(guild.get_role(role.id), reason=None, atomic=True) - - # Public message - await ctx.send(f"{user} agora és cinturão {belt} :tada:") - - # Private message - file_handler = FileHandler(belt) - emoji = translator_to_emoji[belt] - user = member - embed = discord.Embed( - title=f"{emoji} Parabéns, subiste de cinturão :tada:", - description=file_handler.msg, - color=file_handler.color, - ) - - await user.send(embed=embed) - - # Adding the log to the database - new_log = AttributionLogs( - ninja_id=str(member), - mentor_id=str(ctx.author), - belt_attributed=belt, - timestamp=int(time.time()), - ) - - session.add(new_log) - session.commit() - - elif belt != ninja.next_belt().name: + else: await ctx.send(f"{user} esse cinturão não é valido de se ser atribuido.") + async def log(self, ctx, member, belt): + """This function logs the belt attribution.""" + + new_log = AttributionLogs( + ninja_id=str(member), + mentor_id=str(ctx.author), + belt_attributed=belt, + timestamp=int(time.time()), + ) + + session.add(new_log) + session.commit() + + async def send_private_message(self, member, belt): + """This function sends a private message to the member.""" + + file_handler = FileHandler(belt) + emoji = translator_to_emoji[belt] + user = member + embed = discord.Embed( + title=f"{emoji} Parabéns, subiste de cinturão :tada:", + description=file_handler.msg, + color=file_handler.color, + ) + + await user.send(embed=embed) + def setup(client: commands.Bot) -> None: client.add_cog(BeltsAttributions(client)) diff --git a/bot/cogs/utils/constants.py b/bot/cogs/utils/constants.py index 4f18ded..6eb45b6 100644 --- a/bot/cogs/utils/constants.py +++ b/bot/cogs/utils/constants.py @@ -1,3 +1,4 @@ +import json from enum import Enum, unique import discord @@ -54,3 +55,12 @@ def get_role_from_name(guild: discord.Guild, belt: str) -> discord.Role: for role in guild.roles: if role.name == belt: return role + + +def translate_belt_name(belt: str) -> str: + file = "bot/data/belts.json" + with open(file) as json_file: + data = json.load(json_file) + belt = data[belt]["translation"] + + return belt diff --git a/bot/cogs/utils/file_handler.py b/bot/cogs/utils/file_handler.py new file mode 100644 index 0000000..d02b516 --- /dev/null +++ b/bot/cogs/utils/file_handler.py @@ -0,0 +1,40 @@ +import json + + +class FileHandler: + """ + This is a class to handle a json file. + + Attributes: + file (string): The path to the json file being handled. + """ + + file = "bot/data/belts.json" + + def __init__(self: str, belt: str): + """ + The constructor for the FileHandler class. + + Parameters: + color (int): Color code to be displayed in discord embed. + """ + self.belt = belt + self.msg = self.get_info()[0] + self.color = self.get_info()[1] + + def get_info(self) -> tuple: + """ + The function to get the info from the belts.json file. + + Returns: + msg (string): Variable that contains the message of the respective belt. + color (int): Color code to be displayed in discord embed. + """ + with open(self.file) as json_file: + data = json.load(json_file) + msg = f"Subiste para {self.belt} :clap:\n\nPróximos objetivos:" + color = int(data[self.belt]["color"], 16) + for param in data[self.belt]["goals"]: + msg += "\n" + param + + return (msg, color) diff --git a/bot/cogs/utils/ninja.py b/bot/cogs/utils/ninja.py new file mode 100644 index 0000000..d5ac238 --- /dev/null +++ b/bot/cogs/utils/ninja.py @@ -0,0 +1,37 @@ +import discord + +from bot.cogs.utils.constants import * +from bot.cogs.utils.file_handler import * + + +class Ninja: + """This is a class to get information about a specific ninja.""" + + def __init__(self, guild: discord.Guild, member: discord.Member): + self.guild = guild + self.member = member + self.roles = list(member.roles) + + def current_belt(self): + """This function returns the current belt of the ninja.""" + + highest_belt = None + for role in self.roles: + for belt in Belts: + if belt.name == role.name: + highest_belt = belt + + return highest_belt + + def next_belt(self) -> Belts: + """This function returns the next belt of the ninja.""" + + if self.current_belt() == None: + value = 1 + elif self.current_belt().value < 8: + value = self.current_belt().value + 1 + + else: + value = 8 + + return Belts(value) diff --git a/bot/data/belts.json b/bot/data/belts.json index 74e0ae4..c6be300 100644 --- a/bot/data/belts.json +++ b/bot/data/belts.json @@ -1,70 +1,78 @@ { "Branco": { "goals": [ - "- Finalizar um projeto sozinho", - "- Apresentar o teu projeto a outros ninjas", - "- Saber o nome de 5 ninjas e 2 mentores", - "- Completar o Lightbot" + "- Ter maior autonomia com os conceitos básicos", + "- Saber aplicar condicionais e ciclos", + "- Ter concluído pelo menos um projeto em Scratch" ], - "color": "e9e9e9" + "color": "e9e9e9", + "translation": "white" }, "Amarelo": { "goals": [ - "- Estar presente em 4 sessões", - "- Fazer um projeto e apresentar à mesa" + "- Saber manipular variáveis da forma correta", + "- Saber aplicar o conceito de módulos", + "- Completar todos os níveis do Lightbot", + "- Terminar outros 2 projetos em Scratch", + "- Terminar pelo menos 1 projeto usando o conceito de módulos" ], - "color": "fcd767" + "color": "fcd767", + "translation": "yellow" }, "Azul": { "goals": [ - "- Ajudar como mentor numa sessão", - "- Saber o nome de 7 ninjas e 4 mentores", - "- Conseguir completar o código que os mentores têm preparado para ti", - "- Fazer um site" + "- Aplicar o que aprendeu no Scratch noutra linguagem", + "- Compreender os conceitos e as aplicações de listas e strings", + "- Terminar 2 projetos na nova linguagem (1 deles pode ser uma adaptação de algum já feito em Scratch, se o Ninja quiser)" ], - "color": "7f9acd" + "color": "7f9acd", + "translation": "blue" }, "Verde": { "goals": [ - "- Apresentar um projeto para o Dojo inteiro", - "- Chegar a 5 Kyu em CodeWars", - "- Missão secreta n.º2", - "- Projeto em raspberry c/ apresentação" + "- Aprender a criar interfaces", + "- Utilizar módulos ou bibliotecas extra como Pygame, GUIZero, etc", + "- Terminar 1 projeto usando interface e 1 adaptação de um projeto feito em Scratch", + "- Criar um website simples, recorrendo a HTML (não necessita de CSS nem JavaScript)" ], - "color": "09a777" + "color": "09a777", + "translation": "green" }, "Laranja": { "goals": [ - "- Projeto de Fuler", - "- Com a ajuda de um mentor, montar um computador", - "- Introduzir conceitos de bash", - "- Missão secreta n.º4 (difícil)" + "- Aprender a usar CSS para alterar a estrutura e a estética de uma página HTML, com recurso a layouts flexbox e/ou grid e a animações", + "- Aprender JavaScript para adicionar interatividade (botões, avisos, etc) a uma página HTML", + "- Conhecer os utilitários básicos de linha de comandos (cd, mkdir, ls/dir, rm, cat, etc)", + "- Conhecer os comandos básicos de Git (clone, add, commit, push, pull, etc), em terminal ou em interface gráfica" ], - "color": "f79520" + "color": "f79520", + "translation": "orange" }, "Vermelho": { "goals": [ - "- Ser mentor 1 sessão", - "- Demonstração de conhecimento do paradigma POO", - "- Ter um projeto que use API externa" + "- Aprender como aplicar Regex", + "- Aprender como criar Bases de Dados (usando SQLite, por exemplo)", + "- Aprender os conceitos básicos de programação com threads e exclusão mútua (locks)", + "- Terminar 2 projetos com estes conceitos" ], - "color": "ec2027" + "color": "ec2027", + "translation": "red" }, "Roxo": { "goals": [ - "- Conceitos básicos g7", - "- Criar uma missão secreta", - "- Ser mentor 2 sessões", - "- Ter conta e saber usar o Slack", - "- Criar um Bot com uma API externa", - "- Última missão secreta" + "- Dominar APIs, incluindo utilizar APIs externas e como aplicá-las", + "- Criar uma API REST", + "- Aprender a usar Docker para distribuição de aplicações em containers", + "- Fazer um projeto com Raspberry Pi" ], - "color": "6f5977" + "color": "6f5977", + "translation": "purple" }, "Preto": { "goals": [ "Não tens, és um guru da programação!" ], - "color": "383b3f" + "color": "383b3f", + "translation": "black" } } diff --git a/bot/settings.py b/bot/settings.py index bb02407..23a3ce1 100644 --- a/bot/settings.py +++ b/bot/settings.py @@ -7,3 +7,5 @@ BOT_TOKEN = os.environ["BOT_TOKEN"] GUILD_ID = os.environ["GUILD_ID"] GONGO_CHANNEL_ID = os.environ["GONGO_CHANNEL_ID"] +BOKKEN_JWT = os.environ["BOKKEN_JWT"] +API_URL = os.environ["API_URL"] diff --git a/bot/web.py b/bot/web.py new file mode 100644 index 0000000..337aa4d --- /dev/null +++ b/bot/web.py @@ -0,0 +1,19 @@ +import asyncio + +import aiohttp + +from bot.cogs.utils.constants import translate_belt_name +from bot.settings import API_URL, BOKKEN_JWT + + +async def update_belt(ninja, belt): + async with aiohttp.ClientSession() as session: + belt = translate_belt_name(belt) + headers = {"Authorization": "Bearer " + BOKKEN_JWT} + data = {"ninja": {"belt": belt}} + + # Replace '#' with '%23' + ninja = ninja.replace("#", "%23") + url = API_URL + "/api/bot/ninja/" + str(ninja) + async with session.patch(url, json=data, headers=headers) as resp: + return resp.status