Skip to content

Commit

Permalink
add action items command (#55)
Browse files Browse the repository at this point in the history
Co-authored-by: nova <[email protected]>
  • Loading branch information
wizzdom and novanai authored Nov 18, 2024
1 parent 097e692 commit 6bb0a70
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 2 deletions.
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")
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
)

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)

0 comments on commit 6bb0a70

Please sign in to comment.