A personal app view to see Bsky posts of your followers (for when their app view goes down)
17
fork

Configure Feed

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

store the posts (basic for now) in the database for those accounts that are tracked as follows

Signed-off-by: Will Andrews <did:plc:dadhhalkfcq3gucaq25hjqon>

+200 -7
+118
Cargo.lock
··· 9 9 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 10 10 11 11 [[package]] 12 + name = "android_system_properties" 13 + version = "0.1.5" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 + dependencies = [ 17 + "libc", 18 + ] 19 + 20 + [[package]] 12 21 name = "anyhow" 13 22 version = "1.0.102" 14 23 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 326 335 source = "registry+https://github.com/rust-lang/crates.io-index" 327 336 checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" 328 337 dependencies = [ 338 + "iana-time-zone", 339 + "js-sys", 329 340 "num-traits", 341 + "wasm-bindgen", 342 + "windows-link", 330 343 ] 331 344 332 345 [[package]] ··· 580 593 "const-oid 0.9.6", 581 594 "pem-rfc7468", 582 595 "zeroize", 596 + ] 597 + 598 + [[package]] 599 + name = "deranged" 600 + version = "0.5.8" 601 + source = "registry+https://github.com/rust-lang/crates.io-index" 602 + checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" 603 + dependencies = [ 604 + "powerfmt", 583 605 ] 584 606 585 607 [[package]] ··· 1261 1283 ] 1262 1284 1263 1285 [[package]] 1286 + name = "iana-time-zone" 1287 + version = "0.1.65" 1288 + source = "registry+https://github.com/rust-lang/crates.io-index" 1289 + checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" 1290 + dependencies = [ 1291 + "android_system_properties", 1292 + "core-foundation-sys", 1293 + "iana-time-zone-haiku", 1294 + "js-sys", 1295 + "log", 1296 + "wasm-bindgen", 1297 + "windows-core", 1298 + ] 1299 + 1300 + [[package]] 1301 + name = "iana-time-zone-haiku" 1302 + version = "0.1.2" 1303 + source = "registry+https://github.com/rust-lang/crates.io-index" 1304 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1305 + dependencies = [ 1306 + "cc", 1307 + ] 1308 + 1309 + [[package]] 1264 1310 name = "icu_collections" 1265 1311 version = "2.2.0" 1266 1312 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1630 1676 "anyhow", 1631 1677 "atproto-tap", 1632 1678 "axum", 1679 + "chrono", 1633 1680 "dotenv", 1634 1681 "serde", 1635 1682 "serde_json", 1636 1683 "sqlx", 1684 + "time", 1637 1685 "tokio", 1638 1686 "tokio-stream", 1639 1687 ] ··· 1679 1727 "smallvec", 1680 1728 "zeroize", 1681 1729 ] 1730 + 1731 + [[package]] 1732 + name = "num-conv" 1733 + version = "0.2.1" 1734 + source = "registry+https://github.com/rust-lang/crates.io-index" 1735 + checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" 1682 1736 1683 1737 [[package]] 1684 1738 name = "num-integer" ··· 1887 1941 ] 1888 1942 1889 1943 [[package]] 1944 + name = "powerfmt" 1945 + version = "0.2.0" 1946 + source = "registry+https://github.com/rust-lang/crates.io-index" 1947 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1948 + 1949 + [[package]] 1890 1950 name = "ppv-lite86" 1891 1951 version = "0.2.21" 1892 1952 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2546 2606 dependencies = [ 2547 2607 "base64", 2548 2608 "bytes", 2609 + "chrono", 2549 2610 "crc", 2550 2611 "crossbeam-queue", 2551 2612 "either", ··· 2622 2683 "bitflags", 2623 2684 "byteorder", 2624 2685 "bytes", 2686 + "chrono", 2625 2687 "crc", 2626 2688 "digest 0.10.7", 2627 2689 "dotenvy", ··· 2663 2725 "base64", 2664 2726 "bitflags", 2665 2727 "byteorder", 2728 + "chrono", 2666 2729 "crc", 2667 2730 "dotenvy", 2668 2731 "etcetera", ··· 2697 2760 checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 2698 2761 dependencies = [ 2699 2762 "atoi", 2763 + "chrono", 2700 2764 "flume", 2701 2765 "futures-channel", 2702 2766 "futures-core", ··· 2835 2899 ] 2836 2900 2837 2901 [[package]] 2902 + name = "time" 2903 + version = "0.3.47" 2904 + source = "registry+https://github.com/rust-lang/crates.io-index" 2905 + checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" 2906 + dependencies = [ 2907 + "deranged", 2908 + "num-conv", 2909 + "powerfmt", 2910 + "serde_core", 2911 + "time-core", 2912 + ] 2913 + 2914 + [[package]] 2915 + name = "time-core" 2916 + version = "0.1.8" 2917 + source = "registry+https://github.com/rust-lang/crates.io-index" 2918 + checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" 2919 + 2920 + [[package]] 2838 2921 name = "tinystr" 2839 2922 version = "0.8.3" 2840 2923 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3291 3374 version = "1.2.1" 3292 3375 source = "registry+https://github.com/rust-lang/crates.io-index" 3293 3376 checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 3377 + 3378 + [[package]] 3379 + name = "windows-core" 3380 + version = "0.62.2" 3381 + source = "registry+https://github.com/rust-lang/crates.io-index" 3382 + checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 3383 + dependencies = [ 3384 + "windows-implement", 3385 + "windows-interface", 3386 + "windows-link", 3387 + "windows-result", 3388 + "windows-strings", 3389 + ] 3390 + 3391 + [[package]] 3392 + name = "windows-implement" 3393 + version = "0.60.2" 3394 + source = "registry+https://github.com/rust-lang/crates.io-index" 3395 + checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 3396 + dependencies = [ 3397 + "proc-macro2", 3398 + "quote", 3399 + "syn", 3400 + ] 3401 + 3402 + [[package]] 3403 + name = "windows-interface" 3404 + version = "0.59.3" 3405 + source = "registry+https://github.com/rust-lang/crates.io-index" 3406 + checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 3407 + dependencies = [ 3408 + "proc-macro2", 3409 + "quote", 3410 + "syn", 3411 + ] 3294 3412 3295 3413 [[package]] 3296 3414 name = "windows-link"
+3 -1
Cargo.toml
··· 7 7 anyhow = "1.0.102" 8 8 atproto-tap = "0.14.5" 9 9 axum = "0.8.9" 10 + chrono = "0.4.44" 10 11 dotenv = "0.15.0" 11 12 serde = { version = "1.0.228", features = ["derive"] } 12 13 serde_json = "1.0.149" 13 - sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "sqlite"] } 14 + sqlx = { version = "0.8.6", features = ["chrono", "runtime-tokio-native-tls", "sqlite"] } 15 + time = "0.3.47" 14 16 tokio = { version = "1.52.1", features = ["full"] } 15 17 tokio-stream = "0.1.18"
+9
migrations/20260422183446_posts.sql
··· 1 + -- Create a posts table 2 + CREATE TABLE IF NOT EXISTS posts 3 + ( 4 + id INTEGER PRIMARY KEY NOT NULL, 5 + created TIMESTAMP NOT NULL, 6 + indexed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 + author TEXT NOT NULL, 8 + rkey TEXT NOT NULL 9 + );
+70 -6
src/tap.rs
··· 1 + use ::chrono::{DateTime, ParseError, TimeZone, Utc}; 1 2 use atproto_tap::{RecordAction, RecordEvent, TapClient, TapEvent, connect_to}; 2 - use serde::Deserialize; 3 - use sqlx::Row; 3 + use serde::{Deserialize, Serialize}; 4 + use sqlx::{FromRow, Row, types::chrono}; 4 5 use tokio_stream::StreamExt; 5 6 6 7 #[derive(Deserialize)] ··· 78 79 async fn handle_user_follow_event(record: &RecordEvent, pool: &sqlx::SqlitePool) { 79 80 match record.action { 80 81 RecordAction::Create => { 81 - let follow: FollowRecord = record.parse_record().unwrap(); 82 + let follow: FollowRecord = record.parse_record().unwrap(); // TODO - bad error handling there 82 83 let result = sqlx::query("INSERT INTO follows (subject, rkey) VALUES ($1, $2)") 83 84 .bind(&follow.subject) 84 85 .bind(record.rkey.to_string()) ··· 111 112 } 112 113 } 113 114 114 - async fn handle_post_event(record: &RecordEvent, _pool: &sqlx::SqlitePool) { 115 + async fn handle_post_event(record: &RecordEvent, pool: &sqlx::SqlitePool) { 115 116 match record.action { 116 - RecordAction::Create => {} 117 - RecordAction::Delete => {} 117 + RecordAction::Create => { 118 + let mut post = PostRecord { 119 + created: Utc::now(), 120 + indexed: Utc::now(), 121 + author: record.did.clone().to_string(), 122 + rkey: record.rkey.to_string(), 123 + }; 124 + 125 + let tap_post: TapPost = record.parse_record().unwrap(); // TODO: better error handling here 126 + 127 + let created_at = DateTime::parse_from_rfc3339(&tap_post.created_at); 128 + match created_at { 129 + Ok(ca) => { 130 + post.created = ca.to_utc(); 131 + } 132 + Err(e) => { 133 + println!("parsing created at: {e}"); 134 + } 135 + } 136 + 137 + insert_post(post, pool).await; 138 + } 139 + RecordAction::Delete => { 140 + delete_post(record.rkey.to_string(), pool).await; 141 + } 118 142 RecordAction::Update => {} 119 143 } 120 144 } ··· 141 165 } 142 166 } 143 167 } 168 + #[derive(Debug, Deserialize)] 169 + struct TapPost { 170 + #[serde(rename = "createdAt")] 171 + created_at: String, 172 + } 173 + 174 + #[derive(Debug, FromRow)] 175 + struct PostRecord { 176 + created: chrono::DateTime<chrono::Utc>, 177 + indexed: chrono::DateTime<chrono::Utc>, 178 + author: String, 179 + rkey: String, 180 + // TODO: other fields like reply to etc 181 + } 182 + 183 + async fn insert_post(post: PostRecord, pool: &sqlx::SqlitePool) { 184 + let result = 185 + sqlx::query("INSERT INTO posts (created, indexed, author, rkey) VALUES ($1, $2, $3, $4)") 186 + .bind(post.created) 187 + .bind(post.indexed) 188 + .bind(post.author) 189 + .bind(post.rkey) 190 + .execute(pool) 191 + .await; 192 + 193 + if result.is_err() { 194 + println!("Error inserting post into the database: {result:?}"); 195 + } 196 + } 197 + 198 + async fn delete_post(rkey: String, pool: &sqlx::SqlitePool) { 199 + let result = sqlx::query("DELETE FROM posts WHERE rkey = $1") 200 + .bind(rkey) 201 + .execute(pool) 202 + .await; 203 + 204 + if result.is_err() { 205 + println!("Error deleting post from the database: {result:?}"); 206 + } 207 + }