small_bin/
webapi.rs

1use crate::{config::AppConfig, database::Database, *};
2use anyhow::Result;
3use chrono::{DateTime, Local, Utc};
4use std::{convert::Infallible, sync::Arc};
5use warp::Filter;
6
7
8#[derive(Debug)]
9pub struct WebApi {
10    config: Arc<AppConfig>,
11    database: Arc<Database>,
12}
13
14
15impl WebApi {
16    pub fn new(config: Arc<AppConfig>, database: Arc<Database>) -> Self {
17        WebApi {
18            config,
19            database,
20        }
21    }
22
23
24    pub async fn start(self: Arc<Self>) -> Result<()> {
25        let port = self.config.webapi_port;
26        info!("Launching Small WebApi on http://127.0.0.1:{port}");
27
28        let web_api = self.clone();
29        let routes = warp::path::end()
30            .and(warp::query::<std::collections::HashMap<String, String>>())
31            .and_then(move |params| {
32                let api = web_api.clone();
33                async move { api.handle_request(params).await }
34            })
35            .or(warp::path::param().and_then(move |count: usize| {
36                let api = self.clone();
37                async move { api.handle_count_request(count).await }
38            }));
39
40        warp::serve(routes).run(([127, 0, 0, 1], port)).await;
41        Ok(())
42    }
43
44
45    async fn handle_request(
46        &self,
47        _params: std::collections::HashMap<String, String>,
48    ) -> Result<impl warp::Reply + use<>, Infallible> {
49        let limit = self.config.amount_history_load;
50        debug!("Loading history of {limit} elements (default)");
51
52        match self.database.get_history(Some(limit)) {
53            Ok(history) => Ok(warp::reply::html(self.render_history(&history))),
54            Err(e) => {
55                error!("Error getting history: {e:?}");
56                Ok(warp::reply::html(format!("Error: {e:?}")))
57            }
58        }
59    }
60
61
62    async fn handle_count_request(
63        &self,
64        count: usize,
65    ) -> Result<impl warp::Reply + use<>, Infallible> {
66        info!("Loading history of {count} elements.");
67
68        match self.database.get_history(Some(count)) {
69            Ok(history) => Ok(warp::reply::html(self.render_history(&history))),
70            Err(e) => {
71                error!("Error getting history: {e:?}");
72                Ok(warp::reply::html(format!("Error: {e:?}")))
73            }
74        }
75    }
76
77
78    fn render_history(&self, history: &[database::History]) -> String {
79        let count = history.len();
80        let items: Vec<String> = history
81            .iter()
82            .map(|entry| {
83                let timestamp = DateTime::<Utc>::from_timestamp(entry.timestamp, 0)
84                    .map(|dt_utc| {
85                        // convert Utc to Local, Utc isn't a standard time in Poland
86                        DateTime::<Local>::from(dt_utc)
87                    })
88                    .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
89                    .unwrap_or_else(|| entry.timestamp.to_string());
90
91                let links: Vec<&str> = entry.content.split(' ').collect();
92                let links_html = self.extract_links(&timestamp, &links, &entry.file);
93
94                format!(
95                    "<article id=\"{}\" class=\"text-center\">{}</article>",
96                    entry.uuid, links_html
97                )
98            })
99            .collect();
100
101        format!(
102            r#"<html>
103{}
104<body>
105<pre class="count"><span>small</span> history of: {count}</pre>
106<div>
107{}
108</div>
109<footer>
110<pre class="count">Sync eM ALL - version: {} - © 2015-2025 - Daniel (<a href="https://x.com/dmilith/" target="_blank">@dmilith</a>) Dettlaff</pre>
111</footer>
112</body>
113</html>"#,
114            Self::head(),
115            items.join(" "),
116            env!("CARGO_PKG_VERSION")
117        )
118    }
119
120
121    fn extract_links(&self, timestamp: &str, links: &[&str], file: &str) -> String {
122        links
123            .iter()
124            .filter(|l| !l.is_empty())
125            .map(|link| {
126                if link.ends_with("png")
127                    || link.ends_with("jpg")
128                    || link.ends_with("jpeg")
129                    || link.ends_with("gif")
130                {
131                    format!(
132                        r#"<a href="{link}"><img src="{link}"></img><span class="caption">{timestamp} - {file}</span></a>"#
133                    )
134                } else {
135                    format!(
136                        r#"<a href="{link}"><img src="{IMG_NO_MEDIA}"><span class="caption">{timestamp} - {file}</span></div></a>"#
137                    )
138                }
139            })
140            .collect::<Vec<_>>()
141            .join(" ")
142    }
143
144
145    fn head() -> &'static str {
146        r#"<head>
147  <title>Small dashboard</title>
148  <meta name="viewport" content="width=device-width, initial-scale=1">
149  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
150  <style>
151    article.item { vertical-align: top; display: block; text-align: center; }
152    img { background-color: grey; padding: 0.5em; margin-top: 3em; margin-left: 2em; margin-right: 2em; }
153    .caption { display: block; }
154    .count { display: block; margin: 0.5em; font-weight: bold; text-align: center; background: #CFCFCF }
155    pre.count { margin: 2em; }
156    pre.count span { font-size: 1.6em; }
157    body { background-color: #e1e1e1; }
158    footer { display: block; margin: 1.6em; margin-top: 3.2em; text-align: center; }
159  </style>
160</head>"#
161    }
162}