// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
syntax = "proto3";

package signalbackups;

option java_package = "org.thoughtcrime.securesms.backup.v2.proto";
option swift_prefix = "BackupProto_";

message BackupInfo {
  uint64 version = 1;
  uint64 backupTimeMs = 2;
  bytes mediaRootBackupKey = 3; // 32-byte random value generated when the backup is uploaded for the first time.
  string currentAppVersion = 4;
  string firstAppVersion = 5;
  bytes debugInfo = 6; // Client-specific data field for debug info during testing
}

// Frames must follow in the following ordering rules:
//
// 1. There is exactly one AccountData and it is the first frame.
// 2. A frame referenced by ID must come before the referencing frame.
//    e.g. a Recipient must come before any Chat referencing it.
// 3. All ChatItems must appear in global Chat rendering order.
//    (The order in which they were received by the client.)
// 4. ChatFolders must appear in render order (e.g., left to right for
//    LTR locales), but can appear anywhere relative to other frames respecting
//    rule 2 (after Recipients and Chats).
//
// Recipients, Chats, StickerPacks, AdHocCalls, and NotificationProfiles
// can be in any order. (But must respect rule 2.)
//
// For example, Chats may all be together at the beginning,
// or may each immediately precede its first ChatItem.
message Frame {
  // If unset, importers should skip this frame without throwing an error.
  oneof item {
    AccountData account = 1;
    Recipient recipient = 2;
    Chat chat = 3;
    ChatItem chatItem = 4;
    StickerPack stickerPack = 5;
    AdHocCall adHocCall = 6;
    NotificationProfile notificationProfile = 7;
    ChatFolder chatFolder = 8;
  }
}

message AccountData {
  enum PhoneNumberSharingMode {
    UNKNOWN = 0; // Interpret as "Nobody"
    EVERYBODY = 1;
    NOBODY = 2;
  }
  message UsernameLink {
    enum Color {
      UNKNOWN = 0; // Interpret as "Blue"
      BLUE = 1;
      WHITE = 2;
      GREY = 3;
      OLIVE = 4;
      GREEN = 5;
      ORANGE = 6;
      PINK = 7;
      PURPLE = 8;
    }

    bytes entropy = 1;  // 32 bytes of entropy used for encryption
    bytes serverId = 2; // 16 bytes of encoded UUID provided by the server
    Color color = 3;
  }

  enum SentMediaQuality {
    UNKNOWN_QUALITY = 0; // Interpret as "Standard"
    STANDARD = 1;
    HIGH = 2;
  }

  message AutoDownloadSettings {
    enum AutoDownloadOption {
      UNKNOWN = 0; // Interpret as "Never"
      NEVER = 1;
      WIFI = 2;
      WIFI_AND_CELLULAR = 3;
    }

    AutoDownloadOption images = 1;
    AutoDownloadOption audio = 2;
    AutoDownloadOption video = 3;
    AutoDownloadOption documents = 4;
  }

  enum AppTheme {
    UNKNOWN_APP_THEME = 0; // Interpret as "System"
    SYSTEM = 1;
    LIGHT = 2;
    DARK = 3;
  }

  enum CallsUseLessDataSetting {
    UNKNOWN_CALL_DATA_SETTING = 0; // Interpret as "Never"
    NEVER = 1;
    MOBILE_DATA_ONLY = 2;
    WIFI_AND_MOBILE_DATA = 3;
  }

  message AccountSettings {
    bool readReceipts = 1;
    bool sealedSenderIndicators = 2;
    bool typingIndicators = 3;
    bool linkPreviews = 4;
    bool notDiscoverableByPhoneNumber = 5;
    bool preferContactAvatars = 6;
    uint32 universalExpireTimerSeconds = 7; // 0 means no universal expire timer.
    repeated string preferredReactionEmoji = 8;
    bool displayBadgesOnProfile = 9;
    bool keepMutedChatsArchived = 10;
    bool hasSetMyStoriesPrivacy = 11;
    bool hasViewedOnboardingStory = 12;
    bool storiesDisabled = 13;
    optional bool storyViewReceiptsEnabled = 14;
    bool hasSeenGroupStoryEducationSheet = 15;
    bool hasCompletedUsernameOnboarding = 16;
    PhoneNumberSharingMode phoneNumberSharingMode = 17;
    ChatStyle defaultChatStyle = 18;
    repeated ChatStyle.CustomChatColor customChatColors = 19;
    bool optimizeOnDeviceStorage = 20;
    // See zkgroup for integer particular values. Unset if backups are not enabled.
    optional uint64 backupTier = 21;
    reserved /* showSealedSenderIndicators */ 22;
    SentMediaQuality defaultSentMediaQuality = 23;
    AutoDownloadSettings autoDownloadSettings = 24;
    reserved /* wifiAutoDownloadSettings */ 25;
    optional uint32 screenLockTimeoutMinutes = 26; // If unset, consider screen lock to be disabled.
    optional bool pinReminders = 27; // If unset, consider pin reminders to be enabled.
    AppTheme appTheme = 28;  // If unset, treat the same as "Unknown" case
    CallsUseLessDataSetting callsUseLessDataSetting = 29;  // If unset, treat the same as "Unknown" case
    bool allowSealedSenderFromAnyone = 30;
    bool allowAutomaticKeyVerification = 31;
    bool hasSeenAdminDeleteEducationDialog = 32;
  }

  message SubscriberData {
    bytes subscriberId = 1;
    string currencyCode = 2;
    bool manuallyCancelled = 3;
  }

  message IAPSubscriberData {
    bytes subscriberId = 1;

    // If unset, importers should ignore the subscriber data without throwing an error.
    oneof iapSubscriptionId {
      // Identifies an Android Play Store IAP subscription.
      string purchaseToken = 2;
      // Identifies an iOS App Store IAP subscription.
      uint64 originalTransactionId = 3;
    }
  }

  message AndroidSpecificSettings {
    enum NavigationBarSize {
      UNKNOWN_BAR_SIZE = 0; // Intepret as "Normal"
      NORMAL = 1;
      COMPACT = 2;
    }

    bool useSystemEmoji = 1;
    bool screenshotSecurity = 2;
    NavigationBarSize navigationBarSize = 3; // If unset, treat the same as "Unknown" case
  }

  bytes profileKey = 1;
  optional string username = 2;
  UsernameLink usernameLink = 3;
  string givenName = 4;
  string familyName = 5;
  string avatarUrlPath = 6;
  SubscriberData donationSubscriberData = 7;
  reserved /*backupsSubscriberData*/ 8; // A deprecated format
  AccountSettings accountSettings = 9;
  IAPSubscriberData backupsSubscriberData = 10;
  string svrPin = 11;
  AndroidSpecificSettings androidSpecificSettings = 12;
  string bioText = 13;
  string bioEmoji = 14;
  // Opaque blob containing key transparency data for the account
  optional bytes keyTransparencyData = 15;
}

