use axum::{ Json, Router, extract::State, http::StatusCode, response::{IntoResponse, Response}, routing::get, }; use jacquard_axum::{IntoRouter, service_auth::ServiceAuth, service_auth::ServiceAuthConfig}; use jacquard_common::types::string::Did; use jacquard_identity::JacquardResolver; use jacquard_identity::resolver::ResolverOptions; use serde_json::json; use std::net::SocketAddr; use jacquard_api::{ app_bsky::{ actor::get_profile::{GetProfile, GetProfileRequest}, feed::{get_feed::GetFeedRequest, get_timeline::GetTimelineRequest}, }, com_atproto::repo::get_record::GetRecordRequest, }; use crate::xrpc::routes::{ app_bsky_actor_get_profile, app_bsky_feed_get_feed, app_bsky_feed_get_timeline, com_atproto_repo_get_record, }; #[derive(Clone)] pub struct ServerConfig { pub appview_did: String, pub appview_endpoint: String, pub user_did: 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: String = std::env::var("APPVIEW_DID").expect("APPVIEW_DID missing"); let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").expect("APPVIEW_HOSTNAME missing"); let users_did = std::env::var("USERS_DID").expect("USERS_DID missing"); let server_config = ServerConfig { appview_did: appview_did.clone(), appview_endpoint: appview_endpoint.clone(), user_did: users_did, }; let service_did = Did::new_owned(appview_did).expect("APPVIEW_DID produced an invalid did:web"); let resolver = JacquardResolver::new(reqwest::Client::new(), ResolverOptions::default()); let auth_config = ServiceAuthConfig::new(service_did, resolver.clone()); let app_state = AppState::new(server_config, auth_config, resolver.clone()); let app = Router::::new() .route("/", get(say_hello_text)) .route("/.well-known/did.json", get(well_known_did_json)) .merge(GetTimelineRequest::into_router::<_, AppState, _>( app_bsky_feed_get_timeline, )) .merge(GetFeedRequest::into_router::<_, AppState, _>( app_bsky_feed_get_feed, )) .merge(GetRecordRequest::into_router::<_, AppState, _>( com_atproto_repo_get_record, )) .merge(GetProfileRequest::into_router::<_, AppState, _>( app_bsky_actor_get_profile, )) .with_state(app_state); 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(state): State) -> Response { Json(json!({ "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"], "id": state.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": state.server_config.appview_endpoint }, { "id": "#bsky_appview", "type": "BskyAppView", "serviceEndpoint": state.server_config.appview_endpoint } ] })) .into_response() } pub struct XrpcErrorResponse { error: XrpcError, pub status: StatusCode, } impl XrpcErrorResponse { pub fn internal_server_error() -> Self { Self { error: XrpcError { error: "InternalServerError".to_string(), message: None, }, status: StatusCode::INTERNAL_SERVER_ERROR, } } pub fn auth_not_supplied() -> Self { Self { error: XrpcError { error: "NoAuthSupplied".to_string(), message: Some("Authorization is required".to_string()), }, status: StatusCode::UNAUTHORIZED, } } pub fn not_implemented() -> Self { Self { error: XrpcError { error: "MethodNotImplemented".to_string(), message: Some("Method not yet implemented".to_string()), }, status: StatusCode::NOT_IMPLEMENTED, } } } #[derive(serde::Deserialize, serde::Serialize)] pub struct XrpcError { pub error: String, #[serde(skip_serializing_if = "std::option::Option::is_none")] pub message: Option, } impl IntoResponse for XrpcErrorResponse { fn into_response(self) -> axum::response::Response { (self.status, self.error.error).into_response() } } #[derive(Clone)] pub struct AppState { pub server_config: ServerConfig, pub service_auth: ServiceAuthConfig, pub resolver: JacquardResolver, } impl AppState { pub fn new( server_config: ServerConfig, service_auth: ServiceAuthConfig, resolver: JacquardResolver, ) -> Self { Self { service_auth: service_auth, server_config: server_config, resolver: resolver, } } } impl ServiceAuth for AppState { type Resolver = JacquardResolver; fn service_did(&self) -> &Did<'_> { ServiceAuth::service_did(&self.service_auth) } fn resolver(&self) -> &Self::Resolver { ServiceAuth::resolver(&self.service_auth) } fn require_lxm(&self) -> bool { ServiceAuth::require_lxm(&self.service_auth) } }