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

message BackupInfo {
  uint64 version = 1;
  uint64 backupTimeMs = 2;
}

// 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.)
//
// Recipients, Chats, StickerPacks, and AdHocCalls 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 {
  oneof item {
    AccountData account = 1;
    Recipient recipient = 2;
    Chat chat = 3;
    ChatItem chatItem = 4;
    StickerPack stickerPack = 5;
    AdHocCall adHocCall = 6;
  }
}

message AccountData {
  enum PhoneNumberSharingMode {
    UNKNOWN = 0;
    EVERYBODY = 1;
    NOBODY = 2;
  }
  message UsernameLink {
    enum Color {
      UNKNOWN = 0;
      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;
  }

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

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

  bytes profileKey = 1;
  optional string username = 2;
  UsernameLink usernameLink = 3;
  string givenName = 4;
  string familyName = 5;
  string avatarUrlPath = 6;
  SubscriberData donationSubscriberData = 7;
  SubscriberData backupsSubscriberData = 8;
  AccountSettings accountSettings = 9;
}

message Recipient {
  uint64 id = 1; // generated id for reference only within this file
  oneof destination {
    Contact contact = 2;
    Group group = 3;
    DistributionListItem distributionList = 4;
    Self self = 5;
    ReleaseNotes releaseNotes = 6;
    CallLink callLink = 7;
  }
}

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

  enum Visibility {
    VISIBLE = 0;
    HIDDEN = 1;
    HIDDEN_MESSAGE_REQUEST = 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;

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

message Group {
  enum StorySendMode {
    DEFAULT = 0;
    DISABLED = 1;
    ENABLED = 2;
  }

  bytes masterKey = 1;
  bool whitelisted = 2;
  bool hideStory = 3;
  StorySendMode storySendMode = 4;
  GroupSnapshot snapshot = 5;

  // 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 {
    oneof content {
      string title = 1;
      bytes  avatar = 2;
      uint32 disappearingMessagesDuration = 3;
      string descriptionText = 4;
    }
  }

  message Member {
    enum Role {
      UNKNOWN = 0;
      DEFAULT = 1;
      ADMINISTRATOR = 2;
    }

    bytes userId = 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;
  }

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

  message MemberPendingAdminApproval {
    bytes userId = 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 userId = 1;
    uint64 timestamp = 2;
  }

  message AccessControl {
    enum AccessRequired {
      UNKNOWN = 0;
      ANY = 1;
      MEMBER = 2;
      ADMINISTRATOR = 3;
      UNSATISFIABLE = 4;
    }

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

message Self {}

message ReleaseNotes {}

message Chat {
  uint64 id = 1; // generated id for reference only within this file
  uint64 recipientId = 2;
  bool archived = 3;
  uint32 pinnedOrder = 4; // 0 = unpinned, otherwise chat is considered pinned and will be displayed in ascending order
  uint64 expirationTimerMs = 5; // 0 = no expire timer.
  uint64 muteUntilMs = 6;
  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;
    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;
}

message AdHocCall {
  enum State {
    UNKNOWN_STATE = 0;
    GENERIC = 1;
  }

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

message DistributionListItem {
  bytes distributionId = 1; // distribution list ids are uuids

  oneof item {
    uint64 deletionTimestamp = 2;
    DistributionList distributionList = 3;
  }
}

message DistributionList {
  enum PrivacyMode {
    UNKNOWN = 0;
    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;
    uint64 dateServerSent = 2;
    bool read = 3;
    bool sealedSender = 4;
  }

  message OutgoingMessageDetails {
    repeated SendStatus sendStatus = 1;
  }

  message DirectionlessMessageDetails {
  }

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

  oneof directionalDetails {
    IncomingMessageDetails incoming = 8;
    OutgoingMessageDetails outgoing = 9;
    DirectionlessMessageDetails directionless = 10;
  }

  oneof item {
    StandardMessage standardMessage = 11;
    ContactMessage contactMessage = 12;
    StickerMessage stickerMessage = 13;
    RemoteDeletedMessage remoteDeletedMessage = 14;
    ChatUpdateMessage updateMessage = 15;
    PaymentNotification paymentNotification = 16;
    GiftBadge giftBadge = 17;
  }
}

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

  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 {
  repeated ContactAttachment contact = 1;
  repeated Reaction reactions = 2;
}

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;
        NETWORK = 1;
        INSUFFICIENT_FUNDS = 2;
      }
      FailureReason reason = 1;
    }

    message Transaction {
      enum Status {
        INITIAL = 0;
        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 
    }

    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;
    OPENED = 1;
    REDEEMED = 2;
    FAILED = 3;
  }

  bytes receiptCredentialPresentation = 1;
  State state = 2;
}

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

  message Phone {
    enum Type {
      UNKNOWN = 0;
      HOME = 1;
      MOBILE = 2;
      WORK = 3;
      CUSTOM = 4;
    }

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

  message Email {
    enum Type {
      UNKNOWN = 0;
      HOME = 1;
      MOBILE = 2;
      WORK = 3;
      CUSTOM = 4;
    }

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

  message PostalAddress {
    enum Type {
      UNKNOWN = 0;
      HOME = 1;
      WORK = 2;
      CUSTOM = 3;
    }

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

  optional Name name = 1;
  repeated Phone number = 3;
  repeated Email email = 4;
  repeated PostalAddress address = 5;
  optional FilePointer avatar = 6;
  optional 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;
    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 {
  // References attachments in the backup (media) storage tier.
  message BackupLocator {
    string mediaName = 1;
    // If present, the cdn number of the succesful upload.
    // If empty/0, may still have been uploaded, and clients
    // can discover the cdn number via the list endpoint.
    optional uint32 cdnNumber = 2;
    bytes key = 3;
    bytes digest = 4;
    uint32 size = 5;
    // Fallback in case backup tier upload failed.
    optional string transitCdnKey = 6;
    optional uint32 transitCdnNumber = 7;
  }

  // References attachments in the transit storage tier.
  // May be downloaded or not when the backup is generated;
  // primarily for free-tier users who cannot copy the
  // attachments to the backup (media) storage tier.
  message AttachmentLocator {
    string cdnKey = 1;
    uint32 cdnNumber = 2;
    uint64 uploadTimestamp = 3;
    bytes key = 4;
    bytes digest = 5;
    uint32 size = 6;
  }

  // References attachments that are invalid in such a way where download
  // cannot be attempted. Could range from missing digests to missing 
  // CDN keys or anything else that makes download attempts impossible.
  // This serves as a 'tombstone' so that the UX can show that an attachment
  // did exist, but for whatever reason it's not retrievable.
  message InvalidAttachmentLocator {
  }

  oneof locator {
    BackupLocator backupLocator = 1;
    AttachmentLocator attachmentLocator= 2;
    InvalidAttachmentLocator invalidAttachmentLocator = 3;
  }

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

message Quote {
  enum Type {
    UNKNOWN = 0;
    NORMAL = 1;
    GIFTBADGE = 2;
  }

  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;
    BOLD = 1;
    ITALIC = 2;
    SPOILER = 3;
    STRIKETHROUGH = 4;
    MONOSPACE = 5;
  }

  optional uint32 start = 1;
  optional uint32 length = 2;

  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 ChatUpdateMessage {
  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;
  }
}

message IndividualCall {
  enum Type {
    UNKNOWN_TYPE = 0;
    AUDIO_CALL = 1;
    VIDEO_CALL = 2;
  }

  enum Direction {
    UNKNOWN_DIRECTION = 0;
    INCOMING = 1;
    OUTGOING = 2;
  }

  enum State {
    UNKNOWN_STATE = 0;
    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;
    // 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;
  // The time the call ended. 0 indicates an unknown time.
  uint64 endedCallTimestamp = 6;
  bool read = 7;
}

message SimpleChatUpdate {
  enum Type {
    UNKNOWN = 0;
    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 {
  oneof previousName {
    uint64 e164 = 1;
    string username = 2;
  }
}

message ThreadMergeChatUpdate {
  uint64 previousE164 = 1;
}

message SessionSwitchoverChatUpdate {
  uint64 e164 = 1;
}

message GroupChangeChatUpdate {
  message Update {
    // Note: group expiration timer changes are represented as ExpirationTimerChatUpdate.
    oneof update {
      GenericGroupUpdate genericGroupUpdate = 1;
      GroupCreationUpdate groupCreationUpdate = 2;
      GroupNameUpdate groupNameUpdate = 3;
      GroupAvatarUpdate groupAvatarUpdate = 4;
      GroupDescriptionUpdate groupDescriptionUpdate = 5;
      GroupMembershipAccessLevelChangeUpdate groupMembershipAccessLevelChangeUpdate = 6;
      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;
    }
  }

  // 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;
  ANY = 1;
  MEMBER = 2;
  ADMINISTRATOR = 3;
  UNSATISFIABLE = 4;
}

message GroupMembershipAccessLevelChangeUpdate {
  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 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;

    oneof color {
      fixed32 solid = 2; // 0xAARRGGBB
      Gradient gradient = 3;
    }
  }

  message AutomaticBubbleColor {
  }

  enum WallpaperPreset {
    UNKNOWN_WALLPAPER_PRESET = 0;
    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;
    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;
  }

  oneof wallpaper {
    WallpaperPreset wallpaperPreset = 1;
    // This `FilePointer` is expected not to contain a `fileName`, `width`,
    // `height`, or `caption`.
    FilePointer wallpaperPhoto = 2;
  }

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