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.

added cid to posts and reposts because I think that's needed for hydration

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

+33 -15
+2 -1
migrations/20260422183446_posts.sql
··· 5 5 created TIMESTAMP NOT NULL, 6 6 indexed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 7 author TEXT NOT NULL, 8 - rkey TEXT NOT NULL 8 + rkey TEXT NOT NULL, 9 + cid TEXT NOT NULL 9 10 );
+2 -1
migrations/20260423190103_reposts.sql
··· 6 6 indexed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 7 author TEXT NOT NULL, 8 8 rkey TEXT NOT NULL, 9 - subject TEXT NOT NULL 9 + subject TEXT NOT NULL, 10 + cid TEXT NOT NULL 10 11 );
+5 -3
readme.md
··· 24 24 - [x] For each user call the `/repos/add` endpoint on tap to add the follow to be tracked 25 25 - [x] Call `/repos/add` for the user of the appview 26 26 - [ ] - Handle the events for tracked users 27 - - [ ] Ignore anything other than the post lexicon types for the follows (work out what lexicon types need to be used for the user using the appview) 27 + - [x] Ignore anything other than the post lexicon types for the follows (work out what lexicon types need to be used for the user using the appview) 28 28 - [ ] Ignore if older than x amount of days (configurable number of days) - appview doesn't really need full history, only fairly recent and live 29 - - [ ] Store and index the data in sqlite (schema TBD) 30 - - [ ] MAYBE..... If the post is part of a reply, quote etc, maybe all relevant posts should be fetched too? Not sure how that will look yet, but that will be needed to render the posts in the app correctly I think. 29 + - [x] Store and index the data in sqlite (schema TBD) 30 + - [x] If it's a repost, store the subject too 31 + - [ ] If it's a reply store the parent and root 32 + - [ ] If it's a quote store the post that's quoted 31 33 - [ ] - Start to implement the endpoints required to make this an appview (looking at a [similar project konbini](https://tangled.org/why.bsky.team/konbini/) by [@why.bsky.team](https://bsky.app/profile/why.bsky.team)) 32 34 - [x] `GET /.well-known/did.json` 33 35 - [ ] `GET /xrpc/com.atproto.identity.resolveHandle`
+24 -10
src/tap.rs
··· 118 118 async fn handle_post_event(record: &RecordEvent, pool: &sqlx::SqlitePool) { 119 119 match record.action { 120 120 RecordAction::Create => { 121 + let cid: String = match record.cid.clone() { 122 + Some(x) => x.to_string(), 123 + None => "".to_string(), 124 + }; 121 125 let mut post = PostRecord { 122 126 created: Utc::now(), 123 127 indexed: Utc::now(), 124 128 author: record.did.clone().to_string(), 125 129 rkey: record.rkey.to_string(), 130 + cid: cid, 126 131 }; 127 132 128 133 let tap_post: TapPost = record.parse_record().unwrap(); // TODO: better error handling here ··· 150 155 match record.action { 151 156 RecordAction::Create => { 152 157 let tap_post: TapRepost = record.parse_record().unwrap(); // TODO: better error handling here 153 - 158 + let cid: String = match record.cid.clone() { 159 + Some(x) => x.to_string(), 160 + None => "".to_string(), 161 + }; 154 162 let mut repost = RepostRecord { 155 163 created: Utc::now(), 156 164 indexed: Utc::now(), 157 165 author: record.did.clone().to_string(), 158 166 rkey: record.rkey.to_string(), 159 167 subject: tap_post.subject.uri, 168 + cid: cid, 160 169 }; 161 170 162 171 let created_at = DateTime::parse_from_rfc3339(&tap_post.created_at); ··· 224 233 indexed: chrono::DateTime<chrono::Utc>, 225 234 author: String, 226 235 rkey: String, 236 + cid: String, 227 237 // TODO: other fields like reply to etc 228 238 } 229 239 ··· 234 244 author: String, 235 245 rkey: String, 236 246 subject: String, 247 + cid: String, 237 248 // TODO: other fields like reply to etc 238 249 } 239 250 240 251 async fn insert_post(post: PostRecord, pool: &sqlx::SqlitePool) { 241 - let result = 242 - sqlx::query("INSERT INTO posts (created, indexed, author, rkey) VALUES ($1, $2, $3, $4)") 243 - .bind(post.created) 244 - .bind(post.indexed) 245 - .bind(post.author) 246 - .bind(post.rkey) 247 - .execute(pool) 248 - .await; 252 + let result = sqlx::query( 253 + "INSERT INTO posts (created, indexed, author, rkey, cid) VALUES ($1, $2, $3, $4, $5)", 254 + ) 255 + .bind(post.created) 256 + .bind(post.indexed) 257 + .bind(post.author) 258 + .bind(post.rkey) 259 + .bind(post.cid) 260 + .execute(pool) 261 + .await; 249 262 250 263 if result.is_err() { 251 264 println!("Error inserting post into the database: {result:?}"); ··· 254 267 255 268 async fn insert_repost(repost: RepostRecord, pool: &sqlx::SqlitePool) { 256 269 let result = sqlx::query( 257 - "INSERT INTO reposts (created, indexed, author, rkey, subject) VALUES ($1, $2, $3, $4, $5)", 270 + "INSERT INTO reposts (created, indexed, author, rkey, subject, cid) VALUES ($1, $2, $3, $4, $5, $6)", 258 271 ) 259 272 .bind(repost.created) 260 273 .bind(repost.indexed) 261 274 .bind(repost.author) 262 275 .bind(repost.rkey) 263 276 .bind(repost.subject) 277 + .bind(repost.cid) 264 278 .execute(pool) 265 279 .await; 266 280