Groups: Show in left pane more often, proper join message
This commit is contained in:
parent
4e76259917
commit
dfd1190e8b
10 changed files with 403 additions and 276 deletions
|
@ -40,6 +40,7 @@ try {
|
||||||
window.GV2_ENABLE_SINGLE_CHANGE_PROCESSING = true;
|
window.GV2_ENABLE_SINGLE_CHANGE_PROCESSING = true;
|
||||||
window.GV2_ENABLE_CHANGE_PROCESSING = true;
|
window.GV2_ENABLE_CHANGE_PROCESSING = true;
|
||||||
window.GV2_ENABLE_STATE_PROCESSING = true;
|
window.GV2_ENABLE_STATE_PROCESSING = true;
|
||||||
|
window.GV2_ENABLE_PRE_JOIN_FETCH = true;
|
||||||
|
|
||||||
window.GV2_MIGRATION_DISABLE_ADD = false;
|
window.GV2_MIGRATION_DISABLE_ADD = false;
|
||||||
window.GV2_MIGRATION_DISABLE_INVITE = false;
|
window.GV2_MIGRATION_DISABLE_INVITE = false;
|
||||||
|
|
582
ts/groups.ts
582
ts/groups.ts
|
@ -343,7 +343,7 @@ export async function getPreJoinGroupInfo(
|
||||||
);
|
);
|
||||||
|
|
||||||
return makeRequestWithTemporalRetry({
|
return makeRequestWithTemporalRetry({
|
||||||
logId: `groupv2(${data.id})`,
|
logId: `getPreJoinInfo/groupv2(${data.id})`,
|
||||||
publicParams: Bytes.toBase64(data.publicParams),
|
publicParams: Bytes.toBase64(data.publicParams),
|
||||||
secretParams: Bytes.toBase64(data.secretParams),
|
secretParams: Bytes.toBase64(data.secretParams),
|
||||||
request: (sender, options) =>
|
request: (sender, options) =>
|
||||||
|
@ -2474,6 +2474,7 @@ export async function joinGroupV2ViaLinkAndMigrate({
|
||||||
secretParams,
|
secretParams,
|
||||||
groupInviteLinkPassword: inviteLinkPassword,
|
groupInviteLinkPassword: inviteLinkPassword,
|
||||||
|
|
||||||
|
addedBy: undefined,
|
||||||
left: true,
|
left: true,
|
||||||
|
|
||||||
// Capture previous GroupV1 data for future use
|
// Capture previous GroupV1 data for future use
|
||||||
|
@ -2637,6 +2638,7 @@ export async function respondToGroupV2Migration({
|
||||||
newAttributes: {
|
newAttributes: {
|
||||||
// Because we're using attributes here, we upgrade this to a v2 group
|
// Because we're using attributes here, we upgrade this to a v2 group
|
||||||
...attributes,
|
...attributes,
|
||||||
|
addedBy: undefined,
|
||||||
left: true,
|
left: true,
|
||||||
members: (conversation.get('members') || []).filter(
|
members: (conversation.get('members') || []).filter(
|
||||||
item => item !== ourUuid && item !== ourNumber
|
item => item !== ourUuid && item !== ourNumber
|
||||||
|
@ -2794,7 +2796,7 @@ const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||||
|
|
||||||
export async function waitThenMaybeUpdateGroup(
|
export async function waitThenMaybeUpdateGroup(
|
||||||
options: MaybeUpdatePropsType,
|
options: MaybeUpdatePropsType,
|
||||||
{ viaSync = false } = {}
|
{ viaFirstStorageSync = false } = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { conversation } = options;
|
const { conversation } = options;
|
||||||
|
|
||||||
|
@ -2826,7 +2828,7 @@ export async function waitThenMaybeUpdateGroup(
|
||||||
await conversation.queueJob('waitThenMaybeUpdateGroup', async () => {
|
await conversation.queueJob('waitThenMaybeUpdateGroup', async () => {
|
||||||
try {
|
try {
|
||||||
// And finally try to update the group
|
// And finally try to update the group
|
||||||
await maybeUpdateGroup(options, { viaSync });
|
await maybeUpdateGroup(options, { viaFirstStorageSync });
|
||||||
|
|
||||||
conversation.lastSuccessfulGroupFetch = Date.now();
|
conversation.lastSuccessfulGroupFetch = Date.now();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -2847,7 +2849,7 @@ export async function maybeUpdateGroup(
|
||||||
receivedAt,
|
receivedAt,
|
||||||
sentAt,
|
sentAt,
|
||||||
}: MaybeUpdatePropsType,
|
}: MaybeUpdatePropsType,
|
||||||
{ viaSync = false } = {}
|
{ viaFirstStorageSync = false } = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const logId = conversation.idForLogging();
|
const logId = conversation.idForLogging();
|
||||||
|
|
||||||
|
@ -2865,7 +2867,7 @@ export async function maybeUpdateGroup(
|
||||||
|
|
||||||
await updateGroup(
|
await updateGroup(
|
||||||
{ conversation, receivedAt, sentAt, updates },
|
{ conversation, receivedAt, sentAt, updates },
|
||||||
{ viaSync }
|
{ viaFirstStorageSync }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -2888,21 +2890,27 @@ async function updateGroup(
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
updates: UpdatesResultType;
|
updates: UpdatesResultType;
|
||||||
},
|
},
|
||||||
{ viaSync = false } = {}
|
{ viaFirstStorageSync = false } = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const logId = conversation.idForLogging();
|
const logId = conversation.idForLogging();
|
||||||
|
|
||||||
const { newAttributes, groupChangeMessages, members } = updates;
|
const { newAttributes, groupChangeMessages, members } = updates;
|
||||||
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
|
||||||
|
|
||||||
const startingRevision = conversation.get('revision');
|
const startingRevision = conversation.get('revision');
|
||||||
const endingRevision = newAttributes.revision;
|
const endingRevision = newAttributes.revision;
|
||||||
|
|
||||||
const isInGroup = !updates.newAttributes.left;
|
const wasMemberOrPending =
|
||||||
|
conversation.hasMember(ourUuid) || conversation.isMemberPending(ourUuid);
|
||||||
|
const isMemberOrPending =
|
||||||
|
!newAttributes.left ||
|
||||||
|
newAttributes.pendingMembersV2?.some(item => item.uuid === ourUuid);
|
||||||
|
const isMemberOrPendingOrAwaitingApproval =
|
||||||
|
isMemberOrPending ||
|
||||||
|
newAttributes.pendingAdminApprovalV2?.some(item => item.uuid === ourUuid);
|
||||||
|
|
||||||
const isInitialDataFetch =
|
const isInitialDataFetch =
|
||||||
isInGroup && !isNumber(startingRevision) && isNumber(endingRevision);
|
!isNumber(startingRevision) && isNumber(endingRevision);
|
||||||
const justJoinedGroup =
|
|
||||||
isInGroup && !conversation.hasMember(ourUuid.toString());
|
|
||||||
|
|
||||||
// Ensure that all generated messages are ordered properly.
|
// Ensure that all generated messages are ordered properly.
|
||||||
// Before the provided timestamp so update messages appear before the
|
// Before the provided timestamp so update messages appear before the
|
||||||
|
@ -2916,16 +2924,18 @@ async function updateGroup(
|
||||||
const previousId = conversation.get('groupId');
|
const previousId = conversation.get('groupId');
|
||||||
const idChanged = previousId && previousId !== newAttributes.groupId;
|
const idChanged = previousId && previousId !== newAttributes.groupId;
|
||||||
|
|
||||||
// We force this conversation into the left pane if this is the first time we've
|
// By updating activeAt we force this conversation into the left pane if this is the
|
||||||
// fetched data about it, and we were able to fetch its name. Nobody likes to see
|
// first time we've fetched data about it, and we were able to fetch its name. Nobody
|
||||||
// Unknown Group in the left pane.
|
// likes to see Unknown Group in the left pane. After first fetch, we rely on normal
|
||||||
let activeAt = null;
|
// message activity (including group change messsages) to set the timestamp properly.
|
||||||
if (viaSync) {
|
let activeAt = conversation.get('active_at') || null;
|
||||||
activeAt = conversation.get('active_at') || null;
|
if (
|
||||||
} else if ((isInitialDataFetch || justJoinedGroup) && newAttributes.name) {
|
!viaFirstStorageSync &&
|
||||||
|
isMemberOrPendingOrAwaitingApproval &&
|
||||||
|
isInitialDataFetch &&
|
||||||
|
newAttributes.name
|
||||||
|
) {
|
||||||
activeAt = initialSentAt;
|
activeAt = initialSentAt;
|
||||||
} else {
|
|
||||||
activeAt = newAttributes.active_at;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save all synthetic messages describing group changes
|
// Save all synthetic messages describing group changes
|
||||||
|
@ -3003,7 +3013,7 @@ async function updateGroup(
|
||||||
conversation.set({
|
conversation.set({
|
||||||
...newAttributes,
|
...newAttributes,
|
||||||
active_at: activeAt,
|
active_at: activeAt,
|
||||||
temporaryMemberCount: isInGroup
|
temporaryMemberCount: !newAttributes.left
|
||||||
? undefined
|
? undefined
|
||||||
: newAttributes.temporaryMemberCount,
|
: newAttributes.temporaryMemberCount,
|
||||||
});
|
});
|
||||||
|
@ -3014,6 +3024,37 @@ async function updateGroup(
|
||||||
|
|
||||||
// Save these most recent updates to conversation
|
// Save these most recent updates to conversation
|
||||||
await updateConversation(conversation.attributes);
|
await updateConversation(conversation.attributes);
|
||||||
|
|
||||||
|
// If we've been added by a blocked contact, then schedule a task to leave group
|
||||||
|
const justAdded = !wasMemberOrPending && isMemberOrPending;
|
||||||
|
const addedBy =
|
||||||
|
newAttributes.pendingMembersV2?.find(item => item.uuid === ourUuid)
|
||||||
|
?.addedByUserId || newAttributes.addedBy;
|
||||||
|
|
||||||
|
if (justAdded && addedBy) {
|
||||||
|
const adder = window.ConversationController.get(addedBy);
|
||||||
|
|
||||||
|
if (adder && adder.isBlocked()) {
|
||||||
|
log.warn(
|
||||||
|
`updateGroup/${logId}: Added to group by blocked user ${adder.idForLogging()}. Scheduling group leave.`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for empty queue to make it more likely the group update succeeds
|
||||||
|
const waitThenLeave = async () => {
|
||||||
|
log.warn(`waitThenLeave/${logId}: Waiting for empty event queue.`);
|
||||||
|
await window.waitForEmptyEventQueue();
|
||||||
|
log.warn(
|
||||||
|
`waitThenLeave/${logId}: Empty event queue, starting group leave.`
|
||||||
|
);
|
||||||
|
|
||||||
|
await conversation.leaveGroupV2();
|
||||||
|
log.warn(`waitThenLeave/${logId}: Leave complete.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cannot await here, would infinitely block queue
|
||||||
|
waitThenLeave();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exported for testing
|
// Exported for testing
|
||||||
|
@ -3295,7 +3336,6 @@ async function getGroupUpdates({
|
||||||
group,
|
group,
|
||||||
newRevision,
|
newRevision,
|
||||||
groupChange,
|
groupChange,
|
||||||
serverPublicParamsBase64,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3309,13 +3349,10 @@ async function getGroupUpdates({
|
||||||
window.GV2_ENABLE_CHANGE_PROCESSING
|
window.GV2_ENABLE_CHANGE_PROCESSING
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const result = await updateGroupViaLogs({
|
return await updateGroupViaLogs({
|
||||||
group,
|
group,
|
||||||
serverPublicParamsBase64,
|
|
||||||
newRevision,
|
newRevision,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const nextStep = isFirstFetch
|
const nextStep = isFirstFetch
|
||||||
? `fetching logs since ${newRevision}`
|
? `fetching logs since ${newRevision}`
|
||||||
|
@ -3338,11 +3375,44 @@ async function getGroupUpdates({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.GV2_ENABLE_STATE_PROCESSING) {
|
if (window.GV2_ENABLE_STATE_PROCESSING) {
|
||||||
return updateGroupViaState({
|
try {
|
||||||
dropInitialJoinMessage,
|
return await updateGroupViaState({
|
||||||
group,
|
dropInitialJoinMessage,
|
||||||
serverPublicParamsBase64,
|
group,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === TEMPORAL_AUTH_REJECTED_CODE) {
|
||||||
|
log.info(
|
||||||
|
`getGroupUpdates/${logId}: Temporal credential failure. Failing; we don't know if we have access or not.`
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} else if (error.code === GROUP_ACCESS_DENIED_CODE) {
|
||||||
|
// We will fail over to the updateGroupViaPreJoinInfo call below
|
||||||
|
log.info(
|
||||||
|
`getGroupUpdates/${logId}: Failed to get group state. Attempting to fetch pre-join information.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.GV2_ENABLE_PRE_JOIN_FETCH) {
|
||||||
|
try {
|
||||||
|
return await updateGroupViaPreJoinInfo({
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === GROUP_ACCESS_DENIED_CODE) {
|
||||||
|
return generateLeftGroupChanges(group);
|
||||||
|
}
|
||||||
|
if (error.code === GROUP_NONEXISTENT_CODE) {
|
||||||
|
return generateLeftGroupChanges(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get another temporal failure, we'll fail and try again later.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn(
|
log.warn(
|
||||||
|
@ -3355,70 +3425,141 @@ async function getGroupUpdates({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateGroupViaState({
|
async function updateGroupViaPreJoinInfo({
|
||||||
dropInitialJoinMessage,
|
|
||||||
group,
|
group,
|
||||||
serverPublicParamsBase64,
|
|
||||||
}: {
|
}: {
|
||||||
dropInitialJoinMessage?: boolean;
|
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
serverPublicParamsBase64: string;
|
|
||||||
}): Promise<UpdatesResultType> {
|
}): Promise<UpdatesResultType> {
|
||||||
const logId = idForLogging(group.groupId);
|
const logId = idForLogging(group.groupId);
|
||||||
const data = window.storage.get(GROUP_CREDENTIALS_KEY);
|
const data = window.storage.get(GROUP_CREDENTIALS_KEY);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error('updateGroupViaState: No group credentials!');
|
throw new Error('updateGroupViaPreJoinInfo: No group credentials!');
|
||||||
|
}
|
||||||
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
|
||||||
|
|
||||||
|
const { publicParams, secretParams } = group;
|
||||||
|
if (!secretParams) {
|
||||||
|
throw new Error(
|
||||||
|
'updateGroupViaPreJoinInfo: group was missing secretParams!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!publicParams) {
|
||||||
|
throw new Error(
|
||||||
|
'updateGroupViaPreJoinInfo: group was missing publicParams!'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupCredentials = getCredentialsForToday(data);
|
// No password, but if we're already pending approval, we can access this without it.
|
||||||
|
const inviteLinkPassword = undefined;
|
||||||
|
const preJoinInfo = await makeRequestWithTemporalRetry({
|
||||||
|
logId: `getPreJoinInfo/${logId}`,
|
||||||
|
publicParams,
|
||||||
|
secretParams,
|
||||||
|
request: (sender, options) =>
|
||||||
|
sender.getGroupFromLink(inviteLinkPassword, options),
|
||||||
|
});
|
||||||
|
|
||||||
const stateOptions = {
|
const approvalRequired =
|
||||||
dropInitialJoinMessage,
|
preJoinInfo.addFromInviteLink ===
|
||||||
group,
|
Proto.AccessControl.AccessRequired.ADMINISTRATOR;
|
||||||
serverPublicParamsBase64,
|
|
||||||
authCredentialBase64: groupCredentials.today.credential,
|
// If the group doesn't require approval to join via link, then we should never have
|
||||||
|
// gotten here.
|
||||||
|
if (!approvalRequired) {
|
||||||
|
return generateLeftGroupChanges(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newAttributes: ConversationAttributesType = {
|
||||||
|
...group,
|
||||||
|
description: decryptGroupDescription(
|
||||||
|
preJoinInfo.descriptionBytes,
|
||||||
|
secretParams
|
||||||
|
),
|
||||||
|
name: decryptGroupTitle(preJoinInfo.title, secretParams),
|
||||||
|
members: [],
|
||||||
|
pendingMembersV2: [],
|
||||||
|
pendingAdminApprovalV2: [
|
||||||
|
{
|
||||||
|
uuid: ourUuid,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
revision: preJoinInfo.version,
|
||||||
|
|
||||||
|
temporaryMemberCount: preJoinInfo.memberCount || 1,
|
||||||
};
|
};
|
||||||
try {
|
|
||||||
log.info(`updateGroupViaState/${logId}: Getting full group state...`);
|
|
||||||
// We await this here so our try/catch below takes effect
|
|
||||||
const result = await getCurrentGroupState(stateOptions);
|
|
||||||
|
|
||||||
return result;
|
await applyNewAvatar(dropNull(preJoinInfo.avatar), newAttributes, logId);
|
||||||
} catch (error) {
|
|
||||||
if (error.code === GROUP_ACCESS_DENIED_CODE) {
|
|
||||||
return generateLeftGroupChanges(group);
|
|
||||||
}
|
|
||||||
if (error.code === TEMPORAL_AUTH_REJECTED_CODE) {
|
|
||||||
log.info(
|
|
||||||
`updateGroupViaState/${logId}: Credential for today failed, failing over to tomorrow...`
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
const result = await getCurrentGroupState({
|
|
||||||
...stateOptions,
|
|
||||||
authCredentialBase64: groupCredentials.tomorrow.credential,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
} catch (subError) {
|
|
||||||
if (subError.code === GROUP_ACCESS_DENIED_CODE) {
|
|
||||||
return generateLeftGroupChanges(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
return {
|
||||||
|
newAttributes,
|
||||||
|
groupChangeMessages: extractDiffs({
|
||||||
|
old: group,
|
||||||
|
current: newAttributes,
|
||||||
|
dropInitialJoinMessage: false,
|
||||||
|
}),
|
||||||
|
members: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGroupViaState({
|
||||||
|
dropInitialJoinMessage,
|
||||||
|
group,
|
||||||
|
}: {
|
||||||
|
dropInitialJoinMessage?: boolean;
|
||||||
|
group: ConversationAttributesType;
|
||||||
|
}): Promise<UpdatesResultType> {
|
||||||
|
const logId = idForLogging(group.groupId);
|
||||||
|
const { publicParams, secretParams } = group;
|
||||||
|
if (!secretParams) {
|
||||||
|
throw new Error('updateGroupViaState: group was missing secretParams!');
|
||||||
}
|
}
|
||||||
|
if (!publicParams) {
|
||||||
|
throw new Error('updateGroupViaState: group was missing publicParams!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupState = await makeRequestWithTemporalRetry({
|
||||||
|
logId: `getGroup/${logId}`,
|
||||||
|
publicParams,
|
||||||
|
secretParams,
|
||||||
|
request: (sender, requestOptions) => sender.getGroup(requestOptions),
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedGroupState = decryptGroupState(
|
||||||
|
groupState,
|
||||||
|
secretParams,
|
||||||
|
logId
|
||||||
|
);
|
||||||
|
|
||||||
|
const oldVersion = group.revision;
|
||||||
|
const newVersion = decryptedGroupState.version;
|
||||||
|
log.info(
|
||||||
|
`getCurrentGroupState/${logId}: Applying full group state, from version ${oldVersion} to ${newVersion}.`
|
||||||
|
);
|
||||||
|
const { newAttributes, newProfileKeys } = await applyGroupState({
|
||||||
|
group,
|
||||||
|
groupState: decryptedGroupState,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
newAttributes,
|
||||||
|
groupChangeMessages: extractDiffs({
|
||||||
|
old: group,
|
||||||
|
current: newAttributes,
|
||||||
|
dropInitialJoinMessage,
|
||||||
|
}),
|
||||||
|
members: profileKeysToMembers(newProfileKeys),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateGroupViaSingleChange({
|
async function updateGroupViaSingleChange({
|
||||||
group,
|
group,
|
||||||
groupChange,
|
groupChange,
|
||||||
newRevision,
|
newRevision,
|
||||||
serverPublicParamsBase64,
|
|
||||||
}: {
|
}: {
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
groupChange: Proto.IGroupChange;
|
groupChange: Proto.IGroupChange;
|
||||||
newRevision: number;
|
newRevision: number;
|
||||||
serverPublicParamsBase64: string;
|
|
||||||
}): Promise<UpdatesResultType> {
|
}): Promise<UpdatesResultType> {
|
||||||
const wasInGroup = !group.left;
|
const wasInGroup = !group.left;
|
||||||
const result: UpdatesResultType = await integrateGroupChange({
|
const result: UpdatesResultType = await integrateGroupChange({
|
||||||
|
@ -3434,7 +3575,6 @@ async function updateGroupViaSingleChange({
|
||||||
if (!wasInGroup && nowInGroup) {
|
if (!wasInGroup && nowInGroup) {
|
||||||
const { newAttributes, members } = await updateGroupViaState({
|
const { newAttributes, members } = await updateGroupViaState({
|
||||||
group: result.newAttributes,
|
group: result.newAttributes,
|
||||||
serverPublicParamsBase64,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// We discard any change events that come out of this full group fetch, but we do
|
// We discard any change events that come out of this full group fetch, but we do
|
||||||
|
@ -3451,48 +3591,73 @@ async function updateGroupViaSingleChange({
|
||||||
|
|
||||||
async function updateGroupViaLogs({
|
async function updateGroupViaLogs({
|
||||||
group,
|
group,
|
||||||
serverPublicParamsBase64,
|
|
||||||
newRevision,
|
newRevision,
|
||||||
}: {
|
}: {
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
newRevision: number | undefined;
|
newRevision: number | undefined;
|
||||||
serverPublicParamsBase64: string;
|
|
||||||
}): Promise<UpdatesResultType> {
|
}): Promise<UpdatesResultType> {
|
||||||
const logId = idForLogging(group.groupId);
|
const logId = idForLogging(group.groupId);
|
||||||
const data = window.storage.get(GROUP_CREDENTIALS_KEY);
|
const { publicParams, secretParams } = group;
|
||||||
if (!data) {
|
if (!publicParams) {
|
||||||
throw new Error('getGroupUpdates: No group credentials!');
|
throw new Error('updateGroupViaLogs: group was missing publicParams!');
|
||||||
|
}
|
||||||
|
if (!secretParams) {
|
||||||
|
throw new Error('updateGroupViaLogs: group was missing secretParams!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupCredentials = getCredentialsForToday(data);
|
log.info(
|
||||||
const deltaOptions = {
|
`updateGroupViaLogs/${logId}: Getting group delta from ` +
|
||||||
|
`${group.revision ?? '?'} to ${newRevision ?? '?'} for group ` +
|
||||||
|
`groupv2(${group.groupId})...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentRevision = group.revision;
|
||||||
|
let includeFirstState = true;
|
||||||
|
|
||||||
|
// The range is inclusive so make sure that we always request the revision
|
||||||
|
// that we are currently at since we might want the latest full state in
|
||||||
|
// `integrateGroupChanges`.
|
||||||
|
let revisionToFetch = isNumber(currentRevision) ? currentRevision : undefined;
|
||||||
|
|
||||||
|
let response;
|
||||||
|
const changes: Array<Proto.IGroupChanges> = [];
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
response = await makeRequestWithTemporalRetry({
|
||||||
|
logId: `getGroupLog/${logId}`,
|
||||||
|
publicParams,
|
||||||
|
secretParams,
|
||||||
|
// eslint-disable-next-line no-loop-func
|
||||||
|
request: (sender, requestOptions) =>
|
||||||
|
sender.getGroupLog(
|
||||||
|
{
|
||||||
|
startVersion: revisionToFetch,
|
||||||
|
includeFirstState,
|
||||||
|
includeLastState: true,
|
||||||
|
maxSupportedChangeEpoch: SUPPORTED_CHANGE_EPOCH,
|
||||||
|
},
|
||||||
|
requestOptions
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
changes.push(response.changes);
|
||||||
|
if (response.end) {
|
||||||
|
revisionToFetch = response.end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
includeFirstState = false;
|
||||||
|
} while (
|
||||||
|
response.end &&
|
||||||
|
(newRevision === undefined || response.end < newRevision)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Would be nice to cache the unused groupChanges here, to reduce server roundtrips
|
||||||
|
|
||||||
|
return integrateGroupChanges({
|
||||||
|
changes,
|
||||||
group,
|
group,
|
||||||
newRevision,
|
newRevision,
|
||||||
serverPublicParamsBase64,
|
});
|
||||||
authCredentialBase64: groupCredentials.today.credential,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
log.info(
|
|
||||||
`updateGroupViaLogs/${logId}: Getting group delta from ` +
|
|
||||||
`${group.revision ?? '?'} to ${newRevision ?? '?'} for group ` +
|
|
||||||
`groupv2(${group.groupId})...`
|
|
||||||
);
|
|
||||||
const result = await getGroupDelta(deltaOptions);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === TEMPORAL_AUTH_REJECTED_CODE) {
|
|
||||||
log.info(
|
|
||||||
`updateGroupViaLogs/${logId}: Credential for today failed, failing over to tomorrow...`
|
|
||||||
);
|
|
||||||
|
|
||||||
return getGroupDelta({
|
|
||||||
...deltaOptions,
|
|
||||||
authCredentialBase64: groupCredentials.tomorrow.credential,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateLeftGroupChanges(
|
async function generateLeftGroupChanges(
|
||||||
|
@ -3524,34 +3689,28 @@ async function generateLeftGroupChanges(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingMembers = group.membersV2 || [];
|
|
||||||
const newAttributes: ConversationAttributesType = {
|
const newAttributes: ConversationAttributesType = {
|
||||||
...group,
|
...group,
|
||||||
membersV2: existingMembers.filter(member => member.uuid !== ourUuid),
|
addedBy: undefined,
|
||||||
|
membersV2: (group.membersV2 || []).filter(
|
||||||
|
member => member.uuid !== ourUuid
|
||||||
|
),
|
||||||
|
pendingMembersV2: (group.pendingMembersV2 || []).filter(
|
||||||
|
member => member.uuid !== ourUuid
|
||||||
|
),
|
||||||
|
pendingAdminApprovalV2: (group.pendingAdminApprovalV2 || []).filter(
|
||||||
|
member => member.uuid !== ourUuid
|
||||||
|
),
|
||||||
left: true,
|
left: true,
|
||||||
revision,
|
revision,
|
||||||
};
|
};
|
||||||
const isNewlyRemoved =
|
|
||||||
existingMembers.length > (newAttributes.membersV2 || []).length;
|
|
||||||
|
|
||||||
const youWereRemovedMessage: GroupChangeMessageType = {
|
|
||||||
...generateBasicMessage(),
|
|
||||||
type: 'group-v2-change',
|
|
||||||
groupV2Change: {
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
type: 'member-remove' as const,
|
|
||||||
uuid: ourUuid,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
readStatus: ReadStatus.Read,
|
|
||||||
seenStatus: SeenStatus.Unseen,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newAttributes,
|
newAttributes,
|
||||||
groupChangeMessages: isNewlyRemoved ? [youWereRemovedMessage] : [],
|
groupChangeMessages: extractDiffs({
|
||||||
|
current: newAttributes,
|
||||||
|
old: group,
|
||||||
|
}),
|
||||||
members: [],
|
members: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3583,76 +3742,6 @@ function getGroupCredentials({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getGroupDelta({
|
|
||||||
group,
|
|
||||||
newRevision,
|
|
||||||
serverPublicParamsBase64,
|
|
||||||
authCredentialBase64,
|
|
||||||
}: {
|
|
||||||
group: ConversationAttributesType;
|
|
||||||
newRevision: number | undefined;
|
|
||||||
serverPublicParamsBase64: string;
|
|
||||||
authCredentialBase64: string;
|
|
||||||
}): Promise<UpdatesResultType> {
|
|
||||||
const sender = window.textsecure.messaging;
|
|
||||||
if (!sender) {
|
|
||||||
throw new Error('getGroupDelta: textsecure.messaging is not available!');
|
|
||||||
}
|
|
||||||
if (!group.publicParams) {
|
|
||||||
throw new Error('getGroupDelta: group was missing publicParams!');
|
|
||||||
}
|
|
||||||
if (!group.secretParams) {
|
|
||||||
throw new Error('getGroupDelta: group was missing secretParams!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = getGroupCredentials({
|
|
||||||
authCredentialBase64,
|
|
||||||
groupPublicParamsBase64: group.publicParams,
|
|
||||||
groupSecretParamsBase64: group.secretParams,
|
|
||||||
serverPublicParamsBase64,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentRevision = group.revision;
|
|
||||||
let includeFirstState = true;
|
|
||||||
|
|
||||||
// The range is inclusive so make sure that we always request the revision
|
|
||||||
// that we are currently at since we might want the latest full state in
|
|
||||||
// `integrateGroupChanges`.
|
|
||||||
let revisionToFetch = isNumber(currentRevision) ? currentRevision : undefined;
|
|
||||||
|
|
||||||
let response;
|
|
||||||
const changes: Array<Proto.IGroupChanges> = [];
|
|
||||||
do {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
response = await sender.getGroupLog(
|
|
||||||
{
|
|
||||||
startVersion: revisionToFetch,
|
|
||||||
includeFirstState,
|
|
||||||
includeLastState: true,
|
|
||||||
maxSupportedChangeEpoch: SUPPORTED_CHANGE_EPOCH,
|
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
|
||||||
changes.push(response.changes);
|
|
||||||
if (response.end) {
|
|
||||||
revisionToFetch = response.end + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
includeFirstState = false;
|
|
||||||
} while (
|
|
||||||
response.end &&
|
|
||||||
(newRevision === undefined || response.end < newRevision)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Would be nice to cache the unused groupChanges here, to reduce server roundtrips
|
|
||||||
|
|
||||||
return integrateGroupChanges({
|
|
||||||
changes,
|
|
||||||
group,
|
|
||||||
newRevision,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function integrateGroupChanges({
|
async function integrateGroupChanges({
|
||||||
group,
|
group,
|
||||||
newRevision,
|
newRevision,
|
||||||
|
@ -3840,7 +3929,10 @@ async function integrateGroupChange({
|
||||||
}
|
}
|
||||||
if (groupChangeActions.version === group.revision) {
|
if (groupChangeActions.version === group.revision) {
|
||||||
isSameVersion = true;
|
isSameVersion = true;
|
||||||
} else if (groupChangeActions.version > group.revision + 1) {
|
} else if (
|
||||||
|
groupChangeActions.version > group.revision + 1 ||
|
||||||
|
(!isNumber(group.revision) && groupChangeActions.version > 0)
|
||||||
|
) {
|
||||||
isMoreThanOneVersionUp = true;
|
isMoreThanOneVersionUp = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3956,64 +4048,6 @@ async function integrateGroupChange({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCurrentGroupState({
|
|
||||||
authCredentialBase64,
|
|
||||||
dropInitialJoinMessage,
|
|
||||||
group,
|
|
||||||
serverPublicParamsBase64,
|
|
||||||
}: {
|
|
||||||
authCredentialBase64: string;
|
|
||||||
dropInitialJoinMessage?: boolean;
|
|
||||||
group: ConversationAttributesType;
|
|
||||||
serverPublicParamsBase64: string;
|
|
||||||
}): Promise<UpdatesResultType> {
|
|
||||||
const logId = idForLogging(group.groupId);
|
|
||||||
const sender = window.textsecure.messaging;
|
|
||||||
if (!sender) {
|
|
||||||
throw new Error('textsecure.messaging is not available!');
|
|
||||||
}
|
|
||||||
if (!group.secretParams) {
|
|
||||||
throw new Error('getCurrentGroupState: group was missing secretParams!');
|
|
||||||
}
|
|
||||||
if (!group.publicParams) {
|
|
||||||
throw new Error('getCurrentGroupState: group was missing publicParams!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = getGroupCredentials({
|
|
||||||
authCredentialBase64,
|
|
||||||
groupPublicParamsBase64: group.publicParams,
|
|
||||||
groupSecretParamsBase64: group.secretParams,
|
|
||||||
serverPublicParamsBase64,
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupState = await sender.getGroup(options);
|
|
||||||
const decryptedGroupState = decryptGroupState(
|
|
||||||
groupState,
|
|
||||||
group.secretParams,
|
|
||||||
logId
|
|
||||||
);
|
|
||||||
|
|
||||||
const oldVersion = group.revision;
|
|
||||||
const newVersion = decryptedGroupState.version;
|
|
||||||
log.info(
|
|
||||||
`getCurrentGroupState/${logId}: Applying full group state, from version ${oldVersion} to ${newVersion}.`
|
|
||||||
);
|
|
||||||
const { newAttributes, newProfileKeys } = await applyGroupState({
|
|
||||||
group,
|
|
||||||
groupState: decryptedGroupState,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
newAttributes,
|
|
||||||
groupChangeMessages: extractDiffs({
|
|
||||||
old: group,
|
|
||||||
current: newAttributes,
|
|
||||||
dropInitialJoinMessage,
|
|
||||||
}),
|
|
||||||
members: profileKeysToMembers(newProfileKeys),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractDiffs({
|
function extractDiffs({
|
||||||
current,
|
current,
|
||||||
dropInitialJoinMessage,
|
dropInitialJoinMessage,
|
||||||
|
@ -4032,6 +4066,7 @@ function extractDiffs({
|
||||||
|
|
||||||
let areWeInGroup = false;
|
let areWeInGroup = false;
|
||||||
let areWeInvitedToGroup = false;
|
let areWeInvitedToGroup = false;
|
||||||
|
let areWePendingApproval = false;
|
||||||
let whoInvitedUsUserId = null;
|
let whoInvitedUsUserId = null;
|
||||||
|
|
||||||
// access control
|
// access control
|
||||||
|
@ -4286,6 +4321,10 @@ function extractDiffs({
|
||||||
const { uuid } = currentPendingAdminAprovalMember;
|
const { uuid } = currentPendingAdminAprovalMember;
|
||||||
const oldPendingMember = oldPendingAdminApprovalLookup.get(uuid);
|
const oldPendingMember = oldPendingAdminApprovalLookup.get(uuid);
|
||||||
|
|
||||||
|
if (uuid === ourUuid) {
|
||||||
|
areWePendingApproval = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!oldPendingMember) {
|
if (!oldPendingMember) {
|
||||||
details.push({
|
details.push({
|
||||||
type: 'admin-approval-add-one',
|
type: 'admin-approval-add-one',
|
||||||
|
@ -4349,10 +4388,29 @@ function extractDiffs({
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
seenStatus: isFromUs ? SeenStatus.Seen : SeenStatus.Unseen,
|
seenStatus: isFromUs ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||||
};
|
};
|
||||||
|
} else if (firstUpdate && areWePendingApproval) {
|
||||||
|
message = {
|
||||||
|
...generateBasicMessage(),
|
||||||
|
type: 'group-v2-change',
|
||||||
|
groupV2Change: {
|
||||||
|
from: ourUuid,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-add-one',
|
||||||
|
uuid: ourUuid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
} else if (firstUpdate && dropInitialJoinMessage) {
|
} else if (firstUpdate && dropInitialJoinMessage) {
|
||||||
// None of the rest of the messages should be added if dropInitialJoinMessage = true
|
// None of the rest of the messages should be added if dropInitialJoinMessage = true
|
||||||
message = undefined;
|
message = undefined;
|
||||||
} else if (firstUpdate && sourceUuid && sourceUuid === ourUuid) {
|
} else if (
|
||||||
|
firstUpdate &&
|
||||||
|
current.revision === 0 &&
|
||||||
|
sourceUuid &&
|
||||||
|
sourceUuid === ourUuid
|
||||||
|
) {
|
||||||
message = {
|
message = {
|
||||||
...generateBasicMessage(),
|
...generateBasicMessage(),
|
||||||
type: 'group-v2-change',
|
type: 'group-v2-change',
|
||||||
|
@ -4383,7 +4441,7 @@ function extractDiffs({
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
seenStatus: isFromUs ? SeenStatus.Seen : SeenStatus.Unseen,
|
seenStatus: isFromUs ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||||
};
|
};
|
||||||
} else if (firstUpdate) {
|
} else if (firstUpdate && current.revision === 0) {
|
||||||
message = {
|
message = {
|
||||||
...generateBasicMessage(),
|
...generateBasicMessage(),
|
||||||
type: 'group-v2-change',
|
type: 'group-v2-change',
|
||||||
|
@ -4944,6 +5002,9 @@ async function applyGroupChange({
|
||||||
if (ourUuid) {
|
if (ourUuid) {
|
||||||
result.left = !members[ourUuid];
|
result.left = !members[ourUuid];
|
||||||
}
|
}
|
||||||
|
if (result.left) {
|
||||||
|
result.addedBy = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Go from lookups back to arrays
|
// Go from lookups back to arrays
|
||||||
result.membersV2 = values(members);
|
result.membersV2 = values(members);
|
||||||
|
@ -5092,6 +5153,9 @@ async function applyGroupState({
|
||||||
const ourUuid = window.storage.user.getCheckedUuid().toString();
|
const ourUuid = window.storage.user.getCheckedUuid().toString();
|
||||||
|
|
||||||
// members
|
// members
|
||||||
|
const wasPreviouslyAMember = (result.membersV2 || []).some(
|
||||||
|
item => item.uuid !== ourUuid
|
||||||
|
);
|
||||||
if (groupState.members) {
|
if (groupState.members) {
|
||||||
result.membersV2 = groupState.members.map(member => {
|
result.membersV2 = groupState.members.map(member => {
|
||||||
if (member.userId === ourUuid) {
|
if (member.userId === ourUuid) {
|
||||||
|
@ -5100,7 +5164,9 @@ async function applyGroupState({
|
||||||
// Capture who added us if we were previously not in group
|
// Capture who added us if we were previously not in group
|
||||||
if (
|
if (
|
||||||
sourceUuid &&
|
sourceUuid &&
|
||||||
(result.membersV2 || []).every(item => item.uuid !== ourUuid)
|
!wasPreviouslyAMember &&
|
||||||
|
isNumber(member.joinedAtVersion) &&
|
||||||
|
member.joinedAtVersion === version
|
||||||
) {
|
) {
|
||||||
result.addedBy = sourceUuid;
|
result.addedBy = sourceUuid;
|
||||||
}
|
}
|
||||||
|
@ -5207,6 +5273,10 @@ async function applyGroupState({
|
||||||
// membersBanned
|
// membersBanned
|
||||||
result.bannedMembersV2 = groupState.membersBanned;
|
result.bannedMembersV2 = groupState.membersBanned;
|
||||||
|
|
||||||
|
if (result.left) {
|
||||||
|
result.addedBy = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newAttributes: result,
|
newAttributes: result,
|
||||||
newProfileKeys,
|
newProfileKeys,
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember';
|
||||||
import { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
|
import { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
|
||||||
import { HTTPError } from '../textsecure/Errors';
|
import { HTTPError } from '../textsecure/Errors';
|
||||||
import { isAccessControlEnabled } from './util';
|
import { isAccessControlEnabled } from './util';
|
||||||
|
import { sleep } from '../util/sleep';
|
||||||
|
|
||||||
export async function joinViaLink(hash: string): Promise<void> {
|
export async function joinViaLink(hash: string): Promise<void> {
|
||||||
let inviteLinkPassword: string;
|
let inviteLinkPassword: string;
|
||||||
|
@ -153,6 +154,15 @@ export async function joinViaLink(hash: string): Promise<void> {
|
||||||
log.warn(
|
log.warn(
|
||||||
`joinViaLink/${logId}: Already awaiting approval, opening conversation`
|
`joinViaLink/${logId}: Already awaiting approval, opening conversation`
|
||||||
);
|
);
|
||||||
|
const timestamp = existingConversation.get('timestamp') || Date.now();
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
const active_at = existingConversation.get('active_at') || Date.now();
|
||||||
|
existingConversation.set({ active_at, timestamp });
|
||||||
|
window.Signal.Data.updateConversation(existingConversation.attributes);
|
||||||
|
|
||||||
|
// We're waiting for the left pane to re-sort before we navigate to that conversation
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
window.reduxActions.conversations.openConversationInternal({
|
window.reduxActions.conversations.openConversationInternal({
|
||||||
conversationId: existingConversation.id,
|
conversationId: existingConversation.id,
|
||||||
});
|
});
|
||||||
|
@ -257,6 +267,9 @@ export async function joinViaLink(hash: string): Promise<void> {
|
||||||
// This will cause this conversation to be deleted at next startup
|
// This will cause this conversation to be deleted at next startup
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
|
|
||||||
|
active_at: Date.now(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
|
||||||
groupVersion: 2,
|
groupVersion: 2,
|
||||||
masterKey,
|
masterKey,
|
||||||
secretParams,
|
secretParams,
|
||||||
|
@ -272,6 +285,7 @@ export async function joinViaLink(hash: string): Promise<void> {
|
||||||
path: localAvatar.path,
|
path: localAvatar.path,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
description: groupDescription,
|
||||||
groupInviteLinkPassword: inviteLinkPassword,
|
groupInviteLinkPassword: inviteLinkPassword,
|
||||||
name: title,
|
name: title,
|
||||||
temporaryMemberCount: memberCount,
|
temporaryMemberCount: memberCount,
|
||||||
|
@ -281,7 +295,13 @@ export async function joinViaLink(hash: string): Promise<void> {
|
||||||
} else {
|
} else {
|
||||||
// Ensure the group maintains the title and avatar you saw when attempting
|
// Ensure the group maintains the title and avatar you saw when attempting
|
||||||
// to join it.
|
// to join it.
|
||||||
|
const timestamp =
|
||||||
|
targetConversation.get('timestamp') || Date.now();
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
const active_at =
|
||||||
|
targetConversation.get('active_at') || Date.now();
|
||||||
targetConversation.set({
|
targetConversation.set({
|
||||||
|
active_at,
|
||||||
avatar:
|
avatar:
|
||||||
localAvatar && localAvatar.path && result.avatar
|
localAvatar && localAvatar.path && result.avatar
|
||||||
? {
|
? {
|
||||||
|
@ -289,9 +309,12 @@ export async function joinViaLink(hash: string): Promise<void> {
|
||||||
path: localAvatar.path,
|
path: localAvatar.path,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
description: groupDescription,
|
||||||
groupInviteLinkPassword: inviteLinkPassword,
|
groupInviteLinkPassword: inviteLinkPassword,
|
||||||
name: title,
|
name: title,
|
||||||
|
revision: result.version,
|
||||||
temporaryMemberCount: memberCount,
|
temporaryMemberCount: memberCount,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
window.Signal.Data.updateConversation(
|
window.Signal.Data.updateConversation(
|
||||||
targetConversation.attributes
|
targetConversation.attributes
|
||||||
|
|
|
@ -2277,7 +2277,9 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
const inviteLinkPassword = this.get('groupInviteLinkPassword');
|
const inviteLinkPassword = this.get('groupInviteLinkPassword');
|
||||||
if (!inviteLinkPassword) {
|
if (!inviteLinkPassword) {
|
||||||
throw new Error('Missing groupInviteLinkPassword!');
|
log.warn(
|
||||||
|
`cancelJoinRequest/${this.idForLogging()}: We don't have an inviteLinkPassword!`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.modifyGroupV2({
|
await this.modifyGroupV2({
|
||||||
|
|
|
@ -2019,9 +2019,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const conversation = window.ConversationController.get(conversationId)!;
|
const conversation = window.ConversationController.get(conversationId)!;
|
||||||
|
const idLog = conversation.idForLogging();
|
||||||
await conversation.queueJob('handleDataMessage', async () => {
|
await conversation.queueJob('handleDataMessage', async () => {
|
||||||
log.info(
|
log.info(
|
||||||
`Starting handleDataMessage for message ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
|
`handleDataMessage/${idLog}: processsing message ${message.idForLogging()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -2031,7 +2032,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
log.info(
|
log.info(
|
||||||
'handleDataMessage: dropping story from !accepted',
|
`handleDataMessage/${idLog}: dropping story from !accepted`,
|
||||||
this.getSenderIdentifier()
|
this.getSenderIdentifier()
|
||||||
);
|
);
|
||||||
confirm();
|
confirm();
|
||||||
|
@ -2043,10 +2044,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
this.getSenderIdentifier()
|
this.getSenderIdentifier()
|
||||||
);
|
);
|
||||||
if (inMemoryMessage) {
|
if (inMemoryMessage) {
|
||||||
log.info('handleDataMessage: cache hit', this.getSenderIdentifier());
|
log.info(
|
||||||
|
`handleDataMessage/${idLog}: cache hit`,
|
||||||
|
this.getSenderIdentifier()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
log.info(
|
log.info(
|
||||||
'handleDataMessage: duplicate check db lookup needed',
|
`handleDataMessage/${idLog}: duplicate check db lookup needed`,
|
||||||
this.getSenderIdentifier()
|
this.getSenderIdentifier()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2055,14 +2059,17 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const isUpdate = Boolean(data && data.isRecipientUpdate);
|
const isUpdate = Boolean(data && data.isRecipientUpdate);
|
||||||
|
|
||||||
if (existingMessage && type === 'incoming') {
|
if (existingMessage && type === 'incoming') {
|
||||||
log.warn('Received duplicate message', this.idForLogging());
|
log.warn(
|
||||||
|
`handleDataMessage/${idLog}: Received duplicate message`,
|
||||||
|
this.idForLogging()
|
||||||
|
);
|
||||||
confirm();
|
confirm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === 'outgoing') {
|
if (type === 'outgoing') {
|
||||||
if (isUpdate && existingMessage) {
|
if (isUpdate && existingMessage) {
|
||||||
log.info(
|
log.info(
|
||||||
`handleDataMessage: Updating message ${message.idForLogging()} with received transcript`
|
`handleDataMessage/${idLog}: Updating message ${message.idForLogging()} with received transcript`
|
||||||
);
|
);
|
||||||
|
|
||||||
const toUpdate = window.MessageController.register(
|
const toUpdate = window.MessageController.register(
|
||||||
|
@ -2139,7 +2146,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`handleDataMessage: Received update transcript, but no existing entry for message ${message.idForLogging()}. Dropping.`
|
`handleDataMessage/${idLog}: Received update transcript, but no existing entry for message ${message.idForLogging()}. Dropping.`
|
||||||
);
|
);
|
||||||
|
|
||||||
confirm();
|
confirm();
|
||||||
|
@ -2147,7 +2154,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
if (existingMessage) {
|
if (existingMessage) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`handleDataMessage: Received duplicate transcript for message ${message.idForLogging()}, but it was not an update transcript. Dropping.`
|
`handleDataMessage/${idLog}: Received duplicate transcript for message ${message.idForLogging()}, but it was not an update transcript. Dropping.`
|
||||||
);
|
);
|
||||||
|
|
||||||
confirm();
|
confirm();
|
||||||
|
@ -2212,7 +2219,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error && error.stack ? error.stack : error;
|
const errorText = error && error.stack ? error.stack : error;
|
||||||
log.error(
|
log.error(
|
||||||
`handleDataMessage: Failed to process group update for ${conversation.idForLogging()} as part of message ${message.idForLogging()}: ${errorText}`
|
`handleDataMessage/${idLog}: Failed to process group update as part of message ${message.idForLogging()}: ${errorText}`
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -2233,6 +2240,19 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
initialMessage.group &&
|
initialMessage.group &&
|
||||||
initialMessage.group.type !== Proto.GroupContext.Type.DELIVER;
|
initialMessage.group.type !== Proto.GroupContext.Type.DELIVER;
|
||||||
|
|
||||||
|
// Drop if from blocked user. Only GroupV2 messages should need to be dropped here.
|
||||||
|
const isBlocked =
|
||||||
|
(source && window.storage.blocked.isBlocked(source)) ||
|
||||||
|
(sourceUuid && window.storage.blocked.isUuidBlocked(sourceUuid));
|
||||||
|
if (isBlocked) {
|
||||||
|
log.info(
|
||||||
|
`handleDataMessage/${idLog}: Dropping message from blocked sender. hasGroupV2Prop: ${hasGroupV2Prop}`
|
||||||
|
);
|
||||||
|
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Drop an incoming GroupV2 message if we or the sender are not part of the group
|
// Drop an incoming GroupV2 message if we or the sender are not part of the group
|
||||||
// after applying the message's associated group changes.
|
// after applying the message's associated group changes.
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -744,7 +744,7 @@ export async function mergeGroupV2Record(
|
||||||
conversation,
|
conversation,
|
||||||
dropInitialJoinMessage,
|
dropInitialJoinMessage,
|
||||||
},
|
},
|
||||||
{ viaSync: true }
|
{ viaFirstStorageSync: isFirstSync }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1238,9 +1238,13 @@ export default class MessageReceiver
|
||||||
|
|
||||||
// Note: we need to process this as part of decryption, because we might need this
|
// Note: we need to process this as part of decryption, because we might need this
|
||||||
// sender key to decrypt the next message in the queue!
|
// sender key to decrypt the next message in the queue!
|
||||||
|
let isGroupV2 = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = Proto.Content.decode(plaintext);
|
const content = Proto.Content.decode(plaintext);
|
||||||
|
|
||||||
|
isGroupV2 = Boolean(content.dataMessage?.groupV2);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
content.senderKeyDistributionMessage &&
|
content.senderKeyDistributionMessage &&
|
||||||
Bytes.isNotEmpty(content.senderKeyDistributionMessage)
|
Bytes.isNotEmpty(content.senderKeyDistributionMessage)
|
||||||
|
@ -1258,12 +1262,14 @@ export default class MessageReceiver
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We want to process GroupV2 updates, even from blocked users. We'll drop them later.
|
||||||
if (
|
if (
|
||||||
(envelope.source && this.isBlocked(envelope.source)) ||
|
!isGroupV2 &&
|
||||||
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid))
|
((envelope.source && this.isBlocked(envelope.source)) ||
|
||||||
|
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid)))
|
||||||
) {
|
) {
|
||||||
log.info(
|
log.info(
|
||||||
'MessageReceiver.decryptEnvelope: Dropping message from blocked sender'
|
'MessageReceiver.decryptEnvelope: Dropping non-GV2 message from blocked sender'
|
||||||
);
|
);
|
||||||
return { plaintext: undefined, envelope };
|
return { plaintext: undefined, envelope };
|
||||||
}
|
}
|
||||||
|
|
|
@ -2244,7 +2244,7 @@ export default class MessageSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupFromLink(
|
async getGroupFromLink(
|
||||||
groupInviteLink: string,
|
groupInviteLink: string | undefined,
|
||||||
auth: Readonly<GroupCredentialsType>
|
auth: Readonly<GroupCredentialsType>
|
||||||
): Promise<Proto.GroupJoinInfo> {
|
): Promise<Proto.GroupJoinInfo> {
|
||||||
return this.server.getGroupFromLink(groupInviteLink, auth);
|
return this.server.getGroupFromLink(groupInviteLink, auth);
|
||||||
|
|
|
@ -542,7 +542,7 @@ const URL_CALLS = {
|
||||||
groupLog: 'v1/groups/logs',
|
groupLog: 'v1/groups/logs',
|
||||||
groupJoinedAtVersion: 'v1/groups/joined_at_version',
|
groupJoinedAtVersion: 'v1/groups/joined_at_version',
|
||||||
groups: 'v1/groups',
|
groups: 'v1/groups',
|
||||||
groupsViaLink: 'v1/groups/join',
|
groupsViaLink: 'v1/groups/join/',
|
||||||
groupToken: 'v1/groups/token',
|
groupToken: 'v1/groups/token',
|
||||||
keys: 'v2/keys',
|
keys: 'v2/keys',
|
||||||
messages: 'v1/messages',
|
messages: 'v1/messages',
|
||||||
|
@ -830,7 +830,7 @@ export type WebAPIType = {
|
||||||
getHasSubscription: (subscriberId: Uint8Array) => Promise<boolean>;
|
getHasSubscription: (subscriberId: Uint8Array) => Promise<boolean>;
|
||||||
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
|
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
|
||||||
getGroupFromLink: (
|
getGroupFromLink: (
|
||||||
inviteLinkPassword: string,
|
inviteLinkPassword: string | undefined,
|
||||||
auth: GroupCredentialsType
|
auth: GroupCredentialsType
|
||||||
) => Promise<Proto.GroupJoinInfo>;
|
) => Promise<Proto.GroupJoinInfo>;
|
||||||
getGroupAvatar: (key: string) => Promise<Uint8Array>;
|
getGroupAvatar: (key: string) => Promise<Uint8Array>;
|
||||||
|
@ -2595,14 +2595,16 @@ export function initialize({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getGroupFromLink(
|
async function getGroupFromLink(
|
||||||
inviteLinkPassword: string,
|
inviteLinkPassword: string | undefined,
|
||||||
auth: GroupCredentialsType
|
auth: GroupCredentialsType
|
||||||
): Promise<Proto.GroupJoinInfo> {
|
): Promise<Proto.GroupJoinInfo> {
|
||||||
const basicAuth = generateGroupAuth(
|
const basicAuth = generateGroupAuth(
|
||||||
auth.groupPublicParamsHex,
|
auth.groupPublicParamsHex,
|
||||||
auth.authCredentialPresentationHex
|
auth.authCredentialPresentationHex
|
||||||
);
|
);
|
||||||
const safeInviteLinkPassword = toWebSafeBase64(inviteLinkPassword);
|
const safeInviteLinkPassword = inviteLinkPassword
|
||||||
|
? toWebSafeBase64(inviteLinkPassword)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const response = await _ajax({
|
const response = await _ajax({
|
||||||
basicAuth,
|
basicAuth,
|
||||||
|
@ -2611,7 +2613,9 @@ export function initialize({
|
||||||
host: storageUrl,
|
host: storageUrl,
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
responseType: 'bytes',
|
responseType: 'bytes',
|
||||||
urlParameters: `/${safeInviteLinkPassword}`,
|
urlParameters: safeInviteLinkPassword
|
||||||
|
? `${safeInviteLinkPassword}`
|
||||||
|
: undefined,
|
||||||
redactUrl: _createRedactor(safeInviteLinkPassword),
|
redactUrl: _createRedactor(safeInviteLinkPassword),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
1
ts/window.d.ts
vendored
1
ts/window.d.ts
vendored
|
@ -473,6 +473,7 @@ declare global {
|
||||||
GV2_ENABLE_SINGLE_CHANGE_PROCESSING: boolean;
|
GV2_ENABLE_SINGLE_CHANGE_PROCESSING: boolean;
|
||||||
GV2_ENABLE_CHANGE_PROCESSING: boolean;
|
GV2_ENABLE_CHANGE_PROCESSING: boolean;
|
||||||
GV2_ENABLE_STATE_PROCESSING: boolean;
|
GV2_ENABLE_STATE_PROCESSING: boolean;
|
||||||
|
GV2_ENABLE_PRE_JOIN_FETCH: boolean;
|
||||||
GV2_MIGRATION_DISABLE_ADD: boolean;
|
GV2_MIGRATION_DISABLE_ADD: boolean;
|
||||||
GV2_MIGRATION_DISABLE_INVITE: boolean;
|
GV2_MIGRATION_DISABLE_INVITE: boolean;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue