// 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, Ad-hoc Calls, & StickerPacks 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; } } 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 universalExpireTimer = 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; } bytes profileKey = 1; optional string username = 2; UsernameLink usernameLink = 3; string givenName = 4; string familyName = 5; string avatarUrlPath = 6; bytes subscriberId = 7; string subscriberCurrencyCode = 8; bool subscriptionManuallyCancelled = 9; AccountSettings accountSettings = 10; } message Recipient { uint64 id = 1; // generated id for reference only within this file oneof destination { Contact contact = 2; Group group = 3; DistributionList distributionList = 4; Self self = 5; ReleaseNotes releaseNotes = 6; } } message Contact { enum Registered { UNKNOWN = 0; REGISTERED = 1; NOT_REGISTERED = 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; bool hidden = 6; Registered registered = 7; uint64 unregisteredTimestamp = 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; string name = 5; } 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; FilePointer wallpaper = 9; } message DistributionList { enum PrivacyMode { UNKNOWN = 0; ONLY_WITH = 1; ALL_EXCEPT = 2; ALL = 3; } string name = 1; bytes distributionId = 2; // distribution list ids are uuids bool allowReplies = 3; uint64 deletionTimestamp = 4; PrivacyMode privacyMode = 5; repeated uint64 memberRecipientIds = 6; // generated recipient id } message Identity { bytes serviceId = 1; bytes identityKey = 2; uint64 timestamp = 3; bool firstUse = 4; bool verified = 5; bool nonblockingApproval = 6; } 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; 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; 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; } } message SendStatus { enum Status { UNKNOWN = 0; FAILED = 1; PENDING = 2; SENT = 3; DELIVERED = 4; READ = 5; VIEWED = 6; SKIPPED = 7; // e.g. user in group was blocked, so we skipped sending to them } uint64 recipientId = 1; Status deliveryStatus = 2; bool networkFailure = 3; bool identityKeyMismatch = 4; bool sealedSender = 5; uint64 lastStatusUpdateTimestamp = 6; // the time the status was last updated -- if from a receipt, it should be the sentTime of the receipt } 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 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 displayName = 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 string avatarUrlPath = 6; optional string organization = 7; } message DocumentMessage { Text text = 1; FilePointer document = 2; repeated Reaction reactions = 3; } 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; } 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 string text = 3; repeated QuotedAttachment attachments = 4; repeated BodyRange bodyRanges = 5; Type type = 6; } 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; optional uint64 receivedTimestamp = 4; uint64 sortOrder = 5; // A higher sort order means that a reaction is more recent } message ChatUpdateMessage { oneof update { SimpleChatUpdate simpleUpdate = 1; GroupChangeChatUpdate groupChange = 2; ExpirationTimerChatUpdate expirationTimerChange = 3; ProfileChangeChatUpdate profileChange = 4; ThreadMergeChatUpdate threadMerge = 5; SessionSwitchoverChatUpdate sessionSwitchover = 6; CallChatUpdate callingMessage = 7; } } message CallChatUpdate{ Call call = 1; oneof chatUpdate { IndividualCallChatUpdate callMessage = 2; GroupCallChatUpdate groupCall = 3; } } message Call { enum Type { UNKNOWN_TYPE = 0; AUDIO_CALL = 1; VIDEO_CALL = 2; GROUP_CALL = 3; AD_HOC_CALL = 4; } enum State { UNKNOWN_EVENT = 0; COMPLETED = 1; // A call that was successfully completed or was accepted and in-progress at the time of the backup. DECLINED_BY_USER = 2; // An incoming call that was manually declined by the user. DECLINED_BY_NOTIFICATION_PROFILE = 3; // An incoming call that was automatically declined by an active notification profile. MISSED = 4; // An incoming call that either expired, was cancelled by the sender, or was auto-rejected due to already being in a different call. } uint64 callId = 1; uint64 conversationRecipientId = 2; Type type = 3; bool outgoing = 4; uint64 timestamp = 5; optional uint64 ringerRecipientId = 6; State state = 7; } message IndividualCallChatUpdate { enum Type { UNKNOWN = 0; INCOMING_AUDIO_CALL = 1; INCOMING_VIDEO_CALL = 2; OUTGOING_AUDIO_CALL = 3; OUTGOING_VIDEO_CALL = 4; MISSED_INCOMING_AUDIO_CALL = 5; MISSED_INCOMING_VIDEO_CALL = 6; UNANSWERED_OUTGOING_AUDIO_CALL = 7; UNANSWERED_OUTGOING_VIDEO_CALL = 8; } Type type = 1; } message GroupCallChatUpdate { enum LocalUserJoined { UNKNOWN = 0; JOINED = 1; DID_NOT_JOIN = 2; } optional bytes startedCallAci = 1; uint64 startedCallTimestamp = 2; repeated bytes inCallAcis = 3; uint64 endedCallTimestamp = 4; // 0 indicates we do not know LocalUserJoined localUserJoined = 5; } message SimpleChatUpdate { enum Type { UNKNOWN = 0; JOINED_SIGNAL = 1; IDENTITY_UPDATE = 2; IDENTITY_VERIFIED = 3; IDENTITY_DEFAULT = 4; // marking as unverified CHANGE_NUMBER = 5; BOOST_REQUEST = 6; END_SESSION = 7; CHAT_SESSION_REFRESH = 8; BAD_DECRYPT = 9; PAYMENTS_ACTIVATED = 10; PAYMENT_ACTIVATION_REQUEST = 11; } Type type = 1; } // For 1:1 chat updates only. // For group thread updates use GroupExpirationTimerUpdate. message ExpirationTimerChatUpdate { uint32 expiresInMs = 1; // 0 means the expiration timer was disabled } message ProfileChangeChatUpdate { string previousName = 1; string newName = 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 { uint32 expiresInMs = 1; // 0 means the expiration timer was disabled optional bytes updaterAci = 2; } message StickerPack { bytes packId = 1; bytes packKey = 2; string title = 3; string author = 4; repeated StickerPackSticker stickers = 5; // First one should be cover sticker. } message StickerPackSticker { string emoji = 1; uint32 id = 2; }