realtime.proto 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. syntax = "proto3";
  2. package chatto.realtime.v1;
  3. import "chatto/api/v1/notifications.proto";
  4. import "chatto/api/v1/member_directory.proto";
  5. import "chatto/api/v1/message_types.proto";
  6. import "chatto/api/v1/presence.proto";
  7. import "chatto/api/v1/room_directory.proto";
  8. import "chatto/api/v1/room_timeline.proto";
  9. import "chatto/api/v1/server.proto";
  10. import "chatto/api/v1/server_state.proto";
  11. import "chatto/api/v1/viewer.proto";
  12. import "chatto/api/v1/voice_calls.proto";
  13. import "google/protobuf/timestamp.proto";
  14. // Client-to-server frame for Chatto's protobuf WebSocket realtime protocol.
  15. //
  16. // Clients send binary protobuf frames to `/api/realtime`. The first frame must
  17. // be `hello`; after the server replies with `hello`, clients send
  18. // `subscribe_events` to start the authenticated server-projection stream.
  19. message RealtimeClientFrame {
  20. reserved "ack";
  21. oneof frame {
  22. // Opens the protocol session and optionally carries a bearer token.
  23. RealtimeClientHello hello = 1;
  24. // Starts the caller's authorized server-projection stream.
  25. RealtimeSubscribeEvents subscribe_events = 2;
  26. // Application-level ping. The server replies with `pong`.
  27. RealtimePing ping = 3;
  28. // Adds one joined room's recent timeline to the retained projection.
  29. RealtimeHydrateRoom hydrate_room = 4;
  30. }
  31. }
  32. // Server-to-client frame for Chatto's protobuf WebSocket realtime protocol.
  33. message RealtimeServerFrame {
  34. oneof frame {
  35. // Confirms protocol version, server version, and capabilities.
  36. RealtimeServerHello hello = 1;
  37. // Confirms the server-projection stream has started.
  38. RealtimeSubscribed subscribed = 2;
  39. // An authorized transient, non-replayable event.
  40. RealtimeEventEnvelope event = 3;
  41. // Application-level heartbeat used for liveness checks.
  42. RealtimeHeartbeat heartbeat = 4;
  43. // Protocol or authorization error.
  44. RealtimeError error = 5;
  45. // Server-requested close with reconnect guidance.
  46. RealtimeClose close = 6;
  47. // Reply to a client ping.
  48. RealtimePong pong = 7;
  49. // Confirms that durable replay reached the live-stream boundary.
  50. RealtimeCaughtUp caught_up = 8;
  51. // One idempotent mutation of the caller's server projection.
  52. RealtimeProjectionEvent projection_event = 9;
  53. }
  54. }
  55. // Initial client hello.
  56. message RealtimeClientHello {
  57. reserved 3;
  58. reserved "resume_cursor";
  59. // Protocol version requested by the client. The only supported version is 2.
  60. uint32 protocol_version = 1;
  61. // Optional bearer token. When present, it takes precedence over cookie auth.
  62. optional string bearer_token = 2;
  63. }
  64. // Initial server hello.
  65. message RealtimeServerHello {
  66. reserved 3;
  67. reserved "resume_supported";
  68. // Protocol version accepted by the server.
  69. uint32 protocol_version = 1;
  70. // Chatto server software version.
  71. string server_version = 2;
  72. // Approximate heartbeat interval clients should expect.
  73. uint32 heartbeat_interval_seconds = 4;
  74. // Stable protocol capability keys supported by this server. Current keys:
  75. // `chatto.realtime.events.live.v1`, `chatto.realtime.heartbeat.v1`,
  76. // `chatto.realtime.ping.v1`, `chatto.realtime.events.resume.v1`, and
  77. // `chatto.realtime.projection.v1`.
  78. repeated string capabilities = 5;
  79. }
  80. // Request to start the caller's authorized server-projection stream.
  81. message RealtimeSubscribeEvents {
  82. // Opaque cursor from a previously received durable event or caught-up
  83. // frame. The server replays authorized durable projection changes after
  84. // this cursor before continuing with live delivery. Omit it to receive a
  85. // compacted reset of the current authorised server projection. Cursors
  86. // expire 24 hours after issue; an expired cursor also receives a compacted
  87. // reset rather than historical replay.
  88. optional string resume_cursor = 1;
  89. // Joined rooms whose timeline windows the client already retains. A fresh
  90. // compacted projection includes only these timelines, and resumed delivery
  91. // emits timeline mutations only for these rooms. Lightweight room, unread,
  92. // notification, and call state remains server-wide. At most 64 room IDs
  93. // may be supplied in one subscription.
  94. repeated string retained_room_ids = 2;
  95. }
  96. // Requests lazy materialisation of one joined room's recent timeline.
  97. //
  98. // The server replies on the same ordered projection stream with a
  99. // `room_timeline_replace` operation. Once hydrated, later timeline mutations
  100. // for the room are included for the lifetime of this connection. Clients send
  101. // all still-retained room IDs again in their next subscription. A connection
  102. // may retain at most 64 distinct room IDs; excess requests receive a
  103. // non-fatal `too_many_retained_rooms` error.
  104. message RealtimeHydrateRoom {
  105. // Joined room to materialise. Repeating a retained room is idempotent.
  106. string room_id = 1;
  107. }
  108. // Confirms projection streaming has started.
  109. message RealtimeSubscribed {
  110. reserved 1;
  111. reserved "cursor";
  112. // Opaque cursor from which this subscription starts. When the request
  113. // supplied a resume cursor, this is that validated cursor. Otherwise it is
  114. // the current live boundary.
  115. optional string start_cursor = 2;
  116. }
  117. // Confirms that durable replay is complete and live delivery has begun.
  118. //
  119. // Clients can retain `cursor` after applying every preceding event. A cursor
  120. // must not outlive the projection state it describes. Events after this frame
  121. // are live. Transient events are never replayed. A cursor is usable for up to
  122. // 24 hours after issue and should be replaced by each later caught-up cursor.
  123. message RealtimeCaughtUp {
  124. // Opaque cursor at the replay-to-live handoff boundary.
  125. string cursor = 1;
  126. }
  127. // One ordered, idempotent mutation of the caller's authorised server view.
  128. //
  129. // A fresh subscription starts with a reset operation followed by current
  130. // resource upserts. A resumed subscription derives operations from EVT after
  131. // the supplied cursor. Both paths use this envelope and the same client
  132. // reducer. The stream is a convergence feed, not an immutable audit log:
  133. // replay uses current authorisation, deletion, and erasure state.
  134. message RealtimeProjectionEvent {
  135. // Stable event ID. Synthetic compacted-replay events use server-generated IDs.
  136. string id = 1;
  137. // Time the source fact occurred or the compacted event was generated.
  138. google.protobuf.Timestamp created_at = 2;
  139. // User or system actor that caused the source fact, when known.
  140. optional string actor_id = 3;
  141. // Opaque cursor safe to retain with the resulting projection state after
  142. // applying every operation in this event.
  143. optional string resume_cursor = 4;
  144. // Operations applied atomically and in order by the client reducer.
  145. repeated RealtimeProjectionOperation operations = 5;
  146. }
  147. // One mutation of a server-scoped client projection.
  148. message RealtimeProjectionOperation {
  149. reserved 13;
  150. reserved "notifications_replace";
  151. oneof operation {
  152. // Clears all canonical server projection state before compacted replay.
  153. RealtimeProjectionReset reset = 1;
  154. // Replaces the public server profile.
  155. chatto.api.v1.ServerPublicProfile server_upsert = 2;
  156. // Replaces the authenticated viewer resource.
  157. chatto.api.v1.GetViewerResponse viewer_upsert = 3;
  158. // Adds or replaces one server directory member.
  159. chatto.api.v1.DirectoryMember user_upsert = 4;
  160. // Removes one server directory member.
  161. RealtimeProjectionUserRemove user_remove = 5;
  162. // Adds or replaces one room and its viewer state. Channel membership is
  163. // complete only after that room's timeline has been materialised.
  164. RealtimeProjectionRoom room_upsert = 6;
  165. // Removes one room and all locally retained room state.
  166. RealtimeProjectionRoomRemove room_remove = 7;
  167. // Replaces the complete visible room-group layout.
  168. RealtimeProjectionRoomGroupsReplace room_groups_replace = 8;
  169. // Replaces the lazily retained recent timeline window for one joined room.
  170. RealtimeProjectionRoomTimelineReplace room_timeline_replace = 9;
  171. // Adds or replaces one renderable event in a retained room timeline.
  172. RealtimeProjectionRoomTimelineEventUpsert room_timeline_event_upsert = 10;
  173. // Replaces authenticated server-wide presentation and runtime state.
  174. RealtimeProjectionServerState server_state_upsert = 11;
  175. // Removes one event from a retained room timeline. This is distinct from
  176. // a message tombstone: projection-only channel echoes disappear when the
  177. // underlying thread reply is no longer echoed into the room.
  178. RealtimeProjectionRoomTimelineEventRemove room_timeline_event_remove = 12;
  179. // Replaces one room's current viewer state without
  180. // retransmitting room metadata or membership.
  181. RealtimeProjectionRoomViewerStateReplace room_viewer_state_replace = 14;
  182. // Replaces every active call visible to the caller.
  183. RealtimeProjectionActiveCallsReplace active_calls_replace = 15;
  184. // Replaces the latest presence status for every server member. Presence
  185. // is transient and not replayed from EVT, so every subscription emits
  186. // this reconciliation before caught_up.
  187. RealtimeProjectionPresencesReplace presences_replace = 16;
  188. // Replaces viewer-specific follow and unread state for every followed
  189. // thread. Missing entries authoritatively mean not followed and not
  190. // unread for retained thread roots.
  191. RealtimeProjectionThreadViewerStatesReplace thread_viewer_states_replace = 17;
  192. // Signals root-message activity for lightweight room ordering even when
  193. // that room's timeline is not retained.
  194. RealtimeProjectionRoomActivity room_activity = 18;
  195. // Replaces the viewer's current notification-occurrence page and exact
  196. // unread occurrence counts. This uses a new top-level operation tag rather
  197. // than reinterpreting the released Notifications 1.0 replacement.
  198. RealtimeProjectionNotificationOccurrencesReplace notification_occurrences_replace = 19;
  199. }
  200. }
  201. // Reset marker for a compacted projection replay.
  202. message RealtimeProjectionReset {}
  203. // Authenticated server state required by every signed-in client.
  204. message RealtimeProjectionServerState {
  205. // Optional message of the day shown to authenticated members.
  206. optional string motd = 1;
  207. // Runtime capabilities and limits advertised to the client.
  208. chatto.api.v1.ServerRuntimeConfig runtime = 2;
  209. // Live pin transition that caused this replacement, when applicable.
  210. // Bootstrap and replay reconciliation omit this field.
  211. optional RealtimeProjectionPinnedMessageChange pinned_message_change = 3;
  212. }
  213. // Content-free pinned-message transition used to refresh room pin stores and
  214. // drive the client-local unseen indicator.
  215. message RealtimeProjectionPinnedMessageChange {
  216. RealtimeProjectionPinnedMessageAction action = 1;
  217. string room_id = 2;
  218. string message_event_id = 3;
  219. }
  220. enum RealtimeProjectionPinnedMessageAction {
  221. REALTIME_PROJECTION_PINNED_MESSAGE_ACTION_UNSPECIFIED = 0;
  222. REALTIME_PROJECTION_PINNED_MESSAGE_ACTION_CREATED = 1;
  223. REALTIME_PROJECTION_PINNED_MESSAGE_ACTION_DELETED = 2;
  224. }
  225. // Canonical lightweight room state retained by a server-scoped projection.
  226. message RealtimeProjectionRoom {
  227. // Public room metadata and viewer-specific state.
  228. chatto.api.v1.RoomWithViewerState room = 1;
  229. // Current room membership expressed as references into the projection's
  230. // server-wide user directory. DM participants are always present. Channel
  231. // membership is empty in a cold room summary and complete after that room's
  232. // timeline has been materialised.
  233. repeated string member_user_ids = 2;
  234. reserved 3;
  235. reserved "viewer_notification_count";
  236. // Whether this DM room has ever received a root message. False means the
  237. // durable room exists only so its initiator can compose the first message.
  238. // Once true, message deletion does not reset it. Absent for channel rooms
  239. // and servers that predate this field.
  240. optional bool has_message_history = 4;
  241. }
  242. // Root-message activity that affects lightweight room navigation state.
  243. //
  244. // This deliberately carries no message content. Retained rooms receive the
  245. // corresponding timeline mutation separately; unretained rooms still use this
  246. // operation to keep DM ordering current without materialising their timeline.
  247. message RealtimeProjectionRoomActivity {
  248. string room_id = 1;
  249. }
  250. // Removes one user from the client projection. Reducers must also purge copied
  251. // render data for this user from retained rooms, timelines, notifications, and
  252. // calls while preserving stable IDs on historical facts.
  253. message RealtimeProjectionUserRemove {
  254. // Stable user ID to remove.
  255. string user_id = 1;
  256. }
  257. // Complete latest-value presence state for the projected user directory.
  258. // Missing map entries are offline; the server normally includes every member
  259. // explicitly so reducers can converge without retaining an older value.
  260. message RealtimeProjectionPresencesReplace {
  261. map<string, chatto.api.v1.PresenceStatus> statuses = 1;
  262. }
  263. // Complete current viewer state for followed threads. This reconciles durable
  264. // follow changes and RUNTIME_STATE read markers that may change while a client
  265. // is dormant without retransmitting room timelines.
  266. message RealtimeProjectionThreadViewerStatesReplace {
  267. repeated RealtimeProjectionThreadViewerState states = 1;
  268. }
  269. // Viewer-specific state for one followed thread root.
  270. message RealtimeProjectionThreadViewerState {
  271. string room_id = 1;
  272. string thread_root_event_id = 2;
  273. chatto.api.v1.ThreadViewerState viewer_state = 3;
  274. }
  275. // Removes one room from the client projection.
  276. message RealtimeProjectionRoomRemove {
  277. // Stable room ID to remove.
  278. string room_id = 1;
  279. }
  280. // Complete ordered room-group layout visible to the caller.
  281. message RealtimeProjectionRoomGroupsReplace {
  282. // Visible room groups in sidebar order.
  283. repeated chatto.api.v1.RoomGroup groups = 1;
  284. }
  285. // Complete retained recent timeline window for one room.
  286. message RealtimeProjectionRoomTimelineReplace {
  287. // Room whose retained window is replaced.
  288. string room_id = 1;
  289. // Renderable current timeline state. Deleted and crypto-shredded message
  290. // bodies are absent and represented by their normal tombstone fields.
  291. chatto.api.v1.RoomTimelinePage page = 2;
  292. // Opaque cursor for every retained row, keyed by timeline event ID.
  293. map<string, string> event_cursors = 3;
  294. }
  295. // Current renderable state for one timeline event.
  296. message RealtimeProjectionRoomTimelineEventUpsert {
  297. // Room containing the event.
  298. string room_id = 1;
  299. // Current renderable event state.
  300. chatto.api.v1.RoomTimelineEvent event = 2;
  301. // Related users needed to render this event.
  302. chatto.api.v1.RoomTimelineIncludes includes = 3;
  303. // Reaction transition that caused this upsert, when applicable.
  304. optional RealtimeProjectionReactionChange reaction_change = 4;
  305. // Preserve a deleted echo row as a tombstone. Directly deleted echoes use a
  306. // remove operation instead; this flag identifies canonical-reply deletion.
  307. bool retain_deleted_row = 5;
  308. // Opaque room-timeline cursor for this canonical row. Clients use retained
  309. // row cursors to advance a bounded window without a separate refresh read.
  310. string event_cursor = 6;
  311. }
  312. // Removes one projection-only row from a retained room timeline.
  313. message RealtimeProjectionRoomTimelineEventRemove {
  314. // Room containing the retained row.
  315. string room_id = 1;
  316. // Stable event ID of the row to remove.
  317. string event_id = 2;
  318. }
  319. // Finite current notification state emitted on bootstrap and every resume.
  320. message RealtimeProjectionNotificationOccurrencesReplace {
  321. // True only when this live replacement should produce a one-shot local
  322. // notification sound. Bootstrap and reconciliation replacements are false.
  323. bool play_notification_sound = 1;
  324. // Authoritative Notifications 2.0 occurrences and exact unread count.
  325. chatto.api.v1.ListNotificationOccurrencesResponse occurrences = 2;
  326. }
  327. // Lightweight current viewer state for one projected room.
  328. message RealtimeProjectionRoomViewerStateReplace {
  329. string room_id = 1;
  330. chatto.api.v1.RoomViewerState viewer_state = 2;
  331. }
  332. // Finite current active-call state visible to the caller.
  333. message RealtimeProjectionActiveCallsReplace {
  334. repeated chatto.api.v1.ActiveCall calls = 1;
  335. }
  336. // Reaction transition retained for integrations while the containing timeline
  337. // row carries the authoritative aggregate reaction state.
  338. message RealtimeProjectionReactionChange {
  339. // Whether the actor added or removed the reaction.
  340. RealtimeProjectionReactionAction action = 1;
  341. // Canonical reacted-to message event ID.
  342. string message_event_id = 2;
  343. // Reaction emoji.
  344. string emoji = 3;
  345. // User who changed the reaction.
  346. string user_id = 4;
  347. }
  348. // Kind of reaction transition.
  349. enum RealtimeProjectionReactionAction {
  350. // Unknown action.
  351. REALTIME_PROJECTION_REACTION_ACTION_UNSPECIFIED = 0;
  352. // A reaction was added.
  353. REALTIME_PROJECTION_REACTION_ACTION_ADDED = 1;
  354. // A reaction was removed.
  355. REALTIME_PROJECTION_REACTION_ACTION_REMOVED = 2;
  356. }
  357. // Application-level ping.
  358. message RealtimePing {
  359. // Client-chosen opaque value echoed in the pong.
  360. string nonce = 1;
  361. }
  362. // Application-level pong.
  363. message RealtimePong {
  364. // Echo of the ping nonce.
  365. string nonce = 1;
  366. }
  367. // Application-level heartbeat.
  368. message RealtimeHeartbeat {
  369. // Stable event ID for this heartbeat.
  370. string id = 1;
  371. // Time the heartbeat was emitted.
  372. google.protobuf.Timestamp created_at = 2;
  373. }
  374. // Protocol error.
  375. message RealtimeError {
  376. // Stable machine-readable error code.
  377. string code = 1;
  378. // Human-readable diagnostic message.
  379. string message = 2;
  380. // True when the server will close the socket after sending this error.
  381. bool fatal = 3;
  382. // Suggested retry delay for a non-fatal rejected request. Omitted when the
  383. // request should not be retried automatically.
  384. optional uint32 retry_after_ms = 4;
  385. // Room associated with a room-hydration error. Omitted for errors that are
  386. // not caused by `hydrate_room`.
  387. optional string room_id = 5;
  388. }
  389. // Close instruction sent before the socket is closed when possible.
  390. message RealtimeClose {
  391. // Stable machine-readable close code.
  392. string code = 1;
  393. // Human-readable diagnostic message.
  394. string message = 2;
  395. // True when the client should reconnect after a delay.
  396. bool reconnect = 3;
  397. // Suggested reconnect delay in milliseconds.
  398. uint32 retry_after_ms = 4;
  399. }
  400. // One authorized transient event delivered over the realtime WebSocket.
  401. //
  402. // These events have no resume cursor and are never replayed. Durable state is
  403. // delivered only as RealtimeProjectionEvent operations.
  404. message RealtimeEventEnvelope {
  405. reserved 4, 5, 10 to 12, 20 to 21, 40 to 47, 60 to 76, 80 to 89;
  406. 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";
  407. // Stable event ID.
  408. string id = 1;
  409. // Time the event was created.
  410. google.protobuf.Timestamp created_at = 2;
  411. // User or system actor that caused the event, when known.
  412. optional string actor_id = 3;
  413. oneof event {
  414. // A user is typing in a room or thread that the viewer can read.
  415. RealtimeTypingEvent user_typing = 30;
  416. // A user's live presence changed.
  417. RealtimePresenceChangedEvent presence_changed = 31;
  418. // The current user's session was terminated.
  419. RealtimeSessionTerminatedEvent session_terminated = 90;
  420. }
  421. }
  422. // Typing signal.
  423. //
  424. // This is an ephemeral signal. The viewer must be a room member. Main-room
  425. // typing requires `message.read`. Thread typing requires `message.read` or a
  426. // matching thread relationship with `message.read-interactions`. DM membership
  427. // authorizes DM typing delivery. `room_id` and `thread_root_event_id` identify
  428. // where to display typing state; clients normally do not hydrate it.
  429. message RealtimeTypingEvent {
  430. // Room where the actor is typing.
  431. string room_id = 1;
  432. // Thread root event ID when the actor is typing in a thread.
  433. optional string thread_root_event_id = 2;
  434. }
  435. // Presence-changed signal.
  436. //
  437. // The latest presence status is inline. Use `UserService.GetUser` when
  438. // the surrounding user profile or custom status also needs refreshing.
  439. message RealtimePresenceChangedEvent {
  440. // User whose presence changed.
  441. string user_id = 1;
  442. // Latest presence status.
  443. chatto.api.v1.PresenceStatus status = 2;
  444. }
  445. // Session-terminated signal for the connected user's current session.
  446. message RealtimeSessionTerminatedEvent {
  447. // Termination reason.
  448. string reason = 1;
  449. }