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