From 043c8b4df34ab9c0658b5368eca33ba4b552a7cd Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 2 Apr 2026 11:57:54 -0400 Subject: [PATCH] implemented discord adapter --- pm/tasks-v2.org | 46 +++++++++++++++++++++++++++++++------- youdis/adapters/discord.py | 26 ++++++++++++++++----- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/pm/tasks-v2.org b/pm/tasks-v2.org index 853d464..afa6421 100644 --- a/pm/tasks-v2.org +++ b/pm/tasks-v2.org @@ -122,7 +122,7 @@ update the discord bot into a thin frontend that talks to the backend and verify ** evidence - commit: -- tests: +- tests: https://youtu.be/20HxMMSqRyg?si=3v7mN2L88c_FxpQR 18m 1. start backend: `python3 -m uvicorn youdis.main:app --host 127.0.0.1 --port 8000` 2. create local env file: `cp .env.example .env` 3. add `api_token` to `.env` @@ -140,19 +140,49 @@ Traceback (most recent call last): return self.client._interaction_lookup[self._command_name] KeyError: 'youtube' #+end_src - :end: - -#+end_:out: -#+begin_src + :end: 6. confirm channel response says the job was submitted to backend 7. confirm requester receives DM updates for accepted/running/completed or failed + :out: + accepted job 4ef6fde5-57cc-478c-b0e2-18458c693fa4 for https://www.youtube.com/watch?v=dQw4w9WgXcQ +state=running | phase=running | [youtube] dQw4w9WgXcQ: Downloading webpage | path=/home/user/proj/youdis/downloads +state=running | phase=downloading | [download] Destination: /home/user/proj/youdis/downloads/Rick_Astley/NA/NANARickAstley-_Never_Gonna_Give_You_Up_Official_Video_4K_Remaster.f401.mp4 | path=/home/user/proj/youdis/downloads/Rick_Astley/NA/NANARickAstley-_Never_Gonna_Give_You_Up_Official_Video_4K_Remaster.f401.mp4 +state=completed | [Metadata] There isn't any metadata to add | path=/home/user/proj/youdis/downloads/Rick_Astley/NA/NANARickAstley-_Never_Gonna_Give_You_Up_Official_Video_4K_Remaster.mp4 +:end: 8. while first job is active, submit another `/youtube` and confirm busy behavior + :out: + #+begin_src shell + Error: AttributeError +Traceback (most recent call last): + File "/home/user/proj/youdis/venv/lib/python3.10/site-packages/interactions/client/client.py", line 1900, in __dispatch_interaction + response = await callback + File "/home/user/proj/youdis/venv/lib/python3.10/site-packages/interactions/client/client.py", line 1771, in _run_slash_command + return await command(ctx, **ctx.kwargs) + File "/home/user/proj/youdis/venv/lib/python3.10/site-packages/interactions/models/internal/command.py", line 132, in __call__ + await self.call_callback(self.callback, context) + File "/home/user/proj/youdis/venv/lib/python3.10/site-packages/interactions/models/internal/application_commands.py", line 833, in call_callback + return await self.call_with_binding(callback, ctx, *new_args, **new_kwargs) + File "/home/user/proj/youdis/venv/lib/python3.10/site-packages/interactions/models/internal/callback.py", line 43, in call_with_binding + return await callback(*args, **kwargs) + File "/home/user/proj/youdis/youdis/adapters/discord.py", line 167, in youtube + await ctx.channel.send(f"Submitted <{url}> to the backend. Status updates via DM.") +AttributeError: 'NoneType' object has no attribute 'send' + #+end_src + busy: busy with https://youtu.be/20HxMMSqRyg?si=3v7mN2L88c_FxpQR +state=running | phase=running | cancel requested | path=/home/user/proj/youdis/downloads/James_Hoffmann/NA/NANAThe_Beginner_s_Guide_To_Latte_Art.f401.mp4 + :end: 9. run `/status` and confirm it reflects current or last backend job + :out: + last job: state=completed | [Metadata] There isn't any metadata to add | path=/home/user/proj/youdis/downloads/Rick_Astley/NA/NANARickAstley-_Never_Gonna_Give_You_Up_Official_Video_4K_Remaster.mp4 + :end: 10. run `/interrupt` as owner and confirm cancellation is surfaced via DM -- datetime: + :out: + last job: state=cancelled | cancelled | path=/home/user/proj/youdis/downloads/James_Hoffmann/NA/NANAThe_Beginner_s_Guide_To_Latte_Art.f401.mp4 + :end: +- datetime:[2026-04-02 Thu 11:52] -*** testing tests +*** org-block tests #+begin_src shell :dir ~/proj/youdis :results output verbatim source ./venv/bin/activate python3 -m uvicorn youdis.main:app --host 127.0.0.1 --port 8000 @@ -177,7 +207,7 @@ python ./youdis.py - progress updates are currently implemented by polling `/jobs/current` and DMing only when the summary changes - legacy auth/user-management commands were removed from the active adapter path and should be cleaned up formally in `2.0.3` - `.env` is now supported for local/dev convenience, while real environment variables still override it in prod/docker - +- submitting via DM doesn't work * [ ] 2.0.3: remove deprecated discord-bot functionality (2) delete or retire legacy bot behaviors that no longer fit once the backend split is in place ** pm notes diff --git a/youdis/adapters/discord.py b/youdis/adapters/discord.py index e7aa553..000d0a1 100644 --- a/youdis/adapters/discord.py +++ b/youdis/adapters/discord.py @@ -64,6 +64,13 @@ async def dm(ctx: interactions.SlashContext, message: str) -> None: await ctx.author.send(message) +async def respond(ctx: interactions.SlashContext, message: str) -> None: + if ctx.channel is not None: + await ctx.channel.send(message) + return + await dm(ctx, message) + + async def poll_job_updates(ctx: interactions.SlashContext, job_id: str) -> None: last_sent = None try: @@ -105,14 +112,18 @@ def ensure_poll_task(ctx: interactions.SlashContext, job_id: str) -> None: return poll_tasks[job_id] = asyncio.create_task(poll_job_updates(ctx, job_id)) - -@interactions.listen() +@bot.listen() async def on_startup(): await get_session() print(f"discord adapter configured for backend {BACKEND_URL}") + print(f"discord adapter default scope: {DEFAULT_SCOPE}") + print(f"discord adapter command cache keys: {sorted(bot._interaction_lookup.keys())}") +@bot.listen() +async def on_ready(): + print(f"registered commands: {bot.application_commands}") -@interactions.listen() +@bot.listen() async def on_shutdown(): global http_session for task in list(poll_tasks.values()): @@ -151,16 +162,16 @@ async def youtube(ctx: interactions.SlashContext, url: str): state = job.get("state") job_id = job.get("job_id", "unknown") if state == "busy": - await ctx.channel.send(f"Backend is busy with another job. Details via DM.") + await respond(ctx, "Backend is busy with another job. Details via DM.") await dm(ctx, f"busy: {job.get('message')}") return if state != "accepted": - await ctx.channel.send("Backend rejected the request. Details via DM.") + await respond(ctx, "Backend rejected the request. Details via DM.") await dm(ctx, format_status_message(job)) return - await ctx.channel.send(f"Submitted <{url}> to the backend. Status updates via DM.") + await respond(ctx, f"Submitted <{url}> to the backend. Status updates via DM.") await dm(ctx, f"accepted job {job_id} for <{url}>") ensure_poll_task(ctx, job_id) @@ -211,4 +222,7 @@ def main() -> None: api_token = getenv("DISCORD_BOT_TOKEN") if not api_token: raise ValueError("API token not set. Retrieve from your Discord bot.") + bot.add_command(youtube) + bot.add_command(status) + bot.add_command(interrupt) bot.start(api_token)