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.

track the follow accounts that the user follows (also stop tracking when unfollowing)

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

+64 -7
+2
docker-compose.yaml
··· 15 15 USERS_DID: ${USERS_DID} 16 16 TAP_URL: ${TAP_URL} 17 17 DATABASE_URL: ${DATABASE_URL} 18 + depends_on: 19 + - tap 18 20 19 21 tap: 20 22 container_name: tap
+3 -3
readme.md
··· 18 18 - [x] Run as part of a docker-compose file 19 19 - [x] Configure to be in `Dynamically Configured` mode 20 20 - [x] Configure the filters to be for `app.bsky.*` (maybe limit this just to be the ones needed) 21 - - [ ] - App start up configuration for user 21 + - [x] - App start up configuration for user 22 22 - [x] Get users did from config 23 - - [ ] Fetch and store that users follows 24 - - [ ] For each user call the `/repos/add` endpoint on tap to add the follow to be tracked 23 + - [x] Fetch and store that users follows 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 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)
+59 -4
src/tap.rs
··· 1 1 use atproto_tap::{RecordAction, RecordEvent, TapClient, TapEvent, connect_to}; 2 - use axum::response::sse::Event; 3 2 use serde::Deserialize; 3 + use sqlx::Row; 4 4 use tokio_stream::StreamExt; 5 5 6 6 #[derive(Deserialize)] ··· 13 13 let client = TapClient::new(tap_url.as_str(), Some("password".to_string())); 14 14 15 15 // Add repositories to track 16 - client.add_repos(&[did.as_str()]).await?; 16 + let res = client.add_repos(&[did.as_str()]).await; 17 + match res { 18 + Err(e) => { 19 + println!("failed to add repo with tap: {e}"); 20 + } 21 + Ok(()) => { 22 + println!("added repo {did}"); 23 + } 24 + } 17 25 18 26 Ok(()) 19 27 } 20 28 29 + pub async fn remove_repo(did: &String) { 30 + let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string()); 31 + let client = TapClient::new(tap_url.as_str(), Some("password".to_string())); 32 + 33 + let res = client.remove_repos(&[did.as_str()]).await; 34 + 35 + match res { 36 + Err(e) => { 37 + println!("failed to remove repo with tap: {e}"); 38 + } 39 + Ok(()) => { 40 + println!("removed repo {did}"); 41 + } 42 + } 43 + } 44 + 21 45 pub async fn run_tap(users_did: String, pool: &sqlx::SqlitePool) { 22 46 let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string()); 23 47 let mut stream = connect_to(tap_url.as_str()); ··· 56 80 RecordAction::Create => { 57 81 let follow: FollowRecord = record.parse_record().unwrap(); 58 82 let result = sqlx::query("INSERT INTO follows (subject, rkey) VALUES ($1, $2)") 59 - .bind(follow.subject) 83 + .bind(&follow.subject) 60 84 .bind(record.rkey.to_string()) 61 85 .execute(pool) 62 86 .await; ··· 64 88 if result.is_err() { 65 89 println!("Error inserting follow into the database: {result:?}"); 66 90 } 91 + 92 + // track the follow anyway 93 + _ = add_repo(&follow.subject).await; 67 94 } 68 95 RecordAction::Delete => { 96 + let follow_subject = get_follow_by_rkey(&record.rkey.to_string(), pool).await; 97 + if follow_subject != "" { 98 + _ = remove_repo(&follow_subject).await; 99 + } 100 + 69 101 let result = sqlx::query("DELETE FROM follows WHERE rkey = $1") 70 102 .bind(record.rkey.to_string()) 71 103 .execute(pool) ··· 79 111 } 80 112 } 81 113 82 - async fn handle_post_event(record: &RecordEvent, pool: &sqlx::SqlitePool) { 114 + async fn handle_post_event(record: &RecordEvent, _pool: &sqlx::SqlitePool) { 83 115 match record.action { 84 116 RecordAction::Create => {} 85 117 RecordAction::Delete => {} 86 118 RecordAction::Update => {} 87 119 } 88 120 } 121 + 122 + async fn get_follow_by_rkey(rkey: &String, pool: &sqlx::SqlitePool) -> String { 123 + let result = sqlx::query("SELECT subject FROM follows where rkey = ? LIMIT 1") 124 + .bind(rkey) 125 + .fetch_one(pool) 126 + .await; 127 + 128 + match result { 129 + Ok(row) => { 130 + if row.len() == 0 { 131 + println!("did not find subject in follows with rkey: {rkey}"); 132 + return "".to_string(); 133 + } 134 + let subject = row.get::<String, _>(0); 135 + 136 + return subject; 137 + } 138 + Err(e) => { 139 + println!("error getting follow {e}"); 140 + return "".to_string(); 141 + } 142 + } 143 + }