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
This commit is contained in:
Claude
2026-02-28 08:14:46 +00:00
parent 123252e197
commit 30c9872d04
2 changed files with 52 additions and 1 deletions

View File

@@ -47,6 +47,9 @@ class DealsBot:
else: else:
self._first_run_done = True self._first_run_done = True
# Start presence heartbeat so the bot shows as "online"
await self.matrix.start_presence_heartbeat()
async def stop(self): async def stop(self):
"""Clean shutdown.""" """Clean shutdown."""
await self._http.aclose() await self._http.aclose()

View File

@@ -1,11 +1,14 @@
"""Matrix client wrapper for sending deal messages.""" """Matrix client wrapper for sending deal messages."""
import asyncio
import logging import logging
from nio import AsyncClient, RoomSendError from nio import AsyncClient, PresenceSetError, RoomSendError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PRESENCE_HEARTBEAT_INTERVAL = 60 # seconds
class MatrixDealsClient: class MatrixDealsClient:
def __init__( def __init__(
@@ -19,6 +22,7 @@ class MatrixDealsClient:
self._client = AsyncClient(homeserver_url, user_id) self._client = AsyncClient(homeserver_url, user_id)
self._client.access_token = access_token self._client.access_token = access_token
self._client.user_id = user_id self._client.user_id = user_id
self._heartbeat_task: asyncio.Task | None = None
async def send_deal(self, plain_text: str, html: str) -> bool: async def send_deal(self, plain_text: str, html: str) -> bool:
"""Send a deal message to the configured room. """Send a deal message to the configured room.
@@ -68,5 +72,49 @@ class MatrixDealsClient:
logger.error("Matrix send error: %s", exc) logger.error("Matrix send error: %s", exc)
return False 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): async def close(self):
await self.stop_presence_heartbeat()
await self._client.close() await self._client.close()