message Recipient {
  uint64 id = 1; // generated id for reference only within this file
  // If unset, importers should skip this frame without throwing an error.
  oneof destination {
    Contact contact = 2;
    Group group = 3;
    DistributionListItem distributionList = 4;
    Self self = 5;
    ReleaseNotes releaseNotes = 6;
    CallLink callLink = 7;
  }
}

// If unset - computed as the value of the first byte of SHA-256(msg=CONTACT_ID)
// modulo the count of colors. Once set the avatar color for a recipient is
// never recomputed or changed.
//
// `CONTACT_ID` is the first available identifier from the list:
// - ServiceIdToBinary(ACI)
// - E164
// - ServiceIdToBinary(PNI)
// - Group Id
enum AvatarColor {
  A100 = 0;
  A110 = 1;
  A120 = 2;
  A130 = 3;
  A140 = 4;
  A150 = 5;
  A160 = 6;
  A170 = 7;
  A180 = 8;
  A190 = 9;
  A200 = 10;
  A210 = 11;
}

message Contact {
  enum IdentityState {
    DEFAULT = 0; // A valid value -- indicates unset by the user
    VERIFIED = 1;
    UNVERIFIED = 2; // Was once verified and is now unverified
  }

  message Registered {}
  message NotRegistered {
    uint64 unregisteredTimestamp = 1;
  }

  enum Visibility {
    VISIBLE = 0; // A valid value -- the contact is not hidden
    HIDDEN = 1;
    HIDDEN_MESSAGE_REQUEST = 2;
  }

  message Name {
    string given = 1;
    string family = 2;
  }

  optional bytes aci = 1; // should be 16 bytes
  optional bytes pni = 2; // should be 16 bytes
  optional string username = 3;
  optional uint64 e164 = 4;
  bool blocked = 5;
  Visibility visibility = 6;

  // If unset, consider the user to be registered
  oneof registration {
    Registered registered = 7;
    NotRegistered notRegistered = 8;
  }

  optional bytes profileKey = 9;
  bool profileSharing = 10;
  optional string profileGivenName = 11;
  optional string profileFamilyName = 12;
  bool hideStory = 13;
  optional bytes identityKey = 14;
  IdentityState identityState = 15;
  Name nickname = 16; // absent iff both `given` and `family` are empty
  string note = 17;
  string systemGivenName = 18;
  string systemFamilyName = 19;
  string systemNickname = 20;
  optional AvatarColor avatarColor = 21;
  // Opaque blob containing key transparency data for the contact
  optional bytes keyTransparencyData = 22;
}

message Group {
  enum StorySendMode {
    DEFAULT = 0; // A valid value -- indicates unset by the user
    DISABLED = 1;
    ENABLED = 2;
  }

  bytes masterKey = 1;
  bool whitelisted = 2;
  bool hideStory = 3;
  StorySendMode storySendMode = 4;
  GroupSnapshot snapshot = 5;
  bool blocked = 6;
  optional AvatarColor avatarColor = 7;

  // These are simply plaintext copies of the groups proto from Groups.proto.
  // They should be kept completely in-sync with Groups.proto.
  // These exist to allow us to have the latest snapshot of a group during restoration without having to hit the network.
  // We would use Groups.proto if we could, but we want a plaintext version to improve export readability.
  // For documentation, defer to Groups.proto. The only name change is Group -> GroupSnapshot to avoid the naming conflict.
  message GroupSnapshot {
    reserved /*publicKey*/ 1;  // The field is deprecated in the context of static group state
    GroupAttributeBlob title = 2;
    GroupAttributeBlob description = 11;
    string avatarUrl = 3;
    GroupAttributeBlob disappearingMessagesTimer = 4;
    AccessControl accessControl = 5;
    uint32 version = 6;
    repeated Member members = 7;
    repeated MemberPendingProfileKey membersPendingProfileKey = 8;
    repeated MemberPendingAdminApproval membersPendingAdminApproval = 9;
    bytes inviteLinkPassword = 10;
    bool announcements_only = 12;
    repeated MemberBanned members_banned = 13;
  }

  message GroupAttributeBlob {
    // If unset, consider the field it represents to not be present
    oneof content {
      string title = 1;
      bytes avatar = 2;
      uint32 disappearingMessagesDuration = 3;
      string descriptionText = 4;
    }
  }

  message Member {
    enum Role {
      UNKNOWN = 0; // Intepret as "Default"
      DEFAULT = 1;
      ADMINISTRATOR = 2;
    }

    bytes user_id = 1;
    Role role = 2;
    reserved /*profileKey*/ 3; // This field is ignored in Backups, in favor of Contact frames for members
    reserved /*presentation*/ 4; // This field is deprecated in the context of static group state
    uint32 joinedAtVersion = 5;
    string label_emoji = 6;
    string label_string = 7;
  }

  message MemberPendingProfileKey {
    Member member = 1;
    bytes addedByUserId = 2;
    uint64 timestamp = 3;
  }

  message MemberPendingAdminApproval {
    bytes user_id = 1;
    reserved /*profileKey*/ 2; // This field is ignored in Backups, in favor of Contact frames for members
    reserved /*presentation*/ 3; // This field is deprecated in the context of static group state
    uint64 timestamp = 4;
  }

  message MemberBanned {
    bytes user_id = 1;
    uint64 timestamp = 2;
  }

  message AccessControl {
    enum AccessRequired {
      UNKNOWN = 0; // Intepret as "Unsatisfiable"
      ANY = 1;
      MEMBER = 2;
      ADMINISTRATOR = 3;
      UNSATISFIABLE = 4;
    }

    AccessRequired attributes = 1;
    AccessRequired members = 2;
    AccessRequired addFromInviteLink = 3;
    AccessRequired memberLabel = 4;
  }
}

