Utilise TokenManager and ExpiryManager

This commit is contained in:
andrew 2023-06-16 10:38:33 +09:30
parent 153aa4ceaa
commit 667af27bfb
11 changed files with 260 additions and 184 deletions

View File

@ -508,7 +508,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
} }
public void clearAllData(boolean isMigratingToV2KeyPair) { public void clearAllData(boolean isMigratingToV2KeyPair) {
PushNotificationAPI.unregister();
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) { if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
firebaseInstanceIdJob.cancel(null); firebaseInstanceIdJob.cancel(null);
} }

View File

@ -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()
}

View File

@ -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()
}

View File

@ -2,6 +2,7 @@
package org.thoughtcrime.securesms.notifications package org.thoughtcrime.securesms.notifications
import com.google.android.gms.tasks.Task 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.FirebaseInstanceId
import com.google.firebase.iid.InstanceIdResult import com.google.firebase.iid.InstanceIdResult
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -9,9 +10,7 @@ import kotlinx.coroutines.*
fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().launch(Dispatchers.IO) { fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().launch(Dispatchers.IO) {
val task = FirebaseInstanceId.getInstance().instanceId val task = FirebaseInstanceId.getInstance().instanceId
while (!task.isComplete && isActive) { Tasks.await(task)
// wait for task to complete while we are active
}
if (!isActive) return@launch // don't 'complete' task if we were canceled if (!isActive) return@launch // don't 'complete' task if we were canceled
body(task) body(task)
} }

View File

