mirror of
https://github.com/YGGverse/mb.git
synced 2026-04-01 09:35:31 +00:00
initial commit
This commit is contained in:
parent
f2b0a1979d
commit
8e7df9ff72
14 changed files with 1011 additions and 0 deletions
264
src/main.rs
Normal file
264
src/main.rs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
mod config;
|
||||
mod db;
|
||||
mod feed;
|
||||
|
||||
use config::Config;
|
||||
use db::{Db, Order, Sort};
|
||||
use feed::Feed;
|
||||
use plurify::Plurify;
|
||||
use rocket::{
|
||||
State,
|
||||
form::Form,
|
||||
http::Status,
|
||||
response::{Redirect, content::RawXml},
|
||||
serde::Serialize,
|
||||
};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
|
||||
#[get("/?<search>&<page>&<token>")]
|
||||
fn index(
|
||||
search: Option<&str>,
|
||||
page: Option<usize>,
|
||||
token: Option<&str>,
|
||||
db: &State<Db>,
|
||||
config: &State<Config>,
|
||||
) -> Result<Template, Status> {
|
||||
if token.is_some_and(|t| t != config.token) {
|
||||
warn!("Invalid access token! Access denied.");
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
let posts = db
|
||||
.posts(
|
||||
search,
|
||||
Some((Sort::Time, Order::Desc)),
|
||||
page.map(|p| if p > 0 { p - 1 } else { p } * config.limit),
|
||||
Some(config.limit),
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("DB read error: `{e}`");
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
Ok(Template::render(
|
||||
"index",
|
||||
context! {
|
||||
meta_title: {
|
||||
let mut t = String::new();
|
||||
if let Some(q) = search && !q.is_empty() {
|
||||
t.push_str(q);
|
||||
t.push_str(S);
|
||||
t.push_str("Search");
|
||||
t.push_str(S)
|
||||
}
|
||||
if let Some(p) = page && p > 1 {
|
||||
t.push_str(&format!("Page {p}"));
|
||||
t.push_str(S)
|
||||
}
|
||||
t.push_str(&config.title);
|
||||
if let Some(ref description) = config.description
|
||||
&& page.is_none_or(|p| p == 1) && search.is_none_or(|q| q.is_empty()) {
|
||||
t.push_str(S);
|
||||
t.push_str(description)
|
||||
}
|
||||
t
|
||||
},
|
||||
title: &config.title,
|
||||
description: config.description.as_deref(),
|
||||
back: page.map(|p| uri!(index(search, if p > 2 { Some(p - 1) } else { None }, token))),
|
||||
next: if page.unwrap_or(1) * config.limit >= posts.total { None }
|
||||
else { Some(uri!(index(search, Some(page.map_or(2, |p| p + 1)), token))) },
|
||||
pagination_totals: if posts.total > 0 { Some(format!(
|
||||
"Page {} / {} ({} {} total)",
|
||||
page.unwrap_or(1),
|
||||
(posts.total as f64 / config.limit as f64).ceil(),
|
||||
posts.total,
|
||||
posts.total.plurify(&["post", "posts", "posts"])
|
||||
)) } else { None },
|
||||
posts: posts.posts.into_iter().map(|p| Post {
|
||||
id: p.id,
|
||||
time: p.time().format(&config.time_format).to_string(),
|
||||
message: p.message,
|
||||
href: Href {
|
||||
delete: token.map(|t| uri!(delete(p.id, t)).to_string()),
|
||||
post: uri!(post(p.id, token)).to_string()
|
||||
}
|
||||
}).collect::<Vec<Post>>(),
|
||||
home: uri!(index(None::<&str>, None::<usize>, token)),
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
search,
|
||||
token
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct Submit {
|
||||
message: String,
|
||||
token: String,
|
||||
}
|
||||
#[post("/submit", data = "<input>")]
|
||||
fn submit(input: Form<Submit>, db: &State<Db>, config: &State<Config>) -> Result<Redirect, Status> {
|
||||
if input.token != config.token {
|
||||
warn!("Invalid access token! Access denied.");
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
let i = input.into_inner();
|
||||
db.submit(i.message).map_err(|e| {
|
||||
error!("DB write error: `{e}`");
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
Ok(Redirect::to(uri!(index(
|
||||
None::<&str>,
|
||||
None::<usize>,
|
||||
Some(i.token),
|
||||
))))
|
||||
}
|
||||
|
||||
#[get("/delete?<id>&<token>")]
|
||||
fn delete(
|
||||
id: i64,
|
||||
token: String,
|
||||
db: &State<Db>,
|
||||
config: &State<Config>,
|
||||
) -> Result<Redirect, Status> {
|
||||
if token != config.token {
|
||||
warn!("Invalid access token! Access denied.");
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
db.delete(id).map_err(|e| {
|
||||
error!("DB write error: `{e}`");
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
Ok(Redirect::to(uri!(index(
|
||||
None::<&str>,
|
||||
None::<usize>,
|
||||
Some(token),
|
||||
))))
|
||||
}
|
||||
|
||||
#[get("/<id>?<token>")]
|
||||
fn post(
|
||||
id: i64,
|
||||
token: Option<&str>,
|
||||
db: &State<Db>,
|
||||
config: &State<Config>,
|
||||
) -> Result<Template, Status> {
|
||||
if token.is_some_and(|t| t != config.token) {
|
||||
warn!("Invalid access token! Access denied.");
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
match db.post(id).map_err(|e| {
|
||||
error!("DB read error: `{e}`");
|
||||
Status::InternalServerError
|
||||
})? {
|
||||
Some(post) => {
|
||||
let time = post.time().format(&config.time_format).to_string();
|
||||
Ok(Template::render(
|
||||
"post",
|
||||
context! {
|
||||
meta_title: format!("{time}{S}{}", &config.title),
|
||||
title: &config.title,
|
||||
description: config.description.as_deref(),
|
||||
back: None::<&str>,
|
||||
next: None::<&str>,
|
||||
pagination_totals: None::<&str>,
|
||||
post: Post {
|
||||
id: post.id,
|
||||
message: post.message,
|
||||
href: Href {
|
||||
delete: token.map(|t| uri!(delete(post.id, t)).to_string()),
|
||||
post: uri!(post(post.id, token)).to_string()
|
||||
},
|
||||
time
|
||||
},
|
||||
home: uri!(index(None::<&str>, None::<usize>, token)),
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
search: None::<&str>
|
||||
},
|
||||
))
|
||||
}
|
||||
None => Err(Status::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/rss")]
|
||||
fn rss(db: &State<Db>, config: &State<Config>) -> Result<RawXml<String>, Status> {
|
||||
let mut f = Feed::new(
|
||||
&config.title,
|
||||
config.description.as_deref(),
|
||||
config.url.clone(),
|
||||
1024, // @TODO
|
||||
);
|
||||
for p in db
|
||||
.posts(
|
||||
None,
|
||||
Some((Sort::Time, Order::Desc)),
|
||||
None,
|
||||
Some(config.limit),
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("DB read error: `{e}`");
|
||||
Status::InternalServerError
|
||||
})?
|
||||
.posts
|
||||
{
|
||||
let time = p.time();
|
||||
f.push(
|
||||
p.id,
|
||||
time,
|
||||
time.format(&config.time_format).to_string(),
|
||||
&p.message,
|
||||
)
|
||||
}
|
||||
Ok(RawXml(f.commit()))
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
use clap::Parser;
|
||||
let config = config::Config::parse();
|
||||
if config.url.is_none() {
|
||||
warn!("Canonical URL option is required for the RSS feed by the specification!") // @TODO
|
||||
}
|
||||
rocket::build()
|
||||
.attach(Template::fairing())
|
||||
.configure(rocket::Config {
|
||||
port: config.port,
|
||||
address: config.host,
|
||||
..if config.debug {
|
||||
rocket::Config::debug_default()
|
||||
} else {
|
||||
rocket::Config::release_default()
|
||||
}
|
||||
})
|
||||
.mount("/", rocket::fs::FileServer::from(&config.public))
|
||||
.mount("/", routes![index, post, submit, delete, rss])
|
||||
.manage(Db::init(&config.database).unwrap())
|
||||
.manage(config)
|
||||
}
|
||||
|
||||
/// Meta title separator
|
||||
const S: &str = " • ";
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct Href {
|
||||
/// Reference to post delete action
|
||||
/// * optional as dependent of access permissions
|
||||
delete: Option<String>,
|
||||
/// Reference to post details page
|
||||
post: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct Post {
|
||||
id: i64,
|
||||
message: String,
|
||||
/// Time created
|
||||
/// * edit time should be implemented as the separated history table
|
||||
time: String,
|
||||
href: Href,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue