Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add action items command #55

Merged
merged 13 commits into from
Nov 18, 2024
5 changes: 5 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
TOKEN=
DEBUG=

DISCORD_UID_MAP="user1=1234,user2=4567,user3=7890"

LDAP_USERNAME=
LDAP_PASSWORD=
3 changes: 3 additions & 0 deletions .github/deploy/production.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ 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" }}
EOF
destination = "local/.env"
env = true
Expand Down
3 changes: 3 additions & 0 deletions .github/deploy/review.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ 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" }}
EOF
destination = "local/.env"
env = true
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
aiohttp==3.10.10
fortune-python==1.1.1
hikari==2.1.0
hikari-arc==1.4.0
Expand Down
18 changes: 18 additions & 0 deletions src/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import arc
import hikari
import aiohttp
import miru

from src.config import DEBUG, TOKEN
Expand Down Expand Up @@ -31,6 +32,23 @@
client.load_extensions_from("./src/examples/")


@client.listen(hikari.StartingEvent)
async def on_start(event: hikari.StartingEvent) -> None:
# Create an aiohttp ClientSession to use for web requests
aiohttp_client = aiohttp.ClientSession()
client.set_type_dependency(aiohttp.ClientSession, aiohttp_client)


@client.listen(hikari.StoppedEvent)
# By default, dependency injection is only enabled for command callbacks, pre/post hooks & error handlers
# so dependency injection must be enabled manually for this event listener
@client.inject_dependencies
async def on_stop(
event: hikari.StoppedEvent, aiohttp_client: aiohttp.ClientSession = arc.inject()
) -> None:
await aiohttp_client.close()


@client.set_error_handler
async def error_handler(ctx: arc.GatewayContext, exc: Exception) -> None:
if DEBUG:
Expand Down
24 changes: 23 additions & 1 deletion src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,27 @@

TOKEN = os.environ.get("TOKEN") # required
DEBUG = os.environ.get("DEBUG", False)
DISCORD_UID_MAP = os.environ.get("DISCORD_UID_MAP")

CHANNEL_IDS: dict[str, int] = {"lobby": 627542044390457350}
CHANNEL_IDS: dict[str, int] = {
"lobby": 627542044390457350,
"bot-private": 853071983452225536,
"bots-cmt": 1162038557922312312,
"action-items": 1029132014210793513,
}

# TODO: query API/LDAP for these
ROLE_IDS: dict[str, int] = {
"all": 568762266992902179,
"everyone": 568762266992902179,
"committee": 568762266992902179,
"cmt": 568762266992902179,
"events": 807389174009167902,
"admins": 585512338728419341,
"helpdesk": 1194683307921772594,
}

UID_MAPS = dict(item.split("=") for item in DISCORD_UID_MAP.split(","))

LDAP_USERNAME = os.environ.get("LDAP_USERNAME")
LDAP_PASSWORD = os.environ.get("LDAP_PASSWORD")
wizzdom marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion src/examples/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async def view_check(self, ctx: miru.ViewContext) -> bool:
async def on_timeout(self) -> None:
message = self.message
# Since the view is bound to a message, we can assert it's not None
assert message
assert message

for item in self.children:
item.disabled = True
Expand Down
107 changes: 107 additions & 0 deletions src/extensions/action_items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import arc
import hikari
import re
import aiohttp
from urllib.parse import urlparse

from src.utils import role_mention, hedgedoc_login
from src.hooks import restrict_to_channels, restrict_to_roles
from src.config import CHANNEL_IDS, ROLE_IDS, UID_MAPS


action_items = arc.GatewayPlugin(name="Action Items")


@action_items.include
@arc.with_hook(restrict_to_channels(channel_ids=[CHANNEL_IDS["action-items"]]))
@arc.with_hook(restrict_to_roles(role_ids=[ROLE_IDS["committee"]]))
@arc.slash_command(
"action_items", "Display the action items from the MD", is_dm_enabled=False
)
async def get_action_items(
ctx: arc.GatewayContext,
url: arc.Option[str, arc.StrParams("URL of the minutes from the MD")],
aiohttp_client: aiohttp.ClientSession = arc.inject(),
) -> None:
"""Display the action items from the MD!"""

if "https://md.redbrick.dcu.ie" not in url:
await ctx.respond(
f"❌ `{url}` is not a valid MD URL. Please provide a valid URL.",
flags=hikari.MessageFlag.EPHEMERAL,
)
return

await hedgedoc_login(aiohttp_client)

