/*
 * Copyright 2025 Signal Messenger, LLC
 * SPDX-License-Identifier: AGPL-3.0-only
 */

syntax = "proto3";

option java_multiple_files = true;

package org.signal.chat.messages;

import "google/protobuf/empty.proto";

import "org/signal/chat/common.proto";
import "org/signal/chat/require.proto";
import "org/signal/chat/errors.proto";
import "org/signal/chat/tag.proto";

// Provides methods for sending "unsealed sender" messages.
service Messages {

  option (require.auth) = AUTH_ONLY_AUTHENTICATED;

  // Sends an "unsealed sender" message to all devices linked to a single
  // destination account.
  //
  // The destination account must not be the same as the authenticated caller.
  // Callers should use `SendSyncMessage` to send messages to themselves.
  rpc SendMessage(SendAuthenticatedSenderMessageRequest) returns (SendMessageAuthenticatedSenderResponse) {}

  // Sends a "sync" message to all other devices linked to the authenticated
  // sender's account.
  rpc SendSyncMessage(SendSyncMessageRequest) returns (SendMessageAuthenticatedSenderResponse) {}
}

// Provides methods for sending "sealed sender" messages.
service MessagesAnonymous {

  option (require.auth) = AUTH_ONLY_ANONYMOUS;

  // Sends a "sealed sender" message to all devices linked to a single
  // destination account.
  //
  // If this RPC is authorized with an unidentified access key, it will fail
  // with an authorization failure if the credential is invalid OR if the
  // destination account was not found. If it is authorized using a group send
  // token, it will fail with an authorization failure if the credential is
  // invalid and with an destination not found error if the account does not
  // exist
  rpc SendSingleRecipientMessage(SendSealedSenderMessageRequest) returns (SendMessageResponse) {}

  // Sends a "sealed sender" message with a common payload to all devices linked
  // to multiple destination accounts.
  rpc SendMultiRecipientMessage(SendMultiRecipientMessageRequest) returns (SendMultiRecipientMessageResponse) {}

  // Sends a story message to devices linked to a single destination account.
  rpc SendStory(SendStoryMessageRequest) returns (SendMessageResponse) {}

  // Sends a story message with a common payload to devices linked to devices
  // linked to multiple destination accounts.
  rpc SendMultiRecipientStory(SendMultiRecipientStoryRequest) returns (SendMultiRecipientMessageResponse) {}
}

message IndividualRecipientMessageBundle {

  // A message for an individual device linked to a destination account.
  message Message {

    // The registration ID for the destination device.
    uint32 registration_id = 1 [(require.range).max = 0x3fff];

    // The content of the message to deliver to the destination device.
    bytes payload = 2 [(require.size) = {min: 1, max: 262144}]; // 256 KiB

    // The message type of the message. If this message is part of an
    // unidentified send, this must be UNIDENTIFIED_SENDER
    SendMessageType type = 3;
  }

  // The time, in milliseconds since the epoch, at which this message was
  // originally sent from the perspective of the sender. Note that the maximum
  // allowable timestamp for JavaScript clients is less than Long.MAX_VALUE; see
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date
  // for additional details and discussion.
  uint64 timestamp = 1 [(require.range).min = 1, (require.range).max = 8640000000000000];

  // A map of device IDs to individual messages. Generally, callers must include
  // one message for each device linked to the destination account. In cases of
  // "sync messages" where a sender is distributing information to other devices
  // linked to the sender's account, senders may omit a message for the sending
  // device.
  map<uint32, Message> messages = 2 [(require.nonEmpty) = true];
}

enum SendMessageType {
  UNSPECIFIED = 0;

  // A double-ratchet message represents a "normal," "unsealed-sender" message
  // encrypted using the Double Ratchet within an established Signal session.
  DOUBLE_RATCHET = 1;

  // A prekey message begins a new Signal session. The `content` of a prekey
  // message is a superset of a double-ratchet message's `content` and
  // contains the sender's identity public key and information identifying the
  // pre-keys used in the message's ciphertext.
  PREKEY_MESSAGE = 2;

