From de76bb9d2c1b18e33a2363fab5ac5472d135f84f Mon Sep 17 00:00:00 2001
From: Ethan <47520067+Skelmis@users.noreply.github.com>
Date: Wed, 11 Oct 2023 23:05:50 +1300
Subject: [PATCH] Feature branch: Version 3.18 (#57)

* chore: bump version

* Add the pt-BR locale (#54)

* Create pt_BR.json

* Update pt_BR.json

* Update pt_BR.json

* Update pt_BR.json

* Update pt_BR.json

* Update pt_BR.json

* Create python-app.yml

* Update python-app.yml

* Delete python-app.yml

* Clarifications and updates

* Fixing some mistakes

* feat: add some recognition to translation providers

* feat: add block listing for suggestions

* fix: tests

* feat: add en_US locale (#62)

* fix: update strings

Copied file/strings from master, not the current working branch for 3.18

---------

Co-authored-by: Davi <68123395+Davi-the-Mudkip@users.noreply.github.com>
Co-authored-by: Anthony Collier <gamemaniac101@gmail.com>
Co-authored-by: Anthony Collier <hello@acollier.dev>
---
 suggestions/bot.py                  |  14 ++-
 suggestions/checks.py               |  17 +++-
 suggestions/codes.py                |   2 +
 suggestions/cogs/blacklist_cog.py   | 140 ++++++++++++++++++++++++++++
 suggestions/cogs/suggestion_cog.py  |   1 +
 suggestions/exceptions.py           |   4 +
 suggestions/locales/en_GB.json      |  10 +-
 suggestions/locales/en_US.json      | 125 +++++++++++++++++++++++++
 suggestions/locales/pt_BR.json      | 116 +++++++++++++++++++++++
 suggestions/main.py                 |  13 +++
 suggestions/objects/guild_config.py |   6 ++
 tests/test_bot.py                   |   1 +
 12 files changed, 446 insertions(+), 3 deletions(-)
 create mode 100644 suggestions/cogs/blacklist_cog.py
 create mode 100644 suggestions/locales/en_US.json
 create mode 100644 suggestions/locales/pt_BR.json

diff --git a/suggestions/bot.py b/suggestions/bot.py
index dbd3a6e..58f439a 100644
--- a/suggestions/bot.py
+++ b/suggestions/bot.py
@@ -33,6 +33,7 @@
     ConfiguredChannelNoLongerExists,
     UnhandledError,
     QueueImbalance,
+    BlocklistedUser,
 )
 from suggestions.http_error_parser import try_parse_http_error
 from suggestions.objects import Error, GuildConfig, UserConfig
@@ -45,7 +46,7 @@
 
 class SuggestionsBot(commands.AutoShardedInteractionBot, BotBase):
     def __init__(self, *args, **kwargs):
-        self.version: str = "Public Release 3.17"
+        self.version: str = "Public Release 3.18"
         self.main_guild_id: int = 601219766258106399
         self.legacy_beta_role_id: int = 995588041991274547
         self.automated_beta_role_id: int = 998173237282361425
@@ -414,6 +415,17 @@ async def on_slash_command_error(
                 ephemeral=True,
             )
 
+        elif isinstance(exception, BlocklistedUser):
+            return await interaction.send(
+                embed=self.error_embed(
+                    "Blocked Action",
+                    "Administrators from this guild have removed your ability to run this action.",
+                    error_code=ErrorCode.BLOCKLISTED_USER,
+                    error=error,
+                ),
+                ephemeral=True,
+            )
+
         elif isinstance(exception, ConfiguredChannelNoLongerExists):
             return await interaction.send(
                 embed=self.error_embed(
diff --git a/suggestions/checks.py b/suggestions/checks.py
index 33ad60a..944300d 100644
--- a/suggestions/checks.py
+++ b/suggestions/checks.py
@@ -8,9 +8,9 @@
 from disnake.ext import commands
 
 from suggestions.exceptions import (
-    BetaOnly,
     MissingSuggestionsChannel,
     MissingLogsChannel,
+    BlocklistedUser,
 )
 
 if TYPE_CHECKING:
@@ -62,3 +62,18 @@ async def check(interaction: disnake.Interaction):
         return True
 
     return commands.check(check)  # type: ignore
+
+
+def ensure_user_is_not_blocklisted():
+    async def check(interaction: disnake.Interaction):
+        guild_config: Optional[GuildConfig] = await fetch_guild_config(interaction)
+
+        if not bool(guild_config):
+            return True
+
+        if interaction.author.id in guild_config.blocked_users:
+            raise BlocklistedUser
+
+        return True
+
+    return commands.check(check)  # type: ignore
diff --git a/suggestions/codes.py b/suggestions/codes.py
index fcc4555..ef7e0d4 100644
--- a/suggestions/codes.py
+++ b/suggestions/codes.py
@@ -24,6 +24,8 @@ class ErrorCode(IntEnum):
     MISSING_SEND_PERMISSIONS_IN_SUGGESTION_CHANNEL = 18
     MISSING_THREAD_CREATE_PERMISSIONS = 19
     QUEUE_IMBALANCE = 20
+    MISSING_QUEUE_CHANNEL = 21
+    BLOCKLISTED_USER = 22
 
     @classmethod
     def from_value(cls, value: int) -> ErrorCode:
diff --git a/suggestions/cogs/blacklist_cog.py b/suggestions/cogs/blacklist_cog.py
new file mode 100644
index 0000000..64f3f19
--- /dev/null
+++ b/suggestions/cogs/blacklist_cog.py
@@ -0,0 +1,140 @@
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING
+
+import disnake
+from bot_base import NonExistentEntry
+from disnake.ext import commands
+
+from suggestions.objects import GuildConfig, Suggestion
+
+if TYPE_CHECKING:
+    from suggestions import State, SuggestionsBot
+
+log = logging.getLogger(__name__)
+
+
+class BlacklistCog(commands.Cog):
+    def __init__(self, bot):
+        self.bot: SuggestionsBot = bot
+        self.state: State = self.bot.state
+
+    @commands.slash_command()
+    async def user(self, interaction: disnake.GuildCommandInteraction):
+        ...
+
+    @user.sub_command_group()
+    async def blocklist(self, interaction: disnake.GuildCommandInteraction):
+        ...
+
+    @blocklist.sub_command()
+    async def add(
+        self,
+        interaction: disnake.GuildCommandInteraction,
+        suggestion_id: str = commands.Param(),
+    ):
+        """
+        {{USER_BLOCKLIST_ADD}}
+
+        Parameters
+        ----------
+        suggestion_id: str {{SUGGESTION_ID}}
+        """
+        await interaction.response.defer(ephemeral=True)
+        suggestion: Suggestion = await Suggestion.from_id(
+            suggestion_id, interaction.guild_id, self.state
+        )
+        guild_config: GuildConfig = await GuildConfig.from_id(
+            interaction.guild_id, self.state
+        )
+        if suggestion.suggestion_author_id in guild_config.blocked_users:
+            return await interaction.send(
+                "This user is already blocked from creating new suggestions.",
+                ephemeral=True,
+            )
+
+        guild_config.blocked_users.add(suggestion.suggestion_author_id)
+        await self.bot.db.guild_configs.upsert(guild_config, guild_config)
+        await interaction.send(
+            "I have added that user to the blocklist. "
+            "They will be unable to create suggestions in the future.",
+            ephemeral=True,
+        )
+
+    @blocklist.sub_command()
+    async def remove(
+        self,
+        interaction: disnake.GuildCommandInteraction,
+        suggestion_id: str = commands.Param(default=None),
+        user_id: str = commands.Param(default=None),
+    ):
+        """
+        {{USER_BLOCKLIST_REMOVE}}
+
+        Parameters
+        ----------
+        suggestion_id: str {{SUGGESTION_ID}}
+        user_id: str {{USER_ID}}
+        """
+        await interaction.response.defer(ephemeral=True)
+        if suggestion_id and user_id:
+            return await interaction.send(
+                "Providing suggestion_id and user_id at the same time is not supported.",
+                ephemeral=True,
+            )
+
+        if suggestion_id:
+            suggestion: Suggestion = await Suggestion.from_id(
+                suggestion_id, interaction.guild_id, self.state
+            )
+            user_id = suggestion.suggestion_author_id
+
+        if user_id:
+            try:
+                user_id = int(user_id)
+            except ValueError:
+                return await interaction.send("User id is not valid.", ephemeral=True)
+
+        guild_config: GuildConfig = await GuildConfig.from_id(
+            interaction.guild_id, self.state
+        )
+        guild_config.blocked_users.discard(user_id)
+        await self.bot.db.guild_configs.upsert(guild_config, guild_config)
+        await interaction.send("I have un-blocklisted that user for you.")
+
+    @add.autocomplete("suggestion_id")
+    @remove.autocomplete("suggestion_id")
+    async def get_sid_for(
+        self,
+        interaction: disnake.ApplicationCommandInteraction,
+        user_input: str,
+    ):
+        try:
+            values: list[str] = self.state.autocomplete_cache.get_entry(
+                interaction.guild_id
+            )
+        except NonExistentEntry:
+            values: list[str] = await self.state.populate_sid_cache(
+                interaction.guild_id
+            )
+        else:
+            if not values:
+                log.debug(
+                    "Values was found, but empty in guild %s thus populating",
+                    interaction.guild_id,
+                )
+                values: list[str] = await self.state.populate_sid_cache(
+                    interaction.guild_id
+                )
+
+        possible_choices = [v for v in values if user_input.lower() in v.lower()]
+
+        if len(possible_choices) > 25:
+            return []
+
+        return possible_choices
+
+
+def setup(bot):
+    bot.add_cog(BlacklistCog(bot))
diff --git a/suggestions/cogs/suggestion_cog.py b/suggestions/cogs/suggestion_cog.py
index 3085fe8..5bef9f0 100644
--- a/suggestions/cogs/suggestion_cog.py
+++ b/suggestions/cogs/suggestion_cog.py
@@ -153,6 +153,7 @@ async def suggestion_down_vote(
         dm_permission=False,
     )
     @cooldowns.cooldown(1, 3, bucket=InteractionBucket.author)
+    @checks.ensure_user_is_not_blocklisted()
     @checks.ensure_guild_has_suggestions_channel()
     async def suggest(
         self,
diff --git a/suggestions/exceptions.py b/suggestions/exceptions.py
index c343c70..1e78006 100644
--- a/suggestions/exceptions.py
+++ b/suggestions/exceptions.py
@@ -45,3 +45,7 @@ class UnhandledError(Exception):
 
 class QueueImbalance(disnake.DiscordException):
     """This queued suggestion has already been dealt with in another queue."""
+
+
+class BlocklistedUser(CheckFailure):
+    """This user is blocked from taking this action in this guild."""
diff --git a/suggestions/locales/en_GB.json b/suggestions/locales/en_GB.json
index d78d8e3..4d3cbc5 100644
--- a/suggestions/locales/en_GB.json
+++ b/suggestions/locales/en_GB.json
@@ -113,5 +113,13 @@
   "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_MESSAGE": "This guild {} have images in suggestions.",
   "CONFIG_SUGGESTIONS_IMAGES_ENABLE_INNER_MESSAGE": "All new suggestions can include images.",
   "CONFIG_SUGGESTIONS_IMAGES_DISABLE_INNER_MESSAGE": "All new suggestions cannot include images.",
-  "VIEW_VOTERS_INNER_EMBED_TITLE": "Viewing voters"
+  "VIEW_VOTERS_INNER_EMBED_TITLE": "Viewing voters",
+  "USER_BLOCKLIST_ADD_NAME": "add",
+  "USER_BLOCKLIST_ADD_DESCRIPTION": "Remove a users ability to create suggestions.",
+  "USER_BLOCKLIST_REMOVE_NAME": "remove",
+  "USER_BLOCKLIST_REMOVE_DESCRIPTION": "Re-add a users ability to create suggestions.",
+  "SUGGESTION_ID_NAME": "suggestion_id",
+  "SUGGESTION_ID_DESCRIPTION": "The suggestions ID you wish to reference.",
+  "USER_ID_NAME": "user_id",
+  "USER_ID_DESCRIPTION": "The users discord id."
 }
diff --git a/suggestions/locales/en_US.json b/suggestions/locales/en_US.json
new file mode 100644
index 0000000..4d3cbc5
--- /dev/null
+++ b/suggestions/locales/en_US.json
@@ -0,0 +1,125 @@
+{
+  "SUGGEST_NAME": "suggest",
+  "SUGGEST_DESCRIPTION": "Create a new suggestion.",
+  "SUGGEST_ARG_SUGGESTION_NAME": "suggestion",
+  "SUGGEST_ARG_SUGGESTION_DESCRIPTION": "Your suggestion.",
+  "SUGGEST_ARG_IMAGE_NAME": "image",
+  "SUGGEST_ARG_IMAGE_DESCRIPTION": "An image to add to your suggestion. Images currently expire after a couple weeks.",
+  "SUGGEST_ARG_ANONYMOUSLY_NAME": "anonymously",
+  "SUGGEST_ARG_ANONYMOUSLY_DESCRIPTION": "Submit your suggestion anonymously.",
+  "APPROVE_NAME": "approve",
+  "APPROVE_DESCRIPTION": "Approve a suggestion",
+  "APPROVE_ARG_SUGGESTION_ID_NAME": "suggestion_id",
+  "APPROVE_ARG_SUGGESTION_ID_DESCRIPTION": "The sID you wish to approve",
+  "APPROVE_ARG_RESPONSE_NAME": "response",
+  "APPROVE_ARG_RESPONSE_DESCRIPTION": "An optional response to add to the suggestion",
+  "REJECT_NAME": "reject",
+  "REJECT_DESCRIPTION": "Reject a suggestion",
+  "REJECT_ARG_SUGGESTION_ID_NAME": "suggestion_id",
+  "REJECT_ARG_SUGGESTION_ID_DESCRIPTION": "The sID you wish to reject",
+  "REJECT_ARG_RESPONSE_NAME": "response",
+  "REJECT_ARG_RESPONSE_DESCRIPTION": "An optional response to add to the suggestion",
+  "CLEAR_NAME": "clear",
+  "CLEAR_DESCRIPTION": "Remove a suggestion and any associated messages.",
+  "CLEAR_ARG_SUGGESTION_ID_NAME": "suggestion_id",
+  "CLEAR_ARG_SUGGESTION_ID_DESCRIPTION": "The sID you wish to reject",
+  "CLEAR_ARG_RESPONSE_NAME": "response",
+  "CLEAR_ARG_RESPONSE_DESCRIPTION": "An optional response to add to the suggestion",
+  "STATS_NAME": "stats",
+  "STATS_DESCRIPTION": "Get bot stats!",
+  "INFO_NAME": "info",
+  "INFO_DESCRIPTION": "View bot information.",
+  "INFO_ARG_SUPPORT_NAME": "support",
+  "INFO_ARG_SUPPORT_DESCRIPTION": "Set this to receive info relevant to receiving official support.",
+  "PING_NAME": "ping",
+  "PING_DESCRIPTION": "Pong!",
+  "SUGGESTION_UP_VOTE_INNER_NO_MORE_CASTING": "You can no longer cast votes on this suggestion.",
+  "SUGGESTION_UP_VOTE_INNER_ALREADY_VOTED": "You have already up voted this suggestion.",
+  "SUGGESTION_UP_VOTE_INNER_MODIFIED_VOTE": "I have changed your vote from a down vote to an up vote for this suggestion.\nThe suggestion will be updated shortly.",
+  "SUGGESTION_UP_VOTE_INNER_REGISTERED_VOTE": "Thanks!\nI have registered your up vote.",
+  "SUGGESTION_DOWN_VOTE_INNER_NO_MORE_CASTING": "You can no longer cast votes on this suggestion.",
+  "SUGGESTION_DOWN_VOTE_INNER_ALREADY_VOTED": "You have already down voted this suggestion.",
+  "SUGGESTION_DOWN_VOTE_INNER_MODIFIED_VOTE": "I have changed your vote from an up vote to a down vote for this suggestion.\nThe suggestion will be updated shortly.",
+  "SUGGESTION_DOWN_VOTE_INNER_REGISTERED_VOTE": "Thanks!\nI have registered your down vote.",
+  "VIEW_VOTERS_INNER_TITLE_PREFIX": "Voters",
+  "VIEW_UP_VOTERS_INNER_TITLE_PREFIX": "Up voters",
+  "VIEW_DOWN_VOTERS_INNER_TITLE_PREFIX": "Down voters",
+  "DISPLAY_DATA_INNER_OLD_SUGGESTION_TYPE": "Suggestions using reactions are not supported by this command.",
+  "DISPLAY_DATA_INNER_NO_VOTERS": "There are no voters to show you for this.",
+  "VOTER_PAGINATOR_INNER_EMBED_TITLE": "{} for suggestion `{}`",
+  "VOTER_PAGINATOR_INNER_EMBED_FOOTER": "Page {} of {}",
+  "APPROVE_INNER_MESSAGE": "You have approved **{}**",
+  "REJECT_INNER_MESSAGE": "You have rejected **{}**",
+  "CLEAR_INNER_MESSAGE": "I have cleared `{}` for you.",
+  "SUGGEST_INNER_SUGGESTION_SENT": "Hey, {}. Your suggestion has been sent to {} to be voted on!\n\nPlease wait until it gets approved or rejected by a staff member.\n\nYour suggestion ID (sID) for reference is **{}**.",
+  "SUGGEST_INNER_SUGGESTION_SENT_FOOTER": "Guild ID: {} | sID: {}",
+  "SUGGEST_INNER_THANKS": "Thanks for your suggestion!",
+  "SUGGEST_INNER_SENT_TO_QUEUE": "Your suggestion has been sent to the queue for processing.",
+  "SUGGEST_INNER_NO_ANONYMOUS_SUGGESTIONS": "Your guild does not allow anonymous suggestions.",
+  "SUGGEST_INNER_NO_IMAGES_IN_SUGGESTIONS": "Your guild does not allow images in suggestions.",
+  "CONFIG_ANONYMOUS_ENABLE_INNER_SUCCESS": "I have enabled anonymous suggestions for this guild.",
+  "CONFIG_ANONYMOUS_DISABLE_INNER_SUCCESS": "I have disabled anonymous suggestions for this guild.",
+  "SUGGESTION_OBJECT_LOCK_THREAD": "Locking this thread as the suggestion has reached a resolution.",
+  "CONFIG_CHANNEL_INNER_MESSAGE": "I have set this guilds suggestion channel to {}",
+  "CONFIG_LOGS_INNER_MESSAGE": "I have set this guilds log channel to {}",
+  "CONFIG_GET_INNER_BASE_EMBED_DESCRIPTION": "Configuration for {}\n",
+  "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_SET": "Log channel: <#{}>",
+  "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_NOT_SET": "Not set",
+  "CONFIG_GET_INNER_PARTIAL_SUGGESTION_CHANNEL_SET": "Suggestion channel: <#{}>",
+  "CONFIG_GET_INNER_PARTIAL_SUGGESTION_CHANNEL_NOT_SET": "Not set",
+  "CONFIG_GET_INNER_PARTIAL_DM_RESPONSES_SET": "will",
+  "CONFIG_GET_INNER_PARTIAL_DM_RESPONSES_NOT_SET": "will not",
+  "CONFIG_GET_INNER_PARTIAL_DM_RESPONSES_MESSAGE": "Dm responses: I {} DM users on actions such as suggest",
+  "CONFIG_GET_INNER_PARTIAL_THREADS_SET": "will",
+  "CONFIG_GET_INNER_PARTIAL_THREADS_NOT_SET": "will not",
+  "CONFIG_GET_INNER_PARTIAL_THREADS_MESSAGE": "I {} create threads for new suggestions",
+  "CONFIG_GET_INNER_KEEP_LOGS_SET": "Suggestion logs will be kept in your suggestions channel.",
+  "CONFIG_GET_INNER_KEEP_LOGS_NOT_SET": "Suggestion logs will be kept in your logs channel.",
+  "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_SET": "can",
+  "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_NOT_SET": "cannot",
+  "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_MESSAGE": "This guild {} have anonymous suggestions.",
+  "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_SET": "will",
+  "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_NOT_SET": "will not",
+  "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_MESSAGE": "I {} automatically archive threads created for suggestions.",
+  "CONFIG_DM_ENABLE_INNER_MESSAGE": "I have enabled DM messages for this guild.",
+  "CONFIG_DM_DISABLE_INNER_MESSAGE": "I have disabled DM messages for this guild.",
+  "CONFIG_THREAD_ENABLE_INNER_MESSAGE": "I have enabled threads on new suggestions for this guild.",
+  "CONFIG_THREAD_DISABLE_INNER_MESSAGE": "I have disabled thread creation on new suggestions for this guild.",
+  "CONFIG_KEEPLOGS_ENABLE_INNER_MESSAGE": "Suggestions will now stay in your suggestions channel instead of going to logs.",
+  "CONFIG_KEEPLOGS_DISABLE_INNER_MESSAGE": "Suggestions will now be moved to your logs channel when finished.",
+  "CONFIG_AUTO_ARCHIVE_THREADS_ENABLE_INNER_MESSAGE": "Automatically created threads for suggestions will now be archived upon suggestion resolution.",
+  "CONFIG_AUTO_ARCHIVE_THREADS_DISABLE_INNER_MESSAGE": "Automatically created threads for suggestions will no longer be archived upon suggestion resolution.",
+  "CONFIG_SUGGESTIONS_QUEUE_ENABLE_INNER_MESSAGE": "All new suggestions will be sent to your suggestions queue.",
+  "CONFIG_SUGGESTIONS_QUEUE_DISABLE_INNER_MESSAGE": "All new suggestions will be sent directly to your suggestions channel.",
+  "CONFIG_ANONYMOUS_RESOLUTION_ENABLE_INNER_MESSAGE": "All further suggestions will not show the moderator who resolved them.",
+  "CONFIG_ANONYMOUS_RESOLUTION_DISABLE_INNER_MESSAGE": "All further suggestions will show the moderator who resolved them.",
+  "CONFIG_GET_INNER_SUGGESTIONS_QUEUE_SET": "In use.",
+  "CONFIG_GET_INNER_SUGGESTIONS_QUEUE_NOT_SET": "Not in use.",
+  "CONFIG_GET_INNER_SUGGESTIONS_QUEUE_MESSAGE": "Suggestions queue is $TEXT",
+  "CONFIG_GET_INNER_ANONYMOUS_RESOLUTION_NOT_SET": "Suggesters are shown who resolved their suggestions.",
+  "CONFIG_GET_INNER_ANONYMOUS_RESOLUTION_SET": "Suggesters are not shown who resolved their suggestions.",
+  "PAGINATION_INNER_SESSION_EXPIRED": "This pagination session has expired, please start a new one with `/queue view`",
+  "PAGINATION_INNER_NEXT_ITEM": "Viewing next item in queue.",
+  "PAGINATION_INNER_PREVIOUS_ITEM": "Viewing previous item in queue.",
+  "PAGINATION_INNER_QUEUE_EXPIRED": "This queue has expired.",
+  "PAGINATION_INNER_QUEUE_CANCELLED": "I have cancelled this queue for you.",
+  "PAGINATION_INNER_QUEUE_ACCEPTED": "I have accepted that suggestion from the queue.",
+  "PAGINATION_INNER_QUEUE_REJECTED": "I have removed that suggestion from the queue.",
+  "QUEUE_VIEW_INNER_NOTHING_QUEUED": "Your guild has no suggestions in the queue.",
+  "QUEUE_VIEW_INNER_PRIOR_QUEUE": "These suggestions were queued before your guild disabled the suggestions queue.",
+  "QUEUE_INNER_USER_REJECTED": "Your queued suggestion was rejected.",
+  "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_SET": "can",
+  "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_NOT_SET": "cannot",
+  "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_MESSAGE": "This guild {} have images in suggestions.",
+  "CONFIG_SUGGESTIONS_IMAGES_ENABLE_INNER_MESSAGE": "All new suggestions can include images.",
+  "CONFIG_SUGGESTIONS_IMAGES_DISABLE_INNER_MESSAGE": "All new suggestions cannot include images.",
+  "VIEW_VOTERS_INNER_EMBED_TITLE": "Viewing voters",
+  "USER_BLOCKLIST_ADD_NAME": "add",
+  "USER_BLOCKLIST_ADD_DESCRIPTION": "Remove a users ability to create suggestions.",
+  "USER_BLOCKLIST_REMOVE_NAME": "remove",
+  "USER_BLOCKLIST_REMOVE_DESCRIPTION": "Re-add a users ability to create suggestions.",
+  "SUGGESTION_ID_NAME": "suggestion_id",
+  "SUGGESTION_ID_DESCRIPTION": "The suggestions ID you wish to reference.",
+  "USER_ID_NAME": "user_id",
+  "USER_ID_DESCRIPTION": "The users discord id."
+}
diff --git a/suggestions/locales/pt_BR.json b/suggestions/locales/pt_BR.json
new file mode 100644
index 0000000..d058a04
--- /dev/null
+++ b/suggestions/locales/pt_BR.json
@@ -0,0 +1,116 @@
+{
+  "SUGGEST_NAME": "sugerir",
+  "SUGGESTION_DESCRIPTION": "Crie uma nova sugestão.",
+  "SUGGESTION_ARG_SUGGESTION_DESCRIPTION": "Sua sugestão.",
+  "SUGGEST_ARG_IMAGE_NAME": "imagem",
+  "SUGGESTION_ARG_IMAGE_DESCRIPTION": "Uma imagem para adicionar a sua sugestão. Atualmente, as imagens expiram após algumas semanas.",
+  "SUGGEST_ARG_ANONYMOUSLY_NAME": "anonimamente",
+  "SUGGEST_ARG_ANONYMOUSLY_DESCRIPTION": "Envie sua sugestão anonimamente.",
+  "APPROVE_NAME": "aprovar",
+  "APPROVE_DESCRIPTION": "Aprove uma sugestão",
+  "APPROVE_ARG_SUGGESTION_ID_NAME": "id_da_sugestão",
+  "APPROVE_ARG_SUGGESTION_ID_DESCRIPTION": "O sID que você deseja aprovar",
+  "APPROVE_ARG_RESPONSE_NAME": "resposta",
+  "APPROVE_ARG_RESPONSE_DESCRIPTION": "Uma resposta opcional para adicionar a sua sugestão.",
+  "REJECT_NAME": "rejeitar",
+  "REJECT_DESCRIPTION": "Rejeite uma sugestão",
+  "REJECT_ARG_SUGGESTION_ID_NAME": "id_da_sugestão",
+  "REJECT_ARG_SUGGESTION_ID_DESCRIPTION": "O sID que você deseja rejeitar",
+  "REJECT_ARG_RESPONSE_NAME": "resposta",
+  "REJECT_ARG_RESPONSE_DESCRIPTION": "Uma resposta opcional para adicionar a sua sugestão.",
+  "CLEAR_NAME": "remover",
+  "CLEAR_DESCRIPTION": "Remova uma sugestão e quaisquer mensagens associadas a ela.",
+  "CLEAR_ARG_SUGGESTION_ID_NAME": "id_da_sugestão",
+  "CLEAR_ARG_SUGGESTION_ID_DESCRIPTION": "O sID que você deseja remover",
+  "CLEAR_ARG_RESPONSE_NAME": "resposta",
+  "CLEAR_ARG_RESPONSE_DESCRIPTION": "Uma resposta opcional sobre por que se removeu esta sugestão.",
+  "STATS_NAME": "estatísticas",
+  "STATS_DESCRIPTION": "Obtenha estatísticas do bot!",
+  "INFO_NAME": "info",
+  "INFO_DESCRIPTION": "Veja informações do bot.",
+  "INFO_ARG_SUPPORT_NAME": "suporte",
+  "INFO_ARG_SUPPORT_DESCRIPTION": "Configure isto para receber informação relevante a receber suporte oficial.",
+  "PING_NAME": "pingue",
+  "PING_DESCRIPTION": "Pongue!",
+  "SUGGESTION_UP_VOTE_INNER_NO_MORE_CASTING": "Você não pode mais dar votos a esta sugestão.",
+  "SUGGESTION_UP_VOTE_INNER_ALREADY_VOTED": "Você já votou positivamente nesta sugestão.",
+  "SUGGESTION_UP_VOTE_INNER_MODIFIED_VOTE": "Mudei seu voto de um negativo para um positivo.\nA sugestão será atualizada em breve.",
+  "SUGGESTION_UP_VOTE_INNER_REGISTERED_VOTE": "Obrigado!\nRegistrei seu voto positivo.",
+  "SUGGESTION_DOWN_VOTE_INNER_NO_MORE_CASTING": "Você não pode mais dar votos a esta sugestão.",
+  "SUGGESTION_DOWN_VOTE_INNER_ALREADY_VOTED": "Você já votou negativamente nesta sugestão.",
+  "SUGGESTION_DOWN_VOTE_INNER_MODIFIED_VOTE": "Mudei seu voto de um positivo para um negativo.\nA sugestão será atualizada em breve.",
+  "SUGGESTION_DOWN_VOTE_INNER_REGISTERED_VOTE": "Obrigado!\nRegistrei seu voto negativo.",
+  "VIEW_VOTERS_INNER_TITLE_PREFIX": "Votantes",
+  "VIEW_UP_VOTERS_INNER_TITLE_PREFIX": "Votantes positivos",
+  "VIEW_DOWN_VOTERS_INNER_TITLE_PREFIX": "Votantes negativos",
+  "DISPLAY_DATA_INNER_OLD_SUGGESTION_TYPE": "Sugestões usando reações não são compatíveis com este comando.",
+  "DISPLAY_DATA_INNER_NO_VOTERS": "Não há votantes desta sugestão para lhe mostrar.",
+  "VOTER_PAGINATOR_INNER_EMBED_TITLE": "{} para a sugestão `{}`",
+  "VOTER_PAGINATOR_INNER_EMBED_FOOTER": "Página {} de {}",
+  "APPROVE_INNER_MESSAGE": "Você aprovou **{}**",
+  "REJECT_INNER_MESSAGE": "Você rejeitou **{}**",
+  "CLEAR_INNER_MESSAGE": "Removi `{}` para você.",
+  "SUGGEST_INNER_SUGGESTION_SENT": "Ei, {}. Sua sugestão foi enviada para {} para ser votada!\n\nPor favor, aguarde até ela ser aprovada ou rejeitada por um membro da staff.\n\nPara referência, seu ID de sugestão (sID) é {}.",
+  "SUGGEST_INNER_SUGGESTION_SENT_FOOTER": "ID do servidor: {} | sID: {}",
+  "SUGGEST_INNER_THANKS": "Obrigado por sua sugestão!",
+  "SUGGEST_INNER_SENT_TO_QUEUE": "Enviou-se sua sugestão à fila para processamento.",
+  "SUGGEST_INNER_NO_ANONYMOUS_SUGGESTIONS": "Seu servidor não permite sugestões anônimas.",
+  "SUGGEST_INNER_NO_IMAGES_IN_SUGGESTIONS": "Seu servidor não permite imagens em sugestões.",
+  "CONFIG_ANONYMOUS_ENABLE_INNER_SUCCESS": "Habilitei sugestões anônimas para este servidor.",
+  "CONFIG_ANONYMOUS_DISABLE_INNER_SUCCESS": "Desabilitei sugestões anônimas para este servidor.",
+  "SUGGESTION_OBJECT_LOCK_THREAD": "Trancando este tópico já que a sugestão chegou a uma resolução.",
+  "CONFIG_CHANNEL_INNER_MESSAGE": "Configurei o canal de sugestões deste servidor em {}",
+  "CONFIG_LOGS_INNER_MESSAGE": "Configurei o canal de sugestões deste servidor em {}",
+  "CONFIG_GET_INNER_BASE_EMBED_DESCRIPTION": "Configuração para {}\n",
+  "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_SET": "Canal de registros: <#{}>",
+  "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_NOT_SET": "Não configurado",
+  "CONFIG_GET_INNER_PARTIAL_SUGGESTION_CHANNEL_SET": "Canal de sugestões: <#{}>",
+  "CONFIG_GET_INNER_PARTIAL_SUGGESTION_CHANNEL_NOT_SET": "Não configurado",
+  "CONFIG_GET_INNER_PARTIAL_DM_RESPONSES_SET": "Vou",
+  "CONFIG_GET_INNER_PARTIAL_DM_RESPONSES_NOT_SET": "Não vou",
+  "CONFIG_GET_INNER_PARTIAL_DM_RESPONSES_MESSAGE": "Respostas em mensagem direta: {} enviar a usuários mensagens diretas sobre ações, tais como sugestões.",
+  "CONFIG_GET_INNER_PARTIAL_THREADS_SET": "Vou",
+  "CONFIG_GET_INNER_PARTIAL_THREADS_NOT_SET": "Não vou",
+  "CONFIG_GET_INNER_PARTIAL_THREADS_MESSAGE": "{} criar tópicos para novas sugestões",
+  "CONFIG_GET_INNER_KEEP_LOGS_SET": "Registros de sugestões serão mantidos no seu canal de sugestões.",
+  "CONFIG_GET_INNER_KEEP_LOGS_NOT_SET": "Registros de sugestões serão mantidos no seu canal de registros.",
+  "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_SET": "pode",
+  "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_NOT_SET": "não pode",
+  "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_MESSAGE": "Este servidor {} ter sugestões anônimas.",
+  "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_SET": "Vou",
+  "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_NOT_SET": "Não vou",
+  "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_MESSAGE": "{} trancar automaticamente tópicos criados para sugestões.",
+  "CONFIG_DM_ENABLE_INNER_MESSAGE": "Habilitei mensagens diretas para este servidor.",
+  "CONFIG_DM_DISABLE_INNER_MESSAGE": "Desabilitei mensagens diretas para este servidor.",
+  "CONFIG_THREAD_ENABLE_INNER_MESSAGE": "Habilitei tópicos em novas sugestões para este servidor.",
+  "CONFIG_THREAD_DISABLE_INNER_MESSAGE": "Desabilitei tópicos em novas sugestões para este servidor.",
+  "CONFIG_KEEPLOGS_ENABLE_INNER_MESSAGE": "Registros de sugestões agora serão mantidos no seu canal de sugestões.",
+  "CONFIG_KEEPLOGS_DISABLE_INNER_MESSAGE": "Registros de sugestões agora serão movidas para seu canal de registros quando finalizadas",
+  "CONFIG_AUTO_ARCHIVE_THREADS_ENABLE_INNER_MESSAGE": "Tópicos criados automaticamente para sugestões agora serão arquivados na resolução da sugestão.",
+  "CONFIG_AUTO_ARCHIVE_THREADS_DISABLE_INNER_MESSAGE": "Tópicos criados automaticamente para sugestões não serão mais arquivados na resolução da sugestão.",
+  "CONFIG_SUGGESTIONS_QUEUE_ENABLE_INNER_MESSAGE": "Todas as novas sugestões serão enviadas a sua fila de sugestões.",
+  "CONFIG_SUGGESTIONS_QUEUE_DISABLE_INNER_MESSAGE": "Todas as novas sugestões serão enviadas diretamente a seu canal de sugestões.",
+  "CONFIG_ANONYMOUS_RESOLUTION_ENABLE_INNER_MESSAGE": "Todas as outras sugestões não vão mostrar o moderador que as resolveu.",
+  "CONFIG_ANONYMOUS_RESOLUTION_DISABLE_INNER_MESSAGE": "Todas as outras sugestões vão mostrar o moderador que as resolveu.",
+  "CONFIG_GET_INNER_SUGGESTIONS_QUEUE_SET": "ativada.",
+  "CONFIG_GET_INNER_SUGGESTIONS_QUEUE_NOT_SET": "desativada.",
+  "CONFIG_GET_INNER_SUGGESTIONS_QUEUE_MESSAGE": "A fila de sugestões está $TEXT",
+  "CONFIG_GET_INNER_ANONYMOUS_RESOLUTION_NOT_SET": "Sugestores vão ver quem resolveram as suas sugestões.",
+  "CONFIG_GET_INNER_ANONYMOUS_RESOLUTION_SET": "Sugestores não vão ver quem resolveram as suas sugestões.",
+  "PAGINATION_INNER_SESSION_EXPIRED": "Esta sessão de paginação expirou; por favor, inicie uma nova com `/queue view`",
+  "PAGINATION_INNER_NEXT_ITEM": "Vendo o próximo item na fila...",
+  "PAGINATION_INNER_PREVIOUS_ITEM": "Vendo o item anterior na fila...",
+  "PAGINATION_INNER_QUEUE_EXPIRED": "Esta fila expirou.",
+  "PAGINATION_INNER_QUEUE_CANCELLED": "Anulei esta fila de espera para você.",
+  "PAGINATION_INNER_QUEUE_ACCEPTED": "Aceitei essa sugestão da fila.",
+  "PAGINATION_INNER_QUEUE_REJECTED": "Removi essa sugestão da fila.",
+  "QUEUE_VIEW_INNER_NOTHING_QUEUED": "Seu servidor não tem sugestões na fila.",
+  "QUEUE_VIEW_INNER_PRIOR_QUEUE": "Colocaram-se estas sugestões na fila antes que seu servidor desativasse a fila de sugestões.",
+  "QUEUE_INNER_USER_REJECTED": "Sua sugestão que estava na fila foi rejeitada.",
+  "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_SET": "pode",
+  "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_NOT_SET": "não pode",
+  "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_MESSAGE": "Este servidor {} ter imagens em sugestões.",
+  "CONFIG_SUGGESTIONS_IMAGES_ENABLE_INNER_MESSAGE": "Todas as novas sugestões podem incluir imagens.",
+  "CONFIG_SUGGESTIONS_IMAGES_DISABLE_INNER_MESSAGE": "Todas as novas sugestões não podem incluir imagens.",
+  "VIEW_VOTERS_INNER_EMBED_TITLE": "Vendo votantes..."
+} 
diff --git a/suggestions/main.py b/suggestions/main.py
index f0ba57a..f1d6074 100644
--- a/suggestions/main.py
+++ b/suggestions/main.py
@@ -11,6 +11,7 @@
 
 import cooldowns
 import disnake
+from disnake import Locale
 from disnake.ext import commands
 from bot_base.paginators.disnake_paginator import DisnakePaginator
 
@@ -154,6 +155,18 @@ async def info(
         )
         embed.add_field("Version", bot.version)
         embed.set_footer(text=f"© {year} Anthony Collier")
+
+        translations = {
+            Locale.pt_BR: {
+                "author": 651386805043593237,
+                "language": "Portuguese, Brazilian",
+                "username": "Davi",
+            }
+        }
+        if interaction.locale in translations:
+            data = translations[interaction.locale]
+            embed.description += f"\n\n{data['language']} translations by {data['username']}(`{data['author']}`)"
+
         await interaction.send(embed=embed)
 
     @bot.slash_command()
diff --git a/suggestions/objects/guild_config.py b/suggestions/objects/guild_config.py
index aefdb0e..389386d 100644
--- a/suggestions/objects/guild_config.py
+++ b/suggestions/objects/guild_config.py
@@ -27,6 +27,7 @@ def __init__(
         uses_suggestion_queue: bool = False,
         can_have_images_in_suggestions: bool = True,
         anonymous_resolutions: bool = False,
+        blocked_users: Optional[list[int]] = None,
         **kwargs,
     ):
         self._id: int = _id
@@ -41,6 +42,10 @@ def __init__(
         self.can_have_anonymous_suggestions: bool = can_have_anonymous_suggestions
         self.can_have_images_in_suggestions: bool = can_have_images_in_suggestions
 
+        if blocked_users is None:
+            blocked_users = set()
+        self.blocked_users: set[int] = set(blocked_users)
+
     @property
     def guild_id(self) -> int:
         return self._id
@@ -82,6 +87,7 @@ def as_dict(self) -> Dict:
         return {
             "_id": self.guild_id,
             "keep_logs": self.keep_logs,
+            "blocked_users": list(self.blocked_users),
             "log_channel_id": self.log_channel_id,
             "auto_archive_threads": self.auto_archive_threads,
             "dm_messages_disabled": self.dm_messages_disabled,
diff --git a/tests/test_bot.py b/tests/test_bot.py
index 44a1fc4..9dc4f01 100644
--- a/tests/test_bot.py
+++ b/tests/test_bot.py
@@ -17,6 +17,7 @@ async def test_cogs_loaded(causar: Causar):
         "ViewVotersCog",
         "SuggestionsMessageCommands",
         "SuggestionsQueueCog",
+        "BlacklistCog",
     ]
     assert len(bot.cogs) == len(cog_names)
     for cog_name in cog_names: