1#![doc = include_str!("../README.md")]
2mod config;
3mod database;
4mod markdown;
5mod model;
6mod routes;
7
8use crate::database::DataManager;
9use axum::{Extension, Router};
10use config::Config;
11use nanoneo::core::element::Render;
12use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
13use tera::{Tera, Value};
14use tetratto_core::html;
15use tetratto_shared::hash::salt;
16use tokio::sync::RwLock;
17use tower_http::{
18 catch_panic::CatchPanicLayer,
19 trace::{self, TraceLayer},
20};
21use tracing::{Level, info};
22
23pub(crate) type InnerState = (DataManager, Tera, String);
24pub(crate) type State = Arc<RwLock<InnerState>>;
25
26fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
27 Ok(markdown::render_markdown(value.as_str().unwrap())
28 .replace("\\@", "@")
29 .replace("%5C@", "@")
30 .into())
31}
32
33fn remove_script_tags(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
34 Ok(value
35 .as_str()
36 .unwrap()
37 .replace("</script>", "</script>")
38 .into())
39}
40
41#[macro_export]
42macro_rules! create_dir_if_not_exists {
43 ($dir_path:expr) => {
44 if !std::fs::exists(&$dir_path).unwrap() {
45 std::fs::create_dir($dir_path).unwrap();
46 }
47 };
48}
49
50#[tokio::main(flavor = "multi_thread")]
51async fn main() {
52 dotenv::dotenv().ok();
53 tracing_subscriber::fmt()
54 .with_target(false)
55 .compact()
56 .init();
57
58 let port = match var("PORT") {
59 Ok(port) => port.parse::<u16>().expect("port should be a u16"),
60 Err(_) => 9119,
61 };
62
63 let database = DataManager::new(Config::read())
65 .await
66 .expect("failed to connect to database");
67 database.init().await.expect("failed to init database");
68
69 create_dir_if_not_exists!("./templates_build");
71 create_dir_if_not_exists!("./icons");
72
73 for x in glob::glob("./templates_src/**/*").expect("failed to read pattern") {
74 match x {
75 Ok(x) => std::fs::write(
76 x.to_str()
77 .unwrap()
78 .replace("templates_src/", "templates_build/"),
79 html::pull_icons(
80 nanoneo::parse(&std::fs::read_to_string(x).expect("failed to read template"))
81 .render(&mut HashMap::new()),
82 "./icons",
83 )
84 .await,
85 )
86 .expect("failed to write template"),
87 Err(e) => panic!("{e}"),
88 }
89 }
90
91 create_dir_if_not_exists!("./docs");
93
94 let mut tera = match Tera::new(&format!("./templates_build/**/*")) {
96 Ok(t) => t,
97 Err(e) => {
98 println!("{e}");
99 exit(1);
100 }
101 };
102
103 tera.register_filter("markdown", render_markdown);
104 tera.register_filter("remove_script_tags", remove_script_tags);
105
106 let app = Router::new()
108 .merge(routes::routes())
109 .layer(Extension(Arc::new(RwLock::new((database, tera, salt())))))
110 .layer(axum::extract::DefaultBodyLimit::max(
111 var("BODY_LIMIT")
112 .unwrap_or("8388608".to_string())
113 .parse::<usize>()
114 .unwrap(),
115 ))
116 .layer(
117 TraceLayer::new_for_http()
118 .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
119 .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
120 )
121 .layer(CatchPanicLayer::new());
122
123 let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
125 .await
126 .unwrap();
127
128 info!("🐰 fluffle.");
129 info!("listening on http://0.0.0.0:{}", port);
130 axum::serve(
131 listener,
132 app.into_make_service_with_connect_info::<SocketAddr>(),
133 )
134 .await
135 .unwrap();
136}