syntax = "proto3";

package photon.imessage.v1;

import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "photon/imessage/v1/message_types.proto";
import "photon/imessage/v1/streaming.proto";

option swift_prefix = "PIMsg_";


// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------

// Sends, mutates, reads, and observes messages.
//
// Writes are synchronous to the command path. Methods that return
// `MessageResponse` also perform the reads needed to return a fresh message
// snapshot; methods that return `google.protobuf.Empty` do not add a projection
// read unless their individual contract says so.
//
// Durable message changes flow through `SubscribeMessageEvents`. For gap-free
// reconnect, drain history with `EventService.CatchUpEvents` before joining
// the live subscription.
service MessageService {

  // Writes
  rpc SendTextMessage(SendTextMessageRequest) returns (MessageResponse);

  rpc SendAttachmentMessage(SendAttachmentMessageRequest) returns (MessageResponse);

  rpc SendMultipartMessage(SendMultipartMessageRequest) returns (MessageResponse);

  rpc SendMiniAppMessage(SendMiniAppMessageRequest) returns (MessageResponse);

  // Sends an iMessage mini-app card backed by the caller's own extension.
  rpc SendCustomizedMiniAppMessage(SendCustomizedMiniAppMessageRequest) returns (MessageResponse);

  rpc EditMessage(EditMessageRequest) returns (MessageResponse);

  // Retracts an existing message and returns Empty after helper success.
  rpc UnsendMessage(UnsendMessageRequest) returns (google.protobuf.Empty);

  rpc SetReaction(SetReactionRequest) returns (MessageResponse);

  rpc PlaceSticker(PlaceStickerRequest) returns (MessageResponse);

  // Triggers Apple's per-message "Notify Anyway" action.
  rpc NotifySilencedMessage(NotifySilencedMessageRequest) returns (google.protobuf.Empty);

  // Reads
  rpc GetMessage(GetMessageRequest) returns (GetMessageResponse);

  rpc ListRecentMessages(ListRecentMessagesRequest) returns (ListRecentMessagesResponse);

  rpc ListChatMessages(ListChatMessagesRequest) returns (ListChatMessagesResponse);

  rpc GetEmbeddedMedia(GetEmbeddedMediaRequest) returns (GetEmbeddedMediaResponse);

  // Live durable-event subscription. Pair with `EventService.CatchUpEvents`
  // for gap-free history-then-live consumption.
  rpc SubscribeMessageEvents(SubscribeMessageEventsRequest) returns (stream SubscribeMessageEventsResponse);

}


// ---------------------------------------------------------------------------
// Shared request fragments
// ---------------------------------------------------------------------------

message MessageTarget {

  string chat_guid = 1;

  string message_guid = 2;

  // Selects a single bubble in a multipart message. Absent = the whole
  // message root.
  optional int32 target_part_index = 3;

}


// ---------------------------------------------------------------------------
// Send variants
// ---------------------------------------------------------------------------

// Plain-text or rich-text send.
//
// `enable_data_detection` enables Apple's data-detector pass.
// `enable_link_preview` enables URL-preview generation.
message SendTextMessageRequest {

  string chat_guid = 1;

  string text = 2;

  optional ReplyTarget reply_to = 3;

  optional string subject = 4;

  optional string effect_id = 5;

  optional bool enable_data_detection = 6;

  optional bool enable_link_preview = 7;

  repeated TextFormat formatting = 8;

  optional string client_message_id = 100;

}


// Single-attachment send. The attachment is referenced either by GUID
// (already uploaded via `AttachmentService.UploadAttachment`) or by an
// absolute filesystem path readable by the local mutation process.
message SendAttachmentMessageRequest {

  string chat_guid = 1;

  AttachmentRef attachment = 2;

  optional ReplyTarget reply_to = 3;

  optional string effect_id = 4;

  optional bool is_audio_message = 5;

  optional string client_message_id = 100;

}


// Multipart send: multiple bubbles delivered atomically.
message SendMultipartMessageRequest {

  string chat_guid = 1;

  repeated MessagePart parts = 2;

  optional ReplyTarget reply_to = 3;

  optional string subject = 4;

  optional string effect_id = 5;

  optional bool enable_data_detection = 6;

  optional string client_message_id = 100;

}

// Sends a mini app card.
//
// Recipients open `url` when they tap the card. The caller supplies every
// visible preview field; the mini app host identity is server-managed.
message SendMiniAppMessageRequest {

  // Target chat GUID.
  string chat_guid = 1;

  // Absolute HTTP or HTTPS URL opened from the card.
  string url = 2;

  MiniAppPreview preview = 3;

  optional string client_message_id = 100;

}


message MiniAppPreview {

  // Main title shown on the card.
  string title = 1;

  // Secondary text for the card template.
  optional string subtitle = 2;

  // Supporting body text for the card template.
  optional string body = 3;

  // JPEG preview image bytes.
  optional bytes image_jpeg = 4;

  // Small label shown by the card template.
  optional string caption = 5;

  // Secondary label shown by the card template.
  optional string footer = 6;

  // Additional detail label shown by the card template.
  optional string detail = 7;

  // Fallback text for surfaces that cannot render the full card.
  optional string summary = 8;

}