parsed_url = urlparse(url)
request_url = (
f"{parsed_url.scheme}://{parsed_url.hostname}{parsed_url.path}/download"
)

async with aiohttp_client.get(request_url) as response:
if response.status != 200:
await ctx.respond(
f"❌ Failed to fetch the minutes. Status code: `{response.status}`",
flags=hikari.MessageFlag.EPHEMERAL,
)
return

content = await response.text()

# extract the action items section from the minutes
action_items_section = re.search(
r"# Action Items:?\n(.*?)(\n# |\n---|$)", content, re.DOTALL
wizzdom marked this conversation as resolved.
Show resolved Hide resolved
)

if not action_items_section:
await ctx.respond("❌ No `Action Items` section found.")
return

# Get the matched content (excluding the "Action Items" heading itself)
action_items_content = action_items_section.group(1)

# extract each bullet point without the bullet point itself
bullet_points = re.findall(r"^\s*[-*]\s+(.+)", action_items_content, re.MULTILINE)

# format each bullet point separately in a list
formatted_bullet_points = [
"- " + re.sub(r"^\[.\]\s+", "", item) for item in bullet_points
]

# Replace user names with user mentions
for i, item in enumerate(formatted_bullet_points):
for name, uid in UID_MAPS.items():
item = item.replace(f"`{name}`", f"<@{uid}>")
formatted_bullet_points[i] = item

# Replace role names with role mentions
for i, item in enumerate(formatted_bullet_points):
for role, role_id in ROLE_IDS.items():
item = item.replace(f"`{role}`", role_mention(role_id))
formatted_bullet_points[i] = item

# Send title to the action-items channel
await action_items.client.rest.create_message(
CHANNEL_IDS["action-items"],
content="# Action Items:",
)

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

# respond with success if it executes successfully
await ctx.respond("✅ Action Items sent successfully!")
return


@arc.loader
def loader(client: arc.GatewayClient) -> None:
client.add_plugin(action_items)
52 changes: 52 additions & 0 deletions src/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import arc
import hikari
import typing


async def _restrict_to_roles(
ctx: arc.GatewayContext, role_ids: typing.Sequence[hikari.Snowflake]
) -> arc.HookResult:
assert ctx.member

if not any(role_id in ctx.member.role_ids for role_id in role_ids):
await ctx.respond(
"❌ This command is restricted. Only allowed roles are permitted to use this command.",
flags=hikari.MessageFlag.EPHEMERAL,
)
return arc.HookResult(abort=True)

return arc.HookResult() # by default, abort is set to False


# TODO: make response type a TypeVar for reuse (WrappedHookResult)
def restrict_to_roles(
role_ids: typing.Sequence[hikari.Snowflake],
) -> typing.Callable[[arc.GatewayContext], typing.Awaitable[arc.HookResult]]:
"""Any command which uses this hook requires that the command be disabled in DMs as a guild role is required for this hook to function."""

async def func(ctx: arc.GatewayContext) -> arc.HookResult:
return await _restrict_to_roles(ctx, role_ids)

return func


async def _restrict_to_channels(
ctx: arc.GatewayContext, channel_ids: typing.Sequence[hikari.Snowflake]
) -> arc.HookResult:
if ctx.channel_id not in channel_ids:
await ctx.respond(
"❌ This command cannot be used in this channel.",
flags=hikari.MessageFlag.EPHEMERAL,
)
return arc.HookResult(abort=True)

return arc.HookResult()


def restrict_to_channels(
channel_ids: typing.Sequence[hikari.Snowflake],
) -> typing.Callable[[arc.GatewayContext], typing.Awaitable[arc.HookResult]]:
async def func(ctx: arc.GatewayContext) -> arc.HookResult:
return await _restrict_to_channels(ctx, channel_ids)

return func
11 changes: 11 additions & 0 deletions src/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import hikari
from arc import GatewayClient
import aiohttp
from src.config import LDAP_USERNAME, LDAP_PASSWORD


async def get_guild(
Expand All @@ -10,3 +12,12 @@ async def get_guild(

def role_mention(role_id: hikari.Snowflake | int | str) -> str:
return f"<@&{role_id}>"


async def hedgedoc_login(aiohttp_client: aiohttp.ClientSession) -> None:
data = {
"username": LDAP_USERNAME,
"password": LDAP_PASSWORD,
}

await aiohttp_client.post("https://md.redbrick.dcu.ie/auth/ldap", data=data)