// Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { z } from 'zod'; import Long from 'long'; import { CallMode } from './Calling'; 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'; export enum CallType { Audio = 'Audio', Video = 'Video', Group = 'Group', } export enum CallDirection { Incoming = 'Incoming', Outgoing = 'Outgoing', } export enum CallLogEvent { Clear = 'Clear', } 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', } export type CallEvent = LocalCallEvent | RemoteCallEvent; export enum DirectCallStatus { Pending = 'Pending', Accepted = 'Accepted', Missed = 'Missed', Declined = 'Declined', Deleted = 'Deleted', } export enum GroupCallStatus { GenericGroupCall = 'GenericGroupCall', OutgoingRing = 'OutgoingRing', Ringing = 'Ringing', Joined = 'Joined', // keep these in sync with direct Accepted = DirectCallStatus.Accepted, Missed = DirectCallStatus.Missed, Declined = DirectCallStatus.Declined, Deleted = DirectCallStatus.Deleted, } export type CallStatus = DirectCallStatus | GroupCallStatus; export type CallDetails = Readonly<{ callId: string; peerId: AciString | string; ringerId: AciString | string | null; mode: CallMode; type: CallType; direction: CallDirection; timestamp: number; }>; export type CallEventDetails = CallDetails & Readonly<{ event: CallEvent; }>; export type CallHistoryDetails = CallDetails & Readonly<{ status: CallStatus; }>; export type CallHistoryGroup = Omit & 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; 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), ]); export const callDetailsSchema = z.object({ callId: z.string(), peerId: z.string(), ringerId: ringerIdSchema, mode: callModeSchema, type: callTypeSchema, direction: callDirectionSchema, timestamp: z.number(), }) satisfies z.ZodType; export const callEventDetailsSchema = callDetailsSchema.extend({ event: callEventSchema, }) 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 => { const uuid = bytesToUuid(value); if (uuid != null) { return uuid; } // assuming groupId 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 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 ); }