realtime.proto 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. syntax = "proto3";
  2. package chatto.realtime.v1;
  3. import "chatto/api/v1/notification_preferences.proto";
  4. import "chatto/api/v1/notifications.proto";
  5. import "chatto/api/v1/member_directory.proto";
  6. import "chatto/api/v1/message_types.proto";
  7. import "chatto/api/v1/presence.proto";
  8. import "chatto/api/v1/room_directory.proto";
  9. import "chatto/api/v1/room_timeline.proto";
  10. import "chatto/api/v1/server.proto";
  11. import "chatto/api/v1/server_state.proto";
  12. import "chatto/api/v1/viewer.proto";
  13. import "chatto/api/v1/voice_calls.proto";
  14. import "google/protobuf/timestamp.proto";
  15. // Client-to-server frame for Chatto's protobuf WebSocket realtime protocol.
  16. //
  17. // Clients send binary protobuf frames to `/api/realtime`. The first frame must
  18. // be `hello`; after the server replies with `hello`, clients send
  19. // `subscribe_events` to start the authenticated server-projection stream.
  20. message RealtimeClientFrame {
  21. reserved "ack";
  22. oneof frame {
  23. // Opens the protocol session and optionally carries a bearer token.
  24. RealtimeClientHello hello = 1;
  25. // Starts the caller's authorized server-projection stream.
  26. RealtimeSubscribeEvents subscribe_events = 2;
  27. // Application-level ping. The server replies with `pong`.
  28. RealtimePing ping = 3;
  29. // Adds one joined room's recent timeline to the retained projection.
  30. RealtimeHydrateRoom hydrate_room = 4;
  31. }
  32. }
  33. // Server-to-client frame for Chatto's protobuf WebSocket realtime protocol.
  34. message RealtimeServerFrame {
  35. oneof frame {
  36. // Confirms protocol version, server version, and capabilities.
  37. RealtimeServerHello hello = 1;
  38. // Confirms the server-projection stream has started.
  39. RealtimeSubscribed subscribed = 2;
  40. // An authorized transient, non-replayable event.
  41. RealtimeEventEnvelope event = 3;
  42. // Application-level heartbeat used for liveness checks.
  43. RealtimeHeartbeat heartbeat = 4;
  44. // Protocol or authorization error.
  45. RealtimeError error = 5;
  46. // Server-requested close with reconnect guidance.
  47. RealtimeClose close = 6;
  48. // Reply to a client ping.
  49. RealtimePong pong = 7;
  50. // Confirms that durable replay reached the live-stream boundary.
  51. RealtimeCaughtUp caught_up = 8;
  52. // One idempotent mutation of the caller's server projection.
  53. RealtimeProjectionEvent projection_event = 9;
  54. }
  55. }
  56. // Initial client hello.
  57. message RealtimeClientHello {
  58. reserved 3;
  59. reserved "resume_cursor";
  60. // Protocol version requested by the client. The only supported version is 2.
  61. uint32 protocol_version = 1;
  62. // Optional bearer token. When present, it takes precedence over cookie auth.
  63. optional string bearer_token = 2;
  64. }
  65. // Initial server hello.
  66. message RealtimeServerHello {
  67. reserved 3;
  68. reserved "resume_supported";
  69. // Protocol version accepted by the server.
  70. uint32 protocol_version = 1;
  71. // Chatto server software version.
  72. string server_version = 2;
  73. // Approximate heartbeat interval clients should expect.
  74. uint32 heartbeat_interval_seconds = 4;
  75. // Stable protocol capability keys supported by this server. Current keys:
  76. // `chatto.realtime.events.live.v1`, `chatto.realtime.heartbeat.v1`,
  77. // `chatto.realtime.ping.v1`, `chatto.realtime.events.resume.v1`, and
  78. // `chatto.realtime.projection.v1`.
  79. repeated string capabilities = 5;
  80. }
  81. // Request to start the caller's authorized server-projection stream.
  82. message RealtimeSubscribeEvents {
  83. // Opaque cursor from a previously received durable event or caught-up
  84. // frame. The server replays authorized durable projection changes after
  85. // this cursor before continuing with live delivery. Omit it to receive a
  86. // compacted reset of the current authorised server projection. Cursors
  87. // expire 24 hours after issue; an expired cursor also receives a compacted
  88. // reset rather than historical replay.
  89. optional string resume_cursor = 1;
  90. // Joined rooms whose timeline windows the client already retains. A fresh
  91. // compacted projection includes only these timelines, and resumed delivery
  92. // emits timeline mutations only for these rooms. Lightweight room, unread,
  93. // notification, and call state remains server-wide. At most 64 room IDs
  94. // may be supplied in one subscription.
  95. repeated string retained_room_ids = 2;
  96. }
  97. // Requests lazy materialisation of one joined room's recent timeline.
  98. //
  99. // The server replies on the same ordered projection stream with a
  100. // `room_timeline_replace` operation. Once hydrated, later timeline mutations
  101. // for the room are included for the lifetime of this connection. Clients send
  102. // all still-retained room IDs again in their next subscription. A connection
  103. // may retain at most 64 distinct room IDs; excess requests receive a
  104. // non-fatal `too_many_retained_rooms` error.
  105. message RealtimeHydrateRoom {
  106. // Joined room to materialise. Repeating a retained room is idempotent.
  107. string room_id = 1;
  108. }
  109. // Confirms projection streaming has started.
  110. message RealtimeSubscribed {
  111. reserved 1;
  112. reserved "cursor";
  113. // Opaque cursor from which this subscription starts. When the request
  114. // supplied a resume cursor, this is that validated cursor. Otherwise it is
  115. // the current live boundary.
  116. optional string start_cursor = 2;
  117. }
  118. // Confirms that durable replay is complete and live delivery has begun.
  119. //
  120. // Clients can retain `cursor` after applying every preceding event. A cursor
  121. // must not outlive the projection state it describes. Events after this frame
  122. // are live. Transient events are never replayed. A cursor is usable for up to
  123. // 24 hours after issue and should be replaced by each later caught-up cursor.
  124. message RealtimeCaughtUp {
  125. // Opaque cursor at the replay-to-live handoff boundary.
  126. string cursor = 1;
  127. }
  128. // One ordered, idempotent mutation of the caller's authorised server view.
  129. //
  130. // A fresh subscription starts with a reset operation followed by current
  131. // resource upserts. A resumed subscription derives operations from EVT after
  132. // the supplied cursor. Both paths use this envelope and the same client
  133. // reducer. The stream is a convergence feed, not an immutable audit log:
  134. // replay uses current authorisation, deletion, and erasure state.
  135. message RealtimeProjectionEvent {
  136. // Stable event ID. Synthetic compacted-replay events use server-generated IDs.
  137. string id = 1;
  138. // Time the source fact occurred or the compacted event was generated.
  139. google.protobuf.Timestamp created_at = 2;
  140. // User or system actor that caused the source fact, when known.
  141. optional string actor_id = 3;
  142. // Opaque cursor safe to retain with the resulting projection state after
  143. // applying every operation in this event.
  144. optional string resume_cursor = 4;
  145. // Operations applied atomically and in order by the client reducer.
  146. repeated RealtimeProjectionOperation operations = 5;
  147. }
  148. // One mutation of a server-scoped client projection.
  149. message RealtimeProjectionOperation {
  150. oneof operation {
  151. // Clears all canonical server projection state before compacted replay.
  152. RealtimeProjectionReset reset = 1;
  153. // Replaces the public server profile.
  154. chatto.api.v1.ServerPublicProfile server_upsert = 2;
  155. // Replaces the authenticated viewer resource.
  156. chatto.api.v1.GetViewerResponse viewer_upsert = 3;
  157. // Adds or replaces one server directory member.
  158. chatto.api.v1.DirectoryMember user_upsert = 4;
  159. // Removes one server directory member.
  160. RealtimeProjectionUserRemove user_remove = 5;
  161. // Adds or replaces one room and its viewer state. Channel membership is
  162. // complete only after that room's timeline has been materialised.
  163. RealtimeProjectionRoom room_upsert = 6;
  164. // Removes one room and all locally retained room state.
  165. RealtimeProjectionRoomRemove room_remove = 7;
  166. // Replaces the complete visible room-group layout.
  167. RealtimeProjectionRoomGroupsReplace room_groups_replace = 8;
  168. // Replaces the lazily retained recent timeline window for one joined room.
  169. RealtimeProjectionRoomTimelineReplace room_timeline_replace = 9;
  170. // Adds or replaces one renderable event in a retained room timeline.
  171. RealtimeProjectionRoomTimelineEventUpsert room_timeline_event_upsert = 10;
  172. // Replaces authenticated server-wide presentation and runtime state.
  173. RealtimeProjectionServerState server_state_upsert = 11;
  174. // Removes one event from a retained room timeline. This is distinct from
  175. // a message tombstone: projection-only channel echoes disappear when the
  176. // underlying thread reply is no longer echoed into the room.
  177. RealtimeProjectionRoomTimelineEventRemove room_timeline_event_remove = 12;
  178. // Replaces the viewer's current pending-notification page and room counts.
  179. RealtimeProjectionNotificationsReplace notifications_replace = 13;
  180. // Replaces one room's current viewer state without
  181. // retransmitting room metadata or membership.
  182. RealtimeProjectionRoomViewerStateReplace room_viewer_state_replace = 14;
  183. // Replaces every active call visible to the caller.
  184. RealtimeProjectionActiveCallsReplace active_calls_replace = 15;
  185. // Replaces the latest presence status for every server member. Presence
  186. // is transient and not replayed from EVT, so every subscription emits
  187. // this reconciliation before caught_up.
  188. RealtimeProjectionPresencesReplace presences_replace = 16;
  189. // Replaces viewer-specific follow and unread state for every followed
  190. // thread. Missing entries authoritatively mean not followed and not
  191. // unread for retained thread roots.
  192. RealtimeProjectionThreadViewerStatesReplace thread_viewer_states_replace = 17;
  193. // Signals root-message activity for lightweight room ordering even when
  194. // that room's timeline is not retained.
  195. RealtimeProjectionRoomActivity room_activity = 18;
  196. }
  197. }
  198. // Reset marker for a compacted projection replay.
  199. message RealtimeProjectionReset {}
  200. // Authenticated server state required by every signed-in client.
  201. message RealtimeProjectionServerState {
  202. // Optional message of the day shown to authenticated members.
  203. optional string motd = 1;
  204. // Runtime capabilities and limits advertised to the client.
  205. chatto.api.v1.ServerRuntimeConfig runtime = 2;
  206. }
  207. // Canonical lightweight room state retained by a server-scoped projection.
  208. message RealtimeProjectionRoom {
  209. // Public room metadata and viewer-specific state.
  210. chatto.api.v1.RoomWithViewerState room = 1;
  211. // Current room membership expressed as references into the projection's
  212. // server-wide user directory. DM participants are always present. Channel
  213. // membership is empty in a cold room summary and complete after that room's
  214. // timeline has been materialised.
  215. repeated string member_user_ids = 2;
  216. // Current pending notifications targeting this room for the viewer.
  217. uint32 viewer_notification_count = 3;
  218. // Whether this DM room has ever received a root message. False means the
  219. // durable room exists only so its initiator can compose the first message.
  220. // Once true, message deletion does not reset it. Absent for channel rooms
  221. // and servers that predate this field.
  222. optional bool has_message_history = 4;
  223. }
  224. // Root-message activity that affects lightweight room navigation state.
  225. //
  226. // This deliberately carries no message content. Retained rooms receive the
  227. // corresponding timeline mutation separately; unretained rooms still use this
  228. // operation to keep DM ordering current without materialising their timeline.
  229. message RealtimeProjectionRoomActivity {
  230. string room_id = 1;
  231. }
  232. // Removes one user from the client projection. Reducers must also purge copied
  233. // render data for this user from retained rooms, timelines, notifications, and
  234. // calls while preserving stable IDs on historical facts.
  235. message RealtimeProjectionUserRemove {
  236. // Stable user ID to remove.
  237. string user_id = 1;
  238. }
  239. // Complete latest-value presence state for the projected user directory.
  240. // Missing map entries are offline; the server normally includes every member
  241. // explicitly so reducers can converge without retaining an older value.
  242. message RealtimeProjectionPresencesReplace {
  243. map<string, chatto.api.v1.PresenceStatus> statuses = 1;
  244. }
  245. // Complete current viewer state for followed threads. This reconciles durable
  246. // follow changes and RUNTIME_STATE read markers that may change while a client
  247. // is dormant without retransmitting room timelines.
  248. message RealtimeProjectionThreadViewerStatesReplace {
  249. repeated RealtimeProjectionThreadViewerState states = 1;
  250. }
  251. // Viewer-specific state for one followed thread root.
  252. message RealtimeProjectionThreadViewerState {
  253. string room_id = 1;
  254. string thread_root_event_id = 2;
  255. chatto.api.v1.ThreadViewerState viewer_state = 3;
  256. }
  257. // Removes one room from the client projection.
  258. message RealtimeProjectionRoomRemove {
  259. // Stable room ID to remove.
  260. string room_id = 1;
  261. }
  262. // Complete ordered room-group layout visible to the caller.
  263. message RealtimeProjectionRoomGroupsReplace {
  264. // Visible room groups in sidebar order.
  265. repeated chatto.api.v1.RoomGroup groups = 1;
  266. }
  267. // Complete retained recent timeline window for one room.
  268. message RealtimeProjectionRoomTimelineReplace {
  269. // Room whose retained window is replaced.
  270. string room_id = 1;
  271. // Renderable current timeline state. Deleted and crypto-shredded message
  272. // bodies are absent and represented by their normal tombstone fields.
  273. chatto.api.v1.RoomTimelinePage page = 2;
  274. // Opaque cursor for every retained row, keyed by timeline event ID.
  275. map<string, string> event_cursors = 3;
  276. }
  277. // Current renderable state for one timeline event.
  278. message RealtimeProjectionRoomTimelineEventUpsert {
  279. // Room containing the event.
  280. string room_id = 1;
  281. // Current renderable event state.
  282. chatto.api.v1.RoomTimelineEvent event = 2;
  283. // Related users needed to render this event.
  284. chatto.api.v1.RoomTimelineIncludes includes = 3;
  285. // Reaction transition that caused this upsert, when applicable.
  286. optional RealtimeProjectionReactionChange reaction_change = 4;
  287. // Preserve a deleted echo row as a tombstone. Directly deleted echoes use a
  288. // remove operation instead; this flag identifies canonical-reply deletion.
  289. bool retain_deleted_row = 5;
  290. // Opaque room-timeline cursor for this canonical row. Clients use retained
  291. // row cursors to advance a bounded window without a separate refresh read.
  292. string event_cursor = 6;
  293. }
  294. // Removes one projection-only row from a retained room timeline.
  295. message RealtimeProjectionRoomTimelineEventRemove {
  296. // Room containing the retained row.
  297. string room_id = 1;
  298. // Stable event ID of the row to remove.
  299. string event_id = 2;
  300. }
  301. // Finite current notification state emitted on bootstrap and every resume.
  302. message RealtimeProjectionNotificationsReplace {
  303. // Newest pending notifications and total pending count.
  304. chatto.api.v1.ListNotificationsResponse page = 1;
  305. // Complete current counts for rooms with pending notifications.
  306. repeated chatto.api.v1.RoomNotificationCount room_counts = 2;
  307. // Live transition that caused this replacement, when one exists. Bootstrap,
  308. // replay reconciliation, and compacted reset replacements omit this field.
  309. optional RealtimeProjectionNotificationChange change = 3;
  310. }
  311. // One live notification transition accompanying authoritative current state.
  312. //
  313. // This metadata exists for one-shot presentation effects such as sounds. The
  314. // enclosing replacement remains the canonical notification state.
  315. message RealtimeProjectionNotificationChange {
  316. RealtimeProjectionNotificationAction action = 1;
  317. string notification_id = 2;
  318. // True when a created notification must not produce an alert.
  319. bool silent = 3;
  320. }
  321. // Kind of live notification transition.
  322. enum RealtimeProjectionNotificationAction {
  323. REALTIME_PROJECTION_NOTIFICATION_ACTION_UNSPECIFIED = 0;
  324. REALTIME_PROJECTION_NOTIFICATION_ACTION_CREATED = 1;
  325. REALTIME_PROJECTION_NOTIFICATION_ACTION_DISMISSED = 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 87;
  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";
  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.
  415. RealtimeTypingEvent user_typing = 30;
  416. // A user's live presence changed.
  417. RealtimePresenceChangedEvent presence_changed = 31;
  418. // The current user was mentioned in a room.
  419. RealtimeMentionNotificationEvent mention_notification = 88;
  420. // The current user received a new direct message.
  421. RealtimeNewDirectMessageNotificationEvent new_direct_message_notification = 89;
  422. // The current user's session was terminated.
  423. RealtimeSessionTerminatedEvent session_terminated = 90;
  424. }
  425. }
  426. // Typing signal.
  427. //
  428. // This is an ephemeral signal. `room_id` and `thread_root_event_id` identify
  429. // where to display typing state; clients normally do not hydrate it.
  430. message RealtimeTypingEvent {
  431. // Room where the actor is typing.
  432. string room_id = 1;
  433. // Thread root event ID when the actor is typing in a thread.
  434. optional string thread_root_event_id = 2;
  435. }
  436. // Presence-changed signal.
  437. //
  438. // The latest presence status is inline. Use `UserService.GetUser` when
  439. // the surrounding user profile or custom status also needs refreshing.
  440. message RealtimePresenceChangedEvent {
  441. // User whose presence changed.
  442. string user_id = 1;
  443. // Latest presence status.
  444. chatto.api.v1.PresenceStatus status = 2;
  445. }
  446. // Mention attention signal for the connected user.
  447. //
  448. // Inline names are display hints. Hydrate referenced rooms through
  449. // `RoomDirectoryService.BatchGetRooms` and users through
  450. // `UserService.BatchGetUsers` when local caches are missing or stale.
  451. message RealtimeMentionNotificationEvent {
  452. // Room where the mention occurred.
  453. string room_id = 1;
  454. // Author user ID.
  455. string actor_user_id = 2;
  456. // Display name of the room where the mention occurred, when hydrated for the caller.
  457. optional string room_name = 3;
  458. // Display name of the author who mentioned the connected user, when hydrated.
  459. optional string actor_display_name = 4;
  460. }
  461. // New-DM attention signal for the connected user.
  462. //
  463. // Inline names and avatar URLs are display hints. Hydrate the DM room through
  464. // `RoomDirectoryService.GetRoom` or `RoomDirectoryService.BatchGetRooms`, and
  465. // the sender through `UserService.GetUser` or `UserService.BatchGetUsers` when
  466. // local caches are missing or stale.
  467. message RealtimeNewDirectMessageNotificationEvent {
  468. // DM room ID.
  469. string room_id = 1;
  470. // Sender user ID.
  471. string sender_id = 2;
  472. // Display name of the sender, when hydrated.
  473. optional string sender_display_name = 3;
  474. // Avatar URL of the sender, when one is available.
  475. optional string sender_avatar_url = 4;
  476. // Display name for the DM conversation from the connected user's perspective, when hydrated.
  477. optional string conversation_name = 5;
  478. }
  479. // Session-terminated signal for the connected user's current session.
  480. message RealtimeSessionTerminatedEvent {
  481. // Termination reason.
  482. string reason = 1;
  483. }