syntax = "proto3";

package chatto.realtime.v1;

import "chatto/api/v1/notification_preferences.proto";
import "chatto/api/v1/presence.proto";
import "chatto/api/v1/viewer.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 live event stream.
message RealtimeClientFrame {
  reserved 4;
  reserved "ack";

  oneof frame {
    // Opens the protocol session and optionally carries a bearer token.
    RealtimeClientHello hello = 1;
    // Starts the caller's authorized live-event stream.
    RealtimeSubscribeEvents subscribe_events = 2;
    // Application-level ping. The server replies with `pong`.
    RealtimePing ping = 3;
  }
}

// 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 live-event stream has started.
    RealtimeSubscribed subscribed = 2;
    // A live event authorized for the connected user.
    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;
  }
}

// Initial client hello.
message RealtimeClientHello {
  reserved 3;
  reserved "resume_cursor";

  // Protocol version requested by the client. The current version is 1.
  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`, and
  // `chatto.realtime.ping.v1`.
  repeated string capabilities = 5;
}

// Request to start the caller's authorized live event stream.
message RealtimeSubscribeEvents {}

// Confirms event streaming has started.
message RealtimeSubscribed {
  reserved 1;
  reserved "cursor";
}

// 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;
}

// 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 live event delivered over the realtime WebSocket.
//
// Realtime events are invalidation signals first: payloads carry stable IDs and
// small inline hints, while durable resource state is hydrated through
// chatto.api.v1 ConnectRPC services. Event-specific messages document the
// intended hydration path when a referenced resource may need refreshing.
message RealtimeEventEnvelope {
  reserved 4;
  reserved "cursor";

  // 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 message was posted in a room or thread.
    RealtimeMessagePostedEvent message_posted = 10;
    // A message body or renderable metadata changed.
    RealtimeMessageEditedEvent message_edited = 11;
    // A message was retracted.
    RealtimeMessageRetractedEvent message_retracted = 12;
    // A user added a reaction to a message.
    RealtimeReactionEvent reaction_added = 20;
    // A user removed a reaction from a message.
    RealtimeReactionEvent reaction_removed = 21;
    // A user is typing in a room or thread.
    RealtimeTypingEvent user_typing = 30;
    // A user's live presence changed.
    RealtimePresenceChangedEvent presence_changed = 31;
    // A room was created.
    RealtimeRoomEvent room_created = 40;
    // A room's profile or settings changed.
    RealtimeRoomEvent room_updated = 41;
    // A room was deleted.
    RealtimeRoomEvent room_deleted = 42;
    // A room was archived.
    RealtimeRoomEvent room_archived = 43;
    // A room was restored from the archive.
    RealtimeRoomEvent room_unarchived = 44;
    // A user joined a room visible to the current user.
    RealtimeRoomEvent user_joined_room = 45;
    // A user left a room visible to the current user.
    RealtimeRoomEvent user_left_room = 46;
    // A room's universal visibility flag changed.
    RealtimeRoomUniversalChangedEvent room_universal_changed = 47;
    // A notification was created for the current user.
    RealtimeNotificationCreatedEvent notification_created = 60;
    // A notification was dismissed for the current user.
    RealtimeNotificationDismissedEvent notification_dismissed = 61;
    // The current user's notification level changed.
    RealtimeNotificationLevelChangedEvent notification_level_changed = 62;
    // The current user's thread follow state changed.
    RealtimeThreadFollowChangedEvent thread_follow_changed = 63;
    // The current user's room read state changed.
    RealtimeRoomMarkedAsReadEvent room_marked_as_read = 64;
    // A thread was created in a room visible to the current user.
    RealtimeThreadCreatedEvent thread_created = 65;
    // The server profile/config changed.
    RealtimeServerUpdatedEvent server_updated = 70;
    // A user profile changed.
    RealtimeUserProfileUpdatedEvent user_profile_updated = 71;
    // A user's custom status was set or changed.
    RealtimeUserCustomStatusSetEvent user_custom_status_set = 72;
    // A user's custom status was cleared.
    RealtimeUserCustomStatusClearedEvent user_custom_status_cleared = 73;
    // The current user's display preferences changed.
    RealtimeServerUserPreferencesUpdatedEvent server_user_preferences_updated = 74;
    // Room-group layout changed.
    RealtimeRoomGroupsUpdatedEvent room_groups_updated = 75;
    // A server member was deleted.
    RealtimeServerMemberDeletedEvent server_member_deleted = 76;
    // Asset processing started for a message attachment.
    RealtimeAssetProcessingEvent asset_processing_started = 80;
    // Asset processing completed successfully for a message attachment.
    RealtimeAssetProcessingEvent asset_processing_succeeded = 81;
    // Asset processing failed for a message attachment.
    RealtimeAssetProcessingEvent asset_processing_failed = 82;
    // An asset attached to a message was deleted.
    RealtimeAssetDeletedEvent asset_deleted = 83;
    // A voice call started in a room.
    RealtimeCallEvent call_started = 84;
    // A participant joined a voice call.
    RealtimeCallEvent call_participant_joined = 85;
    // A participant left a voice call.
    RealtimeCallEvent call_participant_left = 86;
    // A voice call ended.
    RealtimeCallEvent call_ended = 87;
    // The current user was mentioned in a room.
    RealtimeMentionNotificationEvent mention_notification = 88;
    // The current user received a new direct message.
    RealtimeNewDirectMessageNotificationEvent new_direct_message_notification = 89;
    // The current user's session was terminated.
    RealtimeSessionTerminatedEvent session_terminated = 90;
  }
}

// Message-posted signal.
//
// Hydrate the affected timeline with `RoomService.GetRoomEventsAround`
// for room messages, or `ThreadService.GetThreadEventsAround` when
// `thread_root_event_id` is set.
message RealtimeMessagePostedEvent {
  // Room containing the message.
  string room_id = 1;
  // Event ID of the posted message.
  string message_event_id = 2;
  // Root message event ID when the message was posted in a thread.
  optional string thread_root_event_id = 3;
}

// Message-edited signal.
//
// Refresh the affected timeline window with
// `RoomService.GetRoomEventsAround` for room messages, or
// `ThreadService.GetThreadEventsAround` when the local message belongs to
// a thread.
message RealtimeMessageEditedEvent {
  // Room containing the message.
  string room_id = 1;
  // Event ID of the edited message.
  string message_event_id = 2;
}

// Message-retracted signal.
//
// Refresh or patch the affected timeline window with
// `RoomService.GetRoomEventsAround` for room messages, or
// `ThreadService.GetThreadEventsAround` when the local message belongs to
// a thread.
message RealtimeMessageRetractedEvent {
  // Room containing the message.
  string room_id = 1;
  // Event ID of the retracted message.
  string message_event_id = 2;
  // Retraction reason, when one was supplied.
  optional string reason = 3;
}

// Reaction signal.
//
// Refresh or patch the affected message in its timeline window. Use
// `RoomService.GetRoomEventsAround` for room messages, or
// `ThreadService.GetThreadEventsAround` when the local message belongs to
// a thread. `message_event_id` is canonical: reactions made through a channel
// echo of a thread reply report the original reply event ID. Clients that show
// channel echoes should also refresh or patch visible echo rows whose
// `echo_of_event_id` matches this ID.
message RealtimeReactionEvent {
  // Room containing the reacted-to message.
  string room_id = 1;
  // Canonical event ID of the reacted-to message.
  string message_event_id = 2;
  // Reaction emoji.
  string emoji = 3;
}

// Typing signal.
//
// This is an ephemeral signal. `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;
}

