"""Hosted-room JSON-RPC contract. These methods expose durable room identity and an append-only, monotonic room log. They deliberately do not drive Bot turns yet. ``groups.capabilities`` makes that boundary machine-readable so a hosted-aware Desktop cannot mistake the log prototype for a complete gateway-side orchestrator. """ from .method_ctx import HandlerRegistry _registry = HandlerRegistry() method = _registry.method LONG_HANDLERS = frozenset({ "groups.list", "groups.capabilities", "groups.create", "groups.state", "groups.send", "groups.log", "groups.disband", }) @method("groups.capabilities") def _(rid, params: dict) -> dict: """Describe the hosted-room protocol implemented by this gateway.""" from gateway.hosted_rooms import ( MAX_LOG_LIMIT, PROTOCOL_VERSION, local_authority_gateway_id, ) return _ok( rid, { "protocol_version": PROTOCOL_VERSION, "driver": False, "authority_gateway_id": local_authority_gateway_id(), "features": [ "authority_epoch", "coordinator_fencing", "room_identity", "monotonic_log", "idempotent_send", "replayable_disband", "typed_events", "actor_identity", ], "methods": [ "groups.capabilities", "groups.list", "groups.create", "groups.state", "groups.send", "groups.log", "groups.disband", ], "max_log_limit": MAX_LOG_LIMIT, }, ) @method("groups.list") def _(rid, params: dict) -> dict: """List rooms hosted by this gateway.""" try: from gateway.hosted_rooms import ( MAX_ROOM_LIST_LIMIT, default_db_path, list_rooms, ) limit = params.get("limit", MAX_ROOM_LIST_LIMIT) offset = params.get("offset", 0) rooms = list_rooms( default_db_path(), include_disbanded=params.get("include_disbanded") is True, limit=limit, offset=offset, ) return _ok( rid, { "rooms": rooms, "next_offset": offset + limit if len(rooms) == limit else None, }, ) except Exception as exc: return _err(rid, 5110, str(exc)) @method("groups.create") def _(rid, params: dict) -> dict: """Create a hosted room idempotently. Required params: ``room_id``, ``name``, and ``members``. Authority is derived from this gateway's stable install identity, never from the client. """ from gateway.hosted_rooms import ( HostedRoomError, create_room, default_db_path, local_authority_gateway_id, ) try: room = create_room( default_db_path(), room_id=params.get("room_id"), name=params.get("name"), members=params.get("members"), authority_gateway_id=local_authority_gateway_id(), ) return _ok(rid, {"room": room}) except HostedRoomError as exc: reason = getattr(exc, "reason", None) return _err(rid, 4110, str(exc), {"reason": reason} if reason else None) except Exception as exc: return _err(rid, 5111, str(exc)) @method("groups.state") def _(rid, params: dict) -> dict: """Return one hosted room's replay cursor and fenced authority state.""" from gateway.hosted_rooms import HostedRoomError, default_db_path, room_state try: return _ok( rid, { "room": room_state( default_db_path(), room_id=params.get("room_id"), include_disbanded=params.get("include_disbanded") is True, ) }, ) except HostedRoomError as exc: reason = getattr(exc, "reason", None) return _err(rid, 4114, str(exc), {"reason": reason} if reason else None) except Exception as exc: return _err(rid, 5115, str(exc)) @method("groups.send") def _(rid, params: dict) -> dict: """Append one typed event to a hosted room idempotently. Required params: ``room_id``, ``event_id``, and object ``payload``. Only inert ``message.user`` events are accepted through this client-facing method. The actor is server-owned rather than trusted from params. Admission is durable; no Bot turn is started by this slice. """ from gateway.hosted_rooms import ( AuthorityConflictError, HostedRoomError, append_event, default_db_path, local_authority_gateway_id, room_state, user_event_id, ) try: room = room_state(default_db_path(), room_id=params.get("room_id")) local_gateway_id = local_authority_gateway_id() if str(room["authority_gateway_id"]) != local_gateway_id: raise AuthorityConflictError( "This Group Chat is managed by another gateway." ) client_event_id = params.get("event_id") event = append_event( default_db_path(), room_id=params.get("room_id"), event_id=user_event_id(client_event_id), kind="message.user", actor={"kind": "user", "id": "desktop"}, payload=params.get("payload"), authority_gateway_id=local_gateway_id, authority_epoch=int(room["authority_epoch"]), ) return _ok( rid, { "event": event, "client_event_id": client_event_id, "accepted": True, "driver_started": False, }, ) except HostedRoomError as exc: reason = getattr(exc, "reason", None) return _err(rid, 4111, str(exc), {"reason": reason} if reason else None) except Exception as exc: return _err(rid, 5112, str(exc)) @method("groups.disband") def _(rid, params: dict) -> dict: """Permanently tombstone a hosted room id.""" from gateway.hosted_rooms import ( AuthorityConflictError, HostedRoomError, RoomHistoryExpiredError, default_db_path, disband_room, local_authority_gateway_id, room_state, ) try: local_gateway_id = local_authority_gateway_id() try: room = room_state( default_db_path(), room_id=params.get("room_id"), include_disbanded=True, ) except RoomHistoryExpiredError: room = { "authority_gateway_id": local_gateway_id, "authority_epoch": 1, } if str(room["authority_gateway_id"]) != local_gateway_id: raise AuthorityConflictError( "This Group Chat is managed by another gateway." ) tombstone = disband_room( default_db_path(), room_id=params.get("room_id"), expected_gateway_id=local_gateway_id, expected_epoch=int(room["authority_epoch"]), ) return _ok(rid, {"tombstone": tombstone}) except HostedRoomError as exc: reason = getattr(exc, "reason", None) return _err(rid, 4113, str(exc), {"reason": reason} if reason else None) except Exception as exc: return _err(rid, 5114, str(exc)) @method("groups.log") def _(rid, params: dict) -> dict: """Return a monotonic room-log delta after ``since_seq``.""" from gateway.hosted_rooms import HostedRoomError, default_db_path, read_events try: delta = read_events( default_db_path(), room_id=params.get("room_id"), since_seq=params.get("since_seq", 0), limit=params.get("limit", 100), include_disbanded=params.get("include_disbanded") is True, ) return _ok(rid, delta) except HostedRoomError as exc: reason = getattr(exc, "reason", None) return _err(rid, 4112, str(exc), {"reason": reason} if reason else None) except Exception as exc: return _err(rid, 5113, str(exc)) def register(server) -> None: _registry.install(server)