Backup: Support for calls
This commit is contained in:
parent
3a631a587f
commit
63e14a7df6
15 changed files with 1047 additions and 116 deletions
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue