use crate::appview_push::AppViewPushClient; use crate::firehose::Firehose; use at_blob::S3BlobStore; use at_identity::plc::PlcClient; use at_lexicon::{Lex, LexRegistry}; use at_repo::blockstore::MemoryBlockstore; use at_shared::config::AppConfig; use sqlx::PgPool; use std::sync::Arc; #[derive(Clone)] pub struct AppState { pub cfg: AppConfig, pub db: PgPool, pub blob: S3BlobStore, pub lex: Arc, pub blockstore: Arc, pub plc: PlcClient, pub appview: AppViewPushClient, /// Live fan-out for `com.atproto.sync.subscribeRepos`. /// /// Lives on the shared state rather than in the route module because the /// *write* paths publish into it — `routes::helpers::apply_repo_write` /// hands every committed event over here — while the WebSocket handler /// only subscribes. Cloning `AppState` clones the sender, which is the /// intended way to reach it from a handler. pub firehose: Firehose, } impl AppState { pub async fn new(cfg: AppConfig, db: PgPool, blob: S3BlobStore) -> Self { let mut lex = LexRegistry::new(); lex.lexicons.insert( "app.twi.post".to_string(), Lex::from_json(include_str!("../../../lexicons/app/twi/post.json")).unwrap(), ); // AT-Protocol standard collections: only the records the user // might legitimately create server-side (feed.like, feed.repost, // graph.follow). The full atproto collection library is out of // scope — for anything else, callers pass `validate: false` in the // createRecord body. lex.lexicons.insert( "app.bsky.feed.like".to_string(), Lex::from_json(include_str!("../../../lexicons/app/bsky/feed/like.json")).unwrap(), ); lex.lexicons.insert( "app.bsky.feed.repost".to_string(), Lex::from_json(include_str!("../../../lexicons/app/bsky/feed/repost.json")).unwrap(), ); // Follow record. Its absence was a real outage: the desktop // client creates follows through `createRecord`, which validates // by default, so every follow came back // `unknown lexicon: app.bsky.graph.follow` — the button could // never have worked. `subject` is a bare DID string here, not a // strongRef like like/repost use, matching what the client sends // and what the AppView's `follow_subject_did` reads. lex.lexicons.insert( "app.bsky.graph.follow".to_string(), Lex::from_json(include_str!("../../../lexicons/app/bsky/graph/follow.json")).unwrap(), ); // Profile record — avatar/banner/display name/description. // Validates the createRecord body when the Tauri client calls // its setProfile command. Other fields stay optional so a // brand-new account with an empty profile is legal. lex.lexicons.insert( "app.bsky.actor.profile".to_string(), Lex::from_json(include_str!("../../../lexicons/app/bsky/actor/profile.json")).unwrap(), ); let plc_url = cfg.plc_directory_url.clone(); // The PDS speaks to the AppView via the cluster-internal URL — // never the public one, because the ingest endpoint is unauth'd // in dev mode (and uses a shared secret in prod). The base URL // is the same as `appview_public_url` in our single-host dev // setup, but operators can override with `APPVIEW_INTERNAL_URL`. let appview_url = std::env::var("APPVIEW_INTERNAL_URL") .unwrap_or_else(|_| cfg.appview_public_url.clone()); let appview = AppViewPushClient::new(appview_url, cfg.appview_ingest_secret.clone()); Self { cfg, db, blob, lex: Arc::new(lex), blockstore: Arc::new(MemoryBlockstore::new()), plc: PlcClient::new(plc_url), appview, firehose: Firehose::new(), } } }