diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-22 08:08:58 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-22 08:08:58 +0000 |
| commit | d8ce4b3c0f383e844b125d2d3abd618715bb3c89 (patch) | |
| tree | b586ff9b42392f2c44645773c2a2f2aadb9db406 /app/src/main/java/org/terst/feedreader/stash/StashClient.kt | |
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/StashClient.kt')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/stash/StashClient.kt | 82 |
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") } + } +} |