message Self {
  optional AvatarColor avatarColor = 1;
}

message ReleaseNotes {}

message Chat {
  uint64 id = 1; // generated id for reference only within this file
  uint64 recipientId = 2;
  bool archived = 3;
  optional uint32 pinnedOrder = 4; // will be displayed in ascending order
  optional uint64 expirationTimerMs = 5;
  optional uint64 muteUntilMs = 6; // INT64_MAX (2^63 - 1) = "always muted".
  bool markedUnread = 7;
  bool dontNotifyForMentionsIfMuted = 8;
  ChatStyle style = 9;
  uint32 expireTimerVersion = 10;
}

/**
 * Call Links have some associated data including a call, but unlike other recipients
 * are not tied to threads because they do not have messages associated with them.
 *
 * note:
 * - room id can be derived from the root key
 * - the presence of an admin key means this user is a call admin
 */
message CallLink {
  enum Restrictions {
    UNKNOWN = 0; // Interpret as "Admin Approval"
    NONE = 1;
    ADMIN_APPROVAL = 2;
  }

  bytes rootKey = 1;
  optional bytes adminKey = 2; // Only present if the user is an admin
  string name = 3;
  Restrictions restrictions = 4;
  uint64 expirationMs = 5;
  reserved 6; // was epoch, never used
}

message AdHocCall {
  enum State {
    UNKNOWN_STATE = 0; // Interpret as "Generic"
    GENERIC = 1;
  }

  uint64 callId = 1;
  // Refers to a `CallLink` recipient.
  uint64 recipientId = 2;
  State state = 3;
  uint64 callTimestamp = 4;
}

message DistributionListItem {
  // distribution ids are UUIDv4s. "My Story" is represented
  // by an all-0 UUID (00000000-0000-0000-0000-000000000000).
  bytes distributionId = 1; // distribution list ids are uuids

  // If unset, importers should skip the item entirely without showing an error.
  oneof item {
    uint64 deletionTimestamp = 2;
    DistributionList distributionList = 3;
  }
}

message DistributionList {
  enum PrivacyMode {
    UNKNOWN = 0; // Interpret as "Only with"
    ONLY_WITH = 1;
    ALL_EXCEPT = 2;
    ALL = 3;
  }

  string name = 1;
  bool allowReplies = 2;
  PrivacyMode privacyMode = 3;
  repeated uint64 memberRecipientIds = 4; // generated recipient id
}

message ChatItem {
  message IncomingMessageDetails {
    uint64 dateReceived = 1;
    optional uint64 dateServerSent = 2;
    bool read = 3;
    bool sealedSender = 4;
  }

  message OutgoingMessageDetails {
    repeated SendStatus sendStatus = 1;
    uint64 dateReceived = 2; // may be different from dateSent for sync messages
  }

  message DirectionlessMessageDetails {
  }

  message PinDetails {
    uint64 pinnedAtTimestamp = 1;
    oneof pinExpiry {
      uint64 pinExpiresAtTimestamp = 2; // timestamp when the pin should expire
      bool pinNeverExpires = 3;
    }
  }

  uint64 chatId = 1;   // conversation id
  uint64 authorId = 2; // recipient id
  uint64 dateSent = 3;
  optional uint64 expireStartDate = 4; // timestamp of when expiration timer started ticking down
  optional uint64 expiresInMs = 5; // how long timer of message is (ms)
  repeated ChatItem revisions = 6; // ordered from oldest to newest
  bool sms = 7;

  // If unset, importers should skip this item without throwing an error.
  oneof directionalDetails {
    IncomingMessageDetails incoming = 8;
    OutgoingMessageDetails outgoing = 9;
    DirectionlessMessageDetails directionless = 10;
  }

  // If unset, importers should skip this item without throwing an error.
  oneof item {
    StandardMessage standardMessage = 11;
    ContactMessage contactMessage = 12;
    StickerMessage stickerMessage = 13;
    RemoteDeletedMessage remoteDeletedMessage = 14;
    ChatUpdateMessage updateMessage = 15;
    PaymentNotification paymentNotification = 16;
    GiftBadge giftBadge = 17;
    ViewOnceMessage viewOnceMessage = 18;
    DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up
    Poll poll = 20;
    AdminDeletedMessage adminDeletedMessage = 22;
  }

  PinDetails pinDetails = 21; // only set if message is pinned
}

message SendStatus {
  message Pending {}

  message Sent {
    bool sealedSender = 1;
  }

  message Delivered {
    bool sealedSender = 1;
  }

  message Read {
    bool sealedSender = 1;
  }

  message Viewed {
    bool sealedSender = 1;
  }

  // e.g. user in group was blocked, so we skipped sending to them
  message Skipped {}

  message Failed {
    enum FailureReason {
      UNKNOWN = 0; // A valid value -- could indicate a crash or lack of information
      NETWORK = 1;
      IDENTITY_KEY_MISMATCH = 2;
    }

    FailureReason reason = 1;
  }

  uint64 recipientId = 1;
  uint64 timestamp = 2; // the time the status was last updated -- if from a receipt, it should be the sentTime of the receipt

  // If unset, importers should consider the status to be "pending"
  oneof deliveryStatus {
    Pending pending = 3;
    Sent sent = 4;
    Delivered delivered = 5;
    Read read = 6;
    Viewed viewed = 7;
    Skipped skipped = 8;
    Failed failed = 9;
  }
}

