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.

start consuming from tap for the appview user

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

+130 -94
+2 -2
Dockerfile
··· 1 - FROM rust:1.89.0-bookworm AS builder 1 + FROM rust:1.90.0-bookworm AS builder 2 2 WORKDIR /app 3 3 COPY . /app 4 4 RUN cargo build --release 5 5 # 6 - FROM rust:1.89-slim-bookworm 6 + FROM rust:1.90-slim-bookworm 7 7 RUN apt-get update 8 8 RUN apt-get install -y ca-certificates 9 9 COPY --from=builder /app/target/release/my-appview /usr/local/bin/my-appview
+2 -2
readme.md
··· 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 21 - [ ] - App start up configuration for user 22 - - [ ] Get users did from config 22 + - [x] Get users did from config 23 23 - [ ] Fetch and store that users follows 24 24 - [ ] For each user call the `/repos/add` endpoint on tap to add the follow to be tracked 25 - - [ ] Call `/repos/add` for the user of the appview 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) 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
+8 -90
src/main.rs
··· 1 - use atproto_tap::{TapEvent, connect_to}; 2 - use axum::{ 3 - Json, Router, 4 - response::{IntoResponse, Response}, 5 - routing::get, 6 - }; 7 - use serde_json::json; 8 - use std::net::SocketAddr; 9 - use tokio_stream::StreamExt; 1 + mod server; 2 + mod tap; 10 3 11 4 #[tokio::main] 12 5 async fn main() -> anyhow::Result<()> { 13 6 dotenv::dotenv().ok(); 14 7 15 - tokio::join!(run_server(), run_tap()); 16 - Ok(()) 17 - } 18 - 19 - async fn run_tap() { 20 - let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string()); 21 - let mut stream = connect_to(tap_url.as_str()); 22 - 23 - while let Some(result) = stream.next().await { 24 - match result { 25 - Ok(event) => match event.as_ref() { 26 - TapEvent::Record { record, .. } => { 27 - println!("{} {} {}", record.action, record.collection, record.did); 28 - } 29 - TapEvent::Identity { identity, .. } => { 30 - println!("Identity: {} = {}", identity.did, identity.handle); 31 - } 32 - }, 33 - Err(e) => eprintln!("Error: {}", e), 34 - } 35 - } 36 - } 37 - 38 - async fn run_server() { 39 - let host = std::env::var("APPVIEW_HOST").unwrap_or("0.0.0.0".to_string()); 40 - let port: u16 = std::env::var("APPVIEW_PORT") 41 - .ok() 42 - .and_then(|s| s.parse().ok()) 43 - .unwrap_or(3000); 44 - 45 - let app = Router::new() 46 - .route("/", get(say_hello_text)) 47 - .route("/.well-known/did.json", get(well_known_did_json)); 48 - 49 - let addr: SocketAddr = format!("{host}:{port}") 50 - .parse() 51 - .expect("valid socket address"); 52 - 53 - println!("listening on {addr}"); 8 + // add the users repo to tap to ensure it has all of that when the tap subscribing starts, 9 + // it has all the users data 10 + let users_did = std::env::var("USERS_DID").unwrap(); 11 + tap::add_repo(&users_did).await?; 54 12 55 - let listener = tokio::net::TcpListener::bind(addr.to_string()) 56 - .await 57 - .unwrap(); 58 - axum::serve(listener, app).await.unwrap(); 59 - } 60 - 61 - async fn say_hello_text() -> &'static str { 62 - return "This is an appview. Work in progress. This is my appview. There are many like it, but this one is mine"; 63 - } 64 - 65 - async fn well_known_did_json() -> Response { 66 - // TODO: work out how to pass these env from the main function 67 - let appview_did = std::env::var("APPVIEW_DID").unwrap_or("did:web:localhost".to_string()); 68 - let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").unwrap_or("localhost".to_string()); 69 - 70 - Json(json!({ 71 - "@context": [ 72 - "https://www.w3.org/ns/did/v1", 73 - "https://w3id.org/security/multikey/v1"], 74 - "id": appview_did, 75 - "verificationMethod": [ 76 - { 77 - "id": "did:web:api.bsky.app#atproto", 78 - "type": "Multikey", 79 - "controller": "did:web:api.bsky.app", 80 - "publicKeyMultibase": "zQ3shpRzb2NDriwCSSsce6EqGxG23kVktHZc57C3NEcuNy1jg" 81 - } 82 - ], 83 - "service": [ 84 - { 85 - "id": "#bsky_notif", 86 - "type": "BskyNotificationService", 87 - "serviceEndpoint": appview_endpoint 88 - }, 89 - { 90 - "id": "#bsky_appview", 91 - "type": "BskyAppView", 92 - "serviceEndpoint": appview_endpoint 93 - } 94 - ] 95 - })) 96 - .into_response() 13 + tokio::join!(server::run_server(), tap::run_tap(users_did)); 14 + Ok(()) 97 15 }
+81
src/server.rs
··· 1 + use axum::{ 2 + Json, Router, 3 + extract::State, 4 + response::{IntoResponse, Response}, 5 + routing::get, 6 + }; 7 + use serde_json::json; 8 + use std::net::SocketAddr; 9 + 10 + #[derive(Clone)] 11 + pub struct ServerConfig { 12 + pub appview_did: String, 13 + pub appview_endpoint: String, 14 + } 15 + 16 + pub async fn run_server() { 17 + let host = std::env::var("APPVIEW_HOST").unwrap_or("0.0.0.0".to_string()); 18 + let port: u16 = std::env::var("APPVIEW_PORT") 19 + .ok() 20 + .and_then(|s| s.parse().ok()) 21 + .unwrap_or(3000); 22 + 23 + let appview_did = std::env::var("APPVIEW_DID").unwrap(); 24 + let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").unwrap(); 25 + 26 + let server_config = ServerConfig { 27 + appview_did: appview_did, 28 + appview_endpoint: appview_endpoint, 29 + }; 30 + 31 + let app = Router::new() 32 + .route("/", get(say_hello_text)) 33 + .route("/.well-known/did.json", get(well_known_did_json)) 34 + .with_state(server_config); 35 + 36 + let addr: SocketAddr = format!("{host}:{port}") 37 + .parse() 38 + .expect("valid socket address"); 39 + 40 + println!("listening on {addr}"); 41 + 42 + let listener = tokio::net::TcpListener::bind(addr.to_string()) 43 + .await 44 + .unwrap(); 45 + 46 + axum::serve(listener, app).await.unwrap(); 47 + } 48 + 49 + async fn say_hello_text() -> &'static str { 50 + return "This is an appview. Work in progress. This is my appview. There are many like it, but this one is mine"; 51 + } 52 + 53 + async fn well_known_did_json(State(server_config): State<ServerConfig>) -> Response { 54 + Json(json!({ 55 + "@context": [ 56 + "https://www.w3.org/ns/did/v1", 57 + "https://w3id.org/security/multikey/v1"], 58 + "id": server_config.appview_did, 59 + "verificationMethod": [ 60 + { 61 + "id": "did:web:api.bsky.app#atproto", 62 + "type": "Multikey", 63 + "controller": "did:web:api.bsky.app", 64 + "publicKeyMultibase": "zQ3shpRzb2NDriwCSSsce6EqGxG23kVktHZc57C3NEcuNy1jg" 65 + } 66 + ], 67 + "service": [ 68 + { 69 + "id": "#bsky_notif", 70 + "type": "BskyNotificationService", 71 + "serviceEndpoint": server_config.appview_endpoint 72 + }, 73 + { 74 + "id": "#bsky_appview", 75 + "type": "BskyAppView", 76 + "serviceEndpoint": server_config.appview_endpoint 77 + } 78 + ] 79 + })) 80 + .into_response() 81 + }
+37
src/tap.rs
··· 1 + use atproto_tap::{TapClient, TapEvent, connect_to}; 2 + use tokio_stream::StreamExt; 3 + 4 + pub async fn run_tap(users_did: String) { 5 + let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string()); 6 + let mut stream = connect_to(tap_url.as_str()); 7 + 8 + while let Some(result) = stream.next().await { 9 + match result { 10 + Ok(event) => match event.as_ref() { 11 + TapEvent::Record { record, .. } => { 12 + // TODO: If the record collection is something other than bsky post and the appview users did 13 + // handle -> such as a follow, block etc 14 + // Otherwise the record collection is a bsky post in which case index 15 + if record.did.clone().into_string() == users_did { 16 + println!("event from user") 17 + } 18 + println!("{} {} {}", record.action, record.collection, record.did); 19 + } 20 + TapEvent::Identity { identity, .. } => { 21 + println!("Identity: {} = {}", identity.did, identity.handle); 22 + } 23 + }, 24 + Err(e) => eprintln!("Error: {}", e), 25 + } 26 + } 27 + } 28 + 29 + pub async fn add_repo(did: &String) -> anyhow::Result<()> { 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 + // Add repositories to track 34 + client.add_repos(&[did.as_str()]).await?; 35 + 36 + Ok(()) 37 + }