ntfy-android/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt

196 lines
8 KiB
Kotlin
Raw Normal View History

2021-11-08 07:13:32 +13:00
package io.heckel.ntfy.msg
2021-12-15 03:23:01 +13:00
import android.os.Build
import io.heckel.ntfy.BuildConfig
2022-01-19 08:28:48 +13:00
import io.heckel.ntfy.db.Notification
2022-01-28 13:57:43 +13:00
import io.heckel.ntfy.db.User
2022-02-13 07:51:41 +13:00
import io.heckel.ntfy.util.*
2021-11-14 13:26:37 +13:00
import okhttp3.*
import okhttp3.RequestBody.Companion.toRequestBody
2021-11-14 13:26:37 +13:00
import java.io.IOException
import java.net.URLEncoder
2022-01-28 13:57:43 +13:00
import java.nio.charset.StandardCharsets.UTF_8
import java.util.concurrent.TimeUnit
import kotlin.random.Random
class ApiService {
private val client = OkHttpClient.Builder()
2021-11-14 13:26:37 +13:00
.callTimeout(15, TimeUnit.SECONDS) // Total timeout for entire request
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
2022-02-13 07:51:41 +13:00
private val publishClient = OkHttpClient.Builder()
.callTimeout(5, TimeUnit.MINUTES) // Total timeout for entire request
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
2021-11-14 13:26:37 +13:00
private val subscriberClient = OkHttpClient.Builder()
.readTimeout(77, TimeUnit.SECONDS) // Assuming that keepalive messages are more frequent than this
.build()
2022-01-16 12:40:38 +13:00
private val parser = NotificationParser()
2022-02-13 07:51:41 +13:00
fun publish(
baseUrl: String,
topic: String,
user: User? = null,
message: String,
title: String = "",
priority: Int = PRIORITY_DEFAULT,
2022-02-13 07:51:41 +13:00
tags: List<String> = emptyList(),
delay: String = "",
body: RequestBody? = null,
filename: String = ""
) {
2021-11-08 07:13:32 +13:00
val url = topicUrl(baseUrl, topic)
val query = mutableListOf<String>()
if (priority in ALL_PRIORITIES) {
query.add("priority=$priority")
}
2021-11-28 10:18:09 +13:00
if (tags.isNotEmpty()) {
query.add("tags=${URLEncoder.encode(tags.joinToString(","), "UTF-8")}")
2021-11-28 10:18:09 +13:00
}
if (title.isNotEmpty()) {
query.add("title=${URLEncoder.encode(title, "UTF-8")}")
2021-11-28 10:18:09 +13:00
}
if (delay.isNotEmpty()) {
query.add("delay=${URLEncoder.encode(delay, "UTF-8")}")
}
2022-02-12 14:34:08 +13:00
if (filename.isNotEmpty()) {
query.add("filename=${URLEncoder.encode(filename, "UTF-8")}")
2022-02-12 14:34:08 +13:00
}
2022-02-12 09:55:08 +13:00
if (body != null) {
query.add("message=${URLEncoder.encode(message.replace("\n", "\\n"), "UTF-8")}")
}
val urlWithQuery = if (query.isNotEmpty()) {
url + "?" + query.joinToString("&")
2022-02-12 09:55:08 +13:00
} else {
url
2022-02-12 09:55:08 +13:00
}
val request = requestBuilder(urlWithQuery, user)
.put(body ?: message.toRequestBody())
.build()
Log.d(TAG, "Publishing to $request")
2022-02-13 07:51:41 +13:00
publishClient.newCall(request).execute().use { response ->
2022-02-05 13:52:34 +13:00
if (response.code == 401 || response.code == 403) {
throw UnauthorizedException(user)
2022-02-13 07:51:41 +13:00
} else if (response.code == 413) {
throw EntityTooLargeException()
} else if (!response.isSuccessful) {
throw Exception("Unexpected response ${response.code} when publishing to $url")
2021-11-08 07:13:32 +13:00
}
Log.d(TAG, "Successfully published to $url")
2021-11-08 07:13:32 +13:00
}
}
2022-06-19 13:01:05 +12:00
fun poll(subscriptionId: Long, baseUrl: String, topic: String, user: User?, since: String? = null): List<Notification> {
val sinceVal = since ?: "all"
val url = topicUrlJsonPoll(baseUrl, topic, sinceVal)
Log.d(TAG, "Polling topic $url")
2022-02-03 06:11:04 +13:00
val request = requestBuilder(url, user).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw Exception("Unexpected response ${response.code} when polling topic $url")
2021-11-08 07:13:32 +13:00
}
val body = response.body?.string()?.trim()
2022-12-07 14:37:30 +13:00
if (body.isNullOrEmpty()) return emptyList()
2022-01-16 12:40:38 +13:00
val notifications = body.lines().mapNotNull { line ->
parser.parse(line, subscriptionId = subscriptionId, notificationId = 0) // No notification when we poll
}
2022-01-16 12:40:38 +13:00
Log.d(TAG, "Notifications: $notifications")
return notifications
2021-11-08 07:13:32 +13:00
}
}
2021-11-17 08:08:52 +13:00
fun subscribe(
baseUrl: String,
topics: String,
2023-03-05 19:10:32 +13:00
unifiedPushTopics: String,
since: String?,
2022-01-28 13:57:43 +13:00
user: User?,
2021-11-17 08:08:52 +13:00
notify: (topic: String, Notification) -> Unit,
fail: (Exception) -> Unit
): Call {
val sinceVal = since ?: "all"
2021-11-17 08:08:52 +13:00
val url = topicUrlJson(baseUrl, topics, sinceVal)
2021-11-14 13:26:37 +13:00
Log.d(TAG, "Opening subscription connection to $url")
2023-03-05 19:10:32 +13:00
val request = requestBuilder(url, user, unifiedPushTopics).build()
2021-11-14 13:26:37 +13:00
val call = subscriberClient.newCall(request)
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
try {
if (!response.isSuccessful) {
throw Exception("Unexpected response ${response.code} when subscribing to topic $url")
}
val source = response.body?.source() ?: throw Exception("Unexpected response for $url: body is empty")
while (!source.exhausted()) {
val line = source.readUtf8Line() ?: throw Exception("Unexpected response for $url: line is null")
2022-01-16 12:40:38 +13:00
val notification = parser.parseWithTopic(line, notificationId = Random.nextInt(), subscriptionId = 0) // subscriptionId to be set downstream
if (notification != null) {
notify(notification.topic, notification.notification)
2021-11-14 13:26:37 +13:00
}
}
} catch (e: Exception) {
Log.e(TAG, "Connection to $url failed (1): ${e.message}", e)
2021-11-14 13:26:37 +13:00
fail(e)
}
}
override fun onFailure(call: Call, e: IOException) {
Log.e(TAG, "Connection to $url failed (2): ${e.message}", e)
2021-11-14 13:26:37 +13:00
fail(e)
}
})
return call
}
fun checkAuth(baseUrl: String, topic: String, user: User?): Boolean {
if (user == null) {
Log.d(TAG, "Checking anonymous read against ${topicUrl(baseUrl, topic)}")
} else {
Log.d(TAG, "Checking read access for user ${user.username} against ${topicUrl(baseUrl, topic)}")
}
2022-01-28 13:57:43 +13:00
val url = topicUrlAuth(baseUrl, topic)
2022-02-03 06:11:04 +13:00
val request = requestBuilder(url, user).build()
2022-01-28 13:57:43 +13:00
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
return true
} else if (user == null && response.code == 404) {
return true // Special case: Anonymous login to old servers return 404 since /<topic>/auth doesn't exist
} else if (response.code == 401 || response.code == 403) { // See server/server.go
return false
2022-01-28 13:57:43 +13:00
}
throw Exception("Unexpected server response ${response.code}")
2022-01-28 13:57:43 +13:00
}
}
2022-02-05 13:52:34 +13:00
class UnauthorizedException(val user: User?) : Exception()
2022-12-07 14:37:30 +13:00
class EntityTooLargeException : Exception()
2022-02-05 13:52:34 +13:00
2021-11-08 07:13:32 +13:00
companion object {
2022-01-04 12:54:18 +13:00
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"
2021-11-08 07:13:32 +13:00
private const val TAG = "NtfyApiService"
2021-12-14 15:10:48 +13:00
// These constants have corresponding values in the server codebase!
const val CONTROL_TOPIC = "~control"
2021-12-14 14:54:36 +13:00
const val EVENT_MESSAGE = "message"
const val EVENT_KEEPALIVE = "keepalive"
const val EVENT_POLL_REQUEST = "poll_request"
2022-02-03 06:11:04 +13:00
2023-03-05 19:10:32 +13:00
fun requestBuilder(url: String, user: User?, unifiedPushTopics: String? = null): Request.Builder {
2022-02-03 06:11:04 +13:00
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))
}
2023-03-05 19:10:32 +13:00
if (unifiedPushTopics != null) {
builder.addHeader("Rate-Topics", unifiedPushTopics)
}
2022-02-03 06:11:04 +13:00
return builder
}
2021-11-08 07:13:32 +13:00
}
}