message Text {
  string body = 1;
  repeated BodyRange bodyRanges = 2;
}

message StandardMessage {
  optional Quote quote = 1;
  optional Text text = 2;
  repeated MessageAttachment attachments = 3;
  repeated LinkPreview linkPreview = 4;
  optional FilePointer longText = 5;
  repeated Reaction reactions = 6;
}

message ContactMessage {
  ContactAttachment contact = 1;
  repeated Reaction reactions = 2;
}

message DirectStoryReplyMessage {
  message TextReply {
    Text text = 1;
    FilePointer longText = 2;
  }

  // If unset, importers should ignore the message without throwing an error.
  oneof reply {
    TextReply textReply = 1;
    string emoji = 2;
  }

  repeated Reaction reactions = 3;
  reserved /*storySentTimestamp*/ 4;
}

message PaymentNotification {
  message TransactionDetails {
    message MobileCoinTxoIdentification { // Used to map to payments on the ledger
      repeated bytes publicKey = 1; // for received transactions
      repeated bytes keyImages = 2; // for sent transactions
    }

    message FailedTransaction { // Failed payments can't be synced from the ledger
      enum FailureReason {
        GENERIC = 0; // A valid value -- reason unknown
        NETWORK = 1;
        INSUFFICIENT_FUNDS = 2;
      }
      FailureReason reason = 1;
    }

    message Transaction {
      enum Status {
        INITIAL = 0; // A valid value -- state unconfirmed
        SUBMITTED = 1;
        SUCCESSFUL = 2;
      }
      Status status = 1;

      // This identification is used to map the payment table to the ledger
      // and is likely required otherwise we may have issues reconciling with
      // the ledger
      MobileCoinTxoIdentification mobileCoinIdentification = 2;
      optional uint64 timestamp = 3;
      optional uint64 blockIndex = 4;
      optional uint64 blockTimestamp = 5;
      optional bytes transaction = 6; // mobile coin blobs
      optional bytes receipt = 7; // mobile coin blobs
    }

    // If unset, importers should treat the transaction as successful with no metadata.
    oneof payment {
      Transaction transaction = 1;
      FailedTransaction failedTransaction = 2;
    }
  }

  optional string amountMob = 1; // stored as a decimal string, e.g. 1.00001
  optional string feeMob = 2; // stored as a decimal string, e.g. 1.00001
  optional string note = 3;
  TransactionDetails transactionDetails = 4;
}

message GiftBadge {
  enum State {
    UNOPENED = 0; // A valid state
    OPENED = 1;
    REDEEMED = 2;
    FAILED = 3;
  }

  bytes receiptCredentialPresentation = 1;
  State state = 2;
}

message ViewOnceMessage {
  // Will be null for viewed messages
  MessageAttachment attachment = 1;
  repeated Reaction reactions = 2;
}

message ContactAttachment {
  message Name {
    string givenName = 1;
    string familyName = 2;
    string prefix = 3;
    string suffix = 4;
    string middleName = 5;
    string nickname = 6;
  }

  message Phone {
    enum Type {
      UNKNOWN = 0; // Interpret as "Home"
      HOME = 1;
      MOBILE = 2;
      WORK = 3;
      CUSTOM = 4;
    }

    string value = 1;
    Type type = 2;
    string label = 3;
  }

  message Email {
    enum Type {
      UNKNOWN = 0; // Intepret as "Home"
      HOME = 1;
      MOBILE = 2;
      WORK = 3;
      CUSTOM = 4;
    }

    string value = 1;
    Type type = 2;
    string label = 3;
  }

  message PostalAddress {
    enum Type {
      UNKNOWN = 0; // Interpret as "Home"
      HOME = 1;
      WORK = 2;
      CUSTOM = 3;
    }

    Type type = 1;
    string label = 2;
    string street = 3;
    string pobox = 4;
    string neighborhood = 5;
    string city = 6;
    string region = 7;
    string postcode = 8;
    string country = 9;
  }

  optional Name name = 1;
  repeated Phone number = 3;
  repeated Email email = 4;
  repeated PostalAddress address = 5;
  optional FilePointer avatar = 6;
  string organization = 7;
}

message StickerMessage {
  Sticker sticker = 1;
  repeated Reaction reactions = 2;
}

// Tombstone for remote delete
message RemoteDeletedMessage {}

message Sticker {
  bytes packId = 1;
  bytes packKey = 2;
  uint32 stickerId = 3;
  optional string emoji = 4;
  // Stickers are uploaded to be sent as attachments; we also
  // back them up as normal attachments when they are in messages.
  // DO NOT treat this as the definitive source of a sticker in
  // an installed StickerPack that shares the same packId.
  FilePointer data = 5;
}

message LinkPreview {
  string url = 1;
  optional string title = 2;
  optional FilePointer image = 3;
  optional string description = 4;
  optional uint64 date = 5;
}

// A FilePointer on a message that has additional
// metadata that applies only to message attachments.
message MessageAttachment {
  // Similar to SignalService.AttachmentPointer.Flags,
  // but explicitly mutually exclusive. Note the different raw values
  // (non-zero starting values are not supported in proto3.)
  enum Flag {
    NONE = 0; // A valid value -- no flag applied
    VOICE_MESSAGE = 1;
    BORDERLESS = 2;
    GIF = 3;
  }

  FilePointer pointer = 1;
  Flag flag = 2;
  bool wasDownloaded = 3;
  // Cross-client identifier for this attachment among all attachments on the
  // owning message. See: SignalService.AttachmentPointer.clientUuid.
  optional bytes clientUuid = 4;
}

