// This file is part of Locus . // Copyright (C) 2022-2022 Fuwn // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // Copyright (C) 2022-2022 Fuwn // SPDX-License-Identifier: GPL-3.0-only #![feature(once_cell)] #![deny( warnings, nonstandard_style, unused, future_incompatible, rust_2018_idioms, unsafe_code )] #![deny(clippy::all, clippy::nursery, clippy::pedantic)] #![recursion_limit = "128"] #![allow(clippy::cast_precision_loss)] mod constants; mod macros; mod modules; #[macro_use] extern crate log; use std::{collections::HashMap, lazy::SyncLazy, sync::Mutex}; use constants::QUOTES; use pickledb::PickleDb; use rand::seq::SliceRandom; use tokio::time::Instant; use windmark::{Response, Router}; use yarte::Template; static DATABASE: SyncLazy> = SyncLazy::new(|| { Mutex::new({ if std::fs::File::open(".locus/locus.db").is_ok() { PickleDb::load_json( ".locus/locus.db", pickledb::PickleDbDumpPolicy::AutoDump, ) .unwrap() } else { PickleDb::new_json( ".locus/locus.db", pickledb::PickleDbDumpPolicy::AutoDump, ) } }) }); static ROUTES: SyncLazy>> = SyncLazy::new(|| Mutex::new(HashMap::new())); #[derive(Template)] #[template(path = "main")] struct Main<'a> { body: &'a str, hits: &'a i32, quote: &'a str, commit: &'a str, mini_commit: &'a str, } fn hits_from_route(route: &str) -> i32 { if let Ok(database) = DATABASE.lock() { (*database) .get::(if route.is_empty() { "/" } else { route }) .unwrap() } else { 0 } } fn track_mount( router: &mut Router, route: &str, description: &str, handler: windmark::handler::RouteResponse, ) { (*ROUTES.lock().unwrap()).insert(route.to_string(), description.to_string()); router.mount(route, handler); } #[windmark::main] async fn main() -> Result<(), Box> { std::env::set_var("RUST_LOG", "windmark,locus=trace"); pretty_env_logger::init(); let mut time_mount = Instant::now(); let mut router = Router::new(); let uptime = Instant::now(); router.set_private_key_file(".locus/locus_private.pem"); router.set_certificate_file(".locus/locus_public.pem"); router.set_pre_route_callback(Box::new(|stream, url, _| { info!( "accepted connection from {} to {}", stream.peer_addr().unwrap().ip(), url.to_string(), ); let url_path = if url.path().is_empty() { "/" } else { url.path() }; let previous_database = (*DATABASE.lock().unwrap()).get::(url_path); match previous_database { None => { (*DATABASE.lock().unwrap()) .set::(url_path, &0) .unwrap(); } Some(_) => {} } let new_database = (*DATABASE.lock().unwrap()).get::(url_path); (*DATABASE.lock().unwrap()) .set(url_path, &(new_database.unwrap() + 1)) .unwrap(); })); router.set_error_handler(Box::new(|_| { Response::NotFound( "The requested resource could not be found at this time. You can try \ refreshing the page, if that doesn't change anything; contact Fuwn! \ (contact@fuwn.me)" .into(), ) })); router.set_fix_path(true); track_mount( &mut router, "/uptime", "The uptime of Locus, in seconds (A.K.A., The Locus Epoch)", Box::new(move |context| { success!( &format!("# UPTIME\n\n{} seconds", uptime.elapsed().as_secs()), context ) }), ); info!( "preliminary mounts took {}ms", time_mount.elapsed().as_nanos() as f64 / 1_000_000.0 ); time_mount = Instant::now(); router.attach_stateless(modules::multi_blog); info!( "blog mounts took {}ms", time_mount.elapsed().as_nanos() as f64 / 1_000_000.0 ); time_mount = Instant::now(); mount_file!( router, "/robots.txt", "Crawler traffic manager, for robots, not humans", "robots.txt" ); mount_file!( router, "/favicon.txt", "This Gemini capsule's icon", "favicon.txt" ); mount_page!(router, "/", "This Gemini capsule's homepage", "index"); mount_page!(router, "/contact", "Many ways to contact Fuwn", "contact"); mount_page!(router, "/donate", "Many ways to donate to Fuwn", "donate"); mount_page!( router, "/gemini", "Information and resources for the Gemini protocol", "gemini" ); mount_page!( router, "/gopher", "Information and resources for the Gopher protocol", "gopher" ); mount_page!(router, "/interests", "A few interests of Fuwn", "interests"); mount_page!(router, "/skills", "A few skills of Fuwn", "skills"); info!( "static mounts took {}ms", time_mount.elapsed().as_nanos() as f64 / 1_000_000.0 ); track_mount( &mut router, "/sitemap", "A map of all publicly available routes on this Gemini capsule", Box::new(|context| { success!( format!( "# SITEMAP\n\n{}", (*ROUTES.lock().unwrap()) .iter() .map(|(r, d)| format!("=> {} {}", r, d)) .collect::>() .join("\n") ), context ) }), ); router.run().await }