Import/export gift badges, other fields

This commit is contained in:
Fedor Indutny 2024-06-12 13:36:02 -07:00 committed by GitHub
parent af1c593fef
commit e6b62001d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 536 additions and 55 deletions

View file

@ -349,6 +349,7 @@ message ChatItem {
RemoteDeletedMessage remoteDeletedMessage = 14;
ChatUpdateMessage updateMessage = 15;
PaymentNotification paymentNotification = 16;
GiftBadge giftBadge = 17;
}
}
@ -439,6 +440,18 @@ message PaymentNotification {
}
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;
@ -1114,4 +1127,4 @@ message ChatStyle {
// Bubble setting is automatically determined based on the wallpaper setting.
AutomaticBubbleColor autoBubbleColor = 6;
}
}
}

View file

@ -195,6 +195,7 @@ message AccountRecord {
optional bytes subscriberId = 21;
optional string subscriberCurrencyCode = 22;
optional bool displayBadgesOnProfile = 23;
optional bool donorSubscriptionManuallyCancelled = 24;
optional bool keepMutedChatsArchived = 25;
optional bool hasSetMyStoriesPrivacy = 26;
optional bool hasViewedOnboardingStory = 27;
@ -202,12 +203,13 @@ message AccountRecord {
optional bool storiesDisabled = 29;
optional OptionalBool storyViewReceiptsEnabled = 30;
reserved 31; // hasReadOnboardingStory
reserved 32; // hasSeenGroupStoryEducationSheet
optional bool hasSeenGroupStoryEducationSheet = 32;
optional string username = 33;
optional bool hasCompletedUsernameOnboarding = 34;
optional UsernameLink usernameLink = 35;
optional bytes backupsSubscriberId = 36;
optional string backupsSubscriberCurrencyCode = 37;
optional bool backupsSubscriptionManuallyCancelled = 38;
}
message StoryDistributionListRecord {

View file

@ -12,6 +12,7 @@ import { Backups, SignalService } from '../../protobuf';
import Data from '../../sql/Client';
import type { PageMessagesCursorType } from '../../sql/Interface';
import * as log from '../../logging/log';
import { GiftBadgeStates } from '../../components/conversation/Message';
import { StorySendMode, MY_STORY_ID } from '../../types/Stories';
import {
isPniString,
@ -478,12 +479,20 @@ export class BackupExportStream extends Readable {
? {
subscriberId: backupsSubscriberId,
currencyCode: storage.get('backupsSubscriberCurrencyCode'),
manuallyCancelled: storage.get(
'backupsSubscriptionManuallyCancelled',
false
),
}
: null,
donationSubscriberData: Bytes.isNotEmpty(subscriberId)
? {
subscriberId,
currencyCode: storage.get('subscriberCurrencyCode'),
manuallyCancelled: storage.get(
'donorSubscriptionManuallyCancelled',
false
),
}
: null,
accountSettings: {
@ -507,6 +516,9 @@ export class BackupExportStream extends Readable {
hasCompletedUsernameOnboarding: storage.get(
'hasCompletedUsernameOnboarding'
),
hasSeenGroupStoryEducationSheet: storage.get(
'hasSeenGroupStoryEducationSheet'
),
phoneNumberSharingMode,
},
};
@ -849,6 +861,31 @@ export class BackupExportStream extends Readable {
sticker: stickerProto,
reactions: this.getMessageReactions(message),
};
} else if (isGiftBadge(message)) {
const { giftBadge } = message;
strictAssert(giftBadge != null, 'Message must have gift badge');
let state: Backups.GiftBadge.State;
switch (giftBadge.state) {
case GiftBadgeStates.Unopened:
state = Backups.GiftBadge.State.UNOPENED;
break;
case GiftBadgeStates.Opened:
state = Backups.GiftBadge.State.OPENED;
break;
case GiftBadgeStates.Redeemed:
state = Backups.GiftBadge.State.REDEEMED;
break;
default:
throw missingCaseError(giftBadge.state);
}
result.giftBadge = {
receiptCredentialPresentation: Bytes.fromBase64(
giftBadge.receiptCredentialPresentation
),
state,
};
} else {
result.standardMessage = await this.toStandardMessage(
message,
@ -1187,10 +1224,6 @@ export class BackupExportStream extends Readable {
return { kind: NonBubbleResultKind.Drop };
}
if (isGiftBadge(message)) {
// TODO (DESKTOP-6964): reuse quote's handling
}
if (isGroupUpdate(message)) {
// GV1 is deprecated.
return { kind: NonBubbleResultKind.Drop };
@ -1837,7 +1870,11 @@ export class BackupExportStream extends Readable {
}: Pick<MessageAttributesType, 'reactions'>):
| Array<Backups.IReaction>
| undefined {
return reactions?.map(reaction => {
if (reactions == null) {
return undefined;
}
return reactions?.map((reaction, sortOrder) => {
return {
emoji: reaction.emoji,
authorId: this.getOrPushPrivateRecipient({
@ -1847,6 +1884,7 @@ export class BackupExportStream extends Readable {
receivedTimestamp: getSafeLongFromTimestamp(
reaction.receivedAtDate ?? reaction.timestamp
),
sortOrder: Long.fromNumber(sortOrder),
};
});
}
@ -1856,12 +1894,14 @@ export class BackupExportStream extends Readable {
editMessageReceivedAtMs,
serverTimestamp,
readStatus,
unidentifiedDeliveryReceived,
}: Pick<
MessageAttributesType,
| 'received_at_ms'
| 'editMessageReceivedAtMs'
| 'serverTimestamp'
| 'readStatus'
| 'unidentifiedDeliveryReceived'
>): Backups.ChatItem.IIncomingMessageDetails {
const dateReceived = editMessageReceivedAtMs || receivedAtMs;
return {
@ -1872,6 +1912,7 @@ export class BackupExportStream extends Readable {
? getSafeLongFromTimestamp(serverTimestamp)
: null,
read: readStatus === ReadStatus.Read,
sealedSender: unidentifiedDeliveryReceived === true,
};
}
@ -1879,10 +1920,22 @@ export class BackupExportStream extends Readable {
sentAt: number,
{
sendStateByConversationId = {},
}: Pick<MessageAttributesType, 'sendStateByConversationId'>
unidentifiedDeliveries = [],
errors = [],
}: Pick<
MessageAttributesType,
'sendStateByConversationId' | 'unidentifiedDeliveries' | 'errors'
>
): Backups.ChatItem.IOutgoingMessageDetails {
const BackupSendStatus = Backups.SendStatus.Status;
const sealedSenderServiceIds = new Set(unidentifiedDeliveries);
const errorMap = new Map(
errors.map(({ serviceId, name }) => {
return [serviceId, name];
})
);
const sendStatus = new Array<Backups.ISendStatus>();
for (const [id, entry] of Object.entries(sendStateByConversationId)) {
const target = window.ConversationController.get(id);
@ -1915,6 +1968,19 @@ export class BackupExportStream extends Readable {
throw missingCaseError(entry.status);
}
const { serviceId } = target.attributes;
let networkFailure = false;
let identityKeyMismatch = false;
let sealedSender = false;
if (serviceId) {
const errorName = errorMap.get(serviceId);
if (errorName !== undefined) {
identityKeyMismatch = errorName === 'OutgoingIdentityKeyError';
networkFailure = !identityKeyMismatch;
}
sealedSender = sealedSenderServiceIds.has(serviceId);
}
sendStatus.push({
recipientId: this.getOrPushPrivateRecipient(target.attributes),
lastStatusUpdateTimestamp:
@ -1922,6 +1988,9 @@ export class BackupExportStream extends Readable {
? getSafeLongFromTimestamp(entry.updatedAt)
: null,
deliveryStatus,
networkFailure,
identityKeyMismatch,
sealedSender,
});
}
return {

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { Aci, Pni } from '@signalapp/libsignal-client';
import { ReceiptCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
import { v4 as generateUuid } from 'uuid';
import pMap from 'p-map';
import { Writable } from 'stream';
@ -11,6 +12,7 @@ import { Backups, SignalService } from '../../protobuf';
import Data from '../../sql/Client';
import type { StoryDistributionWithMembersType } from '../../sql/Interface';
import * as log from '../../logging/log';
import { GiftBadgeStates } from '../../components/conversation/Message';
import { StorySendMode } from '../../types/Stories';
import type { ServiceIdString, AciString } from '../../types/ServiceId';
import { fromAciObject, fromPniObject } from '../../types/ServiceId';
@ -27,6 +29,7 @@ import {
} from '../../types/Stickers';
import type {
ConversationAttributesType,
CustomError,
MessageAttributesType,
MessageReactionType,
EditHistoryType,
@ -34,7 +37,7 @@ import type {
} from '../../model-types.d';
import { assertDev, strictAssert } from '../../util/assert';
import { getTimestampFromLong } from '../../util/timestampLongUtils';
import { DurationInSeconds } from '../../util/durations';
import { DurationInSeconds, SECOND } from '../../util/durations';
import { dropNull } from '../../util/dropNull';
import {
deriveGroupID,
@ -468,22 +471,36 @@ export class BackupImportStream extends Writable {
await storage.put('avatarUrl', avatarUrlPath);
}
if (donationSubscriberData != null) {
const { subscriberId, currencyCode } = donationSubscriberData;
const { subscriberId, currencyCode, manuallyCancelled } =
donationSubscriberData;
if (Bytes.isNotEmpty(subscriberId)) {
await storage.put('subscriberId', subscriberId);
}
if (currencyCode != null) {
await storage.put('subscriberCurrencyCode', currencyCode);
}
if (manuallyCancelled != null) {
await storage.put(
'donorSubscriptionManuallyCancelled',
manuallyCancelled
);
}
}
if (backupsSubscriberData != null) {
const { subscriberId, currencyCode } = backupsSubscriberData;
const { subscriberId, currencyCode, manuallyCancelled } =
backupsSubscriberData;
if (Bytes.isNotEmpty(subscriberId)) {
await storage.put('backupsSubscriberId', subscriberId);
}
if (currencyCode != null) {
await storage.put('backupsSubscriberCurrencyCode', currencyCode);
}
if (manuallyCancelled != null) {
await storage.put(
'backupsSubscriptionManuallyCancelled',
manuallyCancelled
);
}
}
await storage.put(
@ -503,6 +520,12 @@ export class BackupImportStream extends Writable {
'preferContactAvatars',
accountSettings?.preferContactAvatars === true
);
if (accountSettings?.universalExpireTimer) {
await storage.put(
'universalExpireTimer',
accountSettings.universalExpireTimer
);
}
await storage.put(
'displayBadgesOnProfile',
accountSettings?.displayBadgesOnProfile === true
@ -532,8 +555,8 @@ export class BackupImportStream extends Writable {
accountSettings?.hasCompletedUsernameOnboarding === true
);
await storage.put(
'preferredReactionEmoji',
accountSettings?.preferredReactionEmoji || []
'hasSeenGroupStoryEducationSheet',
accountSettings?.hasSeenGroupStoryEducationSheet === true
);
await storage.put(
'preferredReactionEmoji',
@ -618,6 +641,7 @@ export class BackupImportStream extends Writable {
profileName: dropNull(contact.profileGivenName),
profileFamilyName: dropNull(contact.profileFamilyName),
hideStory: contact.hideStory === true,
username: dropNull(contact.username),
};
if (contact.notRegistered) {
@ -871,8 +895,6 @@ export class BackupImportStream extends Writable {
}
if (item.standardMessage) {
// TODO (DESKTOP-6964): gift badge
attributes = {
...attributes,
...(await this.fromStandardMessage(item.standardMessage, chatConvo.id)),
@ -961,6 +983,8 @@ export class BackupImportStream extends Writable {
const BackupSendStatus = Backups.SendStatus.Status;
const unidentifiedDeliveries = new Array<ServiceIdString>();
const errors = new Array<CustomError>();
for (const status of outgoing.sendStatus ?? []) {
strictAssert(
status.recipientId,
@ -997,6 +1021,28 @@ export class BackupImportStream extends Writable {
break;
}
if (target.serviceId) {
if (status.sealedSender) {
unidentifiedDeliveries.push(target.serviceId);
}
if (status.identityKeyMismatch) {
errors.push({
serviceId: target.serviceId,
name: 'OutgoingIdentityKeyError',
// See: ts/textsecure/Errors
message: `The identity of ${target.serviceId} has changed.`,
});
} else if (status.networkFailure) {
errors.push({
serviceId: target.serviceId,
name: 'OutgoingMessageError',
// See: ts/textsecure/Errors
message: 'no http error',
});
}
}
sendStateByConversationId[target.id] = {
status: sendStatus,
updatedAt:
@ -1011,6 +1057,10 @@ export class BackupImportStream extends Writable {
patch: {
sendStateByConversationId,
received_at_ms: timestamp,
unidentifiedDeliveries: unidentifiedDeliveries.length
? unidentifiedDeliveries
: undefined,
errors: errors.length ? errors : undefined,
},
newActiveAt: timestamp,
};
@ -1018,12 +1068,15 @@ export class BackupImportStream extends Writable {
if (incoming) {
const receivedAtMs = incoming.dateReceived?.toNumber() ?? Date.now();
const unidentifiedDeliveryReceived = incoming.sealedSender === true;
if (incoming.read) {
return {
patch: {
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
received_at_ms: receivedAtMs,
unidentifiedDeliveryReceived,
},
newActiveAt: receivedAtMs,
};
@ -1034,6 +1087,7 @@ export class BackupImportStream extends Writable {
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
received_at_ms: receivedAtMs,
unidentifiedDeliveryReceived,
},
newActiveAt: receivedAtMs,
unread: true,
@ -1203,8 +1257,15 @@ export class BackupImportStream extends Writable {
if (!reactions?.length) {
return undefined;
}
return reactions.map(
({ emoji, authorId, sentTimestamp, receivedTimestamp }) => {
return reactions
.slice()
.sort((a, b) => {
if (a.sortOrder && b.sortOrder) {
return a.sortOrder.comp(b.sortOrder);
}
return 0;
})
.map(({ emoji, authorId, sentTimestamp, receivedTimestamp }) => {
strictAssert(emoji != null, 'reaction must have an emoji');
strictAssert(authorId != null, 'reaction must have authorId');
strictAssert(
@ -1229,8 +1290,7 @@ export class BackupImportStream extends Writable {
receivedAtDate: getTimestampFromLong(receivedTimestamp),
timestamp: getTimestampFromLong(sentTimestamp),
};
}
);
});
}
private async fromNonBubbleChatItem(
@ -1401,6 +1461,52 @@ export class BackupImportStream extends Writable {
additionalMessages: [],
};
}
if (chatItem.giftBadge) {
const { giftBadge } = chatItem;
strictAssert(
Bytes.isNotEmpty(giftBadge.receiptCredentialPresentation),
'Gift badge must have a presentation'
);
let state: GiftBadgeStates;
switch (giftBadge.state) {
case Backups.GiftBadge.State.OPENED:
state = GiftBadgeStates.Opened;
break;
case Backups.GiftBadge.State.FAILED:
case Backups.GiftBadge.State.REDEEMED:
state = GiftBadgeStates.Redeemed;
break;
case Backups.GiftBadge.State.UNOPENED:
state = GiftBadgeStates.Unopened;
break;
default:
state = GiftBadgeStates.Unopened;
break;
}
const receipt = new ReceiptCredentialPresentation(
Buffer.from(giftBadge.receiptCredentialPresentation)
);
return {
message: {
giftBadge: {
receiptCredentialPresentation: Bytes.toBase64(
giftBadge.receiptCredentialPresentation
),
expiration: Number(receipt.getReceiptExpirationTime()) * SECOND,
id: undefined,
level: Number(receipt.getReceiptLevel()),
state,
},
},
additionalMessages: [],
};
}
if (chatItem.updateMessage) {
return this.fromChatItemUpdateMessage(chatItem.updateMessage, options);
}

View file

@ -397,6 +397,13 @@ export function toAccountRecord(
if (typeof subscriberCurrencyCode === 'string') {
accountRecord.subscriberCurrencyCode = subscriberCurrencyCode;
}
const donorSubscriptionManuallyCancelled = window.storage.get(
'donorSubscriptionManuallyCancelled'
);
if (typeof donorSubscriptionManuallyCancelled === 'boolean') {
accountRecord.donorSubscriptionManuallyCancelled =
donorSubscriptionManuallyCancelled;
}
const backupsSubscriberId = window.storage.get('backupsSubscriberId');
if (Bytes.isNotEmpty(backupsSubscriberId)) {
accountRecord.backupsSubscriberId = backupsSubscriberId;
@ -407,6 +414,13 @@ export function toAccountRecord(
if (typeof backupsSubscriberCurrencyCode === 'string') {
accountRecord.backupsSubscriberCurrencyCode = backupsSubscriberCurrencyCode;
}
const backupsSubscriptionManuallyCancelled = window.storage.get(
'backupsSubscriptionManuallyCancelled'
);
if (typeof backupsSubscriptionManuallyCancelled === 'boolean') {
accountRecord.backupsSubscriptionManuallyCancelled =
backupsSubscriptionManuallyCancelled;
}
const displayBadgesOnProfile = window.storage.get('displayBadgesOnProfile');
if (displayBadgesOnProfile !== undefined) {
accountRecord.displayBadgesOnProfile = displayBadgesOnProfile;
@ -436,6 +450,14 @@ export function toAccountRecord(
hasCompletedUsernameOnboarding;
}
const hasSeenGroupStoryEducationSheet = window.storage.get(
'hasSeenGroupStoryEducationSheet'
);
if (hasSeenGroupStoryEducationSheet !== undefined) {
accountRecord.hasSeenGroupStoryEducationSheet =
hasSeenGroupStoryEducationSheet;
}
const hasStoriesDisabled = window.storage.get('hasStoriesDisabled');
accountRecord.storiesDisabled = hasStoriesDisabled === true;
@ -1235,11 +1257,14 @@ export async function mergeAccountRecord(
preferredReactionEmoji: rawPreferredReactionEmoji,
subscriberId,
subscriberCurrencyCode,
donorSubscriptionManuallyCancelled,
backupsSubscriberId,
backupsSubscriberCurrencyCode,
backupsSubscriptionManuallyCancelled,
displayBadgesOnProfile,
keepMutedChatsArchived,
hasCompletedUsernameOnboarding,
hasSeenGroupStoryEducationSheet,
hasSetMyStoriesPrivacy,
hasViewedOnboardingStory,
storiesDisabled,
@ -1448,6 +1473,12 @@ export async function mergeAccountRecord(
if (typeof subscriberCurrencyCode === 'string') {
await window.storage.put('subscriberCurrencyCode', subscriberCurrencyCode);
}
if (donorSubscriptionManuallyCancelled != null) {
await window.storage.put(
'donorSubscriptionManuallyCancelled',
donorSubscriptionManuallyCancelled
);
}
if (Bytes.isNotEmpty(backupsSubscriberId)) {
await window.storage.put('backupsSubscriberId', backupsSubscriberId);
}
@ -1457,6 +1488,12 @@ export async function mergeAccountRecord(
backupsSubscriberCurrencyCode
);
}
if (backupsSubscriptionManuallyCancelled != null) {
await window.storage.put(
'backupsSubscriptionManuallyCancelled',
backupsSubscriptionManuallyCancelled
);
}
await window.storage.put(
'displayBadgesOnProfile',
Boolean(displayBadgesOnProfile)
@ -1490,6 +1527,15 @@ export async function mergeAccountRecord(
hasCompletedUsernameOnboardingBool
);
}
{
const hasCompletedUsernameOnboardingBool = Boolean(
hasSeenGroupStoryEducationSheet
);
await window.storage.put(
'hasSeenGroupStoryEducationSheet',
hasCompletedUsernameOnboardingBool
);
}
{
const hasStoriesDisabled = Boolean(storiesDisabled);
await window.storage.put('hasStoriesDisabled', hasStoriesDisabled);

View file

@ -118,6 +118,7 @@ describe('backup/attachments', () => {
timestamp,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
unidentifiedDeliveryReceived: true,
...overrides,
};
}

View file

@ -5,6 +5,7 @@ import { v4 as generateGuid } from 'uuid';
import { SendStatus } from '../../messages/MessageSendState';
import type { ConversationModel } from '../../models/conversations';
import { GiftBadgeStates } from '../../components/conversation/Message';
import Data from '../../sql/Client';
import { generateAci } from '../../types/ServiceId';
@ -15,6 +16,15 @@ import { setupBasics, symmetricRoundtripHarness, OUR_ACI } from './helpers';
const CONTACT_A = generateAci();
const BADGE_RECEIPT =
'AEpyZxbRBT+T5PQw9Wcx1QE2aFvL7LoLir9V4UF09Kk9qiP4SpIlHdlWHrAICy6F' +
'6WdbdCj45fY6cadDKbBmkw+abohRTJnItrFhyKurnA5X+mZHZv4OvS+aZFmAYS6J' +
'W+hpkbI+Fk7Gu3mEix7Pgz1I2EwGFlUBpm7/nuD5A0cKLrUJAMM142fnOEervePV' +
'bf0c6Sw5X5aCsBw9J+dxFUGAAAAAAAAAAMH58UUeUj2oH1jfqc0Hb2RUtdA3ee8X' +
'0Pp83WT8njwFw5rNGSHeKqOvBZzfAhMGJoiz7l1XfIfsPIreaFb/tA9aq2bOAdDl' +
'5OYlxxl6DnjQ3+g3k9ycpl0elkaQnPW2Ai7yjeJ/96K1qssR2a/2b7xi10dmTRGg' +
'gebhZnroYYgIgK22ZgAAAABkAAAAAAAAAD9j4f77Xo2Ox5tVyrV2DUo=';
describe('backup/bubble messages', () => {
let contactA: ConversationModel;
@ -48,6 +58,7 @@ describe('backup/bubble messages', () => {
body: 'd',
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
editMessageTimestamp: 5,
editMessageReceivedAtMs: 5,
editHistory: [
@ -89,6 +100,7 @@ describe('backup/bubble messages', () => {
status: SendStatus.Delivered,
},
},
unidentifiedDeliveries: [CONTACT_A],
timestamp: 3,
editMessageTimestamp: 5,
editMessageReceivedAtMs: 5,
@ -131,4 +143,224 @@ describe('backup/bubble messages', () => {
},
]);
});
it.skip('roundtrips unopened gift badge', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
id: generateGuid(),
type: 'incoming',
received_at: 3,
received_at_ms: 3,
sent_at: 3,
sourceServiceId: CONTACT_A,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
timestamp: 3,
giftBadge: {
id: undefined,
level: 100,
expiration: 1723248000000,
receiptCredentialPresentation: BADGE_RECEIPT,
state: GiftBadgeStates.Opened,
},
},
]);
});
it.skip('roundtrips opened gift badge', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
id: generateGuid(),
type: 'incoming',
received_at: 3,
received_at_ms: 3,
sent_at: 3,
sourceServiceId: CONTACT_A,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
timestamp: 3,
giftBadge: {
id: undefined,
level: 100,
expiration: 1723248000000,
receiptCredentialPresentation: BADGE_RECEIPT,
state: GiftBadgeStates.Opened,
},
},
]);
});
it.skip('roundtrips gift badge quote', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
id: generateGuid(),
type: 'incoming',
received_at: 3,
received_at_ms: 3,
sent_at: 3,
sourceServiceId: CONTACT_A,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
timestamp: 3,
giftBadge: {
id: undefined,
level: 100,
expiration: 1723248000000,
receiptCredentialPresentation: BADGE_RECEIPT,
state: GiftBadgeStates.Opened,
},
},
{
conversationId: contactA.id,
id: generateGuid(),
type: 'incoming',
received_at: 4,
received_at_ms: 4,
sent_at: 4,
sourceServiceId: CONTACT_A,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
timestamp: 4,
quote: {
authorAci: CONTACT_A,
attachments: [],
id: 3,
isViewOnce: false,
isGiftBadge: true,
messageId: '',
referencedMessageNotFound: false,
},
},
]);
});
it('roundtrips sealed/unsealed incoming message', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
id: generateGuid(),
type: 'incoming',
received_at: 3,
received_at_ms: 3,
sent_at: 3,
sourceServiceId: CONTACT_A,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: false,
timestamp: 3,
body: 'unsealed',
},
{
conversationId: contactA.id,
id: generateGuid(),
type: 'incoming',
received_at: 4,
received_at_ms: 4,
sent_at: 4,
sourceServiceId: CONTACT_A,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
timestamp: 4,
body: 'sealed',
},
]);
});
it('roundtrips sealed/unsealed outgoing message', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
id: generateGuid(),
type: 'outgoing',
received_at: 3,
received_at_ms: 3,
sent_at: 3,
sourceServiceId: OUR_ACI,
sendStateByConversationId: {
[contactA.id]: {
status: SendStatus.Delivered,
},
},
unidentifiedDeliveries: undefined,
timestamp: 3,
body: 'unsealed',
},
{
conversationId: contactA.id,
id: generateGuid(),
type: 'outgoing',
received_at: 4,
received_at_ms: 4,
sent_at: 4,
sourceServiceId: OUR_ACI,
sendStateByConversationId: {
[contactA.id]: {
status: SendStatus.Delivered,
},
},
unidentifiedDeliveries: [CONTACT_A],
timestamp: 4,
body: 'sealed',
},
]);
});
it('roundtrips messages with send errors', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
id: generateGuid(),
type: 'outgoing',
received_at: 3,
received_at_ms: 3,
sent_at: 3,
sourceServiceId: OUR_ACI,
sendStateByConversationId: {
[contactA.id]: {
status: SendStatus.Delivered,
},
},
errors: [
{
serviceId: CONTACT_A,
name: 'OutgoingIdentityKeyError',
message: `The identity of ${CONTACT_A} has changed.`,
},
],
timestamp: 3,
body: 'body',
},
{
conversationId: contactA.id,
id: generateGuid(),
type: 'outgoing',
received_at: 4,
received_at_ms: 4,
sent_at: 4,
sourceServiceId: OUR_ACI,
sendStateByConversationId: {
[contactA.id]: {
status: SendStatus.Delivered,
},
},
errors: [
{
serviceId: CONTACT_A,
name: 'OutgoingMessageError',
message: 'no http error',
},
],
timestamp: 4,
body: 'body',
},
]);
});
});

View file

@ -93,36 +93,39 @@ function sortAndNormalize(
return result;
}
return {
...rest,
conversationId: mapConvoId(conversationId),
reactions: reactions?.map(({ fromId, ...restOfReaction }) => {
return {
from: mapConvoId(fromId),
...restOfReaction,
};
}),
changedId: mapConvoId(changedId),
key_changed: mapConvoId(keyChanged),
verifiedChanged: mapConvoId(verifiedChanged),
sendStateByConverationId: mapSendState(sendStateByConversationId),
editHistory: editHistory?.map(history => {
const {
sendStateByConversationId: historySendState,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
received_at: _receivedAtHistory,
...restOfHistory
} = history;
// Get rid of unserializable `undefined` values.
return JSON.parse(
JSON.stringify({
...rest,
conversationId: mapConvoId(conversationId),
reactions: reactions?.map(({ fromId, ...restOfReaction }) => {
return {
from: mapConvoId(fromId),
...restOfReaction,
};
}),
changedId: mapConvoId(changedId),
key_changed: mapConvoId(keyChanged),
verifiedChanged: mapConvoId(verifiedChanged),
sendStateByConverationId: mapSendState(sendStateByConversationId),
editHistory: editHistory?.map(history => {
const {
sendStateByConversationId: historySendState,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
received_at: _receivedAtHistory,
...restOfHistory
} = history;
return {
...restOfHistory,
sendStateByConversationId: mapSendState(historySendState),
};
}),
return {
...restOfHistory,
sendStateByConversationId: mapSendState(historySendState),
};
}),
// Not an original property, but useful
isUnsupported: isUnsupportedMessage(message),
};
// Not an original property, but useful
isUnsupported: isUnsupportedMessage(message),
})
);
});
}

View file

@ -66,6 +66,7 @@ describe('backup/non-bubble messages', () => {
sourceDevice: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
flags: Proto.DataMessage.Flags.END_SESSION,
},
]);
@ -204,6 +205,7 @@ describe('backup/non-bubble messages', () => {
timestamp: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
},
]);
});
@ -224,12 +226,12 @@ describe('backup/non-bubble messages', () => {
timestamp: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
},
]);
});
// TODO: DESKTOP-7122
it.skip('roundtrips bare payments notification', async () => {
it('roundtrips bare payments notification', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
@ -243,6 +245,7 @@ describe('backup/non-bubble messages', () => {
sourceDevice: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
payment: {
kind: PaymentEventKind.Notification,
note: 'note with text',
@ -251,8 +254,7 @@ describe('backup/non-bubble messages', () => {
]);
});
// TODO: DESKTOP-7122
it.skip('roundtrips full payments notification', async () => {
it('roundtrips full payments notification', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
@ -266,6 +268,7 @@ describe('backup/non-bubble messages', () => {
sourceDevice: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
payment: {
kind: PaymentEventKind.Notification,
note: 'note with text',
@ -297,6 +300,7 @@ describe('backup/non-bubble messages', () => {
sourceDevice: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
contact: [
{
name: {
@ -339,6 +343,7 @@ describe('backup/non-bubble messages', () => {
sourceDevice: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
// TODO (DESKTOP-6845): properly handle data FilePointer
sticker: {
emoji: '👍',
@ -373,6 +378,7 @@ describe('backup/non-bubble messages', () => {
sourceDevice: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
unidentifiedDeliveryReceived: true,
isErased: true,
},
]);
@ -475,8 +481,7 @@ describe('backup/non-bubble messages', () => {
]);
});
// TODO: DESKTOP-7122
it.skip('roundtrips unsupported message', async () => {
it('roundtrips unsupported message', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
@ -490,8 +495,9 @@ describe('backup/non-bubble messages', () => {
timestamp: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
supportedVersionAtReceive: 1,
requiredProtocolVersion: 2,
unidentifiedDeliveryReceived: true,
supportedVersionAtReceive: 5,
requiredProtocolVersion: 6,
},
]);
});

View file

@ -72,6 +72,7 @@ export type StorageAccessType = {
hasCompletedUsernameOnboarding: boolean;
hasCompletedUsernameLinkOnboarding: boolean;
hasCompletedSafetyNumberOnboarding: boolean;
hasSeenGroupStoryEducationSheet: boolean;
hasViewedOnboardingStory: boolean;
hasStoriesDisabled: boolean;
storyViewReceiptsEnabled: boolean;
@ -154,8 +155,10 @@ export type StorageAccessType = {
areWeASubscriber: boolean;
subscriberId: Uint8Array;
subscriberCurrencyCode: string;
donorSubscriptionManuallyCancelled: boolean;
backupsSubscriberId: Uint8Array;
backupsSubscriberCurrencyCode: string;
backupsSubscriptionManuallyCancelled: boolean;
displayBadgesOnProfile: boolean;
keepMutedChatsArchived: boolean;
usernameLastIntegrityCheck: number;