Handle Safety Number changes while sending a story
This commit is contained in:
parent
d036803df9
commit
0fb45f045d
13 changed files with 392 additions and 44 deletions
|
@ -17,7 +17,7 @@ import {
|
||||||
} from '@signalapp/libsignal-client';
|
} from '@signalapp/libsignal-client';
|
||||||
|
|
||||||
import * as Bytes from './Bytes';
|
import * as Bytes from './Bytes';
|
||||||
import { constantTimeEqual } from './Crypto';
|
import { constantTimeEqual, sha256 } from './Crypto';
|
||||||
import { assert, strictAssert } from './util/assert';
|
import { assert, strictAssert } from './util/assert';
|
||||||
import { isNotNil } from './util/isNotNil';
|
import { isNotNil } from './util/isNotNil';
|
||||||
import { Zone } from './util/Zone';
|
import { Zone } from './util/Zone';
|
||||||
|
@ -1565,6 +1565,23 @@ export class SignalProtocolStore extends EventsMixin {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFingerprint(uuid: UUID): Promise<string | undefined> {
|
||||||
|
if (uuid === null || uuid === undefined) {
|
||||||
|
throw new Error('loadIdentityKey: uuid was undefined/null');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pubKey = await this.loadIdentityKey(uuid);
|
||||||
|
|
||||||
|
if (!pubKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = sha256(pubKey);
|
||||||
|
const fingerprint = hash.slice(0, 4);
|
||||||
|
|
||||||
|
return Bytes.toBase64(fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
private async _saveIdentityKey(data: IdentityKeyType): Promise<void> {
|
private async _saveIdentityKey(data: IdentityKeyType): Promise<void> {
|
||||||
if (!this.identityKeys) {
|
if (!this.identityKeys) {
|
||||||
throw new Error('_saveIdentityKey: this.identityKeys not yet cached!');
|
throw new Error('_saveIdentityKey: this.identityKeys not yet cached!');
|
||||||
|
@ -1831,7 +1848,7 @@ export class SignalProtocolStore extends EventsMixin {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isUntrusted(uuid: UUID): boolean {
|
isUntrusted(uuid: UUID, timestampThreshold = TIMESTAMP_THRESHOLD): boolean {
|
||||||
if (uuid === null || uuid === undefined) {
|
if (uuid === null || uuid === undefined) {
|
||||||
throw new Error('isUntrusted: uuid was undefined/null');
|
throw new Error('isUntrusted: uuid was undefined/null');
|
||||||
}
|
}
|
||||||
|
@ -1842,7 +1859,7 @@ export class SignalProtocolStore extends EventsMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isMoreRecentThan(identityRecord.timestamp, TIMESTAMP_THRESHOLD) &&
|
isMoreRecentThan(identityRecord.timestamp, timestampThreshold) &&
|
||||||
!identityRecord.nonblockingApproval &&
|
!identityRecord.nonblockingApproval &&
|
||||||
!identityRecord.firstUse
|
!identityRecord.firstUse
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||||
export enum SafetyNumberChangeSource {
|
export enum SafetyNumberChangeSource {
|
||||||
Calling = 'Calling',
|
Calling = 'Calling',
|
||||||
MessageSend = 'MessageSend',
|
MessageSend = 'MessageSend',
|
||||||
|
Story = 'Story',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SafetyNumberProps = {
|
export type SafetyNumberProps = {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories';
|
||||||
import { Modal } from './Modal';
|
import { Modal } from './Modal';
|
||||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
|
import { isNotNil } from '../util/isNotNil';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
candidateConversations: Array<ConversationType>;
|
candidateConversations: Array<ConversationType>;
|
||||||
|
@ -36,6 +37,7 @@ export type PropsType = {
|
||||||
name: string,
|
name: string,
|
||||||
viewerUuids: Array<UUIDStringType>
|
viewerUuids: Array<UUIDStringType>
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
onSelectedStoryList: (memberUuids: Array<string>) => unknown;
|
||||||
onSend: (
|
onSend: (
|
||||||
listIds: Array<UUIDStringType>,
|
listIds: Array<UUIDStringType>,
|
||||||
conversationIds: Array<string>
|
conversationIds: Array<string>
|
||||||
|
@ -56,6 +58,21 @@ const Page = {
|
||||||
|
|
||||||
type PageType = SendStoryPage | StoriesSettingsPage;
|
type PageType = SendStoryPage | StoriesSettingsPage;
|
||||||
|
|
||||||
|
function getListMemberUuids(
|
||||||
|
list: StoryDistributionListDataType,
|
||||||
|
signalConnections: Array<ConversationType>
|
||||||
|
): Array<string> {
|
||||||
|
if (list.id === MY_STORIES_ID && list.isBlockList) {
|
||||||
|
const excludeUuids = new Set<string>(list.memberUuids);
|
||||||
|
return signalConnections
|
||||||
|
.map(conversation => conversation.uuid)
|
||||||
|
.filter(isNotNil)
|
||||||
|
.filter(uuid => !excludeUuids.has(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.memberUuids;
|
||||||
|
}
|
||||||
|
|
||||||
function getListViewers(
|
function getListViewers(
|
||||||
list: StoryDistributionListDataType,
|
list: StoryDistributionListDataType,
|
||||||
i18n: LocalizerType,
|
i18n: LocalizerType,
|
||||||
|
@ -85,6 +102,7 @@ export const SendStoryModal = ({
|
||||||
onClose,
|
onClose,
|
||||||
onDistributionListCreated,
|
onDistributionListCreated,
|
||||||
onSend,
|
onSend,
|
||||||
|
onSelectedStoryList,
|
||||||
signalConnections,
|
signalConnections,
|
||||||
tagGroupsAsNewGroupStory,
|
tagGroupsAsNewGroupStory,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
|
@ -300,6 +318,11 @@ export const SendStoryModal = ({
|
||||||
}
|
}
|
||||||
return new Set([...listIds]);
|
return new Set([...listIds]);
|
||||||
});
|
});
|
||||||
|
if (value) {
|
||||||
|
onSelectedStoryList(
|
||||||
|
getListMemberUuids(list, signalConnections)
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ id, checkboxNode }) => (
|
{({ id, checkboxNode }) => (
|
||||||
|
@ -352,6 +375,10 @@ export const SendStoryModal = ({
|
||||||
moduleClassName="SendStoryModal__distribution-list"
|
moduleClassName="SendStoryModal__distribution-list"
|
||||||
name="SendStoryModal__distribution-list"
|
name="SendStoryModal__distribution-list"
|
||||||
onChange={(value: boolean) => {
|
onChange={(value: boolean) => {
|
||||||
|
if (!group.memberships) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedGroupIds(groupIds => {
|
setSelectedGroupIds(groupIds => {
|
||||||
if (value) {
|
if (value) {
|
||||||
groupIds.add(group.id);
|
groupIds.add(group.id);
|
||||||
|
@ -360,6 +387,9 @@ export const SendStoryModal = ({
|
||||||
}
|
}
|
||||||
return new Set([...groupIds]);
|
return new Set([...groupIds]);
|
||||||
});
|
});
|
||||||
|
if (value) {
|
||||||
|
onSelectedStoryList(group.memberships.map(({ uuid }) => uuid));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ id, checkboxNode }) => (
|
{({ id, checkboxNode }) => (
|
||||||
|
|
|
@ -43,6 +43,7 @@ export type PropsType = {
|
||||||
name: string,
|
name: string,
|
||||||
viewerUuids: Array<UUIDStringType>
|
viewerUuids: Array<UUIDStringType>
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
onSelectedStoryList: (memberUuids: Array<string>) => unknown;
|
||||||
onSend: (
|
onSend: (
|
||||||
listIds: Array<UUIDStringType>,
|
listIds: Array<UUIDStringType>,
|
||||||
conversationIds: Array<string>,
|
conversationIds: Array<string>,
|
||||||
|
@ -51,6 +52,7 @@ export type PropsType = {
|
||||||
processAttachment: (
|
processAttachment: (
|
||||||
file: File
|
file: File
|
||||||
) => Promise<void | InMemoryAttachmentDraftType>;
|
) => Promise<void | InMemoryAttachmentDraftType>;
|
||||||
|
sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown;
|
||||||
signalConnections: Array<ConversationType>;
|
signalConnections: Array<ConversationType>;
|
||||||
tagGroupsAsNewGroupStory: (cids: Array<string>) => unknown;
|
tagGroupsAsNewGroupStory: (cids: Array<string>) => unknown;
|
||||||
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'>;
|
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'>;
|
||||||
|
@ -69,9 +71,11 @@ export const StoryCreator = ({
|
||||||
me,
|
me,
|
||||||
onClose,
|
onClose,
|
||||||
onDistributionListCreated,
|
onDistributionListCreated,
|
||||||
|
onSelectedStoryList,
|
||||||
onSend,
|
onSend,
|
||||||
processAttachment,
|
processAttachment,
|
||||||
recentStickers,
|
recentStickers,
|
||||||
|
sendStoryModalOpenStateChanged,
|
||||||
signalConnections,
|
signalConnections,
|
||||||
tagGroupsAsNewGroupStory,
|
tagGroupsAsNewGroupStory,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
|
@ -112,6 +116,10 @@ export const StoryCreator = ({
|
||||||
};
|
};
|
||||||
}, [file, processAttachment]);
|
}, [file, processAttachment]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sendStoryModalOpenStateChanged(Boolean(draftAttachment));
|
||||||
|
}, [draftAttachment, sendStoryModalOpenStateChanged]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{draftAttachment && (
|
{draftAttachment && (
|
||||||
|
@ -125,6 +133,7 @@ export const StoryCreator = ({
|
||||||
me={me}
|
me={me}
|
||||||
onClose={() => setDraftAttachment(undefined)}
|
onClose={() => setDraftAttachment(undefined)}
|
||||||
onDistributionListCreated={onDistributionListCreated}
|
onDistributionListCreated={onDistributionListCreated}
|
||||||
|
onSelectedStoryList={onSelectedStoryList}
|
||||||
onSend={(listIds, groupIds) => {
|
onSend={(listIds, groupIds) => {
|
||||||
onSend(listIds, groupIds, draftAttachment);
|
onSend(listIds, groupIds, draftAttachment);
|
||||||
setDraftAttachment(undefined);
|
setDraftAttachment(undefined);
|
||||||
|
|
|
@ -2822,19 +2822,22 @@ export class ConversationModel extends window.Backbone
|
||||||
return window.textsecure.storage.protocol.setApproval(uuid, true);
|
return window.textsecure.storage.protocol.setApproval(uuid, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
safeIsUntrusted(): boolean {
|
safeIsUntrusted(timestampThreshold?: number): boolean {
|
||||||
try {
|
try {
|
||||||
const uuid = this.getUuid();
|
const uuid = this.getUuid();
|
||||||
strictAssert(uuid, `No uuid for conversation: ${this.id}`);
|
strictAssert(uuid, `No uuid for conversation: ${this.id}`);
|
||||||
return window.textsecure.storage.protocol.isUntrusted(uuid);
|
return window.textsecure.storage.protocol.isUntrusted(
|
||||||
|
uuid,
|
||||||
|
timestampThreshold
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isUntrusted(): boolean {
|
isUntrusted(timestampThreshold?: number): boolean {
|
||||||
if (isDirectConversation(this.attributes)) {
|
if (isDirectConversation(this.attributes)) {
|
||||||
return this.safeIsUntrusted();
|
return this.safeIsUntrusted(timestampThreshold);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
if (!this.contactCollection!.length) {
|
if (!this.contactCollection!.length) {
|
||||||
|
@ -2846,13 +2849,13 @@ export class ConversationModel extends window.Backbone
|
||||||
if (isMe(contact.attributes)) {
|
if (isMe(contact.attributes)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return contact.safeIsUntrusted();
|
return contact.safeIsUntrusted(timestampThreshold);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getUntrusted(): Array<ConversationModel> {
|
getUntrusted(timestampThreshold?: number): Array<ConversationModel> {
|
||||||
if (isDirectConversation(this.attributes)) {
|
if (isDirectConversation(this.attributes)) {
|
||||||
if (this.isUntrusted()) {
|
if (this.isUntrusted(timestampThreshold)) {
|
||||||
return [this];
|
return [this];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
@ -2863,7 +2866,7 @@ export class ConversationModel extends window.Backbone
|
||||||
if (isMe(contact.attributes)) {
|
if (isMe(contact.attributes)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return contact.isUntrusted();
|
return contact.isUntrusted(timestampThreshold);
|
||||||
}) || []
|
}) || []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
GetProfileOptionsType,
|
GetProfileOptionsType,
|
||||||
GetProfileUnauthOptionsType,
|
GetProfileUnauthOptionsType,
|
||||||
} from '../textsecure/WebAPI';
|
} from '../textsecure/WebAPI';
|
||||||
|
import type { UUID } from '../types/UUID';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
|
@ -359,20 +360,7 @@ async function doGetProfile(c: ConversationModel): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.identityKey) {
|
if (profile.identityKey) {
|
||||||
const identityKey = Bytes.fromBase64(profile.identityKey);
|
await updateIdentityKey(profile.identityKey, uuid);
|
||||||
const changed = await window.textsecure.storage.protocol.saveIdentity(
|
|
||||||
new Address(uuid, 1),
|
|
||||||
identityKey,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
if (changed) {
|
|
||||||
// save identity will close all sessions except for .1, so we
|
|
||||||
// must close that one manually.
|
|
||||||
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
|
||||||
await window.textsecure.storage.protocol.archiveSession(
|
|
||||||
new QualifiedAddress(ourUuid, new Address(uuid, 1))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update accessKey to prevent race conditions. Since we run asynchronous
|
// Update accessKey to prevent race conditions. Since we run asynchronous
|
||||||
|
@ -655,3 +643,27 @@ async function maybeGetPNICredential(
|
||||||
|
|
||||||
log.info('maybeGetPNICredential: updated PNI credential');
|
log.info('maybeGetPNICredential: updated PNI credential');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateIdentityKey(
|
||||||
|
identityKey: string,
|
||||||
|
uuid: UUID
|
||||||
|
): Promise<void> {
|
||||||
|
if (!identityKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityKeyBytes = Bytes.fromBase64(identityKey);
|
||||||
|
const changed = await window.textsecure.storage.protocol.saveIdentity(
|
||||||
|
new Address(uuid, 1),
|
||||||
|
identityKeyBytes,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
if (changed) {
|
||||||
|
// save identity will close all sessions except for .1, so we
|
||||||
|
// must close that one manually.
|
||||||
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||||
|
await window.textsecure.storage.protocol.archiveSession(
|
||||||
|
new QualifiedAddress(ourUuid, new Address(uuid, 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
||||||
import { isEqual, noop, pick } from 'lodash';
|
import { isEqual, noop, pick } from 'lodash';
|
||||||
import type { AttachmentType } from '../../types/Attachment';
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
import type { BodyRangeType } from '../../types/Util';
|
import type { BodyRangeType } from '../../types/Util';
|
||||||
|
import type { ConversationModel } from '../../models/conversations';
|
||||||
import type { MessageAttributesType } from '../../model-types.d';
|
import type { MessageAttributesType } from '../../model-types.d';
|
||||||
import type {
|
import type {
|
||||||
MessageChangedActionType,
|
MessageChangedActionType,
|
||||||
|
@ -20,9 +21,12 @@ import * as log from '../../logging/log';
|
||||||
import dataInterface from '../../sql/Client';
|
import dataInterface from '../../sql/Client';
|
||||||
import { DAY } from '../../util/durations';
|
import { DAY } from '../../util/durations';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
|
||||||
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
|
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
|
||||||
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
|
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
|
||||||
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
||||||
|
import { assert } from '../../util/assert';
|
||||||
|
import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified';
|
||||||
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
||||||
import { getMessageById } from '../../messages/getMessageById';
|
import { getMessageById } from '../../messages/getMessageById';
|
||||||
import { markViewed } from '../../services/MessageUpdater';
|
import { markViewed } from '../../services/MessageUpdater';
|
||||||
|
@ -46,6 +50,7 @@ import { isStory } from '../../messages/helpers';
|
||||||
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
|
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
|
||||||
import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage';
|
import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage';
|
||||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
import { verifyStoryListMembers as doVerifyStoryListMembers } from '../../util/verifyStoryListMembers';
|
||||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||||
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||||
|
|
||||||
|
@ -78,12 +83,16 @@ export type SelectedStoryDataType = {
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export type StoriesStateType = {
|
export type StoriesStateType = {
|
||||||
readonly isShowingStoriesView: boolean;
|
readonly openedAtTimestamp: number | undefined;
|
||||||
readonly replyState?: {
|
readonly replyState?: {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
replies: Array<MessageAttributesType>;
|
replies: Array<MessageAttributesType>;
|
||||||
};
|
};
|
||||||
readonly selectedStoryData?: SelectedStoryDataType;
|
readonly selectedStoryData?: SelectedStoryDataType;
|
||||||
|
readonly sendStoryModalData?: {
|
||||||
|
untrustedUuids: Array<string>;
|
||||||
|
verifiedUuids: Array<string>;
|
||||||
|
};
|
||||||
readonly stories: Array<StoryDataType>;
|
readonly stories: Array<StoryDataType>;
|
||||||
readonly storyViewMode?: StoryViewModeType;
|
readonly storyViewMode?: StoryViewModeType;
|
||||||
};
|
};
|
||||||
|
@ -91,11 +100,14 @@ export type StoriesStateType = {
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
const DOE_STORY = 'stories/DOE';
|
const DOE_STORY = 'stories/DOE';
|
||||||
|
const LIST_MEMBERS_VERIFIED = 'stories/LIST_MEMBERS_VERIFIED';
|
||||||
const LOAD_STORY_REPLIES = 'stories/LOAD_STORY_REPLIES';
|
const LOAD_STORY_REPLIES = 'stories/LOAD_STORY_REPLIES';
|
||||||
const MARK_STORY_READ = 'stories/MARK_STORY_READ';
|
const MARK_STORY_READ = 'stories/MARK_STORY_READ';
|
||||||
const QUEUE_STORY_DOWNLOAD = 'stories/QUEUE_STORY_DOWNLOAD';
|
const QUEUE_STORY_DOWNLOAD = 'stories/QUEUE_STORY_DOWNLOAD';
|
||||||
const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
|
const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
|
||||||
export const RESOLVE_ATTACHMENT_URL = 'stories/RESOLVE_ATTACHMENT_URL';
|
export const RESOLVE_ATTACHMENT_URL = 'stories/RESOLVE_ATTACHMENT_URL';
|
||||||
|
const SEND_STORY_MODAL_OPEN_STATE_CHANGED =
|
||||||
|
'stories/SEND_STORY_MODAL_OPEN_STATE_CHANGED';
|
||||||
const STORY_CHANGED = 'stories/STORY_CHANGED';
|
const STORY_CHANGED = 'stories/STORY_CHANGED';
|
||||||
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
|
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
|
||||||
const VIEW_STORY = 'stories/VIEW_STORY';
|
const VIEW_STORY = 'stories/VIEW_STORY';
|
||||||
|
@ -105,6 +117,14 @@ type DOEStoryActionType = {
|
||||||
payload: string;
|
payload: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ListMembersVerified = {
|
||||||
|
type: typeof LIST_MEMBERS_VERIFIED;
|
||||||
|
payload: {
|
||||||
|
untrustedUuids: Array<string>;
|
||||||
|
verifiedUuids: Array<string>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
type LoadStoryRepliesActionType = {
|
type LoadStoryRepliesActionType = {
|
||||||
type: typeof LOAD_STORY_REPLIES;
|
type: typeof LOAD_STORY_REPLIES;
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -136,6 +156,11 @@ type ResolveAttachmentUrlActionType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SendStoryModalOpenStateChanged = {
|
||||||
|
type: typeof SEND_STORY_MODAL_OPEN_STATE_CHANGED;
|
||||||
|
payload: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
type StoryChangedActionType = {
|
type StoryChangedActionType = {
|
||||||
type: typeof STORY_CHANGED;
|
type: typeof STORY_CHANGED;
|
||||||
payload: StoryDataType;
|
payload: StoryDataType;
|
||||||
|
@ -157,6 +182,7 @@ type ViewStoryActionType = {
|
||||||
|
|
||||||
export type StoriesActionType =
|
export type StoriesActionType =
|
||||||
| DOEStoryActionType
|
| DOEStoryActionType
|
||||||
|
| ListMembersVerified
|
||||||
| LoadStoryRepliesActionType
|
| LoadStoryRepliesActionType
|
||||||
| MarkStoryReadActionType
|
| MarkStoryReadActionType
|
||||||
| MessageChangedActionType
|
| MessageChangedActionType
|
||||||
|
@ -165,6 +191,7 @@ export type StoriesActionType =
|
||||||
| QueueStoryDownloadActionType
|
| QueueStoryDownloadActionType
|
||||||
| ReplyToStoryActionType
|
| ReplyToStoryActionType
|
||||||
| ResolveAttachmentUrlActionType
|
| ResolveAttachmentUrlActionType
|
||||||
|
| SendStoryModalOpenStateChanged
|
||||||
| StoryChangedActionType
|
| StoryChangedActionType
|
||||||
| ToggleViewActionType
|
| ToggleViewActionType
|
||||||
| ViewStoryActionType;
|
| ViewStoryActionType;
|
||||||
|
@ -580,14 +607,52 @@ function sendStoryMessage(
|
||||||
listIds: Array<UUIDStringType>,
|
listIds: Array<UUIDStringType>,
|
||||||
conversationIds: Array<string>,
|
conversationIds: Array<string>,
|
||||||
attachment: AttachmentType
|
attachment: AttachmentType
|
||||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
): ThunkAction<void, RootStateType, unknown, SendStoryModalOpenStateChanged> {
|
||||||
return async dispatch => {
|
return async (dispatch, getState) => {
|
||||||
await doSendStoryMessage(listIds, conversationIds, attachment);
|
const { stories } = getState();
|
||||||
|
const { openedAtTimestamp, sendStoryModalData } = stories;
|
||||||
|
assert(
|
||||||
|
openedAtTimestamp,
|
||||||
|
'sendStoryMessage: openedAtTimestamp is undefined, cannot send'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
sendStoryModalData,
|
||||||
|
'sendStoryMessage: sendStoryModalData is not defined, cannot send'
|
||||||
|
);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'NOOP',
|
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
|
||||||
payload: null,
|
payload: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (sendStoryModalData.untrustedUuids.length) {
|
||||||
|
log.info('sendStoryMessage: SN changed for some conversations');
|
||||||
|
|
||||||
|
const conversationsNeedingVerification: Array<ConversationModel> =
|
||||||
|
sendStoryModalData.untrustedUuids
|
||||||
|
.map(uuid => window.ConversationController.get(uuid))
|
||||||
|
.filter(isNotNil);
|
||||||
|
|
||||||
|
if (!conversationsNeedingVerification.length) {
|
||||||
|
log.warn(
|
||||||
|
'sendStoryMessage: Could not retrieve conversations for untrusted uuids'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await blockSendUntilConversationsAreVerified(
|
||||||
|
conversationsNeedingVerification,
|
||||||
|
SafetyNumberChangeSource.Story,
|
||||||
|
Date.now() - openedAtTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
log.info('sendStoryMessage: did not send');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await doSendStoryMessage(listIds, conversationIds, attachment);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -598,12 +663,69 @@ function storyChanged(story: StoryDataType): StoryChangedActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendStoryModalOpenStateChanged(
|
||||||
|
value: boolean
|
||||||
|
): ThunkAction<void, RootStateType, unknown, SendStoryModalOpenStateChanged> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { stories } = getState();
|
||||||
|
|
||||||
|
if (!stories.sendStoryModalData && value) {
|
||||||
|
dispatch({
|
||||||
|
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
|
||||||
|
payload: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stories.sendStoryModalData && !value) {
|
||||||
|
dispatch({
|
||||||
|
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
|
||||||
|
payload: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toggleStoriesView(): ToggleViewActionType {
|
function toggleStoriesView(): ToggleViewActionType {
|
||||||
return {
|
return {
|
||||||
type: TOGGLE_VIEW,
|
type: TOGGLE_VIEW,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyStoryListMembers(
|
||||||
|
memberUuids: Array<string>
|
||||||
|
): ThunkAction<void, RootStateType, unknown, ListMembersVerified> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const { stories } = getState();
|
||||||
|
const { sendStoryModalData } = stories;
|
||||||
|
|
||||||
|
if (!sendStoryModalData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyVerifiedUuids = new Set([...sendStoryModalData.verifiedUuids]);
|
||||||
|
|
||||||
|
const uuidsNeedingVerification = memberUuids.filter(
|
||||||
|
uuid => !alreadyVerifiedUuids.has(uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uuidsNeedingVerification.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { untrustedUuids, verifiedUuids } = await doVerifyStoryListMembers(
|
||||||
|
uuidsNeedingVerification
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: LIST_MEMBERS_VERIFIED,
|
||||||
|
payload: {
|
||||||
|
untrustedUuids: Array.from(untrustedUuids),
|
||||||
|
verifiedUuids: Array.from(verifiedUuids),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const getSelectedStoryDataForConversationId = (
|
const getSelectedStoryDataForConversationId = (
|
||||||
dispatch: ThunkDispatch<
|
dispatch: ThunkDispatch<
|
||||||
RootStateType,
|
RootStateType,
|
||||||
|
@ -946,8 +1068,10 @@ export const actions = {
|
||||||
reactToStory,
|
reactToStory,
|
||||||
replyToStory,
|
replyToStory,
|
||||||
sendStoryMessage,
|
sendStoryMessage,
|
||||||
|
sendStoryModalOpenStateChanged,
|
||||||
storyChanged,
|
storyChanged,
|
||||||
toggleStoriesView,
|
toggleStoriesView,
|
||||||
|
verifyStoryListMembers,
|
||||||
viewUserStories,
|
viewUserStories,
|
||||||
viewStory,
|
viewStory,
|
||||||
};
|
};
|
||||||
|
@ -960,7 +1084,7 @@ export function getEmptyState(
|
||||||
overrideState: Partial<StoriesStateType> = {}
|
overrideState: Partial<StoriesStateType> = {}
|
||||||
): StoriesStateType {
|
): StoriesStateType {
|
||||||
return {
|
return {
|
||||||
isShowingStoriesView: false,
|
openedAtTimestamp: undefined,
|
||||||
stories: [],
|
stories: [],
|
||||||
...overrideState,
|
...overrideState,
|
||||||
};
|
};
|
||||||
|
@ -971,15 +1095,17 @@ export function reducer(
|
||||||
action: Readonly<StoriesActionType>
|
action: Readonly<StoriesActionType>
|
||||||
): StoriesStateType {
|
): StoriesStateType {
|
||||||
if (action.type === TOGGLE_VIEW) {
|
if (action.type === TOGGLE_VIEW) {
|
||||||
|
const isShowingStoriesView = Boolean(state.openedAtTimestamp);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isShowingStoriesView: !state.isShowingStoriesView,
|
openedAtTimestamp: isShowingStoriesView ? undefined : Date.now(),
|
||||||
selectedStoryData: state.isShowingStoriesView
|
replyState: undefined,
|
||||||
|
sendStoryModalData: undefined,
|
||||||
|
selectedStoryData: isShowingStoriesView
|
||||||
? undefined
|
? undefined
|
||||||
: state.selectedStoryData,
|
: state.selectedStoryData,
|
||||||
storyViewMode: state.isShowingStoriesView
|
storyViewMode: isShowingStoriesView ? undefined : state.storyViewMode,
|
||||||
? undefined
|
|
||||||
: state.storyViewMode,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1244,5 +1370,52 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === SEND_STORY_MODAL_OPEN_STATE_CHANGED) {
|
||||||
|
if (action.payload) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sendStoryModalData: {
|
||||||
|
untrustedUuids: [],
|
||||||
|
verifiedUuids: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sendStoryModalData: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === LIST_MEMBERS_VERIFIED) {
|
||||||
|
const sendStoryModalData = {
|
||||||
|
untrustedUuids: [],
|
||||||
|
verifiedUuids: [],
|
||||||
|
...(state.sendStoryModalData || {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const untrustedUuids = Array.from(
|
||||||
|
new Set([
|
||||||
|
...sendStoryModalData.untrustedUuids,
|
||||||
|
...action.payload.untrustedUuids,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const verifiedUuids = Array.from(
|
||||||
|
new Set([
|
||||||
|
...sendStoryModalData.verifiedUuids,
|
||||||
|
...action.payload.verifiedUuids,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sendStoryModalData: {
|
||||||
|
...sendStoryModalData,
|
||||||
|
untrustedUuids,
|
||||||
|
verifiedUuids,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ export const getStoriesState = (state: StateType): StoriesStateType =>
|
||||||
|
|
||||||
export const shouldShowStoriesView = createSelector(
|
export const shouldShowStoriesView = createSelector(
|
||||||
getStoriesState,
|
getStoriesState,
|
||||||
({ isShowingStoriesView }): boolean => isShowingStoriesView
|
({ openedAtTimestamp }): boolean => Boolean(openedAtTimestamp)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const hasSelectedStoryData = createSelector(
|
export const hasSelectedStoryData = createSelector(
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getMe } from '../selectors/conversations';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
import { getPreferredLeftPaneWidth } from '../selectors/items';
|
import { getPreferredLeftPaneWidth } from '../selectors/items';
|
||||||
import { getStories } from '../selectors/stories';
|
import { getStories, shouldShowStoriesView } from '../selectors/stories';
|
||||||
import { saveAttachment } from '../../util/saveAttachment';
|
import { saveAttachment } from '../../util/saveAttachment';
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||||
|
@ -37,7 +37,7 @@ export function SmartStories(): JSX.Element | null {
|
||||||
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||||
|
|
||||||
const isShowingStoriesView = useSelector<StateType, boolean>(
|
const isShowingStoriesView = useSelector<StateType, boolean>(
|
||||||
(state: StateType) => state.stories.isShowingStoriesView
|
shouldShowStoriesView
|
||||||
);
|
);
|
||||||
|
|
||||||
const preferredWidthFromStorage = useSelector<StateType, number>(
|
const preferredWidthFromStorage = useSelector<StateType, number>(
|
||||||
|
|
|
@ -39,7 +39,11 @@ export function SmartStoryCreator({
|
||||||
onClose,
|
onClose,
|
||||||
}: PropsType): JSX.Element | null {
|
}: PropsType): JSX.Element | null {
|
||||||
const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions();
|
const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions();
|
||||||
const { sendStoryMessage } = useStoriesActions();
|
const {
|
||||||
|
sendStoryModalOpenStateChanged,
|
||||||
|
sendStoryMessage,
|
||||||
|
verifyStoryListMembers,
|
||||||
|
} = useStoriesActions();
|
||||||
const { tagGroupsAsNewGroupStory } = useConversationsActions();
|
const { tagGroupsAsNewGroupStory } = useConversationsActions();
|
||||||
const { createDistributionList } = useStoryDistributionListsActions();
|
const { createDistributionList } = useStoryDistributionListsActions();
|
||||||
|
|
||||||
|
@ -70,9 +74,11 @@ export function SmartStoryCreator({
|
||||||
me={me}
|
me={me}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onDistributionListCreated={createDistributionList}
|
onDistributionListCreated={createDistributionList}
|
||||||
|
onSelectedStoryList={verifyStoryListMembers}
|
||||||
onSend={sendStoryMessage}
|
onSend={sendStoryMessage}
|
||||||
processAttachment={processAttachment}
|
processAttachment={processAttachment}
|
||||||
recentStickers={recentStickers}
|
recentStickers={recentStickers}
|
||||||
|
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
|
||||||
signalConnections={signalConnections}
|
signalConnections={signalConnections}
|
||||||
tagGroupsAsNewGroupStory={tagGroupsAsNewGroupStory}
|
tagGroupsAsNewGroupStory={tagGroupsAsNewGroupStory}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -486,6 +486,7 @@ const URL_CALLS = {
|
||||||
accountExistence: 'v1/accounts/account',
|
accountExistence: 'v1/accounts/account',
|
||||||
attachmentId: 'v2/attachments/form/upload',
|
attachmentId: 'v2/attachments/form/upload',
|
||||||
attestation: 'v1/attestation',
|
attestation: 'v1/attestation',
|
||||||
|
batchIdentityCheck: 'v1/profile/identity_check/batch',
|
||||||
boostBadges: 'v1/subscription/boost/badges',
|
boostBadges: 'v1/subscription/boost/badges',
|
||||||
challenge: 'v1/challenge',
|
challenge: 'v1/challenge',
|
||||||
config: 'v1/config',
|
config: 'v1/config',
|
||||||
|
@ -782,6 +783,20 @@ export type GetGroupCredentialsResultType = Readonly<{
|
||||||
credentials: ReadonlyArray<GroupCredentialType>;
|
credentials: ReadonlyArray<GroupCredentialType>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
const verifyAciResponse = z
|
||||||
|
.object({
|
||||||
|
elements: z.array(
|
||||||
|
z.object({
|
||||||
|
aci: z.string(),
|
||||||
|
identityKey: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
export type VerifyAciRequestType = Array<{ aci: string; fingerprint: string }>;
|
||||||
|
export type VerifyAciResponseType = z.infer<typeof verifyAciResponse>;
|
||||||
|
|
||||||
export type WebAPIType = {
|
export type WebAPIType = {
|
||||||
startRegistration(): unknown;
|
startRegistration(): unknown;
|
||||||
finishRegistration(baton: unknown): void;
|
finishRegistration(baton: unknown): void;
|
||||||
|
@ -878,6 +893,9 @@ export type WebAPIType = {
|
||||||
inviteLinkBase64?: string
|
inviteLinkBase64?: string
|
||||||
) => Promise<Proto.IGroupChange>;
|
) => Promise<Proto.IGroupChange>;
|
||||||
modifyStorageRecords: MessageSender['modifyStorageRecords'];
|
modifyStorageRecords: MessageSender['modifyStorageRecords'];
|
||||||
|
postBatchIdentityCheck: (
|
||||||
|
elements: VerifyAciRequestType
|
||||||
|
) => Promise<VerifyAciResponseType>;
|
||||||
putAttachment: (encryptedBin: Uint8Array) => Promise<string>;
|
putAttachment: (encryptedBin: Uint8Array) => Promise<string>;
|
||||||
putProfile: (
|
putProfile: (
|
||||||
jsonData: ProfileRequestDataType
|
jsonData: ProfileRequestDataType
|
||||||
|
@ -1272,6 +1290,7 @@ export function initialize({
|
||||||
makeSfuRequest,
|
makeSfuRequest,
|
||||||
modifyGroup,
|
modifyGroup,
|
||||||
modifyStorageRecords,
|
modifyStorageRecords,
|
||||||
|
postBatchIdentityCheck,
|
||||||
putAttachment,
|
putAttachment,
|
||||||
putProfile,
|
putProfile,
|
||||||
putStickers,
|
putStickers,
|
||||||
|
@ -1559,6 +1578,28 @@ export function initialize({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function postBatchIdentityCheck(elements: VerifyAciRequestType) {
|
||||||
|
const res = await _ajax({
|
||||||
|
data: JSON.stringify({ elements }),
|
||||||
|
call: 'batchIdentityCheck',
|
||||||
|
httpType: 'POST',
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = verifyAciResponse.safeParse(res);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(
|
||||||
|
'WebAPI: invalid response from postBatchIdentityCheck',
|
||||||
|
toLogFormat(result.error)
|
||||||
|
);
|
||||||
|
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
function getProfileUrl(
|
function getProfileUrl(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,7 +9,8 @@ import { getConversationIdForLogging } from './idForLogging';
|
||||||
|
|
||||||
export async function blockSendUntilConversationsAreVerified(
|
export async function blockSendUntilConversationsAreVerified(
|
||||||
conversations: Array<ConversationModel>,
|
conversations: Array<ConversationModel>,
|
||||||
source?: SafetyNumberChangeSource
|
source?: SafetyNumberChangeSource,
|
||||||
|
timestampThreshold?: number
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const conversationsToPause = new Map<string, Set<string>>();
|
const conversationsToPause = new Map<string, Set<string>>();
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ export async function blockSendUntilConversationsAreVerified(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const untrusted = conversation.getUntrusted();
|
const untrusted = conversation.getUntrusted(timestampThreshold);
|
||||||
if (untrusted.length) {
|
if (untrusted.length) {
|
||||||
untrusted.forEach(untrustedConversation => {
|
untrusted.forEach(untrustedConversation => {
|
||||||
const uuid = untrustedConversation.get('uuid');
|
const uuid = untrustedConversation.get('uuid');
|
||||||
|
|
55
ts/util/verifyStoryListMembers.ts
Normal file
55
ts/util/verifyStoryListMembers.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { UUID } from '../types/UUID';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
import { isNotNil } from './isNotNil';
|
||||||
|
import { updateIdentityKey } from '../services/profiles';
|
||||||
|
|
||||||
|
export async function verifyStoryListMembers(
|
||||||
|
uuids: Array<string>
|
||||||
|
): Promise<{ untrustedUuids: Set<string>; verifiedUuids: Set<string> }> {
|
||||||
|
const { server } = window.textsecure;
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('verifyStoryListMembers: server not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifiedUuids = new Set<string>();
|
||||||
|
const untrustedUuids = new Set<string>();
|
||||||
|
|
||||||
|
const elements = await Promise.all(
|
||||||
|
uuids.map(async aci => {
|
||||||
|
const uuid = new UUID(aci);
|
||||||
|
const fingerprint =
|
||||||
|
await window.textsecure.storage.protocol.getFingerprint(uuid);
|
||||||
|
|
||||||
|
if (!fingerprint) {
|
||||||
|
log.warn('verifyStoryListMembers: no fingerprint found for uuid=', aci);
|
||||||
|
untrustedUuids.add(aci);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedUuids.add(aci);
|
||||||
|
return { aci, fingerprint };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { elements: unverifiedACI } = await server.postBatchIdentityCheck(
|
||||||
|
elements.filter(isNotNil)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
unverifiedACI.map(async ({ aci, identityKey }) => {
|
||||||
|
untrustedUuids.add(aci);
|
||||||
|
verifiedUuids.delete(aci);
|
||||||
|
|
||||||
|
if (identityKey) {
|
||||||
|
await updateIdentityKey(identityKey, new UUID(aci));
|
||||||
|
} else {
|
||||||
|
await window.ConversationController.get(aci)?.getProfiles();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return { untrustedUuids, verifiedUuids };
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue