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

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