message FilePointer {
  message LocatorInfo {
    // Must be non-empty if transitCdnKey or plaintextHash are set/nonempty.
    // Otherwise must be empty.
    bytes key = 1;

    reserved /*legacyDigest*/ 2;

    oneof integrityCheck {
      // Set if file was at one point downloaded and its plaintextHash was calculated
      bytes plaintextHash = 10;

      // Set if file has not been downloaded so its integrity has not been verified
      // From the sender of the attachment
      bytes encryptedDigest = 11;
    }

    // NB: This is the plaintext size, and empty content attachments are legal, so this
    // may be zero even if transitCdnKey or mediaName are set/nonempty.
    uint32 size = 3;

    // Either both transit cdn key and number are set or neither should be set.
    // Upload timestamp is optional but should only be set if key/number are set.
    optional string transitCdnKey = 4;
    optional uint32 transitCdnNumber = 5;
    optional uint64 transitTierUploadTimestamp = 6;

    // If present, the cdn number of the succesful upload to media tier.
    // If unset, may still have been uploaded, and clients
    // can discover the cdn number via the list endpoint.
    // Exporting clients should set this as long as their subscription
    // has not rotated since last upload; even if currently free tier.
    optional uint32 mediaTierCdnNumber = 7;

    reserved /*legacyMediaName*/ 8;

    // Separate key used to encrypt this file for the local backup.
    // Generally required for local backups.
    // Missing field indicates attachment was not available locally
    // when the backup was generated, but remote backup or transit
    // info was available.
    optional bytes localKey = 9;
  }

  reserved /*backupLocator*/ 1;
  reserved /*attachmentLocator*/ 2;
  reserved /*invalidAttachmentLocator*/ 3;
  reserved /*localLocator*/ 12;

  optional string contentType = 4;
  optional bytes incrementalMac = 5;
  optional uint32 incrementalMacChunkSize = 6;
  optional string fileName = 7;
  optional uint32 width = 8;
  optional uint32 height = 9;
  optional string caption = 10;
  optional string blurHash = 11;
  LocatorInfo locatorInfo = 13;
}

message Quote {
  enum Type {
    UNKNOWN = 0; // Interpret as "Normal"
    NORMAL = 1;
    GIFT_BADGE = 2;
    VIEW_ONCE = 3;
    POLL = 4;
  }

  message QuotedAttachment {
    optional string contentType = 1;
    optional string fileName = 2;
    optional MessageAttachment thumbnail = 3;
  }

  optional uint64 targetSentTimestamp = 1; // null if the target message could not be found at time of quote insert
  uint64 authorId = 2;
  optional Text text = 3;
  repeated QuotedAttachment attachments = 4;
  Type type = 5;
}

message BodyRange {
  enum Style {
    NONE = 0; // Importers should ignore the body range without throwing an error.
    BOLD = 1;
    ITALIC = 2;
    SPOILER = 3;
    STRIKETHROUGH = 4;
    MONOSPACE = 5;
  }

  // 'start' and 'length' are measured in UTF-16 code units.
  // They may refer to offsets in a longText attachment.
  uint32 start = 1;
  uint32 length = 2;

  // If unset, importers should ignore the body range without throwing an error.
  oneof associatedValue {
    bytes mentionAci = 3;
    Style style = 4;
  }
}

message Reaction {
  string emoji = 1;
  uint64 authorId = 2;
  uint64 sentTimestamp = 3;
  // A higher sort order means that a reaction is more recent. Some clients may export this as
  // incrementing numbers (e.g. 1, 2, 3), others as timestamps.
  uint64 sortOrder = 4;
}

message Poll {

  message PollOption {

    message PollVote {
      uint64 voterId = 1; // A direct reference to Recipient proto id. Must be self or contact.
      uint32 voteCount = 2; // Tracks how many times you voted.
    }

    string option = 1; // Between 1-100 characters
    repeated PollVote votes = 2;
  }

  string question = 1; // Between 1-100 characters
  bool allowMultiple = 2;
  repeated PollOption options = 3; // At least two
  bool hasEnded = 4;
  repeated Reaction reactions = 5;
}

message AdminDeletedMessage {
  uint64 adminId = 1; // id of the admin that deleted the message
}

message ChatUpdateMessage {
  // If unset, importers should ignore the update message without throwing an error.
  oneof update {
    SimpleChatUpdate simpleUpdate = 1;
    GroupChangeChatUpdate groupChange = 2;
    ExpirationTimerChatUpdate expirationTimerChange = 3;
    ProfileChangeChatUpdate profileChange = 4;
    ThreadMergeChatUpdate threadMerge = 5;
    SessionSwitchoverChatUpdate sessionSwitchover = 6;
    IndividualCall individualCall = 7;
    GroupCall groupCall = 8;
    LearnedProfileChatUpdate learnedProfileChange = 9;
    PollTerminateUpdate pollTerminate = 10;
    PinMessageUpdate pinMessage = 11;
  }
}

message IndividualCall {
  enum Type {
    UNKNOWN_TYPE = 0; // Interpret as "Audio call"
    AUDIO_CALL = 1;
    VIDEO_CALL = 2;
  }

  enum Direction {
    UNKNOWN_DIRECTION = 0; // Interpret as "Incoming"
    INCOMING = 1;
    OUTGOING = 2;
  }

  enum State {
    UNKNOWN_STATE = 0; // Interpret as "Accepted"
    ACCEPTED = 1;
    NOT_ACCEPTED = 2;
    // An incoming call that is no longer ongoing, which we neither accepted
    // not actively declined. For example, it expired, was canceled by the
    // sender, or was rejected due to being in another call.
    MISSED = 3;
    // We auto-declined an incoming call due to a notification profile.
    MISSED_NOTIFICATION_PROFILE = 4;
  }

  optional uint64 callId = 1;
  Type type = 2;
  Direction direction = 3;
  State state = 4;
  uint64 startedCallTimestamp = 5;
  bool read = 6;
}

