Apply conversation bot improvements (proto-improvements)

Diff vs working-copy baseline = exactly this session's bot edits:
- constants.py: env-var config, safe JSON loading, optional-dependency
  guards, env->netrc token resolution, API_SHARED_KEY + service_headers()
- communication_subroutine.py: queue get(timeout) + Empty handling,
  daemon threads, cooperative stop_event, inbound _authorize_request()
- bot.py: single asyncio event loop with cooperative shutdown
- music_functions / radio_commands / librarian_commands: send
  X-Conjurer-Api-Key on internal HTTP calls via service_headers()

Musician runtime data overlay dropped; conjurer_musician/.gitignore now
keeps generated playlists/mp3 out of the repo. The 1+2+3 musician code
port already lives in the base conjurer_musician.

Depends on #9: must merge after restructure/working-copy-root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michal Tuszowski
2026-06-28 23:36:58 +02:00
committed by Michał Tuszowski
parent f9a1e7c03a
commit 8b7058b411
7 changed files with 334 additions and 91 deletions
+48 -17
View File
@@ -5,6 +5,7 @@
"""
Module of a python bot named Conjurer - used to work on BDSM discord servers.
"""
import asyncio
import logging
# *=========================================== Standard Library Imports
@@ -82,22 +83,52 @@ async def on_ready():
logger.info("All systems: operational")
# *=========================================== Runtime orchestration
# The legacy bootstrap used two bare threads (client.run + comm_subroutine) and
# joined them, which made a clean shutdown impossible. We now drive everything
# from a single asyncio loop: the Flask comm layer still runs in its own
# threads (via asyncio.to_thread) but is steered through a shared stop_event so
# the bot can stop both halves cooperatively.
async def _run_comm_subroutine(stop_event: threading.Event) -> None:
"""Run the blocking comm subroutine in a worker thread."""
await asyncio.to_thread(comm_subroutine, stop_event)
async def _run_bot(token: str, shutdown_event: asyncio.Event) -> None:
"""Start the Discord client and flag shutdown when it returns."""
try:
await client.start(token, log_handler=None)
finally:
shutdown_event.set()
async def main() -> None:
if not TOKEN:
logger.error("Discord token missing - set DISCORD_TOKEN or configure netrc")
return
logger.info("Starting discord bot")
shutdown_event = asyncio.Event()
comm_stop_event = threading.Event()
comm_task = asyncio.create_task(_run_comm_subroutine(comm_stop_event))
bot_task = asyncio.create_task(_run_bot(TOKEN, shutdown_event))
try:
await shutdown_event.wait()
except (KeyboardInterrupt, asyncio.CancelledError):
logger.info("Shutdown signal received")
comm_stop_event.set()
await client.close()
finally:
comm_stop_event.set()
if not client.is_closed():
await client.close()
await asyncio.gather(bot_task, comm_task, return_exceptions=True)
# *================================== Run
if __name__ == "__main__":
logger.info("Starting discord bot")
threads = []
logger.info("Starting discord bot: Creating threads")
threads.append(threading.Thread(target=client.run, args=(TOKEN,),kwargs={"log_handler":None}))
threads.append(threading.Thread(target=comm_subroutine))
logger.info("Starting discord bot: Starting threads")
WRK_CNT = 0
for worker in threads:
WRK_CNT += 1
logger.info("Starting discord bot: Starting thread %s", WRK_CNT)
worker.start()
logger.info("Starting discord bot: Joining threads")
WRK_CNT = 0
for worker in threads:
WRK_CNT += 1
logger.info("Starting discord bot: Joining thread %s", WRK_CNT)
worker.join()
asyncio.run(main())