Add support for fetching archive media metadata.
This commit is contained in:
parent
cf59249d3d
commit
2194fbd535
6 changed files with 118 additions and 6 deletions
|
@ -26,7 +26,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
|
@ -168,7 +167,7 @@ object BackupRepository {
|
|||
/**
|
||||
* Returns an object with details about the remote backup state.
|
||||
*/
|
||||
fun getRemoteBackupState(): NetworkResult<ArchiveGetBackupInfoResponse> {
|
||||
fun getRemoteBackupState(): NetworkResult<BackupMetadata> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
|
@ -182,6 +181,18 @@ object BackupRepository {
|
|||
}
|
||||
.then { credential ->
|
||||
api.getBackupInfo(backupKey, credential)
|
||||
.map { it to credential }
|
||||
}
|
||||
.then { pair ->
|
||||
val (info, credential) = pair
|
||||
api.debugGetUploadedMediaItemMetadata(backupKey, credential)
|
||||
.also { Log.i(TAG, "MediaItemMetadataResult: $it") }
|
||||
.map { mediaObjects ->
|
||||
BackupMetadata(
|
||||
usedSpace = info.usedSpace ?: 0,
|
||||
mediaCount = mediaObjects.size.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,3 +266,8 @@ class BackupState {
|
|||
val chatIdToBackupRecipientId = HashMap<Long, Long>()
|
||||
val callIdToType = HashMap<Long, Long>()
|
||||
}
|
||||
|
||||
class BackupMetadata(
|
||||
val usedSpace: Long,
|
||||
val mediaCount: Long
|
||||
)
|
||||
|
|
|
@ -200,7 +200,7 @@ fun Screen(
|
|||
|
||||
when (state.remoteBackupState) {
|
||||
is InternalBackupPlaygroundViewModel.RemoteBackupState.Available -> {
|
||||
StateLabel("Exists/allocated. Space used by media: ${state.remoteBackupState.response.usedSpace ?: 0} bytes (${state.remoteBackupState.response.usedSpace?.bytes?.inMebiBytes?.roundedString(3) ?: 0} MiB)")
|
||||
StateLabel("Exists/allocated. ${state.remoteBackupState.response.mediaCount} media items, using ${state.remoteBackupState.response.usedSpace} bytes (${state.remoteBackupState.response.usedSpace.bytes.inMebiBytes.roundedString(3)} MiB)")
|
||||
}
|
||||
InternalBackupPlaygroundViewModel.RemoteBackupState.GeneralError -> {
|
||||
StateLabel("Hit an unknown error. Check the logs.")
|
||||
|
|
|
@ -15,10 +15,10 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupMetadata
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
|
@ -137,6 +137,6 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
object Unknown : RemoteBackupState()
|
||||
object NotFound : RemoteBackupState()
|
||||
object GeneralError : RemoteBackupState()
|
||||
data class Available(val response: ArchiveGetBackupInfoResponse) : RemoteBackupState()
|
||||
data class Available(val response: BackupMetadata) : RemoteBackupState()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.signal.libsignal.zkgroup.backups.BackupAuthCredential
|
|||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequestContext
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse.StoredMediaObject
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
|
@ -120,6 +121,43 @@ class ArchiveApi(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all media items in the user's archive. Note that this could be a very large number of items, making this only suitable for debugging.
|
||||
* Use [getArchiveMediaItemsPage] in production.
|
||||
*/
|
||||
fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)
|
||||
val credentialPresentation = presentationData.toArchiveCredentialPresentation()
|
||||
|
||||
val mediaObjects: MutableList<StoredMediaObject> = ArrayList()
|
||||
|
||||
var cursor: String? = null
|
||||
do {
|
||||
val response: ArchiveGetMediaItemsResponse = pushServiceSocket.getArchiveMediaItemsPage(credentialPresentation, 512, cursor)
|
||||
mediaObjects += response.storedMediaObjects
|
||||
cursor = response.cursor
|
||||
} while (cursor != null)
|
||||
|
||||
mediaObjects
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a page of media items in the user's archive.
|
||||
* @param limit The maximum number of items to return.
|
||||
* @param cursor A token that can be read from your previous response, telling the server where to start the next page.
|
||||
*/
|
||||
fun getArchiveMediaItemsPage(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String): NetworkResult<ArchiveGetMediaItemsResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)
|
||||
|
||||
pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), 512, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential {
|
||||
val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential)
|
||||
val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid)
|
||||
|
@ -127,7 +165,7 @@ class ArchiveApi(
|
|||
return backupRequestContext.receiveResponse(
|
||||
backupAuthResponse,
|
||||
backupServerPublicParams,
|
||||
10
|
||||
20
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/**
|
||||
* Response body for getting the media items stored in the user's archive.
|
||||
*/
|
||||
class ArchiveGetMediaItemsResponse(
|
||||
@JsonProperty val storedMediaObjects: List<StoredMediaObject>,
|
||||
@JsonProperty val cursor: String?
|
||||
) {
|
||||
class StoredMediaObject(
|
||||
@JsonProperty val cdn: Int,
|
||||
@JsonProperty val mediaId: String,
|
||||
@JsonProperty val objectLength: Long
|
||||
)
|
||||
}
|
|
@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest;
|
|||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload;
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveCredentialPresentation;
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse;
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredentialsResponse;
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse;
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveMessageBackupUploadFormResponse;
|
||||
|
@ -161,6 +162,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
@ -311,6 +313,7 @@ public class PushServiceSocket {
|
|||
private static final String ARCHIVE_PUBLIC_KEY = "/v1/archives/keys";
|
||||
private static final String ARCHIVE_INFO = "/v1/archives";
|
||||
private static final String ARCHIVE_MESSAGE_UPLOAD_FORM = "/v1/archives/upload/form";
|
||||
private static final String ARCHIVE_MEDIA_LIST = "/v1/archives/media?limit=%d";
|
||||
|
||||
private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth";
|
||||
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
|
||||
|
@ -512,6 +515,39 @@ public class PushServiceSocket {
|
|||
return JsonUtil.fromJson(response, ArchiveGetBackupInfoResponse.class);
|
||||
}
|
||||
|
||||
public List<ArchiveGetMediaItemsResponse.StoredMediaObject> debugGetAllArchiveMediaItems(ArchiveCredentialPresentation credentialPresentation) throws IOException {
|
||||
List<ArchiveGetMediaItemsResponse.StoredMediaObject> mediaObjects = new ArrayList<>();
|
||||
|
||||
String cursor = null;
|
||||
do {
|
||||
ArchiveGetMediaItemsResponse response = getArchiveMediaItemsPage(credentialPresentation, 512, cursor);
|
||||
mediaObjects.addAll(response.getStoredMediaObjects());
|
||||
cursor = response.getCursor();
|
||||
} while (cursor != null);
|
||||
|
||||
return mediaObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a page of media items in the user's archive.
|
||||
* @param cursor A token that can be read from your previous response, telling the server where to start the next page.
|
||||
*/
|
||||
public ArchiveGetMediaItemsResponse getArchiveMediaItemsPage(ArchiveCredentialPresentation credentialPresentation, int limit, String cursor) throws IOException {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation()));
|
||||
headers.put("X-Signal-ZK-Auth-Signature", Base64.encodeWithPadding(credentialPresentation.getSignedPresentation()));
|
||||
|
||||
String url = String.format(Locale.US, ARCHIVE_MEDIA_LIST, limit);
|
||||
|
||||
if (cursor != null) {
|
||||
url += "&cursor=" + cursor;
|
||||
}
|
||||
|
||||
String response = makeServiceRequestWithoutAuthentication(url, "GET", null, headers, NO_HANDLER);
|
||||
|
||||
return JsonUtil.fromJson(response, ArchiveGetMediaItemsResponse.class);
|
||||
}
|
||||
|
||||
public ArchiveMessageBackupUploadFormResponse getArchiveMessageBackupUploadForm(ArchiveCredentialPresentation credentialPresentation) throws IOException {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation()));
|
||||
|
|
Loading…
Add table
Reference in a new issue