diff --git a/Dockerfile b/Dockerfile index c280af9..a7f656b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM rust:1.89.0-bookworm AS builder +FROM rust:1.90.0-bookworm AS builder WORKDIR /app COPY . /app RUN cargo build --release # -FROM rust:1.89-slim-bookworm +FROM rust:1.90-slim-bookworm RUN apt-get update RUN apt-get install -y ca-certificates COPY --from=builder /app/target/release/my-appview /usr/local/bin/my-appview diff --git a/readme.md b/readme.md index 0e86578..ec68b25 100644 --- a/readme.md +++ b/readme.md @@ -19,10 +19,10 @@ It is not (at the moment) supposed to be a complex appview, just a way for a use - [x] Configure to be in `Dynamically Configured` mode - [x] Configure the filters to be for `app.bsky.*` (maybe limit this just to be the ones needed) - [ ] - App start up configuration for user - - [ ] Get users did from config + - [x] Get users did from config - [ ] Fetch and store that users follows - [ ] For each user call the `/repos/add` endpoint on tap to add the follow to be tracked - - [ ] Call `/repos/add` for the user of the appview + - [x] Call `/repos/add` for the user of the appview - [ ] - Handle the events for tracked users - [ ] 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) - [ ] Ignore if older than x amount of days (configurable number of days) - appview doesn't really need full history, only fairly recent and live diff --git a/src/main.rs b/src/main.rs index d83d4b4..df71053 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,97 +1,15 @@ -use atproto_tap::{TapEvent, connect_to}; -use axum::{ - Json, Router, - response::{IntoResponse, Response}, - routing::get, -}; -use serde_json::json; -use std::net::SocketAddr; -use tokio_stream::StreamExt; +mod server; +mod tap; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); - tokio::join!(run_server(), run_tap()); - Ok(()) -} - -async fn run_tap() { - let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string()); - let mut stream = connect_to(tap_url.as_str()); - - while let Some(result) = stream.next().await { - match result { - Ok(event) => match event.as_ref() { - TapEvent::Record { record, .. } => { - println!("{} {} {}", record.action, record.collection, record.did); - } - TapEvent::Identity { identity, .. } => { - println!("Identity: {} = {}", identity.did, identity.handle); - } - }, - Err(e) => eprintln!("Error: {}", e), - } - } -} - -async fn run_server() { - let host = std::env::var("APPVIEW_HOST").unwrap_or("0.0.0.0".to_string()); - let port: u16 = std::env::var("APPVIEW_PORT") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(3000); - - let app = Router::new() - .route("/", get(say_hello_text)) - .route("/.well-known/did.json", get(well_known_did_json)); - - let addr: SocketAddr = format!("{host}:{port}") - .parse() - .expect("valid socket address"); + // add the users repo to tap to ensure it has all of that when the tap subscribing starts, + // it has all the users data + let users_did = std::env::var("USERS_DID").unwrap(); + tap::add_repo(&users_did).await?; - println!("listening on {addr}"); - - let listener = tokio::net::TcpListener::bind(addr.to_string()) - .await - .unwrap(); - axum::serve(listener, app).await.unwrap(); -} - -async fn say_hello_text() -> &'static str { - return "This is an appview. Work in progress. This is my appview. There are many like it, but this one is mine"; -} - -async fn well_known_did_json() -> Response { - // TODO: work out how to pass these env from the main function - let appview_did = std::env::var("APPVIEW_DID").unwrap_or("did:web:localhost".to_string()); - let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").unwrap_or("localhost".to_string()); - - Json(json!({ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/multikey/v1"], - "id": appview_did, - "verificationMethod": [ - { - "id": "did:web:api.bsky.app#atproto", - "type": "Multikey", - "controller": "did:web:api.bsky.app", - "publicKeyMultibase": "zQ3shpRzb2NDriwCSSsce6EqGxG23kVktHZc57C3NEcuNy1jg" - } - ], - "service": [ - { - "id": "#bsky_notif", - "type": "BskyNotificationService", - "serviceEndpoint": appview_endpoint - }, - { - "id": "#bsky_appview", - "type": "BskyAppView", - "serviceEndpoint": appview_endpoint - } - ] - })) - .into_response() + tokio::join!(server::run_server(), tap::run_tap(users_did)); + Ok(()) } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..ca0bf6c --- /dev/null +++ b/src/server.rs @@ -0,0 +1,81 @@ +use axum::{ + Json, Router, + extract::State, + response::{IntoResponse, Response}, + routing::get, +}; +use serde_json::json; +use std::net::SocketAddr; + +#[derive(Clone)] +pub struct ServerConfig { + pub appview_did: String, + pub appview_endpoint: String, +} + +pub async fn run_server() { + let host = std::env::var("APPVIEW_HOST").unwrap_or("0.0.0.0".to_string()); + let port: u16 = std::env::var("APPVIEW_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(3000); + + let appview_did = std::env::var("APPVIEW_DID").unwrap(); + let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").unwrap(); + + let server_config = ServerConfig { + appview_did: appview_did, + appview_endpoint: appview_endpoint, + }; + + let app = Router::new() + .route("/", get(say_hello_text)) + .route("/.well-known/did.json", get(well_known_did_json)) + .with_state(server_config); + + let addr: SocketAddr = format!("{host}:{port}") + .parse() + .expect("valid socket address"); + + println!("listening on {addr}"); + + let listener = tokio::net::TcpListener::bind(addr.to_string()) + .await + .unwrap(); + + axum::serve(listener, app).await.unwrap(); +} + +async fn say_hello_text() -> &'static str { + return "This is an appview. Work in progress. This is my appview. There are many like it, but this one is mine"; +} + +async fn well_known_did_json(State(server_config): State) -> Response { + Json(json!({ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1"], + "id": server_config.appview_did, + "verificationMethod": [ + { + "id": "did:web:api.bsky.app#atproto", + "type": "Multikey", + "controller": "did:web:api.bsky.app", + "publicKeyMultibase": "zQ3shpRzb2NDriwCSSsce6EqGxG23kVktHZc57C3NEcuNy1jg" + } + ], + "service": [ + { + "id": "#bsky_notif", + "type": "BskyNotificationService", + "serviceEndpoint": server_config.appview_endpoint + }, + { + "id": "#bsky_appview", + "type": "BskyAppView", + "serviceEndpoint": server_config.appview_endpoint + } + ] + })) + .into_response() +} diff --git a/src/tap.rs b/src/tap.rs new file mode 100644 index 0000000..3e3ef10 --- /dev/null +++ b/src/tap.rs @@ -0,0 +1,37 @@ +use atproto_tap::{TapClient, TapEvent, connect_to}; +use tokio_stream::StreamExt; + +pub async fn run_tap(users_did: String) { + let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string()); + let mut stream = connect_to(tap_url.as_str()); + + while let Some(result) = stream.next().await { + match result { + Ok(event) => match event.as_ref() { + TapEvent::Record { record, .. } => { + // TODO: If the record collection is something other than bsky post and the appview users did + // handle -> such as a follow, block etc + // Otherwise the record collection is a bsky post in which case index + if record.did.clone().into_string() == users_did { + println!("event from user") + } + println!("{} {} {}", record.action, record.collection, record.did); + } + TapEvent::Identity { identity, .. } => { + println!("Identity: {} = {}", identity.did, identity.handle); + } + }, + Err(e) => eprintln!("Error: {}", e), + } + } +} + +pub async fn add_repo(did: &String) -> anyhow::Result<()> { + let tap_url = std::env::var("TAP_URL").unwrap_or("localhost:2480".to_string()); + let client = TapClient::new(tap_url.as_str(), Some("password".to_string())); + + // Add repositories to track + client.add_repos(&[did.as_str()]).await?; + + Ok(()) +}