summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/stash
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-22 08:08:58 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-22 08:08:58 +0000
commitd8ce4b3c0f383e844b125d2d3abd618715bb3c89 (patch)
treeb586ff9b42392f2c44645773c2a2f2aadb9db406 /app/src/main/java/org/terst/feedreader/stash
Initial commit: Feeds, an Android RSS reader
Feed subscriptions (RSS 2.0 + Atom, hand-rolled parser), OPML import/export, WorkManager periodic fetch with conditional GET, Room-backed local storage (feeds/articles/filter rules), keyword mute/highlight filtering. Reader mode: on-device extraction via Mozilla's Readability.js (vendored from github.com/mozilla/readability, pinned to release 0.6.0) run in a hidden WebView, for feed articles whose provided content is too thin to be the real article body. Save: explicit action (from a feed article, or the Android share-sheet target for any URL) archives to Stash via POST /artifacts?sync=true, while a local on-device copy (Readability-extracted) is always kept so saved articles read offline regardless of Stash's reachability. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/stash')
-rw-r--r--app/src/main/java/org/terst/feedreader/stash/StashClient.kt82
1 files changed, 82 insertions, 0 deletions
diff --git a/app/src/main/java/org/terst/feedreader/stash/StashClient.kt b/app/src/main/java/org/terst/feedreader/stash/StashClient.kt
new file mode 100644
index 0000000..28cc2b3
--- /dev/null
+++ b/app/src/main/java/org/terst/feedreader/stash/StashClient.kt
@@ -0,0 +1,82 @@
+package org.terst.feedreader.stash
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+
+private val json = Json { ignoreUnknownKeys = true }
+
+@Serializable
+private data class CreateArtifactRequest(
+ val type: String = "url",
+ @SerialName("source_url") val sourceUrl: String,
+ val title: String? = null
+)
+
+@Serializable
+data class ArtifactResponse(
+ val id: String,
+ val title: String? = null,
+ @SerialName("content_md") val contentMd: String? = null,
+ @SerialName("processing_steps") val processingSteps: List<String> = emptyList()
+)
+
+@Serializable
+private data class DuplicateErrorResponse(@SerialName("existing_id") val existingId: String)
+
+sealed class StashSaveResult {
+ data class Saved(val artifact: ArtifactResponse) : StashSaveResult()
+ data class AlreadySaved(val existingId: String) : StashSaveResult()
+ data class Failed(val message: String) : StashSaveResult()
+}
+
+/**
+ * Client for the parts of Stash's API this app needs: POST /artifacts to
+ * archive an article. Uses sync=true so content_md comes back in the same
+ * response (server runs extract+summary_lite inline, up to ~30s) --
+ * tags/full-summary/embeddings still fill in async server-side afterward,
+ * but this app doesn't need to wait on or poll for those.
+ */
+class StashClient(
+ private val client: OkHttpClient,
+ private val baseUrl: String,
+ private val apiKey: String
+) {
+ suspend fun saveArticle(url: String, title: String?): StashSaveResult = withContext(Dispatchers.IO) {
+ if (baseUrl.isBlank() || apiKey.isBlank()) {
+ return@withContext StashSaveResult.Failed("Stash isn't configured (set base URL and API key in Settings)")
+ }
+
+ val body = json.encodeToString(
+ CreateArtifactRequest.serializer(),
+ CreateArtifactRequest(sourceUrl = url, title = title)
+ ).toRequestBody("application/json".toMediaType())
+
+ val request = Request.Builder()
+ .url("$baseUrl/artifacts?sync=true")
+ .header("X-API-Key", apiKey)
+ .post(body)
+ .build()
+
+ runCatching {
+ client.newCall(request).execute().use { response ->
+ val responseBody = response.body?.string().orEmpty()
+ when {
+ response.code == 201 || response.code == 200 ->
+ StashSaveResult.Saved(json.decodeFromString(ArtifactResponse.serializer(), responseBody))
+ response.code == 409 ->
+ StashSaveResult.AlreadySaved(
+ json.decodeFromString(DuplicateErrorResponse.serializer(), responseBody).existingId
+ )
+ else -> StashSaveResult.Failed("HTTP ${response.code}")
+ }
+ }
+ }.getOrElse { StashSaveResult.Failed(it.message ?: "Network error") }
+ }
+}