// Room lifecycle or membership signal.
//
// Hydrate visible room state with `RoomDirectoryService.GetRoom` or
// `RoomDirectoryService.BatchGetRooms`. Unknown/deleted rooms return
// `NOT_FOUND` from singular reads, hidden rooms return `PERMISSION_DENIED`, and
// batch reads omit unknown, deleted, hidden, and inaccessible rooms.
message RealtimeRoomEvent {
  // Room affected by the event.
  string room_id = 1;
}

// Room universal-visibility signal.
//
// The latest universal flag is inline. Hydrate the rest of the visible room with
// `RoomDirectoryService.GetRoom` or `RoomDirectoryService.BatchGetRooms`.
message RealtimeRoomUniversalChangedEvent {
  // Room whose universal setting changed.
  string room_id = 1;
  // Latest universal setting.
  bool universal = 2;
}

// Notification-created signal for the connected user.
//
// Hydrate full notification rows with `NotificationService.GetNotification` or
// `NotificationService.BatchGetNotifications`. Use `RoomService` or
// `ThreadService` anchor reads when `event_id` or `in_reply_to_id` must be
// opened in context.
message RealtimeNotificationCreatedEvent {
  // Notification ID.
  string notification_id = 1;
  // Room associated with the notification, when the notification navigates to a room.
  optional string room_id = 2;
  // Event ID associated with the notification, when the notification navigates to an event.
  optional string event_id = 3;
  // Parent event ID for reply notifications, when known.
  optional string in_reply_to_id = 4;
  // True when the notification should not alert.
  bool silent = 5;
}

// Notification-dismissed signal for the connected user.
//
// Remove the local notification row by ID. A later singular notification read
// returns `NOT_FOUND`; batch reads omit dismissed or missing notifications.
message RealtimeNotificationDismissedEvent {
  // Notification ID.
  string notification_id = 1;
}

// Notification-level signal for the connected user.
//
// The updated level is inline. Use
// `NotificationPreferencesService.GetRoomNotificationPreference` when clients
// need the complete preference resource.
message RealtimeNotificationLevelChangedEvent {
  // Room whose notification level changed.
  string room_id = 1;
  // Explicit room notification level.
  chatto.api.v1.NotificationLevel level = 2;
  // Effective notification level after defaults are applied.
  chatto.api.v1.NotificationLevel effective_level = 3;
}

// Thread-follow signal for the connected user.
//
// The updated follow state is inline. Use `ThreadService.GetThreadEvents`
// or `ThreadService.GetThreadEventsAround` when the thread itself needs
// refreshing.
message RealtimeThreadFollowChangedEvent {
  // Room containing the thread.
  string room_id = 1;
  // Root message event ID of the thread.
  string thread_root_event_id = 2;
  // True when the user now follows the thread.
  bool following = 3;
}

// Thread-created signal.
//
// Hydrate the new thread with `ThreadService.GetThreadEventsAround` using
// `thread_root_event_id` as the anchor.
message RealtimeThreadCreatedEvent {
  // Room containing the thread.
  string room_id = 1;
  // Root message event ID that identifies the thread.
  string thread_root_event_id = 2;
}

// Room-read signal for the connected user.
//
// This is a current-user read-state invalidation signal. Clients can patch local
// unread state for the room; no separate hydration is normally needed.
message RealtimeRoomMarkedAsReadEvent {
  // Room marked read.
  string room_id = 1;
}

// Server-profile signal.
//
// Public server profile fields are inline. Use `ServerDiscoveryService.GetServer`
// for public metadata and `ServerService.GetMotd` for the authenticated MOTD.
message RealtimeServerUpdatedEvent {
  // Server display name.
  string name = 1;
  // Server description.
  string description = 2;
  // Server logo URL, when configured.
  optional string logo_url = 3;
  // Server banner URL, when configured.
  optional string banner_url = 4;
}

// User-profile signal.
//
// Basic profile fields are inline. Use `UserService.GetUser` or
// `UserService.BatchGetUsers` for complete user-profile hydration.
message RealtimeUserProfileUpdatedEvent {
  // User whose profile changed.
  string user_id = 1;
  // Login name.
  string login = 2;
  // Display name.
  string display_name = 3;
  // Avatar URL, when the user has an avatar.
  optional string avatar_url = 4;
}

// User-custom-status set signal.
//
// The latest custom status is inline. Use `UserService.GetUser` when
// clients need to refresh the complete user profile.
message RealtimeUserCustomStatusSetEvent {
  // User whose status changed.
  string user_id = 1;
  // Emoji part of the status.
  string emoji = 2;
  // Text part of the status.
  string text = 3;
  // Expiration time, when the status expires automatically.
  google.protobuf.Timestamp expires_at = 4;
}

// User-custom-status cleared signal.
//
// Clear local custom status for `user_id`. Use `UserService.GetUser`
// when clients need to refresh the complete user profile.
message RealtimeUserCustomStatusClearedEvent {
  // User whose status was cleared.
  string user_id = 1;
}

// User display-preferences signal for the connected user.
//
// The updated display preferences are inline for the connected user.
message RealtimeServerUserPreferencesUpdatedEvent {
  // IANA timezone name, when the user has selected a fixed timezone.
  optional string timezone = 1;
  // Preferred time display format.
  chatto.api.v1.TimeFormat time_format = 2;
}

