diff --git a/.env.sample b/.env.sample index 3c28718..1ff6cf7 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,7 @@ TOKEN= -DEBUG= # 'true' or 'false', default 'false' + +# 'true' or 'false', default 'false' +DEBUG= DISCORD_UID_MAP="chair=1234,secretary=4567,user1=1234,user2=4567,user3=7890" diff --git a/Dockerfile b/Dockerfile index 349276d..7b62bed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ FROM python:3.14.3-alpine3.23 COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /uvx /bin/ +RUN apk add --no-cache git + WORKDIR /app COPY pyproject.toml . diff --git a/pyproject.toml b/pyproject.toml index 0c28a2e..59cd811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "asyncpg>=0.31.0", "dnspython>=2.8.0", "fortune-python>=1.1.2", - "hikari>=2.5.0", + "hikari", "hikari-arc>=2.2.0", "hikari-miru>=4.2.0", "pyfiglet>=1.0.4", @@ -77,6 +77,7 @@ ignore = [ "PLR0913", # too-many-arguments "COM812", # missing-trailing-comma "TC002", # typing-only-third-party-import + "SIM102", # collapsible-if ] [tool.ruff.lint.pydocstyle] @@ -88,3 +89,6 @@ required-version = "~=0.11" [tool.pyright] pythonVersion = "3.14" typeCheckingMode = "strict" + +[tool.uv.sources] +hikari = { git = "https://github.com/mplatypus/hikari.git", branch = "feature/modals-v2" } diff --git a/src/bot.py b/src/bot.py index e140709..fa41eba 100644 --- a/src/bot.py +++ b/src/bot.py @@ -22,7 +22,7 @@ ) client = Blockbot(bot, invocation_contexts=[hikari.ApplicationContextType.GUILD]) -miru_client = miru.Client.from_arc(client) +miru_client = miru.Client.from_arc(client, ignore_unknown_interactions=True) client.set_type_dependency(miru.Client, miru_client) diff --git a/src/config.py b/src/config.py index efb6c10..e1415ff 100644 --- a/src/config.py +++ b/src/config.py @@ -134,6 +134,17 @@ def set_env_var( "waiting-room": 627548568613552138, } +CATEGORY_IDS: dict[str, int] = { + "official": 568810044309766151, + "administration": 568810044309766151, + "technical": 1202325296133701723, + "general": 627868752310042627, + "general-bots": 1202319516407697438, + "webgroup": 1202321207811395624, + "voice-channels": 568403964098248723, + "archive": 958448000496173056, +} + # TODO: query API/LDAP for these ROLE_IDS: dict[str, int] = { "all": 568762266992902179, diff --git a/src/extensions/ticket.py b/src/extensions/ticket.py new file mode 100644 index 0000000..07995a8 --- /dev/null +++ b/src/extensions/ticket.py @@ -0,0 +1,190 @@ +import asyncio + +import arc +import hikari +from hikari.impl import special_endpoints as se +from hikari.interactions.interaction_components import ( + TextInputInteractionComponent, + TextSelectMenuInteractionComponent, +) + +from src.config import CATEGORY_IDS, ROLE_IDS +from src.hooks import restrict_to_roles +from src.models import Blockbot, BlockbotContext, BlockbotPlugin + +plugin = BlockbotPlugin("Ticketing System") + + +@plugin.include +@arc.with_hook(restrict_to_roles(role_ids=[ROLE_IDS["committee"]])) +@arc.slash_command("create_ticket_message", "Create Ticket Message") +async def options( + ctx: BlockbotContext, + channel: arc.Option[ + hikari.TextableChannel, + arc.ChannelParams("Ticket message channel name"), + ], +) -> None: + embed = hikari.Embed( + title="Tickets", + description="If you need to contact the admins, helpdesk or committee, create a ticket here!", + ) + row = se.MessageActionRowBuilder() + row.add_interactive_button( + hikari.ButtonStyle.PRIMARY, + "meowmeow-create-ticket", + emoji="✉️", + label="Create Ticket", + ) + await ctx.client.rest.create_message(channel, embed=embed, components=[row]) + await ctx.respond("Sent Message") + + +@plugin.listen() +async def component_interaction(event: hikari.InteractionCreateEvent) -> None: + if isinstance(event.interaction, hikari.ComponentInteraction): + if event.interaction.custom_id == "meowmeow-create-ticket": + await button_click(event.interaction) + elif event.interaction.custom_id == "honkhonk-close-ticket": + await close_ticket(event.interaction) + elif event.interaction.custom_id == "moomoo-close-ticket-confirm": + await delete_channel(event.interaction) + + elif isinstance(event.interaction, hikari.ModalInteraction): + if event.interaction.custom_id == "chirpchirp-ticket-channel": + await modal_submit(event.interaction) + + +async def button_click(interaction: hikari.ComponentInteraction) -> None: + text_label = se.LabelComponentBuilder( + label="Question", + component=se.TextInputBuilder( + custom_id="hophop-ticket-question", + placeholder="Ask your question here!", + style=hikari.TextInputStyle.PARAGRAPH, + required=True, + max_length=1000, + ), + ) + + committee_select = se.TextSelectMenuBuilder( + custom_id="ruffruff-committee-select", + placeholder="Choose Committee Position to Contact", + min_values=1, + max_values=1, + ) + for value in ("admins", "helpdesk", "committee"): + committee_select.add_option(value.title(), value) + committee_label = se.LabelComponentBuilder( + label="Committee", component=committee_select + ) + + await interaction.create_modal_response( + title="Ticket", + custom_id="chirpchirp-ticket-channel", + components=[committee_label, text_label], + ) + + +async def modal_submit(interaction: hikari.ModalInteraction) -> None: + committee_choice = interaction.components[0].component + assert isinstance(committee_choice, TextSelectMenuInteractionComponent) + selected_committee = committee_choice.values[0] + + question_choice = interaction.components[1].component + assert isinstance(question_choice, TextInputInteractionComponent) + selected_question = question_choice.value + + assert interaction.guild_id is not None + channels = await plugin.client.rest.fetch_guild_channels(interaction.guild_id) + existing_names = { + channel.name + for channel in channels + if hasattr(channel, "parent_id") + and channel.parent_id == CATEGORY_IDS["technical"] + } + ticket_number = 1 + channel_name = f"ticket-{interaction.user.username}" + while channel_name in existing_names: + channel_name = f"ticket-{interaction.user.username}-{ticket_number}" + ticket_number += 1 + + permission_role = ROLE_IDS[selected_committee] + created_channel = await plugin.client.rest.create_guild_text_channel( + interaction.guild_id, + channel_name, + category=CATEGORY_IDS["technical"], + permission_overwrites=[ + hikari.PermissionOverwrite( + id=permission_role, + type=hikari.PermissionOverwriteType.ROLE, + allow=hikari.Permissions.VIEW_CHANNEL, + ), + hikari.PermissionOverwrite( + id=interaction.user.id, + type=hikari.PermissionOverwriteType.MEMBER, + allow=hikari.Permissions.VIEW_CHANNEL, + ), + hikari.PermissionOverwrite( + id=interaction.guild_id, + type=hikari.PermissionOverwriteType.ROLE, + deny=hikari.Permissions.VIEW_CHANNEL, + ), + ], + ) + + embed = hikari.Embed(title="Ticket", description=selected_question) + row = se.MessageActionRowBuilder() + row.add_interactive_button( + hikari.ButtonStyle.PRIMARY, + "honkhonk-close-ticket", + emoji="⛔", + label="Close Ticket", + ) + await plugin.client.rest.create_message( + created_channel, + f"<@&{permission_role}> <@{interaction.user.id}>", + embed=embed, + components=[row], + user_mentions=True, + role_mentions=True, + ) + await interaction.create_initial_response( + hikari.ResponseType.MESSAGE_CREATE, + f"Ticket Created here: <#{created_channel.id}>!", + flags=hikari.MessageFlag.EPHEMERAL, + ) + + +async def close_ticket(interaction: hikari.ComponentInteraction) -> None: + embed = hikari.Embed( + title="Close Ticket?", description="Are you sure you want to close the ticket?" + ) + row = se.MessageActionRowBuilder() + row.add_interactive_button( + hikari.ButtonStyle.PRIMARY, + "moomoo-close-ticket-confirm", + emoji="⛔", + label="Close Ticket", + ) + await interaction.create_initial_response( + hikari.ResponseType.MESSAGE_CREATE, + embed=embed, + components=[row], + flags=hikari.MessageFlag.EPHEMERAL, + ) + + +async def delete_channel(interaction: hikari.ComponentInteraction) -> None: + await interaction.create_initial_response( + hikari.ResponseType.MESSAGE_CREATE, + "Closing ticket...", + flags=hikari.MessageFlag.EPHEMERAL, + ) + await asyncio.sleep(3) + await plugin.client.rest.delete_channel(interaction.channel_id) + + +@arc.loader +def loader(client: Blockbot) -> None: + client.add_plugin(plugin) diff --git a/uv.lock b/uv.lock index d9e862c..3ae20f2 100644 --- a/uv.lock +++ b/uv.lock @@ -230,7 +230,7 @@ requires-dist = [ { name = "asyncpg", specifier = ">=0.31.0" }, { name = "dnspython", specifier = ">=2.8.0" }, { name = "fortune-python", specifier = ">=1.1.2" }, - { name = "hikari", specifier = ">=2.5.0" }, + { name = "hikari", git = "https://github.com/mplatypus/hikari.git?branch=feature%2Fmodals-v2" }, { name = "hikari-arc", specifier = ">=2.2.0" }, { name = "hikari-miru", specifier = ">=4.2.0" }, { name = "miniopy-async", specifier = ">=1.23.5" }, @@ -435,18 +435,14 @@ wheels = [ [[package]] name = "hikari" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } +version = "2.5.1.dev0" +source = { git = "https://github.com/mplatypus/hikari.git?branch=feature%2Fmodals-v2#c73864e36c0b3c1cc604cf587c0a351421a08ea0" } dependencies = [ { name = "aiohttp" }, { name = "attrs" }, { name = "colorlog" }, { name = "multidict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/bd/e4bd16c662fe7614bb6f38aba40a095aef998af0602674fe24740d3814cd/hikari-2.5.0.tar.gz", hash = "sha256:b8fe7e5cd5adfedcdcc488a86781bc946002fb79d984a473736624dd8201993b", size = 1924829, upload-time = "2025-10-31T12:34:27.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/c5/256c7d498cbe8e67edfd9bb2971740eadb662effcf9b9c1aff8e6d21e90d/hikari-2.5.0-py3-none-any.whl", hash = "sha256:fd108b51c73f46b562ac9e7cd48d4f74655224083ae37a319a417c30418a40ec", size = 580206, upload-time = "2025-10-31T12:34:25.633Z" }, -] [[package]] name = "hikari-arc"