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",
|
"messageformat": "Declined {type, select, Audio {voice} Video {video} Group {group} other {}} call",
|
||||||
"description": "Call History > Short description of call > When call was declined"
|
"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": {
|
"icu:TypingBubble__avatar--overflow-count": {
|
||||||
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.",
|
"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."
|
"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 {
|
message CallLinkUpdate {
|
||||||
optional bytes rootKey = 1;
|
enum Type {
|
||||||
|
UPDATE = 0;
|
||||||
|
DELETE = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes rootKey = 1;
|
||||||
optional bytes adminPasskey = 2;
|
optional bytes adminPasskey = 2;
|
||||||
|
optional Type type = 3; // defaults to UPDATE
|
||||||
}
|
}
|
||||||
|
|
||||||
message CallLogEvent {
|
message CallLogEvent {
|
||||||
|
|
|
@ -199,6 +199,7 @@ import {
|
||||||
import { deriveStorageServiceKey } from './Crypto';
|
import { deriveStorageServiceKey } from './Crypto';
|
||||||
import { getThemeType } from './util/getThemeType';
|
import { getThemeType } from './util/getThemeType';
|
||||||
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
||||||
|
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
|
||||||
|
|
||||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||||
|
@ -681,6 +682,10 @@ export async function startApp(): Promise<void> {
|
||||||
'callEventSync',
|
'callEventSync',
|
||||||
queuedEventListener(onCallEventSync, false)
|
queuedEventListener(onCallEventSync, false)
|
||||||
);
|
);
|
||||||
|
messageReceiver.addEventListener(
|
||||||
|
'callLinkUpdateSync',
|
||||||
|
queuedEventListener(onCallLinkUpdateSync, false)
|
||||||
|
);
|
||||||
messageReceiver.addEventListener(
|
messageReceiver.addEventListener(
|
||||||
'callLogEventSync',
|
'callLogEventSync',
|
||||||
queuedEventListener(onCallLogEventSync, false)
|
queuedEventListener(onCallLogEventSync, false)
|
||||||
|
|
|
@ -44,9 +44,11 @@ import { CallsNewCallButton } from './CallsNewCall';
|
||||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
import type { CallingConversationType } from '../types/Calling';
|
import type { CallingConversationType } from '../types/Calling';
|
||||||
import { CallMode } from '../types/Calling';
|
|
||||||
import type { CallLinkType } from '../types/CallLink';
|
import type { CallLinkType } from '../types/CallLink';
|
||||||
import { callLinkToConversation } from '../util/callLinks';
|
import {
|
||||||
|
callLinkToConversation,
|
||||||
|
getPlaceholderCallLinkConversation,
|
||||||
|
} from '../util/callLinks';
|
||||||
|
|
||||||
function Timestamp({
|
function Timestamp({
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -299,6 +301,26 @@ export function CallsList({
|
||||||
[searchState]
|
[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(
|
const isRowLoaded = useCallback(
|
||||||
(props: Index) => {
|
(props: Index) => {
|
||||||
return searchState.results?.items[props.index] != null;
|
return searchState.results?.items[props.index] != null;
|
||||||
|
@ -309,16 +331,11 @@ export function CallsList({
|
||||||
const rowRenderer = useCallback(
|
const rowRenderer = useCallback(
|
||||||
({ key, index, style }: ListRowProps) => {
|
({ key, index, style }: ListRowProps) => {
|
||||||
const item = searchState.results?.items.at(index) ?? null;
|
const item = searchState.results?.items.at(index) ?? null;
|
||||||
const isAdhoc = item?.mode === CallMode.Adhoc;
|
const conversation = getConversationForItem(item);
|
||||||
const callLink = isAdhoc ? getCallLink(item.peerId) : null;
|
const isAdhoc = item?.type === CallType.Adhoc;
|
||||||
|
const isNewCallVisible = Boolean(
|
||||||
const conversation: CallingConversationType | null =
|
!isAdhoc || (isAdhoc && getCallLink(item.peerId))
|
||||||
// eslint-disable-next-line no-nested-ternary
|
);
|
||||||
item
|
|
||||||
? callLink
|
|
||||||
? callLinkToConversation(callLink, i18n)
|
|
||||||
: getConversation(item.peerId) ?? null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
searchState.state === 'pending' ||
|
searchState.state === 'pending' ||
|
||||||
|
@ -354,7 +371,7 @@ export function CallsList({
|
||||||
let statusText;
|
let statusText;
|
||||||
if (wasMissed) {
|
if (wasMissed) {
|
||||||
statusText = i18n('icu:CallsList__ItemCallInfo--Missed');
|
statusText = i18n('icu:CallsList__ItemCallInfo--Missed');
|
||||||
} else if (callLink) {
|
} else if (isAdhoc) {
|
||||||
statusText = i18n('icu:CallsList__ItemCallInfo--CallLink');
|
statusText = i18n('icu:CallsList__ItemCallInfo--CallLink');
|
||||||
} else if (item.type === CallType.Group) {
|
} else if (item.type === CallType.Group) {
|
||||||
statusText = i18n('icu:CallsList__ItemCallInfo--GroupCall');
|
statusText = i18n('icu:CallsList__ItemCallInfo--GroupCall');
|
||||||
|
@ -393,21 +410,23 @@ export function CallsList({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
trailing={
|
trailing={
|
||||||
<CallsNewCallButton
|
isNewCallVisible ? (
|
||||||
callType={item.type}
|
<CallsNewCallButton
|
||||||
hasActiveCall={hasActiveCall}
|
callType={item.type}
|
||||||
onClick={() => {
|
hasActiveCall={hasActiveCall}
|
||||||
if (callLink) {
|
onClick={() => {
|
||||||
startCallLinkLobbyByRoomId(callLink.roomId);
|
if (isAdhoc) {
|
||||||
} else if (conversation) {
|
startCallLinkLobbyByRoomId(item.peerId);
|
||||||
if (item.type === CallType.Audio) {
|
} else if (conversation) {
|
||||||
onOutgoingAudioCallInConversation(conversation.id);
|
if (item.type === CallType.Audio) {
|
||||||
} else {
|
onOutgoingAudioCallInConversation(conversation.id);
|
||||||
onOutgoingVideoCallInConversation(conversation.id);
|
} else {
|
||||||
|
onOutgoingVideoCallInConversation(conversation.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
) : undefined
|
||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
<span
|
<span
|
||||||
|
@ -426,10 +445,13 @@ export function CallsList({
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (callLink) {
|
if (isAdhoc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (conversation == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onSelectCallHistoryGroup(conversation.id, item);
|
onSelectCallHistoryGroup(conversation.id, item);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -440,7 +462,7 @@ export function CallsList({
|
||||||
hasActiveCall,
|
hasActiveCall,
|
||||||
searchState,
|
searchState,
|
||||||
getCallLink,
|
getCallLink,
|
||||||
getConversation,
|
getConversationForItem,
|
||||||
selectedCallHistoryGroup,
|
selectedCallHistoryGroup,
|
||||||
onSelectCallHistoryGroup,
|
onSelectCallHistoryGroup,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
|
|
|
@ -72,6 +72,10 @@ function describeCallHistory(
|
||||||
direction: CallDirection,
|
direction: CallDirection,
|
||||||
status: CallStatus
|
status: CallStatus
|
||||||
): string {
|
): string {
|
||||||
|
if (type === CallType.Adhoc) {
|
||||||
|
return i18n('icu:CallHistory__Description--Adhoc');
|
||||||
|
}
|
||||||
|
|
||||||
if (status === DirectCallStatus.Missed || status === GroupCallStatus.Missed) {
|
if (status === DirectCallStatus.Missed || status === GroupCallStatus.Missed) {
|
||||||
if (direction === CallDirection.Incoming) {
|
if (direction === CallDirection.Incoming) {
|
||||||
return i18n('icu:CallHistory__Description--Missed', { type });
|
return i18n('icu:CallHistory__Description--Missed', { type });
|
||||||
|
|
|
@ -585,7 +585,8 @@ async function getCallLinkPreview(
|
||||||
}
|
}
|
||||||
|
|
||||||
const callLinkRootKey = CallLinkRootKey.parse(parsedUrl.args.key);
|
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) {
|
if (!callLinkState || callLinkState.revoked) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ipcRenderer } from 'electron';
|
||||||
import type {
|
import type {
|
||||||
AudioDevice,
|
AudioDevice,
|
||||||
CallId,
|
CallId,
|
||||||
CallLinkState,
|
CallLinkState as RingRTCCallLinkState,
|
||||||
DeviceId,
|
DeviceId,
|
||||||
GroupCallObserver,
|
GroupCallObserver,
|
||||||
PeekInfo,
|
PeekInfo,
|
||||||
|
@ -46,7 +46,6 @@ import Long from 'long';
|
||||||
import type { CallLinkAuthCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
|
import type { CallLinkAuthCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
|
||||||
import type {
|
import type {
|
||||||
ActionsType as CallingReduxActionsType,
|
ActionsType as CallingReduxActionsType,
|
||||||
CallLinkStateType,
|
|
||||||
GroupCallParticipantInfoType,
|
GroupCallParticipantInfoType,
|
||||||
GroupCallPeekInfoType,
|
GroupCallPeekInfoType,
|
||||||
} from '../state/ducks/calling';
|
} from '../state/ducks/calling';
|
||||||
|
@ -120,7 +119,7 @@ import {
|
||||||
getCallIdFromRing,
|
getCallIdFromRing,
|
||||||
getLocalCallEventFromRingUpdate,
|
getLocalCallEventFromRingUpdate,
|
||||||
convertJoinState,
|
convertJoinState,
|
||||||
updateLocalAdhocCallHistory,
|
updateAdhocCallHistory,
|
||||||
getCallIdFromEra,
|
getCallIdFromEra,
|
||||||
getCallDetailsForAdhocCall,
|
getCallDetailsForAdhocCall,
|
||||||
} from '../util/callDisposition';
|
} from '../util/callDisposition';
|
||||||
|
@ -134,6 +133,7 @@ import {
|
||||||
} from '../util/callLinks';
|
} from '../util/callLinks';
|
||||||
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
|
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
|
||||||
import { conversationJobQueue } from '../jobs/conversationJobQueue';
|
import { conversationJobQueue } from '../jobs/conversationJobQueue';
|
||||||
|
import type { ReadCallLinkState } from '../types/CallLink';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
processGroupCallRingCancellation,
|
processGroupCallRingCancellation,
|
||||||
|
@ -173,6 +173,7 @@ type CallingReduxInterface = Pick<
|
||||||
| 'groupCallEnded'
|
| 'groupCallEnded'
|
||||||
| 'groupCallRaisedHandsChange'
|
| 'groupCallRaisedHandsChange'
|
||||||
| 'groupCallStateChange'
|
| 'groupCallStateChange'
|
||||||
|
| 'joinedAdhocCall'
|
||||||
| 'outgoingCall'
|
| 'outgoingCall'
|
||||||
| 'receiveGroupCallReactions'
|
| 'receiveGroupCallReactions'
|
||||||
| 'receiveIncomingDirectCall'
|
| 'receiveIncomingDirectCall'
|
||||||
|
@ -538,7 +539,16 @@ export class CallingClass {
|
||||||
callLinkRootKey,
|
callLinkRootKey,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
callLinkRootKey: CallLinkRootKey;
|
callLinkRootKey: CallLinkRootKey;
|
||||||
}>): Promise<CallLinkState | undefined> {
|
}>): Promise<
|
||||||
|
| {
|
||||||
|
callLinkState: ReadCallLinkState;
|
||||||
|
errorStatusCode: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
callLinkState: undefined;
|
||||||
|
errorStatusCode: number;
|
||||||
|
}
|
||||||
|
> {
|
||||||
if (!this._sfuUrl) {
|
if (!this._sfuUrl) {
|
||||||
throw new Error('readCallLink() missing SFU URL; not handling call link');
|
throw new Error('readCallLink() missing SFU URL; not handling call link');
|
||||||
}
|
}
|
||||||
|
@ -555,11 +565,17 @@ export class CallingClass {
|
||||||
);
|
);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
log.warn(`${logId}: failed`);
|
log.warn(`${logId}: failed`);
|
||||||
return;
|
return {
|
||||||
|
callLinkState: undefined,
|
||||||
|
errorStatusCode: result.errorStatusCode,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`${logId}: success`);
|
log.info(`${logId}: success`);
|
||||||
return result.value;
|
return {
|
||||||
|
callLinkState: this.formatCallLinkStateForRedux(result.value),
|
||||||
|
errorStatusCode: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async startCallLinkLobby({
|
async startCallLinkLobby({
|
||||||
|
@ -1050,9 +1066,13 @@ export class CallingClass {
|
||||||
eraId
|
eraId
|
||||||
) {
|
) {
|
||||||
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
||||||
if (callMode === CallMode.Group) {
|
drop(
|
||||||
drop(this.sendGroupCallUpdateMessage(conversationId, eraId));
|
this.onGroupCallJoined({
|
||||||
}
|
peerId: conversationId,
|
||||||
|
eraId,
|
||||||
|
callMode,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1126,10 +1146,13 @@ export class CallingClass {
|
||||||
eraId
|
eraId
|
||||||
) {
|
) {
|
||||||
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
||||||
|
drop(
|
||||||
if (callMode === CallMode.Group) {
|
this.onGroupCallJoined({
|
||||||
drop(this.sendGroupCallUpdateMessage(conversationId, eraId));
|
peerId: conversationId,
|
||||||
}
|
eraId,
|
||||||
|
callMode,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For adhoc calls, conversationId will be a roomId
|
// 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({
|
public async joinCallLinkCall({
|
||||||
roomId,
|
roomId,
|
||||||
rootKey,
|
rootKey,
|
||||||
|
@ -1365,8 +1404,8 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
public formatCallLinkStateForRedux(
|
public formatCallLinkStateForRedux(
|
||||||
callLinkState: CallLinkState
|
callLinkState: RingRTCCallLinkState
|
||||||
): CallLinkStateType {
|
): ReadCallLinkState {
|
||||||
const { name, restrictions, expiration, revoked } = callLinkState;
|
const { name, restrictions, expiration, revoked } = callLinkState;
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
@ -2658,7 +2697,7 @@ export class CallingClass {
|
||||||
LocalCallEvent.Accepted,
|
LocalCallEvent.Accepted,
|
||||||
'CallingClass.updateCallHistoryForGroupCallOnLocalChanged'
|
'CallingClass.updateCallHistoryForGroupCallOnLocalChanged'
|
||||||
);
|
);
|
||||||
await updateLocalAdhocCallHistory(callEvent);
|
await updateAdhocCallHistory(callEvent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
'CallingClass.updateCallHistoryForGroupCallOnLocalChanged: Error updating state',
|
'CallingClass.updateCallHistoryForGroupCallOnLocalChanged: Error updating state',
|
||||||
|
|
|
@ -29,7 +29,7 @@ import type {
|
||||||
CallHistoryGroup,
|
CallHistoryGroup,
|
||||||
CallHistoryPagination,
|
CallHistoryPagination,
|
||||||
} from '../types/CallDisposition';
|
} from '../types/CallDisposition';
|
||||||
import type { CallLinkType, CallLinkRestrictions } from '../types/CallLink';
|
import type { CallLinkStateType, CallLinkType } from '../types/CallLink';
|
||||||
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
||||||
|
|
||||||
export type AdjacentMessagesByConversationOptionsType = Readonly<{
|
export type AdjacentMessagesByConversationOptionsType = Readonly<{
|
||||||
|
@ -681,13 +681,15 @@ export type DataInterface = {
|
||||||
>;
|
>;
|
||||||
callLinkExists(roomId: string): Promise<boolean>;
|
callLinkExists(roomId: string): Promise<boolean>;
|
||||||
getAllCallLinks: () => Promise<ReadonlyArray<CallLinkType>>;
|
getAllCallLinks: () => Promise<ReadonlyArray<CallLinkType>>;
|
||||||
|
getCallLinkByRoomId: (roomId: string) => Promise<CallLinkType | undefined>;
|
||||||
insertCallLink(callLink: CallLinkType): Promise<void>;
|
insertCallLink(callLink: CallLinkType): Promise<void>;
|
||||||
|
updateCallLinkAdminKeyByRoomId(
|
||||||
|
roomId: string,
|
||||||
|
adminKey: string
|
||||||
|
): Promise<void>;
|
||||||
updateCallLinkState(
|
updateCallLinkState(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
name: string,
|
callLinkState: CallLinkStateType
|
||||||
restrictions: CallLinkRestrictions,
|
|
||||||
expiration: number | null,
|
|
||||||
revoked: boolean
|
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
migrateConversationMessages: (
|
migrateConversationMessages: (
|
||||||
obsoleteId: string,
|
obsoleteId: string,
|
||||||
|
|
|
@ -169,7 +169,9 @@ import {
|
||||||
import {
|
import {
|
||||||
callLinkExists,
|
callLinkExists,
|
||||||
getAllCallLinks,
|
getAllCallLinks,
|
||||||
|
getCallLinkByRoomId,
|
||||||
insertCallLink,
|
insertCallLink,
|
||||||
|
updateCallLinkAdminKeyByRoomId,
|
||||||
updateCallLinkState,
|
updateCallLinkState,
|
||||||
} from './server/callLinks';
|
} from './server/callLinks';
|
||||||
import { CallMode } from '../types/Calling';
|
import { CallMode } from '../types/Calling';
|
||||||
|
@ -341,7 +343,9 @@ const dataInterface: ServerInterface = {
|
||||||
getRecentStaleRingsAndMarkOlderMissed,
|
getRecentStaleRingsAndMarkOlderMissed,
|
||||||
callLinkExists,
|
callLinkExists,
|
||||||
getAllCallLinks,
|
getAllCallLinks,
|
||||||
|
getCallLinkByRoomId,
|
||||||
insertCallLink,
|
insertCallLink,
|
||||||
|
updateCallLinkAdminKeyByRoomId,
|
||||||
updateCallLinkState,
|
updateCallLinkState,
|
||||||
migrateConversationMessages,
|
migrateConversationMessages,
|
||||||
getMessagesBetween,
|
getMessagesBetween,
|
||||||
|
|
|
@ -3,12 +3,16 @@
|
||||||
|
|
||||||
import type { Database } from '@signalapp/better-sqlite3';
|
import type { Database } from '@signalapp/better-sqlite3';
|
||||||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||||
import type { CallLinkRestrictions, CallLinkType } from '../../types/CallLink';
|
import type { CallLinkStateType, CallLinkType } from '../../types/CallLink';
|
||||||
import {
|
import {
|
||||||
callLinkRestrictionsSchema,
|
callLinkRestrictionsSchema,
|
||||||
callLinkRecordSchema,
|
callLinkRecordSchema,
|
||||||
} from '../../types/CallLink';
|
} from '../../types/CallLink';
|
||||||
import { callLinkToRecord, callLinkFromRecord } from '../../util/callLinks';
|
import {
|
||||||
|
callLinkToRecord,
|
||||||
|
callLinkFromRecord,
|
||||||
|
toAdminKeyBytes,
|
||||||
|
} from '../../util/callLinks';
|
||||||
import { getReadonlyInstance, getWritableInstance, prepare } from '../Server';
|
import { getReadonlyInstance, getWritableInstance, prepare } from '../Server';
|
||||||
import { sql } from '../util';
|
import { sql } from '../util';
|
||||||
import { strictAssert } from '../../util/assert';
|
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;
|
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>> {
|
export async function getAllCallLinks(): Promise<ReadonlyArray<CallLinkType>> {
|
||||||
const db = getReadonlyInstance();
|
const db = getReadonlyInstance();
|
||||||
const [query] = sql`
|
const [query] = sql`
|
||||||
|
@ -70,11 +91,9 @@ export async function insertCallLink(callLink: CallLinkType): Promise<void> {
|
||||||
|
|
||||||
export async function updateCallLinkState(
|
export async function updateCallLinkState(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
name: string,
|
callLinkState: CallLinkStateType
|
||||||
restrictions: CallLinkRestrictions,
|
|
||||||
expiration: number,
|
|
||||||
revoked: boolean
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { name, restrictions, expiration, revoked } = callLinkState;
|
||||||
const db = await getWritableInstance();
|
const db = await getWritableInstance();
|
||||||
const restrictionsValue = callLinkRestrictionsSchema.parse(restrictions);
|
const restrictionsValue = callLinkRestrictionsSchema.parse(restrictions);
|
||||||
const [query, params] = sql`
|
const [query, params] = sql`
|
||||||
|
@ -89,6 +108,22 @@ export async function updateCallLinkState(
|
||||||
db.prepare(query).run(params);
|
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 {
|
function assertRoomIdMatchesRootKey(roomId: string, rootKey: string): void {
|
||||||
const derivedRoomId = CallLinkRootKey.parse(rootKey)
|
const derivedRoomId = CallLinkRootKey.parse(rootKey)
|
||||||
.deriveRoomId()
|
.deriveRoomId()
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
hasScreenCapturePermission,
|
hasScreenCapturePermission,
|
||||||
openSystemPreferences,
|
openSystemPreferences,
|
||||||
} from 'mac-screen-capture-permissions';
|
} from 'mac-screen-capture-permissions';
|
||||||
import { omit } from 'lodash';
|
import { omit, pick } from 'lodash';
|
||||||
import type { ReadonlyDeep } from 'type-fest';
|
import type { ReadonlyDeep } from 'type-fest';
|
||||||
import {
|
import {
|
||||||
CallLinkRootKey,
|
CallLinkRootKey,
|
||||||
|
@ -32,7 +32,7 @@ import type {
|
||||||
PresentedSource,
|
PresentedSource,
|
||||||
PresentableSource,
|
PresentableSource,
|
||||||
} from '../../types/Calling';
|
} from '../../types/Calling';
|
||||||
import type { CallLinkRestrictions } from '../../types/CallLink';
|
import type { CallLinkStateType } from '../../types/CallLink';
|
||||||
import {
|
import {
|
||||||
CALLING_REACTIONS_LIFETIME,
|
CALLING_REACTIONS_LIFETIME,
|
||||||
MAX_CALLING_REACTIONS,
|
MAX_CALLING_REACTIONS,
|
||||||
|
@ -46,7 +46,11 @@ import {
|
||||||
} from '../../types/Calling';
|
} from '../../types/Calling';
|
||||||
import { callingTones } from '../../util/callingTones';
|
import { callingTones } from '../../util/callingTones';
|
||||||
import { requestCameraPermissions } from '../../util/callingPermissions';
|
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 { sleep } from '../../util/sleep';
|
||||||
import { LatestQueue } from '../../util/LatestQueue';
|
import { LatestQueue } from '../../util/LatestQueue';
|
||||||
import type { AciString } from '../../types/ServiceId';
|
import type { AciString } from '../../types/ServiceId';
|
||||||
|
@ -168,13 +172,6 @@ export type AdhocCallsType = {
|
||||||
[roomId: string]: GroupCallStateType;
|
[roomId: string]: GroupCallStateType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CallLinkStateType = ReadonlyDeep<{
|
|
||||||
name: string;
|
|
||||||
restrictions: CallLinkRestrictions;
|
|
||||||
expiration: number | null;
|
|
||||||
revoked: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type CallLinksByRoomIdStateType = ReadonlyDeep<
|
export type CallLinksByRoomIdStateType = ReadonlyDeep<
|
||||||
CallLinkStateType & {
|
CallLinkStateType & {
|
||||||
rootKey: string;
|
rootKey: string;
|
||||||
|
@ -244,10 +241,20 @@ type GroupCallStateChangeActionPayloadType =
|
||||||
ourAci: AciString;
|
ourAci: AciString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HandleCallLinkUpdateActionPayloadType = ReadonlyDeep<{
|
||||||
|
roomId: string;
|
||||||
|
callLinkDetails: CallLinksByRoomIdStateType;
|
||||||
|
}>;
|
||||||
|
|
||||||
type HangUpActionPayloadType = ReadonlyDeep<{
|
type HangUpActionPayloadType = ReadonlyDeep<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type HandleCallLinkUpdateType = ReadonlyDeep<{
|
||||||
|
rootKey: string;
|
||||||
|
adminKey: string | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type IncomingDirectCallType = ReadonlyDeep<{
|
export type IncomingDirectCallType = ReadonlyDeep<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
isVideoCall: boolean;
|
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_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
|
||||||
const GROUP_CALL_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
|
const GROUP_CALL_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
|
||||||
const GROUP_CALL_REACTIONS_EXPIRED = 'calling/GROUP_CALL_REACTIONS_EXPIRED';
|
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 HANG_UP = 'calling/HANG_UP';
|
||||||
const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL';
|
const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL';
|
||||||
const INCOMING_GROUP_CALL = 'calling/INCOMING_GROUP_CALL';
|
const INCOMING_GROUP_CALL = 'calling/INCOMING_GROUP_CALL';
|
||||||
|
@ -699,6 +707,11 @@ type GroupCallReactionsExpiredActionType = ReadonlyDeep<{
|
||||||
payload: GroupCallReactionsExpiredActionPayloadType;
|
payload: GroupCallReactionsExpiredActionPayloadType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type HandleCallLinkUpdateActionType = ReadonlyDeep<{
|
||||||
|
type: 'calling/HANDLE_CALL_LINK_UPDATE';
|
||||||
|
payload: HandleCallLinkUpdateActionPayloadType;
|
||||||
|
}>;
|
||||||
|
|
||||||
type HangUpActionType = ReadonlyDeep<{
|
type HangUpActionType = ReadonlyDeep<{
|
||||||
type: 'calling/HANG_UP';
|
type: 'calling/HANG_UP';
|
||||||
payload: HangUpActionPayloadType;
|
payload: HangUpActionPayloadType;
|
||||||
|
@ -838,6 +851,7 @@ export type CallingActionType =
|
||||||
| GroupCallStateChangeActionType
|
| GroupCallStateChangeActionType
|
||||||
| GroupCallReactionsReceivedActionType
|
| GroupCallReactionsReceivedActionType
|
||||||
| GroupCallReactionsExpiredActionType
|
| GroupCallReactionsExpiredActionType
|
||||||
|
| HandleCallLinkUpdateActionType
|
||||||
| HangUpActionType
|
| HangUpActionType
|
||||||
| IncomingDirectCallActionType
|
| IncomingDirectCallActionType
|
||||||
| IncomingGroupCallActionType
|
| 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(
|
function hangUpActiveCall(
|
||||||
reason: string
|
reason: string
|
||||||
): ThunkAction<void, RootStateType, unknown, HangUpActionType> {
|
): 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(
|
function peekGroupCallForTheFirstTime(
|
||||||
conversationId: string
|
conversationId: string
|
||||||
): ThunkAction<void, RootStateType, unknown, PeekGroupCallFulfilledActionType> {
|
): ThunkAction<void, RootStateType, unknown, PeekGroupCallFulfilledActionType> {
|
||||||
|
@ -1712,7 +1807,8 @@ const _startCallLinkLobby = async ({
|
||||||
|
|
||||||
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||||
|
|
||||||
const callLinkState = await calling.readCallLink({ callLinkRootKey });
|
const readResult = await calling.readCallLink({ callLinkRootKey });
|
||||||
|
const { callLinkState } = readResult;
|
||||||
if (!callLinkState) {
|
if (!callLinkState) {
|
||||||
const i18n = getIntl(getState());
|
const i18n = getIntl(getState());
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -1725,7 +1821,10 @@ const _startCallLinkLobby = async ({
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (callLinkState.revoked || callLinkState.expiration < new Date()) {
|
if (
|
||||||
|
callLinkState.revoked ||
|
||||||
|
callLinkState.expiration < new Date().getTime()
|
||||||
|
) {
|
||||||
const i18n = getIntl(getState());
|
const i18n = getIntl(getState());
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SHOW_ERROR_MODAL,
|
type: SHOW_ERROR_MODAL,
|
||||||
|
@ -1739,21 +1838,13 @@ const _startCallLinkLobby = async ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||||
const formattedCallLinkState =
|
|
||||||
calling.formatCallLinkStateForRedux(callLinkState);
|
|
||||||
try {
|
try {
|
||||||
const { name, restrictions, expiration, revoked } = formattedCallLinkState;
|
|
||||||
const callLinkExists = await dataInterface.callLinkExists(roomId);
|
const callLinkExists = await dataInterface.callLinkExists(roomId);
|
||||||
if (callLinkExists) {
|
if (callLinkExists) {
|
||||||
await dataInterface.updateCallLinkState(
|
await dataInterface.updateCallLinkState(roomId, callLinkState);
|
||||||
roomId,
|
|
||||||
name,
|
|
||||||
restrictions,
|
|
||||||
expiration,
|
|
||||||
revoked
|
|
||||||
);
|
|
||||||
log.info('startCallLinkLobby: Updated existing call link', roomId);
|
log.info('startCallLinkLobby: Updated existing call link', roomId);
|
||||||
} else {
|
} else {
|
||||||
|
const { name, restrictions, expiration, revoked } = callLinkState;
|
||||||
await dataInterface.insertCallLink({
|
await dataInterface.insertCallLink({
|
||||||
roomId,
|
roomId,
|
||||||
rootKey,
|
rootKey,
|
||||||
|
@ -1790,7 +1881,7 @@ const _startCallLinkLobby = async ({
|
||||||
type: START_CALL_LINK_LOBBY,
|
type: START_CALL_LINK_LOBBY,
|
||||||
payload: {
|
payload: {
|
||||||
...callLobbyData,
|
...callLobbyData,
|
||||||
callLinkState: formattedCallLinkState,
|
callLinkState,
|
||||||
callLinkRootKey: rootKey,
|
callLinkRootKey: rootKey,
|
||||||
conversationId: roomId,
|
conversationId: roomId,
|
||||||
isConversationTooBigToRing: false,
|
isConversationTooBigToRing: false,
|
||||||
|
@ -1983,6 +2074,8 @@ export const actions = {
|
||||||
groupCallRaisedHandsChange,
|
groupCallRaisedHandsChange,
|
||||||
groupCallStateChange,
|
groupCallStateChange,
|
||||||
hangUpActiveCall,
|
hangUpActiveCall,
|
||||||
|
handleCallLinkUpdate,
|
||||||
|
joinedAdhocCall,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
openSystemPreferencesAction,
|
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;
|
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 { assert } from 'chai';
|
||||||
|
|
||||||
import { callLinkToRecord, callLinkFromRecord } from '../../util/callLinks';
|
import { callLinkToRecord, callLinkFromRecord } from '../../util/callLinks';
|
||||||
import type { CallLinkType } from '../../types/CallLink';
|
import {
|
||||||
import { CallLinkRestrictions } from '../../types/CallLink';
|
FAKE_CALL_LINK as CALL_LINK,
|
||||||
import { MONTH } from '../../util/durations/constants';
|
FAKE_CALL_LINK_WITH_ADMIN_KEY as CALL_LINK_WITH_ADMIN_KEY,
|
||||||
|
} from '../helpers/fakeCallLink';
|
||||||
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',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('callLinks', () => {
|
describe('callLinks', () => {
|
||||||
it('callLinkToRecord() and callLinkFromRecord() can convert to record and back', () => {
|
it('callLinkToRecord() and callLinkFromRecord() can convert to record and back', () => {
|
||||||
|
|
|
@ -321,21 +321,21 @@ describe('sql/getCallHistoryGroups', () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const roomId = generateUuid();
|
const roomId = generateUuid();
|
||||||
|
|
||||||
function toCall(callId: string, timestamp: number, type: CallType) {
|
function toCall(callId: string, timestamp: number) {
|
||||||
return {
|
return {
|
||||||
callId,
|
callId,
|
||||||
peerId: roomId,
|
peerId: roomId,
|
||||||
ringerId: null,
|
ringerId: null,
|
||||||
mode: CallMode.Adhoc,
|
mode: CallMode.Adhoc,
|
||||||
type,
|
type: CallType.Adhoc,
|
||||||
direction: CallDirection.Outgoing,
|
direction: CallDirection.Outgoing,
|
||||||
timestamp,
|
timestamp,
|
||||||
status: AdhocCallStatus.Joined,
|
status: AdhocCallStatus.Joined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const call1 = toCall('1', now - 10, CallType.Group);
|
const call1 = toCall('1', now - 10);
|
||||||
const call2 = toCall('2', now, CallType.Group);
|
const call2 = toCall('2', now);
|
||||||
|
|
||||||
await saveCallHistory(call1);
|
await saveCallHistory(call1);
|
||||||
await saveCallHistory(call2);
|
await saveCallHistory(call2);
|
||||||
|
@ -394,28 +394,23 @@ describe('sql/getCallHistoryGroupsCount', () => {
|
||||||
const roomId1 = generateUuid();
|
const roomId1 = generateUuid();
|
||||||
const roomId2 = generateUuid();
|
const roomId2 = generateUuid();
|
||||||
|
|
||||||
function toCall(
|
function toCall(callId: string, roomId: string, timestamp: number) {
|
||||||
callId: string,
|
|
||||||
roomId: string,
|
|
||||||
timestamp: number,
|
|
||||||
type: CallType
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
callId,
|
callId,
|
||||||
peerId: roomId,
|
peerId: roomId,
|
||||||
ringerId: null,
|
ringerId: null,
|
||||||
mode: CallMode.Adhoc,
|
mode: CallMode.Adhoc,
|
||||||
type,
|
type: CallType.Adhoc,
|
||||||
direction: CallDirection.Outgoing,
|
direction: CallDirection.Outgoing,
|
||||||
timestamp,
|
timestamp,
|
||||||
status: AdhocCallStatus.Joined,
|
status: AdhocCallStatus.Joined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const call1 = toCall('1', roomId1, now - 20, CallType.Group);
|
const call1 = toCall('1', roomId1, now - 20);
|
||||||
const call2 = toCall('2', roomId1, now - 10, CallType.Group);
|
const call2 = toCall('2', roomId1, now - 10);
|
||||||
const call3 = toCall('3', roomId1, now, CallType.Group);
|
const call3 = toCall('3', roomId1, now);
|
||||||
const call4 = toCall('4', roomId2, now, CallType.Group);
|
const call4 = toCall('4', roomId2, now);
|
||||||
|
|
||||||
await saveCallHistory(call1);
|
await saveCallHistory(call1);
|
||||||
await saveCallHistory(call2);
|
await saveCallHistory(call2);
|
||||||
|
|
|
@ -36,6 +36,8 @@ import {
|
||||||
import { generateAci } from '../../../types/ServiceId';
|
import { generateAci } from '../../../types/ServiceId';
|
||||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||||
import type { UnwrapPromise } from '../../../types/Util';
|
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 ACI_1 = generateAci();
|
||||||
const NOW = new Date('2020-01-23T04:56:00.000');
|
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', () => {
|
describe('peekNotConnectedGroupCall', () => {
|
||||||
const { peekNotConnectedGroupCall } = actions;
|
const { peekNotConnectedGroupCall } = actions;
|
||||||
|
|
||||||
|
|
|
@ -129,6 +129,7 @@ import {
|
||||||
ContactSyncEvent,
|
ContactSyncEvent,
|
||||||
StoryRecipientUpdateEvent,
|
StoryRecipientUpdateEvent,
|
||||||
CallLogEventSyncEvent,
|
CallLogEventSyncEvent,
|
||||||
|
CallLinkUpdateSyncEvent,
|
||||||
} from './messageReceiverEvents';
|
} from './messageReceiverEvents';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
|
@ -145,6 +146,7 @@ import { filterAndClean } from '../types/BodyRange';
|
||||||
import { getCallEventForProto } from '../util/callDisposition';
|
import { getCallEventForProto } from '../util/callDisposition';
|
||||||
import { checkOurPniIdentityKey } from '../util/checkOurPniIdentityKey';
|
import { checkOurPniIdentityKey } from '../util/checkOurPniIdentityKey';
|
||||||
import { CallLogEvent } from '../types/CallDisposition';
|
import { CallLogEvent } from '../types/CallDisposition';
|
||||||
|
import { CallLinkUpdateSyncType } from '../types/CallLink';
|
||||||
|
|
||||||
const GROUPV2_ID_LENGTH = 32;
|
const GROUPV2_ID_LENGTH = 32;
|
||||||
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
||||||
|
@ -674,6 +676,11 @@ export default class MessageReceiver
|
||||||
handler: (ev: CallEventSyncEvent) => void
|
handler: (ev: CallEventSyncEvent) => void
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
public override addEventListener(
|
||||||
|
name: 'callLinkUpdateSync',
|
||||||
|
handler: (ev: CallLinkUpdateSyncEvent) => void
|
||||||
|
): void;
|
||||||
|
|
||||||
public override addEventListener(
|
public override addEventListener(
|
||||||
name: 'callLogEventSync',
|
name: 'callLogEventSync',
|
||||||
handler: (ev: CallLogEventSyncEvent) => void
|
handler: (ev: CallLogEventSyncEvent) => void
|
||||||
|
@ -3152,6 +3159,9 @@ export default class MessageReceiver
|
||||||
if (syncMessage.callEvent) {
|
if (syncMessage.callEvent) {
|
||||||
return this.handleCallEvent(envelope, syncMessage.callEvent);
|
return this.handleCallEvent(envelope, syncMessage.callEvent);
|
||||||
}
|
}
|
||||||
|
if (syncMessage.callLinkUpdate) {
|
||||||
|
return this.handleCallLinkUpdate(envelope, syncMessage.callLinkUpdate);
|
||||||
|
}
|
||||||
if (syncMessage.callLogEvent) {
|
if (syncMessage.callLogEvent) {
|
||||||
return this.handleCallLogEvent(envelope, syncMessage.callLogEvent);
|
return this.handleCallLogEvent(envelope, syncMessage.callLogEvent);
|
||||||
}
|
}
|
||||||
|
@ -3510,6 +3520,53 @@ export default class MessageReceiver
|
||||||
log.info('handleCallEvent: finished');
|
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(
|
private async handleCallLogEvent(
|
||||||
envelope: ProcessedEnvelope,
|
envelope: ProcessedEnvelope,
|
||||||
callLogEvent: Proto.SyncMessage.ICallLogEvent
|
callLogEvent: Proto.SyncMessage.ICallLogEvent
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
} from './Types.d';
|
} from './Types.d';
|
||||||
import type { ContactDetailsWithAvatar } from './ContactsParser';
|
import type { ContactDetailsWithAvatar } from './ContactsParser';
|
||||||
import type { CallEventDetails, CallLogEvent } from '../types/CallDisposition';
|
import type { CallEventDetails, CallLogEvent } from '../types/CallDisposition';
|
||||||
|
import type { CallLinkUpdateSyncType } from '../types/CallLink';
|
||||||
|
|
||||||
export class EmptyEvent extends Event {
|
export class EmptyEvent extends Event {
|
||||||
constructor() {
|
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<{
|
export type CallLogEventSyncEventData = Readonly<{
|
||||||
event: CallLogEvent;
|
event: CallLogEvent;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
|
@ -9,11 +9,13 @@ import { aciSchema } from './ServiceId';
|
||||||
import { bytesToUuid } from '../util/uuidToBytes';
|
import { bytesToUuid } from '../util/uuidToBytes';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
|
import { UUID_BYTE_SIZE } from './Crypto';
|
||||||
|
|
||||||
export enum CallType {
|
export enum CallType {
|
||||||
Audio = 'Audio',
|
Audio = 'Audio',
|
||||||
Video = 'Video',
|
Video = 'Video',
|
||||||
Group = 'Group',
|
Group = 'Group',
|
||||||
|
Adhoc = 'Adhoc',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CallDirection {
|
export enum CallDirection {
|
||||||
|
@ -45,30 +47,42 @@ export enum RemoteCallEvent {
|
||||||
|
|
||||||
export type CallEvent = LocalCallEvent | RemoteCallEvent;
|
export type CallEvent = LocalCallEvent | RemoteCallEvent;
|
||||||
|
|
||||||
export enum DirectCallStatus {
|
export enum CallStatusValue {
|
||||||
Pending = 'Pending',
|
Pending = 'Pending',
|
||||||
Accepted = 'Accepted',
|
Accepted = 'Accepted',
|
||||||
Missed = 'Missed',
|
Missed = 'Missed',
|
||||||
Declined = 'Declined',
|
Declined = 'Declined',
|
||||||
Deleted = 'Deleted',
|
Deleted = 'Deleted',
|
||||||
}
|
|
||||||
|
|
||||||
export enum GroupCallStatus {
|
|
||||||
GenericGroupCall = 'GenericGroupCall',
|
GenericGroupCall = 'GenericGroupCall',
|
||||||
OutgoingRing = 'OutgoingRing',
|
OutgoingRing = 'OutgoingRing',
|
||||||
Ringing = 'Ringing',
|
Ringing = 'Ringing',
|
||||||
Joined = 'Joined',
|
Joined = 'Joined',
|
||||||
// keep these in sync with direct
|
JoinedAdhoc = 'JoinedAdhoc',
|
||||||
Accepted = DirectCallStatus.Accepted,
|
}
|
||||||
Missed = DirectCallStatus.Missed,
|
|
||||||
Declined = DirectCallStatus.Declined,
|
export enum DirectCallStatus {
|
||||||
Deleted = DirectCallStatus.Deleted,
|
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 {
|
export enum AdhocCallStatus {
|
||||||
Pending = DirectCallStatus.Pending,
|
Pending = CallStatusValue.Pending,
|
||||||
Joined = GroupCallStatus.Joined,
|
Joined = CallStatusValue.JoinedAdhoc,
|
||||||
Deleted = DirectCallStatus.Deleted,
|
Deleted = CallStatusValue.Deleted,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CallStatus = DirectCallStatus | GroupCallStatus | AdhocCallStatus;
|
export type CallStatus = DirectCallStatus | GroupCallStatus | AdhocCallStatus;
|
||||||
|
@ -177,11 +191,15 @@ export const callHistoryGroupSchema = z.object({
|
||||||
}) satisfies z.ZodType<CallHistoryGroup>;
|
}) satisfies z.ZodType<CallHistoryGroup>;
|
||||||
|
|
||||||
const peerIdInBytesSchema = z.instanceof(Uint8Array).transform(value => {
|
const peerIdInBytesSchema = z.instanceof(Uint8Array).transform(value => {
|
||||||
const uuid = bytesToUuid(value);
|
// direct conversationId
|
||||||
if (uuid != null) {
|
if (value.byteLength === UUID_BYTE_SIZE) {
|
||||||
return uuid;
|
const uuid = bytesToUuid(value);
|
||||||
|
if (uuid != null) {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// assuming groupId
|
|
||||||
|
// groupId or call link roomId
|
||||||
return Bytes.toBase64(value);
|
return Bytes.toBase64(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,16 @@ import type { ReadonlyDeep } from 'type-fest';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
|
export enum CallLinkUpdateSyncType {
|
||||||
|
Update = 'Update',
|
||||||
|
Delete = 'Delete',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CallLinkUpdateData = Readonly<{
|
||||||
|
rootKey: Uint8Array;
|
||||||
|
adminKey: Uint8Array | undefined;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restrictions
|
* Restrictions
|
||||||
*/
|
*/
|
||||||
|
@ -33,10 +43,25 @@ export type CallLinkType = Readonly<{
|
||||||
adminKey: string | null;
|
adminKey: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
restrictions: CallLinkRestrictions;
|
restrictions: CallLinkRestrictions;
|
||||||
|
// Revocation is not supported currently but still returned by the server
|
||||||
revoked: boolean;
|
revoked: boolean;
|
||||||
|
// Guaranteed from RingRTC readCallLink, but locally may be null immediately after
|
||||||
|
// CallLinkUpdate sync and before readCallLink
|
||||||
expiration: number | null;
|
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
|
// Ephemeral conversation-like type to satisfy components
|
||||||
export type CallLinkConversationType = ReadonlyDeep<
|
export type CallLinkConversationType = ReadonlyDeep<
|
||||||
Omit<ConversationType, 'type'> & {
|
Omit<ConversationType, 'type'> & {
|
||||||
|
@ -65,6 +90,6 @@ export const callLinkRecordSchema = z.object({
|
||||||
// state
|
// state
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
restrictions: callLinkRestrictionsSchema,
|
restrictions: callLinkRestrictionsSchema,
|
||||||
expiration: z.number().int(),
|
expiration: z.number().int().nullable(),
|
||||||
revoked: z.union([z.literal(1), z.literal(0)]),
|
revoked: z.union([z.literal(1), z.literal(0)]),
|
||||||
}) satisfies z.ZodType<CallLinkRecord>;
|
}) satisfies z.ZodType<CallLinkRecord>;
|
||||||
|
|
|
@ -59,13 +59,20 @@ import {
|
||||||
callHistoryDetailsSchema,
|
callHistoryDetailsSchema,
|
||||||
callDetailsSchema,
|
callDetailsSchema,
|
||||||
AdhocCallStatus,
|
AdhocCallStatus,
|
||||||
|
CallStatusValue,
|
||||||
} from '../types/CallDisposition';
|
} from '../types/CallDisposition';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { ConversationModel } from '../models/conversations';
|
import type { ConversationModel } from '../models/conversations';
|
||||||
|
import { drop } from './drop';
|
||||||
|
|
||||||
// utils
|
// 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 {
|
export function formatCallEvent(callEvent: CallEventDetails): string {
|
||||||
const {
|
const {
|
||||||
callId,
|
callId,
|
||||||
|
@ -78,14 +85,14 @@ export function formatCallEvent(callEvent: CallEventDetails): string {
|
||||||
ringerId,
|
ringerId,
|
||||||
timestamp,
|
timestamp,
|
||||||
} = callEvent;
|
} = 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})`;
|
return `CallEvent (${callId}, ${peerIdLog}, ${mode}, ${event}, ${direction}, ${type}, ${mode}, ${timestamp}, ${ringerId}, ${eventSource})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCallHistory(callHistory: CallHistoryDetails): string {
|
export function formatCallHistory(callHistory: CallHistoryDetails): string {
|
||||||
const { callId, peerId, direction, status, type, mode, timestamp, ringerId } =
|
const { callId, peerId, direction, status, type, mode, timestamp, ringerId } =
|
||||||
callHistory;
|
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})`;
|
return `CallHistory (${callId}, ${peerIdLog}, ${mode}, ${status}, ${direction}, ${type}, ${mode}, ${timestamp}, ${ringerId})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,6 +196,8 @@ export function getCallEventForProto(
|
||||||
type = CallType.Audio;
|
type = CallType.Audio;
|
||||||
} else if (callEvent.type === Proto.SyncMessage.CallEvent.Type.VIDEO_CALL) {
|
} else if (callEvent.type === Proto.SyncMessage.CallEvent.Type.VIDEO_CALL) {
|
||||||
type = CallType.Video;
|
type = CallType.Video;
|
||||||
|
} else if (callEvent.type === Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL) {
|
||||||
|
type = CallType.Adhoc;
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError(`Unknown call type ${callEvent.type}`);
|
throw new TypeError(`Unknown call type ${callEvent.type}`);
|
||||||
}
|
}
|
||||||
|
@ -196,6 +205,8 @@ export function getCallEventForProto(
|
||||||
let mode: CallMode;
|
let mode: CallMode;
|
||||||
if (type === CallType.Group) {
|
if (type === CallType.Group) {
|
||||||
mode = CallMode.Group;
|
mode = CallMode.Group;
|
||||||
|
} else if (type === CallType.Adhoc) {
|
||||||
|
mode = CallMode.Adhoc;
|
||||||
} else {
|
} else {
|
||||||
mode = CallMode.Direct;
|
mode = CallMode.Direct;
|
||||||
}
|
}
|
||||||
|
@ -246,21 +257,23 @@ const typeToProto = {
|
||||||
[CallType.Audio]: Proto.SyncMessage.CallEvent.Type.AUDIO_CALL,
|
[CallType.Audio]: Proto.SyncMessage.CallEvent.Type.AUDIO_CALL,
|
||||||
[CallType.Video]: Proto.SyncMessage.CallEvent.Type.VIDEO_CALL,
|
[CallType.Video]: Proto.SyncMessage.CallEvent.Type.VIDEO_CALL,
|
||||||
[CallType.Group]: Proto.SyncMessage.CallEvent.Type.GROUP_CALL,
|
[CallType.Group]: Proto.SyncMessage.CallEvent.Type.GROUP_CALL,
|
||||||
|
[CallType.Adhoc]: Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusToProto: Record<
|
const statusToProto: Record<
|
||||||
CallStatus,
|
CallStatus,
|
||||||
Proto.SyncMessage.CallEvent.Event | null
|
Proto.SyncMessage.CallEvent.Event | null
|
||||||
> = {
|
> = {
|
||||||
[DirectCallStatus.Accepted]: Proto.SyncMessage.CallEvent.Event.ACCEPTED, // and GroupCallStatus.Accepted
|
[CallStatusValue.Accepted]: Proto.SyncMessage.CallEvent.Event.ACCEPTED,
|
||||||
[DirectCallStatus.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED, // and GroupCallStatus.Declined
|
[CallStatusValue.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED,
|
||||||
[DirectCallStatus.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE, // and GroupCallStatus.Deleted
|
[CallStatusValue.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE,
|
||||||
[DirectCallStatus.Missed]: null, // and GroupCallStatus.Missed
|
[CallStatusValue.Missed]: null,
|
||||||
[DirectCallStatus.Pending]: null,
|
[CallStatusValue.Pending]: null,
|
||||||
[GroupCallStatus.GenericGroupCall]: null,
|
[CallStatusValue.GenericGroupCall]: null,
|
||||||
[GroupCallStatus.Joined]: null,
|
[CallStatusValue.OutgoingRing]: null,
|
||||||
[GroupCallStatus.OutgoingRing]: null,
|
[CallStatusValue.Ringing]: null,
|
||||||
[GroupCallStatus.Ringing]: null,
|
[CallStatusValue.Joined]: null,
|
||||||
|
[CallStatusValue.JoinedAdhoc]: Proto.SyncMessage.CallEvent.Event.ACCEPTED,
|
||||||
};
|
};
|
||||||
|
|
||||||
function shouldSyncStatus(callStatus: CallStatus) {
|
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) {
|
if (peerId.length === 0) {
|
||||||
peerId = Bytes.fromBase64(callHistory.peerId);
|
peerId = Bytes.fromBase64(callHistory.peerId);
|
||||||
}
|
}
|
||||||
|
@ -479,8 +495,10 @@ export function getCallDetailsForAdhocCall(
|
||||||
peerId,
|
peerId,
|
||||||
ringerId: null,
|
ringerId: null,
|
||||||
mode: CallMode.Adhoc,
|
mode: CallMode.Adhoc,
|
||||||
type: CallType.Group,
|
type: CallType.Adhoc,
|
||||||
direction: CallDirection.Outgoing,
|
// 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(),
|
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(
|
export async function updateLocalAdhocCallHistory(
|
||||||
callEvent: CallEventDetails
|
callEvent: CallEventDetails
|
||||||
): Promise<CallHistoryDetails | null> {
|
): Promise<CallHistoryDetails | null> {
|
||||||
|
@ -900,11 +929,11 @@ export async function updateLocalAdhocCallHistory(
|
||||||
|
|
||||||
if (prevCallHistory != null) {
|
if (prevCallHistory != null) {
|
||||||
log.info(
|
log.info(
|
||||||
'updateLocalAdhocCallHistory: Found previous call history:',
|
'updateAdhocCallHistory: Found previous call history:',
|
||||||
formatCallHistory(prevCallHistory)
|
formatCallHistory(prevCallHistory)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log.info('updateLocalAdhocCallHistory: No previous call history');
|
log.info('updateAdhocCallHistory: No previous call history');
|
||||||
}
|
}
|
||||||
|
|
||||||
let callHistory: CallHistoryDetails;
|
let callHistory: CallHistoryDetails;
|
||||||
|
@ -912,7 +941,7 @@ export async function updateLocalAdhocCallHistory(
|
||||||
callHistory = transitionCallHistory(prevCallHistory, callEvent);
|
callHistory = transitionCallHistory(prevCallHistory, callEvent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
"updateLocalAdhocCallHistory: Couldn't transition call history:",
|
"updateAdhocCallHistory: Couldn't transition call history:",
|
||||||
formatCallEvent(callEvent),
|
formatCallEvent(callEvent),
|
||||||
Errors.toLogFormat(error)
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
|
@ -923,17 +952,17 @@ export async function updateLocalAdhocCallHistory(
|
||||||
callHistory.status === AdhocCallStatus.Pending ||
|
callHistory.status === AdhocCallStatus.Pending ||
|
||||||
callHistory.status === AdhocCallStatus.Joined ||
|
callHistory.status === AdhocCallStatus.Joined ||
|
||||||
callHistory.status === AdhocCallStatus.Deleted,
|
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;
|
const isDeleted = callHistory.status === AdhocCallStatus.Deleted;
|
||||||
if (prevCallHistory != null && isEqual(prevCallHistory, callHistory)) {
|
if (prevCallHistory != null && isEqual(prevCallHistory, callHistory)) {
|
||||||
log.info(
|
log.info(
|
||||||
'updateLocalAdhocCallHistory: Next call history is identical, skipping save'
|
'updateAdhocCallHistory: Next call history is identical, skipping save'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log.info(
|
log.info(
|
||||||
'updateLocalAdhocCallHistory: Saving call history:',
|
'updateAdhocCallHistory: Saving call history:',
|
||||||
formatCallHistory(callHistory)
|
formatCallHistory(callHistory)
|
||||||
);
|
);
|
||||||
await window.Signal.Data.saveCallHistory(callHistory);
|
await window.Signal.Data.saveCallHistory(callHistory);
|
||||||
|
@ -1126,7 +1155,11 @@ export async function updateCallHistoryFromRemoteEvent(
|
||||||
callEvent: CallEventDetails,
|
callEvent: CallEventDetails,
|
||||||
receivedAtCounter: number
|
receivedAtCounter: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await updateLocalCallHistory(callEvent, receivedAtCounter);
|
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(
|
export async function updateCallHistoryFromLocalEvent(
|
||||||
|
|
|
@ -18,10 +18,18 @@ import type {
|
||||||
} from '../types/CallLink';
|
} from '../types/CallLink';
|
||||||
import {
|
import {
|
||||||
callLinkRecordSchema,
|
callLinkRecordSchema,
|
||||||
|
CallLinkRestrictions,
|
||||||
toCallLinkRestrictions,
|
toCallLinkRestrictions,
|
||||||
} from '../types/CallLink';
|
} from '../types/CallLink';
|
||||||
import type { LocalizerType } from '../types/Util';
|
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 {
|
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
|
||||||
return rootKey.deriveRoomId().toString('hex');
|
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
|
* DB record conversions
|
||||||
*/
|
*/
|
||||||
|
@ -90,12 +129,9 @@ export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
|
||||||
throw new Error('CallLink.callLinkToRecord: rootKey is null');
|
throw new Error('CallLink.callLinkToRecord: rootKey is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
// rootKey has a RingRTC parsing function, adminKey is just bytes
|
const rootKey = toRootKeyBytes(callLink.rootKey);
|
||||||
const rootKey = callLink.rootKey
|
|
||||||
? CallLinkRootKey.parse(callLink.rootKey).bytes
|
|
||||||
: null;
|
|
||||||
const adminKey = callLink.adminKey
|
const adminKey = callLink.adminKey
|
||||||
? Bytes.fromBase64(callLink.adminKey)
|
? toAdminKeyBytes(callLink.adminKey)
|
||||||
: null;
|
: null;
|
||||||
return callLinkRecordSchema.parse({
|
return callLinkRecordSchema.parse({
|
||||||
roomId: callLink.roomId,
|
roomId: callLink.roomId,
|
||||||
|
@ -109,11 +145,13 @@ export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function callLinkFromRecord(record: CallLinkRecord): CallLinkType {
|
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
|
// root keys in memory are strings for simplicity
|
||||||
const rootKey = CallLinkRootKey.fromBytes(
|
const rootKey = fromRootKeyBytes(record.rootKey);
|
||||||
record.rootKey as Buffer
|
const adminKey = record.adminKey ? fromAdminKeyBytes(record.adminKey) : null;
|
||||||
).toString();
|
|
||||||
const adminKey = record.adminKey ? Bytes.toBase64(record.adminKey) : null;
|
|
||||||
return {
|
return {
|
||||||
roomId: record.roomId,
|
roomId: record.roomId,
|
||||||
rootKey,
|
rootKey,
|
||||||
|
|
|
@ -177,7 +177,7 @@ export function getCallingIcon(
|
||||||
}
|
}
|
||||||
return 'video';
|
return 'video';
|
||||||
}
|
}
|
||||||
if (callType === CallType.Group) {
|
if (callType === CallType.Group || callType === CallType.Adhoc) {
|
||||||
return 'video';
|
return 'video';
|
||||||
}
|
}
|
||||||
throw missingCaseError(callType);
|
throw missingCaseError(callType);
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const sendTypesEnum = z.enum([
|
||||||
'viewOnceSync',
|
'viewOnceSync',
|
||||||
'viewSync',
|
'viewSync',
|
||||||
'callEventSync',
|
'callEventSync',
|
||||||
|
'callLinkUpdateSync',
|
||||||
'callLogEventSync',
|
'callLogEventSync',
|
||||||
|
|
||||||
// No longer used, all non-urgent
|
// No longer used, all non-urgent
|
||||||
|
|
|
@ -3,22 +3,32 @@
|
||||||
|
|
||||||
import type { CallEventSyncEvent } from '../textsecure/messageReceiverEvents';
|
import type { CallEventSyncEvent } from '../textsecure/messageReceiverEvents';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { updateCallHistoryFromRemoteEvent } from './callDisposition';
|
import {
|
||||||
|
peerIdToLog,
|
||||||
|
updateCallHistoryFromRemoteEvent,
|
||||||
|
} from './callDisposition';
|
||||||
|
import { CallMode } from '../types/Calling';
|
||||||
|
|
||||||
export async function onCallEventSync(
|
export async function onCallEventSync(
|
||||||
syncEvent: CallEventSyncEvent
|
syncEvent: CallEventSyncEvent
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { callEvent, confirm } = syncEvent;
|
const { callEvent, confirm } = syncEvent;
|
||||||
const { callEventDetails, receivedAtCounter } = callEvent;
|
const { callEventDetails, receivedAtCounter } = callEvent;
|
||||||
const { peerId } = callEventDetails;
|
|
||||||
|
|
||||||
const conversation = window.ConversationController.get(peerId);
|
if (
|
||||||
|
callEventDetails.mode === CallMode.Direct ||
|
||||||
|
callEventDetails.mode === CallMode.Group
|
||||||
|
) {
|
||||||
|
const { peerId } = callEventDetails;
|
||||||
|
const conversation = window.ConversationController.get(peerId);
|
||||||
|
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
log.warn(
|
const peerIdLog = peerIdToLog(peerId, callEventDetails.mode);
|
||||||
`onCallEventSync: No conversation found for conversationId ${peerId}`
|
log.warn(
|
||||||
);
|
`onCallEventSync: No conversation found for conversationId ${peerIdLog}`
|
||||||
return;
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateCallHistoryFromRemoteEvent(callEventDetails, receivedAtCounter);
|
await updateCallHistoryFromRemoteEvent(callEventDetails, receivedAtCounter);
|
||||||
|
|
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