// Room-group layout signal.
//
// Refetch room-group layout with `RoomDirectoryService.ListRoomGroups`. If a
// client already knows specific group IDs, it can use
// `RoomDirectoryService.GetRoomGroup` or
// `RoomDirectoryService.BatchGetRoomGroups`.
message RealtimeRoomGroupsUpdatedEvent {
  // Always true; present for event-bus compatibility.
  bool changed = 1;
}

// Server-member deleted signal.
//
// Remove or invalidate local member/user rows for `user_id`. Admin clients can
// hydrate remaining member rows through `AdminUserService.GetMember` or
// `AdminUserService.BatchGetMembers`.
message RealtimeServerMemberDeletedEvent {
  // Deleted user ID.
  string user_id = 1;
}

// Asset-processing signal.
//
// Message attachments are message-owned subresources. When
// `message_event_id` is known, refetch message state with
// `MessageService.GetMessage` or `MessageService.BatchGetMessages`; otherwise
// refetch the affected attachment list or timeline window.
message RealtimeAssetProcessingEvent {
  // Room containing the message that owns the asset, when known.
  optional string room_id = 1;
  // Asset ID.
  string asset_id = 2;
  // Event ID of the message that owns the asset, when known.
  optional string message_event_id = 3;
}

// Asset-deleted signal.
//
// Remove local attachment state by `asset_id`. When the owning message is known,
// clients can refetch that message through `MessageService.GetMessage` or
// refetch the timeline window.
message RealtimeAssetDeletedEvent {
  // Room containing the message that owned the asset, when known.
  optional string room_id = 1;
  // Asset ID.
  string asset_id = 2;
}

// Voice-call state signal.
//
// Hydrate current room-scoped call state with `VoiceCallService.GetActiveCall`
// or `VoiceCallService.BatchGetActiveCalls`. Compare `call_id` to ignore stale
// transitions from a previous call in the same room.
message RealtimeCallEvent {
  // Room containing the call.
  string room_id = 1;
  // Call ID.
  string call_id = 2;
  // Source that produced the call transition.
  RealtimeCallEventSource source = 3;
}

// Source that produced a realtime voice-call transition.
enum RealtimeCallEventSource {
  // No call event source was specified.
  REALTIME_CALL_EVENT_SOURCE_UNSPECIFIED = 0;
  // The user explicitly requested the transition through Chatto.
  REALTIME_CALL_EVENT_SOURCE_USER = 1;
  // LiveKit reported the transition through a webhook.
  REALTIME_CALL_EVENT_SOURCE_LIVEKIT = 2;
  // Chatto reconciled its local state against LiveKit's current room state.
  REALTIME_CALL_EVENT_SOURCE_RECONCILIATION = 3;
}

// Mention attention signal for the connected user.
//
// Inline names are display hints. Hydrate referenced rooms through
// `RoomDirectoryService.BatchGetRooms` and users through
// `UserService.BatchGetUsers` when local caches are missing or stale.
message RealtimeMentionNotificationEvent {
  // Room where the mention occurred.
  string room_id = 1;
  // Author user ID.
  string actor_user_id = 2;
  // Display name of the room where the mention occurred, when hydrated for the caller.
  optional string room_name = 3;
  // Display name of the author who mentioned the connected user, when hydrated.
  optional string actor_display_name = 4;
}

// New-DM attention signal for the connected user.
//
// Inline names and avatar URLs are display hints. Hydrate the DM room through
// `RoomDirectoryService.GetRoom` or `RoomDirectoryService.BatchGetRooms`, and
// the sender through `UserService.GetUser` or `UserService.BatchGetUsers` when
// local caches are missing or stale.
message RealtimeNewDirectMessageNotificationEvent {
  // DM room ID.
  string room_id = 1;
  // Sender user ID.
  string sender_id = 2;
  // Display name of the sender, when hydrated.
  optional string sender_display_name = 3;
  // Avatar URL of the sender, when one is available.
  optional string sender_avatar_url = 4;
  // Display name for the DM conversation from the connected user's perspective, when hydrated.
  optional string conversation_name = 5;
}

// Session-terminated signal for the connected user's current session.
message RealtimeSessionTerminatedEvent {
  // Termination reason.
  string reason = 1;
}
