Support for GV1 -> GV2 migration
This commit is contained in:
parent
a0baa3e03f
commit
2c69f2c367
32 changed files with 2626 additions and 341 deletions
|
@ -278,10 +278,21 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
|
||||
const groupVersion = this.get('groupVersion') || 0;
|
||||
|
||||
<<<<<<< HEAD
|
||||
return (
|
||||
groupVersion === 2 &&
|
||||
base64ToArrayBuffer(groupId).byteLength === window.Signal.Groups.ID_LENGTH
|
||||
);
|
||||
=======
|
||||
try {
|
||||
return (
|
||||
groupVersion === 2 && base64ToArrayBuffer(groupId).byteLength === 32
|
||||
);
|
||||
} catch (error) {
|
||||
window.log.error('isGroupV2: Failed to process groupId in base64!');
|
||||
return false;
|
||||
}
|
||||
>>>>>>> Support for GV1 -> GV2 migration
|
||||
}
|
||||
|
||||
isMemberPending(conversationId: string): boolean {
|
||||
|
@ -508,7 +519,6 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
const groupChange = await window.Signal.Groups.uploadGroupChange({
|
||||
actions,
|
||||
group: this.attributes,
|
||||
serverPublicParamsBase64: window.getServerPublicParams(),
|
||||
});
|
||||
|
||||
const groupChangeBuffer = groupChange.toArrayBuffer();
|
||||
|
@ -830,6 +840,21 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return this.isPrivate() || this.isGroupV1() || this.isGroupV2();
|
||||
}
|
||||
|
||||
async maybeMigrateV1Group(): Promise<void> {
|
||||
if (!this.isGroupV1()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMigrated = await window.Signal.Groups.hasV1GroupBeenMigrated(this);
|
||||
if (!isMigrated) {
|
||||
return;
|
||||
}
|
||||
|
||||
await window.Signal.Groups.waitThenRespondToGroupV2Migration({
|
||||
conversation: this,
|
||||
});
|
||||
}
|
||||
|
||||
maybeRepairGroupV2(data: {
|
||||
masterKey: string;
|
||||
secretParams: string;
|
||||
|
@ -1509,21 +1534,33 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
)
|
||||
);
|
||||
} catch (result) {
|
||||
if (result instanceof Error) {
|
||||
throw result;
|
||||
} else if (result && result.errors) {
|
||||
// We filter out unregistered user errors, because we ignore those in groups
|
||||
const wasThereARealError = window._.some(
|
||||
result.errors,
|
||||
error => error.name !== 'UnregisteredUserError'
|
||||
);
|
||||
if (wasThereARealError) {
|
||||
throw result;
|
||||
}
|
||||
}
|
||||
this.processSendResponse(result);
|
||||
}
|
||||
}
|
||||
|
||||
// We only want to throw if there's a 'real' error contained with this information
|
||||
// coming back from our low-level send infrastructure.
|
||||
processSendResponse(
|
||||
result: Error | CallbackResultType
|
||||
): result is CallbackResultType {
|
||||
if (result instanceof Error) {
|
||||
throw result;
|
||||
} else if (result && result.errors) {
|
||||
// We filter out unregistered user errors, because we ignore those in groups
|
||||
const wasThereARealError = window._.some(
|
||||
result.errors,
|
||||
error => error.name !== 'UnregisteredUserError'
|
||||
);
|
||||
if (wasThereARealError) {
|
||||
throw result;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onMessageError(): void {
|
||||
this.updateVerified();
|
||||
}
|
||||
|
@ -2916,10 +2953,6 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
};
|
||||
}
|
||||
|
||||
getUuidCapable(): boolean {
|
||||
return Boolean(window._.property('uuid')(this.get('capabilities')));
|
||||
}
|
||||
|
||||
getSendMetadata(
|
||||
options: { syncMessage?: string; disableMeCheck?: boolean } = {}
|
||||
): WhatIsThis | null {
|
||||
|
@ -2946,7 +2979,6 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
|
||||
const accessKey = this.get('accessKey');
|
||||
const sealedSender = this.get('sealedSender');
|
||||
const uuidCapable = this.getUuidCapable();
|
||||
|
||||
// We never send sync messages as sealed sender
|
||||
if (syncMessage && this.isMe()) {
|
||||
|
@ -2960,9 +2992,6 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
if (sealedSender === SEALED_SENDER.UNKNOWN) {
|
||||
const info = {
|
||||
accessKey: accessKey || arrayBufferToBase64(getRandomBytes(16)),
|
||||
// Indicates that a client is capable of receiving uuid-only messages.
|
||||
// Not used yet.
|
||||
uuidCapable,
|
||||
};
|
||||
return {
|
||||
...(e164 ? { [e164]: info } : {}),
|
||||
|
@ -2979,9 +3008,6 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
accessKey && sealedSender === SEALED_SENDER.ENABLED
|
||||
? accessKey
|
||||
: arrayBufferToBase64(getRandomBytes(16)),
|
||||
// Indicates that a client is capable of receiving uuid-only messages.
|
||||
// Not used yet.
|
||||
uuidCapable,
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -4172,19 +4198,16 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
});
|
||||
}
|
||||
|
||||
notifyTyping(
|
||||
options: {
|
||||
isTyping: boolean;
|
||||
senderId: string;
|
||||
isMe: boolean;
|
||||
senderDevice: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} = ({} as unknown) as any
|
||||
): void {
|
||||
const { isTyping, senderId, isMe, senderDevice } = options;
|
||||
notifyTyping(options: {
|
||||
isTyping: boolean;
|
||||
senderId: string;
|
||||
fromMe: boolean;
|
||||
senderDevice: string;
|
||||
}): void {
|
||||
const { isTyping, senderId, fromMe, senderDevice } = options;
|
||||
|
||||
// We don't do anything with typing messages from our other devices
|
||||
if (isMe) {
|
||||
if (fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
MessageAttributesType,
|
||||
CustomError,
|
||||
} from '../model-types.d';
|
||||
import { DataMessageClass } from '../textsecure.d';
|
||||
import { ConversationModel } from './conversations';
|
||||
import {
|
||||
LastMessageStatus,
|
||||
|
@ -22,6 +23,7 @@ import {
|
|||
} from '../components/conversation/TimerNotification';
|
||||
import { PropsData as SafetyNumberNotificationProps } from '../components/conversation/SafetyNumberNotification';
|
||||
import { PropsData as VerificationNotificationProps } from '../components/conversation/VerificationNotification';
|
||||
import { PropsDataType as GroupV1MigrationPropsType } from '../components/conversation/GroupV1Migration';
|
||||
import {
|
||||
PropsData as GroupNotificationProps,
|
||||
ChangeType,
|
||||
|
@ -195,6 +197,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
!this.isExpirationTimerUpdate() &&
|
||||
!this.isGroupUpdate() &&
|
||||
!this.isGroupV2Change() &&
|
||||
!this.isGroupV1Migration() &&
|
||||
!this.isKeyChange() &&
|
||||
!this.isMessageHistoryUnsynced() &&
|
||||
!this.isProfileChange() &&
|
||||
|
@ -217,6 +220,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
data: this.getPropsForGroupV2Change(),
|
||||
};
|
||||
}
|
||||
if (this.isGroupV1Migration()) {
|
||||
return {
|
||||
type: 'groupV1Migration',
|
||||
data: this.getPropsForGroupV1Migration(),
|
||||
};
|
||||
}
|
||||
if (this.isMessageHistoryUnsynced()) {
|
||||
return {
|
||||
type: 'linkNotification',
|
||||
|
@ -428,6 +437,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return Boolean(this.get('groupV2Change'));
|
||||
}
|
||||
|
||||
isGroupV1Migration(): boolean {
|
||||
return this.get('type') === 'group-v1-migration';
|
||||
}
|
||||
|
||||
isExpirationTimerUpdate(): boolean {
|
||||
const flag =
|
||||
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
|
@ -501,6 +514,23 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
};
|
||||
}
|
||||
|
||||
getPropsForGroupV1Migration(): GroupV1MigrationPropsType {
|
||||
const invitedGV2Members = this.get('invitedGV2Members') || [];
|
||||
const droppedGV2MemberIds = this.get('droppedGV2MemberIds') || [];
|
||||
|
||||
const invitedMembers = invitedGV2Members.map(item =>
|
||||
this.findAndFormatContact(item.conversationId)
|
||||
);
|
||||
const droppedMembers = droppedGV2MemberIds.map(conversationId =>
|
||||
this.findAndFormatContact(conversationId)
|
||||
);
|
||||
|
||||
return {
|
||||
droppedMembers,
|
||||
invitedMembers,
|
||||
};
|
||||
}
|
||||
|
||||
getPropsForTimerNotification(): TimerNotificationProps | undefined {
|
||||
const timerUpdate = this.get('expirationTimerUpdate');
|
||||
if (!timerUpdate) {
|
||||
|
@ -1082,9 +1112,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
};
|
||||
}
|
||||
|
||||
if (this.get('deletedForEveryone')) {
|
||||
if (this.isGroupV1Migration()) {
|
||||
return {
|
||||
text: window.i18n('message--deletedForEveryone'),
|
||||
text: window.i18n('GroupV1--Migration--was-upgraded'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2771,7 +2801,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
handleDataMessage(
|
||||
initialMessage: typeof window.WhatIsThis,
|
||||
initialMessage: DataMessageClass,
|
||||
confirm: () => void,
|
||||
options: { data?: typeof window.WhatIsThis } = {}
|
||||
): WhatIsThis {
|
||||
|
@ -2863,40 +2893,58 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
}
|
||||
|
||||
const existingRevision = conversation.get('revision');
|
||||
const isGroupV2 = Boolean(initialMessage.groupV2);
|
||||
const isV2GroupUpdate =
|
||||
initialMessage.groupV2 &&
|
||||
(!existingRevision ||
|
||||
initialMessage.groupV2.revision > existingRevision);
|
||||
|
||||
// GroupV2
|
||||
if (isGroupV2) {
|
||||
conversation.maybeRepairGroupV2(
|
||||
_.pick(initialMessage.groupV2, [
|
||||
'masterKey',
|
||||
'secretParams',
|
||||
'publicParams',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (isV2GroupUpdate) {
|
||||
const { revision, groupChange } = initialMessage.groupV2;
|
||||
try {
|
||||
await window.Signal.Groups.maybeUpdateGroup({
|
||||
if (initialMessage.groupV2) {
|
||||
if (conversation.isGroupV1()) {
|
||||
// If we received a GroupV2 message in a GroupV1 group, we migrate!
|
||||
|
||||
const { revision, groupChange } = initialMessage.groupV2;
|
||||
await window.Signal.Groups.respondToGroupV2Migration({
|
||||
conversation,
|
||||
groupChangeBase64: groupChange,
|
||||
newRevision: revision,
|
||||
receivedAt: message.get('received_at'),
|
||||
sentAt: message.get('sent_at'),
|
||||
});
|
||||
} catch (error) {
|
||||
const errorText = error && error.stack ? error.stack : error;
|
||||
window.log.error(
|
||||
`handleDataMessage: Failed to process group update for ${conversation.idForLogging()} as part of message ${message.idForLogging()}: ${errorText}`
|
||||
);
|
||||
throw error;
|
||||
} else if (
|
||||
initialMessage.groupV2.masterKey &&
|
||||
initialMessage.groupV2.secretParams &&
|
||||
initialMessage.groupV2.publicParams
|
||||
) {
|
||||
// Repair core GroupV2 data if needed
|
||||
await conversation.maybeRepairGroupV2({
|
||||
masterKey: initialMessage.groupV2.masterKey,
|
||||
secretParams: initialMessage.groupV2.secretParams,
|
||||
publicParams: initialMessage.groupV2.publicParams,
|
||||
});
|
||||
|
||||
// Standard GroupV2 modification codepath
|
||||
const existingRevision = conversation.get('revision');
|
||||
const isV2GroupUpdate =
|
||||
initialMessage.groupV2 &&
|
||||
_.isNumber(initialMessage.groupV2.revision) &&
|
||||
(!existingRevision ||
|
||||
initialMessage.groupV2.revision > existingRevision);
|
||||
|
||||
if (isV2GroupUpdate && initialMessage.groupV2) {
|
||||
const { revision, groupChange } = initialMessage.groupV2;
|
||||
try {
|
||||
await window.Signal.Groups.maybeUpdateGroup({
|
||||
conversation,
|
||||
groupChangeBase64: groupChange,
|
||||
newRevision: revision,
|
||||
receivedAt: message.get('received_at'),
|
||||
sentAt: message.get('sent_at'),
|
||||
});
|
||||
} catch (error) {
|
||||
const errorText = error && error.stack ? error.stack : error;
|
||||
window.log.error(
|
||||
`handleDataMessage: Failed to process group update for ${conversation.idForLogging()} as part of message ${message.idForLogging()}: ${errorText}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2907,6 +2955,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
e164: source,
|
||||
uuid: sourceUuid,
|
||||
})!;
|
||||
const isGroupV2 = Boolean(initialMessage.groupV2);
|
||||
const isV1GroupUpdate =
|
||||
initialMessage.group &&
|
||||
initialMessage.group.type !==
|
||||
|
@ -2949,6 +2998,16 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Because GroupV1 messages can now be multiplexed into GroupV2 conversations, we
|
||||
// drop GroupV1 updates in GroupV2 groups.
|
||||
if (isV1GroupUpdate && conversation.isGroupV2()) {
|
||||
window.log.warn(
|
||||
`Received GroupV1 update in GroupV2 conversation ${conversation.idForLogging()}. Dropping.`
|
||||
);
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
// Send delivery receipts, but only for incoming sealed sender messages
|
||||
// and not for messages from unaccepted conversations
|
||||
if (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue