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