  // A plaintext message is used solely to convey encryption error receipts
  // and never contains encrypted message content. Encryption error receipts
  // must be delivered in plaintext because encryption/decryption of a prior
  // message failed and there is no reason to believe that
  // encryption/decryption of subsequent messages with the same key material
  // would succeed.
  //
  // Critically, plaintext messages never have "real" message content
  // generated by users. Plaintext messages include sender information.
  PLAINTEXT_CONTENT = 3;

  // An unidentified sender message is an encrypted message. No other
  // information about the type of the encrypted message is known to the server.
  //
  // Unidenitfied sender messages require an unidentified access token or a
  // group send endorsement token to prove the unidentified sender is authorized
  // to send messages to the destination.
  UNIDENTIFIED_SENDER = 4;
}

message SendAuthenticatedSenderMessageRequest {

  // The service identifier of the account to which to deliver the message.
  common.ServiceIdentifier destination = 1;

  // If true, this message will only be delivered to destination devices that
  // have an active message delivery channel with a Signal server.
  bool ephemeral = 2;

  // Indicates whether this message is urgent and should trigger a high-priority
  // notification if the destination device does not have an active message
  // delivery channel with a Signal server
  bool urgent = 3;

  // The messages to send to the destination account.
  IndividualRecipientMessageBundle messages = 4;
}

message SendMessageAuthenticatedSenderResponse {

  // The outcome of the message delivery
  oneof response {

    // The message was successfully delivered to all destination devices
    google.protobuf.Empty success = 1;

    // A list of discrepancies between the destination devices identified in a
    // request to send a message and the devices that are actually linked to an
    // account.
    MismatchedDevices mismatched_devices = 2 [(tag.reason) = "mismatched_devices"];

    // A description of a challenge callers must complete before sending
    // additional messages.
    ChallengeRequired challenge_required = 3 [(tag.reason) = "challenge_required"];

    // The destination account did not exist
    errors.NotFound destination_not_found = 4 [(tag.reason) = "destination_not_found"];

  }
}


message SendSyncMessageRequest {

  // Indicates whether this message is urgent and should trigger a high-priority
  // notification if the destination device does not have an active message
  // delivery channel with a Signal server
  bool urgent = 1;

  // The messages to send to the destination account.
  IndividualRecipientMessageBundle messages = 2;
}

message SendSealedSenderMessageRequest {

  // The service identifier of the account to which to deliver the message.
  common.ServiceIdentifier destination = 1;

  // If true, this message will only be delivered to destination devices that
  // have an active message delivery channel with a Signal server.
  bool ephemeral = 2;

  // Indicates whether this message is urgent and should trigger a high-priority
  // notification if the destination device does not have an active message
  // delivery channel with a Signal server
  bool urgent = 3;

  // The messages to send to the destination account.
  IndividualRecipientMessageBundle messages = 4;

  // A means to authorize the request.
  oneof authorization {

    // The unidentified access key (UAK) for the destination account.
    bytes unidentified_access_key = 5 [(require.exactlySize) = 16];

    // A group send endorsement token for the destination account.
    bytes group_send_token = 6;

    // The destination account allows unrestricted unidentified access
    google.protobuf.Empty unrestricted_access = 7;
  }
}

message SendStoryMessageRequest {

  // The service identifier of the account to which to deliver the message.
  common.ServiceIdentifier destination = 1;

  // Indicates whether this message is urgent and should trigger a high-priority
  // notification if the destination device does not have an active message
  // delivery channel with a Signal server
  bool urgent = 2;

  // The messages to send to the destination account.
  IndividualRecipientMessageBundle messages = 3;
}

message SendMessageResponse {

  // The outcome of the message delivery
  oneof response {

    // The message was successfully delivered to all destination devices
    google.protobuf.Empty success = 1;

    // A list of discrepancies between the destination devices identified in a
    // request to send a message and the devices that are actually linked to an
    // account.
    MismatchedDevices mismatched_devices = 2 [(tag.reason) = "mismatched_devices"];

    // The provided unidentified authorization credential was invalid
    errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3 [(tag.reason) = "failed_unidentified_authorization"];

    // The destination account did not exist
    errors.NotFound destination_not_found = 4 [(tag.reason) = "destination_not_found"];

  }
}

message MultiRecipientMessage {

  // The time, in milliseconds since the epoch, at which this message was
  // originally sent from the perspective of the sender. Note that the maximum
  // allowable timestamp for JavaScript clients is less than Long.MAX_VALUE; see
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date
  // for additional details and discussion.
  uint64 timestamp = 1 [(require.range).min = 1, (require.range).max = 8640000000000000];

  // The serialized multi-recipient message payload.
  bytes payload = 2 [(require.size).max = 762144]; // 256 KiB payload + (5000 * 100) of overhead
}

message SendMultiRecipientMessageRequest {

  // If true, this message will only be delivered to destination devices that
  // have an active message delivery channel with a Signal server.
  bool ephemeral = 1;

  // Indicates whether this message is urgent and should trigger a high-priority
  // notification if the destination device does not have an active message
  // delivery channel with a Signal server
  bool urgent = 2;

  // The multi-recipient message to send to all destination accounts and
  // devices.
  MultiRecipientMessage message = 3;

  // A group send endorsement token for the destination account.
  bytes group_send_token = 4 [(require.nonEmpty) = true];
}

message SendMultiRecipientStoryRequest {

  // Indicates whether this message is urgent and should trigger a high-priority
  // notification if the destination device does not have an active message
  // delivery channel with a Signal server
  bool urgent = 1;

  // The multi-recipient story message to send to all destination accounts and
  // devices.
  MultiRecipientMessage message = 2;
}

message MultiRecipientSuccess {
  // A list of destination service identifiers that could not be resolved to
  // registered Signal accounts. The  message in the original request was sent
  // to all service identifiers/devices in the original request except for the
  // destination devices associated with the service identifiers in this list.
  repeated common.ServiceIdentifier unresolved_recipients = 1;
}

message SendMultiRecipientMessageResponse {

  // The outcome of the message delivery
  oneof response {
    // The message was sent to at least some of the destination accounts/devices
    // identified in the original request.
    MultiRecipientSuccess success = 1;

    // A list of sets of discrepancies between the destination devices
    // identified in a request to send a message and the devices that are
    // actually linked to a destination account.
    MultiRecipientMismatchedDevices mismatched_devices = 2 [(tag.reason) = "mismatched_devices"];

    // The provided unidentified authorization credential was invalid
    errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3 [(tag.reason) = "failed_unidentified_authorization"];
  }
}

message MismatchedDevices {

  // The service identifier to which the devices named in this object are
  // linked.
  common.ServiceIdentifier service_identifier = 1;

  // A list of device IDs that are linked to the destination account, but were
  // not included in the collection of messages bound for the destination
  // account.
  repeated uint32 missing_devices = 2 [(require.range).max = 0x7f];

  // A list of device IDs that were included in the collection of messages bound
  // for the destination account, but are not currently linked to the
  // destination account.
  repeated uint32 extra_devices = 3 [(require.range).max = 0x7f];

  // A list of device IDs that present in the collection of messages bound for
  // the destination account and are linked to the destination account, but have
  // a different registration ID than the registration ID presented by the
  // sender (indicating that the destination device has likely been replaced by
  // another device).
  repeated uint32 stale_devices = 4 [(require.range).max = 0x7f];
}

message MultiRecipientMismatchedDevices {

  // A list of sets of discrepancies between the destination devices identified
  // in a request to send a message and the devices that are actually linked to
  // a destination account.
  repeated MismatchedDevices mismatched_devices = 1;
}

message ChallengeRequired {

  enum ChallengeType {
    UNSPECIFIED = 0;

    // A challenge that callers can fulfill by completing a captcha.
    CAPTCHA = 1;

    // A challenge that callers can fulfill by supplying a token delivered via
    // push notification.
    PUSH_CHALLENGE = 2;
  };

  // An opaque token identifying this challenge request. Clients must generally
  // submit this token when submitting a challenge response.
  string token = 1;

  // A list of challenge types callers may choose to complete to resolve the
  // challenge requirement. May be empty, in which case callers cannot resolve
  // the challenge by any means other than waiting.
  repeated ChallengeType challenge_options = 2;

  // A duration (in seconds) after which the challenge requirement may be
  // resolved by simply waiting. May not be set if the challenge cannot be
  // resolved by waiting.
  optional uint64 retry_after_seconds = 3;
}
