- 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
95 lines
3.4 KiB
Python
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()
|
|
|