From 00313325be0b49b24c62fee0d4b0cc4a02bb355a Mon Sep 17 00:00:00 2001 From: daniel_sp <danielsp356@gmail.com> Date: Tue, 7 Mar 2023 15:37:10 +0000 Subject: [PATCH 1/2] add api connection & refactor belts module --- .env.sample | 2 + :w | 186 +++++++++++++++++++++++++++++++++ bot/client.py | 1 - bot/cogs/belts.py | 182 +++++++++----------------------- bot/cogs/utils/constants.py | 10 ++ bot/cogs/utils/file_handler.py | 40 +++++++ bot/cogs/utils/ninja.py | 37 +++++++ bot/data/belts.json | 24 +++-- bot/settings.py | 2 + bot/web.py | 19 ++++ 10 files changed, 364 insertions(+), 139 deletions(-) create mode 100644 :w create mode 100644 bot/cogs/utils/file_handler.py create mode 100644 bot/cogs/utils/ninja.py create mode 100644 bot/web.py diff --git a/.env.sample b/.env.sample index e5d5f00..84d36a5 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,5 @@ BOT_TOKEN='<Your Token Here>' GUILD_ID='<Your Guild ID Here>' GONGO_CHANNEL_ID='<The Channel For The Daily Reports Here>' +BOKKEN_JWT='<Jason Web Token provided by the Bokken API here>' +API_URL='<API url to make the requests from>' diff --git a/:w b/:w new file mode 100644 index 0000000..9420930 --- /dev/null +++ b/:w @@ -0,0 +1,186 @@ +import json +import time +import asyncio + +import discord +from discord.ext import commands +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from bot.cogs.utils.constants import * +from bot.cogs.utils.logs import AttributionLogs, Base, log_attribution +from bot.web import * + +# sqlalchemy setup +engine = create_engine("sqlite:///bot/data/daily_logs.db") + +Base.metadata.bind = engine + +DBSession = sessionmaker(bind=engine) + +session = DBSession() + +class BeltsAttributions(commands.Cog): + """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 + ) + async def promove( + self, ctx: discord.ext.commands.Context, user: str, belt: str + ) -> None: + """This function promotes a user to the next belt.""" + + mentions = ctx.message.raw_mentions + guild = ctx.guild + member = guild.get_member(mentions[0]) + ninja = Ninja(guild, member) + + if belt == "Branco" and ninja.current_belt() == None: + role = get_role_from_name(guild, belt) + + await member.add_roles(guild.get_role(role.id), reason=None, atomic=True) + + # send request to update ninja belt + ninja_username = member.name + '#' + member.discriminator + await update_belt(ninja_username, belt) + + # Public message + asyncio.create_task(ctx.send(f"{user} agora és cinturão {belt} :tada:")) + + # Private message + asyncio.create_task(self.send_private_message(member, belt)) + + # Adding the log to the database + asyncio.create_task(self.log(ctx, member, belt)) + + 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) + + # send request to update ninja belt + ninja_username = member.name + '#' + member.discriminator + await update_belt(ninja_username, belt) + + # Public message + await ctx.send(f"{user} agora és cinturão {belt} :tada:") + + # Private message + asyncio.create_task(self.send_private_message(member, belt)) + + # Adding the log to the database + asyncio.create_task(self.log(ctx, member, belt)) + + elif belt != ninja.next_belt().name: + 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) + +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 + + print(highest_belt) + return highest_belt + + def next_belt(self) -> Belts: + """This function returns the next belt of the ninja.""" + + if self.current_belt() == None: + value = 0 + else if self.current_belt().value < 8: + value = self.current_belt().value + 1 + + else: + value = 8 + + return Belts(value) + + +def setup(client: commands.Bot) -> None: + client.add_cog(BeltsAttributions(client)) diff --git a/bot/client.py b/bot/client.py index 4ced661..f47ebca 100644 --- a/bot/client.py +++ b/bot/client.py @@ -1,5 +1,4 @@ import logging -import os import discord from discord.ext import commands diff --git a/bot/cogs/belts.py b/bot/cogs/belts.py index e7531c5..7b31f70 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.send( + 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..6915a04 100644 --- a/bot/data/belts.json +++ b/bot/data/belts.json @@ -6,14 +6,16 @@ "- Saber o nome de 5 ninjas e 2 mentores", "- Completar o Lightbot" ], - "color": "e9e9e9" + "color": "e9e9e9", + "translation": "white" }, "Amarelo": { "goals": [ "- Estar presente em 4 sessões", "- Fazer um projeto e apresentar à mesa" ], - "color": "fcd767" + "color": "fcd767", + "translation": "yellow" }, "Azul": { "goals": [ @@ -22,7 +24,8 @@ "- Conseguir completar o código que os mentores têm preparado para ti", "- Fazer um site" ], - "color": "7f9acd" + "color": "7f9acd", + "translation": "blue" }, "Verde": { "goals": [ @@ -31,7 +34,8 @@ "- Missão secreta n.º2", "- Projeto em raspberry c/ apresentação" ], - "color": "09a777" + "color": "09a777", + "translation": "green" }, "Laranja": { "goals": [ @@ -40,7 +44,8 @@ "- Introduzir conceitos de bash", "- Missão secreta n.º4 (difícil)" ], - "color": "f79520" + "color": "f79520", + "translation": "orange" }, "Vermelho": { "goals": [ @@ -48,7 +53,8 @@ "- Demonstração de conhecimento do paradigma POO", "- Ter um projeto que use API externa" ], - "color": "ec2027" + "color": "ec2027", + "translation": "red" }, "Roxo": { "goals": [ @@ -59,12 +65,14 @@ "- Criar um Bot com uma API externa", "- Última missão secreta" ], - "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..3a9b7db --- /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 From 29b18e9dd1c82ddde0b7d497e56b1f9d8d83104f Mon Sep 17 00:00:00 2001 From: daniel_sp <danielsp356@gmail.com> Date: Fri, 10 Mar 2023 21:56:34 +0000 Subject: [PATCH 2/2] add belts requirements --- bot/data/belts.json | 54 ++++++++++++++++++++++---------------------- bot/web.py | 2 +- requirements-dev.txt | 2 +- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/bot/data/belts.json b/bot/data/belts.json index 6915a04..c6be300 100644 --- a/bot/data/belts.json +++ b/bot/data/belts.json @@ -1,69 +1,69 @@ { "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", "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", "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", "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", "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", "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", "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", "translation": "purple" diff --git a/bot/web.py b/bot/web.py index 3a9b7db..337aa4d 100644 --- a/bot/web.py +++ b/bot/web.py @@ -14,6 +14,6 @@ async def update_belt(ninja, belt): # Replace '#' with '%23' ninja = ninja.replace("#", "%23") - url = API_URL + "api/bot/ninja/" + str(ninja) + url = API_URL + "/api/bot/ninja/" + str(ninja) async with session.patch(url, json=data, headers=headers) as resp: return resp.status diff --git a/requirements-dev.txt b/requirements-dev.txt index 16c5017..6ffb8ac 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,6 +3,6 @@ ipython==8.4.0 isort==5.10.1 mypy==0.971 mypy-extensions==0.4.3 -pre-commit==3.1.1 +pre-commit==2.20.0 pylint==2.14.5 python-dotenv==0.20.0