2021-11-07 19:13:32 +01:00
|
|
|
package io.heckel.ntfy.msg
|
|
|
|
|
2021-12-14 15:23:01 +01:00
|
|
|
import android.os.Build
|
|
|
|
import io.heckel.ntfy.BuildConfig
|
2022-01-18 20:28:48 +01:00
|
|
|
import io.heckel.ntfy.db.Notification
|
2022-01-28 01:57:43 +01:00
|
|
|
import io.heckel.ntfy.db.User
|
2022-01-17 06:19:05 +01:00
|
|
|
import io.heckel.ntfy.log.Log
|
2022-01-28 01:57:43 +01:00
|
|
|
import io.heckel.ntfy.util.topicUrl
|
|
|
|
import io.heckel.ntfy.util.topicUrlAuth
|
|
|
|
import io.heckel.ntfy.util.topicUrlJson
|
|
|
|
import io.heckel.ntfy.util.topicUrlJsonPoll
|
2021-11-14 01:26:37 +01:00
|
|
|
import okhttp3.*
|
2021-11-12 01:41:29 +01:00
|
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
2021-11-14 01:26:37 +01:00
|
|
|
import java.io.IOException
|
2022-01-28 01:57:43 +01:00
|
|
|
import java.nio.charset.StandardCharsets.UTF_8
|
2021-11-12 01:41:29 +01:00
|
|
|
import java.util.concurrent.TimeUnit
|
2021-11-15 22:24:31 +01:00
|
|
|
import kotlin.random.Random
|
2021-11-12 01:41:29 +01:00
|
|
|
|
2022-01-28 01:57:43 +01:00
|
|
|
|
2021-11-12 01:41:29 +01:00
|
|
|
class ApiService {
|
|
|
|
private val client = OkHttpClient.Builder()
|
2021-11-14 01:26:37 +01:00
|
|
|
.callTimeout(15, TimeUnit.SECONDS) // Total timeout for entire request
|
|
|
|
.connectTimeout(15, TimeUnit.SECONDS)
|
|
|
|
.readTimeout(15, TimeUnit.SECONDS)
|
|
|
|
.writeTimeout(15, TimeUnit.SECONDS)
|
|
|
|
.build()
|
|
|
|
private val subscriberClient = OkHttpClient.Builder()
|
2022-01-16 21:21:34 +01:00
|
|
|
.readTimeout(77, TimeUnit.SECONDS) // Assuming that keepalive messages are more frequent than this
|
2021-11-12 01:41:29 +01:00
|
|
|
.build()
|
2022-01-16 00:40:38 +01:00
|
|
|
private val parser = NotificationParser()
|
2021-11-12 01:41:29 +01:00
|
|
|
|
2021-12-11 21:09:07 +01:00
|
|
|
fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List<String>, delay: String) {
|
2021-11-07 19:13:32 +01:00
|
|
|
val url = topicUrl(baseUrl, topic)
|
2021-11-12 01:41:29 +01:00
|
|
|
Log.d(TAG, "Publishing to $url")
|
|
|
|
|
2022-01-28 04:42:22 +01:00
|
|
|
// XXXXXXXXXXXx
|
|
|
|
|
2021-11-27 22:18:09 +01:00
|
|
|
var builder = Request.Builder()
|
|
|
|
.url(url)
|
|
|
|
.put(message.toRequestBody())
|
2021-12-14 15:23:01 +01:00
|
|
|
.addHeader("User-Agent", USER_AGENT)
|
2021-12-11 21:09:07 +01:00
|
|
|
if (priority in 1..5) {
|
|
|
|
builder = builder.addHeader("X-Priority", priority.toString())
|
|
|
|
}
|
2021-11-27 22:18:09 +01:00
|
|
|
if (tags.isNotEmpty()) {
|
|
|
|
builder = builder.addHeader("X-Tags", tags.joinToString(","))
|
|
|
|
}
|
|
|
|
if (title.isNotEmpty()) {
|
|
|
|
builder = builder.addHeader("X-Title", title)
|
|
|
|
}
|
2021-12-11 21:09:07 +01:00
|
|
|
if (delay.isNotEmpty()) {
|
|
|
|
builder = builder.addHeader("X-Delay", delay)
|
|
|
|
}
|
2021-11-27 22:18:09 +01:00
|
|
|
client.newCall(builder.build()).execute().use { response ->
|
2021-11-12 01:41:29 +01:00
|
|
|
if (!response.isSuccessful) {
|
|
|
|
throw Exception("Unexpected response ${response.code} when publishing to $url")
|
2021-11-07 19:13:32 +01:00
|
|
|
}
|
2021-11-12 01:41:29 +01:00
|
|
|
Log.d(TAG, "Successfully published to $url")
|
2021-11-07 19:13:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-12 01:37:34 +01:00
|
|
|
fun poll(subscriptionId: Long, baseUrl: String, topic: String, since: Long = 0L): List<Notification> {
|
2022-01-02 01:37:09 +01:00
|
|
|
val sinceVal = if (since == 0L) "all" else since.toString()
|
|
|
|
val url = topicUrlJsonPoll(baseUrl, topic, sinceVal)
|
2021-11-12 01:41:29 +01:00
|
|
|
Log.d(TAG, "Polling topic $url")
|
|
|
|
|
2021-12-14 15:23:01 +01:00
|
|
|
val request = Request.Builder()
|
|
|
|
.url(url)
|
|
|
|
.addHeader("User-Agent", USER_AGENT)
|
|
|
|
.build()
|
2022-01-30 23:38:33 +01:00
|
|
|
|
|
|
|
// XXXXXXXXXXXx
|
|
|
|
|
2021-11-12 01:41:29 +01:00
|
|
|
client.newCall(request).execute().use { response ->
|
|
|
|
if (!response.isSuccessful) {
|
|
|
|
throw Exception("Unexpected response ${response.code} when polling topic $url")
|
2021-11-07 19:13:32 +01:00
|
|
|
}
|
2021-11-12 01:41:29 +01:00
|
|
|
val body = response.body?.string()?.trim()
|
|
|
|
if (body == null || body.isEmpty()) return emptyList()
|
2022-01-16 00:40:38 +01:00
|
|
|
val notifications = body.lines().mapNotNull { line ->
|
|
|
|
parser.parse(line, subscriptionId = subscriptionId, notificationId = 0) // No notification when we poll
|
2021-11-12 01:41:29 +01:00
|
|
|
}
|
2022-01-16 00:40:38 +01:00
|
|
|
|
2021-11-12 01:41:29 +01:00
|
|
|
Log.d(TAG, "Notifications: $notifications")
|
|
|
|
return notifications
|
2021-11-07 19:13:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-16 20:08:52 +01:00
|
|
|
fun subscribe(
|
|
|
|
baseUrl: String,
|
|
|
|
topics: String,
|
|
|
|
since: Long,
|
2022-01-28 01:57:43 +01:00
|
|
|
user: User?,
|
2021-11-16 20:08:52 +01:00
|
|
|
notify: (topic: String, Notification) -> Unit,
|
|
|
|
fail: (Exception) -> Unit
|
|
|
|
): Call {
|
2021-11-14 01:26:37 +01:00
|
|
|
val sinceVal = if (since == 0L) "all" else since.toString()
|
2021-11-16 20:08:52 +01:00
|
|
|
val url = topicUrlJson(baseUrl, topics, sinceVal)
|
2021-11-14 01:26:37 +01:00
|
|
|
Log.d(TAG, "Opening subscription connection to $url")
|
2022-01-28 01:57:43 +01:00
|
|
|
val builder = Request.Builder()
|
|
|
|
.get()
|
2021-12-14 15:23:01 +01:00
|
|
|
.url(url)
|
|
|
|
.addHeader("User-Agent", USER_AGENT)
|
2022-01-28 01:57:43 +01:00
|
|
|
if (user != null) {
|
|
|
|
builder.addHeader("Authorization", Credentials.basic(user.username, user.password, UTF_8))
|
|
|
|
}
|
|
|
|
val request = builder.build()
|
2021-11-14 01:26:37 +01: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 00:40:38 +01: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 01:26:37 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e: Exception) {
|
2021-11-14 19:54:48 +01:00
|
|
|
Log.e(TAG, "Connection to $url failed (1): ${e.message}", e)
|
2021-11-14 01:26:37 +01:00
|
|
|
fail(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
override fun onFailure(call: Call, e: IOException) {
|
2021-11-14 19:54:48 +01:00
|
|
|
Log.e(TAG, "Connection to $url failed (2): ${e.message}", e)
|
2021-11-14 01:26:37 +01:00
|
|
|
fail(e)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return call
|
|
|
|
}
|
|
|
|
|
2022-02-01 01:34:34 +01:00
|
|
|
fun authTopicRead(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 01:57:43 +01:00
|
|
|
val url = topicUrlAuth(baseUrl, topic)
|
|
|
|
val builder = Request.Builder()
|
|
|
|
.get()
|
|
|
|
.url(url)
|
|
|
|
.addHeader("User-Agent", USER_AGENT)
|
2022-02-01 01:34:34 +01:00
|
|
|
if (user != null) {
|
|
|
|
builder.addHeader("Authorization", Credentials.basic(user.username, user.password, UTF_8))
|
2022-01-28 01:57:43 +01:00
|
|
|
}
|
|
|
|
val request = builder.build()
|
|
|
|
client.newCall(request).execute().use { response ->
|
2022-02-01 01:34:34 +01:00
|
|
|
return if (user == null) {
|
|
|
|
response.isSuccessful || response.code == 404 // Treat 404 as success (old server; to be removed in future versions)
|
2022-01-28 01:57:43 +01:00
|
|
|
} else {
|
2022-02-01 01:34:34 +01:00
|
|
|
response.isSuccessful
|
2022-01-28 01:57:43 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-07 19:13:32 +01:00
|
|
|
companion object {
|
2022-01-04 00:54:18 +01:00
|
|
|
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"
|
2021-11-07 19:13:32 +01:00
|
|
|
private const val TAG = "NtfyApiService"
|
2021-12-14 03:10:48 +01:00
|
|
|
|
|
|
|
// These constants have corresponding values in the server codebase!
|
|
|
|
const val CONTROL_TOPIC = "~control"
|
2021-12-14 02:54:36 +01:00
|
|
|
const val EVENT_MESSAGE = "message"
|
|
|
|
const val EVENT_KEEPALIVE = "keepalive"
|
2021-11-07 19:13:32 +01:00
|
|
|
}
|
|
|
|
}
|