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:
tomdebone
2026-07-05 20:01:31 +02:00
commit c586fd39c9
134 changed files with 35279 additions and 0 deletions
+164
View File
@@ -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,
}),
})
}