Something went wrong. Try again.
A personal app view to see Bsky posts of your followers (for when their app view goes down)
Something went wrong. Try again.
6.2 kB · 209 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210use 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::<AppState>::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<AppState>) -> 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<String>,}
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<JacquardResolver>, pub resolver: JacquardResolver,}
impl AppState { pub fn new( server_config: ServerConfig, service_auth: ServiceAuthConfig<JacquardResolver>, 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) }}