Files
Melora/app/matrix.py
Claude e21f5dfd50 Add presence heartbeat to keep bot showing as online
matrix-nio's AsyncClient.set_presence() is called every 60s in a
background task so Matrix clients see the bot as online. On shutdown
the bot sets itself to offline.

https://claude.ai/code/session_01DuzWyMMXvLMB4VxEwJyV4X
2026-02-28 07:46:40 +00:00

114 lines
3.0 KiB
Python

import asyncio
import logging
from nio import AsyncClient, RoomSendResponse
from app.config import config
log = logging.getLogger(__name__)
_client: AsyncClient | None = None
_presence_task: asyncio.Task | None = None
PRESENCE_INTERVAL_SECONDS = 60
THREAD_ROOT_MESSAGES: dict[str, str] = {
"movies": "\U0001f3ac Movies",
"shows": "\U0001f4fa Shows",
"music": "\U0001f3b5 Music",
}
async def get_client() -> AsyncClient:
global _client
if _client is None:
_client = AsyncClient(config.MATRIX_HOMESERVER_URL)
_client.user_id = config.MATRIX_BOT_USER_ID
_client.access_token = config.MATRIX_BOT_ACCESS_TOKEN
return _client
async def close_client() -> None:
global _client, _presence_task
if _presence_task is not None:
_presence_task.cancel()
try:
await _presence_task
except asyncio.CancelledError:
pass
_presence_task = None
if _client is not None:
await _set_presence("offline")
await _client.close()
_client = None
async def _set_presence(state: str) -> None:
client = await get_client()
try:
await client.set_presence(state)
except Exception:
log.debug("Presence update failed", exc_info=True)
async def _presence_loop() -> None:
while True:
await _set_presence("online")
await asyncio.sleep(PRESENCE_INTERVAL_SECONDS)
async def start_presence() -> None:
global _presence_task
await _set_presence("online")
_presence_task = asyncio.create_task(_presence_loop())
log.info("Presence heartbeat started (every %ds)", PRESENCE_INTERVAL_SECONDS)
async def send_thread_root(media_type: str) -> str:
client = await get_client()
label = THREAD_ROOT_MESSAGES[media_type]
content = {
"msgtype": "m.text",
"body": label,
}
resp = await client.room_send(
config.MATRIX_ARRIVALS_ROOM_ID,
"m.room.message",
content,
)
if isinstance(resp, RoomSendResponse):
log.info("Created thread root for %s: %s", media_type, resp.event_id)
return resp.event_id
raise RuntimeError(f"Failed to create thread root for {media_type}: {resp}")
async def post_to_thread(
thread_root_event_id: str,
plain_body: str,
html_body: str,
) -> str | None:
client = await get_client()
content = {
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"body": plain_body,
"formatted_body": html_body,
"m.relates_to": {
"rel_type": "m.thread",
"event_id": thread_root_event_id,
},
}
try:
resp = await client.room_send(
config.MATRIX_ARRIVALS_ROOM_ID,
"m.room.message",
content,
)
if isinstance(resp, RoomSendResponse):
return resp.event_id
log.error("Matrix send failed: %s", resp)
return None
except Exception:
log.exception("Matrix posting error")
return None