Files
microdao-daarion/services/matrix-presence-aggregator/app/matrix_client.py
Apple 5bed515852 feat: Upgrade Global Presence to SSE architecture
- matrix-presence-aggregator v2 with SSE endpoint
- Created @presence_daemon Matrix user
- SSE proxy in Next.js /api/presence/stream
- Updated frontend to use SSE instead of WebSocket
- Real-time city online count and room presence
2025-11-26 14:43:46 -08:00

95 lines
3.4 KiB
Python

"""Matrix API client for presence aggregation"""
import httpx
from typing import List, Optional
import logging
logger = logging.getLogger(__name__)
class MatrixClient:
"""Simplified Matrix client for reading members, presence, and typing"""
def __init__(self, base_url: str, access_token: str, daemon_user: str = ""):
self.base_url = base_url.rstrip("/")
self.access_token = access_token
self.daemon_user = daemon_user # Filter this user from lists
self._client = httpx.AsyncClient(
base_url=self.base_url,
headers={"Authorization": f"Bearer {self.access_token}"},
timeout=30.0
)
async def get_room_members(self, room_id: str) -> List[dict]:
"""Get all members of a room"""
try:
# Use joined_members for efficiency
res = await self._client.get(
f"/_matrix/client/v3/rooms/{room_id}/joined_members"
)
res.raise_for_status()
data = res.json()
# joined_members returns: {"joined": {"@user:domain": {...}}}
joined = data.get("joined", {})
members = []
for user_id, info in joined.items():
# Filter out presence daemon
if user_id == self.daemon_user:
continue
members.append({
"user_id": user_id,
"display_name": info.get("display_name"),
"avatar_url": info.get("avatar_url"),
})
return members
except httpx.HTTPError as e:
logger.error(f"Failed to get room members for {room_id}: {e}")
return []
async def get_room_typing(self, room_id: str) -> List[str]:
"""Get list of currently typing users in a room"""
# Note: Matrix doesn't have a direct API for this
# Typing info comes from /sync, which we'd need to run continuously
# For now, return empty - we'll get typing from sync loop later
return []
async def get_presence(self, user_id: str) -> str:
"""Get presence status for a user"""
try:
res = await self._client.get(
f"/_matrix/client/v3/presence/{user_id}/status"
)
if res.status_code != 200:
return "offline"
data = res.json()
return data.get("presence", "offline")
except httpx.HTTPError:
return "offline"
async def get_presence_batch(self, user_ids: List[str]) -> dict:
"""Get presence for multiple users (with caching)"""
# For efficiency, we could batch these or use sync
# For now, simple sequential calls with error handling
result = {}
for user_id in user_ids:
result[user_id] = await self.get_presence(user_id)
return result
async def join_room(self, room_id_or_alias: str) -> Optional[str]:
"""Join a room and return the room_id"""
try:
res = await self._client.post(
f"/_matrix/client/v3/join/{room_id_or_alias}",
json={}
)
res.raise_for_status()
data = res.json()
return data.get("room_id")
except httpx.HTTPError as e:
logger.error(f"Failed to join room {room_id_or_alias}: {e}")
return None
async def close(self):
await self._client.aclose()