Homebrew RSS reader server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

api/fever: implement read status tracking

* add is_read column to items table with migration
* handle mark=item/feed + as=read/unread POST params
* filter unread_item_ids by actual read status
* return real is_read value from get_items queries

Previously the API accepted mark operations as no-ops and
returned all items as unread. Now read state persists and
syncs correctly with Fever-compatible clients like ReadKit.

+58 -9
+2
migrations/002_read_status.sql
··· 1 + ALTER TABLE items ADD COLUMN is_read INTEGER NOT NULL DEFAULT 0 CHECK(is_read IN (0,1)); 2 + CREATE INDEX idx_items_is_read ON items(is_read);
+21 -1
src/api/fever.rs
··· 93 93 response["saved_item_ids"] = json!(""); 94 94 } 95 95 96 - // POST write ops (mark read/saved) accepted silently as no-ops 96 + // handle mark operations from POST body 97 + if let (Some(mark), Some(action), Some(id_str)) = ( 98 + body_params.get("mark"), 99 + body_params.get("as"), 100 + body_params.get("id"), 101 + ) { 102 + if let Ok(id) = id_str.parse::<i64>() { 103 + match (mark.as_str(), action.as_str()) { 104 + ("item", "read") => { 105 + let _ = db::mark_item_read(pool, id).await; 106 + } 107 + ("item", "unread") => { 108 + let _ = db::mark_item_unread(pool, id).await; 109 + } 110 + ("feed", "read") => { 111 + let _ = db::mark_feed_read(pool, id).await; 112 + } 113 + _ => {} 114 + } 115 + } 116 + } 97 117 98 118 Json(response) 99 119 }
+35 -8
src/db.rs
··· 186 186 let rows = if let Some(ids) = with_ids { 187 187 let placeholders: Vec<&str> = ids.iter().map(|_| "?").collect(); 188 188 let sql = format!( 189 - "SELECT id, feed_id, title, author, content, url, \ 189 + "SELECT id, feed_id, title, author, content, url, is_read, \ 190 190 COALESCE(published_at, created_at) as ts \ 191 191 FROM items WHERE id IN ({}) ORDER BY id DESC", 192 192 placeholders.join(",") ··· 198 198 query.fetch_all(pool).await? 199 199 } else if let Some(since) = since_id { 200 200 sqlx::query( 201 - "SELECT id, feed_id, title, author, content, url, \ 201 + "SELECT id, feed_id, title, author, content, url, is_read, \ 202 202 COALESCE(published_at, created_at) as ts \ 203 203 FROM items WHERE id > ? ORDER BY id ASC LIMIT 50", 204 204 ) ··· 207 207 .await? 208 208 } else if let Some(max) = max_id { 209 209 sqlx::query( 210 - "SELECT id, feed_id, title, author, content, url, \ 210 + "SELECT id, feed_id, title, author, content, url, is_read, \ 211 211 COALESCE(published_at, created_at) as ts \ 212 212 FROM items WHERE id < ? ORDER BY id DESC LIMIT 50", 213 213 ) ··· 216 216 .await? 217 217 } else { 218 218 sqlx::query( 219 - "SELECT id, feed_id, title, author, content, url, \ 219 + "SELECT id, feed_id, title, author, content, url, is_read, \ 220 220 COALESCE(published_at, created_at) as ts \ 221 221 FROM items ORDER BY id DESC LIMIT 50", 222 222 ) ··· 234 234 html: row.get("content"), 235 235 url: row.get("url"), 236 236 is_saved: 0, 237 - is_read: 0, 237 + is_read: row.get("is_read"), 238 238 created_on_time: row.get("ts"), 239 239 }) 240 240 .collect()) 241 241 } 242 242 243 243 pub async fn get_unread_item_ids(pool: &SqlitePool) -> Result<String> { 244 - let rows: Vec<(i64,)> = sqlx::query_as("SELECT id FROM items ORDER BY id") 245 - .fetch_all(pool) 246 - .await?; 244 + let rows: Vec<(i64,)> = 245 + sqlx::query_as("SELECT id FROM items WHERE is_read = 0 ORDER BY id") 246 + .fetch_all(pool) 247 + .await?; 247 248 let ids: Vec<String> = rows.iter().map(|(id,)| id.to_string()).collect(); 248 249 Ok(ids.join(",")) 249 250 } ··· 340 341 .await?; 341 342 Ok(()) 342 343 } 344 + 345 + // -- Read status operations -- 346 + 347 + pub async fn mark_item_read(pool: &SqlitePool, item_id: i64) -> Result<()> { 348 + sqlx::query("UPDATE items SET is_read = 1 WHERE id = ?") 349 + .bind(item_id) 350 + .execute(pool) 351 + .await?; 352 + Ok(()) 353 + } 354 + 355 + pub async fn mark_item_unread(pool: &SqlitePool, item_id: i64) -> Result<()> { 356 + sqlx::query("UPDATE items SET is_read = 0 WHERE id = ?") 357 + .bind(item_id) 358 + .execute(pool) 359 + .await?; 360 + Ok(()) 361 + } 362 + 363 + pub async fn mark_feed_read(pool: &SqlitePool, feed_id: i64) -> Result<()> { 364 + sqlx::query("UPDATE items SET is_read = 1 WHERE feed_id = ?") 365 + .bind(feed_id) 366 + .execute(pool) 367 + .await?; 368 + Ok(()) 369 + }