mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Integrate call links create/update/read apis.
This commit is contained in:
committed by
Nicholas Tinsley
parent
4d6d31d624
commit
5a38143987
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer
|
||||
@@ -56,11 +55,17 @@ object CallEventSyncMessageUtil {
|
||||
event: CallEvent.Event
|
||||
): CallEvent {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val isGroupCall = recipient.isGroup
|
||||
val conversationId: ByteString = if (isGroupCall) {
|
||||
recipient.requireGroupId().decodedId.toProtoByteString()
|
||||
} else {
|
||||
recipient.requireServiceId().toByteString()
|
||||
val callType = when {
|
||||
recipient.isCallLink -> CallEvent.Type.AD_HOC_CALL
|
||||
recipient.isGroup -> CallEvent.Type.GROUP_CALL
|
||||
isVideoCall -> CallEvent.Type.VIDEO_CALL
|
||||
else -> CallEvent.Type.AUDIO_CALL
|
||||
}
|
||||
|
||||
val conversationId: ByteString = when {
|
||||
recipient.isCallLink -> recipient.requireCallLinkRoomId().encodeForProto()
|
||||
recipient.isGroup -> ByteString.copyFrom(recipient.requireGroupId().decodedId)
|
||||
else -> recipient.requireServiceId().toByteString()
|
||||
}
|
||||
|
||||
return CallEvent
|
||||
@@ -68,13 +73,7 @@ object CallEventSyncMessageUtil {
|
||||
.setConversationId(conversationId)
|
||||
.setId(callId)
|
||||
.setTimestamp(timestamp)
|
||||
.setType(
|
||||
when {
|
||||
isGroupCall -> CallEvent.Type.GROUP_CALL
|
||||
isVideoCall -> CallEvent.Type.VIDEO_CALL
|
||||
else -> CallEvent.Type.AUDIO_CALL
|
||||
}
|
||||
)
|
||||
.setType(callType)
|
||||
.setDirection(if (isOutgoing) CallEvent.Direction.OUTGOING else CallEvent.Direction.INCOMING)
|
||||
.setEvent(event)
|
||||
.build()
|
||||
|
||||
@@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
@@ -81,6 +82,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@@ -992,6 +994,10 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull SignalCallLinkManager getCallLinkManager() {
|
||||
return new SignalCallLinkManager(Objects.requireNonNull(callManager));
|
||||
}
|
||||
|
||||
private void processSendMessageFailureWithChangeDetection(@NonNull RemotePeer remotePeer,
|
||||
@NonNull ProcessAction failureProcessAction)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc.links
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
|
||||
/**
|
||||
* Holds onto the credentials for a given call link.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CallLinkCredentials(
|
||||
val linkKeyBytes: ByteArray,
|
||||
val adminPassBytes: ByteArray?
|
||||
) : Parcelable {
|
||||
|
||||
val roomId: CallLinkRoomId by lazy {
|
||||
CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(linkKeyBytes))
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CallLinkCredentials
|
||||
|
||||
if (!linkKeyBytes.contentEquals(other.linkKeyBytes)) return false
|
||||
if (adminPassBytes != null) {
|
||||
if (other.adminPassBytes == null) return false
|
||||
if (!adminPassBytes.contentEquals(other.adminPassBytes)) return false
|
||||
} else if (other.adminPassBytes != null) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = linkKeyBytes.contentHashCode()
|
||||
result = 31 * result + (adminPassBytes?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Generate a new call link credential for creating a new call.
|
||||
*/
|
||||
fun generate(): CallLinkCredentials {
|
||||
return CallLinkCredentials(
|
||||
CallLinkRootKey.generate().keyBytes,
|
||||
CallLinkRootKey.generateAdminPasskey()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc.links
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
@Parcelize
|
||||
class CallLinkRoomId private constructor(private val roomId: ByteArray) : Parcelable {
|
||||
fun serialize(): String = Base64.encodeBytes(roomId)
|
||||
|
||||
fun encodeForProto(): ByteString = ByteString.copyFrom(roomId)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromBytes(byteArray: ByteArray): CallLinkRoomId {
|
||||
return CallLinkRoomId(byteArray)
|
||||
}
|
||||
|
||||
fun fromCallLinkRootKey(callLinkRootKey: CallLinkRootKey): CallLinkRoomId {
|
||||
return CallLinkRoomId(callLinkRootKey.deriveRoomId())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc.links
|
||||
|
||||
/**
|
||||
* Result type for call link creation.
|
||||
*/
|
||||
sealed interface CreateCallLinkResult {
|
||||
data class Success(
|
||||
val credentials: CallLinkCredentials,
|
||||
val state: SignalCallLinkState
|
||||
) : CreateCallLinkResult
|
||||
|
||||
data class Failure(
|
||||
val status: Short
|
||||
) : CreateCallLinkResult
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc.links
|
||||
|
||||
/**
|
||||
* Result type for call link reads.
|
||||
*/
|
||||
sealed interface ReadCallLinkResult {
|
||||
data class Success(
|
||||
val callLinkState: SignalCallLinkState
|
||||
) : ReadCallLinkResult
|
||||
|
||||
data class Failure(val status: Short) : ReadCallLinkResult
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc.links
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.isAbsent
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.or
|
||||
import org.signal.libsignal.zkgroup.GenericServerPublicParams
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialPresentation
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkSecretParams
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredential
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialPresentation
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequestContext
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialResponse
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
import org.signal.ringrtc.CallLinkState.Restrictions
|
||||
import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Call Link manager which encapsulates CallManager and provides a stable interface.
|
||||
*
|
||||
* We can remove the outer sealed class once we have the final, working builds from core.
|
||||
*/
|
||||
class SignalCallLinkManager(
|
||||
private val callManager: CallManager
|
||||
) {
|
||||
|
||||
private val genericServerPublicParams: GenericServerPublicParams = GenericServerPublicParams(
|
||||
ApplicationDependencies.getSignalServiceNetworkAccess()
|
||||
.getConfiguration()
|
||||
.genericServerPublicParams
|
||||
)
|
||||
|
||||
private fun requestCreateCallLinkCredentailPresentation(
|
||||
linkRootKey: ByteArray,
|
||||
roomId: ByteArray
|
||||
): CreateCallLinkCredentialPresentation {
|
||||
val userUuid = Recipient.self().requireServiceId().uuid()
|
||||
val requestContext = CreateCallLinkCredentialRequestContext.forRoom(roomId)
|
||||
val request = requestContext.request
|
||||
|
||||
Log.d(TAG, "Requesting call link credential response.")
|
||||
|
||||
val serviceResponse: ServiceResponse<CreateCallLinkCredentialResponse> = ApplicationDependencies.getCallLinksService().getCreateCallLinkAuthCredential(request)
|
||||
if (serviceResponse.result.isAbsent()) {
|
||||
throw IOException("Failed to create credential response", serviceResponse.applicationError.or(serviceResponse.executionError).get())
|
||||
}
|
||||
|
||||
Log.d(TAG, "Requesting call link credential.")
|
||||
|
||||
val createCallLinkCredential: CreateCallLinkCredential = requestContext.receiveResponse(
|
||||
serviceResponse.result.get(),
|
||||
userUuid,
|
||||
genericServerPublicParams
|
||||
)
|
||||
|
||||
Log.d(TAG, "Requesting and returning call link presentation.")
|
||||
|
||||
return createCallLinkCredential.present(
|
||||
roomId,
|
||||
userUuid,
|
||||
genericServerPublicParams,
|
||||
CallLinkSecretParams.deriveFromRootKey(linkRootKey)
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestCallLinkAuthCredentialPresentation(
|
||||
linkRootKey: ByteArray
|
||||
): CallLinkAuthCredentialPresentation {
|
||||
return ApplicationDependencies.getGroupsV2Authorization().getCallLinkAuthorizationForToday(
|
||||
genericServerPublicParams,
|
||||
CallLinkSecretParams.deriveFromRootKey(linkRootKey)
|
||||
)
|
||||
}
|
||||
|
||||
fun createCallLink(
|
||||
callLinkCredentials: CallLinkCredentials
|
||||
): Single<CreateCallLinkResult> {
|
||||
return Single.create { emitter ->
|
||||
Log.d(TAG, "Generating keys.")
|
||||
|
||||
val rootKey = CallLinkRootKey(callLinkCredentials.linkKeyBytes)
|
||||
val adminPassKey: ByteArray = requireNotNull(callLinkCredentials.adminPassBytes)
|
||||
val roomId: ByteArray = rootKey.deriveRoomId()
|
||||
|
||||
Log.d(TAG, "Generating credential.")
|
||||
val credentialPresentation = try {
|
||||
requestCreateCallLinkCredentailPresentation(
|
||||
rootKey.keyBytes,
|
||||
roomId
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to create call link credential.", e)
|
||||
emitter.onError(e)
|
||||
return@create
|
||||
}
|
||||
|
||||
Log.d(TAG, "Creating call link.")
|
||||
|
||||
val publicParams = CallLinkSecretParams.deriveFromRootKey(rootKey.keyBytes).publicParams
|
||||
|
||||
// Credential
|
||||
callManager.createCallLink(
|
||||
SignalStore.internalValues().groupCallingServer(),
|
||||
credentialPresentation.serialize(),
|
||||
rootKey,
|
||||
adminPassKey,
|
||||
publicParams.serialize()
|
||||
) { result ->
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "Successfully created call link.")
|
||||
emitter.onSuccess(
|
||||
CreateCallLinkResult.Success(
|
||||
credentials = CallLinkCredentials(rootKey.keyBytes, adminPassKey),
|
||||
state = result.value!!.toAppState()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "Failed to create call link with failure status ${result.status}")
|
||||
emitter.onSuccess(CreateCallLinkResult.Failure(result.status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readCallLink(
|
||||
credentials: CallLinkCredentials
|
||||
): Single<ReadCallLinkResult> {
|
||||
return Single.create { emitter ->
|
||||
|
||||
callManager.readCallLink(
|
||||
SignalStore.internalValues().groupCallingServer(),
|
||||
requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes).serialize(),
|
||||
CallLinkRootKey(credentials.linkKeyBytes)
|
||||
) {
|
||||
if (it.isSuccess) {
|
||||
emitter.onSuccess(ReadCallLinkResult.Success(it.value!!.toAppState()))
|
||||
} else {
|
||||
Log.w(TAG, "Failed to read call link with failure status ${it.status}")
|
||||
emitter.onSuccess(ReadCallLinkResult.Failure(it.status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCallLinkName(
|
||||
credentials: CallLinkCredentials,
|
||||
name: String
|
||||
): Single<UpdateCallLinkResult> {
|
||||
if (credentials.adminPassBytes == null) {
|
||||
return Single.just(UpdateCallLinkResult.NotAuthorized)
|
||||
}
|
||||
|
||||
return Single.create { emitter ->
|
||||
val credentialPresentation = requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes)
|
||||
|
||||
callManager.updateCallLinkName(
|
||||
SignalStore.internalValues().groupCallingServer(),
|
||||
credentialPresentation.serialize(),
|
||||
CallLinkRootKey(credentials.linkKeyBytes),
|
||||
credentials.adminPassBytes,
|
||||
name
|
||||
) { result ->
|
||||
if (result.isSuccess) {
|
||||
emitter.onSuccess(UpdateCallLinkResult.Success(result.value!!.toAppState()))
|
||||
} else {
|
||||
emitter.onSuccess(UpdateCallLinkResult.Failure(result.status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCallLinkRestrictions(
|
||||
credentials: CallLinkCredentials,
|
||||
restrictions: Restrictions
|
||||
): Single<UpdateCallLinkResult> {
|
||||
if (credentials.adminPassBytes == null) {
|
||||
return Single.just(UpdateCallLinkResult.NotAuthorized)
|
||||
}
|
||||
|
||||
return Single.create { emitter ->
|
||||
val credentialPresentation = requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes)
|
||||
|
||||
callManager.updateCallLinkRestrictions(
|
||||
SignalStore.internalValues().groupCallingServer(),
|
||||
credentialPresentation.serialize(),
|
||||
CallLinkRootKey(credentials.linkKeyBytes),
|
||||
credentials.adminPassBytes,
|
||||
restrictions
|
||||
) { result ->
|
||||
if (result.isSuccess) {
|
||||
emitter.onSuccess(UpdateCallLinkResult.Success(result.value!!.toAppState()))
|
||||
} else {
|
||||
emitter.onSuccess(UpdateCallLinkResult.Failure(result.status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCallLinkRevoked(
|
||||
credentials: CallLinkCredentials,
|
||||
revoked: Boolean
|
||||
): Single<UpdateCallLinkResult> {
|
||||
if (credentials.adminPassBytes == null) {
|
||||
return Single.just(UpdateCallLinkResult.NotAuthorized)
|
||||
}
|
||||
|
||||
return Single.create { emitter ->
|
||||
val credentialPresentation = requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes)
|
||||
|
||||
callManager.updateCallLinkRevoked(
|
||||
SignalStore.internalValues().groupCallingServer(),
|
||||
credentialPresentation.serialize(),
|
||||
CallLinkRootKey(credentials.linkKeyBytes),
|
||||
credentials.adminPassBytes,
|
||||
revoked
|
||||
) { result ->
|
||||
if (result.isSuccess) {
|
||||
emitter.onSuccess(UpdateCallLinkResult.Success(result.value!!.toAppState()))
|
||||
} else {
|
||||
emitter.onSuccess(UpdateCallLinkResult.Failure(result.status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = Log.tag(SignalCallLinkManager::class.java)
|
||||
|
||||
private fun CallLinkState.toAppState(): SignalCallLinkState {
|
||||
return SignalCallLinkState(
|
||||
name = name,
|
||||
expiration = expiration,
|
||||
restrictions = restrictions,
|
||||
revoked = hasBeenRevoked()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc.links
|
||||
|
||||
import org.signal.ringrtc.CallLinkState.Restrictions
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Adapter class between our app code and RingRTC CallLinkState.
|
||||
*/
|
||||
data class SignalCallLinkState(
|
||||
val name: String = "",
|
||||
val restrictions: Restrictions = Restrictions.UNKNOWN,
|
||||
@get:JvmName("hasBeenRevoked") val revoked: Boolean = false,
|
||||
val expiration: Instant = Instant.MAX
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc.links
|
||||
|
||||
/**
|
||||
* Result type for call link updates.
|
||||
*/
|
||||
sealed interface UpdateCallLinkResult {
|
||||
data class Success(
|
||||
val state: SignalCallLinkState
|
||||
) : UpdateCallLinkResult
|
||||
|
||||
class Failure(
|
||||
val status: Short
|
||||
) : UpdateCallLinkResult
|
||||
|
||||
object NotAuthorized : UpdateCallLinkResult
|
||||
}
|
||||
Reference in New Issue
Block a user