Implement poll_request for Firebase to account for protected topics
This commit is contained in:
parent
28bfd087c7
commit
82177253a7
8 changed files with 89 additions and 53 deletions
app/src
main
java/io/heckel/ntfy
db
msg
ui
work
res/values
play/java/io/heckel/ntfy/firebase
|
@ -422,9 +422,9 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
|||
private const val TAG = "NtfyRepository"
|
||||
private var instance: Repository? = null
|
||||
|
||||
fun getInstance(activity: Activity): Repository {
|
||||
val database = Database.getInstance(activity.applicationContext)
|
||||
val sharedPrefs = activity.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
fun getInstance(context: Context): Repository {
|
||||
val database = Database.getInstance(context.applicationContext)
|
||||
val sharedPrefs = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
return getInstance(sharedPrefs, database)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ import java.nio.charset.StandardCharsets.UTF_8
|
|||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
class ApiService {
|
||||
private val client = OkHttpClient.Builder()
|
||||
.callTimeout(15, TimeUnit.SECONDS) // Total timeout for entire request
|
||||
|
@ -29,27 +28,23 @@ class ApiService {
|
|||
.build()
|
||||
private val parser = NotificationParser()
|
||||
|
||||
fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List<String>, delay: String) {
|
||||
fun publish(baseUrl: String, topic: String, user: User?, message: String, title: String, priority: Int, tags: List<String>, delay: String) {
|
||||
val url = topicUrl(baseUrl, topic)
|
||||
Log.d(TAG, "Publishing to $url")
|
||||
|
||||
// XXXXXXXXXXXx
|
||||
|
||||
var builder = Request.Builder()
|
||||
.url(url)
|
||||
val builder = builder(url, user)
|
||||
.put(message.toRequestBody())
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
if (priority in 1..5) {
|
||||
builder = builder.addHeader("X-Priority", priority.toString())
|
||||
builder.addHeader("X-Priority", priority.toString())
|
||||
}
|
||||
if (tags.isNotEmpty()) {
|
||||
builder = builder.addHeader("X-Tags", tags.joinToString(","))
|
||||
builder.addHeader("X-Tags", tags.joinToString(","))
|
||||
}
|
||||
if (title.isNotEmpty()) {
|
||||
builder = builder.addHeader("X-Title", title)
|
||||
builder.addHeader("X-Title", title)
|
||||
}
|
||||
if (delay.isNotEmpty()) {
|
||||
builder = builder.addHeader("X-Delay", delay)
|
||||
builder.addHeader("X-Delay", delay)
|
||||
}
|
||||
client.newCall(builder.build()).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
|
@ -59,18 +54,12 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
fun poll(subscriptionId: Long, baseUrl: String, topic: String, since: Long = 0L): List<Notification> {
|
||||
fun poll(subscriptionId: Long, baseUrl: String, topic: String, user: User?, since: Long = 0L): List<Notification> {
|
||||
val sinceVal = if (since == 0L) "all" else since.toString()
|
||||
val url = topicUrlJsonPoll(baseUrl, topic, sinceVal)
|
||||
Log.d(TAG, "Polling topic $url")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
.build()
|
||||
|
||||
// XXXXXXXXXXXx
|
||||
|
||||
val request = builder(url, user).build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Unexpected response ${response.code} when polling topic $url")
|
||||
|
@ -97,14 +86,7 @@ class ApiService {
|
|||
val sinceVal = if (since == 0L) "all" else since.toString()
|
||||
val url = topicUrlJson(baseUrl, topics, sinceVal)
|
||||
Log.d(TAG, "Opening subscription connection to $url")
|
||||
val builder = Request.Builder()
|
||||
.get()
|
||||
.url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
if (user != null) {
|
||||
builder.addHeader("Authorization", Credentials.basic(user.username, user.password, UTF_8))
|
||||
}
|
||||
val request = builder.build()
|
||||
val request = builder(url, user).build()
|
||||
val call = subscriberClient.newCall(request)
|
||||
call.enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
|
@ -140,14 +122,7 @@ class ApiService {
|
|||
Log.d(TAG, "Checking read access for user ${user.username} against ${topicUrl(baseUrl, topic)}")
|
||||
}
|
||||
val url = topicUrlAuth(baseUrl, topic)
|
||||
val builder = Request.Builder()
|
||||
.get()
|
||||
.url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
if (user != null) {
|
||||
builder.addHeader("Authorization", Credentials.basic(user.username, user.password, UTF_8))
|
||||
}
|
||||
val request = builder.build()
|
||||
val request = builder(url, user).build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
return if (user == null) {
|
||||
response.isSuccessful || response.code == 404 // Treat 404 as success (old server; to be removed in future versions)
|
||||
|
@ -157,6 +132,16 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
private fun builder(url: String, user: User?): Request.Builder {
|
||||
val builder = Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
if (user != null) {
|
||||
builder.addHeader("Authorization", Credentials.basic(user.username, user.password, UTF_8))
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
companion object {
|
||||
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"
|
||||
private const val TAG = "NtfyApiService"
|
||||
|
@ -165,5 +150,6 @@ class ApiService {
|
|||
const val CONTROL_TOPIC = "~control"
|
||||
const val EVENT_MESSAGE = "message"
|
||||
const val EVENT_KEEPALIVE = "keepalive"
|
||||
const val EVENT_POLL_REQUEST = "poll_request"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.db.Notification
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.db.Subscription
|
||||
import io.heckel.ntfy.log.Log
|
||||
import io.heckel.ntfy.util.joinTagsMap
|
||||
|
@ -65,9 +66,12 @@ class BroadcastService(private val ctx: Context) {
|
|||
}
|
||||
val delay = getStringExtra(intent,"delay") ?: ""
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val repository = Repository.getInstance(ctx)
|
||||
val user = repository.getUser(baseUrl) // May be null
|
||||
api.publish(
|
||||
baseUrl = baseUrl,
|
||||
topic = topic,
|
||||
user = user,
|
||||
message = message,
|
||||
title = title,
|
||||
priority = priority,
|
||||
|
@ -94,8 +98,10 @@ class BroadcastService(private val ctx: Context) {
|
|||
|
||||
companion object {
|
||||
private const val TAG = "NtfyBroadcastService"
|
||||
private const val MESSAGE_RECEIVED_ACTION = "io.heckel.ntfy.MESSAGE_RECEIVED"
|
||||
private const val MESSAGE_SEND_ACTION = "io.heckel.ntfy.SEND_MESSAGE" // If changed, change in manifest too!
|
||||
private const val DOES_NOT_EXIST = -2586000
|
||||
|
||||
// These constants cannot be changed without breaking the contract; also see manifest
|
||||
private const val MESSAGE_RECEIVED_ACTION = "io.heckel.ntfy.MESSAGE_RECEIVED"
|
||||
private const val MESSAGE_SEND_ACTION = "io.heckel.ntfy.SEND_MESSAGE"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -269,6 +269,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val user = repository.getUser(subscriptionBaseUrl) // May be null
|
||||
val possibleTags = listOf(
|
||||
"warning", "skull", "success", "triangular_flag_on_post", "de", "dog", "rotating_light", "cat", "bike", // Emojis
|
||||
"backup", "rsync", "de-server1", "this-is-a-tag"
|
||||
|
@ -277,7 +278,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
val tags = possibleTags.shuffled().take(Random.nextInt(0, 4))
|
||||
val title = if (Random.nextBoolean()) getString(R.string.detail_test_title) else ""
|
||||
val message = getString(R.string.detail_test_message, priority)
|
||||
api.publish(subscriptionBaseUrl, subscriptionTopic, message, title, priority, tags, delay = "")
|
||||
api.publish(subscriptionBaseUrl, subscriptionTopic, user, message, title, priority, tags, delay = "")
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
Toast
|
||||
|
@ -339,7 +340,8 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val notifications = api.poll(subscriptionId, subscriptionBaseUrl, subscriptionTopic)
|
||||
val user = repository.getUser(subscriptionBaseUrl) // May be null
|
||||
val notifications = api.poll(subscriptionId, subscriptionBaseUrl, subscriptionTopic, user)
|
||||
val newNotifications = repository.onlyNewNotifications(subscriptionId, notifications)
|
||||
val toastMessage = if (newNotifications.isEmpty()) {
|
||||
getString(R.string.refresh_message_no_results)
|
||||
|
|
|
@ -193,10 +193,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
val work = PeriodicWorkRequestBuilder<PollWorker>(POLL_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
|
||||
.setConstraints(constraints)
|
||||
.addTag(PollWorker.TAG)
|
||||
.addTag(PollWorker.WORK_NAME_PERIODIC)
|
||||
.addTag(PollWorker.WORK_NAME_PERIODIC_ALL)
|
||||
.build()
|
||||
Log.d(TAG, "Poll worker: Scheduling period work every $POLL_WORKER_INTERVAL_MINUTES minutes")
|
||||
workManager!!.enqueueUniquePeriodicWork(PollWorker.WORK_NAME_PERIODIC, workPolicy, work)
|
||||
workManager!!.enqueueUniquePeriodicWork(PollWorker.WORK_NAME_PERIODIC_ALL, workPolicy, work)
|
||||
}
|
||||
|
||||
private fun startPeriodicServiceRestartWorker() {
|
||||
|
@ -375,7 +375,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
// Fetch cached messages
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic)
|
||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
|
||||
notifications.forEach { notification -> repository.addNotification(notification) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to fetch notifications: ${e.stackTrace}")
|
||||
|
@ -418,7 +419,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
var newNotificationsCount = 0
|
||||
repository.getSubscriptions().forEach { subscription ->
|
||||
try {
|
||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic)
|
||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
|
||||
val newNotifications = repository.onlyNewNotifications(subscription.id, notifications)
|
||||
newNotifications.forEach { notification ->
|
||||
newNotificationsCount++
|
||||
|
|
|
@ -23,7 +23,6 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
|
|||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Polling for new notifications")
|
||||
val database = Database.getInstance(applicationContext)
|
||||
|
@ -32,9 +31,25 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
|
|||
val dispatcher = NotificationDispatcher(applicationContext, repository)
|
||||
val api = ApiService()
|
||||
|
||||
repository.getSubscriptions().forEach{ subscription ->
|
||||
val baseUrl = inputData.getString(INPUT_DATA_BASE_URL)
|
||||
val topic = inputData.getString(INPUT_DATA_TOPIC)
|
||||
val subscriptions = if (baseUrl != null && topic != null) {
|
||||
val subscription = repository.getSubscription(baseUrl, topic) ?: return@withContext Result.success()
|
||||
listOf(subscription)
|
||||
} else {
|
||||
repository.getSubscriptions()
|
||||
}
|
||||
|
||||
subscriptions.forEach{ subscription ->
|
||||
try {
|
||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, since = subscription.lastActive)
|
||||
val user = repository.getUser(subscription.baseUrl)
|
||||
val notifications = api.poll(
|
||||
subscriptionId = subscription.id,
|
||||
baseUrl = subscription.baseUrl,
|
||||
topic = subscription.topic,
|
||||
user = user,
|
||||
since = subscription.lastActive
|
||||
)
|
||||
val newNotifications = repository
|
||||
.onlyNewNotifications(subscription.id, notifications)
|
||||
.map { it.copy(notificationId = Random.nextInt()) }
|
||||
|
@ -55,6 +70,9 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
|
|||
companion object {
|
||||
const val VERSION = BuildConfig.VERSION_CODE
|
||||
const val TAG = "NtfyPollWorker"
|
||||
const val WORK_NAME_PERIODIC = "NtfyPollWorkerPeriodic" // Do not change
|
||||
const val WORK_NAME_PERIODIC_ALL = "NtfyPollWorkerPeriodic" // Do not change
|
||||
const val WORK_NAME_ONCE_SINGE_PREFIX = "NtfyPollWorkerSingle" // e.g. NtfyPollWorkerSingle_https://ntfy.sh_mytopic
|
||||
const val INPUT_DATA_BASE_URL = "baseUrl"
|
||||
const val INPUT_DATA_TOPIC = "topic"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
<!-- Main app-->
|
||||
<string name="app_name">Ntfy</string>
|
||||
<string name="app_base_url">https://ntfy.sh</string> <!-- If changed, you must also change google-services.json! -->
|
||||
<string name="app_base_scheme">https</string> <!-- If changed, you must also change google-services.json! -->
|
||||
<string name="app_base_host">ntfy.sh</string> <!-- If changed, you must also change google-services.json! -->
|
||||
|
||||
<!-- Notification channels -->
|
||||
<string name="channel_notifications_min_name">Notifications (Min Priority)</string>
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.heckel.ntfy.firebase
|
|||
|
||||
import android.content.Intent
|
||||
import android.util.Base64
|
||||
import androidx.work.*
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import io.heckel.ntfy.R
|
||||
|
@ -14,6 +15,8 @@ import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
|||
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||
import io.heckel.ntfy.service.SubscriberService
|
||||
import io.heckel.ntfy.util.toPriority
|
||||
import io.heckel.ntfy.util.topicShortUrl
|
||||
import io.heckel.ntfy.work.PollWorker
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -38,8 +41,9 @@ class FirebaseService : FirebaseMessagingService() {
|
|||
// Dispatch event
|
||||
val data = remoteMessage.data
|
||||
when (data["event"]) {
|
||||
ApiService.EVENT_KEEPALIVE -> handleKeepalive(remoteMessage)
|
||||
ApiService.EVENT_MESSAGE -> handleMessage(remoteMessage)
|
||||
ApiService.EVENT_KEEPALIVE -> handleKeepalive(remoteMessage)
|
||||
ApiService.EVENT_POLL_REQUEST -> handlePollRequest(remoteMessage)
|
||||
else -> Log.d(TAG, "Discarding unexpected message (2): from=${remoteMessage.from}, data=${data}")
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +58,26 @@ 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)
|
||||
}
|
||||
|
||||
private fun handleMessage(remoteMessage: RemoteMessage) {
|
||||
val data = remoteMessage.data
|
||||
val id = data["id"]
|
||||
|
|
Loading…
Add table
Reference in a new issue