Skip to content

Commit

Permalink
Merge branch 'main' into agenda
Browse files Browse the repository at this point in the history
  • Loading branch information
novanai authored Jan 14, 2025
2 parents 3d2b9cc + 833a99e commit 9b11633
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 1 deletion.
34 changes: 34 additions & 0 deletions .github/deploy/production.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ job "blockbot" {

group "blockbot" {
count = 1
network {
port "db" {
to = 5432
}
}

task "blockbot" {
driver = "docker"
Expand All @@ -24,14 +29,43 @@ job "blockbot" {
template {
data = <<EOF
TOKEN={{ key "blockbot/discord/token" }}
LDAP_USERNAME={{ key "blockbot/ldap/username" }}
LDAP_PASSWORD={{ key "blockbot/ldap/password" }}
DISCORD_UID_MAP={{ key "blockbot/discord/uid_map" }}
AGENDA_TEMPLATE_URL={{ key "blockbot/agenda/template_url" }}
DB_HOST={{ env "NOMAD_ADDR_db" }} # address and port
DB_NAME={{ key "blockbot/db/name" }} # database name
DB_PASSWORD={{ key "blockbot/db/password" }}
DB_USER={{ key "blockbot/db/user" }}
EOF
destination = "local/.env"
env = true
}
}
task "blockbot-db" {
driver = "docker"

config {
image = "postgres:17-alpine"
ports = ["db"]

volumes = [
"/storage/nomad/blockbot/db:/var/lib/postgresql/data",
]
}

template {
data = <<EOH
POSTGRES_PASSWORD={{ key "blockbot/db/password" }}
POSTGRES_USER={{ key "blockbot/db/user" }}
POSTGRES_NAME={{ key "blockbot/db/name" }}
EOH
destination = "local/db.env"
env = true
}
}
}
}
34 changes: 34 additions & 0 deletions .github/deploy/review.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ job "blockbot-[[.environment_slug]]" {

group "blockbot" {
count = 1
network {
port "db" {
to = 5432
}
}

task "blockbot-review-[[.git_sha]]" {
driver = "docker"
Expand All @@ -25,14 +30,43 @@ job "blockbot-[[.environment_slug]]" {
data = <<EOF
TOKEN={{ key "blockbot-dev/discord/token" }}
DEBUG=true
LDAP_USERNAME={{ key "blockbot-dev/ldap/username" }}
LDAP_PASSWORD={{ key "blockbot-dev/ldap/password" }}
DISCORD_UID_MAP={{ key "blockbot-dev/discord/uid_map" }}
AGENDA_TEMPLATE_URL={{ key "blockbot-dev/agenda/template_url" }}
DB_HOST={{ env "NOMAD_ADDR_db" }} # address and port
DB_NAME={{ key "blockbot-dev/db/name" }} # database name
DB_PASSWORD={{ key "blockbot-dev/db/password" }}
DB_USER={{ key "blockbot-dev/db/user" }}
EOF
destination = "local/.env"
env = true
}
}
task "blockbot-dev-db" {
driver = "docker"

config {
image = "postgres:17-alpine"
ports = ["db"]

volumes = [
"/storage/nomad/blockbot-dev/db:/var/lib/postgresql/data",
]
}

template {
data = <<EOH
POSTGRES_PASSWORD={{ key "blockbot-dev/db/password" }}
POSTGRES_USER={{ key "blockbot-dev/db/user" }}
POSTGRES_NAME={{ key "blockbot-dev/db/name" }}
EOH
destination = "local/db.env"
env = true
}
}
}
}
131 changes: 130 additions & 1 deletion src/extensions/action_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,20 @@ async def get_action_items(

# send each bullet point separately
for item in formatted_bullet_points:
await action_items.client.rest.create_message(
item = await action_items.client.rest.create_message(
CHANNEL_IDS["action-items"],
mentions_everyone=False,
user_mentions=True,
role_mentions=True,
content=item,
)

await action_items.client.rest.add_reaction(
channel=item.channel_id,
message=item.id,
emoji="✅",
)

# respond with success if it executes successfully
await ctx.respond(
"✅ Action Items sent successfully!",
Expand All @@ -111,6 +117,129 @@ async def get_action_items(
return


async def check_valid_reaction(
event: hikari.GuildReactionAddEvent | hikari.GuildReactionDeleteEvent,
message: hikari.PartialMessage,
) -> 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)
45 changes: 45 additions & 0 deletions src/extensions/help.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 9b11633

Please sign in to comment.