@ -1,8 +1,6 @@
package org.thoughtcrime.securesms.notifications package org.thoughtcrime.securesms.notifications
import android.content.Context 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.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.interfaces.AEAD import com.goterl.lazysodium.interfaces.AEAD
@ -15,12 +13,14 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.combine.and
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI 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.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.SubscriptionRequest
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse 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.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.Version 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.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.bencode.Bencode import org.session.libsession.utilities.bencode.Bencode
import org.session.libsession.utilities.bencode.BencodeList 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.Base64
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.emptyPromise
import org.session.libsignal.utilities.retryIfNeeded import org.session.libsignal.utilities.retryIfNeeded
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
private const val TAG = "FirebasePushManager" 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 { companion object {
private const val maxRetryCount = 4 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 var firebaseInstanceIdJob: Job? = null
private val sodium = LazySodiumAndroid(SodiumAndroid()) 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) val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF)
IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString)
} }
return Key.fromHexString( return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY))
IdentityKeyUtil.retrieve(
context,
IdentityKeyUtil.NOTIFICATION_KEY
)
)
} }
fun decrypt(encPayload: ByteArray): ByteArray? { 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 val expectedList = (bencoded.decode() as? BencodeList)?.values
?: error("Failed to decode bencoded list from payload") ?: error("Failed to decode bencoded list from payload")
val metadataJson = (expectedList[0] as? BencodeString)?.value val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata")
?: error("no metadata")
val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson))
val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null 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 return content
} }
@Synchronized
override fun refresh(force: Boolean) { override fun refresh(force: Boolean) {
firebaseInstanceIdJob?.apply { firebaseInstanceIdJob?.apply {
if (force) cancel() else if (isActive) return if (force) cancel() else if (isActive) return
} }
firebaseInstanceIdJob = getFcmInstanceId { refresh(it, force) } firebaseInstanceIdJob = getFcmInstanceId { task ->
}
private fun refresh(task: Task<InstanceIdResult>, 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 { when {
prefs.isUsingFCM() -> register(token, userPublicKey, userEdKey, force) task.isSuccessful -> task.result?.token?.let { refresh(it, force).get() }
prefs.getFCMToken() != null -> unregister(token, userPublicKey, userEdKey) else -> Log.w(TAG, "getFcmInstanceId failed." + task.exception)
}
}
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<UnsubscribeResponse>(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)
} }
} }
} }
private fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List<Int> = listOf(Namespace.DEFAULT)) { private fun refresh(token: String, force: Boolean): Promise<*, Exception> {
Log.d(TAG, "register token: $token") val userPublicKey = getLocalNumber(context) ?: return emptyPromise()
val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise()
val oldToken = TextSecurePreferences.getFCMToken(context) return when {
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) tokenManager.isUsingFCM -> register(force, token, userPublicKey, userEdKey)
// if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { tokenManager.requiresUnregister -> unregister(token, userPublicKey, userEdKey)
// Log.d(TAG, "not registering now... not forced or expired") else -> emptyPromise()
// return }
// } }
/**
* 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<Int> = 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<Int> = 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<Int>
): Promise<SubscriptionResponse, Exception> {
val pnKey = getOrCreateNotificationKey() val pnKey = getOrCreateNotificationKey()
val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s 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), signature = Base64.encodeBytes(signature),
service_info = mapOf("token" to token), service_info = mapOf("token" to token),
enc_key = pnKey.asHexString, enc_key = pnKey.asHexString,
) ).let(Json::encodeToString)
val url = "${PushNotificationAPI.server}/subscribe" return retryResponseBody("subscribe", requestParameters)
val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
getResponseBody<SubscriptionResponse>(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)
}
}
} }
private inline fun <reified T> getResponseBody(request: Request): Promise<T, Exception> = private fun getUnsubscription(
OnionRequestAPI.sendOnionRequest( token: String,
userPublicKey: String,
userEdKey: KeyPair
): Promise<UnsubscribeResponse, Exception> {
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 <reified T: Response> retryResponseBody(path: String, requestParameters: String): Promise<T, Exception> =
retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) }
private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
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, request,
PushNotificationAPI.server, PushNotificationAPI.server,
PushNotificationAPI.serverPublicKey, PushNotificationAPI.serverPublicKey,
Version.V4 Version.V4
).map { response -> Json.decodeFromStream(response.body!!.inputStream()) } ).map { response ->
response.body!!.inputStream()
.let { Json.decodeFromStream<T>(it) }
.also { if (it.isFailure()) throw Exception("error: ${it.message}.") }
}
}
} }

View File

@ -17,8 +17,7 @@ object FirebasePushModule {
@Singleton @Singleton
fun provideFirebasePushManager( fun provideFirebasePushManager(
@ApplicationContext context: Context, @ApplicationContext context: Context,
prefs: TextSecurePreferences, ) = FirebasePushManager(context)
) = FirebasePushManager(context, prefs)
} }
@Module @Module

View File

@ -10,6 +10,7 @@ import okhttp3.RequestBody
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE 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
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI.server
import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
@ -33,18 +34,21 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
} }
override fun execute(dispatcherName: String) { override fun execute(dispatcherName: String) {
val server = PushNotificationAPI.server
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
val url = "${server}/notify" val url = "${server}/notify"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) 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) { retryIfNeeded(4) {
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response -> OnionRequestAPI.sendOnionRequest(
val code = response.info["code"] as? Int request,
if (code == null || code == 0) { server,
Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.") 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.") Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
} }
}.success { }.success {

View File

@ -54,37 +54,39 @@ data class UnsubscriptionRequest(
val service_info: Map<String, String>, val service_info: Map<String, String>,
) )
/** 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 @Serializable
data class SubscriptionResponse( data class SubscriptionResponse(
val error: Int? = null, override val error: Int? = null,
val message: String? = null, override val message: String? = null,
val success: Boolean? = null, override val success: Boolean? = null,
val added: Boolean? = null, val added: Boolean? = null,
val updated: Boolean? = null, val updated: Boolean? = null,
) { ): Response
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
}
@Serializable @Serializable
data class UnsubscribeResponse( data class UnsubscribeResponse(
val error: Int? = null, override val error: Int? = null,
val message: String? = null, override val message: String? = null,
val success: Boolean? = null, override val success: Boolean? = null,
val removed: 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 @Serializable
data class PushNotificationMetadata( data class PushNotificationMetadata(

View File

@ -1,7 +1,10 @@
package org.session.libsession.messaging.sending_receiving.notifications package org.session.libsession.messaging.sending_receiving.notifications
import android.annotation.SuppressLint import android.annotation.SuppressLint
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import nl.komponents.kovenant.task
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
@ -11,6 +14,7 @@ import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.emptyPromise
import org.session.libsignal.utilities.retryIfNeeded import org.session.libsignal.utilities.retryIfNeeded
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@ -29,62 +33,57 @@ object PushNotificationAPI {
Unsubscribe("unsubscribe_closed_group"); Unsubscribe("unsubscribe_closed_group");
} }
fun register(token: String? = TextSecurePreferences.getLegacyFCMToken(context)) { fun register(
Log.d(TAG, "register: $token") token: String? = TextSecurePreferences.getLegacyFCMToken(context)
): Promise<*, Exception> = all(
register(token, TextSecurePreferences.getLocalNumber(context)) register(token, TextSecurePreferences.getLocalNumber(context)),
subscribeGroups() subscribeGroups()
} )
fun register(token: String?, publicKey: String?) { fun register(token: String?, publicKey: String?): Promise<Unit, Exception> =
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)
retryIfNeeded(maxRetryCount) { retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), legacyServer, legacyServerPublicKey, Version.V2) val parameters = mapOf("token" to token!!, "pubKey" to publicKey!!)
.map { response -> val url = "$legacyServer/register"
val code = response.info["code"] as? Int val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
if (code != null && code != 0) { val request = Request.Builder().url(url).post(body).build()
TextSecurePreferences.setLegacyFCMToken(context, token)
} else { OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response ->
Log.d(TAG, "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.") when (response.info["code"]) {
null, 0 -> throw Exception("error: ${response.info["message"]}.")
else -> TextSecurePreferences.setLegacyFCMToken(context, token)
} }
}.fail { exception -> } fail { exception ->
Log.d(TAG, "Couldn't register for FCM due to error: ${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. * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager.
*/ */
@JvmStatic fun unregister(): Promise<*, Exception> {
fun unregister() { val token = TextSecurePreferences.getLegacyFCMToken(context) ?: emptyPromise()
val token = TextSecurePreferences.getLegacyFCMToken(context) ?: return
return retryIfNeeded(maxRetryCount) {
val parameters = mapOf( "token" to token ) val parameters = mapOf( "token" to token )
val url = "$legacyServer/unregister" val url = "$legacyServer/unregister"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body).build() val request = Request.Builder().url(url).post(body).build()
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> OnionRequestAPI.sendOnionRequest(
TextSecurePreferences.clearLegacyFCMToken(context) request,
when (response.info["code"]) { legacyServer,
null, 0 -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${response.info["message"]}.") legacyServerPublicKey,
Version.V2
) success {
when (it.info["code"]) {
null, 0 -> throw Exception("error: ${it.info["message"]}.")
else -> Log.d(TAG, "unregisterV1 success token: $token") else -> Log.d(TAG, "unregisterV1 success token: $token")
} }
}.fail { exception -> TextSecurePreferences.clearLegacyFCMToken(context)
Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") } fail {
exception -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.")
} }
} }
unsubscribeGroups()
} }
// Legacy Closed Groups // Legacy Closed Groups
@ -97,7 +96,7 @@ object PushNotificationAPI {
private fun subscribeGroups( private fun subscribeGroups(
closedGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), closedGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(),
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
) = closedGroupPublicKeys.forEach { performGroupOperation(ClosedGroupOperation.Subscribe, it, publicKey) } ) = closedGroupPublicKeys.map { performGroupOperation(ClosedGroupOperation.Subscribe, it, publicKey) }.let(::all)
fun unsubscribeGroup( fun unsubscribeGroup(
closedGroupPublicKey: String, closedGroupPublicKey: String,
@ -107,27 +106,30 @@ object PushNotificationAPI {
private fun unsubscribeGroups( private fun unsubscribeGroups(
closedGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), closedGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(),
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! 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( private fun performGroupOperation(
operation: ClosedGroupOperation, operation: ClosedGroupOperation,
closedGroupPublicKey: String, closedGroupPublicKey: String,
publicKey: String publicKey: String
) { ): Promise<*, Exception> {
if (!TextSecurePreferences.isUsingFCM(context)) return
val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey )
val url = "$legacyServer/${operation.rawValue}" val url = "$legacyServer/${operation.rawValue}"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body).build() val request = Request.Builder().url(url).post(body).build()
retryIfNeeded(maxRetryCount) { return retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> OnionRequestAPI.sendOnionRequest(
when (response.info["code"]) { request,
null, 0 -> Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${response.info["message"]}.") legacyServer,
legacyServerPublicKey,
Version.V2
) success {
when (it.info["code"]) {
null, 0 -> throw Exception("${it.info["message"]}")
else -> Log.d(TAG, "performGroupOperation success: ${operation.rawValue}") else -> Log.d(TAG, "performGroupOperation success: ${operation.rawValue}")
} }
}.fail { exception -> } fail { exception ->
Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${exception}.") Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${exception}.")
} }
} }

View File

@ -3,8 +3,12 @@ package org.session.libsignal.utilities
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.task
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
fun emptyPromise() = EMPTY_PROMISE
private val EMPTY_PROMISE: Promise<*, java.lang.Exception> = task {}
fun <V, E : Throwable> Promise<V, E>.get(defaultValue: V): V { fun <V, E : Throwable> Promise<V, E>.get(defaultValue: V): V {
return try { return try {
get() get()

View File

@ -28,3 +28,4 @@ fun <V, T : Promise<V, Exception>> retryIfNeeded(maxRetryCount: Int, retryInterv
retryIfNeeded() retryIfNeeded()
return deferred.promise return deferred.promise
} }