A personal app view to see Bsky posts of your followers (for when their app view goes down)
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

add some endpoints with not implemented errors

Signed-off-by: Will Andrews <did:plc:dadhhalkfcq3gucaq25hjqon>

+290 -30
+1
Cargo.lock
··· 2748 2748 "chrono", 2749 2749 "dotenv", 2750 2750 "jacquard", 2751 + "jacquard-api", 2751 2752 "jacquard-axum", 2752 2753 "jacquard-common", 2753 2754 "jacquard-identity",
+1
Cargo.toml
··· 10 10 chrono = "0.4.44" 11 11 dotenv = "0.15.0" 12 12 jacquard = "0.11.0" 13 + jacquard-api = "0.11.1" 13 14 jacquard-axum = "0.11.0" 14 15 jacquard-common = "0.11.0" 15 16 jacquard-identity = "0.11.0"
+116 -22
src/server.rs
··· 1 1 use axum::{ 2 2 Json, Router, 3 3 extract::State, 4 + http::StatusCode, 4 5 response::{IntoResponse, Response}, 5 6 routing::get, 6 7 }; 7 - 8 - use jacquard_axum::service_auth::ServiceAuthConfig; 8 + use jacquard_axum::{IntoRouter, service_auth::ServiceAuth, service_auth::ServiceAuthConfig}; 9 9 use jacquard_common::types::string::Did; 10 10 use jacquard_identity::JacquardResolver; 11 11 use jacquard_identity::resolver::ResolverOptions; 12 12 use serde_json::json; 13 13 use std::net::SocketAddr; 14 14 15 - use crate::xrpc::routes::app_bsky_feed_get_timeline; 15 + use jacquard_api::{ 16 + app_bsky::feed::{get_feed::GetFeedRequest, get_timeline::GetTimelineRequest}, 17 + com_atproto::repo::get_record::GetRecordRequest, 18 + }; 19 + 20 + use crate::xrpc::routes::{ 21 + app_bsky_feed_get_feed, app_bsky_feed_get_timeline, com_atproto_repo_get_record, 22 + }; 16 23 17 24 #[derive(Clone)] 18 25 pub struct ServerConfig { 19 26 pub appview_did: String, 20 27 pub appview_endpoint: String, 21 - } 22 - 23 - fn good_input<'a>(s: &'a str) -> &'a str { 24 - s 25 28 } 26 29 27 30 pub async fn run_server() { ··· 31 34 .and_then(|s| s.parse().ok()) 32 35 .unwrap_or(3000); 33 36 34 - let appview_did: String = std::env::var("APPVIEW_DID").unwrap(); 35 - let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").unwrap(); 37 + let appview_did: String = std::env::var("APPVIEW_DID").expect("APPVIEW_DID missing"); 38 + let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").expect("APPVIEW_HOSTNAME missing"); 36 39 37 40 let server_config = ServerConfig { 38 41 appview_did: appview_did.clone(), 39 42 appview_endpoint: appview_endpoint.clone(), 40 43 }; 44 + 45 + let service_did = Did::new_owned(appview_did).expect("APPVIEW_DID produced an invalid did:web"); 41 46 42 47 let resolver = JacquardResolver::new(reqwest::Client::new(), ResolverOptions::default()); 43 - let config = 44 - ServiceAuthConfig::new(Did::new_static("did:web:my-appview.com").unwrap(), resolver); 48 + let auth_config = ServiceAuthConfig::new(service_did, resolver); 49 + 50 + let app_state = AppState::new(server_config, auth_config); 45 51 46 - let app = Router::new() 52 + let app = Router::<AppState>::new() 47 53 .route("/", get(say_hello_text)) 48 54 .route("/.well-known/did.json", get(well_known_did_json)) 49 - .with_state(server_config) 50 - .route( 51 - "/xrpc/app.bsky.feed.getTimeline", 52 - get(app_bsky_feed_get_timeline), 53 - ) 54 - .with_state(config); 55 + .merge(GetTimelineRequest::into_router::<_, AppState, _>( 56 + app_bsky_feed_get_timeline, 57 + )) 58 + .merge(GetFeedRequest::into_router::<_, AppState, _>( 59 + app_bsky_feed_get_feed, 60 + )) 61 + .merge(GetRecordRequest::into_router::<_, AppState, _>( 62 + com_atproto_repo_get_record, 63 + )) 64 + .with_state(app_state); 55 65 56 66 let addr: SocketAddr = format!("{host}:{port}") 57 67 .parse() ··· 70 80 return "This is an appview. Work in progress. This is my appview. There are many like it, but this one is mine"; 71 81 } 72 82 73 - async fn well_known_did_json(State(server_config): State<ServerConfig>) -> Response { 83 + async fn well_known_did_json(State(state): State<AppState>) -> Response { 74 84 Json(json!({ 75 85 "@context": [ 76 86 "https://www.w3.org/ns/did/v1", 77 87 "https://w3id.org/security/multikey/v1"], 78 - "id": server_config.appview_did, 88 + "id": state.server_config.appview_did, 79 89 "verificationMethod": [ 80 90 { 81 91 "id": "did:web:api.bsky.app#atproto", ··· 88 98 { 89 99 "id": "#bsky_notif", 90 100 "type": "BskyNotificationService", 91 - "serviceEndpoint": server_config.appview_endpoint 101 + "serviceEndpoint": state.server_config.appview_endpoint 92 102 }, 93 103 { 94 104 "id": "#bsky_appview", 95 105 "type": "BskyAppView", 96 - "serviceEndpoint": server_config.appview_endpoint 106 + "serviceEndpoint": state.server_config.appview_endpoint 97 107 } 98 108 ] 99 109 })) 100 110 .into_response() 101 111 } 112 + 113 + pub struct XrpcErrorResponse { 114 + error: XrpcError, 115 + pub status: StatusCode, 116 + } 117 + 118 + impl XrpcErrorResponse { 119 + pub fn internal_server_error() -> Self { 120 + Self { 121 + error: XrpcError { 122 + error: "InternalServerError".to_string(), 123 + message: None, 124 + }, 125 + status: StatusCode::INTERNAL_SERVER_ERROR, 126 + } 127 + } 128 + 129 + pub fn auth_not_supplied() -> Self { 130 + Self { 131 + error: XrpcError { 132 + error: "NoAuthSupplied".to_string(), 133 + message: Some("Authorization is required".to_string()), 134 + }, 135 + status: StatusCode::UNAUTHORIZED, 136 + } 137 + } 138 + 139 + pub fn not_implemented() -> Self { 140 + Self { 141 + error: XrpcError { 142 + error: "MethodNotImplemented".to_string(), 143 + message: Some("Method not yet implemented".to_string()), 144 + }, 145 + status: StatusCode::NOT_IMPLEMENTED, 146 + } 147 + } 148 + } 149 + 150 + #[derive(serde::Deserialize, serde::Serialize)] 151 + pub struct XrpcError { 152 + pub error: String, 153 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 154 + pub message: Option<String>, 155 + } 156 + 157 + impl IntoResponse for XrpcErrorResponse { 158 + fn into_response(self) -> axum::response::Response { 159 + Json(self.error).into_response() 160 + } 161 + } 162 + 163 + #[derive(Clone)] 164 + pub struct AppState { 165 + server_config: ServerConfig, 166 + pub service_auth: ServiceAuthConfig<JacquardResolver>, 167 + } 168 + 169 + impl AppState { 170 + pub fn new( 171 + server_config: ServerConfig, 172 + service_auth: ServiceAuthConfig<JacquardResolver>, 173 + ) -> Self { 174 + Self { 175 + service_auth: service_auth, 176 + server_config: server_config, 177 + } 178 + } 179 + } 180 + 181 + impl ServiceAuth for AppState { 182 + type Resolver = JacquardResolver; 183 + 184 + fn service_did(&self) -> &Did<'_> { 185 + ServiceAuth::service_did(&self.service_auth) 186 + } 187 + 188 + fn resolver(&self) -> &Self::Resolver { 189 + ServiceAuth::resolver(&self.service_auth) 190 + } 191 + 192 + fn require_lxm(&self) -> bool { 193 + ServiceAuth::require_lxm(&self.service_auth) 194 + } 195 + }
+172 -8
src/xrpc/routes.rs
··· 1 - use axum::{ 2 - Json, 3 - response::{IntoResponse, Response}, 1 + use crate::server::AppState; 2 + use crate::server::XrpcErrorResponse; 3 + use axum::{Json, extract::State}; 4 + use jacquard::types::{ 5 + aturi::AtUri, 6 + cid::Cid, 7 + did::Did, 8 + string::{Datetime, Handle}, 9 + }; 10 + use jacquard::xrpc::atproto::GetRecordOutput; 11 + use jacquard_api::app_bsky::feed::{ 12 + get_feed::{GetFeedOutput, GetFeedRequest}, 13 + get_timeline::GetTimelineRequest, 14 + }; 15 + use jacquard_api::app_bsky::{ 16 + actor::ProfileViewBasic, 17 + feed::{FeedViewPost, PostView, get_timeline::GetTimelineOutput}, 4 18 }; 5 - use jacquard_axum::service_auth::ExtractServiceAuth; 6 19 20 + use jacquard_api::tools_ozone::moderation::get_record::GetRecordRequest; 21 + use jacquard_axum::ExtractXrpc; 22 + use jacquard_axum::service_auth::ExtractOptionalServiceAuth; 23 + use serde_json::Value; 7 24 use serde_json::json; 8 25 9 - pub async fn app_bsky_feed_get_timeline(ExtractServiceAuth(auth): ExtractServiceAuth) -> Response { 10 - let did = auth.did().to_string(); 11 - println!("{did}"); 12 - Json(json!({})).into_response() 26 + pub async fn com_atproto_repo_get_record( 27 + State(_): State<AppState>, 28 + ExtractOptionalServiceAuth(_auth): ExtractOptionalServiceAuth, 29 + ExtractXrpc(_): ExtractXrpc<GetRecordRequest>, 30 + ) -> Result<Json<GetRecordOutput<'static>>, XrpcErrorResponse> { 31 + return Err(XrpcErrorResponse::not_implemented()); 32 + } 33 + 34 + pub async fn app_bsky_feed_get_feed( 35 + State(_): State<AppState>, 36 + ExtractOptionalServiceAuth(_auth): ExtractOptionalServiceAuth, 37 + ExtractXrpc(_req): ExtractXrpc<GetFeedRequest>, 38 + ) -> Result<Json<GetFeedOutput<'static>>, XrpcErrorResponse> { 39 + return Err(XrpcErrorResponse::not_implemented()); 40 + // let record = jacquard::Data::from_json_owned(get_record()).map_err(|err| { 41 + // println!("parse record data: {err}"); 42 + // XrpcErrorResponse::internal_server_error() 43 + // })?; 44 + 45 + // let feed_item = FeedViewPost { 46 + // feed_context: None, 47 + // reason: None, 48 + // reply: None, 49 + // req_id: None, 50 + // extra_data: None, 51 + // post: PostView { 52 + // author: ProfileViewBasic { 53 + // associated: None, 54 + // avatar: None, 55 + // created_at: None, 56 + // debug: None, 57 + // display_name: None, 58 + // extra_data: None, 59 + // labels: None, 60 + // pronouns: None, 61 + // status: None, 62 + // verification: None, 63 + // viewer: None, 64 + // did: Did::raw("did"), 65 + // handle: Handle::raw("handle"), 66 + // }, 67 + // bookmark_count: None, 68 + // cid: Cid::str("cid"), 69 + // debug: None, 70 + // embed: None, 71 + // indexed_at: Datetime::now(), 72 + // labels: None, 73 + // like_count: Some(1), 74 + // quote_count: Some(1), 75 + // record: record, 76 + // reply_count: Some(1), 77 + // repost_count: Some(1), 78 + // threadgate: None, 79 + // uri: AtUri::raw("uri"), 80 + // viewer: None, 81 + // extra_data: None, 82 + // }, 83 + // }; 84 + 85 + // let feed: Vec<FeedViewPost> = vec![feed_item]; 86 + 87 + // let res = GetFeedOutput { 88 + // feed: feed, 89 + // cursor: None, 90 + // extra_data: None, 91 + // }; 92 + 93 + // Ok(Json(res)) 94 + } 95 + 96 + pub async fn app_bsky_feed_get_timeline( 97 + State(_): State<AppState>, 98 + ExtractOptionalServiceAuth(_auth): ExtractOptionalServiceAuth, 99 + ExtractXrpc(_req): ExtractXrpc<GetTimelineRequest>, 100 + ) -> Result<Json<GetTimelineOutput<'static>>, XrpcErrorResponse> { 101 + return Err(XrpcErrorResponse::not_implemented()); 102 + // let record = jacquard::Data::from_json_owned(get_record()).map_err(|err| { 103 + // println!("parse record data: {err}"); 104 + // XrpcErrorResponse::internal_server_error() 105 + // })?; 106 + 107 + // let feed_item = FeedViewPost { 108 + // feed_context: None, 109 + // reason: None, 110 + // reply: None, 111 + // req_id: None, 112 + // extra_data: None, 113 + // post: PostView { 114 + // author: ProfileViewBasic { 115 + // associated: None, 116 + // avatar: None, 117 + // created_at: None, 118 + // debug: None, 119 + // display_name: None, 120 + // extra_data: None, 121 + // labels: None, 122 + // pronouns: None, 123 + // status: None, 124 + // verification: None, 125 + // viewer: None, 126 + // did: Did::raw("did"), 127 + // handle: Handle::raw("handle"), 128 + // }, 129 + // bookmark_count: None, 130 + // cid: Cid::str("cid"), 131 + // debug: None, 132 + // embed: None, 133 + // indexed_at: Datetime::now(), 134 + // labels: None, 135 + // like_count: Some(1), 136 + // quote_count: Some(1), 137 + // record: record, 138 + // reply_count: Some(1), 139 + // repost_count: Some(1), 140 + // threadgate: None, 141 + // uri: AtUri::raw("uri"), 142 + // viewer: None, 143 + // extra_data: None, 144 + // }, 145 + // }; 146 + 147 + // let feed: Vec<FeedViewPost> = vec![feed_item]; 148 + 149 + // let res = GetTimelineOutput { 150 + // feed: feed, 151 + // cursor: None, 152 + // extra_data: None, 153 + // }; 154 + 155 + // Ok(Json(res)) 156 + } 157 + 158 + fn get_record() -> Value { 159 + json!({ 160 + "text": "This, Night Ripper and All Day get a spin from me at least once a month! 🎧", 161 + "$type": "app.bsky.feed.post", 162 + "langs": [ 163 + "en" 164 + ], 165 + "reply": { 166 + "root": { 167 + "cid": "bafyreidihkhhijzqnofhilusshqbye47qqq6opjrhgafwjvj2e5gmmhzmm", 168 + "uri": "at://did:plc:stznz7qsokto2345qtdzogjb/app.bsky.feed.post/3mkaodfwuxk2m" 169 + }, 170 + "parent": { 171 + "cid": "bafyreidihkhhijzqnofhilusshqbye47qqq6opjrhgafwjvj2e5gmmhzmm", 172 + "uri": "at://did:plc:stznz7qsokto2345qtdzogjb/app.bsky.feed.post/3mkaodfwuxk2m" 173 + } 174 + }, 175 + "createdAt": "2026-04-24T13:54:50.849Z" 176 + }) 13 177 }