// Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { z } from 'zod'; import Long from 'long'; import type { AciString } from './ServiceId'; import { aciSchema } from './ServiceId'; import { bytesToUuid } from '../util/uuidToBytes'; import { SignalService as Proto } from '../protobuf'; import * as Bytes from '../Bytes'; import { UUID_BYTE_SIZE } from './Crypto'; // These are strings (1) for the backup (2) for Storybook. export enum CallMode { Direct = 'Direct', Group = 'Group', Adhoc = 'Adhoc', } export enum CallType { Audio = 'Audio', Video = 'Video', Group = 'Group', Adhoc = 'Adhoc', // Only used for backup roundtripping Unknown = 'Unknown', } export enum CallDirection { Incoming = 'Incoming', Outgoing = 'Outgoing', // Only used for backup roundtripping Unknown = 'Unknown', } export enum CallLogEvent { Clear = 'Clear', MarkedAsRead = 'MarkedAsRead', MarkedAsReadInConversation = 'MarkedAsReadInConversation', } export enum LocalCallEvent { Started = 'LocalStarted', Ringing = 'LocalRinging', Accepted = 'LocalAccepted', Declined = 'LocalDeclined', Hangup = 'LocalHangup', // Incoming = Declined, Outgoing = Missed RemoteHangup = 'LocalRemoteHangup', // Incoming = Missed, Outgoing = Declined Missed = 'LocalMissed', Delete = 'LocalDelete', } export enum RemoteCallEvent { Accepted = 'Accepted', NotAccepted = 'NotAccepted', Delete = 'Delete', Observed = 'Observed', } export type CallEvent = LocalCallEvent | RemoteCallEvent; export enum CallStatusValue { Pending = 'Pending', Accepted = 'Accepted', Missed = 'Missed', // TODO: DESKTOP-3483 - not generated locally MissedNotificationProfile = 'MissedNotificationProfile', Declined = 'Declined', Deleted = 'Deleted', GenericGroupCall = 'GenericGroupCall', GenericAdhocCall = 'GenericAdhocCall', OutgoingRing = 'OutgoingRing', Ringing = 'Ringing', Joined = 'Joined', JoinedAdhoc = 'JoinedAdhoc', // Only used for backup roundtripping Unknown = 'Unknown', } export enum DirectCallStatus { Pending = CallStatusValue.Pending, Accepted = CallStatusValue.Accepted, Missed = CallStatusValue.Missed, // TODO: DESKTOP-3483 - not generated locally MissedNotificationProfile = CallStatusValue.MissedNotificationProfile, Declined = CallStatusValue.Declined, Deleted = CallStatusValue.Deleted, // Only used for backup roundtripping Unknown = CallStatusValue.Unknown, } export enum GroupCallStatus { GenericGroupCall = CallStatusValue.GenericGroupCall, OutgoingRing = CallStatusValue.OutgoingRing, Ringing = CallStatusValue.Ringing, Joined = CallStatusValue.Joined, Accepted = CallStatusValue.Accepted, Missed = CallStatusValue.Missed, // TODO: DESKTOP-3483 - not generated locally MissedNotificationProfile = CallStatusValue.MissedNotificationProfile, Declined = CallStatusValue.Declined, Deleted = CallStatusValue.Deleted, } export enum AdhocCallStatus { Generic = CallStatusValue.GenericAdhocCall, Pending = CallStatusValue.Pending, Joined = CallStatusValue.JoinedAdhoc, Deleted = CallStatusValue.Deleted, // Only used for backup roundtripping Unknown = CallStatusValue.Unknown, } export type CallStatus = DirectCallStatus | GroupCallStatus | AdhocCallStatus; export type CallDetails = Readonly<{ callId: string; peerId: AciString | string; ringerId: AciString | string | null; startedById: AciString | string | null; mode: CallMode; type: CallType; direction: CallDirection; timestamp: number; endedTimestamp: number | null; }>; export type CallLogEventTarget = Readonly<{ timestamp: number; callId: string | null; peerId: AciString | string | null; }>; export type CallLogEventDetails = Readonly<{ type: CallLogEvent; timestamp: number; peerId: AciString | string | null; callId: string | null; }>; export type CallEventDetails = CallDetails & Readonly<{ event: CallEvent; eventSource: string; }>; export type CallHistoryDetails = CallDetails & Readonly<{ status: CallStatus; }>; export type CallHistoryGroup = Omit< CallHistoryDetails, 'callId' | 'ringerId' | 'startedById' | 'endedTimestamp' > & Readonly<{ children: ReadonlyArray<{ callId: string; timestamp: number; }>; }>; export type GroupCallMeta = Readonly<{ callId: string; ringerId: string | AciString; }>; export enum CallHistoryFilterStatus { All = 'All', Missed = 'Missed', } export type CallHistoryFilterOptions = Readonly<{ status: CallHistoryFilterStatus; query: string; }>; export type CallHistoryFilter = Readonly<{ status: CallHistoryFilterStatus; callLinkRoomIds: ReadonlyArray | null; conversationIds: ReadonlyArray | null; }>; export type CallHistoryPagination = Readonly<{ offset: number; limit: number; }>; const ringerIdSchema = z.union([aciSchema, z.string(), z.null()]); const callModeSchema = z.nativeEnum(CallMode); const callTypeSchema = z.nativeEnum(CallType); const callDirectionSchema = z.nativeEnum(CallDirection); const callEventSchema = z.union([ z.nativeEnum(LocalCallEvent), z.nativeEnum(RemoteCallEvent), ]); const callStatusSchema = z.union([ z.nativeEnum(DirectCallStatus), z.nativeEnum(GroupCallStatus), z.nativeEnum(AdhocCallStatus), ]); export const callDetailsSchema = z.object({ callId: z.string(), peerId: z.string(), ringerId: ringerIdSchema, startedById: aciSchema.or(z.null()), mode: callModeSchema, type: callTypeSchema, direction: callDirectionSchema, timestamp: z.number(), endedTimestamp: z.number().or(z.null()), }) satisfies z.ZodType; export const callEventDetailsSchema = callDetailsSchema.extend({ event: callEventSchema, eventSource: z.string(), }) satisfies z.ZodType; export const callHistoryDetailsSchema = callDetailsSchema.extend({ status: callStatusSchema, }) satisfies z.ZodType; export const callHistoryGroupSchema = z.object({ peerId: z.string(), mode: callModeSchema, type: callTypeSchema, direction: callDirectionSchema, status: callStatusSchema, timestamp: z.number(), children: z.array( z.object({ callId: z.string(), timestamp: z.number(), }) ), }) satisfies z.ZodType; const peerIdInBytesSchema = z.instanceof(Uint8Array).transform(value => { // direct conversationId if (value.byteLength === UUID_BYTE_SIZE) { const uuid = bytesToUuid(value); if (uuid != null) { return uuid; } } // groupId or call link roomId return Bytes.toBase64(value); }); const longToStringSchema = z .instanceof(Long) .transform(long => long.toString()); const longToNumberSchema = z .instanceof(Long) .transform(long => long.toNumber()); export const callEventNormalizeSchema = z.object({ peerId: peerIdInBytesSchema, callId: longToStringSchema, timestamp: longToNumberSchema, type: z.nativeEnum(Proto.SyncMessage.CallEvent.Type), direction: z.nativeEnum(Proto.SyncMessage.CallEvent.Direction), event: z.nativeEnum(Proto.SyncMessage.CallEvent.Event), }); export const callLogEventNormalizeSchema = z.object({ type: z.nativeEnum(Proto.SyncMessage.CallLogEvent.Type), timestamp: longToNumberSchema, peerId: peerIdInBytesSchema.optional(), callId: longToStringSchema.optional(), }); export function isSameCallHistoryGroup( a: CallHistoryGroup, b: CallHistoryGroup ): boolean { return ( a.peerId === b.peerId && a.timestamp === b.timestamp && // For a bit more safety. a.mode === b.mode && a.type === b.type && a.direction === b.direction && a.status === b.status ); }