small_bin/
webapi.rs

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