message GroupCall {
  enum State {
    UNKNOWN_STATE = 0; // Interpret as "Generic"
    // A group call was started without ringing.
    GENERIC = 1;
    // We joined a group call that was started without ringing.
    JOINED = 2;
    // An incoming group call is actively ringing.
    RINGING = 3;
    // We accepted an incoming group ring.
    ACCEPTED = 4;
    // We declined an incoming group ring.
    DECLINED = 5;
    // We missed an incoming group ring, for example because it expired.
    MISSED = 6;
    // We auto-declined an incoming group ring due to a notification profile.
    MISSED_NOTIFICATION_PROFILE = 7;
    // An outgoing ring was started. We don't track any state for outgoing rings
    // beyond that they started.
    OUTGOING_RING = 8;
  }

  optional uint64 callId = 1;
  State state = 2;
  optional uint64 ringerRecipientId = 3;
  optional uint64 startedCallRecipientId = 4;
  uint64 startedCallTimestamp = 5;
  optional uint64 endedCallTimestamp = 6; // The time the call ended.
  bool read = 7;
}

message SimpleChatUpdate {
  enum Type {
    UNKNOWN = 0; // Importers should skip the update without throwing an error.
    JOINED_SIGNAL = 1;
    IDENTITY_UPDATE = 2;
    IDENTITY_VERIFIED = 3;
    IDENTITY_DEFAULT = 4; // marking as unverified
    CHANGE_NUMBER = 5;
    RELEASE_CHANNEL_DONATION_REQUEST = 6;
    END_SESSION = 7;
    CHAT_SESSION_REFRESH = 8;
    BAD_DECRYPT = 9;
    PAYMENTS_ACTIVATED = 10;
    PAYMENT_ACTIVATION_REQUEST = 11;
    UNSUPPORTED_PROTOCOL_MESSAGE = 12;
    REPORTED_SPAM = 13;
    BLOCKED = 14;
    UNBLOCKED = 15;
    MESSAGE_REQUEST_ACCEPTED = 16;
  }

  Type type = 1;
}

// For 1:1 chat updates only.
// For group thread updates use GroupExpirationTimerUpdate.
message ExpirationTimerChatUpdate {
  uint64 expiresInMs = 1; // 0 means the expiration timer was disabled
}

message ProfileChangeChatUpdate {
  string previousName = 1;
  string newName = 2;
}

message LearnedProfileChatUpdate {
  // If unset, importers should consider the previous name to be an empty string.
  oneof previousName {
    uint64 e164 = 1;
    string username = 2;
  }
}

message ThreadMergeChatUpdate {
  uint64 previousE164 = 1;
}

message SessionSwitchoverChatUpdate {
  uint64 e164 = 1;
}

message GroupChangeChatUpdate {
  message Update {
    // If unset, importers should consider it to be a GenericGroupUpdate with unset updaterAci
    oneof update {
      GenericGroupUpdate genericGroupUpdate = 1;
      GroupCreationUpdate groupCreationUpdate = 2;
      GroupNameUpdate groupNameUpdate = 3;
      GroupAvatarUpdate groupAvatarUpdate = 4;
      GroupDescriptionUpdate groupDescriptionUpdate = 5;
      GroupMembershipAccessLevelChangeUpdate groupMembershipAccessLevelChangeUpdate = 6;
      GroupMemberLabelAccessLevelChangeUpdate groupMemberLabelAccessLevelChangeUpdate = 35;
      GroupAttributesAccessLevelChangeUpdate groupAttributesAccessLevelChangeUpdate = 7;
      GroupAnnouncementOnlyChangeUpdate groupAnnouncementOnlyChangeUpdate = 8;
      GroupAdminStatusUpdate groupAdminStatusUpdate = 9;
      GroupMemberLeftUpdate groupMemberLeftUpdate = 10;
      GroupMemberRemovedUpdate groupMemberRemovedUpdate = 11;
      SelfInvitedToGroupUpdate selfInvitedToGroupUpdate = 12;
      SelfInvitedOtherUserToGroupUpdate selfInvitedOtherUserToGroupUpdate = 13;
      GroupUnknownInviteeUpdate groupUnknownInviteeUpdate = 14;
      GroupInvitationAcceptedUpdate groupInvitationAcceptedUpdate = 15;
      GroupInvitationDeclinedUpdate groupInvitationDeclinedUpdate = 16;
      GroupMemberJoinedUpdate groupMemberJoinedUpdate = 17;
      GroupMemberAddedUpdate groupMemberAddedUpdate = 18;
      GroupSelfInvitationRevokedUpdate groupSelfInvitationRevokedUpdate = 19;
      GroupInvitationRevokedUpdate groupInvitationRevokedUpdate = 20;
      GroupJoinRequestUpdate groupJoinRequestUpdate = 21;
      GroupJoinRequestApprovalUpdate groupJoinRequestApprovalUpdate = 22;
      GroupJoinRequestCanceledUpdate groupJoinRequestCanceledUpdate = 23;
      GroupInviteLinkResetUpdate groupInviteLinkResetUpdate = 24;
      GroupInviteLinkEnabledUpdate groupInviteLinkEnabledUpdate = 25;
      GroupInviteLinkAdminApprovalUpdate groupInviteLinkAdminApprovalUpdate = 26;
      GroupInviteLinkDisabledUpdate groupInviteLinkDisabledUpdate = 27;
      GroupMemberJoinedByLinkUpdate groupMemberJoinedByLinkUpdate = 28;
      GroupV2MigrationUpdate groupV2MigrationUpdate = 29;
      GroupV2MigrationSelfInvitedUpdate groupV2MigrationSelfInvitedUpdate = 30;
      GroupV2MigrationInvitedMembersUpdate groupV2MigrationInvitedMembersUpdate = 31;
      GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32;
      GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33;
      GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34;
      // next: 36
    }
  }

  // Must be one or more; all updates batched together came from
  // a single batched group state update.
  repeated Update updates = 1;
}

