Update libsignal to 0.50.0

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
Fedor Indutny 2024-06-10 14:37:14 -07:00 committed by GitHub
parent 33ec40d7b4
commit 8b969b5a0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 462 additions and 157 deletions

View file

@ -3373,6 +3373,10 @@ export class ConversationModel extends window.Backbone
return;
}
if (this.attributes.removalStage !== 'justNotification') {
return;
}
if (this.get('pendingRemovedContactNotification')) {
return;
}
@ -4140,6 +4144,11 @@ export class ConversationModel extends window.Backbone
this.maybeSetPendingUniversalTimer(stats.hasUserInitiatedMessages)
)
);
drop(
this.queueJob('maybeAddRemovedNotificaiton', async () =>
this.maybeSetContactRemoved()
)
);
const { preview, activity } = stats;
let previewMessage: MessageModel | undefined;

View file

@ -70,6 +70,7 @@ import {
isVerifiedChange,
isChangeNumberNotification,
isJoinedSignalNotification,
isTitleTransitionNotification,
} from '../../state/selectors/message';
import * as Bytes from '../../Bytes';
import { canBeSynced as canPreferredReactionEmojiBeSynced } from '../../reactions/preferredReactionEmoji';
@ -258,16 +259,21 @@ export class BackupExportStream extends Readable {
recipient: {
id: this.getDistributionListRecipientId(),
distributionList: {
name: list.name,
distributionId: uuidToBytes(list.id),
allowReplies: list.allowsReplies,
deletionTimestamp: list.deletedAtTimestamp
? Long.fromNumber(list.deletedAtTimestamp)
: null,
privacyMode,
memberRecipientIds: list.members.map(serviceId =>
this.getOrPushPrivateRecipient({ serviceId })
),
distributionList: list.deletedAtTimestamp
? null
: {
name: list.name,
allowReplies: list.allowsReplies,
privacyMode,
memberRecipientIds: list.members.map(serviceId =>
this.getOrPushPrivateRecipient({ serviceId })
),
},
},
},
});
@ -451,6 +457,9 @@ export class BackupExportStream extends Readable {
const usernameLink = storage.get('usernameLink');
const subscriberId = storage.get('subscriberId');
const backupsSubscriberId = storage.get('backupsSubscriberId');
return {
profileKey: storage.get('profileKey'),
username: me.get('username') || null,
@ -465,8 +474,18 @@ export class BackupExportStream extends Readable {
givenName: me.get('profileName'),
familyName: me.get('profileFamilyName'),
avatarUrlPath: storage.get('avatarUrl'),
subscriberId: storage.get('subscriberId'),
subscriberCurrencyCode: storage.get('subscriberCurrencyCode'),
backupsSubscriberData: Bytes.isNotEmpty(backupsSubscriberId)
? {
subscriberId: backupsSubscriberId,
currencyCode: storage.get('backupsSubscriberCurrencyCode'),
}
: null,
donationSubscriberData: Bytes.isNotEmpty(subscriberId)
? {
subscriberId,
currencyCode: storage.get('subscriberCurrencyCode'),
}
: null,
accountSettings: {
readReceipts: storage.get('read-receipt-setting'),
sealedSenderIndicators: storage.get('sealedSenderIndicators'),
@ -567,7 +586,17 @@ export class BackupExportStream extends Readable {
if (isMe(convo)) {
res.self = {};
} else if (isDirectConversation(convo)) {
const { Registered } = Backups.Contact;
let visibility: Backups.Contact.Visibility;
if (convo.removalStage == null) {
visibility = Backups.Contact.Visibility.VISIBLE;
} else if (convo.removalStage === 'justNotification') {
visibility = Backups.Contact.Visibility.HIDDEN;
} else if (convo.removalStage === 'messageRequest') {
visibility = Backups.Contact.Visibility.HIDDEN_MESSAGE_REQUEST;
} else {
throw missingCaseError(convo.removalStage);
}
res.contact = {
aci:
convo.serviceId && convo.serviceId !== convo.pni
@ -581,13 +610,18 @@ export class BackupExportStream extends Readable {
blocked: convo.serviceId
? window.storage.blocked.isServiceIdBlocked(convo.serviceId)
: null,
hidden: convo.removalStage !== undefined,
registered: isConversationUnregistered(convo)
? Registered.NOT_REGISTERED
: Registered.REGISTERED,
unregisteredTimestamp: convo.firstUnregisteredAt
? Long.fromNumber(convo.firstUnregisteredAt)
: null,
visibility,
...(isConversationUnregistered(convo)
? {
notRegistered: {
unregisteredTimestamp: convo.firstUnregisteredAt
? Long.fromNumber(convo.firstUnregisteredAt)
: null,
},
}
: {
registered: {},
}),
profileKey: convo.profileKey
? Bytes.fromBase64(convo.profileKey)
: null,
@ -1089,6 +1123,27 @@ export class BackupExportStream extends Readable {
return { kind: NonBubbleResultKind.Directionless, patch };
}
if (isTitleTransitionNotification(message)) {
strictAssert(
message.titleTransition != null,
'Missing title transition data'
);
const { renderInfo } = message.titleTransition;
if (renderInfo.e164) {
updateMessage.learnedProfileChange = {
e164: Long.fromString(renderInfo.e164),
};
} else {
strictAssert(
renderInfo.username,
'Title transition must have username or e164'
);
updateMessage.learnedProfileChange = { username: renderInfo.username };
}
return { kind: NonBubbleResultKind.Directionless, patch };
}
if (isDeliveryIssue(message)) {
updateMessage.simpleUpdate = {
type: Backups.SimpleChatUpdate.Type.BAD_DECRYPT,
@ -1129,7 +1184,6 @@ export class BackupExportStream extends Readable {
if (isContactRemovedNotification(message)) {
// Transient, drop it
// TODO: DESKTOP-7124
return { kind: NonBubbleResultKind.Drop };
}
@ -1138,9 +1192,8 @@ export class BackupExportStream extends Readable {
}
if (isGroupUpdate(message)) {
// TODO (DESKTOP-6964)
// these old-school message types are no longer generated but we probably
// still want to render them
// GV1 is deprecated.
return { kind: NonBubbleResultKind.Drop };
}
if (isUnsupportedMessage(message)) {

View file

@ -9,6 +9,7 @@ import { isNumber } from 'lodash';
import { Backups, SignalService } from '../../protobuf';
import Data from '../../sql/Client';
import type { StoryDistributionWithMembersType } from '../../sql/Interface';
import * as log from '../../logging/log';
import { StorySendMode } from '../../types/Stories';
import type { ServiceIdString, AciString } from '../../types/ServiceId';
@ -426,8 +427,8 @@ export class BackupImportStream extends Writable {
givenName,
familyName,
avatarUrlPath,
subscriberId,
subscriberCurrencyCode,
backupsSubscriberData,
donationSubscriberData,
accountSettings,
}: Backups.IAccountData): Promise<void> {
strictAssert(this.ourConversation === undefined, 'Duplicate AccountData');
@ -466,11 +467,23 @@ export class BackupImportStream extends Writable {
if (avatarUrlPath != null) {
await storage.put('avatarUrl', avatarUrlPath);
}
if (subscriberId != null) {
await storage.put('subscriberId', subscriberId);
if (donationSubscriberData != null) {
const { subscriberId, currencyCode } = donationSubscriberData;
if (Bytes.isNotEmpty(subscriberId)) {
await storage.put('subscriberId', subscriberId);
}
if (currencyCode != null) {
await storage.put('subscriberCurrencyCode', currencyCode);
}
}
if (subscriberCurrencyCode != null) {
await storage.put('subscriberCurrencyCode', subscriberCurrencyCode);
if (backupsSubscriberData != null) {
const { subscriberId, currencyCode } = backupsSubscriberData;
if (Bytes.isNotEmpty(subscriberId)) {
await storage.put('backupsSubscriberId', subscriberId);
}
if (currencyCode != null) {
await storage.put('backupsSubscriberCurrencyCode', currencyCode);
}
}
await storage.put(
@ -576,6 +589,20 @@ export class BackupImportStream extends Writable {
: undefined;
const e164 = contact.e164 ? `+${contact.e164}` : undefined;
let removalStage: 'justNotification' | 'messageRequest' | undefined;
switch (contact.visibility) {
case Backups.Contact.Visibility.HIDDEN:
removalStage = 'justNotification';
break;
case Backups.Contact.Visibility.HIDDEN_MESSAGE_REQUEST:
removalStage = 'messageRequest';
break;
case Backups.Contact.Visibility.VISIBLE:
default:
removalStage = undefined;
break;
}
const attrs: ConversationAttributesType = {
id: generateUuid(),
type: 'private',
@ -583,7 +610,7 @@ export class BackupImportStream extends Writable {
serviceId: aci ?? pni,
pni,
e164,
removalStage: contact.hidden ? 'messageRequest' : undefined,
removalStage,
profileKey: contact.profileKey
? Bytes.toBase64(contact.profileKey)
: undefined,
@ -593,10 +620,16 @@ export class BackupImportStream extends Writable {
hideStory: contact.hideStory === true,
};
if (contact.registered === Backups.Contact.Registered.NOT_REGISTERED) {
const timestamp = contact.unregisteredTimestamp?.toNumber() ?? Date.now();
if (contact.notRegistered) {
const timestamp =
contact.notRegistered.unregisteredTimestamp?.toNumber() ?? Date.now();
attrs.discoveredUnregisteredAt = timestamp;
attrs.firstUnregisteredAt = timestamp;
} else {
strictAssert(
contact.registered,
'contact is either registered or unregistered'
);
}
if (contact.blocked) {
@ -644,72 +677,92 @@ export class BackupImportStream extends Writable {
}
private async fromDistributionList(
list: Backups.IDistributionList
listItem: Backups.IDistributionListItem
): Promise<void> {
strictAssert(
Bytes.isNotEmpty(list.distributionId),
Bytes.isNotEmpty(listItem.distributionId),
'Missing distribution list id'
);
const id = bytesToUuid(list.distributionId);
const id = bytesToUuid(listItem.distributionId);
strictAssert(isStoryDistributionId(id), 'Invalid distribution list id');
strictAssert(
list.privacyMode != null,
'Missing distribution list privacy mode'
);
let isBlockList: boolean;
const { PrivacyMode } = Backups.DistributionList;
switch (list.privacyMode) {
case PrivacyMode.ALL:
strictAssert(
!list.memberRecipientIds?.length,
'Distribution list with ALL privacy mode has members'
);
isBlockList = true;
break;
case PrivacyMode.ALL_EXCEPT:
strictAssert(
list.memberRecipientIds?.length,
'Distribution list with ALL_EXCEPT privacy mode has no members'
);
isBlockList = true;
break;
case PrivacyMode.ONLY_WITH:
isBlockList = false;
break;
case PrivacyMode.UNKNOWN:
throw new Error('Invalid privacy mode for distribution list');
default:
throw missingCaseError(list.privacyMode);
}
const result = {
const commonFields = {
id,
name: list.name ?? '',
deletedAtTimestamp:
list.deletionTimestamp == null
? undefined
: getTimestampFromLong(list.deletionTimestamp),
allowsReplies: list.allowReplies === true,
isBlockList,
members: (list.memberRecipientIds || []).map(recipientId => {
const convo = this.recipientIdToConvo.get(recipientId.toNumber());
strictAssert(convo != null, 'Missing story distribution list member');
strictAssert(
convo.serviceId,
'Story distribution list member has no serviceId'
);
return convo.serviceId;
}),
// Default values
senderKeyInfo: undefined,
storageNeedsSync: false,
};
let result: StoryDistributionWithMembersType;
if (listItem.deletionTimestamp == null) {
const { distributionList: list } = listItem;
strictAssert(
list != null,
'Distribution list is either present or deleted'
);
strictAssert(
list.privacyMode != null,
'Missing distribution list privacy mode'
);
let isBlockList: boolean;
const { PrivacyMode } = Backups.DistributionList;
switch (list.privacyMode) {
case PrivacyMode.ALL:
strictAssert(
!list.memberRecipientIds?.length,
'Distribution list with ALL privacy mode has members'
);
isBlockList = true;
break;
case PrivacyMode.ALL_EXCEPT:
strictAssert(
list.memberRecipientIds?.length,
'Distribution list with ALL_EXCEPT privacy mode has no members'
);
isBlockList = true;
break;
case PrivacyMode.ONLY_WITH:
isBlockList = false;
break;
case PrivacyMode.UNKNOWN:
throw new Error('Invalid privacy mode for distribution list');
default:
throw missingCaseError(list.privacyMode);
}
result = {
...commonFields,
name: list.name ?? '',
allowsReplies: list.allowReplies === true,
isBlockList,
members: (list.memberRecipientIds || []).map(recipientId => {
const convo = this.recipientIdToConvo.get(recipientId.toNumber());
strictAssert(convo != null, 'Missing story distribution list member');
strictAssert(
convo.serviceId,
'Story distribution list member has no serviceId'
);
return convo.serviceId;
}),
};
} else {
result = {
...commonFields,
name: '',
allowsReplies: false,
isBlockList: false,
members: [],
deletedAtTimestamp: getTimestampFromLong(listItem.deletionTimestamp),
};
}
await Data.createNewStoryDistribution(result);
}
@ -1426,6 +1479,27 @@ export class BackupImportStream extends Writable {
};
}
if (updateMessage.learnedProfileChange) {
const { e164, username } = updateMessage.learnedProfileChange;
strictAssert(
e164 != null || username != null,
'learnedProfileChange must have an old name'
);
return {
message: {
type: 'title-transition-notification',
titleTransition: {
renderInfo: {
type: 'private',
e164: e164 && !e164.isZero() ? `+${e164}` : undefined,
username: dropNull(username),
},
},
},
additionalMessages: [],
};
}
if (updateMessage.threadMerge) {
const { previousE164 } = updateMessage.threadMerge;
strictAssert(previousE164 != null, 'threadMerge must have an old e164');

View file

@ -388,13 +388,25 @@ export function toAccountRecord(
accountRecord.pinnedConversations = pinnedConversations;
const subscriberId = window.storage.get('subscriberId');
if (subscriberId instanceof Uint8Array) {
if (Bytes.isNotEmpty(subscriberId)) {
accountRecord.subscriberId = subscriberId;
}
const subscriberCurrencyCode = window.storage.get('subscriberCurrencyCode');
const subscriberCurrencyCode = window.storage.get(
'backupsSubscriberCurrencyCode'
);
if (typeof subscriberCurrencyCode === 'string') {
accountRecord.subscriberCurrencyCode = subscriberCurrencyCode;
}
const backupsSubscriberId = window.storage.get('backupsSubscriberId');
if (Bytes.isNotEmpty(backupsSubscriberId)) {
accountRecord.backupsSubscriberId = backupsSubscriberId;
}
const backupsSubscriberCurrencyCode = window.storage.get(
'backupsSubscriberCurrencyCode'
);
if (typeof backupsSubscriberCurrencyCode === 'string') {
accountRecord.backupsSubscriberCurrencyCode = backupsSubscriberCurrencyCode;
}
const displayBadgesOnProfile = window.storage.get('displayBadgesOnProfile');
if (displayBadgesOnProfile !== undefined) {
accountRecord.displayBadgesOnProfile = displayBadgesOnProfile;
@ -1223,6 +1235,8 @@ export async function mergeAccountRecord(
preferredReactionEmoji: rawPreferredReactionEmoji,
subscriberId,
subscriberCurrencyCode,
backupsSubscriberId,
backupsSubscriberCurrencyCode,
displayBadgesOnProfile,
keepMutedChatsArchived,
hasCompletedUsernameOnboarding,
@ -1428,12 +1442,21 @@ export async function mergeAccountRecord(
);
}
if (subscriberId instanceof Uint8Array) {
if (Bytes.isNotEmpty(subscriberId)) {
await window.storage.put('subscriberId', subscriberId);
}
if (typeof subscriberCurrencyCode === 'string') {
await window.storage.put('subscriberCurrencyCode', subscriberCurrencyCode);
}
if (Bytes.isNotEmpty(backupsSubscriberId)) {
await window.storage.put('backupsSubscriberId', backupsSubscriberId);
}
if (typeof backupsSubscriberCurrencyCode === 'string') {
await window.storage.put(
'backupsSubscriberCurrencyCode',
backupsSubscriberCurrencyCode
);
}
await window.storage.put(
'displayBadgesOnProfile',
Boolean(displayBadgesOnProfile)

View file

@ -418,6 +418,26 @@ describe('backup/non-bubble messages', () => {
]);
});
it('roundtrips title transition notification', async () => {
await symmetricRoundtripHarness([
{
conversationId: contactA.id,
id: generateGuid(),
type: 'title-transition-notification',
received_at: 1,
sent_at: 1,
timestamp: 1,
sourceServiceId: CONTACT_A,
titleTransition: {
renderInfo: {
type: 'private',
e164: '+12125551234',
},
},
},
]);
});
it('roundtrips thread merge', async () => {
await symmetricRoundtripHarness([
{

View file

@ -154,6 +154,8 @@ export type StorageAccessType = {
areWeASubscriber: boolean;
subscriberId: Uint8Array;
subscriberCurrencyCode: string;
backupsSubscriberId: Uint8Array;
backupsSubscriberCurrencyCode: string;
displayBadgesOnProfile: boolean;
keepMutedChatsArchived: boolean;
usernameLastIntegrityCheck: number;