from __future__ import annotations from typing import Dict, Any from sanic import response from sqlalchemy import select from app.core.logger import make_log from app.core.models import NodeEvent, KnownNode from app.core.network.nodesig import verify_request from app.core.network.guard import check_rate_limit from app.core._config import PROJECT_HOST from app.core.events.service import LOCAL_PUBLIC_KEY def _origin_host() -> str | None: return PROJECT_HOST.rstrip('/') if PROJECT_HOST else None async def s_api_v1_network_events(request): remote_ip = (request.headers.get('X-Forwarded-For') or request.remote_addr or request.ip or '').split(',')[0].strip() if not check_rate_limit(request.app.ctx.memory, remote_ip): return response.json({"error": "RATE_LIMIT"}, status=429) ok, node_id, reason = verify_request(request, request.app.ctx.memory) if not ok: return response.json({"error": reason or "UNAUTHORIZED"}, status=401) session = request.ctx.db_session trusted = (await session.execute( select(KnownNode).where(KnownNode.public_key == node_id) )).scalar_one_or_none() role = (trusted.meta or {}).get('role') if trusted and trusted.meta else None if role != 'trusted': make_log("Events", f"Rejected events fetch from non-trusted node {node_id}", level="warning") return response.json({"error": "FORBIDDEN"}, status=403) try: since = int(request.args.get('since') or 0) except (TypeError, ValueError): since = 0 since = max(since, 0) try: limit = int(request.args.get('limit') or 100) except (TypeError, ValueError): limit = 100 limit = max(1, min(limit, 200)) result = await session.execute( select(NodeEvent) .where(NodeEvent.origin_public_key == LOCAL_PUBLIC_KEY, NodeEvent.seq > since) .order_by(NodeEvent.seq.asc()) .limit(limit) ) rows = result.scalars().all() events: list[Dict[str, Any]] = [] next_since = since for row in rows: next_since = max(next_since, int(row.seq)) events.append({ "origin_public_key": row.origin_public_key, "origin_host": row.origin_host or _origin_host(), "seq": int(row.seq), "uid": row.uid, "event_type": row.event_type, "payload": row.payload, "signature": row.signature, "created_at": (row.created_at.isoformat() + 'Z') if row.created_at else None, }) payload = { "events": events, "next_since": next_since, } return response.json(payload)