Backup Server APIs

This commit is contained in:
Fedor Indutny 2024-04-22 16:11:36 +02:00 committed by GitHub
parent 77aea40a63
commit 3eb0e30a23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 991 additions and 201 deletions

View file

@ -49,6 +49,7 @@ import {
untaggedPniSchema,
} from '../types/ServiceId';
import type { DirectoryConfigType } from '../types/RendererConfig';
import type { BackupPresentationHeadersType } from '../types/backups';
import * as Bytes from '../Bytes';
import { randomInt } from '../Crypto';
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
@ -536,6 +537,10 @@ const URL_CALLS = {
'dynamic/desktop/stories/onboarding/manifest.json',
getStickerPackUpload: 'v1/sticker/pack/form',
getArtAuth: 'v1/art/auth',
getBackupCredentials: 'v1/archives/auth',
getBackupCDNCredentials: 'v1/archives/auth/read',
getBackupUploadForm: 'v1/archives/upload/form',
getBackupMediaUploadForm: 'v1/archives/media/upload/form',
groupLog: 'v1/groups/logs',
groupJoinedAtVersion: 'v1/groups/joined_at_version',
groups: 'v1/groups',
@ -547,9 +552,15 @@ const URL_CALLS = {
multiRecipient: 'v1/messages/multi_recipient',
phoneNumberDiscoverability: 'v2/accounts/phone_number_discoverability',
profile: 'v1/profile',
backup: 'v1/archives',
backupMedia: 'v1/archives/media',
backupMediaBatch: 'v1/archives/media/batch',
backupMediaDelete: 'v1/archives/media/delete',
registration: 'v1/registration',
registerCapabilities: 'v1/devices/capabilities',
reportMessage: 'v1/messages/report',
setBackupId: 'v1/archives/backupid',
setBackupSignatureKey: 'v1/archives/keys',
signed: 'v2/keys/signed',
storageManifest: 'v1/storage/manifest',
storageModify: 'v1/storage/',
@ -599,6 +610,18 @@ const WEBSOCKET_CALLS = new Set<keyof typeof URL_CALLS>([
// Account V2
'phoneNumberDiscoverability',
// Backups
'getBackupCredentials',
'getBackupCDNCredentials',
'getBackupMediaUploadForm',
'getBackupUploadForm',
'backup',
'backupMedia',
'backupMediaBatch',
'backupMediaDelete',
'setBackupId',
'setBackupSignatureKey',
]);
type InitializeOptionsType = {
@ -982,6 +1005,136 @@ export type RequestVerificationResultType = Readonly<{
sessionId: string;
}>;
export type SetBackupIdOptionsType = Readonly<{
backupAuthCredentialRequest: Uint8Array;
}>;
export type SetBackupSignatureKeyOptionsType = Readonly<{
headers: BackupPresentationHeadersType;
backupIdPublicKey: Uint8Array;
}>;
export type BackupMediaItemType = Readonly<{
sourceAttachment: Readonly<{
cdn: number;
key: string;
}>;
objectLength: number;
mediaId: string;
hmacKey: Uint8Array;
encryptionKey: Uint8Array;
iv: Uint8Array;
}>;
export type BackupMediaBatchOptionsType = Readonly<{
headers: BackupPresentationHeadersType;
items: ReadonlyArray<BackupMediaItemType>;
}>;
export const backupMediaBatchResponseSchema = z.object({
responses: z
.object({
status: z.number(),
failureReason: z.string().or(z.null()).optional(),
cdn: z.number(),
mediaId: z.string(),
})
.array(),
});
export type BackupMediaBatchResponseType = z.infer<
typeof backupMediaBatchResponseSchema
>;
export type BackupListMediaOptionsType = Readonly<{
headers: BackupPresentationHeadersType;
cursor?: string;
limit: number;
}>;
export const backupListMediaResponseSchema = z.object({
storedMediaObjects: z
.object({
cdn: z.number(),
mediaId: z.string(),
objectLength: z.number(),
})
.array(),
backupDir: z.string(),
mediaDir: z.string(),
cursor: z.string().or(z.null()).optional(),
});
export type BackupListMediaResponseType = z.infer<
typeof backupListMediaResponseSchema
>;
export type BackupDeleteMediaItemType = Readonly<{
cdn: number;
mediaId: string;
}>;
export type BackupDeleteMediaOptionsType = Readonly<{
headers: BackupPresentationHeadersType;
mediaToDelete: ReadonlyArray<BackupDeleteMediaItemType>;
}>;
export type GetBackupCredentialsOptionsType = Readonly<{
startDayInMs: number;
endDayInMs: number;
}>;
export const getBackupCredentialsResponseSchema = z.object({
credentials: z
.object({
credential: z.string().transform(x => Bytes.fromBase64(x)),
redemptionTime: z
.number()
.transform(x => durations.DurationInSeconds.fromSeconds(x)),
})
.array(),
});
export type GetBackupCredentialsResponseType = z.infer<
typeof getBackupCredentialsResponseSchema
>;
export type GetBackupCDNCredentialsOptionsType = Readonly<{
headers: BackupPresentationHeadersType;
cdn: number;
}>;
export const getBackupCDNCredentialsResponseSchema = z.object({
headers: z.record(z.string(), z.string()),
});
export type GetBackupCDNCredentialsResponseType = z.infer<
typeof getBackupCDNCredentialsResponseSchema
>;
export const getBackupInfoResponseSchema = z.object({
cdn: z.number(),
backupDir: z.string(),
mediaDir: z.string(),
backupName: z.string(),
usedSpace: z.number().or(z.null()).optional(),
});
export type GetBackupInfoResponseType = z.infer<
typeof getBackupInfoResponseSchema
>;
export const getBackupUploadFormResponseSchema = z.object({
cdn: z.number(),
key: z.string(),
headers: z.record(z.string(), z.string()),
signedUploadLocation: z.string(),
});
export type GetBackupUploadFormResponseType = z.infer<
typeof getBackupUploadFormResponseSchema
>;
export type WebAPIType = {
startRegistration(): unknown;
finishRegistration(baton: unknown): void;
@ -1166,6 +1319,33 @@ export type WebAPIType = {
urgent?: boolean;
}
) => Promise<MultiRecipient200ResponseType>;
getBackupInfo: (
headers: BackupPresentationHeadersType
) => Promise<GetBackupInfoResponseType>;
getBackupUploadForm: (
headers: BackupPresentationHeadersType
) => Promise<GetBackupUploadFormResponseType>;
getBackupMediaUploadForm: (
headers: BackupPresentationHeadersType
) => Promise<GetBackupUploadFormResponseType>;
refreshBackup: (headers: BackupPresentationHeadersType) => Promise<void>;
getBackupCredentials: (
options: GetBackupCredentialsOptionsType
) => Promise<GetBackupCredentialsResponseType>;
getBackupCDNCredentials: (
options: GetBackupCDNCredentialsOptionsType
) => Promise<GetBackupCDNCredentialsResponseType>;
setBackupId: (options: SetBackupIdOptionsType) => Promise<void>;
setBackupSignatureKey: (
options: SetBackupSignatureKeyOptionsType
) => Promise<void>;
backupMediaBatch: (
options: BackupMediaBatchOptionsType
) => Promise<BackupMediaBatchResponseType>;
backupListMedia: (
options: BackupListMediaOptionsType
) => Promise<BackupListMediaResponseType>;
backupDeleteMedia: (options: BackupDeleteMediaOptionsType) => Promise<void>;
setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>;
updateDeviceName: (deviceName: string) => Promise<void>;
uploadAvatar: (
@ -1447,6 +1627,9 @@ export function initialize({
// Thanks, function hoisting!
return {
authenticate,
backupDeleteMedia,
backupListMedia,
backupMediaBatch,
cancelInflightRequests,
cdsLookup,
checkAccountExistence,
@ -1466,6 +1649,11 @@ export function initialize({
getAttachment,
getAttachmentV2,
getAvatar,
getBackupCredentials,
getBackupCDNCredentials,
getBackupInfo,
getBackupMediaUploadForm,
getBackupUploadForm,
getBadgeImageFile,
getConfig,
getGroup,
@ -1507,6 +1695,7 @@ export function initialize({
putProfile,
putStickers,
reconnect,
refreshBackup,
registerCapabilities,
registerKeys,
registerRequestHandler,
@ -1520,6 +1709,8 @@ export function initialize({
sendMessages,
sendMessagesUnauth,
sendWithSenderKey,
setBackupId,
setBackupSignatureKey,
setPhoneNumberDiscoverability,
startRegistration,
unregisterRequestHandler,
@ -2497,6 +2688,208 @@ export function initialize({
});
}
async function getBackupInfo(headers: BackupPresentationHeadersType) {
const res = await _ajax({
call: 'backup',
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
headers,
responseType: 'json',
});
return getBackupInfoResponseSchema.parse(res);
}
async function getBackupMediaUploadForm(
headers: BackupPresentationHeadersType
) {
const res = await _ajax({
call: 'getBackupMediaUploadForm',
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
headers,
responseType: 'json',
});
return getBackupUploadFormResponseSchema.parse(res);
}
async function getBackupUploadForm(headers: BackupPresentationHeadersType) {
const res = await _ajax({
call: 'getBackupUploadForm',
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
headers,
responseType: 'json',
});
return getBackupUploadFormResponseSchema.parse(res);
}
async function refreshBackup(headers: BackupPresentationHeadersType) {
await _ajax({
call: 'backup',
httpType: 'POST',
unauthenticated: true,
accessKey: undefined,
headers,
});
}
async function getBackupCredentials({
startDayInMs,
endDayInMs,
}: GetBackupCredentialsOptionsType) {
const startDayInSeconds = startDayInMs / durations.SECOND;
const endDayInSeconds = endDayInMs / durations.SECOND;
const res = await _ajax({
call: 'getBackupCredentials',
httpType: 'GET',
urlParameters:
`?redemptionStartSeconds=${startDayInSeconds}&` +
`redemptionEndSeconds=${endDayInSeconds}`,
responseType: 'json',
});
return getBackupCredentialsResponseSchema.parse(res);
}
async function getBackupCDNCredentials({
headers,
cdn,
}: GetBackupCDNCredentialsOptionsType) {
const res = await _ajax({
call: 'getBackupCDNCredentials',
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
headers,
urlParameters: `?cdn=${cdn}`,
responseType: 'json',
});
return getBackupCDNCredentialsResponseSchema.parse(res);
}
async function setBackupId({
backupAuthCredentialRequest,
}: SetBackupIdOptionsType) {
await _ajax({
call: 'setBackupId',
httpType: 'PUT',
jsonData: {
backupAuthCredentialRequest: Bytes.toBase64(
backupAuthCredentialRequest
),
},
});
}
async function setBackupSignatureKey({
headers,
backupIdPublicKey,
}: SetBackupSignatureKeyOptionsType) {
await _ajax({
call: 'setBackupSignatureKey',
httpType: 'PUT',
unauthenticated: true,
accessKey: undefined,
headers,
jsonData: {
backupIdPublicKey: Bytes.toBase64(backupIdPublicKey),
},
});
}
async function backupMediaBatch({
headers,
items,
}: BackupMediaBatchOptionsType) {
const res = await _ajax({
call: 'backupMediaBatch',
httpType: 'PUT',
unauthenticated: true,
accessKey: undefined,
headers,
responseType: 'json',
jsonData: {
items: items.map(item => {
const {
sourceAttachment,
objectLength,
mediaId,
hmacKey,
encryptionKey,
iv,
} = item;
return {
sourceAttachment: {
cdn: sourceAttachment.cdn,
key: sourceAttachment.key,
},
objectLength,
mediaId,
hmacKey: Bytes.toBase64(hmacKey),
encryptionKey: Bytes.toBase64(encryptionKey),
iv: Bytes.toBase64(iv),
};
}),
},
});
return backupMediaBatchResponseSchema.parse(res);
}
async function backupDeleteMedia({
headers,
mediaToDelete,
}: BackupDeleteMediaOptionsType) {
await _ajax({
call: 'backupMediaDelete',
httpType: 'POST',
unauthenticated: true,
accessKey: undefined,
headers,
jsonData: {
mediaToDelete: mediaToDelete.map(({ cdn, mediaId }) => {
return {
cdn,
mediaId,
};
}),
},
});
}
async function backupListMedia({
headers,
cursor,
limit,
}: BackupListMediaOptionsType) {
const params = new Array<string>();
if (cursor != null) {
params.push(`cursor=${encodeURIComponent(cursor)}`);
}
params.push(`limit=${limit}`);
const res = await _ajax({
call: 'backupMedia',
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
headers,
responseType: 'json',
urlParameters: `?${params.join('&')}`,
});
return backupListMediaResponseSchema.parse(res);
}
async function setPhoneNumberDiscoverability(newValue: boolean) {
await _ajax({
call: 'phoneNumberDiscoverability',