From 30c9872d04fefdf807e91e8a653ad8320b4d0c6c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 08:14:46 +0000 Subject: [PATCH] Add presence heartbeat to keep bot shown as online Adds a background asyncio.Task that calls set_presence("online") every 60 seconds to keep the homeserver presence status fresh. On startup the bot sets itself online and spawns the heartbeat; on shutdown the task is cancelled and presence is set to offline before closing the client. https://claude.ai/code/session_01Nu3N2okrfJV3meatEmYdrB --- gaming_deals_bot/bot.py | 3 ++ gaming_deals_bot/matrix_client.py | 50 ++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/gaming_deals_bot/bot.py b/gaming_deals_bot/bot.py index 3ba6a0e..7d4b9a4 100644 --- a/gaming_deals_bot/bot.py +++ b/gaming_deals_bot/bot.py @@ -47,6 +47,9 @@ class DealsBot: else: self._first_run_done = True + # Start presence heartbeat so the bot shows as "online" + await self.matrix.start_presence_heartbeat() + async def stop(self): """Clean shutdown.""" await self._http.aclose() diff --git a/gaming_deals_bot/matrix_client.py b/gaming_deals_bot/matrix_client.py index 7d96da6..1b969ab 100644 --- a/gaming_deals_bot/matrix_client.py +++ b/gaming_deals_bot/matrix_client.py @@ -1,11 +1,14 @@ """Matrix client wrapper for sending deal messages.""" +import asyncio import logging -from nio import AsyncClient, RoomSendError +from nio import AsyncClient, PresenceSetError, RoomSendError logger = logging.getLogger(__name__) +PRESENCE_HEARTBEAT_INTERVAL = 60 # seconds + class MatrixDealsClient: def __init__( @@ -19,6 +22,7 @@ class MatrixDealsClient: self._client = AsyncClient(homeserver_url, user_id) self._client.access_token = access_token self._client.user_id = user_id + self._heartbeat_task: asyncio.Task | None = None async def send_deal(self, plain_text: str, html: str) -> bool: """Send a deal message to the configured room. @@ -68,5 +72,49 @@ class MatrixDealsClient: logger.error("Matrix send error: %s", exc) return False + async def set_presence(self, presence: str) -> bool: + """Set the bot's presence status on the homeserver. + + Returns True on success, False on failure. + """ + try: + resp = await self._client.set_presence(presence) + if isinstance(resp, PresenceSetError): + logger.error("Failed to set presence to %s: %s", presence, resp.message) + return False + logger.debug("Presence set to %s", presence) + return True + except Exception as exc: + logger.error("Presence set error: %s", exc) + return False + + async def start_presence_heartbeat(self): + """Set presence to online and spawn a background task to keep it alive.""" + await self.set_presence("online") + self._heartbeat_task = asyncio.create_task(self._presence_heartbeat_loop()) + logger.info("Presence heartbeat started (every %ds)", PRESENCE_HEARTBEAT_INTERVAL) + + async def _presence_heartbeat_loop(self): + """Re-send 'online' presence every PRESENCE_HEARTBEAT_INTERVAL seconds.""" + try: + while True: + await asyncio.sleep(PRESENCE_HEARTBEAT_INTERVAL) + await self.set_presence("online") + except asyncio.CancelledError: + pass + + async def stop_presence_heartbeat(self): + """Cancel the heartbeat task and set presence to offline.""" + if self._heartbeat_task is not None: + self._heartbeat_task.cancel() + try: + await self._heartbeat_task + except asyncio.CancelledError: + pass + self._heartbeat_task = None + await self.set_presence("offline") + logger.info("Presence heartbeat stopped, set to offline") + async def close(self): + await self.stop_presence_heartbeat() await self._client.close()