···1919 - [x] Configure to be in `Dynamically Configured` mode
2020 - [x] Configure the filters to be for `app.bsky.*` (maybe limit this just to be the ones needed)
2121- [ ] - App start up configuration for user
2222- - [ ] Get users did from config
2222+ - [x] Get users did from config
2323 - [ ] Fetch and store that users follows
2424 - [ ] For each user call the `/repos/add` endpoint on tap to add the follow to be tracked
2525- - [ ] Call `/repos/add` for the user of the appview
2525+ - [x] Call `/repos/add` for the user of the appview
2626- [ ] - Handle the events for tracked users
2727 - [ ] 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)
2828 - [ ] 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
···11-use atproto_tap::{TapEvent, connect_to};
22-use axum::{
33- Json, Router,
44- response::{IntoResponse, Response},
55- routing::get,
66-};
77-use serde_json::json;
88-use std::net::SocketAddr;
99-use tokio_stream::StreamExt;
11+mod server;
22+mod tap;
103114#[tokio::main]
125async fn main() -> anyhow::Result<()> {
136 dotenv::dotenv().ok();
1471515- tokio::join!(run_server(), run_tap());
1616- Ok(())
1717-}
1818-1919-async fn run_tap() {
2020- let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string());
2121- let mut stream = connect_to(tap_url.as_str());
2222-2323- while let Some(result) = stream.next().await {
2424- match result {
2525- Ok(event) => match event.as_ref() {
2626- TapEvent::Record { record, .. } => {
2727- println!("{} {} {}", record.action, record.collection, record.did);
2828- }
2929- TapEvent::Identity { identity, .. } => {
3030- println!("Identity: {} = {}", identity.did, identity.handle);
3131- }
3232- },
3333- Err(e) => eprintln!("Error: {}", e),
3434- }
3535- }
3636-}
3737-3838-async fn run_server() {
3939- let host = std::env::var("APPVIEW_HOST").unwrap_or("0.0.0.0".to_string());
4040- let port: u16 = std::env::var("APPVIEW_PORT")
4141- .ok()
4242- .and_then(|s| s.parse().ok())
4343- .unwrap_or(3000);
4444-4545- let app = Router::new()
4646- .route("/", get(say_hello_text))
4747- .route("/.well-known/did.json", get(well_known_did_json));
4848-4949- let addr: SocketAddr = format!("{host}:{port}")
5050- .parse()
5151- .expect("valid socket address");
5252-5353- println!("listening on {addr}");
88+ // add the users repo to tap to ensure it has all of that when the tap subscribing starts,
99+ // it has all the users data
1010+ let users_did = std::env::var("USERS_DID").unwrap();
1111+ tap::add_repo(&users_did).await?;
54125555- let listener = tokio::net::TcpListener::bind(addr.to_string())
5656- .await
5757- .unwrap();
5858- axum::serve(listener, app).await.unwrap();
5959-}
6060-6161-async fn say_hello_text() -> &'static str {
6262- return "This is an appview. Work in progress. This is my appview. There are many like it, but this one is mine";
6363-}
6464-6565-async fn well_known_did_json() -> Response {
6666- // TODO: work out how to pass these env from the main function
6767- let appview_did = std::env::var("APPVIEW_DID").unwrap_or("did:web:localhost".to_string());
6868- let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").unwrap_or("localhost".to_string());
6969-7070- Json(json!({
7171- "@context": [
7272- "https://www.w3.org/ns/did/v1",
7373- "https://w3id.org/security/multikey/v1"],
7474- "id": appview_did,
7575- "verificationMethod": [
7676- {
7777- "id": "did:web:api.bsky.app#atproto",
7878- "type": "Multikey",
7979- "controller": "did:web:api.bsky.app",
8080- "publicKeyMultibase": "zQ3shpRzb2NDriwCSSsce6EqGxG23kVktHZc57C3NEcuNy1jg"
8181- }
8282- ],
8383- "service": [
8484- {
8585- "id": "#bsky_notif",
8686- "type": "BskyNotificationService",
8787- "serviceEndpoint": appview_endpoint
8888- },
8989- {
9090- "id": "#bsky_appview",
9191- "type": "BskyAppView",
9292- "serviceEndpoint": appview_endpoint
9393- }
9494- ]
9595- }))
9696- .into_response()
1313+ tokio::join!(server::run_server(), tap::run_tap(users_did));
1414+ Ok(())
9715}
+81
src/server.rs
···11+use axum::{
22+ Json, Router,
33+ extract::State,
44+ response::{IntoResponse, Response},
55+ routing::get,
66+};
77+use serde_json::json;
88+use std::net::SocketAddr;
99+1010+#[derive(Clone)]
1111+pub struct ServerConfig {
1212+ pub appview_did: String,
1313+ pub appview_endpoint: String,
1414+}
1515+1616+pub async fn run_server() {
1717+ let host = std::env::var("APPVIEW_HOST").unwrap_or("0.0.0.0".to_string());
1818+ let port: u16 = std::env::var("APPVIEW_PORT")
1919+ .ok()
2020+ .and_then(|s| s.parse().ok())
2121+ .unwrap_or(3000);
2222+2323+ let appview_did = std::env::var("APPVIEW_DID").unwrap();
2424+ let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").unwrap();
2525+2626+ let server_config = ServerConfig {
2727+ appview_did: appview_did,
2828+ appview_endpoint: appview_endpoint,
2929+ };
3030+3131+ let app = Router::new()
3232+ .route("/", get(say_hello_text))
3333+ .route("/.well-known/did.json", get(well_known_did_json))
3434+ .with_state(server_config);
3535+3636+ let addr: SocketAddr = format!("{host}:{port}")
3737+ .parse()
3838+ .expect("valid socket address");
3939+4040+ println!("listening on {addr}");
4141+4242+ let listener = tokio::net::TcpListener::bind(addr.to_string())
4343+ .await
4444+ .unwrap();
4545+4646+ axum::serve(listener, app).await.unwrap();
4747+}
4848+4949+async fn say_hello_text() -> &'static str {
5050+ return "This is an appview. Work in progress. This is my appview. There are many like it, but this one is mine";
5151+}
5252+5353+async fn well_known_did_json(State(server_config): State<ServerConfig>) -> Response {
5454+ Json(json!({
5555+ "@context": [
5656+ "https://www.w3.org/ns/did/v1",
5757+ "https://w3id.org/security/multikey/v1"],
5858+ "id": server_config.appview_did,
5959+ "verificationMethod": [
6060+ {
6161+ "id": "did:web:api.bsky.app#atproto",
6262+ "type": "Multikey",
6363+ "controller": "did:web:api.bsky.app",
6464+ "publicKeyMultibase": "zQ3shpRzb2NDriwCSSsce6EqGxG23kVktHZc57C3NEcuNy1jg"
6565+ }
6666+ ],
6767+ "service": [
6868+ {
6969+ "id": "#bsky_notif",
7070+ "type": "BskyNotificationService",
7171+ "serviceEndpoint": server_config.appview_endpoint
7272+ },
7373+ {
7474+ "id": "#bsky_appview",
7575+ "type": "BskyAppView",
7676+ "serviceEndpoint": server_config.appview_endpoint
7777+ }
7878+ ]
7979+ }))
8080+ .into_response()
8181+}
+37
src/tap.rs
···11+use atproto_tap::{TapClient, TapEvent, connect_to};
22+use tokio_stream::StreamExt;
33+44+pub async fn run_tap(users_did: String) {
55+ let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string());
66+ let mut stream = connect_to(tap_url.as_str());
77+88+ while let Some(result) = stream.next().await {
99+ match result {
1010+ Ok(event) => match event.as_ref() {
1111+ TapEvent::Record { record, .. } => {
1212+ // TODO: If the record collection is something other than bsky post and the appview users did
1313+ // handle -> such as a follow, block etc
1414+ // Otherwise the record collection is a bsky post in which case index
1515+ if record.did.clone().into_string() == users_did {
1616+ println!("event from user")
1717+ }
1818+ println!("{} {} {}", record.action, record.collection, record.did);
1919+ }
2020+ TapEvent::Identity { identity, .. } => {
2121+ println!("Identity: {} = {}", identity.did, identity.handle);
2222+ }
2323+ },
2424+ Err(e) => eprintln!("Error: {}", e),
2525+ }
2626+ }
2727+}
2828+2929+pub async fn add_repo(did: &String) -> anyhow::Result<()> {
3030+ let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string());
3131+ let client = TapClient::new(tap_url.as_str(), Some("password".to_string()));
3232+3333+ // Add repositories to track
3434+ client.add_repos(&[did.as_str()]).await?;
3535+3636+ Ok(())
3737+}