Backup: Support for calls

This commit is contained in:
Scott Nonnenberg 2024-08-14 04:39:04 +10:00 committed by GitHub
parent 3a631a587f
commit 63e14a7df6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1047 additions and 116 deletions

View file

@ -276,7 +276,7 @@ message AdHocCall {
}
uint64 callId = 1;
// Refers to a `CallLink` recipient.
// Refers to a Recipient with the `callLink` field set
uint64 recipientId = 2;
State state = 3;
uint64 callTimestamp = 4;
@ -703,6 +703,7 @@ message IndividualCall {
Direction direction = 3;
State state = 4;
uint64 startedCallTimestamp = 5;
bool read = 6;
}
message GroupCall {
@ -734,6 +735,7 @@ message GroupCall {
uint64 startedCallTimestamp = 5;
// The time the call ended. 0 indicates an unknown time.
uint64 endedCallTimestamp = 6;
bool read = 7;
}
message SimpleChatUpdate {

View file

@ -7,6 +7,8 @@ import type { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
import pMap from 'p-map';
import pTimeout from 'p-timeout';
import { Readable } from 'stream';
import { isNumber } from 'lodash';
import { CallLinkRootKey } from '@signalapp/ringrtc';
import { Backups, SignalService } from '../../protobuf';
import {
@ -89,7 +91,18 @@ import { BACKUP_VERSION } from './constants';
import { getMessageIdForLogging } from '../../util/idForLogging';
import { getCallsHistoryForRedux } from '../callHistoryLoader';
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 { hslToRGB } from '../../util/hslToRGB';
import type { AboutMe, LocalChatStyle } from './types';
@ -113,6 +126,10 @@ import { AttachmentBackupManager } from '../../jobs/AttachmentBackupManager';
import { getBackupCdnInfo } from './util/mediaId';
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
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;
@ -180,6 +197,7 @@ export class BackupExportStream extends Readable {
private readonly backupTimeMs = getSafeLongFromTimestamp(this.now);
private readonly convoIdToRecipientId = new Map<string, number>();
private readonly roomIdToRecipientId = new Map<string, number>();
private attachmentBackupJobs: Array<CoreAttachmentBackupJobType> = [];
private buffers = new Array<Uint8Array>();
private nextRecipientId = 0;
@ -230,6 +248,8 @@ export class BackupExportStream extends Readable {
await this.flush();
const stats = {
adHocCalls: 0,
callLinks: 0,
conversations: 0,
chats: 0,
distributionLists: 0,
@ -283,7 +303,7 @@ export class BackupExportStream extends Readable {
this.pushFrame({
recipient: {
id: this.getDistributionListRecipientId(),
id: Long.fromNumber(this.getNextRecipientId()),
distributionList: {
distributionId: uuidToBytes(list.id),
deletionTimestamp: list.deletedAtTimestamp
@ -309,6 +329,48 @@ export class BackupExportStream extends Readable {
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();
for (const { id, key } of stickerPacks) {
@ -376,6 +438,37 @@ export class BackupExportStream extends Readable {
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;
const callHistory = getCallsHistoryForRedux();
@ -640,11 +733,11 @@ export class BackupExportStream extends Readable {
return result;
}
private getDistributionListRecipientId(): Long {
private getNextRecipientId(): number {
const recipientId = this.nextRecipientId;
this.nextRecipientId += 1;
return Long.fromNumber(recipientId);
return recipientId;
}
private toRecipient(
@ -1082,7 +1175,7 @@ export class BackupExportStream extends Readable {
async toChatItemUpdate(
options: NonBubbleOptionsType
): Promise<NonBubbleResultType> {
const { authorId, message } = options;
const { authorId, callHistoryByCallId, message } = options;
const logId = `toChatItemUpdate(${getMessageIdForLogging(message)})`;
const updateMessage = new Backups.ChatUpdateMessage();
@ -1092,97 +1185,83 @@ export class BackupExportStream extends Readable {
};
if (isCallHistory(message)) {
// TODO (DESKTOP-6964)
// const callingMessage = new Backups.CallChatUpdate();
// const { callId } = message;
// if (!callId) {
// throw new Error(
// `${logId}: Message was callHistory, but missing callId!`
// );
// }
// const callHistory = callHistoryByCallId[callId];
// if (!callHistory) {
// throw new Error(
// `${logId}: Message had callId, but no call history details were found!`
// );
// }
// callingMessage.callId = Long.fromString(callId);
// if (callHistory.mode === CallMode.Group) {
// const groupCall = new Backups.GroupCallChatUpdate();
// const { ringerId } = callHistory;
// if (!ringerId) {
// throw new Error(
// `${logId}: Message had missing ringerId for a group call!`
// );
// }
// groupCall.startedCallAci = this.aciToBytes(ringerId);
// groupCall.startedCallTimestamp = Long.fromNumber(callHistory.timestamp);
// // Note: we don't store inCallACIs, instead relying on RingRTC in-memory state
// callingMessage.groupCall = groupCall;
// } else {
// const callMessage = new Backups.IndividualCallChatUpdate();
// const { direction, type, status } = callHistory;
// if (
// status === DirectCallStatus.Accepted ||
// status === DirectCallStatus.Pending
// ) {
// if (type === CallType.Audio) {
// callMessage.type =
// direction === CallDirection.Incoming
// ? Backups.IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL
// : Backups.IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL;
// } else if (type === CallType.Video) {
// callMessage.type =
// direction === CallDirection.Incoming
// ? Backups.IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL
// : Backups.IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL;
// } else {
// throw new Error(
// `${logId}: Message direct status '${status}' call had type ${type}`
// );
// }
// } else if (status === DirectCallStatus.Declined) {
// if (direction === CallDirection.Incoming) {
// // question: do we really not call declined calls things that we decline?
// throw new Error(
// `${logId}: Message direct call was declined but incoming`
// );
// }
// if (type === CallType.Audio) {
// callMessage.type =
// Backups.IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_AUDIO_CALL;
// } else if (type === CallType.Video) {
// callMessage.type =
// Backups.IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_VIDEO_CALL;
// } else {
// throw new Error(
// `${logId}: Message direct status '${status}' call had type ${type}`
// );
// }
// } else if (status === DirectCallStatus.Missed) {
// if (direction === CallDirection.Outgoing) {
// throw new Error(
// `${logId}: Message direct call was missed but outgoing`
// );
// }
// if (type === CallType.Audio) {
// callMessage.type =
// Backups.IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL;
// } else if (type === CallType.Video) {
// 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;
const conversation = window.ConversationController.get(
message.conversationId
);
if (!conversation) {
throw new Error(
`${logId}: callHistory message had unknown conversationId!`
);
}
const { callId } = message;
if (!callId) {
throw new Error(`${logId}: callHistory message was missing callId!`);
}
const callHistory = callHistoryByCallId[callId];
if (!callHistory) {
throw new Error(
`${logId}: callHistory message had callId, but no call history details were found!`
);
}
if (isGroup(conversation.attributes)) {
const groupCall = new Backups.GroupCall();
strictAssert(
callHistory.mode === CallMode.Group,
'in group, should be group call'
);
if (callHistory.status === GroupCallStatus.Deleted) {
return { kind: NonBubbleResultKind.Drop };
}
const { ringerId } = callHistory;
if (ringerId) {
const ringerConversation =
window.ConversationController.get(ringerId);
if (!ringerConversation) {
throw new Error(
'toChatItemUpdate/callHistory: ringerId conversation not found!'
);
}
const recipientId = this.getRecipientId(
ringerConversation.attributes
);
groupCall.ringerRecipientId = recipientId;
groupCall.startedCallRecipientId = recipientId;
}
groupCall.callId = Long.fromString(callId);
groupCall.state = toGroupCallStateProto(callHistory.status);
groupCall.startedCallTimestamp = Long.fromNumber(callHistory.timestamp);
groupCall.endedCallTimestamp = Long.fromNumber(0);
groupCall.read = message.seenStatus === SeenStatus.Seen;
updateMessage.groupCall = groupCall;
return { kind: NonBubbleResultKind.Directionless, patch };
}
const individualCall = new Backups.IndividualCall();
const { direction, type, status, timestamp } = callHistory;
if (status === GroupCallStatus.Deleted) {
return { kind: NonBubbleResultKind.Drop };
}
individualCall.callId = Long.fromString(callId);
individualCall.type = toIndividualCallTypeProto(type);
individualCall.direction = toIndividualCallDirectionProto(direction);
individualCall.state = toIndividualCallStateProto(status);
individualCall.startedCallTimestamp = Long.fromNumber(timestamp);
individualCall.read = message.seenStatus === SeenStatus.Seen;
updateMessage.individualCall = individualCall;
return { kind: NonBubbleResultKind.Directionless, patch };
}
if (isExpirationTimerUpdate(message)) {
@ -2484,3 +2563,127 @@ function hslToRGBInt(hue: number, saturation: number): number {
// eslint-disable-next-line no-bitwise
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;
}

View file

@ -7,6 +7,7 @@ import { v4 as generateUuid } from 'uuid';
import pMap from 'p-map';
import { Writable } from 'stream';
import { isNumber } from 'lodash';
import { CallLinkRootKey } from '@signalapp/ringrtc';
import { Backups, SignalService } from '../../protobuf';
import { DataWriter } from '../../sql/Client';
@ -84,6 +85,20 @@ import { filterAndClean } from '../../types/BodyRange';
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../../types/MIME';
import { copyFromQuotedMessage } from '../../messages/copyQuote';
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;
@ -231,6 +246,7 @@ export class BackupImportStream extends Writable {
number,
ConversationAttributesType
>();
private readonly recipientIdToCallLink = new Map<number, CallLinkType>();
private readonly chatIdToConvo = new Map<
number,
ConversationAttributesType
@ -382,6 +398,8 @@ export class BackupImportStream extends Writable {
if (frame.recipient) {
const { recipient } = frame;
strictAssert(recipient.id != null, 'Recipient must have an id');
const recipientId = recipient.id.toNumber();
let convo: ConversationAttributesType;
if (recipient.contact) {
convo = await this.fromContact(recipient.contact);
@ -402,6 +420,11 @@ export class BackupImportStream extends Writable {
} else if (recipient.distributionList) {
await this.fromDistributionList(recipient.distributionList);
// Not a conversation
return;
} else if (recipient.callLink) {
await this.fromCallLink(recipientId, recipient.callLink);
// Not a conversation
return;
} else {
@ -413,7 +436,7 @@ export class BackupImportStream extends Writable {
this.saveConversation(convo);
}
this.recipientIdToConvo.set(recipient.id.toNumber(), convo);
this.recipientIdToConvo.set(recipientId, convo);
} else if (frame.chat) {
await this.fromChat(frame.chat);
} else if (frame.chatItem) {
@ -426,6 +449,8 @@ export class BackupImportStream extends Writable {
await this.fromChatItem(frame.chatItem, { aboutMe });
} else if (frame.stickerPack) {
await this.fromStickerPack(frame.stickerPack);
} else if (frame.adHocCall) {
await this.fromAdHocCall(frame.adHocCall);
} else {
log.warn(`${this.logId}: unsupported frame item ${frame.item}`);
}
@ -463,6 +488,12 @@ export class BackupImportStream extends Writable {
this.saveMessageBatcher.add(attributes);
}
private async saveCallHistory(
callHistory: CallHistoryDetails
): Promise<void> {
await DataWriter.saveCallHistory(callHistory);
}
private async fromAccount({
profileKey,
username,
@ -987,6 +1018,38 @@ export class BackupImportStream extends Writable {
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> {
strictAssert(chat.id != null, 'chat must have an id');
strictAssert(chat.recipientId != null, 'chat must have a recipientId');
@ -1329,7 +1392,12 @@ export class BackupImportStream extends Writable {
}
strictAssert(directionless, 'Absent direction state');
return { patch: {} };
return {
patch: {
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
},
};
}
private async fromStandardMessage(
@ -1757,7 +1825,7 @@ export class BackupImportStream extends Writable {
timestamp: number;
}
): Promise<ChatItemParseResult | undefined> {
const { aboutMe, author } = options;
const { aboutMe, author, conversation } = options;
if (updateMessage.groupChange) {
return this.fromGroupUpdateMessage(updateMessage.groupChange, options);
@ -1870,8 +1938,117 @@ export class BackupImportStream extends Writable {
};
}
// TODO (DESKTOP-6964): check these fields
// updateMessage.callingMessage
if (updateMessage.groupCall) {
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;
}
@ -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(
customChatColors:
| ReadonlyArray<Backups.ChatStyle.ICustomChatColor>
@ -2766,3 +2981,133 @@ function rgbIntToHSL(intValue: number): { hue: number; saturation: number } {
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;
}

View file

@ -779,6 +779,7 @@ type WritableInterface = {
_removeAllMessages: () => void;
clearCallHistory: (target: CallLogEventTarget) => ReadonlyArray<string>;
_removeAllCallHistory: () => void;
markCallHistoryDeleted: (callId: string) => void;
cleanupCallHistoryMessages: () => void;
markCallHistoryRead(callId: string): void;
@ -796,6 +797,7 @@ type WritableInterface = {
beginDeleteAllCallLinks(): void;
beginDeleteCallLink(roomId: string): void;
finalizeDeleteCallLink(roomId: string): void;
_removeAllCallLinks(): void;
deleteCallLinkFromSync(roomId: string): void;
migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
saveEditedMessage: (

View file

@ -182,6 +182,7 @@ import {
finalizeDeleteCallLink,
beginDeleteCallLink,
deleteCallLinkFromSync,
_removeAllCallLinks,
} from './server/callLinks';
import {
replaceAllEndorsementsForGroup,
@ -425,6 +426,7 @@ export const DataWriter: ServerWritableInterface = {
_removeAllMessages,
getUnreadEditedMessagesAndMarkRead,
clearCallHistory,
_removeAllCallHistory,
markCallHistoryDeleted,
cleanupCallHistoryMessages,
markCallHistoryRead,
@ -438,6 +440,7 @@ export const DataWriter: ServerWritableInterface = {
beginDeleteAllCallLinks,
beginDeleteCallLink,
finalizeDeleteCallLink,
_removeAllCallLinks,
deleteCallLinkFromSync,
migrateConversationMessages,
saveEditedMessage,
@ -3472,6 +3475,13 @@ function getAllCallHistory(db: ReadableDB): ReadonlyArray<CallHistoryDetails> {
return db.prepare(query).all();
}
function _removeAllCallHistory(db: WritableDB): void {
const [query, params] = sql`
DELETE FROM callsHistory;
`;
db.prepare(query).run(params);
}
function clearCallHistory(
db: WritableDB,
target: CallLogEventTarget

View file

@ -237,3 +237,10 @@ export function finalizeDeleteCallLink(db: WritableDB, roomId: string): void {
`;
db.prepare(query).run(params);
}
export function _removeAllCallLinks(db: WritableDB): void {
const [query, params] = sql`
DELETE FROM callLinks;
`;
db.prepare(query).run(params);
}

View file

@ -39,9 +39,9 @@ describe('backup/attachments', () => {
let contactA: ConversationModel;
beforeEach(async () => {
await DataWriter._removeAllMessages();
await DataWriter._removeAllConversations();
await DataWriter.removeAll();
window.storage.reset();
window.ConversationController.reset();
await setupBasics();
@ -69,7 +69,9 @@ describe('backup/attachments', () => {
});
});
afterEach(() => {
afterEach(async () => {
await DataWriter.removeAll();
sandbox.restore();
});

View file

@ -22,6 +22,8 @@ import {
asymmetricRoundtripHarness,
symmetricRoundtripHarness,
} from './helpers';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SeenStatus } from '../../MessageSeenStatus';
// Note: this should be kept up to date with GroupV2Change.stories.tsx, to
// maintain the comprehensive set of GroupV2 notifications we need to handle
@ -66,6 +68,8 @@ function createMessage(
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
type: 'group-v2-change',
sourceServiceId,
};
@ -73,8 +77,8 @@ function createMessage(
describe('backup/groupv2/notifications', () => {
beforeEach(async () => {
await DataWriter._removeAllMessages();
await DataWriter._removeAllConversations();
await DataWriter.removeAll();
window.ConversationController.reset();
window.storage.reset();
await setupBasics();
@ -112,6 +116,9 @@ describe('backup/groupv2/notifications', () => {
await loadCallsHistory();
});
afterEach(async () => {
await DataWriter.removeAll();
});
describe('roundtrips given groupv2 notifications with', () => {
it('Multiple items', async () => {
@ -2031,6 +2038,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
};
@ -2047,6 +2056,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
};
@ -2076,6 +2087,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
};
counter += 1;
@ -2092,6 +2105,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
};
const messages: Array<MessageAttributesType> = [
@ -2124,6 +2139,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: OUR_ACI,
};
@ -2140,6 +2157,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: OUR_ACI,
};
@ -2156,6 +2175,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: OUR_ACI,
};
@ -2188,6 +2209,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: OUR_ACI,
};
const legacyAfter = {
@ -2202,6 +2225,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: OUR_ACI,
};
@ -2221,6 +2246,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: OUR_ACI,
};
const allDataAfter = {
@ -2235,6 +2262,8 @@ describe('backup/groupv2/notifications', () => {
received_at: counter,
sent_at: counter,
timestamp: counter,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: OUR_ACI,
};

View 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);
});
});
});

View file

@ -199,9 +199,7 @@ export async function asymmetricRoundtripHarness(
}
async function clearData() {
await DataWriter._removeAllMessages();
await DataWriter._removeAllConversations();
await DataWriter.removeAllItems();
await DataWriter.removeAll();
window.storage.reset();
window.ConversationController.reset();

View file

@ -87,6 +87,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: OUR_ACI,
},
]);
@ -101,6 +103,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
]);
@ -116,6 +120,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
]);
@ -132,6 +138,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
]);
@ -148,6 +156,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
]);
@ -162,6 +172,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
]);
@ -176,6 +188,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
]);
@ -190,6 +204,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
]);
@ -399,6 +415,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
sourceServiceId: CONTACT_A,
sourceDevice: 1,
@ -419,6 +437,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
changedId: contactA.id,
sourceServiceId: CONTACT_A,
profileChange: {
@ -439,6 +459,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
titleTransition: {
renderInfo: {
@ -460,6 +482,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
conversationMerge: {
renderInfo: {
type: 'private',
@ -479,6 +503,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
phoneNumberDiscovery: {
e164: '+12125551234',
@ -499,8 +525,8 @@ describe('backup/non-bubble messages', () => {
sourceDevice: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
unidentifiedDeliveryReceived: true,
supportedVersionAtReceive: 5,
requiredProtocolVersion: 6,
@ -537,6 +563,8 @@ describe('backup/non-bubble messages', () => {
},
received_at: 1,
sent_at: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
timestamp: 1,
},
@ -553,6 +581,8 @@ describe('backup/non-bubble messages', () => {
received_at: 1,
sourceServiceId: CONTACT_A,
sourceDevice: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sent_at: 1,
timestamp: 1,
messageRequestResponseEvent: MessageRequestResponseEvent.SPAM,

View file

@ -70,6 +70,9 @@ describe('sql/getCallHistoryGroups', () => {
beforeEach(async () => {
await removeAll();
});
after(async () => {
await removeAll();
});
it('should merge related items in order', async () => {
const now = Date.now();

View file

@ -10,7 +10,7 @@ import { SignalService as Proto } from '../protobuf';
import * as Bytes from '../Bytes';
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 {
Direct = 'Direct',
Group = 'Group',
@ -22,11 +22,15 @@ export enum CallType {
Video = 'Video',
Group = 'Group',
Adhoc = 'Adhoc',
// Only used for backup roundtripping
Unknown = 'Unknown',
}
export enum CallDirection {
Incoming = 'Incoming',
Outgoing = 'Outgoing',
// Only used for backup roundtripping
Unknown = 'Unknown',
}
export enum CallLogEvent {
@ -59,6 +63,8 @@ export enum CallStatusValue {
Pending = 'Pending',
Accepted = 'Accepted',
Missed = 'Missed',
// TODO: DESKTOP-3483 - not generated locally
MissedNotificationProfile = 'MissedNotificationProfile',
Declined = 'Declined',
Deleted = 'Deleted',
GenericGroupCall = 'GenericGroupCall',
@ -67,14 +73,20 @@ export enum CallStatusValue {
Ringing = 'Ringing',
Joined = 'Joined',
JoinedAdhoc = 'JoinedAdhoc',
// Only used for backup roundtripping
Unknown = 'Unknown',
}
export enum DirectCallStatus {
Pending = CallStatusValue.Pending,
Accepted = CallStatusValue.Accepted,
Missed = CallStatusValue.Missed,
// TODO: DESKTOP-3483 - not generated locally
MissedNotificationProfile = CallStatusValue.MissedNotificationProfile,
Declined = CallStatusValue.Declined,
Deleted = CallStatusValue.Deleted,
// Only used for backup roundtripping
Unknown = CallStatusValue.Unknown,
}
export enum GroupCallStatus {
@ -84,6 +96,8 @@ export enum GroupCallStatus {
Joined = CallStatusValue.Joined,
Accepted = CallStatusValue.Accepted,
Missed = CallStatusValue.Missed,
// TODO: DESKTOP-3483 - not generated locally
MissedNotificationProfile = CallStatusValue.MissedNotificationProfile,
Declined = CallStatusValue.Declined,
Deleted = CallStatusValue.Deleted,
}
@ -93,6 +107,8 @@ export enum AdhocCallStatus {
Pending = CallStatusValue.Pending,
Joined = CallStatusValue.JoinedAdhoc,
Deleted = CallStatusValue.Deleted,
// Only used for backup roundtripping
Unknown = CallStatusValue.Unknown,
}
export type CallStatus = DirectCallStatus | GroupCallStatus | AdhocCallStatus;

View file

@ -284,6 +284,7 @@ export function getCallLogEventForProto(
const directionToProto = {
[CallDirection.Incoming]: Proto.SyncMessage.CallEvent.Direction.INCOMING,
[CallDirection.Outgoing]: Proto.SyncMessage.CallEvent.Direction.OUTGOING,
[CallDirection.Unknown]: Proto.SyncMessage.CallEvent.Direction.UNKNOWN,
};
const typeToProto = {
@ -291,6 +292,7 @@ const typeToProto = {
[CallType.Video]: Proto.SyncMessage.CallEvent.Type.VIDEO_CALL,
[CallType.Group]: Proto.SyncMessage.CallEvent.Type.GROUP_CALL,
[CallType.Adhoc]: Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL,
[CallType.Unknown]: Proto.SyncMessage.CallEvent.Type.UNKNOWN,
};
const statusToProto: Record<
@ -301,6 +303,7 @@ const statusToProto: Record<
[CallStatusValue.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED,
[CallStatusValue.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE,
[CallStatusValue.Missed]: null,
[CallStatusValue.MissedNotificationProfile]: null,
[CallStatusValue.Pending]: null,
[CallStatusValue.GenericGroupCall]: null,
[CallStatusValue.GenericAdhocCall]:
@ -309,6 +312,7 @@ const statusToProto: Record<
[CallStatusValue.Ringing]: null,
[CallStatusValue.Joined]: null,
[CallStatusValue.JoinedAdhoc]: Proto.SyncMessage.CallEvent.Event.ACCEPTED,
[CallStatusValue.Unknown]: Proto.SyncMessage.CallEvent.Event.UNKNOWN,
};
function shouldSyncStatus(callStatus: CallStatus) {
@ -681,12 +685,16 @@ function transitionTimestamp(
// We don't care about holding onto timestamps that were from these states
if (
callHistory.status === DirectCallStatus.Pending ||
callHistory.status === DirectCallStatus.Unknown ||
callHistory.status === GroupCallStatus.GenericGroupCall ||
callHistory.status === GroupCallStatus.OutgoingRing ||
callHistory.status === GroupCallStatus.Ringing ||
callHistory.status === DirectCallStatus.Missed ||
callHistory.status === DirectCallStatus.MissedNotificationProfile ||
callHistory.status === GroupCallStatus.Missed ||
callHistory.status === AdhocCallStatus.Pending
callHistory.status === GroupCallStatus.MissedNotificationProfile ||
callHistory.status === AdhocCallStatus.Pending ||
callHistory.status === AdhocCallStatus.Unknown
) {
return latestTimestamp;
}
@ -801,6 +809,7 @@ function transitionGroupCallStatus(
}
case GroupCallStatus.Ringing:
case GroupCallStatus.Missed:
case GroupCallStatus.MissedNotificationProfile:
case GroupCallStatus.Declined: {
return GroupCallStatus.Accepted;
}

View file

@ -35,7 +35,10 @@ export function getDirectCallNotificationText(
callStatus: DirectCallStatus,
i18n: LocalizerType
): string {
if (callStatus === DirectCallStatus.Pending) {
if (
callStatus === DirectCallStatus.Pending ||
callStatus === DirectCallStatus.Unknown
) {
if (callDirection === CallDirection.Incoming) {
return callType === CallType.Video
? i18n('icu:incomingVideoCall')
@ -68,7 +71,10 @@ export function getDirectCallNotificationText(
: i18n('icu:missedOrDeclinedOutgoingAudioCall');
}
if (callStatus === DirectCallStatus.Missed) {
if (
callStatus === DirectCallStatus.Missed ||
callStatus === DirectCallStatus.MissedNotificationProfile
) {
if (callDirection === CallDirection.Incoming) {
return callType === CallType.Video
? i18n('icu:missedIncomingVideoCall')
@ -219,5 +225,8 @@ export function getCallingIcon(
if (callType === CallType.Group || callType === CallType.Adhoc) {
return 'video';
}
if (callType === CallType.Unknown) {
return 'video';
}
throw missingCaseError(callType);
}