syntax = "proto3"; package chatto.realtime.v1; import "chatto/api/v1/notifications.proto"; import "chatto/api/v1/member_directory.proto"; import "chatto/api/v1/message_types.proto"; import "chatto/api/v1/presence.proto"; import "chatto/api/v1/room_directory.proto"; import "chatto/api/v1/room_timeline.proto"; import "chatto/api/v1/server.proto"; import "chatto/api/v1/server_state.proto"; import "chatto/api/v1/viewer.proto"; import "chatto/api/v1/voice_calls.proto"; import "google/protobuf/timestamp.proto"; // Client-to-server frame for Chatto's protobuf WebSocket realtime protocol. // // Clients send binary protobuf frames to `/api/realtime`. The first frame must // be `hello`; after the server replies with `hello`, clients send // `subscribe_events` to start the authenticated server-projection stream. message RealtimeClientFrame { reserved "ack"; oneof frame { // Opens the protocol session and optionally carries a bearer token. RealtimeClientHello hello = 1; // Starts the caller's authorized server-projection stream. RealtimeSubscribeEvents subscribe_events = 2; // Application-level ping. The server replies with `pong`. RealtimePing ping = 3; // Adds one joined room's recent timeline to the retained projection. RealtimeHydrateRoom hydrate_room = 4; } } // Server-to-client frame for Chatto's protobuf WebSocket realtime protocol. message RealtimeServerFrame { oneof frame { // Confirms protocol version, server version, and capabilities. RealtimeServerHello hello = 1; // Confirms the server-projection stream has started. RealtimeSubscribed subscribed = 2; // An authorized transient, non-replayable event. RealtimeEventEnvelope event = 3; // Application-level heartbeat used for liveness checks. RealtimeHeartbeat heartbeat = 4; // Protocol or authorization error. RealtimeError error = 5; // Server-requested close with reconnect guidance. RealtimeClose close = 6; // Reply to a client ping. RealtimePong pong = 7; // Confirms that durable replay reached the live-stream boundary. RealtimeCaughtUp caught_up = 8; // One idempotent mutation of the caller's server projection. RealtimeProjectionEvent projection_event = 9; } } // Initial client hello. message RealtimeClientHello { reserved 3; reserved "resume_cursor"; // Protocol version requested by the client. The only supported version is 2. uint32 protocol_version = 1; // Optional bearer token. When present, it takes precedence over cookie auth. optional string bearer_token = 2; } // Initial server hello. message RealtimeServerHello { reserved 3; reserved "resume_supported"; // Protocol version accepted by the server. uint32 protocol_version = 1; // Chatto server software version. string server_version = 2; // Approximate heartbeat interval clients should expect. uint32 heartbeat_interval_seconds = 4; // Stable protocol capability keys supported by this server. Current keys: // `chatto.realtime.events.live.v1`, `chatto.realtime.heartbeat.v1`, // `chatto.realtime.ping.v1`, `chatto.realtime.events.resume.v1`, and // `chatto.realtime.projection.v1`. repeated string capabilities = 5; } // Request to start the caller's authorized server-projection stream. message RealtimeSubscribeEvents { // Opaque cursor from a previously received durable event or caught-up // frame. The server replays authorized durable projection changes after // this cursor before continuing with live delivery. Omit it to receive a // compacted reset of the current authorised server projection. Cursors // expire 24 hours after issue; an expired cursor also receives a compacted // reset rather than historical replay. optional string resume_cursor = 1; // Joined rooms whose timeline windows the client already retains. A fresh // compacted projection includes only these timelines, and resumed delivery // emits timeline mutations only for these rooms. Lightweight room, unread, // notification, and call state remains server-wide. At most 64 room IDs // may be supplied in one subscription. repeated string retained_room_ids = 2; } // Requests lazy materialisation of one joined room's recent timeline. // // The server replies on the same ordered projection stream with a // `room_timeline_replace` operation. Once hydrated, later timeline mutations // for the room are included for the lifetime of this connection. Clients send // all still-retained room IDs again in their next subscription. A connection // may retain at most 64 distinct room IDs; excess requests receive a // non-fatal `too_many_retained_rooms` error. message RealtimeHydrateRoom { // Joined room to materialise. Repeating a retained room is idempotent. string room_id = 1; } // Confirms projection streaming has started. message RealtimeSubscribed { reserved 1; reserved "cursor"; // Opaque cursor from which this subscription starts. When the request // supplied a resume cursor, this is that validated cursor. Otherwise it is // the current live boundary. optional string start_cursor = 2; } // Confirms that durable replay is complete and live delivery has begun. // // Clients can retain `cursor` after applying every preceding event. A cursor // must not outlive the projection state it describes. Events after this frame // are live. Transient events are never replayed. A cursor is usable for up to // 24 hours after issue and should be replaced by each later caught-up cursor. message RealtimeCaughtUp { // Opaque cursor at the replay-to-live handoff boundary. string cursor = 1; } // One ordered, idempotent mutation of the caller's authorised server view. // // A fresh subscription starts with a reset operation followed by current // resource upserts. A resumed subscription derives operations from EVT after // the supplied cursor. Both paths use this envelope and the same client // reducer. The stream is a convergence feed, not an immutable audit log: // replay uses current authorisation, deletion, and erasure state. message RealtimeProjectionEvent { // Stable event ID. Synthetic compacted-replay events use server-generated IDs. string id = 1; // Time the source fact occurred or the compacted event was generated. google.protobuf.Timestamp created_at = 2; // User or system actor that caused the source fact, when known. optional string actor_id = 3; // Opaque cursor safe to retain with the resulting projection state after // applying every operation in this event. optional string resume_cursor = 4; // Operations applied atomically and in order by the client reducer. repeated RealtimeProjectionOperation operations = 5; } // One mutation of a server-scoped client projection. message RealtimeProjectionOperation { reserved 13; reserved "notifications_replace"; oneof operation { // Clears all canonical server projection state before compacted replay. RealtimeProjectionReset reset = 1; // Replaces the public server profile. chatto.api.v1.ServerPublicProfile server_upsert = 2; // Replaces the authenticated viewer resource. chatto.api.v1.GetViewerResponse viewer_upsert = 3; // Adds or replaces one server directory member. chatto.api.v1.DirectoryMember user_upsert = 4; // Removes one server directory member. RealtimeProjectionUserRemove user_remove = 5; // Adds or replaces one room and its viewer state. Channel membership is // complete only after that room's timeline has been materialised. RealtimeProjectionRoom room_upsert = 6; // Removes one room and all locally retained room state. RealtimeProjectionRoomRemove room_remove = 7; // Replaces the complete visible room-group layout. RealtimeProjectionRoomGroupsReplace room_groups_replace = 8; // Replaces the lazily retained recent timeline window for one joined room. RealtimeProjectionRoomTimelineReplace room_timeline_replace = 9; // Adds or replaces one renderable event in a retained room timeline. RealtimeProjectionRoomTimelineEventUpsert room_timeline_event_upsert = 10; // Replaces authenticated server-wide presentation and runtime state. RealtimeProjectionServerState server_state_upsert = 11; // Removes one event from a retained room timeline. This is distinct from // a message tombstone: projection-only channel echoes disappear when the // underlying thread reply is no longer echoed into the room. RealtimeProjectionRoomTimelineEventRemove room_timeline_event_remove = 12; // Replaces one room's current viewer state without // retransmitting room metadata or membership. RealtimeProjectionRoomViewerStateReplace room_viewer_state_replace = 14; // Replaces every active call visible to the caller. RealtimeProjectionActiveCallsReplace active_calls_replace = 15; // Replaces the latest presence status for every server member. Presence // is transient and not replayed from EVT, so every subscription emits // this reconciliation before caught_up. RealtimeProjectionPresencesReplace presences_replace = 16; // Replaces viewer-specific follow and unread state for every followed // thread. Missing entries authoritatively mean not followed and not // unread for retained thread roots. RealtimeProjectionThreadViewerStatesReplace thread_viewer_states_replace = 17; // Signals root-message activity for lightweight room ordering even when // that room's timeline is not retained. RealtimeProjectionRoomActivity room_activity = 18; // Replaces the viewer's current notification-occurrence page and exact // unread occurrence counts. This uses a new top-level operation tag rather // than reinterpreting the released Notifications 1.0 replacement. RealtimeProjectionNotificationOccurrencesReplace notification_occurrences_replace = 19; } } // Reset marker for a compacted projection replay. message RealtimeProjectionReset {} // Authenticated server state required by every signed-in client. message RealtimeProjectionServerState { // Optional message of the day shown to authenticated members. optional string motd = 1; // Runtime capabilities and limits advertised to the client. chatto.api.v1.ServerRuntimeConfig runtime = 2; // Live pin transition that caused this replacement, when applicable. // Bootstrap and replay reconciliation omit this field. optional RealtimeProjectionPinnedMessageChange pinned_message_change = 3; } // Content-free pinned-message transition used to refresh room pin stores and // drive the client-local unseen indicator. message RealtimeProjectionPinnedMessageChange { RealtimeProjectionPinnedMessageAction action = 1; string room_id = 2; string message_event_id = 3; } enum RealtimeProjectionPinnedMessageAction { REALTIME_PROJECTION_PINNED_MESSAGE_ACTION_UNSPECIFIED = 0; REALTIME_PROJECTION_PINNED_MESSAGE_ACTION_CREATED = 1; REALTIME_PROJECTION_PINNED_MESSAGE_ACTION_DELETED = 2; } // Canonical lightweight room state retained by a server-scoped projection. message RealtimeProjectionRoom { // Public room metadata and viewer-specific state. chatto.api.v1.RoomWithViewerState room = 1; // Current room membership expressed as references into the projection's // server-wide user directory. DM participants are always present. Channel // membership is empty in a cold room summary and complete after that room's // timeline has been materialised. repeated string member_user_ids = 2; reserved 3; reserved "viewer_notification_count"; // Whether this DM room has ever received a root message. False means the // durable room exists only so its initiator can compose the first message. // Once true, message deletion does not reset it. Absent for channel rooms // and servers that predate this field. optional bool has_message_history = 4; } // Root-message activity that affects lightweight room navigation state. // // This deliberately carries no message content. Retained rooms receive the // corresponding timeline mutation separately; unretained rooms still use this // operation to keep DM ordering current without materialising their timeline. message RealtimeProjectionRoomActivity { string room_id = 1; } // Removes one user from the client projection. Reducers must also purge copied // render data for this user from retained rooms, timelines, notifications, and // calls while preserving stable IDs on historical facts. message RealtimeProjectionUserRemove { // Stable user ID to remove. string user_id = 1; } // Complete latest-value presence state for the projected user directory. // Missing map entries are offline; the server normally includes every member // explicitly so reducers can converge without retaining an older value. message RealtimeProjectionPresencesReplace { map statuses = 1; } // Complete current viewer state for followed threads. This reconciles durable // follow changes and RUNTIME_STATE read markers that may change while a client // is dormant without retransmitting room timelines. message RealtimeProjectionThreadViewerStatesReplace { repeated RealtimeProjectionThreadViewerState states = 1; } // Viewer-specific state for one followed thread root. message RealtimeProjectionThreadViewerState { string room_id = 1; string thread_root_event_id = 2; chatto.api.v1.ThreadViewerState viewer_state = 3; } // Removes one room from the client projection. message RealtimeProjectionRoomRemove { // Stable room ID to remove. string room_id = 1; } // Complete ordered room-group layout visible to the caller. message RealtimeProjectionRoomGroupsReplace { // Visible room groups in sidebar order. repeated chatto.api.v1.RoomGroup groups = 1; } // Complete retained recent timeline window for one room. message RealtimeProjectionRoomTimelineReplace { // Room whose retained window is replaced. string room_id = 1; // Renderable current timeline state. Deleted and crypto-shredded message // bodies are absent and represented by their normal tombstone fields. chatto.api.v1.RoomTimelinePage page = 2; // Opaque cursor for every retained row, keyed by timeline event ID. map event_cursors = 3; } // Current renderable state for one timeline event. message RealtimeProjectionRoomTimelineEventUpsert { // Room containing the event. string room_id = 1; // Current renderable event state. chatto.api.v1.RoomTimelineEvent event = 2; // Related users needed to render this event. chatto.api.v1.RoomTimelineIncludes includes = 3; // Reaction transition that caused this upsert, when applicable. optional RealtimeProjectionReactionChange reaction_change = 4; // Preserve a deleted echo row as a tombstone. Directly deleted echoes use a // remove operation instead; this flag identifies canonical-reply deletion. bool retain_deleted_row = 5; // Opaque room-timeline cursor for this canonical row. Clients use retained // row cursors to advance a bounded window without a separate refresh read. string event_cursor = 6; } // Removes one projection-only row from a retained room timeline. message RealtimeProjectionRoomTimelineEventRemove { // Room containing the retained row. string room_id = 1; // Stable event ID of the row to remove. string event_id = 2; } // Finite current notification state emitted on bootstrap and every resume. message RealtimeProjectionNotificationOccurrencesReplace { // True only when this live replacement should produce a one-shot local // notification sound. Bootstrap and reconciliation replacements are false. bool play_notification_sound = 1; // Authoritative Notifications 2.0 occurrences and exact unread count. chatto.api.v1.ListNotificationOccurrencesResponse occurrences = 2; } // Lightweight current viewer state for one projected room. message RealtimeProjectionRoomViewerStateReplace { string room_id = 1; chatto.api.v1.RoomViewerState viewer_state = 2; } // Finite current active-call state visible to the caller. message RealtimeProjectionActiveCallsReplace { repeated chatto.api.v1.ActiveCall calls = 1; } // Reaction transition retained for integrations while the containing timeline // row carries the authoritative aggregate reaction state. message RealtimeProjectionReactionChange { // Whether the actor added or removed the reaction. RealtimeProjectionReactionAction action = 1; // Canonical reacted-to message event ID. string message_event_id = 2; // Reaction emoji. string emoji = 3; // User who changed the reaction. string user_id = 4; } // Kind of reaction transition. enum RealtimeProjectionReactionAction { // Unknown action. REALTIME_PROJECTION_REACTION_ACTION_UNSPECIFIED = 0; // A reaction was added. REALTIME_PROJECTION_REACTION_ACTION_ADDED = 1; // A reaction was removed. REALTIME_PROJECTION_REACTION_ACTION_REMOVED = 2; } // Application-level ping. message RealtimePing { // Client-chosen opaque value echoed in the pong. string nonce = 1; } // Application-level pong. message RealtimePong { // Echo of the ping nonce. string nonce = 1; } // Application-level heartbeat. message RealtimeHeartbeat { // Stable event ID for this heartbeat. string id = 1; // Time the heartbeat was emitted. google.protobuf.Timestamp created_at = 2; } // Protocol error. message RealtimeError { // Stable machine-readable error code. string code = 1; // Human-readable diagnostic message. string message = 2; // True when the server will close the socket after sending this error. bool fatal = 3; // Suggested retry delay for a non-fatal rejected request. Omitted when the // request should not be retried automatically. optional uint32 retry_after_ms = 4; // Room associated with a room-hydration error. Omitted for errors that are // not caused by `hydrate_room`. optional string room_id = 5; } // Close instruction sent before the socket is closed when possible. message RealtimeClose { // Stable machine-readable close code. string code = 1; // Human-readable diagnostic message. string message = 2; // True when the client should reconnect after a delay. bool reconnect = 3; // Suggested reconnect delay in milliseconds. uint32 retry_after_ms = 4; } // One authorized transient event delivered over the realtime WebSocket. // // These events have no resume cursor and are never replayed. Durable state is // delivered only as RealtimeProjectionEvent operations. message RealtimeEventEnvelope { reserved 4, 5, 10 to 12, 20 to 21, 40 to 47, 60 to 76, 80 to 89; reserved "cursor", "resume_cursor", "message_posted", "message_edited", "message_retracted", "reaction_added", "reaction_removed", "room_created", "room_updated", "room_deleted", "room_archived", "room_unarchived", "user_joined_room", "user_left_room", "room_universal_changed", "thread_created", "user_custom_status_set", "user_custom_status_cleared", "asset_processing_started", "asset_processing_succeeded", "asset_processing_failed", "asset_deleted", "notification_created", "notification_dismissed", "notification_level_changed", "thread_follow_changed", "room_marked_as_read", "server_updated", "user_profile_updated", "server_user_preferences_updated", "room_groups_updated", "server_member_deleted", "call_started", "call_participant_joined", "call_participant_left", "call_ended", "mention_notification", "new_direct_message_notification"; // Stable event ID. string id = 1; // Time the event was created. google.protobuf.Timestamp created_at = 2; // User or system actor that caused the event, when known. optional string actor_id = 3; oneof event { // A user is typing in a room or thread that the viewer can read. RealtimeTypingEvent user_typing = 30; // A user's live presence changed. RealtimePresenceChangedEvent presence_changed = 31; // The current user's session was terminated. RealtimeSessionTerminatedEvent session_terminated = 90; } } // Typing signal. // // This is an ephemeral signal. The viewer must be a room member. Main-room // typing requires `message.read`. Thread typing requires `message.read` or a // matching thread relationship with `message.read-interactions`. DM membership // authorizes DM typing delivery. `room_id` and `thread_root_event_id` identify // where to display typing state; clients normally do not hydrate it. message RealtimeTypingEvent { // Room where the actor is typing. string room_id = 1; // Thread root event ID when the actor is typing in a thread. optional string thread_root_event_id = 2; } // Presence-changed signal. // // The latest presence status is inline. Use `UserService.GetUser` when // the surrounding user profile or custom status also needs refreshing. message RealtimePresenceChangedEvent { // User whose presence changed. string user_id = 1; // Latest presence status. chatto.api.v1.PresenceStatus status = 2; } // Session-terminated signal for the connected user's current session. message RealtimeSessionTerminatedEvent { // Termination reason. string reason = 1; }