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

@ -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;
}