From 667af27bfb49164cdc5f7d7307b16d8fd7eaf7b3 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 16 Jun 2023 10:38:33 +0930 Subject: [PATCH] Utilise TokenManager and ExpiryManager --- .../securesms/ApplicationContext.java | 1 - .../notifications/FcmTokenManager.kt | 26 +++ .../securesms/notifications/ExpiryManager.kt | 25 +++ .../securesms/notifications/FcmUtils.kt | 7 +- .../notifications/FirebasePushManager.kt | 207 ++++++++++-------- .../notifications/FirebasePushModule.kt | 3 +- .../messaging/jobs/NotifyPNServerJob.kt | 18 +- .../sending_receiving/notifications/Models.kt | 48 ++-- .../notifications/PushNotificationAPI.kt | 104 ++++----- .../libsignal/utilities/PromiseUtilities.kt | 4 + .../session/libsignal/utilities/Retrying.kt | 1 + 11 files changed, 260 insertions(+), 184 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt create mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 688e30d879..e10a7a7dc7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -508,7 +508,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO } public void clearAllData(boolean isMigratingToV2KeyPair) { - PushNotificationAPI.unregister(); if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) { firebaseInstanceIdJob.cancel(null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt new file mode 100644 index 0000000000..e1182a480b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import org.session.libsession.utilities.TextSecurePreferences + +class FcmTokenManager( + private val context: Context, + private val expiryManager: ExpiryManager +) { + val isUsingFCM get() = TextSecurePreferences.isUsingFCM(context) + + var fcmToken + get() = TextSecurePreferences.getFCMToken(context) + set(value) { + TextSecurePreferences.setFCMToken(context, value) + if (value != null) markTime() else clearTime() + } + + val requiresUnregister get() = fcmToken != null + + private fun clearTime() = expiryManager.clearTime() + private fun markTime() = expiryManager.markTime() + private fun isExpired() = expiryManager.isExpired() + + fun isInvalid(): Boolean = fcmToken == null || isExpired() +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt new file mode 100644 index 0000000000..36c986f0ea --- /dev/null +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt @@ -0,0 +1,25 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import org.session.libsession.utilities.TextSecurePreferences + +class ExpiryManager( + private val context: Context, + private val interval: Int = 12 * 60 * 60 * 1000 +) { + fun isExpired() = currentTime() > time + interval + + fun markTime() { + time = currentTime() + } + + fun clearTime() { + time = 0 + } + + private var time + get() = TextSecurePreferences.getLastFCMUploadTime(context) + set(value) = TextSecurePreferences.setLastFCMUploadTime(context, value) + + private fun currentTime() = System.currentTimeMillis() +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt index de3d14d430..af3f49a26a 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.notifications import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks import com.google.firebase.iid.FirebaseInstanceId import com.google.firebase.iid.InstanceIdResult import kotlinx.coroutines.* @@ -9,9 +10,7 @@ import kotlinx.coroutines.* fun getFcmInstanceId(body: (Task)->Unit): Job = MainScope().launch(Dispatchers.IO) { val task = FirebaseInstanceId.getInstance().instanceId - while (!task.isComplete && isActive) { - // wait for task to complete while we are active - } + Tasks.await(task) if (!isActive) return@launch // don't 'complete' task if we were canceled body(task) -} \ No newline at end of file +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 108b10f147..4a0d8307e5 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.notifications import android.content.Context -import com.google.android.gms.tasks.Task -import com.google.firebase.iid.InstanceIdResult import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.interfaces.AEAD @@ -15,12 +13,14 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.combine.and import nl.komponents.kovenant.functional.map import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata +import org.session.libsession.messaging.sending_receiving.notifications.Response import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse @@ -29,7 +29,6 @@ import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.Version -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.bencode.Bencode import org.session.libsession.utilities.bencode.BencodeList @@ -37,19 +36,20 @@ import org.session.libsession.utilities.bencode.BencodeString import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Namespace +import org.session.libsignal.utilities.emptyPromise import org.session.libsignal.utilities.retryIfNeeded import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.KeyPairUtilities private const val TAG = "FirebasePushManager" -class FirebasePushManager(private val context: Context, private val prefs: TextSecurePreferences): PushManager { +class FirebasePushManager (private val context: Context): PushManager { companion object { private const val maxRetryCount = 4 - private const val tokenExpirationInterval = 12 * 60 * 60 * 1000 } + private val tokenManager = FcmTokenManager(context, ExpiryManager(context)) private var firebaseInstanceIdJob: Job? = null private val sodium = LazySodiumAndroid(SodiumAndroid()) @@ -59,12 +59,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) } - return Key.fromHexString( - IdentityKeyUtil.retrieve( - context, - IdentityKeyUtil.NOTIFICATION_KEY - ) - ) + return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) } fun decrypt(encPayload: ByteArray): ByteArray? { @@ -78,8 +73,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val expectedList = (bencoded.decode() as? BencodeList)?.values ?: error("Failed to decode bencoded list from payload") - val metadataJson = (expectedList[0] as? BencodeString)?.value - ?: error("no metadata") + val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null @@ -94,78 +88,81 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS return content } + @Synchronized override fun refresh(force: Boolean) { firebaseInstanceIdJob?.apply { if (force) cancel() else if (isActive) return } - firebaseInstanceIdJob = getFcmInstanceId { refresh(it, force) } - } - - private fun refresh(task: Task, force: Boolean) { - Log.d(TAG, "refresh") - - // context in here is Dispatchers.IO - if (!task.isSuccessful) { - Log.w(TAG, "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception - ) - return - } - - val token: String = task.result?.token ?: return - val userPublicKey = getLocalNumber(context) ?: return - val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return - - when { - prefs.isUsingFCM() -> register(token, userPublicKey, userEdKey, force) - prefs.getFCMToken() != null -> unregister(token, userPublicKey, userEdKey) - } - } - - private fun unregister(token: String, userPublicKey: String, userEdKey: KeyPair) { - val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s - // if we want to support passing namespace list, here is the place to do it - val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() - val signature = ByteArray(Sign.BYTES) - sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) - - val requestParameters = UnsubscriptionRequest( - pubkey = userPublicKey, - session_ed25519 = userEdKey.publicKey.asHexString, - service = "firebase", - sig_ts = timestamp, - signature = Base64.encodeBytes(signature), - service_info = mapOf("token" to token), - ) - - val url = "${PushNotificationAPI.server}/unsubscribe" - val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { response -> - if (response.success == true) { - Log.d(TAG, "Unsubscribe FCM success") - TextSecurePreferences.setFCMToken(context, null) - PushNotificationAPI.unregister() - } else { - Log.e(TAG, "Couldn't unregister for FCM due to error: ${response.message}") - } - }.fail { exception -> - Log.e(TAG, "Couldn't unregister for FCM due to error: ${exception}.", exception) + firebaseInstanceIdJob = getFcmInstanceId { task -> + when { + task.isSuccessful -> task.result?.token?.let { refresh(it, force).get() } + else -> Log.w(TAG, "getFcmInstanceId failed." + task.exception) } } } - private fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List = listOf(Namespace.DEFAULT)) { - Log.d(TAG, "register token: $token") + private fun refresh(token: String, force: Boolean): Promise<*, Exception> { + val userPublicKey = getLocalNumber(context) ?: return emptyPromise() + val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() - val oldToken = TextSecurePreferences.getFCMToken(context) - val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) -// if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { -// Log.d(TAG, "not registering now... not forced or expired") -// return -// } + return when { + tokenManager.isUsingFCM -> register(force, token, userPublicKey, userEdKey) + tokenManager.requiresUnregister -> unregister(token, userPublicKey, userEdKey) + else -> emptyPromise() + } + } + /** + * Register for push notifications if: + * force is true + * there is no FCM Token + * FCM Token has expired + */ + private fun register( + force: Boolean, + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List = listOf(Namespace.DEFAULT) + ): Promise<*, Exception> = if (force || tokenManager.isInvalid()) { + register(token, publicKey, userEd25519Key, namespaces) + } else emptyPromise() + + /** + * Register for push notifications. + */ + private fun register( + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List = listOf(Namespace.DEFAULT) + ): Promise<*, Exception> = PushNotificationAPI.register(token) and getSubscription( + token, publicKey, userEd25519Key, namespaces + ) fail { + Log.e(TAG, "Couldn't register for FCM due to error: $it.", it) + } success { + tokenManager.fcmToken = token + } + + private fun unregister( + token: String, + userPublicKey: String, + userEdKey: KeyPair + ): Promise<*, Exception> = PushNotificationAPI.unregister() and getUnsubscription( + token, userPublicKey, userEdKey + ) fail { + Log.e(TAG, "Couldn't unregister for FCM due to error: ${it}.", it) + } success { + tokenManager.fcmToken = null + } + + private fun getSubscription( + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List + ): Promise { val pnKey = getOrCreateNotificationKey() val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s @@ -183,33 +180,51 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS signature = Base64.encodeBytes(signature), service_info = mapOf("token" to token), enc_key = pnKey.asHexString, - ) + ).let(Json::encodeToString) - val url = "${PushNotificationAPI.server}/subscribe" - val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { response -> - if (response.isSuccess()) { - Log.d(TAG, "Success $token") - TextSecurePreferences.setFCMToken(context, token) - TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) - PushNotificationAPI.register(token) - } else { - val (_, message) = response.errorInfo() - Log.e(TAG, "Couldn't register for FCM due to error: $message.") - } - }.fail { exception -> - Log.e(TAG, "Couldn't register for FCM due to error: ${exception}.", exception) - } - } + return retryResponseBody("subscribe", requestParameters) } - private inline fun getResponseBody(request: Request): Promise = - OnionRequestAPI.sendOnionRequest( + private fun getUnsubscription( + token: String, + userPublicKey: String, + userEdKey: KeyPair + ): Promise { + val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s + // if we want to support passing namespace list, here is the place to do it + val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) + + val requestParameters = UnsubscriptionRequest( + pubkey = userPublicKey, + session_ed25519 = userEdKey.publicKey.asHexString, + service = "firebase", + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + ).let(Json::encodeToString) + + return retryResponseBody("unsubscribe", requestParameters) + } + + private inline fun retryResponseBody(path: String, requestParameters: String): Promise = + retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) } + + private inline fun getResponseBody(path: String, requestParameters: String): Promise { + val url = "${PushNotificationAPI.server}/$path" + val body = RequestBody.create(MediaType.get("application/json"), requestParameters) + val request = Request.Builder().url(url).post(body).build() + + return OnionRequestAPI.sendOnionRequest( request, PushNotificationAPI.server, PushNotificationAPI.serverPublicKey, Version.V4 - ).map { response -> Json.decodeFromStream(response.body!!.inputStream()) } + ).map { response -> + response.body!!.inputStream() + .let { Json.decodeFromStream(it) } + .also { if (it.isFailure()) throw Exception("error: ${it.message}.") } + } + } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt index 30045d6c25..3f33016a02 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt @@ -17,8 +17,7 @@ object FirebasePushModule { @Singleton fun provideFirebasePushManager( @ApplicationContext context: Context, - prefs: TextSecurePreferences, - ) = FirebasePushManager(context, prefs) + ) = FirebasePushManager(context) } @Module diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 25fb2194c8..7bb2707a80 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -10,6 +10,7 @@ import okhttp3.RequestBody import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI.server import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.OnionRequestAPI @@ -33,18 +34,21 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { } override fun execute(dispatcherName: String) { - val server = PushNotificationAPI.server val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) val url = "${server}/notify" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) + val request = Request.Builder().url(url).post(body).build() retryIfNeeded(4) { - OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response -> - val code = response.info["code"] as? Int - if (code == null || code == 0) { - Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.") + OnionRequestAPI.sendOnionRequest( + request, + server, + PushNotificationAPI.serverPublicKey, + Version.V2 + ) success { response -> + when (response.info["code"]) { + null, 0 -> Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"]}.") } - }.fail { exception -> + } fail { exception -> Log.d("Loki", "Couldn't notify PN server due to error: $exception.") } }.success { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt index 141202ef66..ea2492d5af 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt @@ -54,37 +54,39 @@ data class UnsubscriptionRequest( val service_info: Map, ) +/** invalid values, missing reuqired arguments etc, details in message */ +private const val UNPARSEABLE_ERROR = 1 +/** the "service" value is not active / valid */ +private const val SERVICE_NOT_AVAILABLE = 2 +/** something getting wrong internally talking to the backend */ +private const val SERVICE_TIMEOUT = 3 +/** other error processing the subscription (details in the message) */ +private const val GENERIC_ERROR = 4 + @Serializable data class SubscriptionResponse( - val error: Int? = null, - val message: String? = null, - val success: Boolean? = null, + override val error: Int? = null, + override val message: String? = null, + override val success: Boolean? = null, val added: Boolean? = null, val updated: Boolean? = null, -) { - companion object { - /** invalid values, missing reuqired arguments etc, details in message */ - const val UNPARSEABLE_ERROR = 1 - /** the "service" value is not active / valid */ - const val SERVICE_NOT_AVAILABLE = 2 - /** something getting wrong internally talking to the backend */ - const val SERVICE_TIMEOUT = 3 - /** other error processing the subscription (details in the message) */ - const val GENERIC_ERROR = 4 - } - fun isSuccess() = success == true && error == null - fun errorInfo() = if (success != true && error != null) { - error to message - } else null to null -} +): Response @Serializable data class UnsubscribeResponse( - val error: Int? = null, - val message: String? = null, - val success: Boolean? = null, + override val error: Int? = null, + override val message: String? = null, + override val success: Boolean? = null, val removed: Boolean? = null, -) +): Response + +interface Response { + val error: Int? + val message: String? + val success: Boolean? + fun isSuccess() = success == true && error == null + fun isFailure() = !isSuccess() +} @Serializable data class PushNotificationMetadata( diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index a2eaf353fd..b39b30f090 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -1,7 +1,10 @@ package org.session.libsession.messaging.sending_receiving.notifications import android.annotation.SuppressLint +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.all import nl.komponents.kovenant.functional.map +import nl.komponents.kovenant.task import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody @@ -11,6 +14,7 @@ import org.session.libsession.snode.Version import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.emptyPromise import org.session.libsignal.utilities.retryIfNeeded @SuppressLint("StaticFieldLeak") @@ -29,62 +33,57 @@ object PushNotificationAPI { Unsubscribe("unsubscribe_closed_group"); } - fun register(token: String? = TextSecurePreferences.getLegacyFCMToken(context)) { - Log.d(TAG, "register: $token") - - register(token, TextSecurePreferences.getLocalNumber(context)) + fun register( + token: String? = TextSecurePreferences.getLegacyFCMToken(context) + ): Promise<*, Exception> = all( + register(token, TextSecurePreferences.getLocalNumber(context)), subscribeGroups() - } + ) - fun register(token: String?, publicKey: String?) { - Log.d(TAG, "register($token)") - - token ?: return - publicKey ?: return - val parameters = mapOf("token" to token, "pubKey" to publicKey) - val url = "$legacyServer/register" - val body = - RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) + fun register(token: String?, publicKey: String?): Promise = retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), legacyServer, legacyServerPublicKey, Version.V2) - .map { response -> - val code = response.info["code"] as? Int - if (code != null && code != 0) { - TextSecurePreferences.setLegacyFCMToken(context, token) - } else { - Log.d(TAG, "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.") - } - }.fail { exception -> + val parameters = mapOf("token" to token!!, "pubKey" to publicKey!!) + val url = "$legacyServer/register" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body).build() + + OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> + when (response.info["code"]) { + null, 0 -> throw Exception("error: ${response.info["message"]}.") + else -> TextSecurePreferences.setLegacyFCMToken(context, token) + } + } fail { exception -> Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") } } - } /** * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager. */ - @JvmStatic - fun unregister() { - val token = TextSecurePreferences.getLegacyFCMToken(context) ?: return + fun unregister(): Promise<*, Exception> { + val token = TextSecurePreferences.getLegacyFCMToken(context) ?: emptyPromise() - val parameters = mapOf( "token" to token ) - val url = "$legacyServer/unregister" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body).build() - retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> - TextSecurePreferences.clearLegacyFCMToken(context) - when (response.info["code"]) { - null, 0 -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${response.info["message"]}.") + return retryIfNeeded(maxRetryCount) { + val parameters = mapOf( "token" to token ) + val url = "$legacyServer/unregister" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body).build() + + OnionRequestAPI.sendOnionRequest( + request, + legacyServer, + legacyServerPublicKey, + Version.V2 + ) success { + when (it.info["code"]) { + null, 0 -> throw Exception("error: ${it.info["message"]}.") else -> Log.d(TAG, "unregisterV1 success token: $token") } - }.fail { exception -> - Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") + TextSecurePreferences.clearLegacyFCMToken(context) + } fail { + exception -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") } } - - unsubscribeGroups() } // Legacy Closed Groups @@ -97,7 +96,7 @@ object PushNotificationAPI { private fun subscribeGroups( closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = closedGroupPublicKeys.forEach { performGroupOperation(ClosedGroupOperation.Subscribe, it, publicKey) } + ) = closedGroupPublicKeys.map { performGroupOperation(ClosedGroupOperation.Subscribe, it, publicKey) }.let(::all) fun unsubscribeGroup( closedGroupPublicKey: String, @@ -107,27 +106,30 @@ object PushNotificationAPI { private fun unsubscribeGroups( closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = closedGroupPublicKeys.forEach { performGroupOperation(ClosedGroupOperation.Unsubscribe, it, publicKey) } + ) = closedGroupPublicKeys.map { performGroupOperation(ClosedGroupOperation.Unsubscribe, it, publicKey) }.let(::all) private fun performGroupOperation( operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String - ) { - if (!TextSecurePreferences.isUsingFCM(context)) return - + ): Promise<*, Exception> { val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) val url = "$legacyServer/${operation.rawValue}" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() - retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> - when (response.info["code"]) { - null, 0 -> Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${response.info["message"]}.") + return retryIfNeeded(maxRetryCount) { + OnionRequestAPI.sendOnionRequest( + request, + legacyServer, + legacyServerPublicKey, + Version.V2 + ) success { + when (it.info["code"]) { + null, 0 -> throw Exception("${it.info["message"]}") else -> Log.d(TAG, "performGroupOperation success: ${operation.rawValue}") } - }.fail { exception -> + } fail { exception -> Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${exception}.") } } diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt b/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt index d6361289c5..8095b44597 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt @@ -3,8 +3,12 @@ package org.session.libsignal.utilities import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred +import nl.komponents.kovenant.task import java.util.concurrent.TimeoutException +fun emptyPromise() = EMPTY_PROMISE +private val EMPTY_PROMISE: Promise<*, java.lang.Exception> = task {} + fun Promise.get(defaultValue: V): V { return try { get() diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt b/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt index 9f1b1a3e82..28ed93b5a5 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt @@ -28,3 +28,4 @@ fun > retryIfNeeded(maxRetryCount: Int, retryInterv retryIfNeeded() return deferred.promise } +