1use super::{WebResult, error_resp};
2use crate::sysmod::camera::{PicEntry, TakePicOption, create_thumbnail, take_a_pic};
3use crate::sysmod::twitter::LIMIT_PHOTO_COUNT;
4use crate::sysmod::{camera::resize, http::error_resp_msg, twitter::LIMIT_PHOTO_SIZE};
5use crate::taskserver::Control;
6use actix_web::{HttpResponse, Responder, http::header::ContentType, web};
7use anyhow::{Result, anyhow, bail};
8use log::error;
9use serde::Deserialize;
10use serenity::http::StatusCode;
11use std::{cmp, collections::BTreeMap};
12use tokio::{fs::File, io::AsyncReadExt};
13
14#[actix_web::get("/camera/")]
16async fn index_get() -> impl Responder {
17 let body = r#"<!DOCTYPE html>
18<html lang="en">
19 <head>
20 <title>(Privileged) Camera</title>
21 </head>
22 <body>
23 <h1>(Privileged) Camera</h1>
24 <form action="./take" method="post">
25 <input type="submit" value="Take a picture!">
26 </form>
27 <p><a href="./history">Picture List</a></p>
28 <p><a href="./archive">Archive</a></p>
29 <h2>Navigation</h2>
30 <p><a href="../">Main Page</a></p>
31 </body>
32</html>
33"#;
34
35 HttpResponse::Ok()
36 .content_type(ContentType::html())
37 .body(body)
38}
39
40#[derive(Deserialize)]
41struct HistArGetQuery {
42 #[serde(default)]
43 page: usize,
44}
45
46#[actix_web::get("/camera/history")]
48async fn history_get(ctrl: web::Data<Control>, query: web::Query<HistArGetQuery>) -> HttpResponse {
49 let html = {
50 let camera = ctrl.sysmods().camera.lock().await;
51 let page_by = camera.config.page_by as usize;
52 let (hist, _) = camera.pic_list();
53
54 create_pic_list_page(
55 hist,
56 "history",
57 query.page,
58 page_by,
59 "(Privileged) Picture History",
60 &[("archive", "Archive"), ("delete", "Delete")],
61 )
62 };
63
64 HttpResponse::Ok()
65 .content_type(ContentType::html())
66 .body(html)
67}
68
69#[actix_web::get("/camera/archive")]
71async fn archive_get(ctrl: web::Data<Control>, query: web::Query<HistArGetQuery>) -> HttpResponse {
72 let html = {
73 let camera = ctrl.sysmods().camera.lock().await;
74 let page_by = camera.config.page_by as usize;
75 let (_, archive) = camera.pic_list();
76
77 create_pic_list_page(
78 archive,
79 "archive",
80 query.page,
81 page_by,
82 "(Privileged) Picture Archive",
83 &[
84 ("twitter", "Post on Twitter (Max 4 pics or less)"),
85 ("delete", "Delete"),
86 ],
87 )
88 };
89
90 HttpResponse::Ok()
91 .content_type(ContentType::html())
92 .body(html)
93}
94
95fn create_pic_list_page(
104 pic_list: &BTreeMap<String, PicEntry>,
105 img_path_dir: &str,
106 start: usize,
107 page_by: usize,
108 title: &str,
109 commands: &[(&str, &str)],
110) -> String {
111 let total = pic_list.len();
112 let data: Vec<_> = pic_list.keys().rev().skip(start).take(page_by).collect();
113
114 let mut fig_list = String::new();
115 fig_list += r#" <form method="post">
116 <fieldset>
117 <legend>Commands for selected items</legend>
118 <input type="submit" value="Execute">
119"#;
120 let mut is_first = true;
121 for &(cmd, label) in commands {
122 let checked = if is_first {
123 is_first = false;
124 r#" checked="checked""#
125 } else {
126 ""
127 };
128 fig_list += &format!(
129 r#" <label><input type="radio" name="cmd" value="{cmd}"{checked}>{label}</label>
130"#
131 );
132 }
133 fig_list += r#" </fieldset>
134 <p><input type="reset" value="Reset"></p>
135
136"#;
137
138 for name in data {
139 fig_list += &format!(
140 r#" <input type="checkbox" id="{name}" name="target" value="{name}">
141 <figure class="pic">
142 <a href="./pic/{img_path_dir}/{name}/main"><img src="./pic/{img_path_dir}/{name}/thumb" alt="{name}"></a>
143 <figcaption class="pic"><label for="{name}">{name}</label></figcaption>
144 </figure>
145"#
146 );
147 }
148 fig_list += " </form>";
149
150 let mut page_navi = String::new();
151 page_navi += &format!("<p>{} files</p>", pic_list.len());
152 page_navi += "<p>";
153 for left in (0..total).step_by(page_by) {
154 let right = cmp::min(left + page_by - 1, total - 1);
155 let link = if (left..=right).contains(&start) {
156 format!(r#"{}-{} "#, left + 1, right + 1)
157 } else {
158 format!(
159 r#"<a href="?page={}">{}-{}</a> "#,
160 left,
161 left + 1,
162 right + 1
163 )
164 };
165 page_navi += &link;
166 }
167 page_navi += "</p>";
168
169 format!(
170 r#"<!DOCTYPE html>
171<html lang="en">
172 <head>
173 <title>{title}</title>
174 <style>
175 figure.pic {{
176 display: inline-block;
177 margin: 5px;
178 }}
179 figcaption.pic {{
180 font-size: 100%;
181 text-align: center;
182 }}
183 </style>
184 </head>
185 <body>
186 <h1>{title}</h1>
187 {page_navi}
188
189{fig_list}
190
191 <h2>Navigation</h2>
192 <p><a href="./">Camera Main Page</a></p>
193 </body>
194</html>
195"#
196 )
197}
198
199fn parse_cmd_targets(
203 iter: impl IntoIterator<Item = (String, String)>,
204) -> Result<(String, Vec<String>)> {
205 let mut cmd = None;
206 let mut targets = Vec::new();
207
208 for (key, value) in iter {
209 match key.as_str() {
210 "cmd" => {
211 cmd = Some(value);
212 }
213 "target" => {
214 targets.push(value);
215 }
216 _ => {}
217 }
218 }
219
220 cmd.map_or_else(|| Err(anyhow!("cmd is required")), |cmd| Ok((cmd, targets)))
221}
222
223#[actix_web::post("/camera/history")]
228async fn history_post(
229 ctrl: web::Data<Control>,
230 form: web::Form<Vec<(String, String)>>,
231) -> HttpResponse {
232 let param = parse_cmd_targets(form.0);
233 if let Err(why) = param {
234 return HttpResponse::BadRequest()
235 .content_type(ContentType::plaintext())
236 .body(why.to_string());
237 }
238 let (cmd, targets) = param.unwrap();
239
240 match cmd.as_str() {
241 "archive" => {
242 if let Err(why) = archive_pics(&ctrl, &targets).await {
243 if why.is::<std::io::Error>() {
244 HttpResponse::InternalServerError()
245 .content_type(ContentType::plaintext())
246 .body(why.to_string())
247 } else {
248 HttpResponse::BadRequest()
249 .content_type(ContentType::plaintext())
250 .body(why.to_string())
251 }
252 } else {
253 HttpResponse::SeeOther()
255 .append_header(("LOCATION", "./archive"))
256 .finish()
257 }
258 }
259 "delete" => {
260 if let Err(why) = delete_history_pics(&ctrl, &targets).await {
261 HttpResponse::BadRequest()
262 .content_type(ContentType::plaintext())
263 .body(why.to_string())
264 } else {
265 HttpResponse::SeeOther()
267 .append_header(("LOCATION", "./history"))
268 .finish()
269 }
270 }
271 _ => HttpResponse::BadRequest()
272 .content_type(ContentType::plaintext())
273 .body("invalid command"),
274 }
275}
276
277#[actix_web::post("/camera/archive")]
282async fn archive_post(
283 ctrl: web::Data<Control>,
284 form: web::Form<Vec<(String, String)>>,
285) -> HttpResponse {
286 let param = parse_cmd_targets(form.0);
287 if let Err(why) = param {
288 return HttpResponse::BadRequest()
289 .content_type(ContentType::plaintext())
290 .body(why.to_string());
291 }
292 let (cmd, targets) = param.unwrap();
293
294 match cmd.as_str() {
295 "twitter" => {
296 if targets.is_empty() || targets.len() > LIMIT_PHOTO_COUNT {
297 error_resp_msg(StatusCode::BAD_REQUEST, "invalid pic count")
298 } else if let Err(why) = twitter_post(&ctrl, &targets).await {
299 error_resp_msg(StatusCode::INTERNAL_SERVER_ERROR, &why.to_string())
300 } else {
301 HttpResponse::SeeOther()
303 .append_header(("LOCATION", "./history"))
304 .finish()
305 }
306 }
307 "delete" => {
308 if let Err(why) = delete_archive_pics(&ctrl, &targets).await {
309 error_resp_msg(StatusCode::BAD_REQUEST, &why.to_string())
310 } else {
311 HttpResponse::SeeOther()
313 .append_header(("LOCATION", "./history"))
314 .finish()
315 }
316 }
317 _ => HttpResponse::BadRequest()
318 .content_type(ContentType::plaintext())
319 .body("invalid command"),
320 }
321}
322
323async fn archive_pics(ctrl: &Control, ids: &[String]) -> Result<()> {
324 let mut camera = ctrl.sysmods().camera.lock().await;
325 for id in ids {
326 camera.push_pic_archive(id).await?;
327 }
328
329 Ok(())
330}
331
332async fn delete_history_pics(ctrl: &Control, ids: &[String]) -> Result<()> {
333 let mut camera = ctrl.sysmods().camera.lock().await;
334 for id in ids {
335 camera.delete_pic_history(id).await?;
336 }
337
338 Ok(())
339}
340
341async fn delete_archive_pics(ctrl: &Control, ids: &[String]) -> Result<()> {
342 let mut camera = ctrl.sysmods().camera.lock().await;
343 for id in ids {
344 camera.delete_pic_archive(id).await?;
345 }
346
347 Ok(())
348}
349
350async fn twitter_post(ctrl: &Control, ids: &[String]) -> Result<()> {
351 assert!(ids.len() <= LIMIT_PHOTO_COUNT);
352
353 let mut binlist = Vec::new();
354 {
355 let camera = ctrl.sysmods().camera.lock().await;
356
357 let (_, archive) = camera.pic_list();
358 for id in ids {
359 if let Some(entry) = archive.get(id) {
360 let mut file = File::open(&entry.path_main).await?;
361 let mut bin = Vec::new();
362 let _ = file.read_to_end(&mut bin).await?;
363 binlist.push(bin);
364 } else {
365 bail!("ID not found");
366 }
367 }
368 }
369 let mut resized_list = Vec::new();
370 for bin in binlist {
371 let mut w = 1280_u32;
372 let mut h = 720_u32;
373 let mut resized = bin;
374 while resized.len() > LIMIT_PHOTO_SIZE {
375 resized = resize(&resized, w, h)?;
376 w /= 2;
377 h /= 2;
378 }
379 resized_list.push(resized);
380 }
381 {
382 let mut tw = ctrl.sysmods().twitter.lock().await;
383
384 let mut midlist: Vec<u64> = Vec::new();
385 for bin in resized_list {
386 let media_id = tw.media_upload(bin).await?;
387 midlist.push(media_id);
388 }
389 let text = ids.join("\n");
390 tw.tweet_custom(&text, None, &midlist).await?;
391 }
392
393 Ok(())
394}
395
396#[actix_web::get("/camera/pic/history/{name}/{kind}")]
401async fn pic_history_get(ctrl: web::Data<Control>, path: web::Path<(String, String)>) -> WebResult {
402 let (name, kind) = path.into_inner();
403
404 pic_get_internal(&ctrl, StorageType::History, &name, &kind).await
405}
406
407#[actix_web::get("/camera/pic/archive/{name}/{kind}")]
412async fn pic_archive_get(ctrl: web::Data<Control>, path: web::Path<(String, String)>) -> WebResult {
413 let (name, kind) = path.into_inner();
414
415 pic_get_internal(&ctrl, StorageType::Archive, &name, &kind).await
416}
417
418enum StorageType {
419 History,
420 Archive,
421}
422
423async fn pic_get_internal(ctrl: &Control, stype: StorageType, name: &str, kind: &str) -> WebResult {
424 let is_th = match kind {
425 "main" => false,
426 "thumb" => true,
427 _ => return Ok(error_resp(StatusCode::BAD_REQUEST)),
428 };
429
430 let value = {
431 let camera = ctrl.sysmods().camera.lock().await;
432 let (hist, ar) = camera.pic_list();
433 let dict = match stype {
434 StorageType::History => hist,
435 StorageType::Archive => ar,
436 };
437 dict.get(name).cloned()
438 };
439
440 if let Some(entry) = value {
441 let path = match is_th {
442 false => entry.path_main,
443 true => entry.path_th,
444 };
445 let mut file = File::open(path).await.map_err(|e| anyhow!(e))?;
446 let mut bin = Vec::new();
447 let _ = file.read_to_end(&mut bin).await.map_err(|e| anyhow!(e))?;
448
449 let resp = HttpResponse::Ok()
450 .content_type(ContentType::jpeg())
451 .body(bin);
452 Ok(resp)
453 } else {
454 Ok(error_resp(StatusCode::NOT_FOUND))
455 }
456}
457
458#[actix_web::post("/camera/take")]
459async fn take_post(ctrl: web::Data<Control>) -> WebResult {
460 let pic = take_a_pic(TakePicOption::new()).await;
461 if let Err(ref e) = pic {
462 error!("take a picture error");
463 error!("{e:#}");
464 }
465 let pic = pic?;
466
467 let thumb = create_thumbnail(&pic)?;
479
480 let mut camera = ctrl.sysmods().camera.lock().await;
481 camera.push_pic_history(&pic, &thumb).await?;
482 drop(camera);
483
484 let resp = HttpResponse::Ok()
485 .content_type(ContentType::jpeg())
486 .body(pic);
487 Ok(resp)
488}