Introduce Service Id Types
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
414c0a58d3
commit
366b875fd2
269 changed files with 5832 additions and 5550 deletions
|
@ -3,29 +3,33 @@
|
|||
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
||||
import type { UUIDStringType } from './UUID';
|
||||
import { UUID } from './UUID';
|
||||
import type { ServiceIdString } from './ServiceId';
|
||||
import { isServiceIdString } from './ServiceId';
|
||||
|
||||
export type AddressStringType = `${UUIDStringType}.${number}`;
|
||||
export type AddressStringType = `${ServiceIdString}.${number}`;
|
||||
|
||||
const ADDRESS_REGEXP = /^([0-9a-f-]+).(\d+)$/i;
|
||||
const ADDRESS_REGEXP = /^([:0-9a-f-]+).(\d+)$/i;
|
||||
|
||||
export class Address {
|
||||
constructor(public readonly uuid: UUID, public readonly deviceId: number) {}
|
||||
constructor(
|
||||
public readonly serviceId: ServiceIdString,
|
||||
public readonly deviceId: number
|
||||
) {}
|
||||
|
||||
public toString(): AddressStringType {
|
||||
return `${this.uuid.toString()}.${this.deviceId}`;
|
||||
return `${this.serviceId}.${this.deviceId}`;
|
||||
}
|
||||
|
||||
public static parse(value: string): Address {
|
||||
const match = value.match(ADDRESS_REGEXP);
|
||||
strictAssert(match != null, `Invalid Address: ${value}`);
|
||||
const [whole, uuid, deviceId] = match;
|
||||
const [whole, serviceId, deviceId] = match;
|
||||
strictAssert(whole === value, 'Integrity check');
|
||||
return Address.create(uuid, parseInt(deviceId, 10));
|
||||
strictAssert(isServiceIdString(serviceId), 'Their service id is incorrect');
|
||||
return Address.create(serviceId, parseInt(deviceId, 10));
|
||||
}
|
||||
|
||||
public static create(uuid: string, deviceId: number): Address {
|
||||
return new Address(new UUID(uuid), deviceId);
|
||||
public static create(serviceId: ServiceIdString, deviceId: number): Address {
|
||||
return new Address(serviceId, deviceId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { escapeRegExp, isNumber, omit } from 'lodash';
|
|||
import { SignalService as Proto } from '../protobuf';
|
||||
import * as log from '../logging/log';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
SNIPPET_LEFT_PLACEHOLDER,
|
||||
|
@ -15,6 +16,8 @@ import {
|
|||
SNIPPET_TRUNCATION_PLACEHOLDER,
|
||||
} from '../util/search';
|
||||
import { assertDev } from '../util/assert';
|
||||
import type { AciString } from './ServiceId';
|
||||
import { normalizeAci } from './ServiceId';
|
||||
|
||||
// Cold storage of body ranges
|
||||
|
||||
|
@ -37,7 +40,7 @@ export namespace BodyRange {
|
|||
export const { Style } = Proto.DataMessage.BodyRange;
|
||||
|
||||
export type Mention = {
|
||||
mentionUuid: string;
|
||||
mentionUuid: AciString;
|
||||
};
|
||||
export type Link = {
|
||||
url: string;
|
||||
|
@ -144,7 +147,10 @@ const MENTION_NAME = 'mention';
|
|||
|
||||
// We drop unknown bodyRanges and remove extra stuff so they serialize properly
|
||||
export function filterAndClean(
|
||||
ranges: ReadonlyArray<Proto.DataMessage.IBodyRange> | undefined | null
|
||||
ranges:
|
||||
| ReadonlyArray<Proto.DataMessage.IBodyRange | RawBodyRange>
|
||||
| undefined
|
||||
| null
|
||||
): ReadonlyArray<RawBodyRange> | undefined {
|
||||
if (!ranges) {
|
||||
return undefined;
|
||||
|
@ -164,35 +170,54 @@ export function filterAndClean(
|
|||
};
|
||||
|
||||
return ranges
|
||||
.filter((range: Proto.DataMessage.IBodyRange): range is RawBodyRange => {
|
||||
if (!isNumber(range.start)) {
|
||||
.map(range => {
|
||||
const { start, length, ...restOfRange } = range;
|
||||
if (!isNumber(start)) {
|
||||
log.warn('filterAndClean: Dropping bodyRange with non-number start');
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
if (!isNumber(range.length)) {
|
||||
if (!isNumber(length)) {
|
||||
log.warn('filterAndClean: Dropping bodyRange with non-number length');
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (range.mentionUuid) {
|
||||
let mentionUuid: AciString | undefined;
|
||||
if ('mentionAci' in range && range.mentionAci) {
|
||||
mentionUuid = normalizeAci(range.mentionAci, 'BodyRange.mentionAci');
|
||||
} else if ('mentionUuid' in range && range.mentionUuid) {
|
||||
mentionUuid = normalizeAci(range.mentionUuid, 'BodyRange.mentionUuid');
|
||||
}
|
||||
|
||||
if (mentionUuid) {
|
||||
countByTypeRecord[MENTION_NAME] += 1;
|
||||
if (countByTypeRecord[MENTION_NAME] > MAX_PER_TYPE) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
return true;
|
||||
|
||||
return {
|
||||
...restOfRange,
|
||||
start,
|
||||
length,
|
||||
mentionUuid,
|
||||
};
|
||||
}
|
||||
if (range.style) {
|
||||
if ('style' in range && range.style) {
|
||||
countByTypeRecord[range.style] += 1;
|
||||
if (countByTypeRecord[range.style] > MAX_PER_TYPE) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
return true;
|
||||
return {
|
||||
...restOfRange,
|
||||
start,
|
||||
length,
|
||||
style: range.style,
|
||||
};
|
||||
}
|
||||
|
||||
log.warn('filterAndClean: Dropping unknown bodyRange');
|
||||
return false;
|
||||
return undefined;
|
||||
})
|
||||
.map(range => ({ ...range }));
|
||||
.filter(isNotNil);
|
||||
}
|
||||
|
||||
export function hydrateRanges(
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { z } from 'zod';
|
||||
import Long from 'long';
|
||||
import { CallMode } from './Calling';
|
||||
import type { ServiceIdString } from './ServiceId';
|
||||
import { bytesToUuid } from '../util/uuidToBytes';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import * as Bytes from '../Bytes';
|
||||
|
@ -66,7 +67,7 @@ export type CallStatus = DirectCallStatus | GroupCallStatus;
|
|||
|
||||
export type CallDetails = Readonly<{
|
||||
callId: string;
|
||||
peerId: string;
|
||||
peerId: ServiceIdString | string;
|
||||
ringerId: string | null;
|
||||
mode: CallMode;
|
||||
type: CallType;
|
||||
|
|
|
@ -19,7 +19,7 @@ import type {
|
|||
} from './Attachment';
|
||||
import { toLogFormat } from './errors';
|
||||
import type { LoggerType } from './Logging';
|
||||
import type { UUIDStringType } from './UUID';
|
||||
import type { ServiceIdString } from './ServiceId';
|
||||
|
||||
type GenericEmbeddedContactType<AvatarType> = {
|
||||
name?: Name;
|
||||
|
@ -31,7 +31,7 @@ type GenericEmbeddedContactType<AvatarType> = {
|
|||
|
||||
// Populated by selector
|
||||
firstNumber?: string;
|
||||
uuid?: UUIDStringType;
|
||||
uuid?: ServiceIdString;
|
||||
};
|
||||
|
||||
export type EmbeddedContactType = GenericEmbeddedContactType<Avatar>;
|
||||
|
@ -149,7 +149,7 @@ export function embeddedContactSelector(
|
|||
options: {
|
||||
regionCode?: string;
|
||||
firstNumber?: string;
|
||||
uuid?: UUIDStringType;
|
||||
uuid?: ServiceIdString;
|
||||
getAbsoluteAttachmentPath: (path: string) => string;
|
||||
}
|
||||
): EmbeddedContactType {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import type { EmbeddedContactType } from './EmbeddedContact';
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import type { UUIDStringType } from './UUID';
|
||||
import type { ServiceIdString } from './ServiceId';
|
||||
|
||||
export enum PanelType {
|
||||
AllMedia = 'AllMedia',
|
||||
|
@ -28,7 +28,7 @@ export type PanelRequestType =
|
|||
contact: EmbeddedContactType;
|
||||
signalAccount?: {
|
||||
phoneNumber: string;
|
||||
uuid: UUIDStringType;
|
||||
uuid: ServiceIdString;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ export type PanelRenderType =
|
|||
contact: EmbeddedContactType;
|
||||
signalAccount?: {
|
||||
phoneNumber: string;
|
||||
uuid: UUIDStringType;
|
||||
uuid: ServiceIdString;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,30 +3,30 @@
|
|||
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
||||
import type { UUIDStringType } from './UUID';
|
||||
import { UUID } from './UUID';
|
||||
import type { ServiceIdString } from './ServiceId';
|
||||
import { isServiceIdString } from './ServiceId';
|
||||
import type { AddressStringType } from './Address';
|
||||
import { Address } from './Address';
|
||||
|
||||
const QUALIFIED_ADDRESS_REGEXP = /^([0-9a-f-]+):([0-9a-f-]+).(\d+)$/i;
|
||||
const QUALIFIED_ADDRESS_REGEXP = /^([:0-9a-f-]+):([:0-9a-f-]+).(\d+)$/i;
|
||||
|
||||
export type QualifiedAddressCreateOptionsType = Readonly<{
|
||||
ourUuid: string;
|
||||
uuid: string;
|
||||
ourServiceId: ServiceIdString;
|
||||
serviceId: ServiceIdString;
|
||||
deviceId: number;
|
||||
}>;
|
||||
|
||||
export type QualifiedAddressStringType =
|
||||
`${UUIDStringType}:${AddressStringType}`;
|
||||
`${ServiceIdString}:${AddressStringType}`;
|
||||
|
||||
export class QualifiedAddress {
|
||||
constructor(
|
||||
public readonly ourUuid: UUID,
|
||||
public readonly ourServiceId: ServiceIdString,
|
||||
public readonly address: Address
|
||||
) {}
|
||||
|
||||
public get uuid(): UUID {
|
||||
return this.address.uuid;
|
||||
public get serviceId(): ServiceIdString {
|
||||
return this.address.serviceId;
|
||||
}
|
||||
|
||||
public get deviceId(): number {
|
||||
|
@ -34,18 +34,23 @@ export class QualifiedAddress {
|
|||
}
|
||||
|
||||
public toString(): QualifiedAddressStringType {
|
||||
return `${this.ourUuid.toString()}:${this.address.toString()}`;
|
||||
return `${this.ourServiceId}:${this.address.toString()}`;
|
||||
}
|
||||
|
||||
public static parse(value: string): QualifiedAddress {
|
||||
const match = value.match(QUALIFIED_ADDRESS_REGEXP);
|
||||
strictAssert(match != null, `Invalid QualifiedAddress: ${value}`);
|
||||
const [whole, ourUuid, uuid, deviceId] = match;
|
||||
const [whole, ourServiceId, serviceId, deviceId] = match;
|
||||
strictAssert(whole === value, 'Integrity check');
|
||||
strictAssert(
|
||||
isServiceIdString(ourServiceId),
|
||||
'Our service id is incorrect'
|
||||
);
|
||||
strictAssert(isServiceIdString(serviceId), 'Their service id is incorrect');
|
||||
|
||||
return new QualifiedAddress(
|
||||
new UUID(ourUuid),
|
||||
Address.create(uuid, parseInt(deviceId, 10))
|
||||
ourServiceId,
|
||||
Address.create(serviceId, parseInt(deviceId, 10))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
124
ts/types/ServiceId.ts
Normal file
124
ts/types/ServiceId.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
|
||||
import { isValidUuid } from '../util/isValidUuid';
|
||||
import * as log from '../logging/log';
|
||||
import type { LoggerType } from './Logging';
|
||||
|
||||
export enum ServiceIdKind {
|
||||
ACI = 'ACI',
|
||||
PNI = 'PNI',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export type PniString = string & { __pni: never };
|
||||
export type UntaggedPniString = string & { __pni: never };
|
||||
export type AciString = string & { __aci: never };
|
||||
export type ServiceIdString = PniString | AciString;
|
||||
|
||||
export function isServiceIdString(value?: string): value is ServiceIdString {
|
||||
return isAciString(value) || isPniString(value);
|
||||
}
|
||||
|
||||
export function isAciString(value?: string): value is AciString {
|
||||
return isValidUuid(value);
|
||||
}
|
||||
|
||||
export function isPniString(value?: string): value is PniString {
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.startsWith('PNI:')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy IDs
|
||||
return isValidUuid(value);
|
||||
}
|
||||
|
||||
export function isUntaggedPniString(
|
||||
value?: string
|
||||
): value is UntaggedPniString {
|
||||
return isValidUuid(value);
|
||||
}
|
||||
|
||||
export function toTaggedPni(untagged: UntaggedPniString): PniString {
|
||||
return `PNI:${untagged}` as PniString;
|
||||
}
|
||||
|
||||
export function normalizeServiceId(
|
||||
rawServiceId: string,
|
||||
context: string,
|
||||
logger: Pick<LoggerType, 'warn'> = log
|
||||
): ServiceIdString {
|
||||
const result = rawServiceId.toLowerCase().replace(/^pni:/, 'PNI:');
|
||||
|
||||
if (!isAciString(result) && !isPniString(result)) {
|
||||
logger.warn(
|
||||
`Normalizing invalid serviceId: ${rawServiceId} to ${result} in context "${context}"`
|
||||
);
|
||||
|
||||
// Cast anyway we don't want to throw here
|
||||
return result as ServiceIdString;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function normalizeAci(
|
||||
rawAci: string,
|
||||
context: string,
|
||||
logger: Pick<LoggerType, 'warn'> = log
|
||||
): AciString {
|
||||
const result = rawAci.toLowerCase();
|
||||
|
||||
if (!isAciString(result)) {
|
||||
logger.warn(
|
||||
`Normalizing invalid serviceId: ${rawAci} to ${result} in context "${context}"`
|
||||
);
|
||||
|
||||
// Cast anyway we don't want to throw here
|
||||
return result as AciString;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function normalizePni(
|
||||
rawPni: string,
|
||||
context: string,
|
||||
logger: Pick<LoggerType, 'warn'> = log
|
||||
): PniString {
|
||||
const result = rawPni.toLowerCase().replace(/^pni:/, 'PNI:');
|
||||
|
||||
if (!isPniString(result)) {
|
||||
logger.warn(
|
||||
`Normalizing invalid serviceId: ${rawPni} to ${result} in context "${context}"`
|
||||
);
|
||||
|
||||
// Cast anyway we don't want to throw here
|
||||
return result as PniString;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// For tests
|
||||
export function generateAci(): AciString {
|
||||
return generateUuid() as AciString;
|
||||
}
|
||||
|
||||
export function generatePni(): PniString {
|
||||
return `PNI:${generateUuid()}` as PniString;
|
||||
}
|
||||
|
||||
export function getAciFromPrefix(prefix: string): AciString {
|
||||
let padded = prefix;
|
||||
while (padded.length < 8) {
|
||||
padded += '0';
|
||||
}
|
||||
return `${padded}-0000-4000-8000-${'0'.repeat(12)}` as AciString;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { UUID } from './UUID';
|
||||
import type { AciString } from './ServiceId';
|
||||
|
||||
export const SIGNAL_ACI = UUID.cast('11111111-1111-4111-8111-111111111111');
|
||||
export const SIGNAL_ACI = '11111111-1111-4111-8111-111111111111' as AciString;
|
||||
export const SIGNAL_AVATAR_PATH = 'images/profile-avatar.svg';
|
||||
|
|
7
ts/types/Storage.d.ts
vendored
7
ts/types/Storage.d.ts
vendored
|
@ -20,6 +20,7 @@ import type {
|
|||
StorageServiceCredentials,
|
||||
} from '../textsecure/Types.d';
|
||||
import type { ThemeSettingType } from './StorageUIKeys';
|
||||
import type { ServiceIdString } from './ServiceId';
|
||||
|
||||
import type { RegisteredChallengeType } from '../challenge';
|
||||
|
||||
|
@ -35,7 +36,7 @@ export type SentMediaQualitySettingType = 'standard' | 'high';
|
|||
export type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
||||
|
||||
export type IdentityKeyMap = Record<
|
||||
string,
|
||||
ServiceIdString,
|
||||
{
|
||||
privKey: Uint8Array;
|
||||
pubKey: Uint8Array;
|
||||
|
@ -50,7 +51,7 @@ export type StorageAccessType = {
|
|||
'auto-download-update': boolean;
|
||||
'badge-count-muted-conversations': boolean;
|
||||
'blocked-groups': ReadonlyArray<string>;
|
||||
'blocked-uuids': ReadonlyArray<string>;
|
||||
'blocked-uuids': ReadonlyArray<ServiceIdString>;
|
||||
'call-ringtone-notification': boolean;
|
||||
'call-system-notification': boolean;
|
||||
'hide-menu-bar': boolean;
|
||||
|
@ -93,7 +94,7 @@ export type StorageAccessType = {
|
|||
password: string;
|
||||
profileKey: Uint8Array;
|
||||
regionCode: string;
|
||||
registrationIdMap: Record<string, number>;
|
||||
registrationIdMap: Record<ServiceIdString, number>;
|
||||
remoteBuildExpiration: number;
|
||||
sendEditWarningShown: boolean;
|
||||
sessionResets: SessionResetsType;
|
||||
|
|
|
@ -9,7 +9,8 @@ import type { ConversationType } from '../state/ducks/conversations';
|
|||
import type { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import type { SendStatus } from '../messages/MessageSendState';
|
||||
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
|
||||
import type { UUIDStringType } from './UUID';
|
||||
import type { ServiceIdString } from './ServiceId';
|
||||
import type { StoryDistributionIdString } from './StoryDistributionId';
|
||||
|
||||
export type ReplyType = {
|
||||
author: Pick<
|
||||
|
@ -102,14 +103,15 @@ export type StoryViewType = {
|
|||
};
|
||||
|
||||
export type MyStoryType = {
|
||||
id: string;
|
||||
// Either a distribution list id or a conversation (group) id
|
||||
id: StoryDistributionIdString | string;
|
||||
name: string;
|
||||
reducedSendStatus: ResolvedSendStatus;
|
||||
stories: Array<StoryViewType>;
|
||||
};
|
||||
|
||||
export const MY_STORY_ID: UUIDStringType =
|
||||
'00000000-0000-0000-0000-000000000000';
|
||||
export const MY_STORY_ID: StoryDistributionIdString =
|
||||
'00000000-0000-0000-0000-000000000000' as StoryDistributionIdString;
|
||||
|
||||
export enum StoryViewDirectionType {
|
||||
Next = 'Next',
|
||||
|
@ -138,14 +140,15 @@ export enum StoryViewModeType {
|
|||
|
||||
export type StoryDistributionListWithMembersDataType = Omit<
|
||||
StoryDistributionListDataType,
|
||||
'memberUuids'
|
||||
'memberServiceIds'
|
||||
> & {
|
||||
members: Array<ConversationType>;
|
||||
};
|
||||
|
||||
export function getStoryDistributionListName(
|
||||
i18n: LocalizerType,
|
||||
id: string,
|
||||
// Distribution id or conversation (group) id
|
||||
id: StoryDistributionIdString | string | undefined,
|
||||
name: string
|
||||
): string {
|
||||
return id === MY_STORY_ID ? i18n('icu:Stories__mine') : name;
|
||||
|
@ -170,8 +173,7 @@ export enum ResolvedSendStatus {
|
|||
}
|
||||
|
||||
export type StoryMessageRecipientsType = Array<{
|
||||
destinationAci?: string;
|
||||
destinationPni?: string;
|
||||
distributionListIds: Array<string>;
|
||||
destinationServiceId?: ServiceIdString;
|
||||
distributionListIds: Array<StoryDistributionIdString>;
|
||||
isAllowedToReply: boolean;
|
||||
}>;
|
||||
|
|
42
ts/types/StoryDistributionId.ts
Normal file
42
ts/types/StoryDistributionId.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
|
||||
import { isValidUuid } from '../util/isValidUuid';
|
||||
import * as log from '../logging/log';
|
||||
import type { LoggerType } from './Logging';
|
||||
|
||||
export type StoryDistributionIdString = string & {
|
||||
__story_distribution_id: never;
|
||||
};
|
||||
|
||||
export function isStoryDistributionId(
|
||||
value?: string
|
||||
): value is StoryDistributionIdString {
|
||||
return isValidUuid(value);
|
||||
}
|
||||
|
||||
export function generateStoryDistributionId(): StoryDistributionIdString {
|
||||
return generateUuid() as StoryDistributionIdString;
|
||||
}
|
||||
|
||||
export function normalizeStoryDistributionId(
|
||||
distributionId: string,
|
||||
context: string,
|
||||
logger: Pick<LoggerType, 'warn'> = log
|
||||
): StoryDistributionIdString {
|
||||
const result = distributionId.toLowerCase();
|
||||
|
||||
if (!isStoryDistributionId(result)) {
|
||||
logger.warn(
|
||||
'Normalizing invalid story distribution id: ' +
|
||||
`${distributionId} to ${result} in context "${context}"`
|
||||
);
|
||||
|
||||
// Cast anyway we don't want to throw here
|
||||
return result as unknown as StoryDistributionIdString;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
100
ts/types/UUID.ts
100
ts/types/UUID.ts
|
@ -1,100 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
||||
export type UUIDStringType =
|
||||
`${string}-${string}-${string}-${string}-${string}`;
|
||||
|
||||
export type TaggedUUIDStringType = Readonly<
|
||||
| {
|
||||
aci: UUIDStringType;
|
||||
pni?: undefined;
|
||||
}
|
||||
| {
|
||||
aci?: undefined;
|
||||
pni: UUIDStringType;
|
||||
}
|
||||
>;
|
||||
|
||||
export enum UUIDKind {
|
||||
ACI = 'ACI',
|
||||
PNI = 'PNI',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export const UUID_BYTE_SIZE = 16;
|
||||
|
||||
const UUID_REGEXP =
|
||||
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
|
||||
|
||||
export const isValidUuid = (value: unknown): value is UUIDStringType => {
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Zero UUID is a valid uuid.
|
||||
if (value === '00000000-0000-0000-0000-000000000000') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return UUID_REGEXP.test(value);
|
||||
};
|
||||
|
||||
export class UUID {
|
||||
constructor(protected readonly value: string) {
|
||||
strictAssert(isValidUuid(value), `Invalid UUID: ${value}`);
|
||||
}
|
||||
|
||||
public toString(): UUIDStringType {
|
||||
return this.value as UUIDStringType;
|
||||
}
|
||||
|
||||
public isEqual(other: UUID): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
|
||||
public static parse(value: string): UUID {
|
||||
return new UUID(value);
|
||||
}
|
||||
|
||||
public static lookup(identifier: string): UUID | undefined {
|
||||
const conversation = window.ConversationController.get(identifier);
|
||||
const uuid = conversation?.get('uuid');
|
||||
if (uuid === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new UUID(uuid);
|
||||
}
|
||||
|
||||
public static checkedLookup(identifier: string): UUID {
|
||||
const uuid = UUID.lookup(identifier);
|
||||
strictAssert(
|
||||
uuid !== undefined,
|
||||
`Conversation ${identifier} not found or has no uuid`
|
||||
);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public static generate(): UUID {
|
||||
return new UUID(generateUUID());
|
||||
}
|
||||
|
||||
public static cast(value: UUIDStringType): never;
|
||||
public static cast(value: string): UUIDStringType;
|
||||
|
||||
public static cast(value: string): UUIDStringType {
|
||||
return new UUID(value.toLowerCase()).toString();
|
||||
}
|
||||
|
||||
// For testing
|
||||
public static fromPrefix(value: string): UUID {
|
||||
let padded = value;
|
||||
while (padded.length < 8) {
|
||||
padded += '0';
|
||||
}
|
||||
return new UUID(`${padded}-0000-4000-8000-${'0'.repeat(12)}`);
|
||||
}
|
||||
}
|
|
@ -2,13 +2,13 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import type { UUIDStringType } from './UUID';
|
||||
import type { AciString } from './ServiceId';
|
||||
import type { LocaleDirection } from '../../app/locale';
|
||||
|
||||
import type { HourCyclePreference, LocaleMessagesType } from './I18N';
|
||||
|
||||
export type StoryContextType = {
|
||||
authorUuid?: UUIDStringType;
|
||||
authorAci?: AciString;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue