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.
  • Loading branch information
kruton committed May 26, 2024
1 parent 31eadd9 commit eee093f
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 34 deletions.
7 changes: 4 additions & 3 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 @@ -115,9 +116,9 @@ class Backuper(val context: Context) {
repository.addSubscription(subscription)

// Subscribe to Firebase topics
if (s.baseUrl == appBaseUrl) {
messenger.subscribe(s.topic)
}
val firebaseTopic = if (s.baseUrl == appBaseUrl) s.topic else topicHash(s.baseUrl, s.topic)
Log.d(TAG, "Subscribing to Firebase topic $firebaseTopic")
messenger.subscribe(firebaseTopic)

// Create dedicated channels
if (s.dedicatedChannels) {
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
16 changes: 8 additions & 8 deletions app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
)
repository.addSubscription(subscription)

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

// Fetch cached messages
try {
Expand Down Expand Up @@ -608,9 +607,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
GlobalScope.launch(Dispatchers.IO) {
repository.removeAllNotifications(subscriptionId)
repository.removeSubscription(subscriptionId)
if (subscriptionBaseUrl == appBaseUrl) {
messenger.unsubscribe(subscriptionTopic)
}

val firebaseTopic = if (subscriptionBaseUrl == appBaseUrl) subscriptionTopic else topicHash(subscriptionBaseUrl, subscriptionTopic)
Log.d(TAG, "Unsubscribing from Firebase topic $firebaseTopic")
messenger.unsubscribe(firebaseTopic)
}
finish()
}
Expand Down
9 changes: 4 additions & 5 deletions app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -466,11 +466,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
)
viewModel.add(subscription)

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

// Fetch cached messages
lifecycleScope.launch(Dispatchers.IO) {
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 eee093f

Please sign in to comment.