message GenericGroupUpdate {
  optional bytes updaterAci = 1;
}

message GroupCreationUpdate {
  optional bytes updaterAci = 1;
}

message GroupNameUpdate {
  optional bytes updaterAci = 1;
  // Null value means the group name was removed.
  optional string newGroupName = 2;
}

message GroupAvatarUpdate {
  optional bytes updaterAci = 1;
  bool wasRemoved = 2;
}

message GroupDescriptionUpdate {
  optional bytes updaterAci = 1;
  // Null value means the group description was removed.
  optional string newDescription = 2;
}

enum GroupV2AccessLevel {
  UNKNOWN = 0; // Interpret as "Unsatisfiable"
  ANY = 1;
  MEMBER = 2;
  ADMINISTRATOR = 3;
  UNSATISFIABLE = 4;
}

message GroupMembershipAccessLevelChangeUpdate {
  optional bytes updaterAci = 1;
  GroupV2AccessLevel accessLevel = 2;
}

message GroupMemberLabelAccessLevelChangeUpdate {
  optional bytes updaterAci = 1;
  GroupV2AccessLevel accessLevel = 2;
}

message GroupAttributesAccessLevelChangeUpdate {
  optional bytes updaterAci = 1;
  GroupV2AccessLevel accessLevel = 2;
}

message GroupAnnouncementOnlyChangeUpdate {
  optional bytes updaterAci = 1;
  bool isAnnouncementOnly = 2;
}

message GroupAdminStatusUpdate {
  optional bytes updaterAci = 1;
  // The aci who had admin status granted or revoked.
  bytes memberAci = 2;
  bool wasAdminStatusGranted = 3;
}

message GroupMemberLeftUpdate {
  bytes aci = 1;
}

message GroupMemberRemovedUpdate {
  optional bytes removerAci = 1;
  bytes removedAci = 2;
}

message SelfInvitedToGroupUpdate {
  optional bytes inviterAci = 1;
}

message SelfInvitedOtherUserToGroupUpdate {
  // If no invitee id available, use GroupUnknownInviteeUpdate
  bytes inviteeServiceId = 1;
}

message GroupUnknownInviteeUpdate {
  // Can be the self user.
  optional bytes inviterAci = 1;
  uint32 inviteeCount = 2;
}

message GroupInvitationAcceptedUpdate {
  optional bytes inviterAci = 1;
  bytes newMemberAci = 2;
}

message GroupInvitationDeclinedUpdate {
  optional bytes inviterAci = 1;
  // Note: if invited by pni, just set inviteeAci to nil.
  optional bytes inviteeAci = 2;
}

message GroupMemberJoinedUpdate {
  bytes newMemberAci = 1;
}

message GroupMemberAddedUpdate {
  optional bytes updaterAci = 1;
  bytes newMemberAci = 2;
  bool hadOpenInvitation = 3;
  // If hadOpenInvitation is true, optionally include aci of the inviter.
  optional bytes inviterAci = 4;
}

// An invitation to self was revoked.
message GroupSelfInvitationRevokedUpdate {
  optional bytes revokerAci = 1;
}

// These invitees should never be the local user.
// Use GroupSelfInvitationRevokedUpdate in those cases.
// The inviter or updater can be the local user.
message GroupInvitationRevokedUpdate {
  message Invitee {
    optional bytes inviterAci = 1;
    // Prefer to use aci over pni. No need to set
    // pni if aci is set. Both can be missing.
    optional bytes inviteeAci = 2;
    optional bytes inviteePni = 3;
  }

  // The member that revoked the invite(s), not the inviter!
  // Assumed to be an admin (at the time, may no longer be an
  // admin or even a member).
  optional bytes updaterAci = 1;
  repeated Invitee invitees = 2;
}

message GroupJoinRequestUpdate {
  bytes requestorAci = 1;
}

message GroupJoinRequestApprovalUpdate {
  bytes requestorAci = 1;
  // The aci that approved or rejected the request.
  optional bytes updaterAci = 2;
  bool wasApproved = 3;
}

message GroupJoinRequestCanceledUpdate {
  bytes requestorAci = 1;
}

// A single requestor has requested to join and cancelled
// their request repeatedly with no other updates in between.
// The last action encompassed by this update is always a
// cancellation; if there was another open request immediately
// after, it will be a separate GroupJoinRequestUpdate, either
// in the same frame or in a subsequent frame.
message GroupSequenceOfRequestsAndCancelsUpdate {
  bytes requestorAci = 1;
  uint32 count = 2;
}

message GroupInviteLinkResetUpdate {
  optional bytes updaterAci = 1;
}

message GroupInviteLinkEnabledUpdate {
  optional bytes updaterAci = 1;
  bool linkRequiresAdminApproval = 2;
}

message GroupInviteLinkAdminApprovalUpdate {
  optional bytes updaterAci = 1;
  bool linkRequiresAdminApproval = 2;
}

message GroupInviteLinkDisabledUpdate {
  optional bytes updaterAci = 1;
}

message GroupMemberJoinedByLinkUpdate {
  bytes newMemberAci = 1;
}

// A gv1->gv2 migration occurred.
message GroupV2MigrationUpdate {}

// Another user migrated gv1->gv2 but was unable to add
// the local user and invited them instead.
message GroupV2MigrationSelfInvitedUpdate {}

// The local user migrated gv1->gv2 but was unable to
// add some members and invited them instead.
// (Happens if we don't have the invitee's profile key)
message GroupV2MigrationInvitedMembersUpdate {
  uint32 invitedMembersCount = 1;
}

