diff --git a/.github/deploy/production.hcl b/.github/deploy/production.hcl index 1f25176..765233a 100644 --- a/.github/deploy/production.hcl +++ b/.github/deploy/production.hcl @@ -8,6 +8,11 @@ job "blockbot" { group "blockbot" { count = 1 + network { + port "db" { + to = 5432 + } + } task "blockbot" { driver = "docker" @@ -24,14 +29,43 @@ job "blockbot" { template { data = < bool: + bot_user = action_items.client.app.get_me() + if not bot_user: # bot_user will always be available after the bot has started + return False + + # ignore reactions by the bot, reactions that are not ✅ + # and reactions not created in the #action-items channel + if ( + event.user_id == bot_user.id + or event.emoji_name != "✅" + or event.channel_id != CHANNEL_IDS["action-items"] + ): + return False + + assert message.author # it will always be available + + # ignore messages not sent by the bot and messages with no content + if message.author.id != bot_user.id or not message.content: + return False + + return True + + +async def validate_user_reaction( + user_id: int, message_content: str, guild_id: int +) -> bool: + # extract user and role mentions from the message content + mention_regex = r"<@[!&]?(\d+)>" + mentions = re.findall(mention_regex, message_content) + + # make a list of all mentions + mentioned_ids = [int(id_) for id_ in mentions] + + if user_id in mentioned_ids: + return True + + member = action_items.client.cache.get_member( + guild_id, user_id + ) or await action_items.client.rest.fetch_member(guild_id, user_id) + + if any(role_id in mentioned_ids for role_id in member.role_ids): + return True + + return False + + +@action_items.listen() +async def reaction_add(event: hikari.GuildReactionAddEvent) -> None: + # retrieve the message that was reacted to + message = action_items.client.cache.get_message( + event.message_id + ) or await action_items.client.rest.fetch_message( + event.channel_id, event.message_id + ) + + is_valid_reaction = await check_valid_reaction(event, message) + if not is_valid_reaction: + return + + assert message.content # check_valid_reaction verifies the message content exists + + is_valid_reaction = await validate_user_reaction( + event.user_id, message.content, event.guild_id + ) + if not is_valid_reaction: + return + + # cross out the action item, if it was not crossed out already + if not message.content.startswith("- ✅ ~~"): + # add strikethrough and checkmark + updated_content = f"- ✅ ~~{message.content[2:]}~~" + await action_items.client.rest.edit_message( + event.channel_id, event.message_id, content=updated_content + ) + + +@action_items.listen() +async def reaction_remove(event: hikari.GuildReactionDeleteEvent) -> None: + # retrieve the message that was un-reacted to + # NOTE: cannot use cached message as the reaction count will be outdated + message = await action_items.client.rest.fetch_message( + event.channel_id, event.message_id + ) + + is_valid_reaction = await check_valid_reaction(event, message) + if not is_valid_reaction: + return + + assert message.content # check_valid_reaction verifies the message content exists + + checkmark_reactions = await event.app.rest.fetch_reactions_for_emoji( + event.channel_id, + event.message_id, + "✅", + ) + + reactions = [ + await validate_user_reaction(user.id, message.content, event.guild_id) + for user in checkmark_reactions + ] + valid_reaction_count = len( + list( + filter( + lambda r: r is True, + reactions, + ) + ) + ) + + assert message.content # check_valid_reaction verifies the message content exists + # remove the strikethrough on the item, provided all mentioned users/roles + # are not currently reacted to the message + if message.content.startswith("- ✅ ~~") and valid_reaction_count == 0: + # add strikethrough and checkmark + updated_content = f"- {message.content[6:-2]}" + await action_items.client.rest.edit_message( + event.channel_id, event.message_id, content=updated_content + ) + + @arc.loader def loader(client: arc.GatewayClient) -> None: client.add_plugin(action_items) diff --git a/src/extensions/help.py b/src/extensions/help.py new file mode 100644 index 0000000..6f4302f --- /dev/null +++ b/src/extensions/help.py @@ -0,0 +1,45 @@ +import hikari +import arc +import itertools +import collections + +plugin = arc.GatewayPlugin(name="Help Command Plugin") + + +def gather_commands() -> dict[str | None, list[str]]: + plugin_commands: dict[str | None, list[str]] = collections.defaultdict(list) + + for plugin_, commands in itertools.groupby( + plugin.client.walk_commands(hikari.CommandType.SLASH), + key=lambda cmd: cmd.plugin, + ): + for cmd in commands: + if not isinstance(cmd, (arc.SlashCommand, arc.SlashSubCommand)): + continue + + plugin_commands[plugin_.name if plugin_ else None].append( + f"{cmd.make_mention()} - {cmd.description}" + ) + + return plugin_commands + + +@plugin.include +@arc.slash_command("help", "Displays a list of all commands.") +async def help_command(ctx: arc.GatewayContext) -> None: + """Displays a simple list of all bot commands.""" + + plugin_commands = gather_commands() + embed = hikari.Embed(title="Bot Commands", color=0x00FF00) + + for plugin_, commands in plugin_commands.items(): + embed.add_field( + name=plugin_ or "No plugin", value="\n".join(commands), inline=False + ) + + await ctx.respond(embed=embed) + + +@arc.loader +def load(client: arc.GatewayClient) -> None: + client.add_plugin(plugin)