use std::borrow::Cow;
use std::env;
use std::collections::HashSet;
#[allow(unused_imports)]
use log::{info, warn, error, debug};
use actix_web::{get, web, HttpResponse, Result, body::Body};
use serde::{Deserialize, Serialize};
use rust_embed::RustEmbed;
use mime_guess::from_path;
use handlebars::{Handlebars, RenderContext, Helper, Context, JsonRender, HelperResult, Output, RenderError};
use handlebars::handlebars_helper;
use handlebars_misc_helpers::register;
use crate::util::{shorten_string, string_to_idcolor,base64_to_32, get_file_url};
use crate::db::DBClient;
use crate::models::{IndexThread, Post, IndexPost, Board};
#[derive(Debug, Clone, Deserialize, Serialize)]
struct TemplateThread {
pub boards: Vec<Board>,
pub op: Post,
pub posts: Vec<Post>
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct TemplateThreadIndex {
pub boards: Vec<Board>,
pub next: i64,
pub prev: i64,
pub current: i64,
pub op: Post,
pub threads: Vec<TemplateThreadIndexThread>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct TemplateThreadIndexThread {
pub op: IndexPost,
pub posts: Vec<IndexPost>
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct TemplateHomePage {
pub boards: Vec<Board>,
pub posts: Vec<Post>
}
fn get_home_boards(boards: &Vec<Board>) -> Vec<String> {
let env_str = env::var("HOME_PAGE_BOARDS").ok().unwrap_or_default();
let board_set = env_str.split(",").fold(HashSet::new(), |mut set, s| {if !s.is_empty() {set.insert(s.to_string());} set});
boards.iter().map(|b| b.name.clone()).filter(|b| board_set.contains(b) || board_set.is_empty()).collect()
}
#[get("/")]
pub(crate) async fn home_page(db: web::Data<DBClient>, hb: web::Data<Handlebars<'_>>)
-> Result<HttpResponse, HttpResponse> {
let boards = db.get_all_boards().await
.map_err(|e| {
error!("Error getting boards from DB: {}", e);
HttpResponse::InternalServerError().finish()
})?;
let home_boards = get_home_boards(&boards);
let posts = db.get_latest_images(500i64, 0i64, home_boards).await
.map_err(|e| {
error!("Error getting posts from DB: {}", e);
HttpResponse::InternalServerError().finish()
})?;
let body = hb.render("home", &TemplateHomePage{
boards,
posts
}).unwrap();
Ok(HttpResponse::Ok().body(body))
}
#[get("/{board:[A-z0-9]+}/thread/{no:\\d+}{foo:/?[^/]*}")]
pub(crate) async fn thread_page(db: web::Data<DBClient>, hb: web::Data<Handlebars<'_>>, info: web::Path<(String, i64)>)
-> Result<HttpResponse, HttpResponse> {
let (board, no) = info.into_inner();
let boards = db.get_all_boards().await
.map_err(|e| {
error!("Error getting boards from DB: {}", e);
HttpResponse::InternalServerError().finish()
})?;
let thread = db.get_thread(&board, no).await
.map_err(|e| {
error!("Error getting thread from DB: {}", e);
HttpResponse::InternalServerError().finish()
})?
.ok_or(HttpResponse::NotFound().finish())?;
let body = hb.render("thread", &TemplateThread{
boards,
op: thread.posts[0].clone(),
posts: thread.posts[1..].to_vec()
}).unwrap();
Ok(HttpResponse::Ok().body(body))
}
#[get("/{board:[A-z0-9]+}/{idx:\\d+}")]
pub(crate) async fn index_page_handler(db: web::Data<DBClient>, hb: web::Data<Handlebars<'_>>, info: web::Path<(String, i64)>)
-> Result<HttpResponse, HttpResponse> {
let (board, index) = info.into_inner();
index_page(db, hb, board, index).await
}
#[get("/{board:[A-z0-9]+}")]
pub(crate) async fn board_page(db: web::Data<DBClient>, hb: web::Data<Handlebars<'_>>, info: web::Path<String>)
-> Result<HttpResponse, HttpResponse> {
let board = info.into_inner();
index_page(db, hb, board, 1).await
}
async fn index_page(db: web::Data<DBClient>, hb: web::Data<Handlebars<'_>>, board: String, index: i64)
-> Result<HttpResponse, HttpResponse> {
let mut nonzero_index = 1;
if index > 0 {
nonzero_index = index;
}
let boards = db.get_all_boards().await
.map_err(|e| {
error!("Error getting boards from DB: {}", e);
HttpResponse::InternalServerError().finish()
})?;
let threads = db.get_thread_index(&board, nonzero_index-1, 15).await
.map_err(|e| {
error!("Error getting post from DB: {}", e);
HttpResponse::InternalServerError().finish()
})?;
if threads.len() == 0 {
return Err(HttpResponse::NotFound().finish())
}
let index_threads: Vec<IndexThread> = threads.clone().into_iter().map(|t| t.into()).collect();
let prev = match nonzero_index == 1 {
true => nonzero_index,
false=> nonzero_index-1
};
let body = hb.render("index_page", &TemplateThreadIndex {
boards,
next: nonzero_index+1,
current: nonzero_index,
prev,
op: threads[0].posts[0].clone(),
threads: index_threads.into_iter().map(|t| TemplateThreadIndexThread{op: t.posts[0].clone(), posts: t.posts[1..].to_vec()}).collect()
}).unwrap();
Ok(HttpResponse::Ok().body(body))
}
#[derive(RustEmbed)]
#[folder = "src/templates"]
struct Templates;
pub(crate) fn build_handlebars() -> Handlebars<'static> {
let mut handlebars = Handlebars::new();
for template_path in Templates::iter() {
if let Some(template_file) = Templates::get(&template_path) {
let path_vec: Vec<&str> = template_path.split(".").collect();
let name = path_vec[0];
let template_str: String = std::str::from_utf8(template_file.as_ref()).unwrap().to_string();
handlebars.register_template_string(&name, &template_str).unwrap();
}
}
register(&mut handlebars);
handlebars_helper!(b_to_kb: |b: i64| b/1024i64);
handlebars.register_helper("b_to_kb", Box::new(b_to_kb));
handlebars.register_helper("shorten",
Box::new(|h: &Helper, _r: &Handlebars, _: &Context, _rc: &mut RenderContext, out: &mut dyn Output| -> HelperResult {
let length = h.param(0).ok_or(RenderError::new("Length not found"))?;
let text = h.param(1).ok_or(RenderError::new("String not found"))?.value().render();
let sz = length.value().as_u64().unwrap_or_default();
out.write(shorten_string(sz as usize, text).as_ref())?;
Ok(())
}));
handlebars.register_helper("id_color",
Box::new(|h: &Helper, _r: &Handlebars, _: &Context, _rc: &mut RenderContext, out: &mut dyn Output| -> HelperResult {
let id_text = h.param(0).ok_or(RenderError::new("ID not found"))?.value().render();
out.write(string_to_idcolor(id_text).as_ref())?;
Ok(())
}));
handlebars.register_helper("base64_to_32",
Box::new(|h: &Helper, _r: &Handlebars, _: &Context, _rc: &mut RenderContext, out: &mut dyn Output| -> HelperResult {
let b64_text = h.param(0).ok_or(RenderError::new("base64 not found"))?.value().render();
out.write(base64_to_32(b64_text).unwrap_or_default().as_ref())?;
Ok(())
}));
handlebars.register_helper("get_file_url",
Box::new(|h: &Helper, _r: &Handlebars, _: &Context, _rc: &mut RenderContext, out: &mut dyn Output| -> HelperResult {
let sha256 = h.param(0).ok_or(RenderError::new("sha256 not found"))?.value().render();
let ext = h.param(1).ok_or(RenderError::new("ext not found"))?.value().render();
out.write(get_file_url(&sha256, &ext, false).as_ref())?;
Ok(())
}));
handlebars.register_helper("get_thumbnail_url",
Box::new(|h: &Helper, _r: &Handlebars, _: &Context, _rc: &mut RenderContext, out: &mut dyn Output| -> HelperResult {
let sha256 = h.param(0).ok_or(RenderError::new("sha256 not found"))?.value().render();
out.write(get_file_url(&sha256, &".jpg".to_string(), true).as_ref())?;
Ok(())
}));
handlebars
}
#[derive(RustEmbed)]
#[folder = "static"]
struct Asset;
fn handle_embedded_file(path: &str) -> HttpResponse {
match Asset::get(path) {
Some(content) => {
let body: Body = match content {
Cow::Borrowed(bytes) => bytes.into(),
Cow::Owned(bytes) => bytes.into(),
};
HttpResponse::Ok().content_type(from_path(path).first_or_octet_stream().as_ref()).body(body)
}
None => HttpResponse::NotFound().body("404 Not Found"),
}
}
pub(crate) fn dist(path: web::Path<String>) -> HttpResponse {
handle_embedded_file(&path.into_inner())
}