// Visible content of a customized mini-app card. Field names match Apple's
// `MSMessageTemplateLayout` public API so callers do not need to translate
// between our names and Apple's docs.
//
// Slot map:
//   * caption/subcaption render on the left;
//   * trailing_caption/trailing_subcaption render on the right;
//   * image_title/image_subtitle overlay the image.
//
// Validation enforced by the server:
//   * at least one of {caption, subcaption, trailing_caption,
//     trailing_subcaption, image} must be non-empty; otherwise the
//     recipient would see an empty bubble;
//   * `image` and `image_title` must be set together;
//   * `image_subtitle` requires `image`.
message MiniAppLayout {

  // Top-left, bold. The most prominent text slot.
  optional string caption = 5;

  // Below `caption`, on the left.
  optional string subcaption = 3;

  // Top-right.
  optional string trailing_caption = 6;

  // Below `trailing_caption`, on the right.
  optional string trailing_subcaption = 7;

  // JPEG preview image bytes. Server validates the JPEG SOI marker.
  optional bytes image = 4;

  // Overlay text shown above the image. Must be set together with `image`.
  optional string image_title = 1;

  // Overlay text shown below `image_title`, above the image edge.
  // Requires `image`.
  optional string image_subtitle = 2;

  // Fallback text shown on surfaces that cannot render the full card.
  optional string summary = 8;

}


// Request for `SendCustomizedMiniAppMessage`.
message SendCustomizedMiniAppMessageRequest {

  // Target chat GUID.
  string chat_guid = 1;

  // 10-character uppercase alphanumeric Apple Team ID.
  string team_id = 2;

  // Bundle identifier of the iMessage extension target.
  string extension_bundle_id = 3;

  // Human-readable name of the owning app.
  string app_name = 4;

  // Absolute URL the recipient's installed extension receives on tap.
  string url = 5;

  // The visible layout of the card. Mirrors Apple's
  // `MSMessageTemplateLayout`; see `MiniAppLayout` for the rendering map.
  MiniAppLayout layout = 6;

  // Apple App Store numeric id of the owning app. When set, must be > 0.
  optional int64 app_store_id = 7;

  optional string client_message_id = 100;

}


message MessageResponse {

  // Snapshot of the persisted message after Apple has accepted the send
  // and chat.db has been observed.
  Message message = 1;

}


// ---------------------------------------------------------------------------
// Command writes (target an existing message)
// ---------------------------------------------------------------------------

message EditMessageRequest {

  MessageTarget target = 1;

  string new_text = 2;

  // Plain-text fallback for recipients on older clients that cannot
  // render rich edits. Absent = derive from `new_text`.
  optional string backward_compat_text = 3;

  optional string client_message_id = 100;

}


message UnsendMessageRequest {

  MessageTarget target = 1;

  optional string client_message_id = 100;

}


message SetReactionRequest {

  MessageTarget target = 1;

  MessageReaction reaction = 2;

  // `true` adds or replaces the caller's reaction of this kind on the target.
  // `false` removes it.
  bool is_set = 3;

  optional string client_message_id = 100;

}


message PlaceStickerRequest {

  MessageTarget target = 1;

  AttachmentRef sticker = 2;

  StickerPlacement placement = 3;

  optional string client_message_id = 100;

}


message NotifySilencedMessageRequest {

  string chat_guid = 1;

  string message_guid = 2;

  optional string client_message_id = 100;

}


// ---------------------------------------------------------------------------
// Reads
// ---------------------------------------------------------------------------

message GetMessageRequest {

  string message_guid = 2;

}


message GetMessageResponse {

  Message message = 1;

}


// Pages across the caller's recent message history.
//
// Filters compose with AND semantics. `before` / `after` define a half-open window:
// `[after, before)`.
message ListRecentMessagesRequest {

  optional int32 page_size = 1;

  optional string page_token = 2;

  optional bool is_from_me = 3;

  optional bool is_read = 4;

  optional google.protobuf.Timestamp before = 5;

  optional google.protobuf.Timestamp after = 6;

}


message ListRecentMessagesResponse {

  repeated Message messages = 1;

  optional string next_page_token = 2;

}


// As `ListRecentMessages` but constrained to one chat.
message ListChatMessagesRequest {

  string chat_guid = 1;

  optional int32 page_size = 2;

  optional string page_token = 3;

  optional bool is_from_me = 4;

  optional bool is_read = 5;

  optional google.protobuf.Timestamp before = 6;

  optional google.protobuf.Timestamp after = 7;

}


message ListChatMessagesResponse {

  repeated Message messages = 1;

  optional string next_page_token = 2;

}


message GetEmbeddedMediaRequest {

  string chat_guid = 1;

  string message_guid = 2;

}


message GetEmbeddedMediaResponse {

  EmbeddedMedia media = 1;

}


// ---------------------------------------------------------------------------
// Live event subscription
// ---------------------------------------------------------------------------

message SubscribeMessageEventsRequest {

  // Absent = subscribe to every chat the caller can observe.
  optional string chat_guid = 1;

}


message SubscribeMessageEventsResponse {

  // Monotonic global sequence; absent on heartbeat frames. Shared with
  // `EventService.CatchUpEvents` and every other `Subscribe*` stream.
  optional uint64 sequence = 1;

  oneof payload {

    MessageChangeEvent message_changed = 10;

    Heartbeat heartbeat = 99;

  }

}
