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()