···1+-- This file was automatically created by Diesel to setup helper functions
2+-- and other internal bookkeeping. This file is safe to edit, any future
3+-- changes will be added to existing projects as new migrations.
4+5+DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
6+DROP FUNCTION IF EXISTS diesel_set_updated_at();
···1+-- This file was automatically created by Diesel to setup helper functions
2+-- and other internal bookkeeping. This file is safe to edit, any future
3+-- changes will be added to existing projects as new migrations.
4+5+6+7+8+-- Sets up a trigger for the given table to automatically set a column called
9+-- `updated_at` whenever the row is modified (unless `updated_at` was included
10+-- in the modified columns)
11+--
12+-- # Example
13+--
14+-- ```sql
15+-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
16+--
17+-- SELECT diesel_manage_updated_at('users');
18+-- ```
19+CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
20+BEGIN
21+ EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
22+ FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
23+END;
24+$$ LANGUAGE plpgsql;
25+26+CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
27+BEGIN
28+ IF (
29+ NEW IS DISTINCT FROM OLD AND
30+ NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
31+ ) THEN
32+ NEW.updated_at := current_timestamp;
33+ END IF;
34+ RETURN NEW;
35+END;
36+$$ LANGUAGE plpgsql;
···1+-- This file should undo anything in `up.sql`
2+drop table if exists profile_pronouns;
3+4+drop table if exists profile_links;
5+6+drop table if exists profile;
7+8+drop table if exists emails;
9+10+drop table if exists _jetstream;
11+12+drop table if exists follows;
13+14+drop table if exists public_keys;
15+16+drop table if exists registrations;
17+18+drop table if exists oauth_sessions;
19+20+drop table if exists oauth_requests;
···1+create table if not exists registrations (
2+ id serial primary key,
3+ domain text not null unique,
4+ did text not null,
5+ secret text not null,
6+ created timestamp
7+ with
8+ time zone not null default (now () at time zone 'utc'),
9+ registered text
10+);
11+12+create table if not exists public_keys (
13+ id serial primary key,
14+ did text not null,
15+ name text not null,
16+ key_contents text not null,
17+ rkey text not null,
18+ created timestamp
19+ with
20+ time zone not null default (now () at time zone 'utc'),
21+ unique (did, name, key_contents)
22+);
23+24+create table if not exists follows (
25+ user_did text not null,
26+ subject_did text not null,
27+ rkey text not null,
28+ followed_at timestamp
29+ with
30+ time zone not null default (now () at time zone 'utc'),
31+ primary key (user_did, subject_did),
32+ check (user_did <> subject_did)
33+);
34+35+create table if not exists _jetstream (
36+ id serial primary key,
37+ last_time_us integer not null
38+);
39+40+create table if not exists emails (
41+ id serial primary key,
42+ did text not null,
43+ email text not null,
44+ verified integer not null default 0,
45+ verification_code text not null,
46+ last_sent timestamp
47+ with
48+ time zone not null default (now () at time zone 'utc'),
49+ is_primary integer not null default 0,
50+ created timestamp
51+ with
52+ time zone not null default (now () at time zone 'utc'),
53+ unique (did, email)
54+);
55+56+create table if not exists profile (
57+ -- id
58+ id serial primary key,
59+ did text not null,
60+ -- data
61+ avatar text,
62+ description text not null,
63+ include_bluesky boolean not null default false,
64+ include_tangled boolean not null default false,
65+ location text,
66+ pinned_post jsonb,
67+ created_at timestamp
68+ with
69+ time zone default (now () at time zone 'utc'),
70+ -- constraints
71+ unique (did)
72+);
73+74+create table if not exists profile_links (
75+ -- id
76+ id serial primary key,
77+ did text not null,
78+ -- data
79+ link text not null,
80+ -- constraints
81+ foreign key (did) references profile (did) on delete cascade
82+);
83+84+create table if not exists profile_pronouns (
85+ -- id
86+ id serial primary key,
87+ did text not null,
88+ -- data
89+ pronoun text not null,
90+ -- constraints
91+ foreign key (did) references profile (did) on delete cascade
92+);
93+94+create table if not exists oauth_requests (
95+ id serial primary key,
96+ auth_server_iss text not null,
97+ state text,
98+ did text not null,
99+ pkce_verifier text not null,
100+ dpop_key jsonb not null
101+);
102+103+create table if not exists oauth_sessions (
104+ id serial primary key,
105+ did text not null,
106+ pds_url text not null,
107+ session jsonb not null,
108+ expiry text
109+);
···1+use diesel::prelude::*;
2+use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
3+pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
4+use diesel_async::RunQueryDsl;
5+use diesel_async::pooled_connection::AsyncDieselConnectionManager;
6+use diesel_async::pooled_connection::deadpool::Pool;
7+8+#[derive(Clone)]
9+pub struct Db {
10+ pub pool: Pool<diesel_async::AsyncPgConnection>,
11+}
12+13+impl Db {
14+ /// Yes, this fuction can and WILL panic if it can't create the connection pool
15+ /// for some reason. We just want to bail because the appview
16+ /// does not work without a database.
17+ pub async fn new(db_path: Option<String>) -> Self {
18+ let database_url = if let Some(db_path) = db_path {
19+ db_path
20+ } else {
21+ std::env::var("DATABASE_URL").expect("DATABASE_URL must be set")
22+ };
23+ let config =
24+ AsyncDieselConnectionManager::<diesel_async::AsyncPgConnection>::new(database_url);
25+ let pool = Pool::builder(config)
26+ .build()
27+ .expect("Failed to create pool");
28+ Self { pool }
29+ }
30+}
31+32+pub fn run_migrations(
33+ db_path: Option<String>,
34+) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
35+ let database_url = if let Some(db_path) = db_path {
36+ db_path
37+ } else {
38+ std::env::var("DATABASE_URL").expect("DATABASE_URL must be set")
39+ };
40+ let mut connection = PgConnection::establish(&database_url)
41+ .unwrap_or_else(|_| panic!("Error connecting to {}", database_url));
42+ // This will run the necessary migrations.
43+ //
44+ // See the documentation for `MigrationHarness` for
45+ // all available methods.
46+ println!("Attempting migrations...");
47+ let result = connection.run_pending_migrations(MIGRATIONS);
48+ println!("{:?}", result);
49+ if result.is_err() {
50+ println!("Failed to run migrations");
51+ return result.map(|_| ());
52+ }
53+ println!("Migrations Applied:");
54+ let applied_migrations = connection.applied_migrations()?;
55+ for migration in applied_migrations {
56+ println!(" * {}", migration);
57+ }
58+ Ok(())
59+}
···1+pub mod api_error;
2+3+pub mod config;
4+pub mod db;
5+pub mod middleware;
6+pub mod models;
7+pub mod oauth;
8+pub mod routes;
9+pub mod schema;
10+pub mod state;
11+pub mod telemetry;
12+13+use axum::Router;
14+use clap::Parser;
15+use config::*;
16+use db::*;
17+use diesel::prelude::*;
18+use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
19+use dotenvy::dotenv;
20+use miette::IntoDiagnostic;
21+use miette::miette;
22+use state::*;
23+use std::env;
24+25+use tokio::net::TcpListener;
26+use tracing::{debug, error, info};
27+28+#[derive(Parser)]
29+#[command(author, version, about, long_about = None)]
30+struct Cli {
31+ #[arg(
32+ short,
33+ long,
34+ value_name = "FILE",
35+ default_value = "appview-config.toml"
36+ )]
37+ config: String,
38+}
3940#[tokio::main]
41+async fn main() -> miette::Result<()> {
42+ let config = initialize()?;
43+ // Run any migrations before we do anything else.
44+ let db_path = config.core.db_path.clone();
45+ let _ = tokio::task::spawn_blocking(|| db::run_migrations(Some(db_path)))
46+ .await
47+ .into_diagnostic()?;
48+ let db = Db::new(Some(config.core.db_path.clone())).await;
49+ debug!("Connected to database");
50+ // Spin up our server.
51+ info!("Starting server on {}", config.core.listen_addr);
52+ let listener = TcpListener::bind(&config.core.listen_addr)
53+ .await
54+ .expect("Failed to bind address");
55+ let router = router(config, db);
56+ axum::serve(listener, router)
57+ .await
58+ .expect("Failed to start server");
59+ Ok(())
60+}
61+62+pub fn router(cfg: Config, db: Db) -> Router {
63+ let app_state = AppState::new(cfg, db);
64+65+ // Middleware that adds high level tracing to a Service.
66+ // Trace comes with good defaults but also supports customizing many aspects of the output:
67+ // https://docs.rs/tower-http/latest/tower_http/trace/index.html
68+ let trace_layer = telemetry::trace_layer();
69+70+ // Sets 'x-request-id' header with randomly generated uuid v7.
71+ let request_id_layer = middleware::request_id_layer();
72+73+ // Propagates 'x-request-id' header from the request to the response.
74+ let propagate_request_id_layer = middleware::propagate_request_id_layer();
75+76+ // Layer that applies the Cors middleware which adds headers for CORS.
77+ let cors_layer = middleware::cors_layer();
78+79+ // Layer that applies the Timeout middleware, which sets a timeout for requests.
80+ // The default value is 15 seconds.
81+ let timeout_layer = middleware::timeout_layer();
82+83+ // Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
84+ // will be changed to `/foo` before reaching the internal service.
85+ let normalize_path_layer = middleware::normalize_path_layer();
86+87+ // Create the router with the routes.
88+ let router = routes::router();
89+90+ // Combine all the routes and apply the middleware layers.
91+ // The order of the layers is important. The first layer is the outermost layer.
92+ Router::new()
93+ .merge(router)
94+ .layer(normalize_path_layer)
95+ .layer(cors_layer)
96+ .layer(timeout_layer)
97+ .layer(propagate_request_id_layer)
98+ .layer(trace_layer)
99+ .layer(request_id_layer)
100+ .with_state(app_state)
101+}
102+103+pub fn initialize() -> miette::Result<Config> {
104+ miette::set_hook(Box::new(|_| {
105+ Box::new(
106+ miette::MietteHandlerOpts::new()
107+ .terminal_links(true)
108+ //.rgb_colors(miette::RgbColors::)
109+ .with_cause_chain()
110+ .with_syntax_highlighting(miette::highlighters::SyntectHighlighter::default())
111+ .color(true)
112+ .context_lines(5)
113+ .tab_width(2)
114+ .break_words(true)
115+ .build(),
116+ )
117+ }))
118+ .map_err(|e| miette!("Failed to set miette hook: {}", e))?;
119+ miette::set_panic_hook();
120+ dotenv().ok();
121+ let cli = Cli::parse();
122+ let config = config::Config::load(&cli.config);
123+ let config = if let Err(e) = config {
124+ error!("{}", e);
125+ config::Config::load(
126+ &env::var("APPVIEW_CONFIG").expect("Either set APPVIEW_CONFIG to the path to your config file, pass --config FILE to specify the path, or create a file called appview-config.toml in the directory where you are running the binary from."),
127+ )
128+ .map_err(|e| miette!(e))
129+ } else {
130+ config
131+ }?;
132+ let log_dir = env::var("LOG_DIR").unwrap_or_else(|_| "/tmp/appview".to_string());
133+ std::fs::create_dir_all(&log_dir).unwrap();
134+ let _guard = telemetry::setup_tracing(&log_dir);
135+ Ok(config)
136+}
···1+use std::time::Duration;
2+3+use axum::http::HeaderName;
4+use hyper::Request;
5+use tower_http::{
6+ cors::{AllowHeaders, Any, CorsLayer},
7+ normalize_path::NormalizePathLayer,
8+ request_id::{MakeRequestId, PropagateRequestIdLayer, RequestId, SetRequestIdLayer},
9+ timeout::TimeoutLayer,
10+};
11+12+#[derive(Clone, Default)]
13+pub struct Id;
14+15+impl MakeRequestId for Id {
16+ fn make_request_id<B>(&mut self, _: &Request<B>) -> Option<RequestId> {
17+ let id = uuid::Uuid::now_v7().to_string().parse().unwrap();
18+ Some(RequestId::new(id))
19+ }
20+}
21+22+/// Sets the 'x-request-id' header with a randomly generated UUID v7.
23+///
24+/// SetRequestId will not override request IDs if they are already present
25+/// on requests or responses.
26+pub fn request_id_layer() -> SetRequestIdLayer<Id> {
27+ let x_request_id = HeaderName::from_static("x-request-id");
28+ SetRequestIdLayer::new(x_request_id.clone(), Id)
29+}
30+31+// Propagates 'x-request-id' header from the request to the response.
32+///
33+/// PropagateRequestId wont override request ids if its already
34+/// present on requests or responses.
35+pub fn propagate_request_id_layer() -> PropagateRequestIdLayer {
36+ let x_request_id = HeaderName::from_static("x-request-id");
37+ PropagateRequestIdLayer::new(x_request_id)
38+}
39+40+/// Layer that applies the Cors middleware which adds headers for CORS.
41+pub fn cors_layer() -> CorsLayer {
42+ CorsLayer::new()
43+ .allow_origin(Any)
44+ .allow_methods(Any)
45+ .allow_headers(AllowHeaders::mirror_request())
46+ .max_age(Duration::from_secs(600))
47+}
48+49+/// Layer that applies the Timeout middleware which apply a timeout to requests.
50+/// The default timeout value is set to 15 seconds.
51+pub fn timeout_layer() -> TimeoutLayer {
52+ TimeoutLayer::new(Duration::from_secs(15))
53+}
54+55+/// Middleware that normalizes paths.
56+///
57+/// Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
58+/// will be changed to `/foo` before reaching the inner service.
59+pub fn normalize_path_layer() -> NormalizePathLayer {
60+ NormalizePathLayer::trim_trailing_slash()
61+}
···1+use tower_http::{
2+ classify::{ServerErrorsAsFailures, SharedClassifier},
3+ trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer},
4+};
5+use tracing::Level;
6+use tracing_appender::{self, non_blocking, non_blocking::WorkerGuard, rolling::daily};
7+use tracing_subscriber::{
8+ EnvFilter,
9+ fmt::{self, layer, writer::MakeWriterExt},
10+ layer::SubscriberExt,
11+ registry,
12+ util::SubscriberInitExt,
13+};
14+/// The `EnvFilter` type is used to filter log events based on the value of an environment variable.
15+/// In this case, we are using the `try_from_default_env` method to attempt to read the `RUST_LOG` environment variable,
16+/// which is used to set the log level for the application.
17+/// If the environment variable is not set, we default to the log level of `debug`.
18+/// The `RUST_LOG` environment variable is set in the Dockerfile and .env files.
19+pub fn setup_tracing<S: AsRef<str>>(logdir: S) -> WorkerGuard {
20+ let (non_blocking_appender, guard) = non_blocking(daily(logdir.as_ref(), "general.log"));
21+ let env_filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
22+ format!(
23+ "debug,{}=debug,tower_http=debug,axum=debug,hyper=debug,axum::rejection=trace,markdown=info",
24+ env!("CARGO_PKG_NAME"),
25+ ).into()
26+ });
27+ let formatting_layer = fmt::layer().json();
28+ tracing_subscriber::registry()
29+ .with(env_filter_layer)
30+ .with(formatting_layer)
31+ .with(
32+ layer()
33+ .with_writer(std::io::stdout.with_max_level(Level::DEBUG))
34+ .event_format(tracing_subscriber::fmt::format().pretty()),
35+ )
36+ .with(layer().with_writer(non_blocking_appender.with_max_level(Level::INFO)))
37+ .init();
38+ guard
39+}
40+41+/// Returns a `TraceLayer` for HTTP requests and responses.
42+/// The `TraceLayer` is used to trace requests and responses in the application.
43+pub fn trace_layer() -> TraceLayer<SharedClassifier<ServerErrorsAsFailures>> {
44+ TraceLayer::new_for_http()
45+ .make_span_with(DefaultMakeSpan::new().level(Level::INFO))
46+ .on_request(DefaultOnRequest::new().level(Level::INFO))
47+ .on_response(DefaultOnResponse::new().level(Level::INFO))
48+}
+1
crates/weaver-cli/Cargo.toml
···2627# temp for testing
28tokio = { version = "1.45.0", features = ["full"] }
0
···2627# temp for testing
28tokio = { version = "1.45.0", features = ["full"] }
29+rouille = { version = "3.6.2", features = ["rustls"] }
···1pub mod client;
02pub mod config;
3pub mod error;
4pub mod lexicons;
···89pub use crate::error::{Error, IoError, ParseError, SerDeError};
1011-/// Canonical Cow for us, thanks Amos
12pub use merde::CowStr;
13-14/// too many cows, so we have conversions
15pub fn mcow_to_cow(cow: CowStr<'_>) -> std::borrow::Cow<'_, str> {
16 match cow {
···1pub mod client;
2+pub mod compat;
3pub mod config;
4pub mod error;
5pub mod lexicons;
···910pub use crate::error::{Error, IoError, ParseError, SerDeError};
11012pub use merde::CowStr;
013/// too many cows, so we have conversions
14pub fn mcow_to_cow(cow: CowStr<'_>) -> std::borrow::Cow<'_, str> {
15 match cow {