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 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(×tamp, &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}