Skip to content

Commit

Permalink
Allow use upstream-base-url config for Android app
Browse files Browse the repository at this point in the history
This mirrors the logic used for instant notifications in the iOS app:

 * When a user subscribes to a topic, the app will subscribe to
   Firebase using sha256(baseUrl + topic).

 * When a "poll_request" call is received from Firebase, the app will
   look up the sha256(baseUrl + topic) of all subscriptions in the
   database and see if the poll_request topic matches that. If it does,
   it will poll the original server using the stored baseUrl from the
   subscriptions database.

This fixes #358.
  • Loading branch information
kruton committed May 26, 2024
1 parent 31eadd9 commit d1fc271
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 21 deletions.
3 changes: 3 additions & 0 deletions app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.topicHash
import io.heckel.ntfy.util.topicUrl
import java.io.InputStreamReader

Expand Down Expand Up @@ -117,6 +118,8 @@ class Backuper(val context: Context) {
// Subscribe to Firebase topics
if (s.baseUrl == appBaseUrl) {
messenger.subscribe(s.topic)
} else {
messenger.subscribe(topicHash(s.baseUrl, s.topic))
}

// Create dedicated channels
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/io/heckel/ntfy/db/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import io.heckel.ntfy.util.topicHash
import kotlinx.coroutines.flow.Flow
import java.lang.reflect.Type

Expand Down Expand Up @@ -90,7 +91,9 @@ data class SubscriptionWithMetadata(
val totalCount: Int,
val newCount: Int,
val lastActive: Long
)
) {
fun urlHash() = topicHash(baseUrl, topic)
}

@Entity(primaryKeys = ["id", "subscriptionId"])
data class Notification(
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/io/heckel/ntfy/db/Repository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.*
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.validUrl
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong

Expand Down Expand Up @@ -44,6 +47,13 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
.map { list -> list.map { Pair(it.id, it.instant) }.toSet() }
}

suspend fun getSubscriptionByHash(topicHash: String): Subscription? {
return toSubscription(
subscriptionDao.listFlow().map { subs -> subs.first { topicHash == it.urlHash() } }
.first()
)
}

suspend fun getSubscriptions(): List<Subscription> {
return toSubscriptionList(subscriptionDao.list())
}
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,11 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
repository.addSubscription(subscription)

// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
Log.d(TAG, "Subscribing to Firebase topic $topic")
if (baseUrl == appBaseUrl) {
Log.d(TAG, "Subscribing to Firebase topic $topic")
messenger.subscribe(topic)
} else {
messenger.subscribe(topicHash(baseUrl, topic))
}

// Fetch cached messages
Expand Down Expand Up @@ -610,6 +612,8 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
repository.removeSubscription(subscriptionId)
if (subscriptionBaseUrl == appBaseUrl) {
messenger.unsubscribe(subscriptionTopic)
} else {
messenger.unsubscribe(topicHash(subscriptionBaseUrl, subscriptionTopic))
}
}
finish()
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -466,10 +466,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
)
viewModel.add(subscription)

// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
// Subscribe to Firebase topic (even if instant, just to be sure!)
Log.d(TAG, "Subscribing to Firebase topic $topic")
if (baseUrl == appBaseUrl) {
Log.d(TAG, "Subscribing to Firebase topic $topic")
messenger.subscribe(topic)
} else {
messenger.subscribe(topicHash(baseUrl, topic))
}

// Fetch cached messages
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/io/heckel/ntfy/util/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fun topicUrlWs(baseUrl: String, topic: String, since: String) = "${topicUrl(base
fun topicUrlAuth(baseUrl: String, topic: String) = "${topicUrl(baseUrl, topic)}/auth"
fun topicUrlJsonPoll(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?poll=1&since=$since"
fun topicShortUrl(baseUrl: String, topic: String) = shortUrl(topicUrl(baseUrl, topic))
fun topicHash(baseUrl: String, topic: String) = topicUrl(baseUrl, topic).sha256()

fun subscriptionTopicShortUrl(subscription: Subscription) : String {
return topicShortUrl(subscription.baseUrl, subscription.topic)
Expand Down
39 changes: 22 additions & 17 deletions app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,28 @@ class FirebaseService : FirebaseMessagingService() {
}

private fun handlePollRequest(remoteMessage: RemoteMessage) {
val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL!
val topic = remoteMessage.data["topic"] ?: return
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val workName = "${PollWorker.WORK_NAME_ONCE_SINGE_PREFIX}_${baseUrl}_${topic}"
val workManager = WorkManager.getInstance(this)
val workRequest = OneTimeWorkRequest.Builder(PollWorker::class.java)
.setInputData(workDataOf(
PollWorker.INPUT_DATA_BASE_URL to baseUrl,
PollWorker.INPUT_DATA_TOPIC to topic
))
.setConstraints(constraints)
.build()
Log.d(TAG, "Poll request for ${topicShortUrl(baseUrl, topic)} received, scheduling unique poll worker with name $workName")

workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest)
CoroutineScope(job).launch {
val pollTopic = remoteMessage.data["topic"] ?: return@launch
val subscription = repository.getSubscriptionByHash(pollTopic)
val baseUrl = subscription?.baseUrl ?: getString(R.string.app_base_url)
val topic = subscription?.topic ?: pollTopic

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val workName = "${PollWorker.WORK_NAME_ONCE_SINGE_PREFIX}_${baseUrl}_${topic}"
val workManager = WorkManager.getInstance(this@FirebaseService)
val workRequest = OneTimeWorkRequest.Builder(PollWorker::class.java)
.setInputData(workDataOf(
PollWorker.INPUT_DATA_BASE_URL to baseUrl,
PollWorker.INPUT_DATA_TOPIC to topic
))
.setConstraints(constraints)
.build()
Log.d(TAG, "Poll request for ${topicShortUrl(baseUrl, topic)} received, scheduling unique poll worker with name $workName")

workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest)
}
}

private fun handleMessage(remoteMessage: RemoteMessage) {
Expand Down

0 comments on commit d1fc271

Please sign in to comment.