maarcadetweet: initial commit
AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit. - PDS (Rust + axum + sqlx) - Auth: createAccount, createSession, refreshSession - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE) - Feed: feed.like.create, feed.repost.create - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos - Identity: resolveHandle - MST: spec-conformant (at-mst crate, 27 tests) - Repo: signed commits, TID counter (monotonic, 4096 wrap safe) - AppView (Rust + axum + sqlx) - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed) - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration) - Handle-sync worker (did:plc + did:web) - JSONB embed storage + thread columns (migration 0003) - Like/repost counter cache (migration 0004) - Tauri 2 + Svelte 5 Desktop Client - System tray (Show/Compose/Quit menu) - OS notifications (tauri-plugin-notification) - Auto-update (tauri-plugin-updater, placeholder endpoint) - Window-state (tauri-plugin-window-state) - 160-char compose with live counter - Image/Link embed rendering - LocalStorage-persisted like state - Timeline with poll (prepend new posts) - Custom TitleBar (transparent, no decorations) - Orange/IBM Plex Mono maarcade design Tests: 231 Rust + 9 vitest = 240 passed.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
mod appview_push;
|
||||
mod car;
|
||||
mod jwt_issuer;
|
||||
mod keys;
|
||||
mod password;
|
||||
mod routes;
|
||||
mod state;
|
||||
|
||||
use crate::routes::types::DescribeServerResp;
|
||||
use crate::state::AppState;
|
||||
use axum::extract::State;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use serde_json::json;
|
||||
use tracing::{info, warn};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
|
||||
.init();
|
||||
|
||||
let cfg = at_shared::config::AppConfig::from_env()?;
|
||||
let db = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(32)
|
||||
.min_connections(2)
|
||||
.acquire_timeout(std::time::Duration::from_secs(10))
|
||||
.connect(&cfg.database_url_pds)
|
||||
.await?;
|
||||
sqlx::migrate!("../../migrations/pds").run(&db).await?;
|
||||
|
||||
let blob = at_blob::S3BlobStore::new(
|
||||
cfg.s3_endpoint.clone(),
|
||||
cfg.s3_region.clone(),
|
||||
cfg.s3_access_key.clone(),
|
||||
cfg.s3_secret_key.clone(),
|
||||
cfg.s3_bucket_pds.clone(),
|
||||
cfg.pds_public_url.clone(),
|
||||
);
|
||||
|
||||
// Best-effort reachability check for the configured S3 endpoint.
|
||||
// The PDS continues to operate if MinIO is unreachable — `uploadBlob`
|
||||
// falls back to local-only storage and the S3 push is logged at
|
||||
// warn level — but we want this surfaced loudly at startup so
|
||||
// operators notice in dev. See `at_blob::s3` for the
|
||||
// MinIO-only limitation.
|
||||
if !blob.ping().await {
|
||||
warn!(
|
||||
endpoint = %cfg.s3_endpoint,
|
||||
bucket = %cfg.s3_bucket_pds,
|
||||
"s3 ping failed at startup; uploadBlob will serve from local blockstore only"
|
||||
);
|
||||
}
|
||||
|
||||
let state = AppState::new(cfg.clone(), db, blob).await;
|
||||
let app = router(state);
|
||||
|
||||
let addr: std::net::SocketAddr = format!("{}:{}", cfg.pds_host, cfg.pds_port).parse()?;
|
||||
info!("pds-server listening on http://{addr}");
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/healthz", get(healthz))
|
||||
.route(
|
||||
"/xrpc/com.atproto.server.describeServer",
|
||||
get(describe_server),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.server.createAccount",
|
||||
post(routes::auth::create_account),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.server.createSession",
|
||||
post(routes::auth::create_session),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.server.refreshSession",
|
||||
post(routes::auth::refresh_session),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.identity.resolveHandle",
|
||||
post(routes::identity::resolve_handle),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.repo.createRecord",
|
||||
post(routes::repo::create_record),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.repo.deleteRecord",
|
||||
post(routes::feed::delete_record),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.feed.like.create",
|
||||
post(routes::feed::create_like),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.uploadBlob",
|
||||
post(routes::blob::upload_blob)
|
||||
.layer(routes::blob::upload_blob_body_limit())
|
||||
.layer(axum::middleware::from_fn(routes::blob::body_limit_fallback)),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.sync.getRepo",
|
||||
get(routes::sync::get_repo),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.sync.getBlocks",
|
||||
get(routes::sync::get_blocks),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.sync.getLatestCommit",
|
||||
get(routes::sync::get_latest_commit),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.sync.getRecord",
|
||||
get(routes::sync::get_record),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.sync.listRepos",
|
||||
get(routes::sync::list_repos),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/com.atproto.sync.getBlob",
|
||||
get(routes::blob::get_blob),
|
||||
)
|
||||
.route(
|
||||
"/blob/:cid",
|
||||
get(routes::blob::get_blob_by_cid),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn root() -> Json<serde_json::Value> {
|
||||
Json(json!({
|
||||
"name": "maarcadetweet-pds",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn healthz() -> Json<serde_json::Value> {
|
||||
Json(json!({ "ok": true }))
|
||||
}
|
||||
|
||||
async fn describe_server(State(state): State<AppState>) -> Json<DescribeServerResp> {
|
||||
Json(DescribeServerResp {
|
||||
did: "did:web:pds.maarcadetweet.local".into(),
|
||||
available_user_domains: vec![state
|
||||
.cfg
|
||||
.pds_handle_dns_zone
|
||||
.trim_start_matches('.')
|
||||
.to_string()],
|
||||
invite_code_required: false,
|
||||
links: json!({
|
||||
"termsOfService": null,
|
||||
"privacyPolicy": null,
|
||||
}),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user