Backups: support direct story replies
This commit is contained in:
parent
4b6ef3a1ed
commit
0d6cd429d0
9 changed files with 417 additions and 32 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -194,7 +194,7 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: 'signalapp/Signal-Message-Backup-Tests'
|
repository: 'signalapp/Signal-Message-Backup-Tests'
|
||||||
ref: '05062de9656c5ed7c7e6c6a49897b42e7ad083fc'
|
ref: '22d7f507b61691e0a7da1fd4b233f219bdaf2280'
|
||||||
path: 'backup-integration-tests'
|
path: 'backup-integration-tests'
|
||||||
|
|
||||||
- run: xvfb-run --auto-servernum npm run test-electron
|
- run: xvfb-run --auto-servernum npm run test-electron
|
||||||
|
|
|
@ -6052,7 +6052,7 @@ third-party/chromium/LICENSE.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## windows-core 0.52.0, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-targets 0.42.2, windows-targets 0.48.5, windows-targets 0.52.6, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.48.5, windows_aarch64_msvc 0.52.6, windows_x86_64_gnu 0.48.5, windows_x86_64_gnu 0.52.6, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.48.5, windows_x86_64_msvc 0.52.6
|
## windows-core 0.52.0, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-targets 0.42.2, windows-targets 0.52.6, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_x86_64_gnu 0.52.6, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6
|
||||||
|
|
||||||
```
|
```
|
||||||
MIT License
|
MIT License
|
||||||
|
@ -10809,7 +10809,7 @@ SOFTWARE.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## derive_more 0.99.18
|
## derive_more-impl 1.0.0, derive_more 0.99.18, derive_more 1.0.0
|
||||||
|
|
||||||
```
|
```
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -22,7 +22,7 @@
|
||||||
"@react-aria/utils": "3.25.3",
|
"@react-aria/utils": "3.25.3",
|
||||||
"@react-spring/web": "9.7.5",
|
"@react-spring/web": "9.7.5",
|
||||||
"@signalapp/better-sqlite3": "9.0.9",
|
"@signalapp/better-sqlite3": "9.0.9",
|
||||||
"@signalapp/libsignal-client": "0.65.0",
|
"@signalapp/libsignal-client": "0.65.2",
|
||||||
"@signalapp/ringrtc": "2.49.3",
|
"@signalapp/ringrtc": "2.49.3",
|
||||||
"@types/fabric": "4.5.3",
|
"@types/fabric": "4.5.3",
|
||||||
"backbone": "1.6.0",
|
"backbone": "1.6.0",
|
||||||
|
@ -6471,11 +6471,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@signalapp/libsignal-client": {
|
"node_modules/@signalapp/libsignal-client": {
|
||||||
"version": "0.65.0",
|
"version": "0.65.2",
|
||||||
"resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.65.0.tgz",
|
"resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.65.2.tgz",
|
||||||
"integrity": "sha512-6bb+454/ZoOu6EhHdKHSFVoNKRhka1FhqfiDL/FYLf79g+Nz4kXNxCMRJwYO3hzPAgjjs/6Q7S1mXr9CfS7Kkg==",
|
"integrity": "sha512-OfemOQcVxykG8fwUwqkbxt1Vg0HZSGrrWG8wqjJbXj93kOOmQNkpuiJlfm+NBqNYkTYnwRy8rd6PQbu04q7NeA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "AGPL-3.0-only",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-gyp-build": "^4.8.0",
|
"node-gyp-build": "^4.8.0",
|
||||||
"type-fest": "^4.26.0",
|
"type-fest": "^4.26.0",
|
||||||
|
|
|
@ -113,7 +113,7 @@
|
||||||
"@react-aria/utils": "3.25.3",
|
"@react-aria/utils": "3.25.3",
|
||||||
"@react-spring/web": "9.7.5",
|
"@react-spring/web": "9.7.5",
|
||||||
"@signalapp/better-sqlite3": "9.0.9",
|
"@signalapp/better-sqlite3": "9.0.9",
|
||||||
"@signalapp/libsignal-client": "0.65.0",
|
"@signalapp/libsignal-client": "0.65.2",
|
||||||
"@signalapp/ringrtc": "2.49.3",
|
"@signalapp/ringrtc": "2.49.3",
|
||||||
"@types/fabric": "4.5.3",
|
"@types/fabric": "4.5.3",
|
||||||
"backbone": "1.6.0",
|
"backbone": "1.6.0",
|
||||||
|
|
|
@ -173,6 +173,7 @@ message Contact {
|
||||||
optional bytes identityKey = 14;
|
optional bytes identityKey = 14;
|
||||||
IdentityState identityState = 15;
|
IdentityState identityState = 15;
|
||||||
Name nickname = 16; // absent iff both `given` and `family` are empty
|
Name nickname = 16; // absent iff both `given` and `family` are empty
|
||||||
|
string note = 17;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Group {
|
message Group {
|
||||||
|
@ -380,6 +381,7 @@ message ChatItem {
|
||||||
PaymentNotification paymentNotification = 16;
|
PaymentNotification paymentNotification = 16;
|
||||||
GiftBadge giftBadge = 17;
|
GiftBadge giftBadge = 17;
|
||||||
ViewOnceMessage viewOnceMessage = 18;
|
ViewOnceMessage viewOnceMessage = 18;
|
||||||
|
DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,6 +450,21 @@ message ContactMessage {
|
||||||
repeated Reaction reactions = 2;
|
repeated Reaction reactions = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DirectStoryReplyMessage {
|
||||||
|
message TextReply {
|
||||||
|
Text text = 1;
|
||||||
|
FilePointer longText = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof reply {
|
||||||
|
TextReply textReply = 1;
|
||||||
|
string emoji = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
repeated Reaction reactions = 3;
|
||||||
|
reserved /*storySentTimestamp*/ 4;
|
||||||
|
}
|
||||||
|
|
||||||
message PaymentNotification {
|
message PaymentNotification {
|
||||||
message TransactionDetails {
|
message TransactionDetails {
|
||||||
message MobileCoinTxoIdentification { // Used to map to payments on the ledger
|
message MobileCoinTxoIdentification { // Used to map to payments on the ledger
|
||||||
|
@ -1241,4 +1258,4 @@ message ChatFolder {
|
||||||
FolderType folderType = 6;
|
FolderType folderType = 6;
|
||||||
repeated uint64 includedRecipientIds = 7; // generated recipient id of groups, contacts, and/or note to self
|
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
|
repeated uint64 excludedRecipientIds = 8; // generated recipient id of groups, contacts, and/or note to self
|
||||||
}
|
}
|
|
@ -842,7 +842,7 @@ export class BackupExportStream extends Readable {
|
||||||
identityKey = identityKeysById.get(convo.serviceId);
|
identityKey = identityKeysById.get(convo.serviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nicknameGivenName, nicknameFamilyName } = convo;
|
const { nicknameGivenName, nicknameFamilyName, note } = convo;
|
||||||
|
|
||||||
res.contact = {
|
res.contact = {
|
||||||
aci:
|
aci:
|
||||||
|
@ -887,6 +887,7 @@ export class BackupExportStream extends Readable {
|
||||||
family: nicknameFamilyName,
|
family: nicknameFamilyName,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
note,
|
||||||
};
|
};
|
||||||
} else if (isGroupV2(convo) && convo.masterKey) {
|
} else if (isGroupV2(convo) && convo.masterKey) {
|
||||||
let storySendMode: Backups.Group.StorySendMode;
|
let storySendMode: Backups.Group.StorySendMode;
|
||||||
|
@ -994,6 +995,19 @@ export class BackupExportStream extends Readable {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === 'story') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
conversation &&
|
||||||
|
isGroupV2(conversation.attributes) &&
|
||||||
|
message.storyReplyContext
|
||||||
|
) {
|
||||||
|
// We drop group story replies
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const expirationTimestamp = calculateExpirationTimestamp(message);
|
const expirationTimestamp = calculateExpirationTimestamp(message);
|
||||||
if (expirationTimestamp != null && expirationTimestamp <= this.#now + DAY) {
|
if (expirationTimestamp != null && expirationTimestamp <= this.#now + DAY) {
|
||||||
// Message expires too soon
|
// Message expires too soon
|
||||||
|
@ -1220,6 +1234,17 @@ export class BackupExportStream extends Readable {
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (message.storyReplyContext) {
|
||||||
|
result.directStoryReplyMessage = await this.#toDirectStoryReplyMessage({
|
||||||
|
message,
|
||||||
|
backupLevel,
|
||||||
|
});
|
||||||
|
|
||||||
|
result.revisions = await this.#toChatItemRevisions(
|
||||||
|
result,
|
||||||
|
message,
|
||||||
|
backupLevel
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
result.standardMessage = await this.#toStandardMessage({
|
result.standardMessage = await this.#toStandardMessage({
|
||||||
message,
|
message,
|
||||||
|
@ -2527,6 +2552,51 @@ export class BackupExportStream extends Readable {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #toDirectStoryReplyMessage({
|
||||||
|
message,
|
||||||
|
backupLevel,
|
||||||
|
}: {
|
||||||
|
message: Pick<
|
||||||
|
MessageAttributesType,
|
||||||
|
| 'body'
|
||||||
|
| 'bodyAttachment'
|
||||||
|
| 'bodyRanges'
|
||||||
|
| 'storyReaction'
|
||||||
|
| 'storyReplyContext'
|
||||||
|
| 'received_at'
|
||||||
|
| 'reactions'
|
||||||
|
>;
|
||||||
|
backupLevel: BackupLevel;
|
||||||
|
}): Promise<Backups.IDirectStoryReplyMessage> {
|
||||||
|
const result = new Backups.DirectStoryReplyMessage({
|
||||||
|
reactions: this.#getMessageReactions(message),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (message.storyReaction) {
|
||||||
|
result.emoji = message.storyReaction.emoji;
|
||||||
|
} else {
|
||||||
|
result.textReply = {
|
||||||
|
longText: message.bodyAttachment
|
||||||
|
? await this.#processAttachment({
|
||||||
|
attachment: message.bodyAttachment,
|
||||||
|
backupLevel,
|
||||||
|
messageReceivedAt: message.received_at,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
text:
|
||||||
|
message.body != null
|
||||||
|
? {
|
||||||
|
body: message.body ? trimBody(message.body) : undefined,
|
||||||
|
bodyRanges: message.bodyRanges?.map(range =>
|
||||||
|
this.#toBodyRange(range)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async #toViewOnceMessage({
|
async #toViewOnceMessage({
|
||||||
message,
|
message,
|
||||||
backupLevel,
|
backupLevel,
|
||||||
|
@ -2568,7 +2638,7 @@ export class BackupExportStream extends Readable {
|
||||||
// The first history is the copy of the current message
|
// The first history is the copy of the current message
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map(async history => {
|
.map(async history => {
|
||||||
return {
|
const result: Backups.IChatItem = {
|
||||||
// Required fields
|
// Required fields
|
||||||
chatId: parent.chatId,
|
chatId: parent.chatId,
|
||||||
authorId: parent.authorId,
|
authorId: parent.authorId,
|
||||||
|
@ -2586,16 +2656,23 @@ export class BackupExportStream extends Readable {
|
||||||
incoming: isOutgoing
|
incoming: isOutgoing
|
||||||
? undefined
|
? undefined
|
||||||
: this.#getIncomingMessageDetails(history),
|
: this.#getIncomingMessageDetails(history),
|
||||||
|
|
||||||
// Message itself
|
|
||||||
standardMessage: await this.#toStandardMessage({
|
|
||||||
message: history,
|
|
||||||
backupLevel,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Backups use oldest to newest order
|
if (parent.directStoryReplyMessage) {
|
||||||
|
result.directStoryReplyMessage =
|
||||||
|
await this.#toDirectStoryReplyMessage({
|
||||||
|
message: history,
|
||||||
|
backupLevel,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.standardMessage = await this.#toStandardMessage({
|
||||||
|
message: history,
|
||||||
|
backupLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
})
|
})
|
||||||
|
// Backups use oldest to newest order
|
||||||
.reverse()
|
.reverse()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -914,6 +914,7 @@ export class BackupImportStream extends Writable {
|
||||||
expireTimerVersion: 1,
|
expireTimerVersion: 1,
|
||||||
nicknameGivenName: dropNull(contact.nickname?.given),
|
nicknameGivenName: dropNull(contact.nickname?.given),
|
||||||
nicknameFamilyName: dropNull(contact.nickname?.family),
|
nicknameFamilyName: dropNull(contact.nickname?.family),
|
||||||
|
note: dropNull(contact.note),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (serviceId != null && Bytes.isNotEmpty(contact.identityKey)) {
|
if (serviceId != null && Bytes.isNotEmpty(contact.identityKey)) {
|
||||||
|
@ -1441,6 +1442,26 @@ export class BackupImportStream extends Writable {
|
||||||
...attributes,
|
...attributes,
|
||||||
...(await this.#fromViewOnceMessage(item.viewOnceMessage)),
|
...(await this.#fromViewOnceMessage(item.viewOnceMessage)),
|
||||||
};
|
};
|
||||||
|
} else if (item.directStoryReplyMessage) {
|
||||||
|
strictAssert(item.directionless == null, 'reply cannot be directionless');
|
||||||
|
let storyAuthorAci: AciString | undefined;
|
||||||
|
if (item.incoming) {
|
||||||
|
strictAssert(this.#aboutMe?.aci, 'about me must exist');
|
||||||
|
storyAuthorAci = this.#aboutMe.aci;
|
||||||
|
} else {
|
||||||
|
strictAssert(
|
||||||
|
isAciString(chatConvo.serviceId),
|
||||||
|
'must have ACI for story author'
|
||||||
|
);
|
||||||
|
storyAuthorAci = chatConvo.serviceId;
|
||||||
|
}
|
||||||
|
attributes = {
|
||||||
|
...attributes,
|
||||||
|
...this.#fromDirectStoryReplyMessage(
|
||||||
|
item.directStoryReplyMessage,
|
||||||
|
storyAuthorAci
|
||||||
|
),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const result = await this.#fromNonBubbleChatItem(item, {
|
const result = await this.#fromNonBubbleChatItem(item, {
|
||||||
aboutMe,
|
aboutMe,
|
||||||
|
@ -1472,8 +1493,8 @@ export class BackupImportStream extends Writable {
|
||||||
|
|
||||||
if (item.revisions?.length) {
|
if (item.revisions?.length) {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
item.standardMessage,
|
item.standardMessage || item.directStoryReplyMessage,
|
||||||
`${logId}: Only standard message can have revisions`
|
`${logId}: Only standard or story reply message can have revisions`
|
||||||
);
|
);
|
||||||
|
|
||||||
const history = await this.#fromRevisions({
|
const history = await this.#fromRevisions({
|
||||||
|
@ -1793,9 +1814,6 @@ export class BackupImportStream extends Writable {
|
||||||
isStory: false,
|
isStory: false,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
if (isNightly(window.getVersion())) {
|
|
||||||
throw new Error(`${logId}: dropping invalid link preview`);
|
|
||||||
}
|
|
||||||
log.warn(`${logId}: dropping invalid link preview`);
|
log.warn(`${logId}: dropping invalid link preview`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1836,6 +1854,55 @@ export class BackupImportStream extends Writable {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#fromDirectStoryReplyMessage(
|
||||||
|
directStoryReplyMessage: Backups.IDirectStoryReplyMessage,
|
||||||
|
storyAuthorAci: AciString
|
||||||
|
): Partial<MessageAttributesType> {
|
||||||
|
const { reactions, textReply, emoji } = directStoryReplyMessage;
|
||||||
|
|
||||||
|
const result: Partial<MessageAttributesType> = {
|
||||||
|
reactions: this.#fromReactions(reactions),
|
||||||
|
storyReplyContext: {
|
||||||
|
authorAci: storyAuthorAci,
|
||||||
|
messageId: '', // stories are never imported
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (textReply) {
|
||||||
|
result.body = textReply.text?.body ?? undefined;
|
||||||
|
result.bodyRanges = this.#fromBodyRanges(textReply.text);
|
||||||
|
result.bodyAttachment = textReply.longText
|
||||||
|
? convertFilePointerToAttachment(textReply.longText)
|
||||||
|
: undefined;
|
||||||
|
} else if (emoji) {
|
||||||
|
result.storyReaction = {
|
||||||
|
emoji,
|
||||||
|
targetAuthorAci: storyAuthorAci,
|
||||||
|
targetTimestamp: 0, // stories are never imported
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #fromDirectStoryReplyRevision(
|
||||||
|
revision: Backups.IDirectStoryReplyMessage
|
||||||
|
): Promise<Partial<EditHistoryType>> {
|
||||||
|
const { textReply } = revision;
|
||||||
|
|
||||||
|
if (!textReply) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
body: textReply.text?.body ?? undefined,
|
||||||
|
bodyRanges: this.#fromBodyRanges(textReply.text),
|
||||||
|
bodyAttachment: textReply.longText
|
||||||
|
? convertFilePointerToAttachment(textReply.longText)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async #fromRevisions({
|
async #fromRevisions({
|
||||||
mainMessage,
|
mainMessage,
|
||||||
revisions,
|
revisions,
|
||||||
|
@ -1849,8 +1916,8 @@ export class BackupImportStream extends Writable {
|
||||||
revisions
|
revisions
|
||||||
.map(async rev => {
|
.map(async rev => {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
rev.standardMessage,
|
rev.standardMessage || rev.directStoryReplyMessage,
|
||||||
'Edit history has non-standard messages'
|
'Edit history on a message that does not support revisions'
|
||||||
);
|
);
|
||||||
|
|
||||||
const timestamp = getCheckedTimestampFromLong(rev.dateSent);
|
const timestamp = getCheckedTimestampFromLong(rev.dateSent);
|
||||||
|
@ -1866,11 +1933,7 @@ export class BackupImportStream extends Writable {
|
||||||
},
|
},
|
||||||
} = this.#fromDirectionDetails(rev, timestamp);
|
} = this.#fromDirectionDetails(rev, timestamp);
|
||||||
|
|
||||||
return {
|
const commonFields = {
|
||||||
...(await this.#fromStandardMessage({
|
|
||||||
logId,
|
|
||||||
data: rev.standardMessage,
|
|
||||||
})),
|
|
||||||
timestamp,
|
timestamp,
|
||||||
received_at: incrementMessageCounter(),
|
received_at: incrementMessageCounter(),
|
||||||
sendStateByConversationId,
|
sendStateByConversationId,
|
||||||
|
@ -1880,6 +1943,28 @@ export class BackupImportStream extends Writable {
|
||||||
readStatus,
|
readStatus,
|
||||||
unidentifiedDeliveryReceived,
|
unidentifiedDeliveryReceived,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (rev.standardMessage) {
|
||||||
|
return {
|
||||||
|
...(await this.#fromStandardMessage({
|
||||||
|
logId,
|
||||||
|
data: rev.standardMessage,
|
||||||
|
})),
|
||||||
|
...commonFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rev.directStoryReplyMessage) {
|
||||||
|
return {
|
||||||
|
...(await this.#fromDirectStoryReplyRevision(
|
||||||
|
rev.directStoryReplyMessage
|
||||||
|
)),
|
||||||
|
...commonFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'Edit history on a message that does not support revisions'
|
||||||
|
);
|
||||||
})
|
})
|
||||||
// Fix order: from newest to oldest
|
// Fix order: from newest to oldest
|
||||||
.reverse()
|
.reverse()
|
||||||
|
|
|
@ -24,6 +24,8 @@ import {
|
||||||
import { loadAllAndReinitializeRedux } from '../../services/allLoaders';
|
import { loadAllAndReinitializeRedux } from '../../services/allLoaders';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import type { MessageAttributesType } from '../../model-types';
|
import type { MessageAttributesType } from '../../model-types';
|
||||||
|
import { TEXT_ATTACHMENT } from '../../types/MIME';
|
||||||
|
import { MY_STORY_ID } from '../../types/Stories';
|
||||||
|
|
||||||
const CONTACT_A = generateAci();
|
const CONTACT_A = generateAci();
|
||||||
const CONTACT_B = generateAci();
|
const CONTACT_B = generateAci();
|
||||||
|
@ -817,4 +819,209 @@ describe('backup/bubble messages', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('stories', () => {
|
||||||
|
const GROUP_ID = Bytes.toBase64(getRandomBytes(32));
|
||||||
|
let group: ConversationModel | undefined;
|
||||||
|
let ourConversation: ConversationModel | undefined;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
group = await window.ConversationController.getOrCreateAndWait(
|
||||||
|
GROUP_ID,
|
||||||
|
'group',
|
||||||
|
{
|
||||||
|
groupVersion: 2,
|
||||||
|
masterKey: Bytes.toBase64(getRandomBytes(32)),
|
||||||
|
name: 'Rock Enthusiasts',
|
||||||
|
active_at: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ourConversation = window.ConversationController.get(OUR_ACI);
|
||||||
|
});
|
||||||
|
it('does not export stories', async () => {
|
||||||
|
strictAssert(ourConversation, 'conversations exist');
|
||||||
|
strictAssert(group, 'conversations exist');
|
||||||
|
const commonProps = {
|
||||||
|
type: 'story',
|
||||||
|
received_at: 3,
|
||||||
|
received_at_ms: 3,
|
||||||
|
sent_at: 3,
|
||||||
|
timestamp: 3,
|
||||||
|
sourceServiceId: OUR_ACI,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: TEXT_ATTACHMENT,
|
||||||
|
size: 4,
|
||||||
|
textAttachment: {
|
||||||
|
color: 4285041620,
|
||||||
|
text: 'test',
|
||||||
|
textForegroundColor: 4294967295,
|
||||||
|
textStyle: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
expirationStartTimestamp: Date.now(),
|
||||||
|
expireTimer: DurationInSeconds.fromMillis(WEEK),
|
||||||
|
} satisfies Partial<MessageAttributesType>;
|
||||||
|
|
||||||
|
const directStory: MessageAttributesType = {
|
||||||
|
...commonProps,
|
||||||
|
id: generateGuid(),
|
||||||
|
conversationId: ourConversation.id,
|
||||||
|
storyDistributionListId: MY_STORY_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupStory: MessageAttributesType = {
|
||||||
|
...commonProps,
|
||||||
|
id: generateGuid(),
|
||||||
|
conversationId: group.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await asymmetricRoundtripHarness([directStory, groupStory], []);
|
||||||
|
});
|
||||||
|
it('roundtrips direct story emoji replies', async () => {
|
||||||
|
strictAssert(ourConversation, 'conversations exist');
|
||||||
|
const commonProps = {
|
||||||
|
received_at: 3,
|
||||||
|
received_at_ms: 3,
|
||||||
|
sent_at: 3,
|
||||||
|
timestamp: 3,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
} satisfies Partial<MessageAttributesType>;
|
||||||
|
|
||||||
|
const incomingReply: MessageAttributesType = {
|
||||||
|
...commonProps,
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'incoming',
|
||||||
|
unidentifiedDeliveryReceived: true,
|
||||||
|
sourceServiceId: CONTACT_A,
|
||||||
|
storyReaction: {
|
||||||
|
emoji: '🤷♂️',
|
||||||
|
targetAuthorAci: OUR_ACI,
|
||||||
|
targetTimestamp: 0, // targetTimestamp is not roundtripped
|
||||||
|
},
|
||||||
|
storyReplyContext: {
|
||||||
|
authorAci: OUR_ACI,
|
||||||
|
messageId: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const outgoingReply: MessageAttributesType = {
|
||||||
|
...commonProps,
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'outgoing',
|
||||||
|
sourceServiceId: OUR_ACI,
|
||||||
|
storyReaction: {
|
||||||
|
emoji: '🤷♂️',
|
||||||
|
targetAuthorAci: CONTACT_A,
|
||||||
|
targetTimestamp: 0, // targetTimestamp is not roundtripped
|
||||||
|
},
|
||||||
|
storyReplyContext: {
|
||||||
|
authorAci: CONTACT_A,
|
||||||
|
messageId: '',
|
||||||
|
},
|
||||||
|
sendStateByConversationId: {
|
||||||
|
[CONTACT_A]: {
|
||||||
|
status: SendStatus.Read,
|
||||||
|
updatedAt: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await symmetricRoundtripHarness([incomingReply, outgoingReply]);
|
||||||
|
});
|
||||||
|
it('roundtrips direct story text replies', async () => {
|
||||||
|
strictAssert(ourConversation, 'conversations exist');
|
||||||
|
const commonProps = {
|
||||||
|
received_at: 3,
|
||||||
|
received_at_ms: 3,
|
||||||
|
sent_at: 3,
|
||||||
|
timestamp: 3,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
body: 'text reply to story',
|
||||||
|
} satisfies Partial<MessageAttributesType>;
|
||||||
|
|
||||||
|
const incomingReply: MessageAttributesType = {
|
||||||
|
...commonProps,
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'incoming',
|
||||||
|
unidentifiedDeliveryReceived: true,
|
||||||
|
sourceServiceId: CONTACT_A,
|
||||||
|
storyReplyContext: {
|
||||||
|
authorAci: OUR_ACI,
|
||||||
|
messageId: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const outgoingReply: MessageAttributesType = {
|
||||||
|
...commonProps,
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'outgoing',
|
||||||
|
sourceServiceId: OUR_ACI,
|
||||||
|
storyReplyContext: {
|
||||||
|
authorAci: CONTACT_A,
|
||||||
|
messageId: '',
|
||||||
|
},
|
||||||
|
sendStateByConversationId: {
|
||||||
|
[CONTACT_A]: {
|
||||||
|
status: SendStatus.Read,
|
||||||
|
updatedAt: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await symmetricRoundtripHarness([incomingReply, outgoingReply]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not export group story replies', async () => {
|
||||||
|
strictAssert(ourConversation, 'conversations exist');
|
||||||
|
strictAssert(group, 'conversations exist');
|
||||||
|
const commonProps = {
|
||||||
|
conversationId: group.id,
|
||||||
|
received_at: 3,
|
||||||
|
received_at_ms: 3,
|
||||||
|
sent_at: 3,
|
||||||
|
timestamp: 3,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
body: 'text reply to story',
|
||||||
|
} satisfies Partial<MessageAttributesType>;
|
||||||
|
|
||||||
|
const incomingReply: MessageAttributesType = {
|
||||||
|
...commonProps,
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'incoming',
|
||||||
|
unidentifiedDeliveryReceived: true,
|
||||||
|
sourceServiceId: CONTACT_A,
|
||||||
|
storyReplyContext: {
|
||||||
|
authorAci: OUR_ACI,
|
||||||
|
messageId: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const outgoingReply: MessageAttributesType = {
|
||||||
|
...commonProps,
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'outgoing',
|
||||||
|
sourceServiceId: OUR_ACI,
|
||||||
|
storyReplyContext: {
|
||||||
|
authorAci: CONTACT_A,
|
||||||
|
messageId: '',
|
||||||
|
},
|
||||||
|
sendStateByConversationId: {
|
||||||
|
[CONTACT_A]: {
|
||||||
|
status: SendStatus.Read,
|
||||||
|
updatedAt: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await asymmetricRoundtripHarness([incomingReply, outgoingReply], []);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -583,7 +583,7 @@ describe('backup/non-bubble messages', () => {
|
||||||
id: generateGuid(),
|
id: generateGuid(),
|
||||||
type: 'message-request-response-event',
|
type: 'message-request-response-event',
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: OUR_ACI,
|
||||||
sourceDevice: 1,
|
sourceDevice: 1,
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
seenStatus: SeenStatus.Seen,
|
seenStatus: SeenStatus.Seen,
|
||||||
|
|
Loading…
Reference in a new issue