mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Integrate call links create/update/read apis.
This commit is contained in:
committed by
Nicholas Tinsley
parent
4d6d31d624
commit
5a38143987
2
.gitignore
vendored
2
.gitignore
vendored
@@ -28,4 +28,4 @@ jni/libspeex/.deps/
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
maps.key
|
||||
local/
|
||||
local/
|
||||
@@ -210,6 +210,7 @@ android {
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\""
|
||||
buildConfigField "String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||
@@ -385,6 +386,7 @@ android {
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\""
|
||||
buildConfigField "String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\""
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
||||
|
||||
@@ -599,6 +599,14 @@
|
||||
<data android:scheme="sgnl"
|
||||
android:host="signal.me" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https"
|
||||
android:host="signal.link" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".conversation.v2.ConversationActivity"
|
||||
|
||||
@@ -82,6 +82,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
handleProxyInIntent(getIntent());
|
||||
handleSignalMeIntent(getIntent());
|
||||
handleCallLinkInIntent(getIntent());
|
||||
|
||||
CachedInflater.from(this).clear();
|
||||
|
||||
@@ -183,6 +184,13 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallLinkInIntent(Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
CommunicationActions.handlePotentialCallLinkUrl(this, data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public void onFirstRender() {
|
||||
onFirstRender = true;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,83 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import java.net.URLDecoder
|
||||
|
||||
/**
|
||||
* Utility object for call links to try to keep some common logic in one place.
|
||||
*/
|
||||
object CallLinks {
|
||||
fun url(identifier: String) = "https://calls.signal.org/#$identifier"
|
||||
private const val ROOT_KEY = "key"
|
||||
|
||||
private val TAG = Log.tag(CallLinks::class.java)
|
||||
|
||||
fun url(linkKeyBytes: ByteArray) = "https://signal.link/call/#key=${Hex.dump(linkKeyBytes)}"
|
||||
|
||||
fun watchCallLink(roomId: CallLinkRoomId): Observable<CallLinkTable.CallLink> {
|
||||
return Observable.create { emitter ->
|
||||
|
||||
fun refresh() {
|
||||
val callLink = SignalDatabase.callLinks.getCallLinkByRoomId(roomId)
|
||||
if (callLink != null) {
|
||||
emitter.onNext(callLink)
|
||||
}
|
||||
}
|
||||
|
||||
val observer = DatabaseObserver.Observer {
|
||||
refresh()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerCallLinkObserver(roomId, observer)
|
||||
emitter.setCancellable {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseUrl(url: String): CallLinkRootKey? {
|
||||
val parts = url.split("#")
|
||||
if (parts.size != 2) {
|
||||
Log.w(TAG, "Invalid fragment delimiter count in url.")
|
||||
return null
|
||||
}
|
||||
|
||||
val fragment = parts[1]
|
||||
val fragmentParts = fragment.split("&")
|
||||
val fragmentQuery = fragmentParts.associate {
|
||||
val kv = it.split("=")
|
||||
if (kv.size != 2) {
|
||||
Log.w(TAG, "Invalid fragment keypair. Skipping.")
|
||||
}
|
||||
|
||||
val key = URLDecoder.decode(kv[0], "utf8")
|
||||
val value = URLDecoder.decode(kv[1], "utf8")
|
||||
|
||||
key to value
|
||||
}
|
||||
|
||||
val key = fragmentQuery[ROOT_KEY]
|
||||
if (key == null) {
|
||||
Log.w(TAG, "Root key not found in fragment query string.")
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO Parse the key into a byte array
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links
|
||||
|
||||
import android.app.Dialog
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -30,21 +35,34 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import java.time.Instant
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SignalCallRowPreview() {
|
||||
val avatarColor = remember { AvatarColor.random() }
|
||||
val callLink = remember {
|
||||
val credentials = CallLinkCredentials.generate()
|
||||
CallLinkTable.CallLink(
|
||||
name = "Call Name",
|
||||
identifier = "blahblahblah",
|
||||
avatarColor = avatarColor,
|
||||
isApprovalRequired = false
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(credentials.linkKeyBytes)),
|
||||
credentials = credentials,
|
||||
state = SignalCallLinkState(
|
||||
name = "Call Name",
|
||||
restrictions = org.signal.ringrtc.CallLinkState.Restrictions.NONE,
|
||||
expiration = Instant.MAX,
|
||||
revoked = false
|
||||
),
|
||||
avatarColor = avatarColor
|
||||
)
|
||||
}
|
||||
SignalTheme(false) {
|
||||
@@ -95,10 +113,10 @@ fun SignalCallRow(
|
||||
.align(CenterVertically)
|
||||
) {
|
||||
Text(
|
||||
text = callLink.name.ifEmpty { stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__signal_call) }
|
||||
text = callLink.state.name.ifEmpty { stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__signal_call) }
|
||||
)
|
||||
Text(
|
||||
text = CallLinks.url(callLink.identifier),
|
||||
text = callLink.credentials?.let { CallLinks.url(it.linkKeyBytes) } ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
|
||||
/**
|
||||
* Repository for performing update operations on call links:
|
||||
* <ul>
|
||||
* <li>Set name</li>
|
||||
* <li>Set restrictions</li>
|
||||
* <li>Revoke link</li>
|
||||
* </ul>
|
||||
*
|
||||
* All of these will delegate to the [SignalCallLinkManager] but will additionally update the database state.
|
||||
*/
|
||||
class UpdateCallLinkRepository(
|
||||
private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
|
||||
) {
|
||||
fun setCallName(credentials: CallLinkCredentials, name: String): Single<UpdateCallLinkResult> {
|
||||
return callLinkManager
|
||||
.updateCallLinkName(
|
||||
credentials = credentials,
|
||||
name = name
|
||||
)
|
||||
.doOnSuccess(updateState(credentials))
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun setCallRestrictions(credentials: CallLinkCredentials, restrictions: CallLinkState.Restrictions): Single<UpdateCallLinkResult> {
|
||||
return callLinkManager
|
||||
.updateCallLinkRestrictions(
|
||||
credentials = credentials,
|
||||
restrictions = restrictions
|
||||
)
|
||||
.doOnSuccess(updateState(credentials))
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun revokeCallLink(credentials: CallLinkCredentials): Single<UpdateCallLinkResult> {
|
||||
return callLinkManager
|
||||
.updateCallLinkRevoked(credentials, true)
|
||||
.doOnSuccess(updateState(credentials))
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun updateState(credentials: CallLinkCredentials): (UpdateCallLinkResult) -> Unit {
|
||||
return { result ->
|
||||
if (result is UpdateCallLinkResult.Success) {
|
||||
SignalDatabase.callLinks.updateCallLinkState(credentials.roomId, result.state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.create
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
@@ -28,9 +33,13 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
|
||||
@@ -39,6 +48,7 @@ import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
@@ -47,14 +57,20 @@ import org.thoughtcrime.securesms.util.Util
|
||||
*/
|
||||
class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(CreateCallLinkBottomSheetDialogFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel: CreateCallLinkViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
parentFragmentManager.setFragmentResultListener(EditCallLinkNameDialogFragment.RESULT_KEY, viewLifecycleOwner) { resultKey, bundle ->
|
||||
if (bundle.containsKey(resultKey)) {
|
||||
viewModel.setCallName(bundle.getString(resultKey)!!)
|
||||
setCallName(bundle.getString(resultKey)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,10 +110,10 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
checked = callLink.isApprovalRequired,
|
||||
checked = callLink.state.restrictions == CallLinkState.Restrictions.ADMIN_APPROVAL,
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__approve_all_members),
|
||||
onCheckChanged = viewModel::setApproveAllMembers,
|
||||
modifier = Modifier.clickable(onClick = viewModel::toggleApproveAllMembers)
|
||||
onCheckChanged = this@CreateCallLinkBottomSheetDialogFragment::setApproveAllMembers,
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::toggleApproveAllMembers)
|
||||
)
|
||||
|
||||
Dividers.Default()
|
||||
@@ -133,54 +149,82 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCallName(callName: String) {
|
||||
lifecycleDisposable += viewModel.setCallName(callName).subscribeBy {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setApproveAllMembers(approveAllMembers: Boolean) {
|
||||
lifecycleDisposable += viewModel.setApproveAllMembers(approveAllMembers).subscribeBy {
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleApproveAllMembers() {
|
||||
lifecycleDisposable += viewModel.toggleApproveAllMembers().subscribeBy {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAddACallNameClicked() {
|
||||
val snapshot = viewModel.callLink.value
|
||||
findNavController().navigate(
|
||||
CreateCallLinkBottomSheetDialogFragmentDirections.actionCreateCallLinkBottomSheetToEditCallLinkNameDialogFragment(snapshot.name)
|
||||
CreateCallLinkBottomSheetDialogFragmentDirections.actionCreateCallLinkBottomSheetToEditCallLinkNameDialogFragment(snapshot.state.name)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onJoinClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDoneClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> dismissAllowingStateLoss()
|
||||
is EnsureCallLinkCreatedResult.Failure -> handleCreateCallLinkFailure(it.failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreateCallLinkFailure(failure: CreateCallLinkResult.Failure) {
|
||||
Log.w(TAG, "Failed to create call link: $failure")
|
||||
}
|
||||
|
||||
private fun onShareViaSignalClicked() {
|
||||
val snapshot = viewModel.callLink.value
|
||||
|
||||
MultiselectForwardFragment.showFullScreen(
|
||||
childFragmentManager,
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = false,
|
||||
multiShareArgs = listOf(
|
||||
MultiShareArgs.Builder()
|
||||
.withDraftText(snapshot.identifier)
|
||||
.build()
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
MultiselectForwardFragment.showFullScreen(
|
||||
childFragmentManager,
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = false,
|
||||
multiShareArgs = listOf(
|
||||
MultiShareArgs.Builder()
|
||||
.withDraftText(CallLinks.url(viewModel.linkKeyBytes))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCopyLinkClicked() {
|
||||
val snapshot = viewModel.callLink.value
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(snapshot.identifier))
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes))
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShareLinkClicked() {
|
||||
val snapshot = viewModel.callLink.value
|
||||
val mimeType = Intent.normalizeMimeType("text/plain")
|
||||
val shareIntent = ShareCompat.IntentBuilder(requireContext())
|
||||
.setText(CallLinks.url(snapshot.identifier))
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
val mimeType = Intent.normalizeMimeType("text/plain")
|
||||
val shareIntent = ShareCompat.IntentBuilder(requireContext())
|
||||
.setText(CallLinks.url(viewModel.linkKeyBytes))
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
|
||||
try {
|
||||
startActivity(shareIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__failed_to_open_share_sheet, Toast.LENGTH_LONG).show()
|
||||
try {
|
||||
startActivity(shareIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__failed_to_open_share_sheet, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.create
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
|
||||
/**
|
||||
* Repository for creating new call links. This will delegate to the [SignalCallLinkManager]
|
||||
* but will also ensure the database is updated.
|
||||
*/
|
||||
class CreateCallLinkRepository(
|
||||
private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
|
||||
) {
|
||||
fun ensureCallLinkCreated(credentials: CallLinkCredentials, avatarColor: AvatarColor): Single<EnsureCallLinkCreatedResult> {
|
||||
val doesCallLinkExistInLocalDatabase = Single.fromCallable {
|
||||
SignalDatabase.callLinks.callLinkExists(credentials.roomId)
|
||||
}
|
||||
|
||||
return doesCallLinkExistInLocalDatabase.flatMap { exists ->
|
||||
if (exists) {
|
||||
Single.just(EnsureCallLinkCreatedResult.Success)
|
||||
} else {
|
||||
callLinkManager.createCallLink(credentials).map {
|
||||
when (it) {
|
||||
is CreateCallLinkResult.Success -> {
|
||||
SignalDatabase.callLinks.insertCallLink(
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = credentials.roomId,
|
||||
credentials = credentials,
|
||||
state = it.state,
|
||||
avatarColor = avatarColor
|
||||
)
|
||||
)
|
||||
|
||||
EnsureCallLinkCreatedResult.Success
|
||||
}
|
||||
|
||||
is CreateCallLinkResult.Failure -> EnsureCallLinkCreatedResult.Failure(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,98 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.create
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.ringrtc.CallLinkState.Restrictions
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
import java.time.Instant
|
||||
|
||||
class CreateCallLinkViewModel : ViewModel() {
|
||||
class CreateCallLinkViewModel(
|
||||
private val repository: CreateCallLinkRepository = CreateCallLinkRepository(),
|
||||
private val mutationRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
|
||||
) : ViewModel() {
|
||||
private val credentials = CallLinkCredentials.generate()
|
||||
private val avatarColor = AvatarColor.random()
|
||||
private val _callLink: MutableState<CallLinkTable.CallLink> = mutableStateOf(
|
||||
CallLinkTable.CallLink("", "", AvatarColor.random(), false)
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = credentials.roomId,
|
||||
credentials = credentials,
|
||||
state = SignalCallLinkState(
|
||||
name = "",
|
||||
restrictions = Restrictions.NONE,
|
||||
revoked = false,
|
||||
expiration = Instant.MAX
|
||||
),
|
||||
avatarColor = avatarColor
|
||||
)
|
||||
)
|
||||
|
||||
val callLink: State<CallLinkTable.CallLink> = _callLink
|
||||
val linkKeyBytes: ByteArray = credentials.linkKeyBytes
|
||||
|
||||
fun setApproveAllMembers(approveAllMembers: Boolean) {
|
||||
_callLink.value = _callLink.value.copy(isApprovalRequired = approveAllMembers)
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
disposables += CallLinks.watchCallLink(credentials.roomId)
|
||||
.subscribeBy {
|
||||
_callLink.value = it
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleApproveAllMembers() {
|
||||
_callLink.value = _callLink.value.copy(isApprovalRequired = _callLink.value.isApprovalRequired)
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun setCallName(callName: String) {
|
||||
_callLink.value = _callLink.value.copy(name = callName)
|
||||
fun commitCallLink(): Single<EnsureCallLinkCreatedResult> {
|
||||
return repository.ensureCallLinkCreated(credentials, avatarColor)
|
||||
}
|
||||
|
||||
fun setApproveAllMembers(approveAllMembers: Boolean): Single<UpdateCallLinkResult> {
|
||||
return commitCallLink()
|
||||
.flatMap {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> mutationRepository.setCallRestrictions(
|
||||
credentials,
|
||||
if (approveAllMembers) Restrictions.ADMIN_APPROVAL else Restrictions.NONE
|
||||
)
|
||||
is EnsureCallLinkCreatedResult.Failure -> Single.just(UpdateCallLinkResult.Failure(it.failure.status))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleApproveAllMembers(): Single<UpdateCallLinkResult> {
|
||||
return setApproveAllMembers(_callLink.value.state.restrictions != Restrictions.ADMIN_APPROVAL)
|
||||
}
|
||||
|
||||
fun setCallName(callName: String): Single<UpdateCallLinkResult> {
|
||||
return commitCallLink()
|
||||
.flatMap {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> mutationRepository.setCallName(
|
||||
credentials,
|
||||
callName
|
||||
)
|
||||
is EnsureCallLinkCreatedResult.Failure -> Single.just(UpdateCallLinkResult.Failure(it.failure.status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.create
|
||||
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
||||
|
||||
sealed interface EnsureCallLinkCreatedResult {
|
||||
object Success : EnsureCallLinkCreatedResult
|
||||
data class Failure(val failure: CreateCallLinkResult.Failure) : EnsureCallLinkCreatedResult
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -16,19 +24,27 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.ringrtc.CallLinkState.Restrictions
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
|
||||
import org.thoughtcrime.securesms.calls.links.SignalCallRow
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Provides detailed info about a call link and allows the owner of that link
|
||||
@@ -36,24 +52,24 @@ import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
*/
|
||||
class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
|
||||
private val viewModel: CallLinkViewModel by viewModels()
|
||||
private val viewModel: CallLinkDetailsViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
parentFragmentManager.setFragmentResultListener(EditCallLinkNameDialogFragment.RESULT_KEY, viewLifecycleOwner) { resultKey, bundle ->
|
||||
if (bundle.containsKey(resultKey)) {
|
||||
viewModel.setName(bundle.getString(resultKey)!!)
|
||||
setName(bundle.getString(resultKey)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val isLoading by viewModel.isLoading
|
||||
val callLink by viewModel.callLink
|
||||
val state by viewModel.state
|
||||
|
||||
CallLinkDetails(
|
||||
isLoading,
|
||||
callLink,
|
||||
state,
|
||||
this
|
||||
)
|
||||
}
|
||||
@@ -67,22 +83,39 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
}
|
||||
|
||||
override fun onEditNameClicked() {
|
||||
val name = viewModel.callLink.value.name
|
||||
val name = viewModel.nameSnapshot
|
||||
findNavController().navigate(
|
||||
CallLinkDetailsFragmentDirections.actionCallLinkDetailsFragmentToEditCallLinkNameDialogFragment(name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onShareClicked() {
|
||||
// TODO("Not yet implemented")
|
||||
val mimeType = Intent.normalizeMimeType("text/plain")
|
||||
val shareIntent = ShareCompat.IntentBuilder(requireContext())
|
||||
.setText(CallLinks.url(viewModel.rootKeySnapshot))
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
|
||||
try {
|
||||
startActivity(shareIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__failed_to_open_share_sheet, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleteClicked() {
|
||||
// TODO("Not yet implemented")
|
||||
lifecycleDisposable += viewModel.revoke().subscribeBy {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onApproveAllMembersChanged(checked: Boolean) {
|
||||
// TODO("Not yet implemented")
|
||||
lifecycleDisposable += viewModel.setApproveAllMembers(checked).subscribeBy {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setName(name: String) {
|
||||
lifecycleDisposable += viewModel.setName(name).subscribeBy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,18 +136,26 @@ private fun CallLinkDetailsPreview() {
|
||||
}
|
||||
|
||||
val callLink = remember {
|
||||
val credentials = CallLinkCredentials.generate()
|
||||
CallLinkTable.CallLink(
|
||||
name = "Call Name",
|
||||
identifier = "call-id-1",
|
||||
isApprovalRequired = false,
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = credentials.roomId,
|
||||
credentials = credentials,
|
||||
state = SignalCallLinkState(
|
||||
name = "Call Name",
|
||||
revoked = false,
|
||||
restrictions = Restrictions.NONE,
|
||||
expiration = Instant.MAX
|
||||
),
|
||||
avatarColor = avatarColor
|
||||
)
|
||||
}
|
||||
|
||||
SignalTheme(false) {
|
||||
CallLinkDetails(
|
||||
false,
|
||||
callLink,
|
||||
CallLinkDetailsState(
|
||||
callLink
|
||||
),
|
||||
object : CallLinkDetailsCallback {
|
||||
override fun onNavigationClicked() = Unit
|
||||
override fun onJoinClicked() = Unit
|
||||
@@ -129,8 +170,7 @@ private fun CallLinkDetailsPreview() {
|
||||
|
||||
@Composable
|
||||
private fun CallLinkDetails(
|
||||
isLoading: Boolean,
|
||||
callLink: CallLinkTable.CallLink,
|
||||
state: CallLinkDetailsState,
|
||||
callback: CallLinkDetailsCallback
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
@@ -138,13 +178,13 @@ private fun CallLinkDetails(
|
||||
onNavigationClick = callback::onNavigationClicked,
|
||||
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
|
||||
) { paddingValues ->
|
||||
if (isLoading) {
|
||||
if (state.callLink == null) {
|
||||
return@Settings
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
SignalCallRow(
|
||||
callLink = callLink,
|
||||
callLink = state.callLink,
|
||||
onJoinClicked = callback::onJoinClicked,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp)
|
||||
)
|
||||
@@ -155,7 +195,7 @@ private fun CallLinkDetails(
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
checked = callLink.isApprovalRequired,
|
||||
checked = state.callLink.state.restrictions == Restrictions.ADMIN_APPROVAL,
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__approve_all_members),
|
||||
onCheckChanged = callback::onApproveAllMembersChanged
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
|
||||
class CallLinkDetailsRepository(
|
||||
private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
|
||||
) {
|
||||
fun refreshCallLinkState(callLinkRoomId: CallLinkRoomId): Disposable {
|
||||
return Maybe.fromCallable<CallLinkTable.CallLink> { SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) }
|
||||
.flatMapSingle { callLinkManager.readCallLink(it.credentials!!) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribeBy {
|
||||
when (it) {
|
||||
is ReadCallLinkResult.Success -> SignalDatabase.callLinks.updateCallLinkState(callLinkRoomId, it.callLinkState)
|
||||
is ReadCallLinkResult.Failure -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
|
||||
data class CallLinkDetailsState(
|
||||
val callLink: CallLinkTable.CallLink? = null
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
|
||||
class CallLinkDetailsViewModel(
|
||||
private val callLinkRoomId: CallLinkRoomId,
|
||||
private val repository: CallLinkDetailsRepository = CallLinkDetailsRepository(),
|
||||
private val mutationRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
|
||||
) : ViewModel() {
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val _state: MutableState<CallLinkDetailsState> = mutableStateOf(CallLinkDetailsState())
|
||||
val state: State<CallLinkDetailsState> = _state
|
||||
val nameSnapshot: String
|
||||
get() = state.value.callLink?.state?.name ?: error("Call link not loaded yet.")
|
||||
|
||||
val rootKeySnapshot: ByteArray
|
||||
get() = state.value.callLink?.credentials?.linkKeyBytes ?: error("Call link not loaded yet.")
|
||||
|
||||
init {
|
||||
disposables += repository.refreshCallLinkState(callLinkRoomId)
|
||||
disposables += CallLinks.watchCallLink(callLinkRoomId).subscribeBy {
|
||||
_state.value = CallLinkDetailsState(
|
||||
callLink = it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun setApproveAllMembers(approveAllMembers: Boolean): Single<UpdateCallLinkResult> {
|
||||
val credentials = _state.value.callLink?.credentials ?: error("User cannot change the name of this call.")
|
||||
return mutationRepository.setCallRestrictions(credentials, if (approveAllMembers) CallLinkState.Restrictions.ADMIN_APPROVAL else CallLinkState.Restrictions.NONE)
|
||||
}
|
||||
|
||||
fun setName(name: String): Single<UpdateCallLinkResult> {
|
||||
val credentials = _state.value.callLink?.credentials ?: error("User cannot change the name of this call.")
|
||||
return mutationRepository.setCallName(credentials, name)
|
||||
}
|
||||
|
||||
fun revoke(): Single<UpdateCallLinkResult> {
|
||||
val credentials = _state.value.callLink?.credentials ?: error("User cannot change the name of this call.")
|
||||
return mutationRepository.revokeCallLink(credentials)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
|
||||
class CallLinkViewModel : ViewModel() {
|
||||
private val isLoadingState: MutableState<Boolean> = mutableStateOf(true)
|
||||
val isLoading: State<Boolean> = isLoadingState
|
||||
|
||||
private val callLinkState: MutableState<CallLinkTable.CallLink> = mutableStateOf(
|
||||
CallLinkTable.CallLink("", "", AvatarColor.A120, false)
|
||||
)
|
||||
val callLink: State<CallLinkTable.CallLink> = callLinkState
|
||||
|
||||
fun setName(name: String) {
|
||||
callLinkState.value = callLinkState.value.copy(name = name)
|
||||
}
|
||||
}
|
||||
@@ -153,13 +153,17 @@ class CallLogAdapter(
|
||||
return
|
||||
}
|
||||
|
||||
binding.callRecipientAvatar.setAvatar(GlideApp.with(binding.callRecipientAvatar), model.call.peer, true)
|
||||
binding.callRecipientBadge.setBadgeFromRecipient(model.call.peer)
|
||||
binding.callRecipientName.text = model.call.peer.getDisplayName(context)
|
||||
presentRecipientDetails(model.call.peer)
|
||||
presentCallInfo(model.call, model.call.date)
|
||||
presentCallType(model)
|
||||
}
|
||||
|
||||
private fun presentRecipientDetails(recipient: Recipient) {
|
||||
binding.callRecipientAvatar.setAvatar(GlideApp.with(binding.callRecipientAvatar), recipient, true)
|
||||
binding.callRecipientBadge.setBadgeFromRecipient(recipient)
|
||||
binding.callRecipientName.text = recipient.getDisplayName(context)
|
||||
}
|
||||
|
||||
private fun presentCallInfo(call: CallLogRow.Call, date: Long) {
|
||||
val callState = context.getString(getCallStateStringRes(call.record))
|
||||
binding.callInfo.text = context.getString(
|
||||
@@ -321,11 +325,11 @@ class CallLogAdapter(
|
||||
/**
|
||||
* Invoked when user presses the audio icon
|
||||
*/
|
||||
fun onStartAudioCallClicked(peer: Recipient)
|
||||
fun onStartAudioCallClicked(recipient: Recipient)
|
||||
|
||||
/**
|
||||
* Invoked when user presses the video icon
|
||||
*/
|
||||
fun onStartVideoCallClicked(peer: Recipient)
|
||||
fun onStartVideoCallClicked(recipient: Recipient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class CallLogContextMenu(
|
||||
}
|
||||
|
||||
private fun getAudioCallActionItem(call: CallLogRow.Call): ActionItem? {
|
||||
if (call.peer.isGroup) {
|
||||
if (call.peer.isCallLink || call.peer.isGroup) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -63,12 +63,15 @@ class CallLogContextMenu(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGoToChatActionItem(call: CallLogRow.Call): ActionItem {
|
||||
return ActionItem(
|
||||
iconRes = R.drawable.symbol_open_24,
|
||||
title = fragment.getString(R.string.CallContextMenu__go_to_chat)
|
||||
) {
|
||||
fragment.startActivity(ConversationIntents.createBuilder(fragment.requireContext(), call.peer.id, -1L).build())
|
||||
private fun getGoToChatActionItem(call: CallLogRow.Call): ActionItem? {
|
||||
return when {
|
||||
call.peer.isCallLink -> null
|
||||
else -> ActionItem(
|
||||
iconRes = R.drawable.symbol_open_24,
|
||||
title = fragment.getString(R.string.CallContextMenu__go_to_chat)
|
||||
) {
|
||||
fragment.startActivity(ConversationIntents.createBuilder(fragment.requireContext(), call.peer.id, -1L).build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +80,10 @@ class CallLogContextMenu(
|
||||
iconRes = R.drawable.symbol_info_24,
|
||||
title = fragment.getString(R.string.CallContextMenu__info)
|
||||
) {
|
||||
val intent = ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer, longArrayOf(call.record.messageId!!))
|
||||
val intent = when {
|
||||
call.peer.isCallLink -> throw NotImplementedError("Launch CallLinkDetailsActivity")
|
||||
else -> ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer, longArrayOf(call.record.messageId!!))
|
||||
}
|
||||
fragment.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,9 +311,15 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
override fun onCallClicked(callLogRow: CallLogRow.Call) {
|
||||
if (viewModel.selectionStateSnapshot.isNotEmpty(binding.recycler.adapter!!.itemCount)) {
|
||||
viewModel.toggleSelected(callLogRow.id)
|
||||
} else {
|
||||
val intent = ConversationSettingsActivity.forCall(requireContext(), callLogRow.peer, (callLogRow.id as CallLogRow.Id.Call).children.toLongArray())
|
||||
} else if (!callLogRow.peer.isCallLink) {
|
||||
val intent = ConversationSettingsActivity.forCall(
|
||||
requireContext(),
|
||||
callLogRow.peer,
|
||||
(callLogRow.id as CallLogRow.Id.Call).children.toLongArray()
|
||||
)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
throw NotImplementedError("On call link event clicked.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,12 +333,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
binding.recyclerCoordinatorAppBar.setExpanded(false, true)
|
||||
}
|
||||
|
||||
override fun onStartAudioCallClicked(peer: Recipient) {
|
||||
CommunicationActions.startVoiceCall(this, peer)
|
||||
override fun onStartAudioCallClicked(recipient: Recipient) {
|
||||
CommunicationActions.startVoiceCall(this, recipient)
|
||||
}
|
||||
|
||||
override fun onStartVideoCallClicked(peer: Recipient) {
|
||||
CommunicationActions.startVideoCall(this, peer)
|
||||
override fun onStartVideoCallClicked(recipient: Recipient) {
|
||||
CommunicationActions.startVideoCall(this, recipient)
|
||||
}
|
||||
|
||||
override fun startSelection(call: CallLogRow.Call) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.log
|
||||
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.Serializer
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToSingleInt
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullBlob
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.ringrtc.CallLinkState.Restrictions
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
/**
|
||||
* Table containing ad-hoc call link details
|
||||
@@ -42,13 +65,168 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
private fun SignalCallLinkState.serialize(): ContentValues {
|
||||
return contentValuesOf(
|
||||
NAME to name,
|
||||
RESTRICTIONS to restrictions.mapToInt(),
|
||||
EXPIRATION to expiration.toEpochMilli(),
|
||||
REVOKED to revoked
|
||||
)
|
||||
}
|
||||
|
||||
private fun Restrictions.mapToInt(): Int {
|
||||
return when (this) {
|
||||
Restrictions.NONE -> 0
|
||||
Restrictions.ADMIN_APPROVAL -> 1
|
||||
Restrictions.UNKNOWN -> 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCallLink(
|
||||
callLink: CallLink
|
||||
) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val recipientId = SignalDatabase.recipients.getOrInsertFromCallLinkRoomId(callLink.roomId)
|
||||
|
||||
db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values(CallLinkSerializer.serialize(callLink.copy(recipientId = recipientId)))
|
||||
.run()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(callLink.roomId)
|
||||
}
|
||||
|
||||
fun updateCallLinkCredentials(
|
||||
roomId: CallLinkRoomId,
|
||||
credentials: CallLinkCredentials
|
||||
) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
contentValuesOf(
|
||||
ROOT_KEY to credentials.linkKeyBytes,
|
||||
ADMIN_KEY to credentials.adminPassBytes
|
||||
)
|
||||
)
|
||||
.where("$ROOM_ID = ?", roomId.serialize())
|
||||
.run()
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(roomId)
|
||||
}
|
||||
|
||||
fun updateCallLinkState(
|
||||
roomId: CallLinkRoomId,
|
||||
state: SignalCallLinkState
|
||||
) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(state.serialize())
|
||||
.where("$ROOM_ID = ?", roomId.serialize())
|
||||
.run()
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(roomId)
|
||||
}
|
||||
|
||||
fun callLinkExists(
|
||||
callLinkRoomId: CallLinkRoomId
|
||||
): Boolean {
|
||||
return writableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$ROOM_ID = ?", callLinkRoomId.serialize())
|
||||
.run()
|
||||
.readToSingleInt() > 0
|
||||
}
|
||||
|
||||
fun getCallLinkByRoomId(
|
||||
callLinkRoomId: CallLinkRoomId
|
||||
): CallLink? {
|
||||
return writableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$ROOM_ID = ?", callLinkRoomId.serialize())
|
||||
.run()
|
||||
.readToSingleObject { CallLinkDeserializer.deserialize(it) }
|
||||
}
|
||||
|
||||
fun getOrCreateCallLinkByRoomId(
|
||||
callLinkRoomId: CallLinkRoomId
|
||||
): CallLink {
|
||||
val callLink = getCallLinkByRoomId(callLinkRoomId)
|
||||
return if (callLink == null) {
|
||||
val link = CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = callLinkRoomId,
|
||||
credentials = null,
|
||||
state = SignalCallLinkState(),
|
||||
avatarColor = AvatarColor.random()
|
||||
)
|
||||
insertCallLink(link)
|
||||
return getCallLinkByRoomId(callLinkRoomId)!!
|
||||
} else {
|
||||
callLink
|
||||
}
|
||||
}
|
||||
|
||||
private object CallLinkSerializer : Serializer<CallLink, ContentValues> {
|
||||
override fun serialize(data: CallLink): ContentValues {
|
||||
return contentValuesOf(
|
||||
RECIPIENT_ID to data.recipientId.takeIf { it != RecipientId.UNKNOWN }?.toLong(),
|
||||
ROOM_ID to data.roomId.serialize(),
|
||||
ROOT_KEY to data.credentials?.linkKeyBytes,
|
||||
ADMIN_KEY to data.credentials?.adminPassBytes,
|
||||
AVATAR_COLOR to data.avatarColor.serialize()
|
||||
).apply {
|
||||
putAll(data.state.serialize())
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(data: ContentValues): CallLink {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
private object CallLinkDeserializer : Serializer<CallLink, Cursor> {
|
||||
override fun serialize(data: CallLink): Cursor {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun deserialize(data: Cursor): CallLink {
|
||||
return CallLink(
|
||||
recipientId = data.requireLong(RECIPIENT_ID).let { if (it > 0) RecipientId.from(it) else RecipientId.UNKNOWN },
|
||||
roomId = CallLinkRoomId.fromBytes(Base64.decode(data.requireNonNullString(ROOM_ID))),
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = data.requireNonNullBlob(ROOT_KEY),
|
||||
adminPassBytes = data.requireBlob(ADMIN_KEY)
|
||||
),
|
||||
state = SignalCallLinkState(
|
||||
name = data.requireNonNullString(NAME),
|
||||
restrictions = data.requireInt(RESTRICTIONS).mapToRestrictions(),
|
||||
revoked = data.requireBoolean(REVOKED),
|
||||
expiration = Instant.ofEpochMilli(data.requireLong(EXPIRATION)).truncatedTo(ChronoUnit.DAYS)
|
||||
),
|
||||
avatarColor = AvatarColor.deserialize(data.requireString(AVATAR_COLOR))
|
||||
)
|
||||
}
|
||||
|
||||
private fun Int.mapToRestrictions(): Restrictions {
|
||||
return when (this) {
|
||||
0 -> Restrictions.NONE
|
||||
1 -> Restrictions.ADMIN_APPROVAL
|
||||
else -> Restrictions.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CallLink(
|
||||
val name: String,
|
||||
val identifier: String,
|
||||
val avatarColor: AvatarColor,
|
||||
val isApprovalRequired: Boolean
|
||||
val recipientId: RecipientId,
|
||||
val roomId: CallLinkRoomId,
|
||||
val credentials: CallLinkCredentials?,
|
||||
val state: SignalCallLinkState,
|
||||
val avatarColor: AvatarColor
|
||||
)
|
||||
|
||||
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
|
||||
|
||||
@@ -244,10 +244,11 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
}
|
||||
|
||||
// region Group / Ad-Hoc Calling
|
||||
|
||||
fun deleteGroupCall(call: Call) {
|
||||
checkIsGroupOrAdHocCall(call)
|
||||
|
||||
val filter: SqlUtil.Query = getCallSelectionQuery(call.callId, call.peer)
|
||||
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db
|
||||
.update(TABLE_NAME)
|
||||
@@ -255,7 +256,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
EVENT to Event.serialize(Event.DELETE),
|
||||
DELETION_TIMESTAMP to System.currentTimeMillis()
|
||||
)
|
||||
.where("$CALL_ID = ? AND $PEER = ?", call.callId, call.peer)
|
||||
.where(filter.where, filter.whereArgs)
|
||||
.run()
|
||||
|
||||
if (call.messageId != null) {
|
||||
@@ -274,7 +275,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
direction: Direction,
|
||||
timestamp: Long
|
||||
) {
|
||||
val type = Type.GROUP_CALL
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val type = if (recipient.isCallLink) Type.AD_HOC_CALL else Type.GROUP_CALL
|
||||
|
||||
writableDatabase
|
||||
.insertInto(TABLE_NAME)
|
||||
@@ -322,7 +324,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
direction: Direction,
|
||||
timestamp: Long
|
||||
) {
|
||||
val type = Type.GROUP_CALL
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val type = if (recipient.isCallLink) Type.AD_HOC_CALL else Type.GROUP_CALL
|
||||
val event = if (direction == Direction.OUTGOING) Event.OUTGOING_RING else Event.JOINED
|
||||
val ringer = if (direction == Direction.OUTGOING) Recipient.self().id.toLong() else null
|
||||
|
||||
@@ -951,7 +954,6 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
fun getCalls(offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): List<CallLogRow.Call> {
|
||||
return getCallsCursor(false, offset, limit, searchTerm, filter).readToList { cursor ->
|
||||
val call = Call.deserialize(cursor)
|
||||
val recipient = Recipient.resolved(call.peer)
|
||||
val groupCallDetails = GroupCallUpdateDetailsUtil.parse(cursor.requireString(MessageTable.BODY))
|
||||
|
||||
val children = cursor.requireNonNullString("children")
|
||||
@@ -969,8 +971,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
|
||||
CallLogRow.Call(
|
||||
record = call,
|
||||
peer = recipient,
|
||||
date = call.timestamp,
|
||||
peer = Recipient.resolved(call.peer),
|
||||
groupCallState = CallLogRow.GroupCallState.fromDetails(groupCallDetails),
|
||||
children = actualChildren.toSet()
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -46,27 +47,28 @@ public class DatabaseObserver {
|
||||
private static final String KEY_CONVERSATION_DELETES = "ConversationDeletes";
|
||||
|
||||
private static final String KEY_CALL_UPDATES = "CallUpdates";
|
||||
private static final String KEY_CALL_LINK_UPDATES = "CallLinkUpdates";
|
||||
|
||||
private final Application application;
|
||||
private final Executor executor;
|
||||
|
||||
private final Set<Observer> conversationListObservers;
|
||||
private final Map<Long, Set<Observer>> conversationObservers;
|
||||
private final Map<Long, Set<Observer>> verboseConversationObservers;
|
||||
private final Map<Long, Set<Observer>> conversationDeleteObservers;
|
||||
private final Map<UUID, Set<Observer>> paymentObservers;
|
||||
private final Map<Long, Set<Observer>> scheduledMessageObservers;
|
||||
private final Set<Observer> allPaymentsObservers;
|
||||
private final Set<Observer> chatColorsObservers;
|
||||
private final Set<Observer> stickerObservers;
|
||||
private final Set<Observer> stickerPackObservers;
|
||||
private final Set<Observer> attachmentObservers;
|
||||
private final Set<MessageObserver> messageUpdateObservers;
|
||||
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
|
||||
private final Set<Observer> notificationProfileObservers;
|
||||
private final Map<RecipientId, Set<Observer>> storyObservers;
|
||||
|
||||
private final Set<Observer> callUpdateObservers;
|
||||
private final Set<Observer> conversationListObservers;
|
||||
private final Map<Long, Set<Observer>> conversationObservers;
|
||||
private final Map<Long, Set<Observer>> verboseConversationObservers;
|
||||
private final Map<Long, Set<Observer>> conversationDeleteObservers;
|
||||
private final Map<UUID, Set<Observer>> paymentObservers;
|
||||
private final Map<Long, Set<Observer>> scheduledMessageObservers;
|
||||
private final Set<Observer> allPaymentsObservers;
|
||||
private final Set<Observer> chatColorsObservers;
|
||||
private final Set<Observer> stickerObservers;
|
||||
private final Set<Observer> stickerPackObservers;
|
||||
private final Set<Observer> attachmentObservers;
|
||||
private final Set<MessageObserver> messageUpdateObservers;
|
||||
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
|
||||
private final Set<Observer> notificationProfileObservers;
|
||||
private final Map<RecipientId, Set<Observer>> storyObservers;
|
||||
private final Set<Observer> callUpdateObservers;
|
||||
private final Map<CallLinkRoomId, Set<Observer>> callLinkObservers;
|
||||
|
||||
public DatabaseObserver(Application application) {
|
||||
this.application = application;
|
||||
@@ -87,6 +89,7 @@ public class DatabaseObserver {
|
||||
this.storyObservers = new HashMap<>();
|
||||
this.scheduledMessageObservers = new HashMap<>();
|
||||
this.callUpdateObservers = new HashSet<>();
|
||||
this.callLinkObservers = new HashMap<>();
|
||||
}
|
||||
|
||||
public void registerConversationListObserver(@NonNull Observer listener) {
|
||||
@@ -186,6 +189,12 @@ public class DatabaseObserver {
|
||||
executor.execute(() -> callUpdateObservers.add(observer));
|
||||
}
|
||||
|
||||
public void registerCallLinkObserver(@NonNull CallLinkRoomId callLinkRoomId, @NonNull Observer observer) {
|
||||
executor.execute(() -> {
|
||||
registerMapped(callLinkObservers, callLinkRoomId, observer);
|
||||
});
|
||||
}
|
||||
|
||||
public void unregisterObserver(@NonNull Observer listener) {
|
||||
executor.execute(() -> {
|
||||
conversationListObservers.remove(listener);
|
||||
@@ -201,6 +210,7 @@ public class DatabaseObserver {
|
||||
unregisterMapped(scheduledMessageObservers, listener);
|
||||
unregisterMapped(conversationDeleteObservers, listener);
|
||||
callUpdateObservers.remove(listener);
|
||||
unregisterMapped(callLinkObservers, listener);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -342,6 +352,10 @@ public class DatabaseObserver {
|
||||
runPostSuccessfulTransaction(KEY_CALL_UPDATES, () -> notifySet(callUpdateObservers));
|
||||
}
|
||||
|
||||
public void notifyCallLinkObservers(@NonNull CallLinkRoomId callLinkRoomId) {
|
||||
runPostSuccessfulTransaction(KEY_CALL_LINK_UPDATES, () -> notifyMapped(callLinkObservers, callLinkRoomId));
|
||||
}
|
||||
|
||||
private void runPostSuccessfulTransaction(@NonNull String dedupeKey, @NonNull Runnable runnable) {
|
||||
SignalDatabase.runPostSuccessfulTransaction(dedupeKey, () -> {
|
||||
executor.execute(runnable);
|
||||
|
||||
@@ -87,6 +87,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||
@@ -421,6 +422,10 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
return getByColumn(SERVICE_ID, serviceId.toString())
|
||||
}
|
||||
|
||||
fun getByCallLinkRoomId(callLinkRoomId: CallLinkRoomId): Optional<RecipientId> {
|
||||
return getByColumn(CALL_LINK_ROOM_ID, callLinkRoomId.serialize())
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return a recipient matching the PNI, but only in the explicit [PNI_COLUMN]. This should only be checked in conjunction with [getByServiceId] as a way
|
||||
* to avoid creating a recipient we already merged.
|
||||
@@ -559,6 +564,18 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
).recipientId
|
||||
}
|
||||
|
||||
fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId): RecipientId {
|
||||
return getOrInsertByColumn(
|
||||
CALL_LINK_ROOM_ID,
|
||||
callLinkRoomId.serialize(),
|
||||
contentValuesOf(
|
||||
GROUP_TYPE to GroupType.CALL_LINK.id,
|
||||
CALL_LINK_ROOM_ID to callLinkRoomId.serialize(),
|
||||
PROFILE_SHARING to 1
|
||||
)
|
||||
).recipientId
|
||||
}
|
||||
|
||||
fun getDistributionListRecipientIds(): List<RecipientId> {
|
||||
val recipientIds = mutableListOf<RecipientId>()
|
||||
readableDatabase.query(TABLE_NAME, arrayOf(ID), "$DISTRIBUTION_LIST_ID is not NULL", null, null, null, null).use { cursor ->
|
||||
|
||||
@@ -74,6 +74,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val pendingPniSignatureMessageTable: PendingPniSignatureMessageTable = PendingPniSignatureMessageTable(context, this)
|
||||
val callTable: CallTable = CallTable(context, this)
|
||||
val kyberPreKeyTable: KyberPreKeyTable = KyberPreKeyTable(context, this)
|
||||
val callLinkTable: CallLinkTable = CallLinkTable(context, this)
|
||||
|
||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
@@ -536,5 +537,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
@get:JvmName("calls")
|
||||
val calls: CallTable
|
||||
get() = instance!!.callTable
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("callLinks")
|
||||
val callLinks: CallLinkTable
|
||||
get() = instance!!.callLinkTable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.api.services.CallLinksService;
|
||||
import org.whispersystems.signalservice.api.services.DonationsService;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
|
||||
@@ -128,6 +129,7 @@ public class ApplicationDependencies {
|
||||
private static volatile SimpleExoPlayerPool exoPlayerPool;
|
||||
private static volatile AudioManagerCompat audioManagerCompat;
|
||||
private static volatile DonationsService donationsService;
|
||||
private static volatile CallLinksService callLinksService;
|
||||
private static volatile ProfileService profileService;
|
||||
private static volatile DeadlockDetector deadlockDetector;
|
||||
private static volatile ClientZkReceiptOperations clientZkReceiptOperations;
|
||||
@@ -652,6 +654,17 @@ public class ApplicationDependencies {
|
||||
return donationsService;
|
||||
}
|
||||
|
||||
public static @NonNull CallLinksService getCallLinksService() {
|
||||
if (callLinksService == null) {
|
||||
synchronized (LOCK) {
|
||||
if (callLinksService == null) {
|
||||
callLinksService = provider.provideCallLinksService(getSignalServiceNetworkAccess().getConfiguration(), getGroupsV2Operations());
|
||||
}
|
||||
}
|
||||
}
|
||||
return callLinksService;
|
||||
}
|
||||
|
||||
public static @NonNull ProfileService getProfileService() {
|
||||
if (profileService == null) {
|
||||
synchronized (LOCK) {
|
||||
@@ -721,6 +734,7 @@ public class ApplicationDependencies {
|
||||
@NonNull SimpleExoPlayerPool provideExoPlayerPool();
|
||||
@NonNull AudioManagerCompat provideAndroidCallAudioManager();
|
||||
@NonNull DonationsService provideDonationsService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations);
|
||||
@NonNull CallLinksService provideCallLinksService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations);
|
||||
@NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations profileOperations, @NonNull SignalServiceMessageReceiver signalServiceMessageReceiver, @NonNull SignalWebSocket signalWebSocket);
|
||||
@NonNull DeadlockDetector provideDeadlockDetector();
|
||||
@NonNull ClientZkReceiptOperations provideClientZkReceiptOperations(@NonNull SignalServiceConfiguration signalServiceConfiguration);
|
||||
|
||||
@@ -87,6 +87,7 @@ import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.services.CallLinksService;
|
||||
import org.whispersystems.signalservice.api.services.DonationsService;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
@@ -372,6 +373,15 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
FeatureFlags.okHttpAutomaticRetry());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull CallLinksService provideCallLinksService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) {
|
||||
return new CallLinksService(signalServiceConfiguration,
|
||||
new DynamicCredentialsProvider(),
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
groupsV2Operations,
|
||||
FeatureFlags.okHttpAutomaticRetry());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations clientZkProfileOperations,
|
||||
@NonNull SignalServiceMessageReceiver receiver,
|
||||
|
||||
@@ -3,15 +3,26 @@ package org.thoughtcrime.securesms.groups;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.GenericServerPublicParams;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredential;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIds;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -33,10 +44,10 @@ public class GroupsV2Authorization {
|
||||
{
|
||||
final long today = currentDaySeconds();
|
||||
|
||||
Map<Long, AuthCredentialWithPniResponse> credentials = authCache.read();
|
||||
GroupsV2Api.CredentialResponseMaps credentials = authCache.read();
|
||||
|
||||
try {
|
||||
return getAuthorization(serviceIds, groupSecretParams, credentials, today);
|
||||
return getAuthorization(serviceIds, groupSecretParams, credentials.getAuthCredentialWithPniResponseHashMap(), today);
|
||||
} catch (NoCredentialForRedemptionTimeException e) {
|
||||
Log.i(TAG, "Auth out of date, will update auth and try again");
|
||||
authCache.clear();
|
||||
@@ -50,13 +61,54 @@ public class GroupsV2Authorization {
|
||||
authCache.write(credentials);
|
||||
|
||||
try {
|
||||
return getAuthorization(serviceIds, groupSecretParams, credentials, today);
|
||||
return getAuthorization(serviceIds, groupSecretParams, credentials.getAuthCredentialWithPniResponseHashMap(), today);
|
||||
} catch (NoCredentialForRedemptionTimeException e) {
|
||||
Log.w(TAG, "The credentials returned did not include the day requested");
|
||||
throw new IOException("Failed to get credentials");
|
||||
}
|
||||
}
|
||||
|
||||
public CallLinkAuthCredentialPresentation getCallLinkAuthorizationForToday(@NonNull GenericServerPublicParams genericServerPublicParams,
|
||||
@NonNull CallLinkSecretParams callLinkSecretParams)
|
||||
throws IOException, VerificationFailedException
|
||||
{
|
||||
final long today = currentDaySeconds();
|
||||
|
||||
GroupsV2Api.CredentialResponseMaps credentials = authCache.read();
|
||||
|
||||
try {
|
||||
return getCallLinkAuthCredentialPresentation(
|
||||
genericServerPublicParams,
|
||||
callLinkSecretParams,
|
||||
credentials.getCallLinkAuthCredentialResponseHashMap(),
|
||||
today
|
||||
);
|
||||
} catch (NoCredentialForRedemptionTimeException e) {
|
||||
Log.i(TAG, "Auth out of date, will update auth and try again");
|
||||
authCache.clear();
|
||||
} catch (VerificationFailedException e) {
|
||||
Log.w(TAG, "Verification failed, will update auth and try again", e);
|
||||
authCache.clear();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Getting new auth credential responses");
|
||||
credentials = groupsV2Api.getCredentials(today);
|
||||
authCache.write(credentials);
|
||||
|
||||
try {
|
||||
return getCallLinkAuthCredentialPresentation(
|
||||
genericServerPublicParams,
|
||||
callLinkSecretParams,
|
||||
credentials.getCallLinkAuthCredentialResponseHashMap(),
|
||||
today
|
||||
);
|
||||
} catch (NoCredentialForRedemptionTimeException e) {
|
||||
Log.w(TAG, "The credentials returned did not include the day requested");
|
||||
throw new IOException("Failed to get credentials");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void clear() {
|
||||
authCache.clear();
|
||||
}
|
||||
@@ -65,6 +117,32 @@ public class GroupsV2Authorization {
|
||||
return TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
private CallLinkAuthCredentialPresentation getCallLinkAuthCredentialPresentation(GenericServerPublicParams genericServerPublicParams,
|
||||
CallLinkSecretParams callLinkSecretParams,
|
||||
Map<Long, CallLinkAuthCredentialResponse> credentials,
|
||||
long todaySeconds)
|
||||
throws NoCredentialForRedemptionTimeException, VerificationFailedException
|
||||
{
|
||||
CallLinkAuthCredentialResponse authCredentialResponse = credentials.get(todaySeconds);
|
||||
|
||||
if (authCredentialResponse == null) {
|
||||
throw new NoCredentialForRedemptionTimeException();
|
||||
}
|
||||
|
||||
CallLinkAuthCredential credential = authCredentialResponse.receive(
|
||||
Recipient.self().requireServiceId().uuid(),
|
||||
Instant.ofEpochSecond(todaySeconds),
|
||||
genericServerPublicParams
|
||||
);
|
||||
|
||||
return credential.present(
|
||||
Recipient.self().requireServiceId().uuid(),
|
||||
Instant.ofEpochSecond(todaySeconds),
|
||||
genericServerPublicParams,
|
||||
callLinkSecretParams
|
||||
);
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationString getAuthorization(ServiceIds serviceIds,
|
||||
GroupSecretParams groupSecretParams,
|
||||
Map<Long, AuthCredentialWithPniResponse> credentials,
|
||||
@@ -84,8 +162,8 @@ public class GroupsV2Authorization {
|
||||
|
||||
void clear();
|
||||
|
||||
@NonNull Map<Long, AuthCredentialWithPniResponse> read();
|
||||
@NonNull GroupsV2Api.CredentialResponseMaps read();
|
||||
|
||||
void write(@NonNull Map<Long, AuthCredentialWithPniResponse> values);
|
||||
void write(@NonNull GroupsV2Api.CredentialResponseMaps values);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -10,8 +11,8 @@ import java.util.Map;
|
||||
|
||||
public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Authorization.ValueCache {
|
||||
|
||||
private final GroupsV2Authorization.ValueCache inner;
|
||||
private Map<Long, AuthCredentialWithPniResponse> values;
|
||||
private final GroupsV2Authorization.ValueCache inner;
|
||||
private GroupsV2Api.CredentialResponseMaps values;
|
||||
|
||||
public GroupsV2AuthorizationMemoryValueCache(@NonNull GroupsV2Authorization.ValueCache inner) {
|
||||
this.inner = inner;
|
||||
@@ -24,8 +25,8 @@ public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Auth
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull synchronized Map<Long, AuthCredentialWithPniResponse> read() {
|
||||
Map<Long, AuthCredentialWithPniResponse> map = values;
|
||||
public @NonNull synchronized GroupsV2Api.CredentialResponseMaps read() {
|
||||
GroupsV2Api.CredentialResponseMaps map = values;
|
||||
|
||||
if (map == null) {
|
||||
map = inner.read();
|
||||
@@ -36,8 +37,8 @@ public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Auth
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(@NonNull Map<Long, AuthCredentialWithPniResponse> values) {
|
||||
public synchronized void write(@NonNull GroupsV2Api.CredentialResponseMaps values) {
|
||||
inner.write(values);
|
||||
this.values = Collections.unmodifiableMap(new HashMap<>(values));
|
||||
this.values = values.createUnmodifiableCopy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class CallSyncEventJob private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun CallSyncEventJobRecord.deserializeRecipientId(): RecipientId = RecipientId.from(recipientId!!)
|
||||
private fun CallSyncEventJobRecord.deserializeRecipientId(): RecipientId = RecipientId.from(recipientId)
|
||||
|
||||
private fun CallSyncEventJobRecord.deserializeDirection(): CallTable.Direction = CallTable.Direction.deserialize(direction)
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ public final class JobManagerFactories {
|
||||
put(MmsReceiveJob.KEY, new MmsReceiveJob.Factory());
|
||||
put(MmsSendJob.KEY, new MmsSendJob.Factory());
|
||||
put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory());
|
||||
put(MultiDeviceCallLinkSyncJob.KEY, new MultiDeviceCallLinkSyncJob.Factory());
|
||||
put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory());
|
||||
put(MultiDeviceContactSyncJob.KEY, new MultiDeviceContactSyncJob.Factory());
|
||||
put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory());
|
||||
@@ -173,6 +174,7 @@ public final class JobManagerFactories {
|
||||
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
|
||||
put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory());
|
||||
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
|
||||
put(RefreshCallLinkDetailsJob.KEY, new RefreshCallLinkDetailsJob.Factory());
|
||||
put(RefreshKbsCredentialsJob.KEY, new RefreshKbsCredentialsJob.Factory());
|
||||
put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
|
||||
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallLinkUpdate
|
||||
import java.util.Optional
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Sends a sync message to linked devices when a new call link is created locally.
|
||||
*/
|
||||
class MultiDeviceCallLinkSyncJob private constructor(
|
||||
parameters: Parameters,
|
||||
private val callLinkUpdate: CallLinkUpdate
|
||||
) : BaseJob(parameters) {
|
||||
|
||||
constructor(credentials: CallLinkCredentials) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue("__MULTI_DEVICE_CALL_LINK_UPDATE_JOB__")
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(1.days.inWholeMilliseconds)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
CallLinkUpdate.newBuilder()
|
||||
.setRootKey(ByteString.copyFrom(credentials.linkKeyBytes))
|
||||
.setAdminPassKey(ByteString.copyFrom(credentials.adminPassBytes!!))
|
||||
.build()
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val KEY = "MultiDeviceCallLinkSyncJob"
|
||||
|
||||
private val TAG = Log.tag(MultiDeviceCallLinkSyncJob::class.java)
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray {
|
||||
return callLinkUpdate.toByteArray()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
override fun onRun() {
|
||||
val syncMessage = SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate)
|
||||
|
||||
try {
|
||||
ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(syncMessage, Optional.empty())
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to send call link update message.", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShouldRetry(exception: Exception): Boolean {
|
||||
return when (exception) {
|
||||
is PushNetworkException -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<MultiDeviceCallLinkSyncJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceCallLinkSyncJob {
|
||||
val data = CallLinkUpdate.parseFrom(serializedData)
|
||||
return MultiDeviceCallLinkSyncJob(parameters, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.concurrent.safeBlockingGet
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallLinkUpdate
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Requests the latest call link state from the call service.
|
||||
*/
|
||||
class RefreshCallLinkDetailsJob private constructor(
|
||||
parameters: Parameters,
|
||||
private val callLinkUpdate: CallLinkUpdate
|
||||
) : BaseJob(parameters) {
|
||||
|
||||
constructor(callLinkUpdate: CallLinkUpdate) : this(
|
||||
Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("__RefreshCallLinkDetailsJob__")
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
callLinkUpdate
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val KEY = "RefreshCallLinkDetailsJob"
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray = callLinkUpdate.toByteArray()
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
override fun onRun() {
|
||||
val manager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
|
||||
val credentials = CallLinkCredentials(
|
||||
linkKeyBytes = callLinkUpdate.rootKey.toByteArray(),
|
||||
adminPassBytes = callLinkUpdate.adminPassKey?.toByteArray()
|
||||
)
|
||||
|
||||
when (val result = manager.readCallLink(credentials).safeBlockingGet()) {
|
||||
is ReadCallLinkResult.Success -> {
|
||||
SignalDatabase.callLinks.updateCallLinkState(credentials.roomId, result.callLinkState)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean = false
|
||||
|
||||
class Factory : Job.Factory<RefreshCallLinkDetailsJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): RefreshCallLinkDetailsJob {
|
||||
val callLinkUpdate = CallLinkUpdate.parseFrom(serializedData)
|
||||
return RefreshCallLinkDetailsJob(parameters, callLinkUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,29 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.internal.ByteArray;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TemporalAuthCredentialResponse;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TemporalAuthCredentialResponses;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Authorization.ValueCache {
|
||||
|
||||
private static final String TAG = Log.tag(GroupsV2AuthorizationSignalStoreCache.class);
|
||||
|
||||
private static final String ACI_PNI_PREFIX = "gv2:auth_token_cache";
|
||||
private static final int ACI_PNI_VERSION = 3;
|
||||
private static final String CALL_LINK_AUTH_PREFIX = "call_link_auth:";
|
||||
private static final String ACI_PNI_PREFIX = "gv2:auth_token_cache";
|
||||
private static final int ACI_PNI_VERSION = 3;
|
||||
|
||||
private final String key;
|
||||
private final String callLinkAuthKey;
|
||||
private final KeyValueStore store;
|
||||
|
||||
public static GroupsV2AuthorizationSignalStoreCache createAciCache(@NonNull KeyValueStore store) {
|
||||
@@ -38,21 +44,30 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationSignalStoreCache(@NonNull KeyValueStore store, @NonNull String key) {
|
||||
this.store = store;
|
||||
this.key = key;
|
||||
this.store = store;
|
||||
this.key = key;
|
||||
this.callLinkAuthKey = CALL_LINK_AUTH_PREFIX + key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
store.beginWrite()
|
||||
.remove(key)
|
||||
.remove(callLinkAuthKey)
|
||||
.commit();
|
||||
|
||||
Log.i(TAG, "Cleared local response cache");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Map<Long, AuthCredentialWithPniResponse> read() {
|
||||
public @NonNull GroupsV2Api.CredentialResponseMaps read() {
|
||||
Map<Long, AuthCredentialWithPniResponse> credentials = read(key, AuthCredentialWithPniResponse::new);
|
||||
Map<Long, CallLinkAuthCredentialResponse> callLinkCredentials = read(callLinkAuthKey, CallLinkAuthCredentialResponse::new);
|
||||
|
||||
return new GroupsV2Api.CredentialResponseMaps(credentials, callLinkCredentials);
|
||||
}
|
||||
|
||||
public <T extends ByteArray> @NonNull Map<Long, T> read(@NonNull String key, @NonNull CredentialConstructor<T> factory) {
|
||||
byte[] credentialBlob = store.getBlob(key, null);
|
||||
|
||||
if (credentialBlob == null) {
|
||||
@@ -61,11 +76,11 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
|
||||
}
|
||||
|
||||
try {
|
||||
TemporalAuthCredentialResponses temporalCredentials = TemporalAuthCredentialResponses.parseFrom(credentialBlob);
|
||||
HashMap<Long, AuthCredentialWithPniResponse> result = new HashMap<>(temporalCredentials.getCredentialResponseCount());
|
||||
TemporalAuthCredentialResponses temporalCredentials = TemporalAuthCredentialResponses.parseFrom(credentialBlob);
|
||||
HashMap<Long, T> result = new HashMap<>(temporalCredentials.getCredentialResponseCount());
|
||||
|
||||
for (TemporalAuthCredentialResponse credential : temporalCredentials.getCredentialResponseList()) {
|
||||
result.put(credential.getDate(), new AuthCredentialWithPniResponse(credential.getAuthCredentialResponse().toByteArray()));
|
||||
result.put(credential.getDate(), factory.apply(credential.getAuthCredentialResponse().toByteArray()));
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Loaded %d credentials from local storage", result.size()));
|
||||
@@ -77,10 +92,15 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(@NonNull Map<Long, AuthCredentialWithPniResponse> values) {
|
||||
public void write(@NonNull GroupsV2Api.CredentialResponseMaps values) {
|
||||
write(key, values.getAuthCredentialWithPniResponseHashMap());
|
||||
write(callLinkAuthKey, values.getCallLinkAuthCredentialResponseHashMap());
|
||||
}
|
||||
|
||||
private <T extends ByteArray> void write(@NonNull String key, @NonNull Map<Long, T> values) {
|
||||
TemporalAuthCredentialResponses.Builder builder = TemporalAuthCredentialResponses.newBuilder();
|
||||
|
||||
for (Map.Entry<Long, AuthCredentialWithPniResponse> entry : values.entrySet()) {
|
||||
for (Map.Entry<Long, T> entry : values.entrySet()) {
|
||||
builder.addCredentialResponse(TemporalAuthCredentialResponse.newBuilder()
|
||||
.setDate(entry.getKey())
|
||||
.setAuthCredentialResponse(ByteString.copyFrom(entry.getValue().serialize())));
|
||||
@@ -92,4 +112,8 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Written %d credentials to local storage", values.size()));
|
||||
}
|
||||
|
||||
private interface CredentialConstructor<T extends ByteArray> {
|
||||
T apply(byte[] bytes) throws InvalidInputException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
|
||||
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
@@ -133,8 +134,8 @@ import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.LinkUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
@@ -1336,8 +1337,27 @@ public class MessageContentProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
GroupId groupId = GroupId.push(callEvent.getConversationId().toByteArray());
|
||||
RecipientId recipientId = Recipient.externalGroupExact(groupId).getId();
|
||||
RecipientId recipientId;
|
||||
switch (type) {
|
||||
case AD_HOC_CALL:
|
||||
CallLinkRoomId callLinkRoomId = CallLinkRoomId.fromBytes(callEvent.getConversationId().toByteArray());
|
||||
|
||||
recipientId = SignalDatabase.recipients().getByCallLinkRoomId(callLinkRoomId).orElse(null);
|
||||
break;
|
||||
case GROUP_CALL:
|
||||
GroupId groupId = GroupId.push(callEvent.getConversationId().toByteArray());
|
||||
|
||||
recipientId = Recipient.externalGroupExact(groupId).getId();
|
||||
break;
|
||||
default:
|
||||
warn(envelopeTimestamp, "Group/Ad-hoc call event has a bad type " + type + ". Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipientId == null) {
|
||||
Log.w(TAG, "Could not find a matching group or ad-hoc call. Dropping sync event.");
|
||||
return;
|
||||
}
|
||||
|
||||
CallTable.Call call = SignalDatabase.calls().getCallById(callId, recipientId);
|
||||
if (call != null) {
|
||||
|
||||
@@ -6,12 +6,17 @@ import com.mobilecoin.lib.exceptions.SerializationException
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.libsignal.protocol.util.Pair
|
||||
import org.signal.ringrtc.CallException
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
@@ -45,6 +50,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackSyncJob
|
||||
import org.thoughtcrime.securesms.jobs.PushProcessEarlyMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshCallLinkDetailsJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -79,6 +85,9 @@ import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry
|
||||
@@ -101,6 +110,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelo
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.StoryMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.Blocked
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallLinkUpdate
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.Configuration
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.FetchLatest
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.MessageRequestResponse
|
||||
@@ -110,6 +120,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMe
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.StickerPackOperation
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.ViewOnceOpen
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -142,6 +153,7 @@ object SyncMessageProcessor {
|
||||
syncMessage.hasKeys() && syncMessage.keys.hasStorageService() -> handleSynchronizeKeys(syncMessage.keys.storageService, envelope.timestamp)
|
||||
syncMessage.hasContacts() -> handleSynchronizeContacts(syncMessage.contacts, envelope.timestamp)
|
||||
syncMessage.hasCallEvent() -> handleSynchronizeCallEvent(syncMessage.callEvent, envelope.timestamp)
|
||||
syncMessage.hasCallLinkUpdate() -> handleSynchronizeCallLink(syncMessage.callLinkUpdate, envelope.timestamp)
|
||||
else -> warn(envelope.timestamp, "Contains no known sync types...")
|
||||
}
|
||||
}
|
||||
@@ -1155,6 +1167,54 @@ object SyncMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSynchronizeCallLink(callLinkUpdate: CallLinkUpdate, envelopeTimestamp: Long) {
|
||||
if (!callLinkUpdate.hasRootKey()) {
|
||||
log(envelopeTimestamp, "Synchronize call link missing root key, ignoring.")
|
||||
return
|
||||
}
|
||||
|
||||
val callLinkRootKey = try {
|
||||
CallLinkRootKey(callLinkUpdate.rootKey.toByteArray())
|
||||
} catch (e: CallException) {
|
||||
log(envelopeTimestamp, "Synchronize call link has invalid root key, ignoring.")
|
||||
return
|
||||
}
|
||||
|
||||
val roomId = CallLinkRoomId.fromCallLinkRootKey(callLinkRootKey)
|
||||
if (SignalDatabase.callLinks.callLinkExists(roomId)) {
|
||||
log(envelopeTimestamp, "Synchronize call link for a link we already know about. Updating credentials.")
|
||||
SignalDatabase.callLinks.updateCallLinkCredentials(
|
||||
roomId,
|
||||
CallLinkCredentials(
|
||||
callLinkUpdate.rootKey.toByteArray(),
|
||||
callLinkUpdate.adminPassKey?.toByteArray()
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
SignalDatabase.callLinks.insertCallLink(
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = roomId,
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||
adminPassBytes = callLinkUpdate.adminPassKey?.toByteArray()
|
||||
),
|
||||
state = SignalCallLinkState(
|
||||
name = "",
|
||||
restrictions = CallLinkState.Restrictions.UNKNOWN,
|
||||
revoked = false,
|
||||
expiration = Instant.MIN
|
||||
),
|
||||
avatarColor = AvatarColor.random()
|
||||
)
|
||||
)
|
||||
|
||||
ApplicationDependencies.getJobManager().add(RefreshCallLinkDetailsJob(callLinkUpdate))
|
||||
}
|
||||
|
||||
private fun handleSynchronizeOneToOneCallEvent(callEvent: SyncMessage.CallEvent, envelopeTimestamp: Long) {
|
||||
val callId: Long = callEvent.id
|
||||
val timestamp: Long = callEvent.timestamp
|
||||
@@ -1207,10 +1267,28 @@ object SyncMessageProcessor {
|
||||
return
|
||||
}
|
||||
|
||||
val groupId: GroupId = GroupId.push(callEvent.conversationId.toByteArray())
|
||||
val recipientId = Recipient.externalGroupExact(groupId).id
|
||||
val recipient: Recipient? = when (type) {
|
||||
CallTable.Type.AD_HOC_CALL -> {
|
||||
val callLinkRoomId = CallLinkRoomId.fromBytes(callEvent.conversationId.toByteArray())
|
||||
val callLink = SignalDatabase.callLinks.getOrCreateCallLinkByRoomId(callLinkRoomId)
|
||||
Recipient.resolved(callLink.recipientId)
|
||||
}
|
||||
CallTable.Type.GROUP_CALL -> {
|
||||
val groupId: GroupId = GroupId.push(callEvent.conversationId.toByteArray())
|
||||
Recipient.externalGroupExact(groupId)
|
||||
}
|
||||
else -> {
|
||||
warn(envelopeTimestamp, "Unexpected type $type. Ignoring.")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, recipientId)
|
||||
if (recipient == null) {
|
||||
warn(envelopeTimestamp, "Could not process conversation id.")
|
||||
return
|
||||
}
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, recipient.id)
|
||||
|
||||
if (call != null) {
|
||||
if (call.type !== type) {
|
||||
@@ -1221,7 +1299,7 @@ object SyncMessageProcessor {
|
||||
CallTable.Event.DELETE -> SignalDatabase.calls.deleteGroupCall(call)
|
||||
CallTable.Event.ACCEPTED -> {
|
||||
if (call.timestamp < callEvent.timestamp) {
|
||||
SignalDatabase.calls.setTimestamp(call.callId, recipientId, callEvent.timestamp)
|
||||
SignalDatabase.calls.setTimestamp(call.callId, recipient.id, callEvent.timestamp)
|
||||
}
|
||||
if (callEvent.direction == SyncMessage.CallEvent.Direction.INCOMING) {
|
||||
SignalDatabase.calls.acceptIncomingGroupCall(call)
|
||||
@@ -1234,8 +1312,8 @@ object SyncMessageProcessor {
|
||||
}
|
||||
} else {
|
||||
when (event) {
|
||||
CallTable.Event.DELETE -> SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(callEvent.id, recipientId, direction, timestamp)
|
||||
CallTable.Event.ACCEPTED -> SignalDatabase.calls.insertAcceptedGroupCall(callEvent.id, recipientId, direction, timestamp)
|
||||
CallTable.Event.DELETE -> SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(callEvent.id, recipient.id, direction, timestamp)
|
||||
CallTable.Event.ACCEPTED -> SignalDatabase.calls.insertAcceptedGroupCall(callEvent.id, recipient.id, direction, timestamp)
|
||||
CallTable.Event.NOT_ACCEPTED -> warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
|
||||
else -> warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
|
||||
}
|
||||
|
||||
@@ -150,6 +150,12 @@ open class SignalServiceNetworkAccess(context: Context) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
|
||||
private val genericServerPublicParams: ByteArray = try {
|
||||
Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS)
|
||||
} catch (e: IOException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
|
||||
private val baseGHostConfigs: List<HostConfig> = listOf(
|
||||
HostConfig("https://www.google.com", G_HOST, GMAIL_CONNECTION_SPEC),
|
||||
HostConfig("https://android.clients.google.com", G_HOST, PLAY_CONNECTION_SPEC),
|
||||
@@ -173,7 +179,8 @@ open class SignalServiceNetworkAccess(context: Context) {
|
||||
networkInterceptors = interceptors,
|
||||
dns = Optional.of(DNS),
|
||||
signalProxy = Optional.empty(),
|
||||
zkGroupServerPublicParams = zkGroupServerPublicParams
|
||||
zkGroupServerPublicParams = zkGroupServerPublicParams,
|
||||
genericServerPublicParams = genericServerPublicParams
|
||||
)
|
||||
|
||||
private val censorshipConfiguration: Map<Int, SignalServiceConfiguration> = mapOf(
|
||||
@@ -224,7 +231,8 @@ open class SignalServiceNetworkAccess(context: Context) {
|
||||
networkInterceptors = interceptors,
|
||||
dns = Optional.of(DNS),
|
||||
signalProxy = if (SignalStore.proxy().isProxyEnabled) Optional.ofNullable(SignalStore.proxy().proxy) else Optional.empty(),
|
||||
zkGroupServerPublicParams = zkGroupServerPublicParams
|
||||
zkGroupServerPublicParams = zkGroupServerPublicParams,
|
||||
genericServerPublicParams = genericServerPublicParams
|
||||
)
|
||||
|
||||
open fun getConfiguration(): SignalServiceConfiguration {
|
||||
@@ -291,7 +299,8 @@ open class SignalServiceNetworkAccess(context: Context) {
|
||||
networkInterceptors = interceptors,
|
||||
dns = Optional.of(DNS),
|
||||
signalProxy = Optional.empty(),
|
||||
zkGroupServerPublicParams = zkGroupServerPublicParams
|
||||
zkGroupServerPublicParams = zkGroupServerPublicParams,
|
||||
genericServerPublicParams = genericServerPublicParams
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -1207,6 +1208,14 @@ public class Recipient {
|
||||
return FeatureFlags.phoneNumberPrivacy() && needsPniSignature;
|
||||
}
|
||||
|
||||
public boolean isCallLink() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public @NonNull CallLinkRoomId requireCallLinkRoomId() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -25,10 +25,15 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallLinkRootKey;
|
||||
import org.signal.ringrtc.CallLinkState;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -39,11 +44,16 @@ import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.proxy.ProxyBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CommunicationActions {
|
||||
@@ -329,6 +339,39 @@ public class CommunicationActions {
|
||||
}
|
||||
}
|
||||
|
||||
public static void handlePotentialCallLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl) {
|
||||
CallLinkRootKey rootKey = CallLinks.parseUrl(potentialUrl);
|
||||
if (rootKey == null) {
|
||||
Log.w(TAG, "Failed to parse root key from call link.");
|
||||
// TODO [alex] -- Display a dialog informing them that the URL was invalid.
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId());
|
||||
if (!SignalDatabase.callLinks().callLinkExists(roomId)) {
|
||||
SignalDatabase.callLinks().insertCallLink(new CallLinkTable.CallLink(
|
||||
RecipientId.UNKNOWN,
|
||||
roomId,
|
||||
new CallLinkCredentials(
|
||||
rootKey.getKeyBytes(),
|
||||
null
|
||||
),
|
||||
new SignalCallLinkState("", CallLinkState.Restrictions.UNKNOWN, false, Instant.MIN),
|
||||
AvatarColor.random()
|
||||
));
|
||||
}
|
||||
|
||||
return SignalDatabase.recipients().getByCallLinkRoomId(roomId).map(Recipient::resolved);
|
||||
}, callLinkRecipient -> {
|
||||
if (callLinkRecipient.isEmpty()) {
|
||||
// TODO [alex] -- Display a dialog informing them some error happened.
|
||||
} else {
|
||||
startVideoCall(activity, callLinkRecipient.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void startInsecureCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient) {
|
||||
try {
|
||||
Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.requireSmsAddress()));
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.services.CallLinksService;
|
||||
import org.whispersystems.signalservice.api.services.DonationsService;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
@@ -217,6 +218,10 @@ public class MockApplicationDependencyProvider implements ApplicationDependencie
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull @Override public CallLinksService provideCallLinksService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations profileOperations, @NonNull SignalServiceMessageReceiver signalServiceMessageReceiver, @NonNull SignalWebSocket signalWebSocket) {
|
||||
return null;
|
||||
|
||||
@@ -7,7 +7,14 @@ public class CredentialResponse {
|
||||
@JsonProperty
|
||||
private TemporalCredential[] credentials;
|
||||
|
||||
@JsonProperty
|
||||
private TemporalCredential[] callLinkAuthCredentials;
|
||||
|
||||
public TemporalCredential[] getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
public TemporalCredential[] getCallLinkAuthCredentials() {
|
||||
return callLinkAuthCredentials;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,11 @@ import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredential;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPni;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
|
||||
@@ -29,8 +28,10 @@ import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenExcept
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class GroupsV2Api {
|
||||
@@ -46,7 +47,7 @@ public class GroupsV2Api {
|
||||
/**
|
||||
* Provides 7 days of credentials, which you should cache.
|
||||
*/
|
||||
public HashMap<Long, AuthCredentialWithPniResponse> getCredentials(long todaySeconds)
|
||||
public CredentialResponseMaps getCredentials(long todaySeconds)
|
||||
throws IOException
|
||||
{
|
||||
return parseCredentialResponse(socket.retrieveGroupsV2Credentials(todaySeconds));
|
||||
@@ -174,10 +175,11 @@ public class GroupsV2Api {
|
||||
return socket.getGroupExternalCredential(authorization);
|
||||
}
|
||||
|
||||
private static HashMap<Long, AuthCredentialWithPniResponse> parseCredentialResponse(CredentialResponse credentialResponse)
|
||||
private static CredentialResponseMaps parseCredentialResponse(CredentialResponse credentialResponse)
|
||||
throws IOException
|
||||
{
|
||||
HashMap<Long, AuthCredentialWithPniResponse> credentials = new HashMap<>();
|
||||
HashMap<Long, AuthCredentialWithPniResponse> credentials = new HashMap<>();
|
||||
HashMap<Long, CallLinkAuthCredentialResponse> callLinkCredentials = new HashMap<>();
|
||||
|
||||
for (TemporalCredential credential : credentialResponse.getCredentials()) {
|
||||
AuthCredentialWithPniResponse authCredentialWithPniResponse;
|
||||
@@ -190,6 +192,44 @@ public class GroupsV2Api {
|
||||
credentials.put(credential.getRedemptionTime(), authCredentialWithPniResponse);
|
||||
}
|
||||
|
||||
return credentials;
|
||||
for (TemporalCredential credential : credentialResponse.getCallLinkAuthCredentials()) {
|
||||
CallLinkAuthCredentialResponse callLinkAuthCredentialResponse;
|
||||
try {
|
||||
callLinkAuthCredentialResponse = new CallLinkAuthCredentialResponse(credential.getCredential());
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
callLinkCredentials.put(credential.getRedemptionTime(), callLinkAuthCredentialResponse);
|
||||
}
|
||||
|
||||
return new CredentialResponseMaps(credentials, callLinkCredentials);
|
||||
}
|
||||
|
||||
public static class CredentialResponseMaps {
|
||||
private final Map<Long, AuthCredentialWithPniResponse> authCredentialWithPniResponseHashMap;
|
||||
private final Map<Long, CallLinkAuthCredentialResponse> callLinkAuthCredentialResponseHashMap;
|
||||
|
||||
public CredentialResponseMaps(Map<Long, AuthCredentialWithPniResponse> authCredentialWithPniResponseHashMap,
|
||||
Map<Long, CallLinkAuthCredentialResponse> callLinkAuthCredentialResponseHashMap)
|
||||
{
|
||||
this.authCredentialWithPniResponseHashMap = authCredentialWithPniResponseHashMap;
|
||||
this.callLinkAuthCredentialResponseHashMap = callLinkAuthCredentialResponseHashMap;
|
||||
}
|
||||
|
||||
public Map<Long, AuthCredentialWithPniResponse> getAuthCredentialWithPniResponseHashMap() {
|
||||
return authCredentialWithPniResponseHashMap;
|
||||
}
|
||||
|
||||
public Map<Long, CallLinkAuthCredentialResponse> getCallLinkAuthCredentialResponseHashMap() {
|
||||
return callLinkAuthCredentialResponseHashMap;
|
||||
}
|
||||
|
||||
public CredentialResponseMaps createUnmodifiableCopy() {
|
||||
return new CredentialResponseMaps(
|
||||
Map.copyOf(authCredentialWithPniResponseHashMap),
|
||||
Map.copyOf(callLinkAuthCredentialResponseHashMap)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ package org.whispersystems.signalservice.api.messages.multidevice;
|
||||
|
||||
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallLinkUpdate;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -34,6 +36,7 @@ public class SignalServiceSyncMessage {
|
||||
private final Optional<OutgoingPaymentMessage> outgoingPaymentMessage;
|
||||
private final Optional<List<ViewedMessage>> views;
|
||||
private final Optional<CallEvent> callEvent;
|
||||
private final Optional<CallLinkUpdate> callLinkUpdate;
|
||||
|
||||
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
|
||||
Optional<ContactsMessage> contacts,
|
||||
@@ -50,7 +53,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional<MessageRequestResponseMessage> messageRequestResponse,
|
||||
Optional<OutgoingPaymentMessage> outgoingPaymentMessage,
|
||||
Optional<List<ViewedMessage>> views,
|
||||
Optional<CallEvent> callEvent)
|
||||
Optional<CallEvent> callEvent,
|
||||
Optional<CallLinkUpdate> callLinkUpdate)
|
||||
{
|
||||
this.sent = sent;
|
||||
this.contacts = contacts;
|
||||
@@ -68,6 +72,7 @@ public class SignalServiceSyncMessage {
|
||||
this.outgoingPaymentMessage = outgoingPaymentMessage;
|
||||
this.views = views;
|
||||
this.callEvent = callEvent;
|
||||
this.callLinkUpdate = callLinkUpdate;
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
|
||||
@@ -86,6 +91,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -105,6 +111,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -124,6 +131,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -143,6 +151,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -162,6 +171,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -181,6 +191,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(views),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -200,6 +211,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -222,6 +234,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -241,6 +254,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -260,6 +274,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -279,6 +294,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -298,6 +314,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -317,6 +334,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -336,6 +354,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -355,6 +374,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.of(messageRequestResponse),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -374,6 +394,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.of(outgoingPaymentMessage),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
@@ -393,7 +414,28 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(callEvent));
|
||||
Optional.of(callEvent),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forCallLinkUpdate(@Nonnull CallLinkUpdate callLinkUpdate) {
|
||||
return new SignalServiceSyncMessage(Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(callLinkUpdate));
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage empty() {
|
||||
@@ -412,6 +454,7 @@ public class SignalServiceSyncMessage {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.signalservice.api.services
|
||||
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequest
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialResponse
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import java.io.IOException
|
||||
|
||||
class CallLinksService(
|
||||
configuration: SignalServiceConfiguration,
|
||||
credentialsProvider: CredentialsProvider,
|
||||
signalAgent: String,
|
||||
groupsV2Operations: GroupsV2Operations,
|
||||
automaticNetworkRetry: Boolean
|
||||
) {
|
||||
|
||||
private val pushServiceSocket = PushServiceSocket(
|
||||
configuration,
|
||||
credentialsProvider,
|
||||
signalAgent,
|
||||
groupsV2Operations.profileOperations,
|
||||
automaticNetworkRetry
|
||||
)
|
||||
|
||||
fun getCreateCallLinkAuthCredential(request: CreateCallLinkCredentialRequest): ServiceResponse<CreateCallLinkCredentialResponse> {
|
||||
return try {
|
||||
ServiceResponse.forResult(pushServiceSocket.getCallLinkAuthResponse(request), 200, "")
|
||||
} catch (e: IOException) {
|
||||
ServiceResponse.forUnknownError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,5 +17,6 @@ class SignalServiceConfiguration(
|
||||
val networkInterceptors: List<Interceptor>,
|
||||
val dns: Optional<Dns>,
|
||||
val signalProxy: Optional<SignalProxy>,
|
||||
val zkGroupServerPublicParams: ByteArray
|
||||
val zkGroupServerPublicParams: ByteArray,
|
||||
val genericServerPublicParams: ByteArray
|
||||
)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal.push
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequest
|
||||
import org.whispersystems.util.Base64
|
||||
|
||||
/**
|
||||
* Request body to create a call link credential response.
|
||||
*/
|
||||
data class CreateCallLinkAuthRequest @JsonCreator constructor(
|
||||
val createCallLinkCredentialRequest: String
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(createCallLinkCredentialRequest: CreateCallLinkCredentialRequest): CreateCallLinkAuthRequest {
|
||||
return CreateCallLinkAuthRequest(
|
||||
Base64.encodeBytes(createCallLinkCredentialRequest.serialize())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal.push
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialResponse
|
||||
import org.whispersystems.util.Base64
|
||||
|
||||
/**
|
||||
* Response body for CreateCallLinkAuthResponse
|
||||
*/
|
||||
data class CreateCallLinkAuthResponse @JsonCreator constructor(
|
||||
@JsonProperty("credential") val credential: String,
|
||||
@JsonProperty("redemptionTime") val redemptionTime: Long
|
||||
) {
|
||||
val createCallLinkCredentialResponse: CreateCallLinkCredentialResponse
|
||||
get() = CreateCallLinkCredentialResponse(Base64.decode(credential))
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
@@ -289,6 +291,7 @@ public class PushServiceSocket {
|
||||
|
||||
private static final String BACKUP_AUTH_CHECK = "/v1/backup/auth/check";
|
||||
|
||||
private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth";
|
||||
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
|
||||
|
||||
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
|
||||
@@ -1154,6 +1157,17 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public CreateCallLinkCredentialResponse getCallLinkAuthResponse(CreateCallLinkCredentialRequest request) throws IOException {
|
||||
String payload = JsonUtil.toJson(CreateCallLinkAuthRequest.create(request));
|
||||
String response = makeServiceRequest(
|
||||
CALL_LINK_CREATION_AUTH,
|
||||
"POST",
|
||||
payload
|
||||
);
|
||||
|
||||
return JsonUtil.fromJson(response, CreateCallLinkAuthResponse.class).getCreateCallLinkCredentialResponse();
|
||||
}
|
||||
|
||||
private AuthCredentials getAuthCredentials(String authPath) throws IOException {
|
||||
String response = makeServiceRequest(authPath, "GET", null);
|
||||
AuthCredentials token = JsonUtil.fromJson(response, AuthCredentials.class);
|
||||
|
||||
@@ -629,6 +629,11 @@ message SyncMessage {
|
||||
optional Event event = 6;
|
||||
}
|
||||
|
||||
message CallLinkUpdate {
|
||||
optional bytes rootKey = 1;
|
||||
optional bytes adminPassKey = 2;
|
||||
}
|
||||
|
||||
optional Sent sent = 1;
|
||||
optional Contacts contacts = 2;
|
||||
optional Groups groups = 3;
|
||||
@@ -648,6 +653,7 @@ message SyncMessage {
|
||||
reserved /*pniIdentity*/ 17;
|
||||
optional PniChangeNumber pniChangeNumber = 18;
|
||||
optional CallEvent callEvent = 19;
|
||||
optional CallLinkUpdate callLinkUpdate = 20;
|
||||
}
|
||||
|
||||
message AttachmentPointer {
|
||||
|
||||
Reference in New Issue
Block a user