From b9d6497cb1a37ca332a32b8c8cc396f74b0f765c Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Tue, 21 Sep 2021 17:58:03 -0700 Subject: [PATCH] Better types for WebAPI --- ts/background.ts | 9 +- ts/challenge.ts | 4 +- .../ConversationDetails.stories.tsx | 5 +- .../ConversationDetails.tsx | 3 +- .../helpers/handleCommonJobRequestError.ts | 3 +- .../sleepFor413RetryAfterTimeIfApplicable.ts | 3 +- ts/jobs/normalMessageSendJobQueue.ts | 3 +- ts/jobs/reportSpamJobQueue.ts | 3 +- ts/models/conversations.ts | 5 +- ts/services/calling.ts | 4 +- ts/textsecure/AccountManager.ts | 9 +- ts/textsecure/Errors.ts | 25 +++-- ts/textsecure/MessageReceiver.ts | 3 +- ts/textsecure/OutgoingMessage.ts | 33 ++++-- ts/textsecure/SendMessage.ts | 12 +- ts/textsecure/Utils.ts | 3 +- ts/textsecure/WebAPI.ts | 105 +++++++++++------- ts/textsecure/getKeysForIdentifier.ts | 4 +- ts/textsecure/processDataMessage.ts | 5 +- ts/types/errors.ts | 2 + ts/util/sendToGroup.ts | 5 +- ts/views/install_view.ts | 15 +-- 22 files changed, 156 insertions(+), 107 deletions(-) diff --git a/ts/background.ts b/ts/background.ts index ecde28747..9bbf0493e 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -8,6 +8,7 @@ import { render, unstable_batchedUpdates as batchedUpdates } from 'react-dom'; import MessageReceiver from './textsecure/MessageReceiver'; import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d'; +import { HTTPError } from './textsecure/Errors'; import { MessageAttributesType, ConversationAttributesType, @@ -1797,7 +1798,7 @@ export async function startApp(): Promise { try { await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(server); } catch (error) { - if (error && window._.isNumber(error.code)) { + if (error instanceof HTTPError) { log.warn( `registerForActive: Failed to to refresh remote config. Code: ${error.code}` ); @@ -3403,8 +3404,7 @@ export async function startApp(): Promise { log.error('background onError:', Errors.toLogFormat(error)); if ( - error && - error.name === 'HTTPError' && + error instanceof HTTPError && (error.code === 401 || error.code === 403) ) { unlinkAndDisconnect(RemoveAllConfiguration.Full); @@ -3412,8 +3412,7 @@ export async function startApp(): Promise { } if ( - error && - error.name === 'HTTPError' && + error instanceof HTTPError && (error.code === -1 || error.code === 502) ) { // Failed to connect to server diff --git a/ts/challenge.ts b/ts/challenge.ts index 37da84a8a..958ffe7c3 100644 --- a/ts/challenge.ts +++ b/ts/challenge.ts @@ -19,6 +19,7 @@ import { isOlderThan } from './util/timestamp'; import { parseRetryAfter } from './util/parseRetryAfter'; import { getEnvironment, Environment } from './environment'; import { StorageInterface } from './types/Storage.d'; +import { HTTPError } from './textsecure/Errors'; import * as log from './logging/log'; export type ChallengeResponse = { @@ -454,8 +455,7 @@ export class ChallengeHandler { await this.options.sendChallengeResponse(data); } catch (error) { if ( - !(error instanceof Error) || - error.name !== 'HTTPError' || + !(error instanceof HTTPError) || error.code !== 413 || !error.responseHeaders ) { diff --git a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx index 8c3db3022..ac9d2951e 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx @@ -8,6 +8,7 @@ import { action } from '@storybook/addon-actions'; import { times } from 'lodash'; import { setupI18n } from '../../../util/setupI18n'; +import { CapabilityError } from '../../../types/errors'; import enMessages from '../../../../_locales/en/messages.json'; import { ConversationDetails, Props } from './ConversationDetails'; import { ConversationType } from '../../../state/ducks/conversations'; @@ -152,9 +153,7 @@ story.add('Group add with missing capabilities', () => ( {...createProps()} canEditGroupInfo addMembers={async () => { - const error = new Error(); - error.code = 'E_NO_CAPABILITY'; - throw error; + throw new CapabilityError('stories'); }} /> )); diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index 4882f3efc..dc0f6ca2e 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -9,6 +9,7 @@ import { getMutedUntilText } from '../../../util/getMutedUntilText'; import { LocalizerType } from '../../../types/Util'; import { MediaItemType } from '../../../types/MediaItem'; +import { CapabilityError } from '../../../types/errors'; import { missingCaseError } from '../../../util/missingCaseError'; import { DisappearingTimerSelect } from '../../DisappearingTimerSelect'; @@ -224,7 +225,7 @@ export const ConversationDetails: React.ComponentType = ({ setModalState(ModalState.NothingOpen); setAddGroupMembersRequestState(RequestState.Inactive); } catch (err) { - if (err.code === 'E_NO_CAPABILITY') { + if (err instanceof CapabilityError) { setMembersMissingCapability(true); setAddGroupMembersRequestState(RequestState.InactiveWithError); } else { diff --git a/ts/jobs/helpers/handleCommonJobRequestError.ts b/ts/jobs/helpers/handleCommonJobRequestError.ts index 51a193f81..7c1120570 100644 --- a/ts/jobs/helpers/handleCommonJobRequestError.ts +++ b/ts/jobs/helpers/handleCommonJobRequestError.ts @@ -3,6 +3,7 @@ import type { LoggerType } from '../../types/Logging'; import { parseIntWithFallback } from '../../util/parseIntWithFallback'; +import { HTTPError } from '../../textsecure/Errors'; import { sleepFor413RetryAfterTimeIfApplicable } from './sleepFor413RetryAfterTimeIfApplicable'; export async function handleCommonJobRequestError({ @@ -14,7 +15,7 @@ export async function handleCommonJobRequestError({ log: LoggerType; timeRemaining: number; }>): Promise { - if (!(err instanceof Error)) { + if (!(err instanceof HTTPError)) { throw err; } diff --git a/ts/jobs/helpers/sleepFor413RetryAfterTimeIfApplicable.ts b/ts/jobs/helpers/sleepFor413RetryAfterTimeIfApplicable.ts index 8c5964053..392c7c03b 100644 --- a/ts/jobs/helpers/sleepFor413RetryAfterTimeIfApplicable.ts +++ b/ts/jobs/helpers/sleepFor413RetryAfterTimeIfApplicable.ts @@ -5,6 +5,7 @@ import type { LoggerType } from '../../types/Logging'; import { sleep } from '../../util/sleep'; import { parseRetryAfter } from '../../util/parseRetryAfter'; import { isRecord } from '../../util/isRecord'; +import { HTTPError } from '../../textsecure/Errors'; export async function sleepFor413RetryAfterTimeIfApplicable({ err, @@ -17,7 +18,7 @@ export async function sleepFor413RetryAfterTimeIfApplicable({ }>): Promise { if ( timeRemaining <= 0 || - !(err instanceof Error) || + !(err instanceof HTTPError) || err.code !== 413 || !isRecord(err.responseHeaders) ) { diff --git a/ts/jobs/normalMessageSendJobQueue.ts b/ts/jobs/normalMessageSendJobQueue.ts index 587acd52d..48936d13b 100644 --- a/ts/jobs/normalMessageSendJobQueue.ts +++ b/ts/jobs/normalMessageSendJobQueue.ts @@ -20,6 +20,7 @@ import { getSendOptions } from '../util/getSendOptions'; import { SignalService as Proto } from '../protobuf'; import { handleMessageSend } from '../util/handleMessageSend'; import type { CallbackResultType } from '../textsecure/Types.d'; +import { HTTPError } from '../textsecure/Errors'; import { isSent } from '../messages/MessageSendState'; import { getLastChallengeError, isOutgoing } from '../state/selectors/message'; import { parseIntWithFallback } from '../util/parseIntWithFallback'; @@ -340,7 +341,7 @@ export class NormalMessageSendJobQueue extends JobQueue { formattedMessageSendErrors.push(Errors.toLogFormat(messageSendError)); - if (!(messageSendError instanceof Error)) { + if (!(messageSendError instanceof HTTPError)) { return; } switch (parseIntWithFallback(messageSendError.code, -1)) { diff --git a/ts/jobs/reportSpamJobQueue.ts b/ts/jobs/reportSpamJobQueue.ts index 820150f8c..cc83cf617 100644 --- a/ts/jobs/reportSpamJobQueue.ts +++ b/ts/jobs/reportSpamJobQueue.ts @@ -15,6 +15,7 @@ import { JobQueue } from './JobQueue'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { parseIntWithFallback } from '../util/parseIntWithFallback'; import type { WebAPIType } from '../textsecure/WebAPI'; +import { HTTPError } from '../textsecure/Errors'; const RETRY_WAIT_TIME = durations.MINUTE; const RETRYABLE_4XX_FAILURE_STATUSES = new Set([ @@ -83,7 +84,7 @@ export class ReportSpamJobQueue extends JobQueue { map(serverGuids, serverGuid => server.reportMessage(e164, serverGuid)) ); } catch (err: unknown) { - if (!(err instanceof Error)) { + if (!(err instanceof HTTPError)) { throw err; } diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 0a81eae18..af7362302 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -17,6 +17,7 @@ import { import { AttachmentType } from '../types/Attachment'; import { CallMode, CallHistoryDetailsType } from '../types/Calling'; import * as Stickers from '../types/Stickers'; +import { CapabilityError } from '../types/errors'; import type { GroupV1InfoType, GroupV2InfoType, @@ -1889,11 +1890,9 @@ export class ConversationModel extends window.Backbone return Boolean(model?.get('capabilities')?.announcementGroup); }); if (!isEveryMemberCapable) { - const error = new Error( + throw new CapabilityError( 'addMembersV2: some or all members need to upgrade.' ); - error.code = 'E_NO_CAPABILITY'; - throw error; } } diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 789762431..62da38984 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -1902,7 +1902,7 @@ export class CallingClass { throw new Error('getCallSettings: offline!'); } - const iceServerJson = await window.textsecure.messaging.server.getIceServers(); + const iceServer = await window.textsecure.messaging.server.getIceServers(); const shouldRelayCalls = window.Events.getAlwaysRelayCalls(); @@ -1910,7 +1910,7 @@ export class CallingClass { const isContactUnknown = !conversation.isFromOrAddedByTrustedContact(); return { - iceServer: JSON.parse(iceServerJson), + iceServer, hideIp: shouldRelayCalls || isContactUnknown, bandwidthMode: BandwidthMode.Normal, }; diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index 94946f12c..8308cca2c 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -10,7 +10,8 @@ import PQueue from 'p-queue'; import { omit } from 'lodash'; import EventTarget from './EventTarget'; -import { WebAPIType } from './WebAPI'; +import type { WebAPIType } from './WebAPI'; +import { HTTPError } from './Errors'; import { KeyPairType, CompatSignedPreKeyType } from './Types.d'; import utils from './Helpers'; import ProvisioningCipher from './ProvisioningCipher'; @@ -29,6 +30,7 @@ import { generateSignedPreKey, generatePreKey, } from '../Curve'; +import { UUID } from '../types/UUID'; import { isMoreRecentThan, isOlderThan } from '../util/timestamp'; import { ourProfileKeyService } from '../services/ourProfileKey'; import { assert, strictAssert } from '../util/assert'; @@ -390,8 +392,7 @@ export default class AccountManager extends EventTarget { log.error('rotateSignedPrekey error:', e && e.stack ? e.stack : e); if ( - e instanceof Error && - e.name === 'HTTPError' && + e instanceof HTTPError && e.code && e.code >= 400 && e.code <= 599 @@ -589,7 +590,7 @@ export default class AccountManager extends EventTarget { // update our own identity key, which may have changed // if we're relinking after a reinstall on the master device - await storage.protocol.saveIdentityWithAttributes(ourUuid, { + await storage.protocol.saveIdentityWithAttributes(new UUID(ourUuid), { publicKey: identityKeyPair.pubKey, firstUse: true, timestamp: Date.now(), diff --git a/ts/textsecure/Errors.ts b/ts/textsecure/Errors.ts index a8b33a187..b1bb27c1d 100644 --- a/ts/textsecure/Errors.ts +++ b/ts/textsecure/Errors.ts @@ -1,7 +1,6 @@ // Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable max-classes-per-file */ import { parseRetryAfter } from '../util/parseRetryAfter'; @@ -102,14 +101,14 @@ export class OutgoingIdentityKeyError extends ReplayableError { export class OutgoingMessageError extends ReplayableError { identifier: string; - code?: any; + code?: number; // Note: Data to resend message is no longer captured constructor( incomingIdentifier: string, _m: unknown, _t: unknown, - httpError?: Error + httpError?: HTTPError ) { const identifier = incomingIdentifier.split('.')[0]; @@ -128,11 +127,13 @@ export class OutgoingMessageError extends ReplayableError { } export class SendMessageNetworkError extends ReplayableError { + code: number; + identifier: string; responseHeaders?: HeaderListType | undefined; - constructor(identifier: string, _m: unknown, httpError: Error) { + constructor(identifier: string, _m: unknown, httpError: HTTPError) { super({ name: 'SendMessageNetworkError', message: httpError.message, @@ -152,13 +153,15 @@ export type SendMessageChallengeData = { }; export class SendMessageChallengeError extends ReplayableError { + public code: number; + public identifier: string; public readonly data: SendMessageChallengeData | undefined; public readonly retryAfter: number; - constructor(identifier: string, httpError: Error) { + constructor(identifier: string, httpError: HTTPError) { super({ name: 'SendMessageChallengeError', message: httpError.message, @@ -166,7 +169,7 @@ export class SendMessageChallengeError extends ReplayableError { [this.identifier] = identifier.split('.'); this.code = httpError.code; - this.data = httpError.response; + this.data = httpError.response as SendMessageChallengeData; const headers = httpError.responseHeaders || {}; @@ -241,9 +244,9 @@ export class SignedPreKeyRotationError extends ReplayableError { } export class MessageError extends ReplayableError { - code?: any; + code: number; - constructor(_m: unknown, httpError: Error) { + constructor(_m: unknown, httpError: HTTPError) { super({ name: 'MessageError', message: httpError.message, @@ -258,9 +261,9 @@ export class MessageError extends ReplayableError { export class UnregisteredUserError extends Error { identifier: string; - code?: any; + code: number; - constructor(identifier: string, httpError: Error) { + constructor(identifier: string, httpError: HTTPError) { const { message } = httpError; super(message); @@ -282,3 +285,5 @@ export class UnregisteredUserError extends Error { } export class ConnectTimeoutError extends Error {} + +export class WarnOnlyError extends Error {} diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 496a3a9c5..2dedb5571 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -63,6 +63,7 @@ import { IncomingWebSocketRequest } from './WebsocketResources'; import { ContactBuffer, GroupBuffer } from './ContactsParser'; import type { WebAPIType } from './WebAPI'; import type { Storage } from './Storage'; +import { WarnOnlyError } from './Errors'; import * as Bytes from '../Bytes'; import { ProcessedDataMessage, @@ -922,7 +923,7 @@ export default class MessageReceiver ':', Errors.toLogFormat(error), ]; - if (error.warn) { + if (error instanceof WarnOnlyError) { log.warn(...args); } else { log.error(...args); diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index b664708e8..5d25d7646 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -21,7 +21,7 @@ import { UnidentifiedSenderMessageContent, } from '@signalapp/signal-client'; -import { WebAPIType } from './WebAPI'; +import type { WebAPIType } from './WebAPI'; import { SendMetadataType, SendOptionsType } from './SendMessage'; import { OutgoingIdentityKeyError, @@ -29,6 +29,7 @@ import { SendMessageNetworkError, SendMessageChallengeError, UnregisteredUserError, + HTTPError, } from './Errors'; import { CallbackResultType, CustomError } from './Types.d'; import { isValidNumber } from '../types/PhoneNumber'; @@ -221,7 +222,7 @@ export default class OutgoingMessage { ): void { let error = providedError; - if (!error || (error.name === 'HTTPError' && error.code !== 404)) { + if (!error || (error instanceof HTTPError && error.code !== 404)) { if (error && error.code === 428) { error = new SendMessageChallengeError(identifier, error); } else { @@ -313,7 +314,7 @@ export default class OutgoingMessage { } return promise.catch(e => { - if (e.name === 'HTTPError' && e.code !== 409 && e.code !== 410) { + if (e instanceof HTTPError && e.code !== 409 && e.code !== 410) { // 409 and 410 should bubble and be handled by doSendMessage // 404 should throw UnregisteredUserError // 428 should throw SendMessageChallengeError @@ -517,7 +518,10 @@ export default class OutgoingMessage { } }, async (error: Error) => { - if (error.code === 401 || error.code === 403) { + if ( + error instanceof HTTPError && + (error.code === 401 || error.code === 403) + ) { if (this.failoverIdentifiers.indexOf(identifier) === -1) { this.failoverIdentifiers.push(identifier); } @@ -556,8 +560,7 @@ export default class OutgoingMessage { }) .catch(async error => { if ( - error instanceof Error && - error.name === 'HTTPError' && + error instanceof HTTPError && (error.code === 410 || error.code === 409) ) { if (!recurse) { @@ -569,15 +572,20 @@ export default class OutgoingMessage { return undefined; } + const response = error.response as { + extraDevices?: Array; + staleDevices?: Array; + missingDevices?: Array; + }; let p: Promise = Promise.resolve(); if (error.code === 409) { p = this.removeDeviceIdsForIdentifier( identifier, - error.response.extraDevices || [] + response.extraDevices || [] ); } else { p = Promise.all( - error.response.staleDevices.map(async (deviceId: number) => { + (response.staleDevices || []).map(async (deviceId: number) => { await window.textsecure.storage.protocol.archiveSession( new QualifiedAddress( ourUuid, @@ -591,8 +599,8 @@ export default class OutgoingMessage { return p.then(async () => { const resetDevices = error.code === 410 - ? error.response.staleDevices - : error.response.missingDevices; + ? response.staleDevices + : response.missingDevices; return this.getKeysForIdentifier(identifier, resetDevices).then( // We continue to retry as long as the error code was 409; the assumption is // that we'll request new device info and the next request will succeed. @@ -678,7 +686,10 @@ export default class OutgoingMessage { if (!uuid) { throw new UnregisteredUserError( identifier, - new Error('User is not registered') + new HTTPError('User is not registered', { + code: -1, + headers: {}, + }) ); } identifier = uuid; diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 2e6abc107..67179324e 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -9,6 +9,7 @@ /* eslint-disable max-classes-per-file */ import { Dictionary } from 'lodash'; +import Long from 'long'; import PQueue from 'p-queue'; import { PlaintextContent, @@ -54,6 +55,7 @@ import { MessageError, SignedPreKeyRotationError, SendMessageProtoError, + HTTPError, } from './Errors'; import { BodyRangesType } from '../types/Util'; import { @@ -526,7 +528,7 @@ export default class MessageSender { const id = await this.server.putAttachment(result.ciphertext); const proto = new Proto.AttachmentPointer(); - proto.cdnId = id; + proto.cdnId = Long.fromString(id); proto.contentType = attachment.contentType; proto.key = new FIXMEU8(key); proto.size = attachment.size; @@ -563,7 +565,7 @@ export default class MessageSender { message.attachmentPointers = attachmentPointers; }) .catch(error => { - if (error instanceof Error && error.name === 'HTTPError') { + if (error instanceof HTTPError) { throw new MessageError(message, error); } else { throw error; @@ -584,7 +586,7 @@ export default class MessageSender { // eslint-disable-next-line no-param-reassign message.preview = preview; } catch (error) { - if (error instanceof Error && error.name === 'HTTPError') { + if (error instanceof HTTPError) { throw new MessageError(message, error); } else { throw error; @@ -609,7 +611,7 @@ export default class MessageSender { attachmentPointer: await this.makeAttachmentPointer(sticker.data), }; } catch (error) { - if (error instanceof Error && error.name === 'HTTPError') { + if (error instanceof HTTPError) { throw new MessageError(message, error); } else { throw error; @@ -637,7 +639,7 @@ export default class MessageSender { }); }) ).catch(error => { - if (error instanceof Error && error.name === 'HTTPError') { + if (error instanceof HTTPError) { throw new MessageError(message, error); } else { throw error; diff --git a/ts/textsecure/Utils.ts b/ts/textsecure/Utils.ts index 76b278ba2..f2f18f205 100644 --- a/ts/textsecure/Utils.ts +++ b/ts/textsecure/Utils.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as log from '../logging/log'; +import { HTTPError } from './Errors'; export async function handleStatusCode(status: number): Promise { if (status === 499) { @@ -11,7 +12,7 @@ export async function handleStatusCode(status: number): Promise { } } -export function translateError(error: Error): Error | undefined { +export function translateError(error: HTTPError): HTTPError | undefined { const { code } = error; if (code === 200) { // Happens sometimes when we get no response. Might be nice to get 204 instead. diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 41690e44f..d81efa68d 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -598,7 +598,7 @@ async function _retryAjax( const limit = providedLimit || 3; return _promiseAjax(url, options).catch(async (e: Error) => { - if (e.name === 'HTTPError' && e.code === -1 && count < limit) { + if (e instanceof HTTPError && e.code === -1 && count < limit) { return new Promise(resolve => { setTimeout(() => { resolve(_retryAjax(url, options, limit, count)); @@ -615,17 +615,6 @@ async function _outerAjax(url: string | null, options: PromiseAjaxOptionsType) { return _retryAjax(url, options); } -declare global { - // We want to extend `Error`, so we need an interface. - // eslint-disable-next-line no-restricted-syntax - interface Error { - code?: number | string; - response?: any; - responseHeaders?: HeaderListType; - warn?: boolean; - } -} - function makeHTTPError( message: string, providedCode: number, @@ -722,7 +711,7 @@ type InitializeOptionsType = { version: string; }; -type MessageType = any; +type MessageType = unknown; type AjaxOptionsType = { accessKey?: string; @@ -737,7 +726,7 @@ type AjaxOptionsType = { password?: string; redactUrl?: RedactUrl; responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails'; - schema?: any; + schema?: unknown; timeout?: number; unauthenticated?: boolean; urlParameters?: string; @@ -768,7 +757,7 @@ export type CapabilitiesUploadType = { changeNumber: true; }; -type StickerPackManifestType = any; +type StickerPackManifestType = ArrayBuffer; export type GroupCredentialType = { credential: string; @@ -808,6 +797,20 @@ const uploadAvatarHeadersZod = z .passthrough(); export type UploadAvatarHeadersType = z.infer; +export type ProfileType = Readonly<{ + identityKey?: string; + name?: string; + about?: string; + aboutEmoji?: string; + avatar?: string; + unidentifiedAccess?: string; + unrestrictedUnidentifiedAccess?: string; + username?: string; + uuid?: string; + credential?: string; + capabilities?: unknown; +}>; + export type WebAPIType = { confirmCode: ( number: string, @@ -816,14 +819,21 @@ export type WebAPIType = { registrationId: number, deviceName?: string | null, options?: { accessKey?: ArrayBuffer; uuid?: string } - ) => Promise; + ) => Promise<{ uuid?: string; deviceId: number }>; createGroup: ( group: Proto.IGroup, options: GroupCredentialsType ) => Promise; - getAttachment: (cdnKey: string, cdnNumber?: number) => Promise; - getAvatar: (path: string) => Promise; - getDevices: () => Promise; + getAttachment: (cdnKey: string, cdnNumber?: number) => Promise; + getAvatar: (path: string) => Promise; + getDevices: () => Promise< + Array<{ + id: number; + name: string; + lastSeen: number; + created: number; + }> + >; getGroup: (options: GroupCredentialsType) => Promise; getGroupFromLink: ( inviteLinkPassword: string, @@ -841,7 +851,11 @@ export type WebAPIType = { startVersion: number, options: GroupCredentialsType ) => Promise; - getIceServers: () => Promise; + getIceServers: () => Promise<{ + username: string; + password: string; + urls: Array; + }>; getKeysForIdentifier: ( identifier: string, deviceId?: number @@ -858,7 +872,7 @@ export type WebAPIType = { profileKeyVersion?: string; profileKeyCredentialRequest?: string; } - ) => Promise; + ) => Promise; getProfileUnauth: ( identifier: string, options: { @@ -866,7 +880,7 @@ export type WebAPIType = { profileKeyVersion?: string; profileKeyCredentialRequest?: string; } - ) => Promise; + ) => Promise; getProvisioningResource: ( handler: IRequestHandler ) => Promise; @@ -892,7 +906,13 @@ export type WebAPIType = { makeProxiedRequest: ( targetUrl: string, options?: ProxiedRequestOptionsType - ) => Promise; + ) => Promise< + | ArrayBufferWithDetailsType + | { + result: ArrayBufferWithDetailsType; + totalSize: number; + } + >; makeSfuRequest: ( targetUrl: string, type: HTTPCodeType, @@ -905,7 +925,7 @@ export type WebAPIType = { inviteLinkBase64?: string ) => Promise; modifyStorageRecords: MessageSender['modifyStorageRecords']; - putAttachment: (encryptedBin: ArrayBuffer) => Promise; + putAttachment: (encryptedBin: ArrayBuffer) => Promise; putProfile: ( jsonData: ProfileRequestDataType ) => Promise; @@ -916,10 +936,10 @@ export type WebAPIType = { onProgress?: () => void ) => Promise; registerKeys: (genKeys: KeysType) => Promise; - registerSupportForUnauthenticatedDelivery: () => Promise; + registerSupportForUnauthenticatedDelivery: () => Promise; reportMessage: (senderE164: string, serverGuid: string) => Promise; - requestVerificationSMS: (number: string) => Promise; - requestVerificationVoice: (number: string) => Promise; + requestVerificationSMS: (number: string) => Promise; + requestVerificationVoice: (number: string) => Promise; sendMessages: ( destination: string, messageArray: Array, @@ -949,8 +969,11 @@ export type WebAPIType = { avatarData: Uint8Array, options: GroupCredentialsType ) => Promise; - whoami: () => Promise; - sendChallengeResponse: (challengeResponse: ChallengeType) => Promise; + whoami: () => Promise<{ + uuid?: string; + number?: string; + }>; + sendChallengeResponse: (challengeResponse: ChallengeType) => Promise; getConfig: () => Promise< Array<{ name: string; enabled: boolean; value: string | null }> >; @@ -1188,6 +1211,9 @@ export function initialize({ unauthenticated: param.unauthenticated, accessKey: param.accessKey, }).catch((e: Error) => { + if (!(e instanceof HTTPError)) { + throw e; + } const translatedError = translateError(e); if (translatedError) { throw translatedError; @@ -1458,7 +1484,7 @@ export function initialize({ async function getAvatar(path: string) { // Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our // attachment CDN, it uses our self-signed certificate, so we pass it in. - return _outerAjax(`${cdnUrlObject['0']}/${path}`, { + return (await _outerAjax(`${cdnUrlObject['0']}/${path}`, { certificateAuthority, contentType: 'application/octet-stream', proxyUrl, @@ -1470,7 +1496,7 @@ export function initialize({ return href.replace(pattern, `[REDACTED]${path.slice(-3)}`); }, version, - }); + })) as ArrayBuffer; } async function reportMessage( @@ -1571,6 +1597,7 @@ export function initialize({ return _ajax({ call: 'getIceServers', httpType: 'GET', + responseType: 'json', }); } @@ -1832,7 +1859,7 @@ export function initialize({ if (!isPackIdValid(packId)) { throw new Error('getSticker: pack ID was invalid'); } - return _outerAjax( + return (await _outerAjax( `${cdnUrlObject['0']}/stickers/${packId}/full/${stickerId}`, { certificateAuthority, @@ -1842,14 +1869,14 @@ export function initialize({ redactUrl: redactStickerUrl, version, } - ); + )) as ArrayBuffer; } async function getStickerPackManifest(packId: string) { if (!isPackIdValid(packId)) { throw new Error('getStickerPackManifest: pack ID was invalid'); } - return _outerAjax( + return (await _outerAjax( `${cdnUrlObject['0']}/stickers/${packId}/manifest.proto`, { certificateAuthority, @@ -1859,7 +1886,7 @@ export function initialize({ redactUrl: redactStickerUrl, version, } - ); + )) as ArrayBuffer; } type ServerAttachmentType = { @@ -1989,7 +2016,7 @@ export function initialize({ ? cdnUrlObject[cdnNumber] || cdnUrlObject['0'] : cdnUrlObject['0']; // This is going to the CDN, not the service, so we use _outerAjax - return _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, { + return (await _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, { certificateAuthority, proxyUrl, responseType: 'arraybuffer', @@ -1997,7 +2024,7 @@ export function initialize({ type: 'GET', redactUrl: _createRedactor(cdnKey), version, - }); + })) as ArrayBuffer; } async function putAttachment(encryptedBin: ArrayBuffer) { @@ -2066,7 +2093,7 @@ export function initialize({ headers.Range = `bytes=${start}-${end}`; } - const result = await _outerAjax(targetUrl, { + const result = (await _outerAjax(targetUrl, { responseType: returnArrayBuffer ? 'arraybufferwithdetails' : undefined, proxyUrl: contentProxyUrl, type: 'GET', @@ -2074,7 +2101,7 @@ export function initialize({ redactUrl: () => '[REDACTED_URL]', headers, version, - }); + })) as ArrayBufferWithDetailsType; if (!returnArrayBuffer) { return result; diff --git a/ts/textsecure/getKeysForIdentifier.ts b/ts/textsecure/getKeysForIdentifier.ts index 09239299f..71e544dff 100644 --- a/ts/textsecure/getKeysForIdentifier.ts +++ b/ts/textsecure/getKeysForIdentifier.ts @@ -8,7 +8,7 @@ import { PublicKey, } from '@signalapp/signal-client'; -import { UnregisteredUserError } from './Errors'; +import { UnregisteredUserError, HTTPError } from './Errors'; import { Sessions, IdentityKeys } from '../LibSignalStores'; import { Address } from '../types/Address'; import { QualifiedAddress } from '../types/QualifiedAddress'; @@ -35,7 +35,7 @@ export async function getKeysForIdentifier( accessKeyFailed, }; } catch (error) { - if (error.name === 'HTTPError' && error.code === 404) { + if (error instanceof HTTPError && error.code === 404) { const theirUuid = UUID.lookup(identifier); if (theirUuid) { diff --git a/ts/textsecure/processDataMessage.ts b/ts/textsecure/processDataMessage.ts index 8307abe6b..1b72b05e3 100644 --- a/ts/textsecure/processDataMessage.ts +++ b/ts/textsecure/processDataMessage.ts @@ -23,6 +23,7 @@ import { ProcessedReaction, ProcessedDelete, } from './Types.d'; +import { WarnOnlyError } from './Errors'; // TODO: remove once we move away from ArrayBuffers const FIXMEU8 = Uint8Array; @@ -335,11 +336,9 @@ export async function processDataMessage( // Cleaned up in `processGroupContext` break; default: { - const err = new Error( + throw new WarnOnlyError( `Unknown group message type: ${result.group.type}` ); - err.warn = true; - throw err; } } } diff --git a/ts/types/errors.ts b/ts/types/errors.ts index 4b6eda885..4f7ecbf72 100644 --- a/ts/types/errors.ts +++ b/ts/types/errors.ts @@ -8,3 +8,5 @@ export function toLogFormat(error: unknown): string { return String(error); } + +export class CapabilityError extends Error {} diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index d99f360ce..6a5928f44 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -30,6 +30,7 @@ import { GroupSendOptionsType, SendOptionsType, } from '../textsecure/SendMessage'; +import { HTTPError } from '../textsecure/Errors'; import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores'; import { ConversationModel } from '../models/conversations'; import { DeviceType, CallbackResultType } from '../textsecure/Types.d'; @@ -696,7 +697,7 @@ function isIdentifierRegistered(identifier: string) { return !isUnregistered; } -async function handle409Response(logId: string, error: Error) { +async function handle409Response(logId: string, error: HTTPError) { const parsed = multiRecipient409ResponseSchema.safeParse(error.response); if (parsed.success) { await _waitForAll({ @@ -734,7 +735,7 @@ async function handle409Response(logId: string, error: Error) { async function handle410Response( conversation: ConversationModel, - error: Error + error: HTTPError ) { const logId = conversation.idForLogging(); diff --git a/ts/views/install_view.ts b/ts/views/install_view.ts index 6d518a211..73fb83ae0 100644 --- a/ts/views/install_view.ts +++ b/ts/views/install_view.ts @@ -3,6 +3,7 @@ import * as log from '../logging/log'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; +import { HTTPError } from '../textsecure/Errors'; window.Whisper = window.Whisper || {}; const { Whisper } = window; @@ -47,19 +48,19 @@ Whisper.InstallView = Whisper.View.extend({ if (this.error) { if ( - this.error.name === 'HTTPError' && + this.error instanceof HTTPError && this.error.code === TOO_MANY_DEVICES ) { errorMessage = window.i18n('installTooManyDevices'); } else if ( - this.error.name === 'HTTPError' && + this.error instanceof HTTPError && this.error.code === TOO_OLD ) { errorMessage = window.i18n('installTooOld'); errorButton = window.i18n('upgrade'); errorSecondButton = window.i18n('quit'); } else if ( - this.error.name === 'HTTPError' && + this.error instanceof HTTPError && this.error.code === CONNECTION_ERROR ) { errorMessage = window.i18n('installConnectionFailed'); @@ -102,11 +103,7 @@ Whisper.InstallView = Whisper.View.extend({ window.shutdown(); }, async connect() { - if ( - this.error && - this.error.name === 'HTTPError' && - this.error.code === TOO_OLD - ) { + if (this.error instanceof HTTPError && this.error.code === TOO_OLD) { openLinkInWebBrowser('https://signal.org/download'); return; } @@ -142,7 +139,7 @@ Whisper.InstallView = Whisper.View.extend({ if (error.message === 'websocket closed') { this.trigger('disconnected'); } else if ( - error.name !== 'HTTPError' || + !(error instanceof HTTPError) || (error.code !== CONNECTION_ERROR && error.code !== TOO_MANY_DEVICES) ) { throw error;