// The local user migrated gv1->gv2 but was unable to
// add or invite some members and dropped them instead.
// (Happens for e164 members where we don't have an aci).
message GroupV2MigrationDroppedMembersUpdate {
  uint32 droppedMembersCount = 1;
}

// For 1:1 timer updates, use ExpirationTimerChatUpdate.
message GroupExpirationTimerUpdate {
  uint64 expiresInMs = 1; // 0 means the expiration timer was disabled
  optional bytes updaterAci = 2;
}

message PollTerminateUpdate {
  uint64 targetSentTimestamp = 1;
  string question = 2; // Between 1-100 characters
}

message PinMessageUpdate {
  uint64 targetSentTimestamp = 1;
  uint64 authorId = 2; // recipient id
}

message StickerPack {
  bytes packId = 1;
  bytes packKey = 2;
}

message ChatStyle {
  message Gradient {
    uint32 angle = 1; // degrees
    repeated fixed32 colors = 2; // 0xAARRGGBB
    repeated float positions = 3; // percent from 0 to 1
  }

  message CustomChatColor {
    uint64 id = 1;

    // If unset, use the default chat color
    oneof color {
      fixed32 solid = 2; // 0xAARRGGBB
      Gradient gradient = 3;
    }
  }

  message AutomaticBubbleColor {
  }

  enum WallpaperPreset {
    UNKNOWN_WALLPAPER_PRESET = 0; // Interpret as the wallpaper being unset
    SOLID_BLUSH = 1;
    SOLID_COPPER = 2;
    SOLID_DUST = 3;
    SOLID_CELADON = 4;
    SOLID_RAINFOREST = 5;
    SOLID_PACIFIC = 6;
    SOLID_FROST = 7;
    SOLID_NAVY = 8;
    SOLID_LILAC = 9;
    SOLID_PINK = 10;
    SOLID_EGGPLANT = 11;
    SOLID_SILVER = 12;
    GRADIENT_SUNSET = 13;
    GRADIENT_NOIR = 14;
    GRADIENT_HEATMAP = 15;
    GRADIENT_AQUA = 16;
    GRADIENT_IRIDESCENT = 17;
    GRADIENT_MONSTERA = 18;
    GRADIENT_BLISS = 19;
    GRADIENT_SKY = 20;
    GRADIENT_PEACH = 21;
  }

  enum BubbleColorPreset {
    UNKNOWN_BUBBLE_COLOR_PRESET = 0; // Interpret as the user's default chat bubble color
    SOLID_ULTRAMARINE = 1;
    SOLID_CRIMSON = 2;
    SOLID_VERMILION = 3;
    SOLID_BURLAP = 4;
    SOLID_FOREST = 5;
    SOLID_WINTERGREEN = 6;
    SOLID_TEAL = 7;
    SOLID_BLUE = 8;
    SOLID_INDIGO = 9;
    SOLID_VIOLET = 10;
    SOLID_PLUM = 11;
    SOLID_TAUPE = 12;
    SOLID_STEEL = 13;
    GRADIENT_EMBER = 14;
    GRADIENT_MIDNIGHT = 15;
    GRADIENT_INFRARED = 16;
    GRADIENT_LAGOON = 17;
    GRADIENT_FLUORESCENT = 18;
    GRADIENT_BASIL = 19;
    GRADIENT_SUBLIME = 20;
    GRADIENT_SEA = 21;
    GRADIENT_TANGERINE = 22;
  }

  // If unset, importers should consider there to be no wallpaper.
  oneof wallpaper {
    WallpaperPreset wallpaperPreset = 1;
    // This `FilePointer` is expected not to contain a `fileName`, `width`,
    // `height`, or `caption`.
    FilePointer wallpaperPhoto = 2;
  }

  // If unset, importers should consider it to be AutomaticBubbleColor
  oneof bubbleColor {
    // Bubble setting is automatically determined based on the wallpaper setting,
    // or `SOLID_ULTRAMARINE` for `noWallpaper`
    AutomaticBubbleColor autoBubbleColor = 3;
    BubbleColorPreset bubbleColorPreset = 4;

    // See AccountSettings.customChatColors
    uint64 customColorId = 5;
  }

  bool dimWallpaperInDarkMode = 7;
}

message NotificationProfile {
  enum DayOfWeek {
    UNKNOWN = 0; // Interpret as "Monday"
    MONDAY = 1;
    TUESDAY = 2;
    WEDNESDAY = 3;
    THURSDAY = 4;
    FRIDAY = 5;
    SATURDAY = 6;
    SUNDAY = 7;
  }

  string name = 1;
  optional string emoji = 2;
  fixed32 color = 3; // 0xAARRGGBB
  uint64 createdAtMs = 4;
  bool allowAllCalls = 5;
  bool allowAllMentions = 6;
  repeated uint64 allowedMembers = 7; // generated recipient id for allowed groups and contacts
  bool scheduleEnabled = 8;
  uint32 scheduleStartTime = 9; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345)
  uint32 scheduleEndTime = 10; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345)
  repeated DayOfWeek scheduleDaysEnabled = 11;
  bytes id = 12; // should be 16 bytes
}

message ChatFolder {
  // Represents the default "All chats" folder record vs all other custom folders
  enum FolderType {
    UNKNOWN = 0; // Interpret as "Custom"
    ALL = 1;
    CUSTOM = 2;
  }

  string name = 1;
  bool showOnlyUnread = 2;
  bool showMutedChats = 3;
  // Folder includes all 1:1 chats, unless excluded
  bool includeAllIndividualChats = 4;
  // Folder includes all group chats, unless excluded
  bool includeAllGroupChats = 5;
  FolderType folderType = 6;
  repeated uint64 includedRecipientIds = 7; // generated recipient id of groups, contacts, and/or note to self
  repeated uint64 excludedRecipientIds = 8; // generated recipient id of groups, contacts, and/or note to self
  bytes id = 9; // should be 16 bytes
}
