Sync call link call history
This commit is contained in:
parent
ce83195170
commit
2785501f82
26 changed files with 800 additions and 175 deletions
|
@ -7048,6 +7048,10 @@
|
|||
"messageformat": "Declined {type, select, Audio {voice} Video {video} Group {group} other {}} call",
|
||||
"description": "Call History > Short description of call > When call was declined"
|
||||
},
|
||||
"icu:CallHistory__Description--Adhoc": {
|
||||
"messageformat": "Call link",
|
||||
"description": "Call History > Short description of call > When you joined a call link call"
|
||||
},
|
||||
"icu:TypingBubble__avatar--overflow-count": {
|
||||
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.",
|
||||
"description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden."
|
||||
|
|
|
@ -613,8 +613,14 @@ message SyncMessage {
|
|||
}
|
||||
|
||||
message CallLinkUpdate {
|
||||
enum Type {
|
||||
UPDATE = 0;
|
||||
DELETE = 1;
|
||||
}
|
||||
|
||||
optional bytes rootKey = 1;
|
||||
optional bytes adminPasskey = 2;
|
||||
optional Type type = 3; // defaults to UPDATE
|
||||
}
|
||||
|
||||
message CallLogEvent {
|
||||
|
|
|
@ -199,6 +199,7 @@ import {
|
|||
import { deriveStorageServiceKey } from './Crypto';
|
||||
import { getThemeType } from './util/getThemeType';
|
||||
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
||||
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
|
@ -681,6 +682,10 @@ export async function startApp(): Promise<void> {
|
|||
'callEventSync',
|
||||
queuedEventListener(onCallEventSync, false)
|
||||
);
|
||||
messageReceiver.addEventListener(
|
||||
'callLinkUpdateSync',
|
||||
queuedEventListener(onCallLinkUpdateSync, false)
|
||||
);
|
||||
messageReceiver.addEventListener(
|
||||
'callLogEventSync',
|
||||
queuedEventListener(onCallLogEventSync, false)
|
||||
|
|
|
@ -44,9 +44,11 @@ import { CallsNewCallButton } from './CallsNewCall';
|
|||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
import { Theme } from '../util/theme';
|
||||
import type { CallingConversationType } from '../types/Calling';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import type { CallLinkType } from '../types/CallLink';
|
||||
import { callLinkToConversation } from '../util/callLinks';
|
||||
import {
|
||||
callLinkToConversation,
|
||||
getPlaceholderCallLinkConversation,
|
||||
} from '../util/callLinks';
|
||||
|
||||
function Timestamp({
|
||||
i18n,
|
||||
|
@ -299,6 +301,26 @@ export function CallsList({
|
|||
[searchState]
|
||||
);
|
||||
|
||||
const getConversationForItem = useCallback(
|
||||
(item: CallHistoryGroup | null): CallingConversationType | null => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAdhoc = item?.type === CallType.Adhoc;
|
||||
if (isAdhoc) {
|
||||
const callLink = isAdhoc ? getCallLink(item.peerId) : null;
|
||||
if (callLink) {
|
||||
return callLinkToConversation(callLink, i18n);
|
||||
}
|
||||
return getPlaceholderCallLinkConversation(item.peerId, i18n);
|
||||
}
|
||||
|
||||
return getConversation(item.peerId) ?? null;
|
||||
},
|
||||
[getCallLink, getConversation, i18n]
|
||||
);
|
||||
|
||||
const isRowLoaded = useCallback(
|
||||
(props: Index) => {
|
||||
return searchState.results?.items[props.index] != null;
|
||||
|
@ -309,16 +331,11 @@ export function CallsList({
|
|||
const rowRenderer = useCallback(
|
||||
({ key, index, style }: ListRowProps) => {
|
||||
const item = searchState.results?.items.at(index) ?? null;
|
||||
const isAdhoc = item?.mode === CallMode.Adhoc;
|
||||
const callLink = isAdhoc ? getCallLink(item.peerId) : null;
|
||||
|
||||
const conversation: CallingConversationType | null =
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
item
|
||||
? callLink
|
||||
? callLinkToConversation(callLink, i18n)
|
||||
: getConversation(item.peerId) ?? null
|
||||
: null;
|
||||
const conversation = getConversationForItem(item);
|
||||
const isAdhoc = item?.type === CallType.Adhoc;
|
||||
const isNewCallVisible = Boolean(
|
||||
!isAdhoc || (isAdhoc && getCallLink(item.peerId))
|
||||
);
|
||||
|
||||
if (
|
||||
searchState.state === 'pending' ||
|
||||
|
@ -354,7 +371,7 @@ export function CallsList({
|
|||
let statusText;
|
||||
if (wasMissed) {
|
||||
statusText = i18n('icu:CallsList__ItemCallInfo--Missed');
|
||||
} else if (callLink) {
|
||||
} else if (isAdhoc) {
|
||||
statusText = i18n('icu:CallsList__ItemCallInfo--CallLink');
|
||||
} else if (item.type === CallType.Group) {
|
||||
statusText = i18n('icu:CallsList__ItemCallInfo--GroupCall');
|
||||
|
@ -393,12 +410,13 @@ export function CallsList({
|
|||
/>
|
||||
}
|
||||
trailing={
|
||||
isNewCallVisible ? (
|
||||
<CallsNewCallButton
|
||||
callType={item.type}
|
||||
hasActiveCall={hasActiveCall}
|
||||
onClick={() => {
|
||||
if (callLink) {
|
||||
startCallLinkLobbyByRoomId(callLink.roomId);
|
||||
if (isAdhoc) {
|
||||
startCallLinkLobbyByRoomId(item.peerId);
|
||||
} else if (conversation) {
|
||||
if (item.type === CallType.Audio) {
|
||||
onOutgoingAudioCallInConversation(conversation.id);
|
||||
|
@ -408,6 +426,7 @@ export function CallsList({
|
|||
}
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
title={
|
||||
<span
|
||||
|
@ -426,10 +445,13 @@ export function CallsList({
|
|||
</span>
|
||||
}
|
||||
onClick={() => {
|
||||
if (callLink) {
|
||||
if (isAdhoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (conversation == null) {
|
||||
return;
|
||||
}
|
||||
onSelectCallHistoryGroup(conversation.id, item);
|
||||
}}
|
||||
/>
|
||||
|
@ -440,7 +462,7 @@ export function CallsList({
|
|||
hasActiveCall,
|
||||
searchState,
|
||||
getCallLink,
|
||||
getConversation,
|
||||
getConversationForItem,
|
||||
selectedCallHistoryGroup,
|
||||
onSelectCallHistoryGroup,
|
||||
onOutgoingAudioCallInConversation,
|
||||
|
|
|
@ -72,6 +72,10 @@ function describeCallHistory(
|
|||
direction: CallDirection,
|
||||
status: CallStatus
|
||||
): string {
|
||||
if (type === CallType.Adhoc) {
|
||||
return i18n('icu:CallHistory__Description--Adhoc');
|
||||
}
|
||||
|
||||
if (status === DirectCallStatus.Missed || status === GroupCallStatus.Missed) {
|
||||
if (direction === CallDirection.Incoming) {
|
||||
return i18n('icu:CallHistory__Description--Missed', { type });
|
||||
|
|
|
@ -585,7 +585,8 @@ async function getCallLinkPreview(
|
|||
}
|
||||
|
||||
const callLinkRootKey = CallLinkRootKey.parse(parsedUrl.args.key);
|
||||
const callLinkState = await calling.readCallLink({ callLinkRootKey });
|
||||
const readResult = await calling.readCallLink({ callLinkRootKey });
|
||||
const { callLinkState } = readResult;
|
||||
if (!callLinkState || callLinkState.revoked) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { ipcRenderer } from 'electron';
|
|||
import type {
|
||||
AudioDevice,
|
||||
CallId,
|
||||
CallLinkState,
|
||||
CallLinkState as RingRTCCallLinkState,
|
||||
DeviceId,
|
||||
GroupCallObserver,
|
||||
PeekInfo,
|
||||
|
@ -46,7 +46,6 @@ import Long from 'long';
|
|||
import type { CallLinkAuthCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
|
||||
import type {
|
||||
ActionsType as CallingReduxActionsType,
|
||||
CallLinkStateType,
|
||||
GroupCallParticipantInfoType,
|
||||
GroupCallPeekInfoType,
|
||||
} from '../state/ducks/calling';
|
||||
|
@ -120,7 +119,7 @@ import {
|
|||
getCallIdFromRing,
|
||||
getLocalCallEventFromRingUpdate,
|
||||
convertJoinState,
|
||||
updateLocalAdhocCallHistory,
|
||||
updateAdhocCallHistory,
|
||||
getCallIdFromEra,
|
||||
getCallDetailsForAdhocCall,
|
||||
} from '../util/callDisposition';
|
||||
|
@ -134,6 +133,7 @@ import {
|
|||
} from '../util/callLinks';
|
||||
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
|
||||
import { conversationJobQueue } from '../jobs/conversationJobQueue';
|
||||
import type { ReadCallLinkState } from '../types/CallLink';
|
||||
|
||||
const {
|
||||
processGroupCallRingCancellation,
|
||||
|
@ -173,6 +173,7 @@ type CallingReduxInterface = Pick<
|
|||
| 'groupCallEnded'
|
||||
| 'groupCallRaisedHandsChange'
|
||||
| 'groupCallStateChange'
|
||||
| 'joinedAdhocCall'
|
||||
| 'outgoingCall'
|
||||
| 'receiveGroupCallReactions'
|
||||
| 'receiveIncomingDirectCall'
|
||||
|
@ -538,7 +539,16 @@ export class CallingClass {
|
|||
callLinkRootKey,
|
||||
}: Readonly<{
|
||||
callLinkRootKey: CallLinkRootKey;
|
||||
}>): Promise<CallLinkState | undefined> {
|
||||
}>): Promise<
|
||||
| {
|
||||
callLinkState: ReadCallLinkState;
|
||||
errorStatusCode: undefined;
|
||||
}
|
||||
| {
|
||||
callLinkState: undefined;
|
||||
errorStatusCode: number;
|
||||
}
|
||||
> {
|
||||
if (!this._sfuUrl) {
|
||||
throw new Error('readCallLink() missing SFU URL; not handling call link');
|
||||
}
|
||||
|
@ -555,11 +565,17 @@ export class CallingClass {
|
|||
);
|
||||
if (!result.success) {
|
||||
log.warn(`${logId}: failed`);
|
||||
return;
|
||||
return {
|
||||
callLinkState: undefined,
|
||||
errorStatusCode: result.errorStatusCode,
|
||||
};
|
||||
}
|
||||
|
||||
log.info(`${logId}: success`);
|
||||
return result.value;
|
||||
return {
|
||||
callLinkState: this.formatCallLinkStateForRedux(result.value),
|
||||
errorStatusCode: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async startCallLinkLobby({
|
||||
|
@ -1050,9 +1066,13 @@ export class CallingClass {
|
|||
eraId
|
||||
) {
|
||||
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
||||
if (callMode === CallMode.Group) {
|
||||
drop(this.sendGroupCallUpdateMessage(conversationId, eraId));
|
||||
}
|
||||
drop(
|
||||
this.onGroupCallJoined({
|
||||
peerId: conversationId,
|
||||
eraId,
|
||||
callMode,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1126,10 +1146,13 @@ export class CallingClass {
|
|||
eraId
|
||||
) {
|
||||
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
||||
|
||||
if (callMode === CallMode.Group) {
|
||||
drop(this.sendGroupCallUpdateMessage(conversationId, eraId));
|
||||
}
|
||||
drop(
|
||||
this.onGroupCallJoined({
|
||||
peerId: conversationId,
|
||||
eraId,
|
||||
callMode,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// For adhoc calls, conversationId will be a roomId
|
||||
|
@ -1168,6 +1191,22 @@ export class CallingClass {
|
|||
};
|
||||
}
|
||||
|
||||
private async onGroupCallJoined({
|
||||
callMode,
|
||||
peerId,
|
||||
eraId,
|
||||
}: {
|
||||
callMode: CallMode.Group | CallMode.Adhoc;
|
||||
peerId: string;
|
||||
eraId: string;
|
||||
}): Promise<void> {
|
||||
if (callMode === CallMode.Group) {
|
||||
drop(this.sendGroupCallUpdateMessage(peerId, eraId));
|
||||
} else if (callMode === CallMode.Adhoc) {
|
||||
this.reduxInterface?.joinedAdhocCall(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
public async joinCallLinkCall({
|
||||
roomId,
|
||||
rootKey,
|
||||
|
@ -1365,8 +1404,8 @@ export class CallingClass {
|
|||
}
|
||||
|
||||
public formatCallLinkStateForRedux(
|
||||
callLinkState: CallLinkState
|
||||
): CallLinkStateType {
|
||||
callLinkState: RingRTCCallLinkState
|
||||
): ReadCallLinkState {
|
||||
const { name, restrictions, expiration, revoked } = callLinkState;
|
||||
return {
|
||||
name,
|
||||
|
@ -2658,7 +2697,7 @@ export class CallingClass {
|
|||
LocalCallEvent.Accepted,
|
||||
'CallingClass.updateCallHistoryForGroupCallOnLocalChanged'
|
||||
);
|
||||
await updateLocalAdhocCallHistory(callEvent);
|
||||
await updateAdhocCallHistory(callEvent);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'CallingClass.updateCallHistoryForGroupCallOnLocalChanged: Error updating state',
|
||||
|
|
|
@ -29,7 +29,7 @@ import type {
|
|||
CallHistoryGroup,
|
||||
CallHistoryPagination,
|
||||
} from '../types/CallDisposition';
|
||||
import type { CallLinkType, CallLinkRestrictions } from '../types/CallLink';
|
||||
import type { CallLinkStateType, CallLinkType } from '../types/CallLink';
|
||||
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
||||
|
||||
export type AdjacentMessagesByConversationOptionsType = Readonly<{
|
||||
|
@ -681,13 +681,15 @@ export type DataInterface = {
|
|||
>;
|
||||
callLinkExists(roomId: string): Promise<boolean>;
|
||||
getAllCallLinks: () => Promise<ReadonlyArray<CallLinkType>>;
|
||||
getCallLinkByRoomId: (roomId: string) => Promise<CallLinkType | undefined>;
|
||||
insertCallLink(callLink: CallLinkType): Promise<void>;
|
||||
updateCallLinkAdminKeyByRoomId(
|
||||
roomId: string,
|
||||
adminKey: string
|
||||
): Promise<void>;
|
||||
updateCallLinkState(
|
||||
roomId: string,
|
||||
name: string,
|
||||
restrictions: CallLinkRestrictions,
|
||||
expiration: number | null,
|
||||
revoked: boolean
|
||||
callLinkState: CallLinkStateType
|
||||
): Promise<void>;
|
||||
migrateConversationMessages: (
|
||||
obsoleteId: string,
|
||||
|
|
|
@ -169,7 +169,9 @@ import {
|
|||
import {
|
||||
callLinkExists,
|
||||
getAllCallLinks,
|
||||
getCallLinkByRoomId,
|
||||
insertCallLink,
|
||||
updateCallLinkAdminKeyByRoomId,
|
||||
updateCallLinkState,
|
||||
} from './server/callLinks';
|
||||
import { CallMode } from '../types/Calling';
|
||||
|
@ -341,7 +343,9 @@ const dataInterface: ServerInterface = {
|
|||
getRecentStaleRingsAndMarkOlderMissed,
|
||||
callLinkExists,
|
||||
getAllCallLinks,
|
||||
getCallLinkByRoomId,
|
||||
insertCallLink,
|
||||
updateCallLinkAdminKeyByRoomId,
|
||||
updateCallLinkState,
|
||||
migrateConversationMessages,
|
||||
getMessagesBetween,
|
||||
|
|
|
@ -3,12 +3,16 @@
|
|||
|
||||
import type { Database } from '@signalapp/better-sqlite3';
|
||||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||
import type { CallLinkRestrictions, CallLinkType } from '../../types/CallLink';
|
||||
import type { CallLinkStateType, CallLinkType } from '../../types/CallLink';
|
||||
import {
|
||||
callLinkRestrictionsSchema,
|
||||
callLinkRecordSchema,
|
||||
} from '../../types/CallLink';
|
||||
import { callLinkToRecord, callLinkFromRecord } from '../../util/callLinks';
|
||||
import {
|
||||
callLinkToRecord,
|
||||
callLinkFromRecord,
|
||||
toAdminKeyBytes,
|
||||
} from '../../util/callLinks';
|
||||
import { getReadonlyInstance, getWritableInstance, prepare } from '../Server';
|
||||
import { sql } from '../util';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
@ -23,6 +27,23 @@ export async function callLinkExists(roomId: string): Promise<boolean> {
|
|||
return db.prepare(query).pluck(true).get(params) === 1;
|
||||
}
|
||||
|
||||
export async function getCallLinkByRoomId(
|
||||
roomId: string
|
||||
): Promise<CallLinkType | undefined> {
|
||||
const db = getReadonlyInstance();
|
||||
const row = prepare(db, 'SELECT * FROM callLinks WHERE roomId = $roomId').get(
|
||||
{
|
||||
roomId,
|
||||
}
|
||||
);
|
||||
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return callLinkFromRecord(callLinkRecordSchema.parse(row));
|
||||
}
|
||||
|
||||
export async function getAllCallLinks(): Promise<ReadonlyArray<CallLinkType>> {
|
||||
const db = getReadonlyInstance();
|
||||
const [query] = sql`
|
||||
|
@ -70,11 +91,9 @@ export async function insertCallLink(callLink: CallLinkType): Promise<void> {
|
|||
|
||||
export async function updateCallLinkState(
|
||||
roomId: string,
|
||||
name: string,
|
||||
restrictions: CallLinkRestrictions,
|
||||
expiration: number,
|
||||
revoked: boolean
|
||||
callLinkState: CallLinkStateType
|
||||
): Promise<void> {
|
||||
const { name, restrictions, expiration, revoked } = callLinkState;
|
||||
const db = await getWritableInstance();
|
||||
const restrictionsValue = callLinkRestrictionsSchema.parse(restrictions);
|
||||
const [query, params] = sql`
|
||||
|
@ -89,6 +108,22 @@ export async function updateCallLinkState(
|
|||
db.prepare(query).run(params);
|
||||
}
|
||||
|
||||
export async function updateCallLinkAdminKeyByRoomId(
|
||||
roomId: string,
|
||||
adminKey: string
|
||||
): Promise<void> {
|
||||
const db = await getWritableInstance();
|
||||
const adminKeyBytes = toAdminKeyBytes(adminKey);
|
||||
prepare(
|
||||
db,
|
||||
`
|
||||
UPDATE callLinks
|
||||
SET adminKey = $adminKeyBytes
|
||||
WHERE roomId = $roomId;
|
||||
`
|
||||
).run({ roomId, adminKeyBytes });
|
||||
}
|
||||
|
||||
function assertRoomIdMatchesRootKey(roomId: string, rootKey: string): void {
|
||||
const derivedRoomId = CallLinkRootKey.parse(rootKey)
|
||||
.deriveRoomId()
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
hasScreenCapturePermission,
|
||||
openSystemPreferences,
|
||||
} from 'mac-screen-capture-permissions';
|
||||
import { omit } from 'lodash';
|
||||
import { omit, pick } from 'lodash';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import {
|
||||
CallLinkRootKey,
|
||||
|
@ -32,7 +32,7 @@ import type {
|
|||
PresentedSource,
|
||||
PresentableSource,
|
||||
} from '../../types/Calling';
|
||||
import type { CallLinkRestrictions } from '../../types/CallLink';
|
||||
import type { CallLinkStateType } from '../../types/CallLink';
|
||||
import {
|
||||
CALLING_REACTIONS_LIFETIME,
|
||||
MAX_CALLING_REACTIONS,
|
||||
|
@ -46,7 +46,11 @@ import {
|
|||
} from '../../types/Calling';
|
||||
import { callingTones } from '../../util/callingTones';
|
||||
import { requestCameraPermissions } from '../../util/callingPermissions';
|
||||
import { getRoomIdFromRootKey } from '../../util/callLinks';
|
||||
import {
|
||||
CALL_LINK_DEFAULT_STATE,
|
||||
getRoomIdFromRootKey,
|
||||
} from '../../util/callLinks';
|
||||
import { sendCallLinkUpdateSync } from '../../util/sendCallLinkUpdateSync';
|
||||
import { sleep } from '../../util/sleep';
|
||||
import { LatestQueue } from '../../util/LatestQueue';
|
||||
import type { AciString } from '../../types/ServiceId';
|
||||
|
@ -168,13 +172,6 @@ export type AdhocCallsType = {
|
|||
[roomId: string]: GroupCallStateType;
|
||||
};
|
||||
|
||||
export type CallLinkStateType = ReadonlyDeep<{
|
||||
name: string;
|
||||
restrictions: CallLinkRestrictions;
|
||||
expiration: number | null;
|
||||
revoked: boolean;
|
||||
}>;
|
||||
|
||||
export type CallLinksByRoomIdStateType = ReadonlyDeep<
|
||||
CallLinkStateType & {
|
||||
rootKey: string;
|
||||
|
@ -244,10 +241,20 @@ type GroupCallStateChangeActionPayloadType =
|
|||
ourAci: AciString;
|
||||
};
|
||||
|
||||
type HandleCallLinkUpdateActionPayloadType = ReadonlyDeep<{
|
||||
roomId: string;
|
||||
callLinkDetails: CallLinksByRoomIdStateType;
|
||||
}>;
|
||||
|
||||
type HangUpActionPayloadType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
}>;
|
||||
|
||||
type HandleCallLinkUpdateType = ReadonlyDeep<{
|
||||
rootKey: string;
|
||||
adminKey: string | null;
|
||||
}>;
|
||||
|
||||
export type IncomingDirectCallType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
isVideoCall: boolean;
|
||||
|
@ -567,6 +574,7 @@ const GROUP_CALL_RAISED_HANDS_CHANGE = 'calling/GROUP_CALL_RAISED_HANDS_CHANGE';
|
|||
const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
|
||||
const GROUP_CALL_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
|
||||
const GROUP_CALL_REACTIONS_EXPIRED = 'calling/GROUP_CALL_REACTIONS_EXPIRED';
|
||||
const HANDLE_CALL_LINK_UPDATE = 'calling/HANDLE_CALL_LINK_UPDATE';
|
||||
const HANG_UP = 'calling/HANG_UP';
|
||||
const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL';
|
||||
const INCOMING_GROUP_CALL = 'calling/INCOMING_GROUP_CALL';
|
||||
|
@ -699,6 +707,11 @@ type GroupCallReactionsExpiredActionType = ReadonlyDeep<{
|
|||
payload: GroupCallReactionsExpiredActionPayloadType;
|
||||
}>;
|
||||
|
||||
type HandleCallLinkUpdateActionType = ReadonlyDeep<{
|
||||
type: 'calling/HANDLE_CALL_LINK_UPDATE';
|
||||
payload: HandleCallLinkUpdateActionPayloadType;
|
||||
}>;
|
||||
|
||||
type HangUpActionType = ReadonlyDeep<{
|
||||
type: 'calling/HANG_UP';
|
||||
payload: HangUpActionPayloadType;
|
||||
|
@ -838,6 +851,7 @@ export type CallingActionType =
|
|||
| GroupCallStateChangeActionType
|
||||
| GroupCallReactionsReceivedActionType
|
||||
| GroupCallReactionsExpiredActionType
|
||||
| HandleCallLinkUpdateActionType
|
||||
| HangUpActionType
|
||||
| IncomingDirectCallActionType
|
||||
| IncomingGroupCallActionType
|
||||
|
@ -1206,6 +1220,72 @@ function groupCallStateChange(
|
|||
};
|
||||
}
|
||||
|
||||
// From sync messages, to notify us that another device joined or changed a call link.
|
||||
function handleCallLinkUpdate(
|
||||
payload: HandleCallLinkUpdateType
|
||||
): ThunkAction<void, RootStateType, unknown, HandleCallLinkUpdateActionType> {
|
||||
return async dispatch => {
|
||||
const { rootKey, adminKey } = payload;
|
||||
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||
const logId = `handleCallLinkUpdate(${roomId})`;
|
||||
|
||||
const readResult = await calling.readCallLink({
|
||||
callLinkRootKey,
|
||||
});
|
||||
|
||||
// Only give up when server confirms the call link is gone. If we fail to fetch
|
||||
// state due to unexpected errors, continue to save rootKey and adminKey.
|
||||
if (readResult.errorStatusCode === 404) {
|
||||
log.info(`${logId}: Call link not found, ignoring`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { callLinkState: freshCallLinkState } = readResult;
|
||||
const existingCallLink = await dataInterface.getCallLinkByRoomId(roomId);
|
||||
const existingCallLinkState = pick(existingCallLink, [
|
||||
'name',
|
||||
'restrictions',
|
||||
'expiration',
|
||||
'revoked',
|
||||
]);
|
||||
|
||||
const callLinkDetails: CallLinksByRoomIdStateType = {
|
||||
...CALL_LINK_DEFAULT_STATE,
|
||||
...existingCallLinkState,
|
||||
...freshCallLinkState,
|
||||
rootKey,
|
||||
adminKey,
|
||||
};
|
||||
|
||||
if (existingCallLink) {
|
||||
if (adminKey && adminKey !== existingCallLink.adminKey) {
|
||||
await dataInterface.updateCallLinkAdminKeyByRoomId(roomId, adminKey);
|
||||
log.info(`${logId}: Updated existing call link with new adminKey`);
|
||||
}
|
||||
|
||||
if (freshCallLinkState) {
|
||||
await dataInterface.updateCallLinkState(roomId, freshCallLinkState);
|
||||
log.info(`${logId}: Updated existing call link state`);
|
||||
}
|
||||
} else {
|
||||
await dataInterface.insertCallLink({
|
||||
roomId,
|
||||
...callLinkDetails,
|
||||
});
|
||||
log.info(`${logId}: Saved new call link`);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: HANDLE_CALL_LINK_UPDATE,
|
||||
payload: {
|
||||
roomId,
|
||||
callLinkDetails,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function hangUpActiveCall(
|
||||
reason: string
|
||||
): ThunkAction<void, RootStateType, unknown, HangUpActionType> {
|
||||
|
@ -1327,6 +1407,21 @@ function outgoingCall(payload: StartDirectCallType): OutgoingCallActionType {
|
|||
};
|
||||
}
|
||||
|
||||
function joinedAdhocCall(
|
||||
roomId: string
|
||||
): ThunkAction<void, RootStateType, unknown, never> {
|
||||
return async (_dispatch, getState) => {
|
||||
const state = getState();
|
||||
const callLink = getOwn(state.calling.callLinks, roomId);
|
||||
if (!callLink) {
|
||||
log.warn(`joinedAdhocCall(${roomId}): call link not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
drop(sendCallLinkUpdateSync(callLink));
|
||||
};
|
||||
}
|
||||
|
||||
function peekGroupCallForTheFirstTime(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, PeekGroupCallFulfilledActionType> {
|
||||
|
@ -1712,7 +1807,8 @@ const _startCallLinkLobby = async ({
|
|||
|
||||
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||
|
||||
const callLinkState = await calling.readCallLink({ callLinkRootKey });
|
||||
const readResult = await calling.readCallLink({ callLinkRootKey });
|
||||
const { callLinkState } = readResult;
|
||||
if (!callLinkState) {
|
||||
const i18n = getIntl(getState());
|
||||
dispatch({
|
||||
|
@ -1725,7 +1821,10 @@ const _startCallLinkLobby = async ({
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (callLinkState.revoked || callLinkState.expiration < new Date()) {
|
||||
if (
|
||||
callLinkState.revoked ||
|
||||
callLinkState.expiration < new Date().getTime()
|
||||
) {
|
||||
const i18n = getIntl(getState());
|
||||
dispatch({
|
||||
type: SHOW_ERROR_MODAL,
|
||||
|
@ -1739,21 +1838,13 @@ const _startCallLinkLobby = async ({
|
|||
}
|
||||
|
||||
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||
const formattedCallLinkState =
|
||||
calling.formatCallLinkStateForRedux(callLinkState);
|
||||
try {
|
||||
const { name, restrictions, expiration, revoked } = formattedCallLinkState;
|
||||
const callLinkExists = await dataInterface.callLinkExists(roomId);
|
||||
if (callLinkExists) {
|
||||
await dataInterface.updateCallLinkState(
|
||||
roomId,
|
||||
name,
|
||||
restrictions,
|
||||
expiration,
|
||||
revoked
|
||||
);
|
||||
await dataInterface.updateCallLinkState(roomId, callLinkState);
|
||||
log.info('startCallLinkLobby: Updated existing call link', roomId);
|
||||
} else {
|
||||
const { name, restrictions, expiration, revoked } = callLinkState;
|
||||
await dataInterface.insertCallLink({
|
||||
roomId,
|
||||
rootKey,
|
||||
|
@ -1790,7 +1881,7 @@ const _startCallLinkLobby = async ({
|
|||
type: START_CALL_LINK_LOBBY,
|
||||
payload: {
|
||||
...callLobbyData,
|
||||
callLinkState: formattedCallLinkState,
|
||||
callLinkState,
|
||||
callLinkRootKey: rootKey,
|
||||
conversationId: roomId,
|
||||
isConversationTooBigToRing: false,
|
||||
|
@ -1983,6 +2074,8 @@ export const actions = {
|
|||
groupCallRaisedHandsChange,
|
||||
groupCallStateChange,
|
||||
hangUpActiveCall,
|
||||
handleCallLinkUpdate,
|
||||
joinedAdhocCall,
|
||||
onOutgoingVideoCallInConversation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
openSystemPreferencesAction,
|
||||
|
@ -3100,5 +3193,18 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === HANDLE_CALL_LINK_UPDATE) {
|
||||
const { callLinks } = state;
|
||||
const { roomId, callLinkDetails } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
callLinks: {
|
||||
...callLinks,
|
||||
[roomId]: callLinkDetails,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
26
ts/test-both/helpers/fakeCallLink.ts
Normal file
26
ts/test-both/helpers/fakeCallLink.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||
import { MONTH } from '../../util/durations/constants';
|
||||
|
||||
export const FAKE_CALL_LINK: CallLinkType = {
|
||||
adminKey: null,
|
||||
expiration: Date.now() + MONTH, // set me
|
||||
name: 'Fun Link',
|
||||
restrictions: CallLinkRestrictions.None,
|
||||
revoked: false,
|
||||
roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4',
|
||||
rootKey: 'bpmc-mrgn-hntf-mffd-mndd-xbxk-zmgq-qszg',
|
||||
};
|
||||
|
||||
// Please set expiration
|
||||
export const FAKE_CALL_LINK_WITH_ADMIN_KEY: CallLinkType = {
|
||||
adminKey: 'xXPI77e6MoVHYREW8iKYmQ==',
|
||||
expiration: Date.now() + MONTH, // set me
|
||||
name: 'Fun Link',
|
||||
restrictions: CallLinkRestrictions.None,
|
||||
revoked: false,
|
||||
roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4',
|
||||
rootKey: 'bpmc-mrgn-hntf-mffd-mndd-xbxk-zmgq-qszg',
|
||||
};
|
|
@ -4,29 +4,10 @@
|
|||
import { assert } from 'chai';
|
||||
|
||||
import { callLinkToRecord, callLinkFromRecord } from '../../util/callLinks';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||
import { MONTH } from '../../util/durations/constants';
|
||||
|
||||
const CALL_LINK: CallLinkType = {
|
||||
adminKey: null,
|
||||
expiration: Date.now() + MONTH,
|
||||
name: 'Fun Link',
|
||||
restrictions: CallLinkRestrictions.None,
|
||||
revoked: false,
|
||||
roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4',
|
||||
rootKey: 'bpmc-mrgn-hntf-mffd-mndd-xbxk-zmgq-qszg',
|
||||
};
|
||||
|
||||
const CALL_LINK_WITH_ADMIN_KEY: CallLinkType = {
|
||||
adminKey: 'xXPI77e6MoVHYREW8iKYmQ==',
|
||||
expiration: Date.now() + MONTH,
|
||||
name: 'Fun Link',
|
||||
restrictions: CallLinkRestrictions.None,
|
||||
revoked: false,
|
||||
roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4',
|
||||
rootKey: 'bpmc-mrgn-hntf-mffd-mndd-xbxk-zmgq-qszg',
|
||||
};
|
||||
import {
|
||||
FAKE_CALL_LINK as CALL_LINK,
|
||||
FAKE_CALL_LINK_WITH_ADMIN_KEY as CALL_LINK_WITH_ADMIN_KEY,
|
||||
} from '../helpers/fakeCallLink';
|
||||
|
||||
describe('callLinks', () => {
|
||||
it('callLinkToRecord() and callLinkFromRecord() can convert to record and back', () => {
|
||||
|
|
|
@ -321,21 +321,21 @@ describe('sql/getCallHistoryGroups', () => {
|
|||
const now = Date.now();
|
||||
const roomId = generateUuid();
|
||||
|
||||
function toCall(callId: string, timestamp: number, type: CallType) {
|
||||
function toCall(callId: string, timestamp: number) {
|
||||
return {
|
||||
callId,
|
||||
peerId: roomId,
|
||||
ringerId: null,
|
||||
mode: CallMode.Adhoc,
|
||||
type,
|
||||
type: CallType.Adhoc,
|
||||
direction: CallDirection.Outgoing,
|
||||
timestamp,
|
||||
status: AdhocCallStatus.Joined,
|
||||
};
|
||||
}
|
||||
|
||||
const call1 = toCall('1', now - 10, CallType.Group);
|
||||
const call2 = toCall('2', now, CallType.Group);
|
||||
const call1 = toCall('1', now - 10);
|
||||
const call2 = toCall('2', now);
|
||||
|
||||
await saveCallHistory(call1);
|
||||
await saveCallHistory(call2);
|
||||
|
@ -394,28 +394,23 @@ describe('sql/getCallHistoryGroupsCount', () => {
|
|||
const roomId1 = generateUuid();
|
||||
const roomId2 = generateUuid();
|
||||
|
||||
function toCall(
|
||||
callId: string,
|
||||
roomId: string,
|
||||
timestamp: number,
|
||||
type: CallType
|
||||
) {
|
||||
function toCall(callId: string, roomId: string, timestamp: number) {
|
||||
return {
|
||||
callId,
|
||||
peerId: roomId,
|
||||
ringerId: null,
|
||||
mode: CallMode.Adhoc,
|
||||
type,
|
||||
type: CallType.Adhoc,
|
||||
direction: CallDirection.Outgoing,
|
||||
timestamp,
|
||||
status: AdhocCallStatus.Joined,
|
||||
};
|
||||
}
|
||||
|
||||
const call1 = toCall('1', roomId1, now - 20, CallType.Group);
|
||||
const call2 = toCall('2', roomId1, now - 10, CallType.Group);
|
||||
const call3 = toCall('3', roomId1, now, CallType.Group);
|
||||
const call4 = toCall('4', roomId2, now, CallType.Group);
|
||||
const call1 = toCall('1', roomId1, now - 20);
|
||||
const call2 = toCall('2', roomId1, now - 10);
|
||||
const call3 = toCall('3', roomId1, now);
|
||||
const call4 = toCall('4', roomId2, now);
|
||||
|
||||
await saveCallHistory(call1);
|
||||
await saveCallHistory(call2);
|
||||
|
|
|
@ -36,6 +36,8 @@ import {
|
|||
import { generateAci } from '../../../types/ServiceId';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import type { UnwrapPromise } from '../../../types/Util';
|
||||
import { CallLinkRestrictions } from '../../../types/CallLink';
|
||||
import { FAKE_CALL_LINK } from '../../../test-both/helpers/fakeCallLink';
|
||||
|
||||
const ACI_1 = generateAci();
|
||||
const NOW = new Date('2020-01-23T04:56:00.000');
|
||||
|
@ -1301,6 +1303,62 @@ describe('calling duck', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('handleCallLinkUpdate', () => {
|
||||
const { roomId, rootKey, expiration } = FAKE_CALL_LINK;
|
||||
|
||||
beforeEach(function (this: Mocha.Context) {
|
||||
this.callingServiceReadCallLink = this.sandbox
|
||||
.stub(callingService, 'readCallLink')
|
||||
.resolves({
|
||||
callLinkState: {
|
||||
name: 'Signal Call',
|
||||
restrictions: CallLinkRestrictions.None,
|
||||
expiration,
|
||||
revoked: false,
|
||||
},
|
||||
errorStatusCode: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('reads the call link from calling service', async function (this: Mocha.Context) {
|
||||
const { handleCallLinkUpdate } = actions;
|
||||
const dispatch = sinon.spy();
|
||||
await handleCallLinkUpdate({ rootKey, adminKey: null })(
|
||||
dispatch,
|
||||
getEmptyRootState,
|
||||
null
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(this.callingServiceReadCallLink);
|
||||
});
|
||||
|
||||
it('dispatches HANDLE_CALL_LINK_UPDATE', async () => {
|
||||
const { handleCallLinkUpdate } = actions;
|
||||
const dispatch = sinon.spy();
|
||||
await handleCallLinkUpdate({ rootKey, adminKey: null })(
|
||||
dispatch,
|
||||
getEmptyRootState,
|
||||
null
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'calling/HANDLE_CALL_LINK_UPDATE',
|
||||
payload: {
|
||||
roomId,
|
||||
callLinkDetails: {
|
||||
name: 'Signal Call',
|
||||
restrictions: CallLinkRestrictions.None,
|
||||
expiration,
|
||||
revoked: false,
|
||||
rootKey,
|
||||
adminKey: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('peekNotConnectedGroupCall', () => {
|
||||
const { peekNotConnectedGroupCall } = actions;
|
||||
|
||||
|
|
|
@ -129,6 +129,7 @@ import {
|
|||
ContactSyncEvent,
|
||||
StoryRecipientUpdateEvent,
|
||||
CallLogEventSyncEvent,
|
||||
CallLinkUpdateSyncEvent,
|
||||
} from './messageReceiverEvents';
|
||||
import * as log from '../logging/log';
|
||||
import * as durations from '../util/durations';
|
||||
|
@ -145,6 +146,7 @@ import { filterAndClean } from '../types/BodyRange';
|
|||
import { getCallEventForProto } from '../util/callDisposition';
|
||||
import { checkOurPniIdentityKey } from '../util/checkOurPniIdentityKey';
|
||||
import { CallLogEvent } from '../types/CallDisposition';
|
||||
import { CallLinkUpdateSyncType } from '../types/CallLink';
|
||||
|
||||
const GROUPV2_ID_LENGTH = 32;
|
||||
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
||||
|
@ -674,6 +676,11 @@ export default class MessageReceiver
|
|||
handler: (ev: CallEventSyncEvent) => void
|
||||
): void;
|
||||
|
||||
public override addEventListener(
|
||||
name: 'callLinkUpdateSync',
|
||||
handler: (ev: CallLinkUpdateSyncEvent) => void
|
||||
): void;
|
||||
|
||||
public override addEventListener(
|
||||
name: 'callLogEventSync',
|
||||
handler: (ev: CallLogEventSyncEvent) => void
|
||||
|
@ -3152,6 +3159,9 @@ export default class MessageReceiver
|
|||
if (syncMessage.callEvent) {
|
||||
return this.handleCallEvent(envelope, syncMessage.callEvent);
|
||||
}
|
||||
if (syncMessage.callLinkUpdate) {
|
||||
return this.handleCallLinkUpdate(envelope, syncMessage.callLinkUpdate);
|
||||
}
|
||||
if (syncMessage.callLogEvent) {
|
||||
return this.handleCallLogEvent(envelope, syncMessage.callLogEvent);
|
||||
}
|
||||
|
@ -3510,6 +3520,53 @@ export default class MessageReceiver
|
|||
log.info('handleCallEvent: finished');
|
||||
}
|
||||
|
||||
private async handleCallLinkUpdate(
|
||||
envelope: ProcessedEnvelope,
|
||||
callLinkUpdate: Proto.SyncMessage.ICallLinkUpdate
|
||||
): Promise<void> {
|
||||
const logId = getEnvelopeId(envelope);
|
||||
log.info('MessageReceiver.handleCallLinkUpdate', logId);
|
||||
|
||||
logUnexpectedUrgentValue(envelope, 'callLinkUpdateSync');
|
||||
|
||||
let callLinkUpdateSyncType: CallLinkUpdateSyncType;
|
||||
if (callLinkUpdate.type == null) {
|
||||
throw new Error('MessageReceiver.handleCallLinkUpdate: type was null');
|
||||
} else if (
|
||||
callLinkUpdate.type === Proto.SyncMessage.CallLinkUpdate.Type.UPDATE
|
||||
) {
|
||||
callLinkUpdateSyncType = CallLinkUpdateSyncType.Update;
|
||||
} else if (
|
||||
callLinkUpdate.type === Proto.SyncMessage.CallLinkUpdate.Type.DELETE
|
||||
) {
|
||||
callLinkUpdateSyncType = CallLinkUpdateSyncType.Delete;
|
||||
} else {
|
||||
throw new Error(
|
||||
`MessageReceiver.handleCallLinkUpdate: unknown type ${callLinkUpdate.type}`
|
||||
);
|
||||
}
|
||||
|
||||
const rootKey = Bytes.isNotEmpty(callLinkUpdate.rootKey)
|
||||
? callLinkUpdate.rootKey
|
||||
: undefined;
|
||||
const adminKey = Bytes.isNotEmpty(callLinkUpdate.adminPasskey)
|
||||
? callLinkUpdate.adminPasskey
|
||||
: undefined;
|
||||
|
||||
const ev = new CallLinkUpdateSyncEvent(
|
||||
{
|
||||
type: callLinkUpdateSyncType,
|
||||
rootKey,
|
||||
adminKey,
|
||||
},
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
);
|
||||
|
||||
await this.dispatchAndWait(logId, ev);
|
||||
|
||||
log.info('handleCallLinkUpdate: finished');
|
||||
}
|
||||
|
||||
private async handleCallLogEvent(
|
||||
envelope: ProcessedEnvelope,
|
||||
callLogEvent: Proto.SyncMessage.ICallLogEvent
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
} from './Types.d';
|
||||
import type { ContactDetailsWithAvatar } from './ContactsParser';
|
||||
import type { CallEventDetails, CallLogEvent } from '../types/CallDisposition';
|
||||
import type { CallLinkUpdateSyncType } from '../types/CallLink';
|
||||
|
||||
export class EmptyEvent extends Event {
|
||||
constructor() {
|
||||
|
@ -439,6 +440,21 @@ export class CallEventSyncEvent extends ConfirmableEvent {
|
|||
}
|
||||
}
|
||||
|
||||
export type CallLinkUpdateSyncEventData = Readonly<{
|
||||
type: CallLinkUpdateSyncType;
|
||||
rootKey: Uint8Array | undefined;
|
||||
adminKey: Uint8Array | undefined;
|
||||
}>;
|
||||
|
||||
export class CallLinkUpdateSyncEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly callLinkUpdate: CallLinkUpdateSyncEventData,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('callLinkUpdateSync', confirm);
|
||||
}
|
||||
}
|
||||
|
||||
export type CallLogEventSyncEventData = Readonly<{
|
||||
event: CallLogEvent;
|
||||
timestamp: number;
|
||||
|
|
|
@ -9,11 +9,13 @@ 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';
|
||||
|
||||
export enum CallType {
|
||||
Audio = 'Audio',
|
||||
Video = 'Video',
|
||||
Group = 'Group',
|
||||
Adhoc = 'Adhoc',
|
||||
}
|
||||
|
||||
export enum CallDirection {
|
||||
|
@ -45,30 +47,42 @@ export enum RemoteCallEvent {
|
|||
|
||||
export type CallEvent = LocalCallEvent | RemoteCallEvent;
|
||||
|
||||
export enum DirectCallStatus {
|
||||
export enum CallStatusValue {
|
||||
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,
|
||||
JoinedAdhoc = 'JoinedAdhoc',
|
||||
}
|
||||
|
||||
export enum DirectCallStatus {
|
||||
Pending = CallStatusValue.Pending,
|
||||
Accepted = CallStatusValue.Accepted,
|
||||
Missed = CallStatusValue.Missed,
|
||||
Declined = CallStatusValue.Declined,
|
||||
Deleted = CallStatusValue.Deleted,
|
||||
}
|
||||
|
||||
export enum GroupCallStatus {
|
||||
GenericGroupCall = CallStatusValue.GenericGroupCall,
|
||||
OutgoingRing = CallStatusValue.OutgoingRing,
|
||||
Ringing = CallStatusValue.Ringing,
|
||||
Joined = CallStatusValue.Joined,
|
||||
Accepted = CallStatusValue.Accepted,
|
||||
Missed = CallStatusValue.Missed,
|
||||
Declined = CallStatusValue.Declined,
|
||||
Deleted = CallStatusValue.Deleted,
|
||||
}
|
||||
|
||||
export enum AdhocCallStatus {
|
||||
Pending = DirectCallStatus.Pending,
|
||||
Joined = GroupCallStatus.Joined,
|
||||
Deleted = DirectCallStatus.Deleted,
|
||||
Pending = CallStatusValue.Pending,
|
||||
Joined = CallStatusValue.JoinedAdhoc,
|
||||
Deleted = CallStatusValue.Deleted,
|
||||
}
|
||||
|
||||
export type CallStatus = DirectCallStatus | GroupCallStatus | AdhocCallStatus;
|
||||
|
@ -177,11 +191,15 @@ export const callHistoryGroupSchema = z.object({
|
|||
}) satisfies z.ZodType<CallHistoryGroup>;
|
||||
|
||||
const peerIdInBytesSchema = z.instanceof(Uint8Array).transform(value => {
|
||||
// direct conversationId
|
||||
if (value.byteLength === UUID_BYTE_SIZE) {
|
||||
const uuid = bytesToUuid(value);
|
||||
if (uuid != null) {
|
||||
return uuid;
|
||||
}
|
||||
// assuming groupId
|
||||
}
|
||||
|
||||
// groupId or call link roomId
|
||||
return Bytes.toBase64(value);
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,16 @@ import type { ReadonlyDeep } from 'type-fest';
|
|||
import { z } from 'zod';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export enum CallLinkUpdateSyncType {
|
||||
Update = 'Update',
|
||||
Delete = 'Delete',
|
||||
}
|
||||
|
||||
export type CallLinkUpdateData = Readonly<{
|
||||
rootKey: Uint8Array;
|
||||
adminKey: Uint8Array | undefined;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Restrictions
|
||||
*/
|
||||
|
@ -33,10 +43,25 @@ export type CallLinkType = Readonly<{
|
|||
adminKey: string | null;
|
||||
name: string;
|
||||
restrictions: CallLinkRestrictions;
|
||||
// Revocation is not supported currently but still returned by the server
|
||||
revoked: boolean;
|
||||
// Guaranteed from RingRTC readCallLink, but locally may be null immediately after
|
||||
// CallLinkUpdate sync and before readCallLink
|
||||
expiration: number | null;
|
||||
}>;
|
||||
|
||||
export type CallLinkStateType = Pick<
|
||||
CallLinkType,
|
||||
'name' | 'restrictions' | 'revoked' | 'expiration'
|
||||
>;
|
||||
|
||||
export type ReadCallLinkState = Readonly<{
|
||||
name: string;
|
||||
restrictions: CallLinkRestrictions;
|
||||
revoked: boolean;
|
||||
expiration: number;
|
||||
}>;
|
||||
|
||||
// Ephemeral conversation-like type to satisfy components
|
||||
export type CallLinkConversationType = ReadonlyDeep<
|
||||
Omit<ConversationType, 'type'> & {
|
||||
|
@ -65,6 +90,6 @@ export const callLinkRecordSchema = z.object({
|
|||
// state
|
||||
name: z.string(),
|
||||
restrictions: callLinkRestrictionsSchema,
|
||||
expiration: z.number().int(),
|
||||
expiration: z.number().int().nullable(),
|
||||
revoked: z.union([z.literal(1), z.literal(0)]),
|
||||
}) satisfies z.ZodType<CallLinkRecord>;
|
||||
|
|
|
@ -59,13 +59,20 @@ import {
|
|||
callHistoryDetailsSchema,
|
||||
callDetailsSchema,
|
||||
AdhocCallStatus,
|
||||
CallStatusValue,
|
||||
} from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import { drop } from './drop';
|
||||
|
||||
// utils
|
||||
// -----
|
||||
|
||||
// call link roomIds are automatically redacted via redactCallLinkRoomIds()
|
||||
export function peerIdToLog(peerId: string, mode: CallMode): string {
|
||||
return mode === CallMode.Group ? `groupv2(${peerId})` : peerId;
|
||||
}
|
||||
|
||||
export function formatCallEvent(callEvent: CallEventDetails): string {
|
||||
const {
|
||||
callId,
|
||||
|
@ -78,14 +85,14 @@ export function formatCallEvent(callEvent: CallEventDetails): string {
|
|||
ringerId,
|
||||
timestamp,
|
||||
} = callEvent;
|
||||
const peerIdLog = mode === CallMode.Group ? `groupv2(${peerId})` : peerId;
|
||||
const peerIdLog = peerIdToLog(peerId, mode);
|
||||
return `CallEvent (${callId}, ${peerIdLog}, ${mode}, ${event}, ${direction}, ${type}, ${mode}, ${timestamp}, ${ringerId}, ${eventSource})`;
|
||||
}
|
||||
|
||||
export function formatCallHistory(callHistory: CallHistoryDetails): string {
|
||||
const { callId, peerId, direction, status, type, mode, timestamp, ringerId } =
|
||||
callHistory;
|
||||
const peerIdLog = mode === CallMode.Group ? `groupv2(${peerId})` : peerId;
|
||||
const peerIdLog = peerIdToLog(peerId, mode);
|
||||
return `CallHistory (${callId}, ${peerIdLog}, ${mode}, ${status}, ${direction}, ${type}, ${mode}, ${timestamp}, ${ringerId})`;
|
||||
}
|
||||
|
||||
|
@ -189,6 +196,8 @@ export function getCallEventForProto(
|
|||
type = CallType.Audio;
|
||||
} else if (callEvent.type === Proto.SyncMessage.CallEvent.Type.VIDEO_CALL) {
|
||||
type = CallType.Video;
|
||||
} else if (callEvent.type === Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL) {
|
||||
type = CallType.Adhoc;
|
||||
} else {
|
||||
throw new TypeError(`Unknown call type ${callEvent.type}`);
|
||||
}
|
||||
|
@ -196,6 +205,8 @@ export function getCallEventForProto(
|
|||
let mode: CallMode;
|
||||
if (type === CallType.Group) {
|
||||
mode = CallMode.Group;
|
||||
} else if (type === CallType.Adhoc) {
|
||||
mode = CallMode.Adhoc;
|
||||
} else {
|
||||
mode = CallMode.Direct;
|
||||
}
|
||||
|
@ -246,21 +257,23 @@ const typeToProto = {
|
|||
[CallType.Audio]: Proto.SyncMessage.CallEvent.Type.AUDIO_CALL,
|
||||
[CallType.Video]: Proto.SyncMessage.CallEvent.Type.VIDEO_CALL,
|
||||
[CallType.Group]: Proto.SyncMessage.CallEvent.Type.GROUP_CALL,
|
||||
[CallType.Adhoc]: Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL,
|
||||
};
|
||||
|
||||
const statusToProto: Record<
|
||||
CallStatus,
|
||||
Proto.SyncMessage.CallEvent.Event | null
|
||||
> = {
|
||||
[DirectCallStatus.Accepted]: Proto.SyncMessage.CallEvent.Event.ACCEPTED, // and GroupCallStatus.Accepted
|
||||
[DirectCallStatus.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED, // and GroupCallStatus.Declined
|
||||
[DirectCallStatus.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE, // and GroupCallStatus.Deleted
|
||||
[DirectCallStatus.Missed]: null, // and GroupCallStatus.Missed
|
||||
[DirectCallStatus.Pending]: null,
|
||||
[GroupCallStatus.GenericGroupCall]: null,
|
||||
[GroupCallStatus.Joined]: null,
|
||||
[GroupCallStatus.OutgoingRing]: null,
|
||||
[GroupCallStatus.Ringing]: null,
|
||||
[CallStatusValue.Accepted]: Proto.SyncMessage.CallEvent.Event.ACCEPTED,
|
||||
[CallStatusValue.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED,
|
||||
[CallStatusValue.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE,
|
||||
[CallStatusValue.Missed]: null,
|
||||
[CallStatusValue.Pending]: null,
|
||||
[CallStatusValue.GenericGroupCall]: null,
|
||||
[CallStatusValue.OutgoingRing]: null,
|
||||
[CallStatusValue.Ringing]: null,
|
||||
[CallStatusValue.Joined]: null,
|
||||
[CallStatusValue.JoinedAdhoc]: Proto.SyncMessage.CallEvent.Event.ACCEPTED,
|
||||
};
|
||||
|
||||
function shouldSyncStatus(callStatus: CallStatus) {
|
||||
|
@ -279,7 +292,10 @@ function getProtoForCallHistory(
|
|||
)}`
|
||||
);
|
||||
|
||||
let peerId = uuidToBytes(callHistory.peerId);
|
||||
let peerId =
|
||||
callHistory.mode === CallMode.Adhoc
|
||||
? Bytes.fromBase64(callHistory.peerId)
|
||||
: uuidToBytes(callHistory.peerId);
|
||||
if (peerId.length === 0) {
|
||||
peerId = Bytes.fromBase64(callHistory.peerId);
|
||||
}
|
||||
|
@ -479,8 +495,10 @@ export function getCallDetailsForAdhocCall(
|
|||
peerId,
|
||||
ringerId: null,
|
||||
mode: CallMode.Adhoc,
|
||||
type: CallType.Group,
|
||||
direction: CallDirection.Outgoing,
|
||||
type: CallType.Adhoc,
|
||||
// Direction is only outgoing when your action causes ringing for others.
|
||||
// As Adhoc calls do not support ringing, this is always incoming for now
|
||||
direction: CallDirection.Incoming,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
@ -884,6 +902,17 @@ async function updateLocalCallHistory(
|
|||
);
|
||||
}
|
||||
|
||||
export async function updateAdhocCallHistory(
|
||||
callEvent: CallEventDetails
|
||||
): Promise<void> {
|
||||
const callHistory = await updateLocalAdhocCallHistory(callEvent);
|
||||
if (!callHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
drop(updateRemoteCallHistory(callHistory));
|
||||
}
|
||||
|
||||
export async function updateLocalAdhocCallHistory(
|
||||
callEvent: CallEventDetails
|
||||
): Promise<CallHistoryDetails | null> {
|
||||
|
@ -900,11 +929,11 @@ export async function updateLocalAdhocCallHistory(
|
|||
|
||||
if (prevCallHistory != null) {
|
||||
log.info(
|
||||
'updateLocalAdhocCallHistory: Found previous call history:',
|
||||
'updateAdhocCallHistory: Found previous call history:',
|
||||
formatCallHistory(prevCallHistory)
|
||||
);
|
||||
} else {
|
||||
log.info('updateLocalAdhocCallHistory: No previous call history');
|
||||
log.info('updateAdhocCallHistory: No previous call history');
|
||||
}
|
||||
|
||||
let callHistory: CallHistoryDetails;
|
||||
|
@ -912,7 +941,7 @@ export async function updateLocalAdhocCallHistory(
|
|||
callHistory = transitionCallHistory(prevCallHistory, callEvent);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
"updateLocalAdhocCallHistory: Couldn't transition call history:",
|
||||
"updateAdhocCallHistory: Couldn't transition call history:",
|
||||
formatCallEvent(callEvent),
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
|
@ -923,17 +952,17 @@ export async function updateLocalAdhocCallHistory(
|
|||
callHistory.status === AdhocCallStatus.Pending ||
|
||||
callHistory.status === AdhocCallStatus.Joined ||
|
||||
callHistory.status === AdhocCallStatus.Deleted,
|
||||
`updateLocalAdhocCallHistory: callHistory must have adhoc status (was ${callHistory.status})`
|
||||
`updateAdhocCallHistory: callHistory must have adhoc status (was ${callHistory.status})`
|
||||
);
|
||||
|
||||
const isDeleted = callHistory.status === AdhocCallStatus.Deleted;
|
||||
if (prevCallHistory != null && isEqual(prevCallHistory, callHistory)) {
|
||||
log.info(
|
||||
'updateLocalAdhocCallHistory: Next call history is identical, skipping save'
|
||||
'updateAdhocCallHistory: Next call history is identical, skipping save'
|
||||
);
|
||||
} else {
|
||||
log.info(
|
||||
'updateLocalAdhocCallHistory: Saving call history:',
|
||||
'updateAdhocCallHistory: Saving call history:',
|
||||
formatCallHistory(callHistory)
|
||||
);
|
||||
await window.Signal.Data.saveCallHistory(callHistory);
|
||||
|
@ -1126,7 +1155,11 @@ export async function updateCallHistoryFromRemoteEvent(
|
|||
callEvent: CallEventDetails,
|
||||
receivedAtCounter: number
|
||||
): Promise<void> {
|
||||
if (callEvent.mode === CallMode.Direct || callEvent.mode === CallMode.Group) {
|
||||
await updateLocalCallHistory(callEvent, receivedAtCounter);
|
||||
} else if (callEvent.mode === CallMode.Adhoc) {
|
||||
await updateLocalAdhocCallHistory(callEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCallHistoryFromLocalEvent(
|
||||
|
|
|
@ -18,10 +18,18 @@ import type {
|
|||
} from '../types/CallLink';
|
||||
import {
|
||||
callLinkRecordSchema,
|
||||
CallLinkRestrictions,
|
||||
toCallLinkRestrictions,
|
||||
} from '../types/CallLink';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
export const CALL_LINK_DEFAULT_STATE = {
|
||||
name: '',
|
||||
restrictions: CallLinkRestrictions.Unknown,
|
||||
revoked: false,
|
||||
expiration: null,
|
||||
};
|
||||
|
||||
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
|
||||
return rootKey.deriveRoomId().toString('hex');
|
||||
}
|
||||
|
@ -81,6 +89,37 @@ export function callLinkToConversation(
|
|||
};
|
||||
}
|
||||
|
||||
export function getPlaceholderCallLinkConversation(
|
||||
roomId: string,
|
||||
i18n: LocalizerType
|
||||
): CallLinkConversationType {
|
||||
return {
|
||||
id: roomId,
|
||||
type: 'callLink',
|
||||
isMe: false,
|
||||
title: i18n('icu:calling__call-link-default-title'),
|
||||
sharedGroupNames: [],
|
||||
acceptedMessageRequest: true,
|
||||
badges: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function toRootKeyBytes(rootKey: string): Uint8Array {
|
||||
return CallLinkRootKey.parse(rootKey).bytes;
|
||||
}
|
||||
|
||||
export function fromRootKeyBytes(rootKey: Uint8Array): string {
|
||||
return CallLinkRootKey.fromBytes(rootKey as Buffer).toString();
|
||||
}
|
||||
|
||||
export function toAdminKeyBytes(adminKey: string): Uint8Array {
|
||||
return Bytes.fromBase64(adminKey);
|
||||
}
|
||||
|
||||
export function fromAdminKeyBytes(adminKey: Uint8Array): string {
|
||||
return Bytes.toBase64(adminKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* DB record conversions
|
||||
*/
|
||||
|
@ -90,12 +129,9 @@ export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
|
|||
throw new Error('CallLink.callLinkToRecord: rootKey is null');
|
||||
}
|
||||
|
||||
// rootKey has a RingRTC parsing function, adminKey is just bytes
|
||||
const rootKey = callLink.rootKey
|
||||
? CallLinkRootKey.parse(callLink.rootKey).bytes
|
||||
: null;
|
||||
const rootKey = toRootKeyBytes(callLink.rootKey);
|
||||
const adminKey = callLink.adminKey
|
||||
? Bytes.fromBase64(callLink.adminKey)
|
||||
? toAdminKeyBytes(callLink.adminKey)
|
||||
: null;
|
||||
return callLinkRecordSchema.parse({
|
||||
roomId: callLink.roomId,
|
||||
|
@ -109,11 +145,13 @@ export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
|
|||
}
|
||||
|
||||
export function callLinkFromRecord(record: CallLinkRecord): CallLinkType {
|
||||
if (record.rootKey == null) {
|
||||
throw new Error('CallLink.callLinkFromRecord: rootKey is null');
|
||||
}
|
||||
|
||||
// root keys in memory are strings for simplicity
|
||||
const rootKey = CallLinkRootKey.fromBytes(
|
||||
record.rootKey as Buffer
|
||||
).toString();
|
||||
const adminKey = record.adminKey ? Bytes.toBase64(record.adminKey) : null;
|
||||
const rootKey = fromRootKeyBytes(record.rootKey);
|
||||
const adminKey = record.adminKey ? fromAdminKeyBytes(record.adminKey) : null;
|
||||
return {
|
||||
roomId: record.roomId,
|
||||
rootKey,
|
||||
|
|
|
@ -177,7 +177,7 @@ export function getCallingIcon(
|
|||
}
|
||||
return 'video';
|
||||
}
|
||||
if (callType === CallType.Group) {
|
||||
if (callType === CallType.Group || callType === CallType.Adhoc) {
|
||||
return 'video';
|
||||
}
|
||||
throw missingCaseError(callType);
|
||||
|
|
|
@ -65,6 +65,7 @@ export const sendTypesEnum = z.enum([
|
|||
'viewOnceSync',
|
||||
'viewSync',
|
||||
'callEventSync',
|
||||
'callLinkUpdateSync',
|
||||
'callLogEventSync',
|
||||
|
||||
// No longer used, all non-urgent
|
||||
|
|
|
@ -3,23 +3,33 @@
|
|||
|
||||
import type { CallEventSyncEvent } from '../textsecure/messageReceiverEvents';
|
||||
import * as log from '../logging/log';
|
||||
import { updateCallHistoryFromRemoteEvent } from './callDisposition';
|
||||
import {
|
||||
peerIdToLog,
|
||||
updateCallHistoryFromRemoteEvent,
|
||||
} from './callDisposition';
|
||||
import { CallMode } from '../types/Calling';
|
||||
|
||||
export async function onCallEventSync(
|
||||
syncEvent: CallEventSyncEvent
|
||||
): Promise<void> {
|
||||
const { callEvent, confirm } = syncEvent;
|
||||
const { callEventDetails, receivedAtCounter } = callEvent;
|
||||
const { peerId } = callEventDetails;
|
||||
|
||||
if (
|
||||
callEventDetails.mode === CallMode.Direct ||
|
||||
callEventDetails.mode === CallMode.Group
|
||||
) {
|
||||
const { peerId } = callEventDetails;
|
||||
const conversation = window.ConversationController.get(peerId);
|
||||
|
||||
if (!conversation) {
|
||||
const peerIdLog = peerIdToLog(peerId, callEventDetails.mode);
|
||||
log.warn(
|
||||
`onCallEventSync: No conversation found for conversationId ${peerId}`
|
||||
`onCallEventSync: No conversation found for conversationId ${peerIdLog}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await updateCallHistoryFromRemoteEvent(callEventDetails, receivedAtCounter);
|
||||
confirm();
|
||||
|
|
57
ts/util/onCallLinkUpdateSync.ts
Normal file
57
ts/util/onCallLinkUpdateSync.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||
import type { CallLinkUpdateSyncEvent } from '../textsecure/messageReceiverEvents';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { fromAdminKeyBytes, getRoomIdFromRootKey } from './callLinks';
|
||||
import { strictAssert } from './assert';
|
||||
import { CallLinkUpdateSyncType } from '../types/CallLink';
|
||||
|
||||
export async function onCallLinkUpdateSync(
|
||||
syncEvent: CallLinkUpdateSyncEvent
|
||||
): Promise<void> {
|
||||
const { callLinkUpdate, confirm } = syncEvent;
|
||||
const { type, rootKey, adminKey } = callLinkUpdate;
|
||||
|
||||
if (!rootKey) {
|
||||
log.warn('onCallLinkUpdateSync: Missing rootKey, invalid sync message');
|
||||
return;
|
||||
}
|
||||
|
||||
let callLinkRootKey: CallLinkRootKey;
|
||||
let roomId: string;
|
||||
try {
|
||||
callLinkRootKey = CallLinkRootKey.fromBytes(rootKey as Buffer);
|
||||
roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||
strictAssert(
|
||||
roomId,
|
||||
'onCallLinkUpdateSync: roomId is required in sync message'
|
||||
);
|
||||
} catch (err) {
|
||||
log.error('onCallLinkUpdateSync: Could not parse root key');
|
||||
return;
|
||||
}
|
||||
|
||||
const logId = `onCallLinkUpdateSync(${roomId}, ${type})`;
|
||||
log.info(`${logId}: Processing`);
|
||||
|
||||
try {
|
||||
if (type === CallLinkUpdateSyncType.Update) {
|
||||
const rootKeyString = callLinkRootKey.toString();
|
||||
const adminKeyString = adminKey ? fromAdminKeyBytes(adminKey) : null;
|
||||
window.reduxActions.calling.handleCallLinkUpdate({
|
||||
rootKey: rootKeyString,
|
||||
adminKey: adminKeyString,
|
||||
});
|
||||
} else if (type === CallLinkUpdateSyncType.Delete) {
|
||||
// TODO: DESKTOP-6951
|
||||
log.warn(`${logId}: Deleting call links is not supported`);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error(`${logId}: Failed to process`, Errors.toLogFormat(err));
|
||||
}
|
||||
|
||||
confirm();
|
||||
}
|
82
ts/util/sendCallLinkUpdateSync.ts
Normal file
82
ts/util/sendCallLinkUpdateSync.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import * as Bytes from '../Bytes';
|
||||
import { CallLinkUpdateSyncType } from '../types/CallLink';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import MessageSender from '../textsecure/SendMessage';
|
||||
import { toAdminKeyBytes, toRootKeyBytes } from './callLinks';
|
||||
|
||||
export type sendCallLinkUpdateSyncCallLinkType = {
|
||||
rootKey: string;
|
||||
adminKey: string | null;
|
||||
};
|
||||
|
||||
export async function sendCallLinkUpdateSync(
|
||||
callLink: sendCallLinkUpdateSyncCallLinkType
|
||||
): Promise<void> {
|
||||
return _sendCallLinkUpdateSync(callLink, CallLinkUpdateSyncType.Update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Underlying sync message is CallLinkUpdate with type set to DELETE.
|
||||
*/
|
||||
export async function sendCallLinkDeleteSync(
|
||||
callLink: sendCallLinkUpdateSyncCallLinkType
|
||||
): Promise<void> {
|
||||
return _sendCallLinkUpdateSync(callLink, CallLinkUpdateSyncType.Delete);
|
||||
}
|
||||
|
||||
async function _sendCallLinkUpdateSync(
|
||||
callLink: sendCallLinkUpdateSyncCallLinkType,
|
||||
type: CallLinkUpdateSyncType
|
||||
): Promise<void> {
|
||||
let protoType: Proto.SyncMessage.CallLinkUpdate.Type;
|
||||
if (type === CallLinkUpdateSyncType.Update) {
|
||||
protoType = Proto.SyncMessage.CallLinkUpdate.Type.UPDATE;
|
||||
} else if (type === CallLinkUpdateSyncType.Delete) {
|
||||
protoType = Proto.SyncMessage.CallLinkUpdate.Type.DELETE;
|
||||
} else {
|
||||
throw new Error(`sendCallLinkUpdateSync: unknown type ${type}`);
|
||||
}
|
||||
|
||||
log.info(`sendCallLinkUpdateSync: Sending CallLinkUpdate type=${type}`);
|
||||
|
||||
try {
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
|
||||
const callLinkUpdate = new Proto.SyncMessage.CallLinkUpdate({
|
||||
type: protoType,
|
||||
rootKey: toRootKeyBytes(callLink.rootKey),
|
||||
adminPasskey: callLink.adminKey
|
||||
? toAdminKeyBytes(callLink.adminKey)
|
||||
: null,
|
||||
});
|
||||
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.callLinkUpdate = callLinkUpdate;
|
||||
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await singleProtoJobQueue.add({
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
serviceId: ourAci,
|
||||
isSyncMessage: true,
|
||||
protoBase64: Bytes.toBase64(
|
||||
Proto.Content.encode(contentMessage).finish()
|
||||
),
|
||||
type: 'callLinkUpdateSync',
|
||||
urgent: false,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'sendCallLinkUpdateSync: Failed to queue sync message:',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue