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';
|
||||
|
||||
import * as Bytes from './Bytes';
|
||||
import { constantTimeEqual } from './Crypto';
|
||||
import { constantTimeEqual, sha256 } from './Crypto';
|
||||
import { assert, strictAssert } from './util/assert';
|
||||
import { isNotNil } from './util/isNotNil';
|
||||
import { Zone } from './util/Zone';
|
||||
|
@ -1565,6 +1565,23 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
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> {
|
||||
if (!this.identityKeys) {
|
||||
throw new Error('_saveIdentityKey: this.identityKeys not yet cached!');
|
||||
|
@ -1831,7 +1848,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
return false;
|
||||
}
|
||||
|
||||
isUntrusted(uuid: UUID): boolean {
|
||||
isUntrusted(uuid: UUID, timestampThreshold = TIMESTAMP_THRESHOLD): boolean {
|
||||
if (uuid === null || uuid === undefined) {
|
||||
throw new Error('isUntrusted: uuid was undefined/null');
|
||||
}
|
||||
|
@ -1842,7 +1859,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
}
|
||||
|
||||
if (
|
||||
isMoreRecentThan(identityRecord.timestamp, TIMESTAMP_THRESHOLD) &&
|
||||
isMoreRecentThan(identityRecord.timestamp, timestampThreshold) &&
|
||||
!identityRecord.nonblockingApproval &&
|
||||
!identityRecord.firstUse
|
||||
) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { isInSystemContacts } from '../util/isInSystemContacts';
|
|||
export enum SafetyNumberChangeSource {
|
||||
Calling = 'Calling',
|
||||
MessageSend = 'MessageSend',
|
||||
Story = 'Story',
|
||||
}
|
||||
|
||||
export type SafetyNumberProps = {
|
||||
|
|
|
@ -22,6 +22,7 @@ import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories';
|
|||
import { Modal } from './Modal';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { Theme } from '../util/theme';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
|
||||
export type PropsType = {
|
||||
candidateConversations: Array<ConversationType>;
|
||||
|
@ -36,6 +37,7 @@ export type PropsType = {
|
|||
name: string,
|
||||
viewerUuids: Array<UUIDStringType>
|
||||
) => unknown;
|
||||
onSelectedStoryList: (memberUuids: Array<string>) => unknown;
|
||||
onSend: (
|
||||
listIds: Array<UUIDStringType>,
|
||||
conversationIds: Array<string>
|
||||
|
@ -56,6 +58,21 @@ const Page = {
|
|||
|
||||
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(
|
||||
list: StoryDistributionListDataType,
|
||||
i18n: LocalizerType,
|
||||
|
@ -85,6 +102,7 @@ export const SendStoryModal = ({
|
|||
onClose,
|
||||
onDistributionListCreated,
|
||||
onSend,
|
||||
onSelectedStoryList,
|
||||
signalConnections,
|
||||
tagGroupsAsNewGroupStory,
|
||||
}: PropsType): JSX.Element => {
|
||||
|
@ -300,6 +318,11 @@ export const SendStoryModal = ({
|
|||
}
|
||||
return new Set([...listIds]);
|
||||
});
|
||||
if (value) {
|
||||
onSelectedStoryList(
|
||||
getListMemberUuids(list, signalConnections)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ id, checkboxNode }) => (
|
||||
|
@ -352,6 +375,10 @@ export const SendStoryModal = ({
|
|||
moduleClassName="SendStoryModal__distribution-list"
|
||||
name="SendStoryModal__distribution-list"
|
||||
onChange={(value: boolean) => {
|
||||
if (!group.memberships) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedGroupIds(groupIds => {
|
||||
if (value) {
|
||||
groupIds.add(group.id);
|
||||
|
@ -360,6 +387,9 @@ export const SendStoryModal = ({
|
|||
}
|
||||
return new Set([...groupIds]);
|
||||
});
|
||||
if (value) {
|
||||
onSelectedStoryList(group.memberships.map(({ uuid }) => uuid));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ id, checkboxNode }) => (
|
||||
|
|
|
@ -43,6 +43,7 @@ export type PropsType = {
|
|||
name: string,
|
||||
viewerUuids: Array<UUIDStringType>
|
||||
) => unknown;
|
||||
onSelectedStoryList: (memberUuids: Array<string>) => unknown;
|
||||
onSend: (
|
||||
listIds: Array<UUIDStringType>,
|
||||
conversationIds: Array<string>,
|
||||
|
@ -51,6 +52,7 @@ export type PropsType = {
|
|||
processAttachment: (
|
||||
file: File
|
||||
) => Promise<void | InMemoryAttachmentDraftType>;
|
||||
sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown;
|
||||
signalConnections: Array<ConversationType>;
|
||||
tagGroupsAsNewGroupStory: (cids: Array<string>) => unknown;
|
||||
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'>;
|
||||
|
@ -69,9 +71,11 @@ export const StoryCreator = ({
|
|||
me,
|
||||
onClose,
|
||||
onDistributionListCreated,
|
||||
onSelectedStoryList,
|
||||
onSend,
|
||||
processAttachment,
|
||||
recentStickers,
|
||||
sendStoryModalOpenStateChanged,
|
||||
signalConnections,
|
||||
tagGroupsAsNewGroupStory,
|
||||
}: PropsType): JSX.Element => {
|
||||
|
@ -112,6 +116,10 @@ export const StoryCreator = ({
|
|||
};
|
||||
}, [file, processAttachment]);
|
||||
|
||||
useEffect(() => {
|
||||
sendStoryModalOpenStateChanged(Boolean(draftAttachment));
|
||||
}, [draftAttachment, sendStoryModalOpenStateChanged]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{draftAttachment && (
|
||||
|
@ -125,6 +133,7 @@ export const StoryCreator = ({
|
|||
me={me}
|
||||
onClose={() => setDraftAttachment(undefined)}
|
||||
onDistributionListCreated={onDistributionListCreated}
|
||||
onSelectedStoryList={onSelectedStoryList}
|
||||
onSend={(listIds, groupIds) => {
|
||||
onSend(listIds, groupIds, draftAttachment);
|
||||
setDraftAttachment(undefined);
|
||||
|
|
|
@ -2822,19 +2822,22 @@ export class ConversationModel extends window.Backbone
|
|||
return window.textsecure.storage.protocol.setApproval(uuid, true);
|
||||
}
|
||||
|
||||
safeIsUntrusted(): boolean {
|
||||
safeIsUntrusted(timestampThreshold?: number): boolean {
|
||||
try {
|
||||
const uuid = this.getUuid();
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isUntrusted(): boolean {
|
||||
isUntrusted(timestampThreshold?: number): boolean {
|
||||
if (isDirectConversation(this.attributes)) {
|
||||
return this.safeIsUntrusted();
|
||||
return this.safeIsUntrusted(timestampThreshold);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if (!this.contactCollection!.length) {
|
||||
|
@ -2846,13 +2849,13 @@ export class ConversationModel extends window.Backbone
|
|||
if (isMe(contact.attributes)) {
|
||||
return false;
|
||||
}
|
||||
return contact.safeIsUntrusted();
|
||||
return contact.safeIsUntrusted(timestampThreshold);
|
||||
});
|
||||
}
|
||||
|
||||
getUntrusted(): Array<ConversationModel> {
|
||||
getUntrusted(timestampThreshold?: number): Array<ConversationModel> {
|
||||
if (isDirectConversation(this.attributes)) {
|
||||
if (this.isUntrusted()) {
|
||||
if (this.isUntrusted(timestampThreshold)) {
|
||||
return [this];
|
||||
}
|
||||
return [];
|
||||
|
@ -2863,7 +2866,7 @@ export class ConversationModel extends window.Backbone
|
|||
if (isMe(contact.attributes)) {
|
||||
return false;
|
||||
}
|
||||
return contact.isUntrusted();
|
||||
return contact.isUntrusted(timestampThreshold);
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
GetProfileOptionsType,
|
||||
GetProfileUnauthOptionsType,
|
||||
} from '../textsecure/WebAPI';
|
||||
import type { UUID } from '../types/UUID';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as Bytes from '../Bytes';
|
||||
|
@ -359,20 +360,7 @@ async function doGetProfile(c: ConversationModel): Promise<void> {
|
|||
}
|
||||
|
||||
if (profile.identityKey) {
|
||||
const identityKey = Bytes.fromBase64(profile.identityKey);
|
||||
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))
|
||||
);
|
||||
}
|
||||
await updateIdentityKey(profile.identityKey, uuid);
|
||||
}
|
||||
|
||||
// Update accessKey to prevent race conditions. Since we run asynchronous
|
||||
|
@ -655,3 +643,27 @@ async function maybeGetPNICredential(
|
|||
|
||||
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
|
||||
|
||||
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
||||
import { isEqual, noop, pick } from 'lodash';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import type { BodyRangeType } from '../../types/Util';
|
||||
import type { ConversationModel } from '../../models/conversations';
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
import type {
|
||||
MessageChangedActionType,
|
||||
|
@ -20,9 +21,12 @@ import * as log from '../../logging/log';
|
|||
import dataInterface from '../../sql/Client';
|
||||
import { DAY } from '../../util/durations';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
|
||||
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
|
||||
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
|
||||
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
||||
import { assert } from '../../util/assert';
|
||||
import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified';
|
||||
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import { markViewed } from '../../services/MessageUpdater';
|
||||
|
@ -46,6 +50,7 @@ import { isStory } from '../../messages/helpers';
|
|||
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
|
||||
import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import { verifyStoryListMembers as doVerifyStoryListMembers } from '../../util/verifyStoryListMembers';
|
||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||
|
||||
|
@ -78,12 +83,16 @@ export type SelectedStoryDataType = {
|
|||
// State
|
||||
|
||||
export type StoriesStateType = {
|
||||
readonly isShowingStoriesView: boolean;
|
||||
readonly openedAtTimestamp: number | undefined;
|
||||
readonly replyState?: {
|
||||
messageId: string;
|
||||
replies: Array<MessageAttributesType>;
|
||||
};
|
||||
readonly selectedStoryData?: SelectedStoryDataType;
|
||||
readonly sendStoryModalData?: {
|
||||
untrustedUuids: Array<string>;
|
||||
verifiedUuids: Array<string>;
|
||||
};
|
||||
readonly stories: Array<StoryDataType>;
|
||||
readonly storyViewMode?: StoryViewModeType;
|
||||
};
|
||||
|
@ -91,11 +100,14 @@ export type StoriesStateType = {
|
|||
// Actions
|
||||
|
||||
const DOE_STORY = 'stories/DOE';
|
||||
const LIST_MEMBERS_VERIFIED = 'stories/LIST_MEMBERS_VERIFIED';
|
||||
const LOAD_STORY_REPLIES = 'stories/LOAD_STORY_REPLIES';
|
||||
const MARK_STORY_READ = 'stories/MARK_STORY_READ';
|
||||
const QUEUE_STORY_DOWNLOAD = 'stories/QUEUE_STORY_DOWNLOAD';
|
||||
const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
|
||||
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 TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
|
||||
const VIEW_STORY = 'stories/VIEW_STORY';
|
||||
|
@ -105,6 +117,14 @@ type DOEStoryActionType = {
|
|||
payload: string;
|
||||
};
|
||||
|
||||
type ListMembersVerified = {
|
||||
type: typeof LIST_MEMBERS_VERIFIED;
|
||||
payload: {
|
||||
untrustedUuids: Array<string>;
|
||||
verifiedUuids: Array<string>;
|
||||
};
|
||||
};
|
||||
|
||||
type LoadStoryRepliesActionType = {
|
||||
type: typeof LOAD_STORY_REPLIES;
|
||||
payload: {
|
||||
|
@ -136,6 +156,11 @@ type ResolveAttachmentUrlActionType = {
|
|||
};
|
||||
};
|
||||
|
||||
type SendStoryModalOpenStateChanged = {
|
||||
type: typeof SEND_STORY_MODAL_OPEN_STATE_CHANGED;
|
||||
payload: number | undefined;
|
||||
};
|
||||
|
||||
type StoryChangedActionType = {
|
||||
type: typeof STORY_CHANGED;
|
||||
payload: StoryDataType;
|
||||
|
@ -157,6 +182,7 @@ type ViewStoryActionType = {
|
|||
|
||||
export type StoriesActionType =
|
||||
| DOEStoryActionType
|
||||
| ListMembersVerified
|
||||
| LoadStoryRepliesActionType
|
||||
| MarkStoryReadActionType
|
||||
| MessageChangedActionType
|
||||
|
@ -165,6 +191,7 @@ export type StoriesActionType =
|
|||
| QueueStoryDownloadActionType
|
||||
| ReplyToStoryActionType
|
||||
| ResolveAttachmentUrlActionType
|
||||
| SendStoryModalOpenStateChanged
|
||||
| StoryChangedActionType
|
||||
| ToggleViewActionType
|
||||
| ViewStoryActionType;
|
||||
|
@ -580,14 +607,52 @@ function sendStoryMessage(
|
|||
listIds: Array<UUIDStringType>,
|
||||
conversationIds: Array<string>,
|
||||
attachment: AttachmentType
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
await doSendStoryMessage(listIds, conversationIds, attachment);
|
||||
): ThunkAction<void, RootStateType, unknown, SendStoryModalOpenStateChanged> {
|
||||
return async (dispatch, getState) => {
|
||||
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({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
|
||||
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 {
|
||||
return {
|
||||
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 = (
|
||||
dispatch: ThunkDispatch<
|
||||
RootStateType,
|
||||
|
@ -946,8 +1068,10 @@ export const actions = {
|
|||
reactToStory,
|
||||
replyToStory,
|
||||
sendStoryMessage,
|
||||
sendStoryModalOpenStateChanged,
|
||||
storyChanged,
|
||||
toggleStoriesView,
|
||||
verifyStoryListMembers,
|
||||
viewUserStories,
|
||||
viewStory,
|
||||
};
|
||||
|
@ -960,7 +1084,7 @@ export function getEmptyState(
|
|||
overrideState: Partial<StoriesStateType> = {}
|
||||
): StoriesStateType {
|
||||
return {
|
||||
isShowingStoriesView: false,
|
||||
openedAtTimestamp: undefined,
|
||||
stories: [],
|
||||
...overrideState,
|
||||
};
|
||||
|
@ -971,15 +1095,17 @@ export function reducer(
|
|||
action: Readonly<StoriesActionType>
|
||||
): StoriesStateType {
|
||||
if (action.type === TOGGLE_VIEW) {
|
||||
const isShowingStoriesView = Boolean(state.openedAtTimestamp);
|
||||
|
||||
return {
|
||||
...state,
|
||||
isShowingStoriesView: !state.isShowingStoriesView,
|
||||
selectedStoryData: state.isShowingStoriesView
|
||||
openedAtTimestamp: isShowingStoriesView ? undefined : Date.now(),
|
||||
replyState: undefined,
|
||||
sendStoryModalData: undefined,
|
||||
selectedStoryData: isShowingStoriesView
|
||||
? undefined
|
||||
: state.selectedStoryData,
|
||||
storyViewMode: state.isShowingStoriesView
|
||||
? undefined
|
||||
: state.storyViewMode,
|
||||
storyViewMode: isShowingStoriesView ? 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;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ export const getStoriesState = (state: StateType): StoriesStateType =>
|
|||
|
||||
export const shouldShowStoriesView = createSelector(
|
||||
getStoriesState,
|
||||
({ isShowingStoriesView }): boolean => isShowingStoriesView
|
||||
({ openedAtTimestamp }): boolean => Boolean(openedAtTimestamp)
|
||||
);
|
||||
|
||||
export const hasSelectedStoryData = createSelector(
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getMe } from '../selectors/conversations';
|
|||
import { getIntl } from '../selectors/user';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { getPreferredLeftPaneWidth } from '../selectors/items';
|
||||
import { getStories } from '../selectors/stories';
|
||||
import { getStories, shouldShowStoriesView } from '../selectors/stories';
|
||||
import { saveAttachment } from '../../util/saveAttachment';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
|
@ -37,7 +37,7 @@ export function SmartStories(): JSX.Element | null {
|
|||
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||
|
||||
const isShowingStoriesView = useSelector<StateType, boolean>(
|
||||
(state: StateType) => state.stories.isShowingStoriesView
|
||||
shouldShowStoriesView
|
||||
);
|
||||
|
||||
const preferredWidthFromStorage = useSelector<StateType, number>(
|
||||
|
|
|
@ -39,7 +39,11 @@ export function SmartStoryCreator({
|
|||
onClose,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions();
|
||||
const { sendStoryMessage } = useStoriesActions();
|
||||
const {
|
||||
sendStoryModalOpenStateChanged,
|
||||
sendStoryMessage,
|
||||
verifyStoryListMembers,
|
||||
} = useStoriesActions();
|
||||
const { tagGroupsAsNewGroupStory } = useConversationsActions();
|
||||
const { createDistributionList } = useStoryDistributionListsActions();
|
||||
|
||||
|
@ -70,9 +74,11 @@ export function SmartStoryCreator({
|
|||
me={me}
|
||||
onClose={onClose}
|
||||
onDistributionListCreated={createDistributionList}
|
||||
onSelectedStoryList={verifyStoryListMembers}
|
||||
onSend={sendStoryMessage}
|
||||
processAttachment={processAttachment}
|
||||
recentStickers={recentStickers}
|
||||
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
|
||||
signalConnections={signalConnections}
|
||||
tagGroupsAsNewGroupStory={tagGroupsAsNewGroupStory}
|
||||
/>
|
||||
|
|
|
@ -486,6 +486,7 @@ const URL_CALLS = {
|
|||
accountExistence: 'v1/accounts/account',
|
||||
attachmentId: 'v2/attachments/form/upload',
|
||||
attestation: 'v1/attestation',
|
||||
batchIdentityCheck: 'v1/profile/identity_check/batch',
|
||||
boostBadges: 'v1/subscription/boost/badges',
|
||||
challenge: 'v1/challenge',
|
||||
config: 'v1/config',
|
||||
|
@ -782,6 +783,20 @@ export type GetGroupCredentialsResultType = Readonly<{
|
|||
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 = {
|
||||
startRegistration(): unknown;
|
||||
finishRegistration(baton: unknown): void;
|
||||
|
@ -878,6 +893,9 @@ export type WebAPIType = {
|
|||
inviteLinkBase64?: string
|
||||
) => Promise<Proto.IGroupChange>;
|
||||
modifyStorageRecords: MessageSender['modifyStorageRecords'];
|
||||
postBatchIdentityCheck: (
|
||||
elements: VerifyAciRequestType
|
||||
) => Promise<VerifyAciResponseType>;
|
||||
putAttachment: (encryptedBin: Uint8Array) => Promise<string>;
|
||||
putProfile: (
|
||||
jsonData: ProfileRequestDataType
|
||||
|
@ -1272,6 +1290,7 @@ export function initialize({
|
|||
makeSfuRequest,
|
||||
modifyGroup,
|
||||
modifyStorageRecords,
|
||||
postBatchIdentityCheck,
|
||||
putAttachment,
|
||||
putProfile,
|
||||
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(
|
||||
identifier: string,
|
||||
{
|
||||
|
|
|
@ -9,7 +9,8 @@ import { getConversationIdForLogging } from './idForLogging';
|
|||
|
||||
export async function blockSendUntilConversationsAreVerified(
|
||||
conversations: Array<ConversationModel>,
|
||||
source?: SafetyNumberChangeSource
|
||||
source?: SafetyNumberChangeSource,
|
||||
timestampThreshold?: number
|
||||
): Promise<boolean> {
|
||||
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) {
|
||||
untrusted.forEach(untrustedConversation => {
|
||||
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…
Reference in a new issue