1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//! This is a binary crate which hosts an HTTP server which implements a part of
//! the ActivityPub protocol and serves a small Javascript-based ActivityPub
//! client for testing.
//!
//! CAUTION: This crate is not ready for use on the public internet in any
//! capacity: there's no authorization or authentication implemented, and no
//! anti-spam measures.

#![warn(missing_docs)]

use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer};
use tokio::task;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{filter, fmt, EnvFilter};

mod activitypub;
mod args;
mod database;
mod frontend;
mod http_api;
mod remote;
mod snowflake;

use database::Database;
use http_api::CanonicalServerUrl;
use remote::RemoteAccessor;

/// Entry point for the program. Reads cli and env [args], sets up logging,
/// initializes the [Database], and starts up the Actix HTTP server and inbox
/// delivery loop ([activitypub::inbox_queue]). The HTTP server serves requests
/// using the responders in [http_api] (most of the API) and [frontend] (see the
/// files in the "frontend" folder at the root of the repository for the
/// specific files served).
#[tokio::main]
pub async fn main() {
    let args: args::Args = argh::from_env();

    let log_filter = EnvFilter::try_from_default_env()
        .or_else(|_| EnvFilter::try_new("info"))
        .unwrap();
    let event_format = fmt::format().without_time().compact();
    let fmt_layer = fmt::layer().event_format(event_format);
    let sqlx_filter = filter::filter_fn(|metadata| {
        metadata.module_path() != Some("sqlx::query") || metadata.level() <= &LevelFilter::WARN
    });
    tracing_subscriber::registry()
        .with(fmt_layer)
        .with(log_filter)
        .with(sqlx_filter)
        .init();

    tracing::debug!("Starting up notedealer...");

    /// Put ut the thing on the heap and return a static borrow. Used for global
    /// state intitialized in the main function instead of Lazy or similar ways.
    fn make_static<T>(t: T) -> &'static T {
        Box::leak(Box::new(t))
    }

    let db = match Database::new(&args.database_url).await {
        Ok(db) => make_static(db),
        Err(err) => {
            tracing::error!("Cannot open db: {err}");
            std::process::exit(1);
        }
    };

    let addr = &args.bind_addr;
    let canonical_server_url = CanonicalServerUrl(args.activitypub_url);
    let factory = move || {
        let canonical_server_url = canonical_server_url.clone();
        App::new()
            .app_data(web::Data::new(db))
            .app_data(web::Data::new(RemoteAccessor::default()))
            .app_data(web::Data::new(canonical_server_url))
            .route("/", web::get().to(frontend::html!("index.html")))
            .route("/favicon.png", web::get().to(frontend::png!("favicon.png")))
            .service(http_api::users::service())
            .service(http_api::webfinger::get)
            .wrap(Logger::default())
    };
    let Ok(server) = HttpServer::new(factory).bind(addr) else {
        tracing::error!("Cannot bind HTTP server to {addr}, is it already bound?");
        std::process::exit(1);
    };

    let delivery_loop_task_set = task::LocalSet::new();
    let delivery_loop = delivery_loop_task_set.run_until(async {
        activitypub::inbox_queue::delivery_loop(db).await;
    });

    let server_name = env!("CARGO_PKG_NAME");
    tracing::info!("{server_name} HTTP server bound to: http://{addr}/");
    if let (Err(err), _) = tokio::join!(server.run(), delivery_loop) {
        tracing::error!("Fatal error in {server_name}: {err}");
        std::process::exit(1);
    }
}