Backup: Support for calls
This commit is contained in:
parent
3a631a587f
commit
63e14a7df6
15 changed files with 1047 additions and 116 deletions
|
@ -276,7 +276,7 @@ message AdHocCall {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64 callId = 1;
|
uint64 callId = 1;
|
||||||
// Refers to a `CallLink` recipient.
|
// Refers to a Recipient with the `callLink` field set
|
||||||
uint64 recipientId = 2;
|
uint64 recipientId = 2;
|
||||||
State state = 3;
|
State state = 3;
|
||||||
uint64 callTimestamp = 4;
|
uint64 callTimestamp = 4;
|
||||||
|
@ -703,6 +703,7 @@ message IndividualCall {
|
||||||
Direction direction = 3;
|
Direction direction = 3;
|
||||||
State state = 4;
|
State state = 4;
|
||||||
uint64 startedCallTimestamp = 5;
|
uint64 startedCallTimestamp = 5;
|
||||||
|
bool read = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupCall {
|
message GroupCall {
|
||||||
|
@ -734,6 +735,7 @@ message GroupCall {
|
||||||
uint64 startedCallTimestamp = 5;
|
uint64 startedCallTimestamp = 5;
|
||||||
// The time the call ended. 0 indicates an unknown time.
|
// The time the call ended. 0 indicates an unknown time.
|
||||||
uint64 endedCallTimestamp = 6;
|
uint64 endedCallTimestamp = 6;
|
||||||
|
bool read = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SimpleChatUpdate {
|
message SimpleChatUpdate {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import type { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import pTimeout from 'p-timeout';
|
import pTimeout from 'p-timeout';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||||
|
|
||||||
import { Backups, SignalService } from '../../protobuf';
|
import { Backups, SignalService } from '../../protobuf';
|
||||||
import {
|
import {
|
||||||
|
@ -89,7 +91,18 @@ import { BACKUP_VERSION } from './constants';
|
||||||
import { getMessageIdForLogging } from '../../util/idForLogging';
|
import { getMessageIdForLogging } from '../../util/idForLogging';
|
||||||
import { getCallsHistoryForRedux } from '../callHistoryLoader';
|
import { getCallsHistoryForRedux } from '../callHistoryLoader';
|
||||||
import { makeLookup } from '../../util/makeLookup';
|
import { makeLookup } from '../../util/makeLookup';
|
||||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
import type {
|
||||||
|
CallHistoryDetails,
|
||||||
|
CallStatus,
|
||||||
|
} from '../../types/CallDisposition';
|
||||||
|
import {
|
||||||
|
CallMode,
|
||||||
|
CallDirection,
|
||||||
|
CallType,
|
||||||
|
DirectCallStatus,
|
||||||
|
GroupCallStatus,
|
||||||
|
AdhocCallStatus,
|
||||||
|
} from '../../types/CallDisposition';
|
||||||
import { isAciString } from '../../util/isAciString';
|
import { isAciString } from '../../util/isAciString';
|
||||||
import { hslToRGB } from '../../util/hslToRGB';
|
import { hslToRGB } from '../../util/hslToRGB';
|
||||||
import type { AboutMe, LocalChatStyle } from './types';
|
import type { AboutMe, LocalChatStyle } from './types';
|
||||||
|
@ -113,6 +126,10 @@ import { AttachmentBackupManager } from '../../jobs/AttachmentBackupManager';
|
||||||
import { getBackupCdnInfo } from './util/mediaId';
|
import { getBackupCdnInfo } from './util/mediaId';
|
||||||
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||||
|
import { toAdminKeyBytes } from '../../util/callLinks';
|
||||||
|
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
|
||||||
|
import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
|
|
||||||
const MAX_CONCURRENCY = 10;
|
const MAX_CONCURRENCY = 10;
|
||||||
|
|
||||||
|
@ -180,6 +197,7 @@ export class BackupExportStream extends Readable {
|
||||||
|
|
||||||
private readonly backupTimeMs = getSafeLongFromTimestamp(this.now);
|
private readonly backupTimeMs = getSafeLongFromTimestamp(this.now);
|
||||||
private readonly convoIdToRecipientId = new Map<string, number>();
|
private readonly convoIdToRecipientId = new Map<string, number>();
|
||||||
|
private readonly roomIdToRecipientId = new Map<string, number>();
|
||||||
private attachmentBackupJobs: Array<CoreAttachmentBackupJobType> = [];
|
private attachmentBackupJobs: Array<CoreAttachmentBackupJobType> = [];
|
||||||
private buffers = new Array<Uint8Array>();
|
private buffers = new Array<Uint8Array>();
|
||||||
private nextRecipientId = 0;
|
private nextRecipientId = 0;
|
||||||
|
@ -230,6 +248,8 @@ export class BackupExportStream extends Readable {
|
||||||
await this.flush();
|
await this.flush();
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
|
adHocCalls: 0,
|
||||||
|
callLinks: 0,
|
||||||
conversations: 0,
|
conversations: 0,
|
||||||
chats: 0,
|
chats: 0,
|
||||||
distributionLists: 0,
|
distributionLists: 0,
|
||||||
|
@ -283,7 +303,7 @@ export class BackupExportStream extends Readable {
|
||||||
|
|
||||||
this.pushFrame({
|
this.pushFrame({
|
||||||
recipient: {
|
recipient: {
|
||||||
id: this.getDistributionListRecipientId(),
|
id: Long.fromNumber(this.getNextRecipientId()),
|
||||||
distributionList: {
|
distributionList: {
|
||||||
distributionId: uuidToBytes(list.id),
|
distributionId: uuidToBytes(list.id),
|
||||||
deletionTimestamp: list.deletedAtTimestamp
|
deletionTimestamp: list.deletedAtTimestamp
|
||||||
|
@ -309,6 +329,48 @@ export class BackupExportStream extends Readable {
|
||||||
stats.distributionLists += 1;
|
stats.distributionLists += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const callLinks = await DataReader.getAllCallLinks();
|
||||||
|
|
||||||
|
for (const link of callLinks) {
|
||||||
|
const {
|
||||||
|
rootKey: rootKeyString,
|
||||||
|
adminKey,
|
||||||
|
name,
|
||||||
|
restrictions,
|
||||||
|
revoked,
|
||||||
|
expiration,
|
||||||
|
} = link;
|
||||||
|
|
||||||
|
if (revoked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = this.getNextRecipientId();
|
||||||
|
const rootKey = CallLinkRootKey.parse(rootKeyString);
|
||||||
|
const roomId = getRoomIdFromRootKey(rootKey);
|
||||||
|
|
||||||
|
this.roomIdToRecipientId.set(roomId, id);
|
||||||
|
|
||||||
|
this.pushFrame({
|
||||||
|
recipient: {
|
||||||
|
id: Long.fromNumber(id),
|
||||||
|
callLink: {
|
||||||
|
rootKey: rootKey.bytes,
|
||||||
|
adminKey: adminKey ? toAdminKeyBytes(adminKey) : null,
|
||||||
|
name,
|
||||||
|
restrictions: toCallLinkRestrictionsProto(restrictions),
|
||||||
|
expirationMs: isNumber(expiration)
|
||||||
|
? Long.fromNumber(expiration)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.flush();
|
||||||
|
stats.callLinks += 1;
|
||||||
|
}
|
||||||
|
|
||||||
const stickerPacks = await DataReader.getInstalledStickerPacks();
|
const stickerPacks = await DataReader.getInstalledStickerPacks();
|
||||||
|
|
||||||
for (const { id, key } of stickerPacks) {
|
for (const { id, key } of stickerPacks) {
|
||||||
|
@ -376,6 +438,37 @@ export class BackupExportStream extends Readable {
|
||||||
stats.chats += 1;
|
stats.chats += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allCallHistoryItems = await DataReader.getAllCallHistory();
|
||||||
|
|
||||||
|
for (const item of allCallHistoryItems) {
|
||||||
|
const { callId, type, peerId: roomId, status, timestamp } = item;
|
||||||
|
|
||||||
|
if (type !== CallType.Adhoc) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientId = this.roomIdToRecipientId.get(roomId);
|
||||||
|
if (!recipientId) {
|
||||||
|
log.warn(
|
||||||
|
`backups: Dropping ad-hoc call; recipientId for roomId ${roomId.slice(-2)} not found`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pushFrame({
|
||||||
|
adHocCall: {
|
||||||
|
callId: Long.fromString(callId),
|
||||||
|
recipientId: Long.fromNumber(recipientId),
|
||||||
|
state: toAdHocCallStateProto(status),
|
||||||
|
callTimestamp: Long.fromNumber(timestamp),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.flush();
|
||||||
|
stats.adHocCalls += 1;
|
||||||
|
}
|
||||||
|
|
||||||
let cursor: PageMessagesCursorType | undefined;
|
let cursor: PageMessagesCursorType | undefined;
|
||||||
|
|
||||||
const callHistory = getCallsHistoryForRedux();
|
const callHistory = getCallsHistoryForRedux();
|
||||||
|
@ -640,11 +733,11 @@ export class BackupExportStream extends Readable {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDistributionListRecipientId(): Long {
|
private getNextRecipientId(): number {
|
||||||
const recipientId = this.nextRecipientId;
|
const recipientId = this.nextRecipientId;
|
||||||
this.nextRecipientId += 1;
|
this.nextRecipientId += 1;
|
||||||
|
|
||||||
return Long.fromNumber(recipientId);
|
return recipientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private toRecipient(
|
private toRecipient(
|
||||||
|
@ -1082,7 +1175,7 @@ export class BackupExportStream extends Readable {
|
||||||
async toChatItemUpdate(
|
async toChatItemUpdate(
|
||||||
options: NonBubbleOptionsType
|
options: NonBubbleOptionsType
|
||||||
): Promise<NonBubbleResultType> {
|
): Promise<NonBubbleResultType> {
|
||||||
const { authorId, message } = options;
|
const { authorId, callHistoryByCallId, message } = options;
|
||||||
const logId = `toChatItemUpdate(${getMessageIdForLogging(message)})`;
|
const logId = `toChatItemUpdate(${getMessageIdForLogging(message)})`;
|
||||||
|
|
||||||
const updateMessage = new Backups.ChatUpdateMessage();
|
const updateMessage = new Backups.ChatUpdateMessage();
|
||||||
|
@ -1092,97 +1185,83 @@ export class BackupExportStream extends Readable {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isCallHistory(message)) {
|
if (isCallHistory(message)) {
|
||||||
// TODO (DESKTOP-6964)
|
const conversation = window.ConversationController.get(
|
||||||
// const callingMessage = new Backups.CallChatUpdate();
|
message.conversationId
|
||||||
// const { callId } = message;
|
);
|
||||||
// if (!callId) {
|
|
||||||
// throw new Error(
|
if (!conversation) {
|
||||||
// `${logId}: Message was callHistory, but missing callId!`
|
throw new Error(
|
||||||
// );
|
`${logId}: callHistory message had unknown conversationId!`
|
||||||
// }
|
);
|
||||||
// const callHistory = callHistoryByCallId[callId];
|
}
|
||||||
// if (!callHistory) {
|
|
||||||
// throw new Error(
|
const { callId } = message;
|
||||||
// `${logId}: Message had callId, but no call history details were found!`
|
if (!callId) {
|
||||||
// );
|
throw new Error(`${logId}: callHistory message was missing callId!`);
|
||||||
// }
|
}
|
||||||
// callingMessage.callId = Long.fromString(callId);
|
|
||||||
// if (callHistory.mode === CallMode.Group) {
|
const callHistory = callHistoryByCallId[callId];
|
||||||
// const groupCall = new Backups.GroupCallChatUpdate();
|
if (!callHistory) {
|
||||||
// const { ringerId } = callHistory;
|
throw new Error(
|
||||||
// if (!ringerId) {
|
`${logId}: callHistory message had callId, but no call history details were found!`
|
||||||
// throw new Error(
|
);
|
||||||
// `${logId}: Message had missing ringerId for a group call!`
|
}
|
||||||
// );
|
|
||||||
// }
|
if (isGroup(conversation.attributes)) {
|
||||||
// groupCall.startedCallAci = this.aciToBytes(ringerId);
|
const groupCall = new Backups.GroupCall();
|
||||||
// groupCall.startedCallTimestamp = Long.fromNumber(callHistory.timestamp);
|
|
||||||
// // Note: we don't store inCallACIs, instead relying on RingRTC in-memory state
|
strictAssert(
|
||||||
// callingMessage.groupCall = groupCall;
|
callHistory.mode === CallMode.Group,
|
||||||
// } else {
|
'in group, should be group call'
|
||||||
// const callMessage = new Backups.IndividualCallChatUpdate();
|
);
|
||||||
// const { direction, type, status } = callHistory;
|
|
||||||
// if (
|
if (callHistory.status === GroupCallStatus.Deleted) {
|
||||||
// status === DirectCallStatus.Accepted ||
|
return { kind: NonBubbleResultKind.Drop };
|
||||||
// status === DirectCallStatus.Pending
|
}
|
||||||
// ) {
|
|
||||||
// if (type === CallType.Audio) {
|
const { ringerId } = callHistory;
|
||||||
// callMessage.type =
|
if (ringerId) {
|
||||||
// direction === CallDirection.Incoming
|
const ringerConversation =
|
||||||
// ? Backups.IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL
|
window.ConversationController.get(ringerId);
|
||||||
// : Backups.IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL;
|
if (!ringerConversation) {
|
||||||
// } else if (type === CallType.Video) {
|
throw new Error(
|
||||||
// callMessage.type =
|
'toChatItemUpdate/callHistory: ringerId conversation not found!'
|
||||||
// direction === CallDirection.Incoming
|
);
|
||||||
// ? Backups.IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL
|
}
|
||||||
// : Backups.IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL;
|
|
||||||
// } else {
|
const recipientId = this.getRecipientId(
|
||||||
// throw new Error(
|
ringerConversation.attributes
|
||||||
// `${logId}: Message direct status '${status}' call had type ${type}`
|
);
|
||||||
// );
|
groupCall.ringerRecipientId = recipientId;
|
||||||
// }
|
groupCall.startedCallRecipientId = recipientId;
|
||||||
// } else if (status === DirectCallStatus.Declined) {
|
}
|
||||||
// if (direction === CallDirection.Incoming) {
|
|
||||||
// // question: do we really not call declined calls things that we decline?
|
groupCall.callId = Long.fromString(callId);
|
||||||
// throw new Error(
|
groupCall.state = toGroupCallStateProto(callHistory.status);
|
||||||
// `${logId}: Message direct call was declined but incoming`
|
groupCall.startedCallTimestamp = Long.fromNumber(callHistory.timestamp);
|
||||||
// );
|
groupCall.endedCallTimestamp = Long.fromNumber(0);
|
||||||
// }
|
groupCall.read = message.seenStatus === SeenStatus.Seen;
|
||||||
// if (type === CallType.Audio) {
|
|
||||||
// callMessage.type =
|
updateMessage.groupCall = groupCall;
|
||||||
// Backups.IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_AUDIO_CALL;
|
return { kind: NonBubbleResultKind.Directionless, patch };
|
||||||
// } else if (type === CallType.Video) {
|
}
|
||||||
// callMessage.type =
|
|
||||||
// Backups.IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_VIDEO_CALL;
|
const individualCall = new Backups.IndividualCall();
|
||||||
// } else {
|
const { direction, type, status, timestamp } = callHistory;
|
||||||
// throw new Error(
|
|
||||||
// `${logId}: Message direct status '${status}' call had type ${type}`
|
if (status === GroupCallStatus.Deleted) {
|
||||||
// );
|
return { kind: NonBubbleResultKind.Drop };
|
||||||
// }
|
}
|
||||||
// } else if (status === DirectCallStatus.Missed) {
|
|
||||||
// if (direction === CallDirection.Outgoing) {
|
individualCall.callId = Long.fromString(callId);
|
||||||
// throw new Error(
|
individualCall.type = toIndividualCallTypeProto(type);
|
||||||
// `${logId}: Message direct call was missed but outgoing`
|
individualCall.direction = toIndividualCallDirectionProto(direction);
|
||||||
// );
|
individualCall.state = toIndividualCallStateProto(status);
|
||||||
// }
|
individualCall.startedCallTimestamp = Long.fromNumber(timestamp);
|
||||||
// if (type === CallType.Audio) {
|
individualCall.read = message.seenStatus === SeenStatus.Seen;
|
||||||
// callMessage.type =
|
|
||||||
// Backups.IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL;
|
updateMessage.individualCall = individualCall;
|
||||||
// } else if (type === CallType.Video) {
|
return { kind: NonBubbleResultKind.Directionless, patch };
|
||||||
// callMessage.type =
|
|
||||||
// Backups.IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL;
|
|
||||||
// } else {
|
|
||||||
// throw new Error(
|
|
||||||
// `${logId}: Message direct status '${status}' call had type ${type}`
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// throw new Error(`${logId}: Message direct call had status ${status}`);
|
|
||||||
// }
|
|
||||||
// callingMessage.callMessage = callMessage;
|
|
||||||
// }
|
|
||||||
// updateMessage.callingMessage = callingMessage;
|
|
||||||
// return chatItem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isExpirationTimerUpdate(message)) {
|
if (isExpirationTimerUpdate(message)) {
|
||||||
|
@ -2484,3 +2563,127 @@ function hslToRGBInt(hue: number, saturation: number): number {
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
return ((0xff << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
return ((0xff << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toGroupCallStateProto(state: CallStatus): Backups.GroupCall.State {
|
||||||
|
const values = Backups.GroupCall.State;
|
||||||
|
|
||||||
|
if (state === GroupCallStatus.GenericGroupCall) {
|
||||||
|
return values.GENERIC;
|
||||||
|
}
|
||||||
|
if (state === GroupCallStatus.OutgoingRing) {
|
||||||
|
return values.OUTGOING_RING;
|
||||||
|
}
|
||||||
|
if (state === GroupCallStatus.Ringing) {
|
||||||
|
return values.RINGING;
|
||||||
|
}
|
||||||
|
if (state === GroupCallStatus.Joined) {
|
||||||
|
return values.JOINED;
|
||||||
|
}
|
||||||
|
if (state === GroupCallStatus.Accepted) {
|
||||||
|
return values.ACCEPTED;
|
||||||
|
}
|
||||||
|
if (state === GroupCallStatus.Missed) {
|
||||||
|
return values.MISSED;
|
||||||
|
}
|
||||||
|
if (state === GroupCallStatus.MissedNotificationProfile) {
|
||||||
|
return values.MISSED_NOTIFICATION_PROFILE;
|
||||||
|
}
|
||||||
|
if (state === GroupCallStatus.Declined) {
|
||||||
|
return values.DECLINED;
|
||||||
|
}
|
||||||
|
if (state === GroupCallStatus.Deleted) {
|
||||||
|
throw new Error(
|
||||||
|
'groupCallStatusToGroupCallState: Never back up deleted items!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.UNKNOWN_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIndividualCallDirectionProto(
|
||||||
|
direction: CallDirection
|
||||||
|
): Backups.IndividualCall.Direction {
|
||||||
|
const values = Backups.IndividualCall.Direction;
|
||||||
|
|
||||||
|
if (direction === CallDirection.Incoming) {
|
||||||
|
return values.INCOMING;
|
||||||
|
}
|
||||||
|
if (direction === CallDirection.Outgoing) {
|
||||||
|
return values.OUTGOING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.UNKNOWN_DIRECTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIndividualCallTypeProto(
|
||||||
|
type: CallType
|
||||||
|
): Backups.IndividualCall.Type {
|
||||||
|
const values = Backups.IndividualCall.Type;
|
||||||
|
|
||||||
|
if (type === CallType.Audio) {
|
||||||
|
return values.AUDIO_CALL;
|
||||||
|
}
|
||||||
|
if (type === CallType.Video) {
|
||||||
|
return values.VIDEO_CALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.UNKNOWN_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIndividualCallStateProto(
|
||||||
|
status: CallStatus
|
||||||
|
): Backups.IndividualCall.State {
|
||||||
|
const values = Backups.IndividualCall.State;
|
||||||
|
|
||||||
|
if (status === DirectCallStatus.Accepted) {
|
||||||
|
return values.ACCEPTED;
|
||||||
|
}
|
||||||
|
if (status === DirectCallStatus.Declined) {
|
||||||
|
return values.NOT_ACCEPTED;
|
||||||
|
}
|
||||||
|
if (status === DirectCallStatus.Missed) {
|
||||||
|
return values.MISSED;
|
||||||
|
}
|
||||||
|
if (status === DirectCallStatus.MissedNotificationProfile) {
|
||||||
|
return values.MISSED_NOTIFICATION_PROFILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === DirectCallStatus.Deleted) {
|
||||||
|
throw new Error(
|
||||||
|
'statusToIndividualCallProtoEnum: Never back up deleted items!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.UNKNOWN_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAdHocCallStateProto(status: CallStatus): Backups.AdHocCall.State {
|
||||||
|
const values = Backups.AdHocCall.State;
|
||||||
|
|
||||||
|
if (status === AdhocCallStatus.Generic) {
|
||||||
|
return values.GENERIC;
|
||||||
|
}
|
||||||
|
if (status === AdhocCallStatus.Joined) {
|
||||||
|
return values.GENERIC;
|
||||||
|
}
|
||||||
|
if (status === AdhocCallStatus.Pending) {
|
||||||
|
return values.GENERIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.UNKNOWN_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCallLinkRestrictionsProto(
|
||||||
|
restrictions: CallLinkRestrictions
|
||||||
|
): Backups.CallLink.Restrictions {
|
||||||
|
const values = Backups.CallLink.Restrictions;
|
||||||
|
|
||||||
|
if (restrictions === CallLinkRestrictions.None) {
|
||||||
|
return values.NONE;
|
||||||
|
}
|
||||||
|
if (restrictions === CallLinkRestrictions.AdminApproval) {
|
||||||
|
return values.ADMIN_APPROVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { v4 as generateUuid } from 'uuid';
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import { Writable } from 'stream';
|
import { Writable } from 'stream';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
|
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||||
|
|
||||||
import { Backups, SignalService } from '../../protobuf';
|
import { Backups, SignalService } from '../../protobuf';
|
||||||
import { DataWriter } from '../../sql/Client';
|
import { DataWriter } from '../../sql/Client';
|
||||||
|
@ -84,6 +85,20 @@ import { filterAndClean } from '../../types/BodyRange';
|
||||||
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../../types/MIME';
|
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../../types/MIME';
|
||||||
import { copyFromQuotedMessage } from '../../messages/copyQuote';
|
import { copyFromQuotedMessage } from '../../messages/copyQuote';
|
||||||
import { groupAvatarJobQueue } from '../../jobs/groupAvatarJobQueue';
|
import { groupAvatarJobQueue } from '../../jobs/groupAvatarJobQueue';
|
||||||
|
import {
|
||||||
|
AdhocCallStatus,
|
||||||
|
CallDirection,
|
||||||
|
CallMode,
|
||||||
|
CallType,
|
||||||
|
DirectCallStatus,
|
||||||
|
GroupCallStatus,
|
||||||
|
} from '../../types/CallDisposition';
|
||||||
|
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||||
|
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||||
|
import type { CallLinkType } from '../../types/CallLink';
|
||||||
|
|
||||||
|
import { fromAdminKeyBytes } from '../../util/callLinks';
|
||||||
|
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
|
||||||
|
|
||||||
const MAX_CONCURRENCY = 10;
|
const MAX_CONCURRENCY = 10;
|
||||||
|
|
||||||
|
@ -231,6 +246,7 @@ export class BackupImportStream extends Writable {
|
||||||
number,
|
number,
|
||||||
ConversationAttributesType
|
ConversationAttributesType
|
||||||
>();
|
>();
|
||||||
|
private readonly recipientIdToCallLink = new Map<number, CallLinkType>();
|
||||||
private readonly chatIdToConvo = new Map<
|
private readonly chatIdToConvo = new Map<
|
||||||
number,
|
number,
|
||||||
ConversationAttributesType
|
ConversationAttributesType
|
||||||
|
@ -382,6 +398,8 @@ export class BackupImportStream extends Writable {
|
||||||
if (frame.recipient) {
|
if (frame.recipient) {
|
||||||
const { recipient } = frame;
|
const { recipient } = frame;
|
||||||
strictAssert(recipient.id != null, 'Recipient must have an id');
|
strictAssert(recipient.id != null, 'Recipient must have an id');
|
||||||
|
const recipientId = recipient.id.toNumber();
|
||||||
|
|
||||||
let convo: ConversationAttributesType;
|
let convo: ConversationAttributesType;
|
||||||
if (recipient.contact) {
|
if (recipient.contact) {
|
||||||
convo = await this.fromContact(recipient.contact);
|
convo = await this.fromContact(recipient.contact);
|
||||||
|
@ -402,6 +420,11 @@ export class BackupImportStream extends Writable {
|
||||||
} else if (recipient.distributionList) {
|
} else if (recipient.distributionList) {
|
||||||
await this.fromDistributionList(recipient.distributionList);
|
await this.fromDistributionList(recipient.distributionList);
|
||||||
|
|
||||||
|
// Not a conversation
|
||||||
|
return;
|
||||||
|
} else if (recipient.callLink) {
|
||||||
|
await this.fromCallLink(recipientId, recipient.callLink);
|
||||||
|
|
||||||
// Not a conversation
|
// Not a conversation
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
@ -413,7 +436,7 @@ export class BackupImportStream extends Writable {
|
||||||
this.saveConversation(convo);
|
this.saveConversation(convo);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recipientIdToConvo.set(recipient.id.toNumber(), convo);
|
this.recipientIdToConvo.set(recipientId, convo);
|
||||||
} else if (frame.chat) {
|
} else if (frame.chat) {
|
||||||
await this.fromChat(frame.chat);
|
await this.fromChat(frame.chat);
|
||||||
} else if (frame.chatItem) {
|
} else if (frame.chatItem) {
|
||||||
|
@ -426,6 +449,8 @@ export class BackupImportStream extends Writable {
|
||||||
await this.fromChatItem(frame.chatItem, { aboutMe });
|
await this.fromChatItem(frame.chatItem, { aboutMe });
|
||||||
} else if (frame.stickerPack) {
|
} else if (frame.stickerPack) {
|
||||||
await this.fromStickerPack(frame.stickerPack);
|
await this.fromStickerPack(frame.stickerPack);
|
||||||
|
} else if (frame.adHocCall) {
|
||||||
|
await this.fromAdHocCall(frame.adHocCall);
|
||||||
} else {
|
} else {
|
||||||
log.warn(`${this.logId}: unsupported frame item ${frame.item}`);
|
log.warn(`${this.logId}: unsupported frame item ${frame.item}`);
|
||||||
}
|
}
|
||||||
|
@ -463,6 +488,12 @@ export class BackupImportStream extends Writable {
|
||||||
this.saveMessageBatcher.add(attributes);
|
this.saveMessageBatcher.add(attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async saveCallHistory(
|
||||||
|
callHistory: CallHistoryDetails
|
||||||
|
): Promise<void> {
|
||||||
|
await DataWriter.saveCallHistory(callHistory);
|
||||||
|
}
|
||||||
|
|
||||||
private async fromAccount({
|
private async fromAccount({
|
||||||
profileKey,
|
profileKey,
|
||||||
username,
|
username,
|
||||||
|
@ -987,6 +1018,38 @@ export class BackupImportStream extends Writable {
|
||||||
await DataWriter.createNewStoryDistribution(result);
|
await DataWriter.createNewStoryDistribution(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fromCallLink(
|
||||||
|
recipientId: number,
|
||||||
|
callLinkProto: Backups.ICallLink
|
||||||
|
): Promise<void> {
|
||||||
|
const {
|
||||||
|
rootKey: rootKeyBytes,
|
||||||
|
adminKey,
|
||||||
|
name,
|
||||||
|
restrictions,
|
||||||
|
expirationMs,
|
||||||
|
} = callLinkProto;
|
||||||
|
|
||||||
|
strictAssert(rootKeyBytes?.length, 'fromCallLink: rootKey is required');
|
||||||
|
strictAssert(name, 'fromCallLink: name is required');
|
||||||
|
|
||||||
|
const rootKey = CallLinkRootKey.fromBytes(Buffer.from(rootKeyBytes));
|
||||||
|
|
||||||
|
const callLink: CallLinkType = {
|
||||||
|
roomId: getRoomIdFromRootKey(rootKey),
|
||||||
|
rootKey: rootKey.toString(),
|
||||||
|
adminKey: adminKey?.length ? fromAdminKeyBytes(adminKey) : null,
|
||||||
|
name,
|
||||||
|
restrictions: fromCallLinkRestrictionsProto(restrictions),
|
||||||
|
revoked: false,
|
||||||
|
expiration: expirationMs?.toNumber() || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.recipientIdToCallLink.set(recipientId, callLink);
|
||||||
|
|
||||||
|
await DataWriter.insertCallLink(callLink);
|
||||||
|
}
|
||||||
|
|
||||||
private async fromChat(chat: Backups.IChat): Promise<void> {
|
private async fromChat(chat: Backups.IChat): Promise<void> {
|
||||||
strictAssert(chat.id != null, 'chat must have an id');
|
strictAssert(chat.id != null, 'chat must have an id');
|
||||||
strictAssert(chat.recipientId != null, 'chat must have a recipientId');
|
strictAssert(chat.recipientId != null, 'chat must have a recipientId');
|
||||||
|
@ -1329,7 +1392,12 @@ export class BackupImportStream extends Writable {
|
||||||
}
|
}
|
||||||
|
|
||||||
strictAssert(directionless, 'Absent direction state');
|
strictAssert(directionless, 'Absent direction state');
|
||||||
return { patch: {} };
|
return {
|
||||||
|
patch: {
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fromStandardMessage(
|
private async fromStandardMessage(
|
||||||
|
@ -1757,7 +1825,7 @@ export class BackupImportStream extends Writable {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
): Promise<ChatItemParseResult | undefined> {
|
): Promise<ChatItemParseResult | undefined> {
|
||||||
const { aboutMe, author } = options;
|
const { aboutMe, author, conversation } = options;
|
||||||
|
|
||||||
if (updateMessage.groupChange) {
|
if (updateMessage.groupChange) {
|
||||||
return this.fromGroupUpdateMessage(updateMessage.groupChange, options);
|
return this.fromGroupUpdateMessage(updateMessage.groupChange, options);
|
||||||
|
@ -1870,8 +1938,117 @@ export class BackupImportStream extends Writable {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (DESKTOP-6964): check these fields
|
if (updateMessage.groupCall) {
|
||||||
// updateMessage.callingMessage
|
const { groupId } = conversation;
|
||||||
|
|
||||||
|
if (!isGroup(conversation)) {
|
||||||
|
throw new Error('groupCall: Conversation is not a group!');
|
||||||
|
}
|
||||||
|
if (!groupId) {
|
||||||
|
throw new Error('groupCall: No groupId available on conversation');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
callId: callIdLong,
|
||||||
|
state,
|
||||||
|
ringerRecipientId,
|
||||||
|
startedCallRecipientId,
|
||||||
|
startedCallTimestamp,
|
||||||
|
read,
|
||||||
|
} = updateMessage.groupCall;
|
||||||
|
|
||||||
|
const initiator =
|
||||||
|
ringerRecipientId?.toNumber() || startedCallRecipientId?.toNumber();
|
||||||
|
const ringer = isNumber(initiator)
|
||||||
|
? this.recipientIdToConvo.get(initiator)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!callIdLong) {
|
||||||
|
throw new Error('groupCall: callId is required!');
|
||||||
|
}
|
||||||
|
if (!startedCallTimestamp) {
|
||||||
|
throw new Error('groupCall: startedCallTimestamp is required!');
|
||||||
|
}
|
||||||
|
const isRingerMe = ringer?.serviceId === aboutMe.aci;
|
||||||
|
|
||||||
|
const callId = callIdLong.toString();
|
||||||
|
const callHistory: CallHistoryDetails = {
|
||||||
|
callId,
|
||||||
|
status: fromGroupCallStateProto(state),
|
||||||
|
mode: CallMode.Group,
|
||||||
|
type: CallType.Group,
|
||||||
|
ringerId: ringer?.serviceId ?? null,
|
||||||
|
peerId: groupId,
|
||||||
|
direction: isRingerMe ? CallDirection.Outgoing : CallDirection.Incoming,
|
||||||
|
timestamp: startedCallTimestamp.toNumber(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveCallHistory(callHistory);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: 'call-history',
|
||||||
|
callId,
|
||||||
|
sourceServiceId: undefined,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: read ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||||
|
},
|
||||||
|
additionalMessages: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateMessage.individualCall) {
|
||||||
|
const {
|
||||||
|
callId: callIdLong,
|
||||||
|
type,
|
||||||
|
direction: protoDirection,
|
||||||
|
state,
|
||||||
|
startedCallTimestamp,
|
||||||
|
read,
|
||||||
|
} = updateMessage.individualCall;
|
||||||
|
|
||||||
|
if (!callIdLong) {
|
||||||
|
throw new Error('individualCall: callId is required!');
|
||||||
|
}
|
||||||
|
if (!startedCallTimestamp) {
|
||||||
|
throw new Error('individualCall: startedCallTimestamp is required!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const peerId = conversation.serviceId || conversation.e164;
|
||||||
|
strictAssert(peerId, 'individualCall: no peerId found for call');
|
||||||
|
|
||||||
|
const callId = callIdLong.toString();
|
||||||
|
const direction = fromIndividualCallDirectionProto(protoDirection);
|
||||||
|
const ringerId =
|
||||||
|
direction === CallDirection.Outgoing
|
||||||
|
? aboutMe.aci
|
||||||
|
: conversation.serviceId;
|
||||||
|
strictAssert(ringerId, 'individualCall: no ringerId found');
|
||||||
|
|
||||||
|
const callHistory: CallHistoryDetails = {
|
||||||
|
callId,
|
||||||
|
status: fromIndividualCallStateProto(state),
|
||||||
|
mode: CallMode.Direct,
|
||||||
|
type: fromIndividualCallTypeProto(type),
|
||||||
|
ringerId,
|
||||||
|
peerId,
|
||||||
|
direction,
|
||||||
|
timestamp: startedCallTimestamp.toNumber(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveCallHistory(callHistory);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: 'call-history',
|
||||||
|
callId,
|
||||||
|
sourceServiceId: undefined,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: read ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||||
|
},
|
||||||
|
additionalMessages: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -2572,6 +2749,44 @@ export class BackupImportStream extends Writable {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fromAdHocCall({
|
||||||
|
callId: callIdLong,
|
||||||
|
recipientId: recipientIdLong,
|
||||||
|
state,
|
||||||
|
callTimestamp,
|
||||||
|
}: Backups.IAdHocCall): Promise<void> {
|
||||||
|
strictAssert(callIdLong, 'AdHocCall must have a callId');
|
||||||
|
|
||||||
|
const callId = callIdLong.toString();
|
||||||
|
const logId = `fromAdhocCall(${callId.slice(-2)})`;
|
||||||
|
|
||||||
|
strictAssert(callTimestamp, `${logId}: must have a valid timestamp`);
|
||||||
|
strictAssert(recipientIdLong, 'AdHocCall must have a recipientIdLong');
|
||||||
|
|
||||||
|
const recipientId = recipientIdLong.toNumber();
|
||||||
|
const callLink = this.recipientIdToCallLink.get(recipientId);
|
||||||
|
|
||||||
|
if (!callLink) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: Dropping ad-hoc call, Call Link for recipientId ${recipientId} not found`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callHistory: CallHistoryDetails = {
|
||||||
|
callId,
|
||||||
|
peerId: callLink.roomId,
|
||||||
|
ringerId: null,
|
||||||
|
mode: CallMode.Adhoc,
|
||||||
|
type: CallType.Adhoc,
|
||||||
|
direction: CallDirection.Unknown,
|
||||||
|
timestamp: callTimestamp.toNumber(),
|
||||||
|
status: fromAdHocCallStateProto(state),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveCallHistory(callHistory);
|
||||||
|
}
|
||||||
|
|
||||||
private async fromCustomChatColors(
|
private async fromCustomChatColors(
|
||||||
customChatColors:
|
customChatColors:
|
||||||
| ReadonlyArray<Backups.ChatStyle.ICustomChatColor>
|
| ReadonlyArray<Backups.ChatStyle.ICustomChatColor>
|
||||||
|
@ -2766,3 +2981,133 @@ function rgbIntToHSL(intValue: number): { hue: number; saturation: number } {
|
||||||
|
|
||||||
return { hue, saturation };
|
return { hue, saturation };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fromGroupCallStateProto(
|
||||||
|
state: Backups.GroupCall.State | undefined | null
|
||||||
|
): GroupCallStatus {
|
||||||
|
const values = Backups.GroupCall.State;
|
||||||
|
|
||||||
|
if (state == null) {
|
||||||
|
return GroupCallStatus.GenericGroupCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === values.GENERIC) {
|
||||||
|
return GroupCallStatus.GenericGroupCall;
|
||||||
|
}
|
||||||
|
if (state === values.OUTGOING_RING) {
|
||||||
|
return GroupCallStatus.OutgoingRing;
|
||||||
|
}
|
||||||
|
if (state === values.RINGING) {
|
||||||
|
return GroupCallStatus.Ringing;
|
||||||
|
}
|
||||||
|
if (state === values.JOINED) {
|
||||||
|
return GroupCallStatus.Joined;
|
||||||
|
}
|
||||||
|
if (state === values.ACCEPTED) {
|
||||||
|
return GroupCallStatus.Accepted;
|
||||||
|
}
|
||||||
|
if (state === values.MISSED) {
|
||||||
|
return GroupCallStatus.Missed;
|
||||||
|
}
|
||||||
|
if (state === values.MISSED_NOTIFICATION_PROFILE) {
|
||||||
|
return GroupCallStatus.MissedNotificationProfile;
|
||||||
|
}
|
||||||
|
if (state === values.DECLINED) {
|
||||||
|
return GroupCallStatus.Declined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GroupCallStatus.GenericGroupCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromIndividualCallDirectionProto(
|
||||||
|
direction: Backups.IndividualCall.Direction | undefined | null
|
||||||
|
): CallDirection {
|
||||||
|
const values = Backups.IndividualCall.Direction;
|
||||||
|
|
||||||
|
if (direction == null) {
|
||||||
|
return CallDirection.Unknown;
|
||||||
|
}
|
||||||
|
if (direction === values.INCOMING) {
|
||||||
|
return CallDirection.Incoming;
|
||||||
|
}
|
||||||
|
if (direction === values.OUTGOING) {
|
||||||
|
return CallDirection.Outgoing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallDirection.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromIndividualCallTypeProto(
|
||||||
|
type: Backups.IndividualCall.Type | undefined | null
|
||||||
|
): CallType {
|
||||||
|
const values = Backups.IndividualCall.Type;
|
||||||
|
|
||||||
|
if (type == null) {
|
||||||
|
return CallType.Unknown;
|
||||||
|
}
|
||||||
|
if (type === values.AUDIO_CALL) {
|
||||||
|
return CallType.Audio;
|
||||||
|
}
|
||||||
|
if (type === values.VIDEO_CALL) {
|
||||||
|
return CallType.Video;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromIndividualCallStateProto(
|
||||||
|
status: Backups.IndividualCall.State | undefined | null
|
||||||
|
): DirectCallStatus {
|
||||||
|
const values = Backups.IndividualCall.State;
|
||||||
|
|
||||||
|
if (status == null) {
|
||||||
|
return DirectCallStatus.Unknown;
|
||||||
|
}
|
||||||
|
if (status === values.ACCEPTED) {
|
||||||
|
return DirectCallStatus.Accepted;
|
||||||
|
}
|
||||||
|
if (status === values.NOT_ACCEPTED) {
|
||||||
|
return DirectCallStatus.Declined;
|
||||||
|
}
|
||||||
|
if (status === values.MISSED) {
|
||||||
|
return DirectCallStatus.Missed;
|
||||||
|
}
|
||||||
|
if (status === values.MISSED_NOTIFICATION_PROFILE) {
|
||||||
|
return DirectCallStatus.MissedNotificationProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DirectCallStatus.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromAdHocCallStateProto(
|
||||||
|
status: Backups.AdHocCall.State | undefined | null
|
||||||
|
): AdhocCallStatus {
|
||||||
|
const values = Backups.AdHocCall.State;
|
||||||
|
|
||||||
|
if (status == null) {
|
||||||
|
return AdhocCallStatus.Unknown;
|
||||||
|
}
|
||||||
|
if (status === values.GENERIC) {
|
||||||
|
return AdhocCallStatus.Generic;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AdhocCallStatus.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromCallLinkRestrictionsProto(
|
||||||
|
restrictions: Backups.CallLink.Restrictions | undefined | null
|
||||||
|
): CallLinkRestrictions {
|
||||||
|
const values = Backups.CallLink.Restrictions;
|
||||||
|
|
||||||
|
if (restrictions == null) {
|
||||||
|
return CallLinkRestrictions.Unknown;
|
||||||
|
}
|
||||||
|
if (restrictions === values.NONE) {
|
||||||
|
return CallLinkRestrictions.None;
|
||||||
|
}
|
||||||
|
if (restrictions === values.ADMIN_APPROVAL) {
|
||||||
|
return CallLinkRestrictions.AdminApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallLinkRestrictions.Unknown;
|
||||||
|
}
|
||||||
|
|
|
@ -779,6 +779,7 @@ type WritableInterface = {
|
||||||
_removeAllMessages: () => void;
|
_removeAllMessages: () => void;
|
||||||
|
|
||||||
clearCallHistory: (target: CallLogEventTarget) => ReadonlyArray<string>;
|
clearCallHistory: (target: CallLogEventTarget) => ReadonlyArray<string>;
|
||||||
|
_removeAllCallHistory: () => void;
|
||||||
markCallHistoryDeleted: (callId: string) => void;
|
markCallHistoryDeleted: (callId: string) => void;
|
||||||
cleanupCallHistoryMessages: () => void;
|
cleanupCallHistoryMessages: () => void;
|
||||||
markCallHistoryRead(callId: string): void;
|
markCallHistoryRead(callId: string): void;
|
||||||
|
@ -796,6 +797,7 @@ type WritableInterface = {
|
||||||
beginDeleteAllCallLinks(): void;
|
beginDeleteAllCallLinks(): void;
|
||||||
beginDeleteCallLink(roomId: string): void;
|
beginDeleteCallLink(roomId: string): void;
|
||||||
finalizeDeleteCallLink(roomId: string): void;
|
finalizeDeleteCallLink(roomId: string): void;
|
||||||
|
_removeAllCallLinks(): void;
|
||||||
deleteCallLinkFromSync(roomId: string): void;
|
deleteCallLinkFromSync(roomId: string): void;
|
||||||
migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
|
migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
|
||||||
saveEditedMessage: (
|
saveEditedMessage: (
|
||||||
|
|
|
@ -182,6 +182,7 @@ import {
|
||||||
finalizeDeleteCallLink,
|
finalizeDeleteCallLink,
|
||||||
beginDeleteCallLink,
|
beginDeleteCallLink,
|
||||||
deleteCallLinkFromSync,
|
deleteCallLinkFromSync,
|
||||||
|
_removeAllCallLinks,
|
||||||
} from './server/callLinks';
|
} from './server/callLinks';
|
||||||
import {
|
import {
|
||||||
replaceAllEndorsementsForGroup,
|
replaceAllEndorsementsForGroup,
|
||||||
|
@ -425,6 +426,7 @@ export const DataWriter: ServerWritableInterface = {
|
||||||
_removeAllMessages,
|
_removeAllMessages,
|
||||||
getUnreadEditedMessagesAndMarkRead,
|
getUnreadEditedMessagesAndMarkRead,
|
||||||
clearCallHistory,
|
clearCallHistory,
|
||||||
|
_removeAllCallHistory,
|
||||||
markCallHistoryDeleted,
|
markCallHistoryDeleted,
|
||||||
cleanupCallHistoryMessages,
|
cleanupCallHistoryMessages,
|
||||||
markCallHistoryRead,
|
markCallHistoryRead,
|
||||||
|
@ -438,6 +440,7 @@ export const DataWriter: ServerWritableInterface = {
|
||||||
beginDeleteAllCallLinks,
|
beginDeleteAllCallLinks,
|
||||||
beginDeleteCallLink,
|
beginDeleteCallLink,
|
||||||
finalizeDeleteCallLink,
|
finalizeDeleteCallLink,
|
||||||
|
_removeAllCallLinks,
|
||||||
deleteCallLinkFromSync,
|
deleteCallLinkFromSync,
|
||||||
migrateConversationMessages,
|
migrateConversationMessages,
|
||||||
saveEditedMessage,
|
saveEditedMessage,
|
||||||
|
@ -3472,6 +3475,13 @@ function getAllCallHistory(db: ReadableDB): ReadonlyArray<CallHistoryDetails> {
|
||||||
return db.prepare(query).all();
|
return db.prepare(query).all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _removeAllCallHistory(db: WritableDB): void {
|
||||||
|
const [query, params] = sql`
|
||||||
|
DELETE FROM callsHistory;
|
||||||
|
`;
|
||||||
|
db.prepare(query).run(params);
|
||||||
|
}
|
||||||
|
|
||||||
function clearCallHistory(
|
function clearCallHistory(
|
||||||
db: WritableDB,
|
db: WritableDB,
|
||||||
target: CallLogEventTarget
|
target: CallLogEventTarget
|
||||||
|
|
|
@ -237,3 +237,10 @@ export function finalizeDeleteCallLink(db: WritableDB, roomId: string): void {
|
||||||
`;
|
`;
|
||||||
db.prepare(query).run(params);
|
db.prepare(query).run(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function _removeAllCallLinks(db: WritableDB): void {
|
||||||
|
const [query, params] = sql`
|
||||||
|
DELETE FROM callLinks;
|
||||||
|
`;
|
||||||
|
db.prepare(query).run(params);
|
||||||
|
}
|
||||||
|
|
|
@ -39,9 +39,9 @@ describe('backup/attachments', () => {
|
||||||
let contactA: ConversationModel;
|
let contactA: ConversationModel;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await DataWriter._removeAllMessages();
|
await DataWriter.removeAll();
|
||||||
await DataWriter._removeAllConversations();
|
|
||||||
window.storage.reset();
|
window.storage.reset();
|
||||||
|
window.ConversationController.reset();
|
||||||
|
|
||||||
await setupBasics();
|
await setupBasics();
|
||||||
|
|
||||||
|
@ -69,7 +69,9 @@ describe('backup/attachments', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(async () => {
|
||||||
|
await DataWriter.removeAll();
|
||||||
|
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ import {
|
||||||
asymmetricRoundtripHarness,
|
asymmetricRoundtripHarness,
|
||||||
symmetricRoundtripHarness,
|
symmetricRoundtripHarness,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
|
|
||||||
// Note: this should be kept up to date with GroupV2Change.stories.tsx, to
|
// Note: this should be kept up to date with GroupV2Change.stories.tsx, to
|
||||||
// maintain the comprehensive set of GroupV2 notifications we need to handle
|
// maintain the comprehensive set of GroupV2 notifications we need to handle
|
||||||
|
@ -66,6 +68,8 @@ function createMessage(
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
type: 'group-v2-change',
|
type: 'group-v2-change',
|
||||||
sourceServiceId,
|
sourceServiceId,
|
||||||
};
|
};
|
||||||
|
@ -73,8 +77,8 @@ function createMessage(
|
||||||
|
|
||||||
describe('backup/groupv2/notifications', () => {
|
describe('backup/groupv2/notifications', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await DataWriter._removeAllMessages();
|
await DataWriter.removeAll();
|
||||||
await DataWriter._removeAllConversations();
|
window.ConversationController.reset();
|
||||||
window.storage.reset();
|
window.storage.reset();
|
||||||
|
|
||||||
await setupBasics();
|
await setupBasics();
|
||||||
|
@ -112,6 +116,9 @@ describe('backup/groupv2/notifications', () => {
|
||||||
|
|
||||||
await loadCallsHistory();
|
await loadCallsHistory();
|
||||||
});
|
});
|
||||||
|
afterEach(async () => {
|
||||||
|
await DataWriter.removeAll();
|
||||||
|
});
|
||||||
|
|
||||||
describe('roundtrips given groupv2 notifications with', () => {
|
describe('roundtrips given groupv2 notifications with', () => {
|
||||||
it('Multiple items', async () => {
|
it('Multiple items', async () => {
|
||||||
|
@ -2031,6 +2038,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2047,6 +2056,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2076,6 +2087,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
};
|
};
|
||||||
|
|
||||||
counter += 1;
|
counter += 1;
|
||||||
|
@ -2092,6 +2105,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages: Array<MessageAttributesType> = [
|
const messages: Array<MessageAttributesType> = [
|
||||||
|
@ -2124,6 +2139,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: OUR_ACI,
|
sourceServiceId: OUR_ACI,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2140,6 +2157,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: OUR_ACI,
|
sourceServiceId: OUR_ACI,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2156,6 +2175,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: OUR_ACI,
|
sourceServiceId: OUR_ACI,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2188,6 +2209,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: OUR_ACI,
|
sourceServiceId: OUR_ACI,
|
||||||
};
|
};
|
||||||
const legacyAfter = {
|
const legacyAfter = {
|
||||||
|
@ -2202,6 +2225,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: OUR_ACI,
|
sourceServiceId: OUR_ACI,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2221,6 +2246,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: OUR_ACI,
|
sourceServiceId: OUR_ACI,
|
||||||
};
|
};
|
||||||
const allDataAfter = {
|
const allDataAfter = {
|
||||||
|
@ -2235,6 +2262,8 @@ describe('backup/groupv2/notifications', () => {
|
||||||
received_at: counter,
|
received_at: counter,
|
||||||
sent_at: counter,
|
sent_at: counter,
|
||||||
timestamp: counter,
|
timestamp: counter,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: OUR_ACI,
|
sourceServiceId: OUR_ACI,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
266
ts/test-electron/backup/calling_test.ts
Normal file
266
ts/test-electron/backup/calling_test.ts
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import assert from 'assert';
|
||||||
|
import { v4 as generateGuid } from 'uuid';
|
||||||
|
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||||
|
|
||||||
|
import type { ConversationModel } from '../../models/conversations';
|
||||||
|
import type { MessageAttributesType } from '../../model-types';
|
||||||
|
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||||
|
import type { CallLinkType } from '../../types/CallLink';
|
||||||
|
|
||||||
|
import * as Bytes from '../../Bytes';
|
||||||
|
import { getRandomBytes } from '../../Crypto';
|
||||||
|
import { DataReader, DataWriter } from '../../sql/Client';
|
||||||
|
import { generateAci } from '../../types/ServiceId';
|
||||||
|
import { loadCallsHistory } from '../../services/callHistoryLoader';
|
||||||
|
import { setupBasics, symmetricRoundtripHarness } from './helpers';
|
||||||
|
import {
|
||||||
|
AdhocCallStatus,
|
||||||
|
CallDirection,
|
||||||
|
CallMode,
|
||||||
|
CallType,
|
||||||
|
DirectCallStatus,
|
||||||
|
GroupCallStatus,
|
||||||
|
} from '../../types/CallDisposition';
|
||||||
|
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||||
|
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
|
||||||
|
import { fromAdminKeyBytes } from '../../util/callLinks';
|
||||||
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
|
import { deriveGroupID, deriveGroupSecretParams } from '../../util/zkgroup';
|
||||||
|
|
||||||
|
const CONTACT_A = generateAci();
|
||||||
|
const GROUP_MASTER_KEY = getRandomBytes(32);
|
||||||
|
const GROUP_SECRET_PARAMS = deriveGroupSecretParams(GROUP_MASTER_KEY);
|
||||||
|
const GROUP_ID_STRING = Bytes.toBase64(deriveGroupID(GROUP_SECRET_PARAMS));
|
||||||
|
|
||||||
|
describe('backup/calling', () => {
|
||||||
|
let contactA: ConversationModel;
|
||||||
|
let groupA: ConversationModel;
|
||||||
|
let callLink: CallLinkType;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await DataWriter.removeAll();
|
||||||
|
window.ConversationController.reset();
|
||||||
|
window.storage.reset();
|
||||||
|
|
||||||
|
await setupBasics();
|
||||||
|
|
||||||
|
contactA = await window.ConversationController.getOrCreateAndWait(
|
||||||
|
CONTACT_A,
|
||||||
|
'private',
|
||||||
|
{ systemGivenName: 'CONTACT_A' }
|
||||||
|
);
|
||||||
|
groupA = await window.ConversationController.getOrCreateAndWait(
|
||||||
|
GROUP_ID_STRING,
|
||||||
|
'group',
|
||||||
|
{
|
||||||
|
groupVersion: 2,
|
||||||
|
masterKey: Bytes.toBase64(GROUP_MASTER_KEY),
|
||||||
|
name: 'Rock Enthusiasts',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const rootKey = CallLinkRootKey.generate();
|
||||||
|
const adminKey = CallLinkRootKey.generateAdminPassKey();
|
||||||
|
callLink = {
|
||||||
|
rootKey: rootKey.toString(),
|
||||||
|
roomId: getRoomIdFromRootKey(rootKey),
|
||||||
|
// TODO: DESKTOP-7511
|
||||||
|
adminKey: fromAdminKeyBytes(Buffer.concat([adminKey, adminKey])),
|
||||||
|
name: "Let's Talk Rocks",
|
||||||
|
restrictions: CallLinkRestrictions.AdminApproval,
|
||||||
|
revoked: false,
|
||||||
|
expiration: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await DataWriter.insertCallLink(callLink);
|
||||||
|
|
||||||
|
await loadCallsHistory();
|
||||||
|
});
|
||||||
|
after(async () => {
|
||||||
|
await DataWriter.removeAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Direct calls', () => {
|
||||||
|
it('roundtrips with a missed call', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const callId = '11111';
|
||||||
|
const callHistory: CallHistoryDetails = {
|
||||||
|
callId,
|
||||||
|
peerId: CONTACT_A,
|
||||||
|
ringerId: CONTACT_A,
|
||||||
|
mode: CallMode.Direct,
|
||||||
|
type: CallType.Audio,
|
||||||
|
status: DirectCallStatus.Missed,
|
||||||
|
direction: CallDirection.Incoming,
|
||||||
|
timestamp: now,
|
||||||
|
};
|
||||||
|
await DataWriter.saveCallHistory(callHistory);
|
||||||
|
await loadCallsHistory();
|
||||||
|
|
||||||
|
const messageUnseen: MessageAttributesType = {
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'call-history',
|
||||||
|
sent_at: now,
|
||||||
|
received_at: now,
|
||||||
|
timestamp: now,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
callId,
|
||||||
|
};
|
||||||
|
const messageSeen: MessageAttributesType = {
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'call-history',
|
||||||
|
sent_at: now + 1,
|
||||||
|
received_at: now + 1,
|
||||||
|
timestamp: now + 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
conversationId: contactA.id,
|
||||||
|
callId,
|
||||||
|
};
|
||||||
|
await symmetricRoundtripHarness([messageUnseen, messageSeen]);
|
||||||
|
|
||||||
|
const allCallHistory = await DataReader.getAllCallHistory();
|
||||||
|
assert.strictEqual(allCallHistory.length, 1);
|
||||||
|
|
||||||
|
assert.deepEqual(callHistory, allCallHistory[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Group calls', () => {
|
||||||
|
it('roundtrips with a missed call', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const callId = '22222';
|
||||||
|
const callHistory: CallHistoryDetails = {
|
||||||
|
callId,
|
||||||
|
peerId: GROUP_ID_STRING,
|
||||||
|
ringerId: CONTACT_A,
|
||||||
|
mode: CallMode.Group,
|
||||||
|
type: CallType.Group,
|
||||||
|
status: GroupCallStatus.Declined,
|
||||||
|
direction: CallDirection.Incoming,
|
||||||
|
timestamp: now,
|
||||||
|
};
|
||||||
|
await DataWriter.saveCallHistory(callHistory);
|
||||||
|
await loadCallsHistory();
|
||||||
|
|
||||||
|
const messageUnseen: MessageAttributesType = {
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'call-history',
|
||||||
|
sent_at: now,
|
||||||
|
received_at: now,
|
||||||
|
timestamp: now,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
|
conversationId: groupA.id,
|
||||||
|
callId,
|
||||||
|
};
|
||||||
|
const messageSeen: MessageAttributesType = {
|
||||||
|
id: generateGuid(),
|
||||||
|
type: 'call-history',
|
||||||
|
sent_at: now + 1,
|
||||||
|
received_at: now + 1,
|
||||||
|
timestamp: now + 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
conversationId: groupA.id,
|
||||||
|
callId,
|
||||||
|
};
|
||||||
|
await symmetricRoundtripHarness([messageUnseen, messageSeen]);
|
||||||
|
|
||||||
|
const allCallHistory = await DataReader.getAllCallHistory();
|
||||||
|
assert.strictEqual(allCallHistory.length, 1);
|
||||||
|
|
||||||
|
assert.deepEqual(callHistory, allCallHistory[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Call Links', () => {
|
||||||
|
it('roundtrips with a link with admin details', async () => {
|
||||||
|
const allCallLinksBefore = await DataReader.getAllCallLinks();
|
||||||
|
assert.strictEqual(allCallLinksBefore.length, 1);
|
||||||
|
|
||||||
|
await symmetricRoundtripHarness([]);
|
||||||
|
|
||||||
|
const allCallLinks = await DataReader.getAllCallLinks();
|
||||||
|
assert.strictEqual(allCallLinks.length, 1);
|
||||||
|
|
||||||
|
assert.deepEqual(callLink, allCallLinks[0]);
|
||||||
|
});
|
||||||
|
it('roundtrips with a link without admin details', async () => {
|
||||||
|
await DataWriter._removeAllCallLinks();
|
||||||
|
|
||||||
|
const rootKey = CallLinkRootKey.generate();
|
||||||
|
const callLinkNoAdmin = {
|
||||||
|
rootKey: rootKey.toString(),
|
||||||
|
roomId: getRoomIdFromRootKey(rootKey),
|
||||||
|
adminKey: null,
|
||||||
|
name: "Let's Talk Rocks #2",
|
||||||
|
restrictions: CallLinkRestrictions.AdminApproval,
|
||||||
|
revoked: false,
|
||||||
|
expiration: null,
|
||||||
|
};
|
||||||
|
await DataWriter.insertCallLink(callLinkNoAdmin);
|
||||||
|
|
||||||
|
const allCallLinksBefore = await DataReader.getAllCallLinks();
|
||||||
|
assert.strictEqual(allCallLinksBefore.length, 1);
|
||||||
|
|
||||||
|
await symmetricRoundtripHarness([]);
|
||||||
|
|
||||||
|
const allCallLinks = await DataReader.getAllCallLinks();
|
||||||
|
assert.strictEqual(allCallLinks.length, 1);
|
||||||
|
|
||||||
|
assert.deepEqual(callLinkNoAdmin, allCallLinks[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Adhoc calls', () => {
|
||||||
|
it('roundtrips with a joined call', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const callId = '333333';
|
||||||
|
const callHistory: CallHistoryDetails = {
|
||||||
|
callId,
|
||||||
|
peerId: callLink.roomId,
|
||||||
|
ringerId: null,
|
||||||
|
mode: CallMode.Adhoc,
|
||||||
|
type: CallType.Adhoc,
|
||||||
|
status: AdhocCallStatus.Generic,
|
||||||
|
direction: CallDirection.Unknown,
|
||||||
|
timestamp: now,
|
||||||
|
};
|
||||||
|
await DataWriter.saveCallHistory(callHistory);
|
||||||
|
await loadCallsHistory();
|
||||||
|
|
||||||
|
await symmetricRoundtripHarness([]);
|
||||||
|
|
||||||
|
const allCallHistory = await DataReader.getAllCallHistory();
|
||||||
|
assert.strictEqual(allCallHistory.length, 1);
|
||||||
|
|
||||||
|
assert.deepEqual(callHistory, allCallHistory[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not roundtrip call with missing call link', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const callId = '44444';
|
||||||
|
const callHistory: CallHistoryDetails = {
|
||||||
|
callId,
|
||||||
|
peerId: 'nonexistent',
|
||||||
|
ringerId: null,
|
||||||
|
mode: CallMode.Adhoc,
|
||||||
|
type: CallType.Adhoc,
|
||||||
|
status: AdhocCallStatus.Generic,
|
||||||
|
direction: CallDirection.Unknown,
|
||||||
|
timestamp: now,
|
||||||
|
};
|
||||||
|
await DataWriter.saveCallHistory(callHistory);
|
||||||
|
await loadCallsHistory();
|
||||||
|
|
||||||
|
await symmetricRoundtripHarness([]);
|
||||||
|
|
||||||
|
const allCallHistory = await DataReader.getAllCallHistory();
|
||||||
|
assert.strictEqual(allCallHistory.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -199,9 +199,7 @@ export async function asymmetricRoundtripHarness(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearData() {
|
async function clearData() {
|
||||||
await DataWriter._removeAllMessages();
|
await DataWriter.removeAll();
|
||||||
await DataWriter._removeAllConversations();
|
|
||||||
await DataWriter.removeAllItems();
|
|
||||||
window.storage.reset();
|
window.storage.reset();
|
||||||
window.ConversationController.reset();
|
window.ConversationController.reset();
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: OUR_ACI,
|
sourceServiceId: OUR_ACI,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -101,6 +103,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -116,6 +120,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -132,6 +138,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -148,6 +156,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -162,6 +172,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -176,6 +188,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -190,6 +204,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -399,6 +415,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
sourceDevice: 1,
|
sourceDevice: 1,
|
||||||
|
@ -419,6 +437,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
changedId: contactA.id,
|
changedId: contactA.id,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
profileChange: {
|
profileChange: {
|
||||||
|
@ -439,6 +459,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
titleTransition: {
|
titleTransition: {
|
||||||
renderInfo: {
|
renderInfo: {
|
||||||
|
@ -460,6 +482,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
conversationMerge: {
|
conversationMerge: {
|
||||||
renderInfo: {
|
renderInfo: {
|
||||||
type: 'private',
|
type: 'private',
|
||||||
|
@ -479,6 +503,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
phoneNumberDiscovery: {
|
phoneNumberDiscovery: {
|
||||||
e164: '+12125551234',
|
e164: '+12125551234',
|
||||||
|
@ -499,8 +525,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
sourceDevice: 1,
|
sourceDevice: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
readStatus: ReadStatus.Unread,
|
readStatus: ReadStatus.Read,
|
||||||
seenStatus: SeenStatus.Unseen,
|
seenStatus: SeenStatus.Seen,
|
||||||
unidentifiedDeliveryReceived: true,
|
unidentifiedDeliveryReceived: true,
|
||||||
supportedVersionAtReceive: 5,
|
supportedVersionAtReceive: 5,
|
||||||
requiredProtocolVersion: 6,
|
requiredProtocolVersion: 6,
|
||||||
|
@ -537,6 +563,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
},
|
},
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
},
|
},
|
||||||
|
@ -553,6 +581,8 @@ describe('backup/non-bubble messages', () => {
|
||||||
received_at: 1,
|
received_at: 1,
|
||||||
sourceServiceId: CONTACT_A,
|
sourceServiceId: CONTACT_A,
|
||||||
sourceDevice: 1,
|
sourceDevice: 1,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
sent_at: 1,
|
sent_at: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
messageRequestResponseEvent: MessageRequestResponseEvent.SPAM,
|
messageRequestResponseEvent: MessageRequestResponseEvent.SPAM,
|
||||||
|
|
|
@ -70,6 +70,9 @@ describe('sql/getCallHistoryGroups', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await removeAll();
|
await removeAll();
|
||||||
});
|
});
|
||||||
|
after(async () => {
|
||||||
|
await removeAll();
|
||||||
|
});
|
||||||
|
|
||||||
it('should merge related items in order', async () => {
|
it('should merge related items in order', async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { SignalService as Proto } from '../protobuf';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import { UUID_BYTE_SIZE } from './Crypto';
|
import { UUID_BYTE_SIZE } from './Crypto';
|
||||||
|
|
||||||
// These are strings (1) for the database (2) for Storybook.
|
// These are strings (1) for the backup (2) for Storybook.
|
||||||
export enum CallMode {
|
export enum CallMode {
|
||||||
Direct = 'Direct',
|
Direct = 'Direct',
|
||||||
Group = 'Group',
|
Group = 'Group',
|
||||||
|
@ -22,11 +22,15 @@ export enum CallType {
|
||||||
Video = 'Video',
|
Video = 'Video',
|
||||||
Group = 'Group',
|
Group = 'Group',
|
||||||
Adhoc = 'Adhoc',
|
Adhoc = 'Adhoc',
|
||||||
|
// Only used for backup roundtripping
|
||||||
|
Unknown = 'Unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CallDirection {
|
export enum CallDirection {
|
||||||
Incoming = 'Incoming',
|
Incoming = 'Incoming',
|
||||||
Outgoing = 'Outgoing',
|
Outgoing = 'Outgoing',
|
||||||
|
// Only used for backup roundtripping
|
||||||
|
Unknown = 'Unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CallLogEvent {
|
export enum CallLogEvent {
|
||||||
|
@ -59,6 +63,8 @@ export enum CallStatusValue {
|
||||||
Pending = 'Pending',
|
Pending = 'Pending',
|
||||||
Accepted = 'Accepted',
|
Accepted = 'Accepted',
|
||||||
Missed = 'Missed',
|
Missed = 'Missed',
|
||||||
|
// TODO: DESKTOP-3483 - not generated locally
|
||||||
|
MissedNotificationProfile = 'MissedNotificationProfile',
|
||||||
Declined = 'Declined',
|
Declined = 'Declined',
|
||||||
Deleted = 'Deleted',
|
Deleted = 'Deleted',
|
||||||
GenericGroupCall = 'GenericGroupCall',
|
GenericGroupCall = 'GenericGroupCall',
|
||||||
|
@ -67,14 +73,20 @@ export enum CallStatusValue {
|
||||||
Ringing = 'Ringing',
|
Ringing = 'Ringing',
|
||||||
Joined = 'Joined',
|
Joined = 'Joined',
|
||||||
JoinedAdhoc = 'JoinedAdhoc',
|
JoinedAdhoc = 'JoinedAdhoc',
|
||||||
|
// Only used for backup roundtripping
|
||||||
|
Unknown = 'Unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DirectCallStatus {
|
export enum DirectCallStatus {
|
||||||
Pending = CallStatusValue.Pending,
|
Pending = CallStatusValue.Pending,
|
||||||
Accepted = CallStatusValue.Accepted,
|
Accepted = CallStatusValue.Accepted,
|
||||||
Missed = CallStatusValue.Missed,
|
Missed = CallStatusValue.Missed,
|
||||||
|
// TODO: DESKTOP-3483 - not generated locally
|
||||||
|
MissedNotificationProfile = CallStatusValue.MissedNotificationProfile,
|
||||||
Declined = CallStatusValue.Declined,
|
Declined = CallStatusValue.Declined,
|
||||||
Deleted = CallStatusValue.Deleted,
|
Deleted = CallStatusValue.Deleted,
|
||||||
|
// Only used for backup roundtripping
|
||||||
|
Unknown = CallStatusValue.Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GroupCallStatus {
|
export enum GroupCallStatus {
|
||||||
|
@ -84,6 +96,8 @@ export enum GroupCallStatus {
|
||||||
Joined = CallStatusValue.Joined,
|
Joined = CallStatusValue.Joined,
|
||||||
Accepted = CallStatusValue.Accepted,
|
Accepted = CallStatusValue.Accepted,
|
||||||
Missed = CallStatusValue.Missed,
|
Missed = CallStatusValue.Missed,
|
||||||
|
// TODO: DESKTOP-3483 - not generated locally
|
||||||
|
MissedNotificationProfile = CallStatusValue.MissedNotificationProfile,
|
||||||
Declined = CallStatusValue.Declined,
|
Declined = CallStatusValue.Declined,
|
||||||
Deleted = CallStatusValue.Deleted,
|
Deleted = CallStatusValue.Deleted,
|
||||||
}
|
}
|
||||||
|
@ -93,6 +107,8 @@ export enum AdhocCallStatus {
|
||||||
Pending = CallStatusValue.Pending,
|
Pending = CallStatusValue.Pending,
|
||||||
Joined = CallStatusValue.JoinedAdhoc,
|
Joined = CallStatusValue.JoinedAdhoc,
|
||||||
Deleted = CallStatusValue.Deleted,
|
Deleted = CallStatusValue.Deleted,
|
||||||
|
// Only used for backup roundtripping
|
||||||
|
Unknown = CallStatusValue.Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CallStatus = DirectCallStatus | GroupCallStatus | AdhocCallStatus;
|
export type CallStatus = DirectCallStatus | GroupCallStatus | AdhocCallStatus;
|
||||||
|
|
|
@ -284,6 +284,7 @@ export function getCallLogEventForProto(
|
||||||
const directionToProto = {
|
const directionToProto = {
|
||||||
[CallDirection.Incoming]: Proto.SyncMessage.CallEvent.Direction.INCOMING,
|
[CallDirection.Incoming]: Proto.SyncMessage.CallEvent.Direction.INCOMING,
|
||||||
[CallDirection.Outgoing]: Proto.SyncMessage.CallEvent.Direction.OUTGOING,
|
[CallDirection.Outgoing]: Proto.SyncMessage.CallEvent.Direction.OUTGOING,
|
||||||
|
[CallDirection.Unknown]: Proto.SyncMessage.CallEvent.Direction.UNKNOWN,
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeToProto = {
|
const typeToProto = {
|
||||||
|
@ -291,6 +292,7 @@ const typeToProto = {
|
||||||
[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,
|
[CallType.Adhoc]: Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL,
|
||||||
|
[CallType.Unknown]: Proto.SyncMessage.CallEvent.Type.UNKNOWN,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusToProto: Record<
|
const statusToProto: Record<
|
||||||
|
@ -301,6 +303,7 @@ const statusToProto: Record<
|
||||||
[CallStatusValue.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED,
|
[CallStatusValue.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED,
|
||||||
[CallStatusValue.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE,
|
[CallStatusValue.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE,
|
||||||
[CallStatusValue.Missed]: null,
|
[CallStatusValue.Missed]: null,
|
||||||
|
[CallStatusValue.MissedNotificationProfile]: null,
|
||||||
[CallStatusValue.Pending]: null,
|
[CallStatusValue.Pending]: null,
|
||||||
[CallStatusValue.GenericGroupCall]: null,
|
[CallStatusValue.GenericGroupCall]: null,
|
||||||
[CallStatusValue.GenericAdhocCall]:
|
[CallStatusValue.GenericAdhocCall]:
|
||||||
|
@ -309,6 +312,7 @@ const statusToProto: Record<
|
||||||
[CallStatusValue.Ringing]: null,
|
[CallStatusValue.Ringing]: null,
|
||||||
[CallStatusValue.Joined]: null,
|
[CallStatusValue.Joined]: null,
|
||||||
[CallStatusValue.JoinedAdhoc]: Proto.SyncMessage.CallEvent.Event.ACCEPTED,
|
[CallStatusValue.JoinedAdhoc]: Proto.SyncMessage.CallEvent.Event.ACCEPTED,
|
||||||
|
[CallStatusValue.Unknown]: Proto.SyncMessage.CallEvent.Event.UNKNOWN,
|
||||||
};
|
};
|
||||||
|
|
||||||
function shouldSyncStatus(callStatus: CallStatus) {
|
function shouldSyncStatus(callStatus: CallStatus) {
|
||||||
|
@ -681,12 +685,16 @@ function transitionTimestamp(
|
||||||
// We don't care about holding onto timestamps that were from these states
|
// We don't care about holding onto timestamps that were from these states
|
||||||
if (
|
if (
|
||||||
callHistory.status === DirectCallStatus.Pending ||
|
callHistory.status === DirectCallStatus.Pending ||
|
||||||
|
callHistory.status === DirectCallStatus.Unknown ||
|
||||||
callHistory.status === GroupCallStatus.GenericGroupCall ||
|
callHistory.status === GroupCallStatus.GenericGroupCall ||
|
||||||
callHistory.status === GroupCallStatus.OutgoingRing ||
|
callHistory.status === GroupCallStatus.OutgoingRing ||
|
||||||
callHistory.status === GroupCallStatus.Ringing ||
|
callHistory.status === GroupCallStatus.Ringing ||
|
||||||
callHistory.status === DirectCallStatus.Missed ||
|
callHistory.status === DirectCallStatus.Missed ||
|
||||||
|
callHistory.status === DirectCallStatus.MissedNotificationProfile ||
|
||||||
callHistory.status === GroupCallStatus.Missed ||
|
callHistory.status === GroupCallStatus.Missed ||
|
||||||
callHistory.status === AdhocCallStatus.Pending
|
callHistory.status === GroupCallStatus.MissedNotificationProfile ||
|
||||||
|
callHistory.status === AdhocCallStatus.Pending ||
|
||||||
|
callHistory.status === AdhocCallStatus.Unknown
|
||||||
) {
|
) {
|
||||||
return latestTimestamp;
|
return latestTimestamp;
|
||||||
}
|
}
|
||||||
|
@ -801,6 +809,7 @@ function transitionGroupCallStatus(
|
||||||
}
|
}
|
||||||
case GroupCallStatus.Ringing:
|
case GroupCallStatus.Ringing:
|
||||||
case GroupCallStatus.Missed:
|
case GroupCallStatus.Missed:
|
||||||
|
case GroupCallStatus.MissedNotificationProfile:
|
||||||
case GroupCallStatus.Declined: {
|
case GroupCallStatus.Declined: {
|
||||||
return GroupCallStatus.Accepted;
|
return GroupCallStatus.Accepted;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,10 @@ export function getDirectCallNotificationText(
|
||||||
callStatus: DirectCallStatus,
|
callStatus: DirectCallStatus,
|
||||||
i18n: LocalizerType
|
i18n: LocalizerType
|
||||||
): string {
|
): string {
|
||||||
if (callStatus === DirectCallStatus.Pending) {
|
if (
|
||||||
|
callStatus === DirectCallStatus.Pending ||
|
||||||
|
callStatus === DirectCallStatus.Unknown
|
||||||
|
) {
|
||||||
if (callDirection === CallDirection.Incoming) {
|
if (callDirection === CallDirection.Incoming) {
|
||||||
return callType === CallType.Video
|
return callType === CallType.Video
|
||||||
? i18n('icu:incomingVideoCall')
|
? i18n('icu:incomingVideoCall')
|
||||||
|
@ -68,7 +71,10 @@ export function getDirectCallNotificationText(
|
||||||
: i18n('icu:missedOrDeclinedOutgoingAudioCall');
|
: i18n('icu:missedOrDeclinedOutgoingAudioCall');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callStatus === DirectCallStatus.Missed) {
|
if (
|
||||||
|
callStatus === DirectCallStatus.Missed ||
|
||||||
|
callStatus === DirectCallStatus.MissedNotificationProfile
|
||||||
|
) {
|
||||||
if (callDirection === CallDirection.Incoming) {
|
if (callDirection === CallDirection.Incoming) {
|
||||||
return callType === CallType.Video
|
return callType === CallType.Video
|
||||||
? i18n('icu:missedIncomingVideoCall')
|
? i18n('icu:missedIncomingVideoCall')
|
||||||
|
@ -219,5 +225,8 @@ export function getCallingIcon(
|
||||||
if (callType === CallType.Group || callType === CallType.Adhoc) {
|
if (callType === CallType.Group || callType === CallType.Adhoc) {
|
||||||
return 'video';
|
return 'video';
|
||||||
}
|
}
|
||||||
|
if (callType === CallType.Unknown) {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
throw missingCaseError(callType);
|
throw missingCaseError(callType);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue