refactor to rocket

This commit is contained in:
JMARyA 2024-07-10 14:13:37 +02:00
parent e1c45d6a45
commit 7507f94141
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
14 changed files with 1194 additions and 895 deletions

1683
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,16 +6,14 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-files = "0.6.2"
actix-web = "4.2.1"
chrono = "0.4.22" chrono = "0.4.22"
env_logger = "0.9.3" env_logger = "0.9.3"
log = "0.4.17" log = "0.4.17"
gnupg-rs = "0.1.0" gnupg-rs = "0.1.0"
web-base = "0.2"
serde = {version = "1.0.147", features = ["derive"] } serde = {version = "1.0.147", features = ["derive"] }
serde_json = "1.0.87" serde_json = "1.0.87"
reqwest = { version = "0.11", features = ["blocking", "json"] } reqwest = { version = "0.11", features = ["blocking", "json"] }
maud = "0.24.0" maud = "0.24.0"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
rocket = "0.5.1"

View file

@ -11,6 +11,7 @@ RUN pacman -Syu --noconfirm
RUN pacman -S --noconfirm gnupg ca-certificates openssl-1.1 RUN pacman -S --noconfirm gnupg ca-certificates openssl-1.1
COPY --from=builder /app/target/release/me-site /me-site COPY --from=builder /app/target/release/me-site /me-site
COPY ./rocket.toml /rocket.toml
WORKDIR / WORKDIR /

View file

@ -3,6 +3,7 @@
## Possible Values to configure: ## Possible Values to configure:
- `name` : Personal Name - `name` : Personal Name
- `email` : Personal Mail - `email` : Personal Mail
- `base_url` : Base URL of the Site
- `secret_key` : Secret Key for Flask - `secret_key` : Secret Key for Flask
- `xmr_address` : Monero Receive Address - `xmr_address` : Monero Receive Address
- `notify` : Notification Object - `notify` : Notification Object

3
rocket.toml Normal file
View file

@ -0,0 +1,3 @@
[default]
address = "0.0.0.0"
port = 8080

View file

@ -23,6 +23,15 @@ impl Config {
Self { root: v, color: c } Self { root: v, color: c }
} }
pub fn base_url(&self) -> String {
self.root
.get("base_url")
.unwrap()
.as_str()
.unwrap()
.to_string()
}
pub fn name(&self) -> Option<String> { pub fn name(&self) -> Option<String> {
Option::from(self.root.get("name")?.as_str()?.to_string()) Option::from(self.root.get("name")?.as_str()?.to_string())
} }

View file

@ -3,44 +3,35 @@ mod msg;
mod notification; mod notification;
mod pages; mod pages;
use actix_web::{web, App}; use rocket::{launch, routes};
#[actix_web::main] #[launch]
async fn main() -> std::io::Result<()> { async fn rocket() -> _ {
std::env::set_var("RUST_LOG", "info"); std::env::set_var("RUST_LOG", "info");
std::env::set_var("RUST_BACKTRACE", "1"); std::env::set_var("RUST_BACKTRACE", "1");
env_logger::init(); env_logger::init();
let conf = config::Config::new(); let conf = config::Config::new();
web_base::bootstrap::cache_bootstrap().await; pages::bootstrap::cache_bootstrap().await;
let manifest = web_base::Manifest::new(&conf.name().unwrap()) rocket::build()
.set_short_name(&conf.name().unwrap()) .mount(
.set_start_url("/") "/",
.set_background_color(&conf.bg_color().unwrap()) routes![
.add_icon("/assets/me.png", "2000x2000", "image/png") pages::assets::wallpaper,
.set_display(web_base::ManifestDisplay::MinimalUI); pages::assets::me_img,
pages::assets::me_img_png,
web_base::map!( pages::bootstrap::bootstrap_css,
web_base::Site::new() pages::bootstrap::bootstrap_icons,
.enable_bootstrap(true) pages::bootstrap::bootstrap_js,
.enable_scaling(true) pages::bootstrap::bootstrap_font1,
.enable_favicon("/assets/me".to_string()) pages::bootstrap::bootstrap_font2,
.add_manifest(manifest), pages::index::message_post,
|x: App<_>| { pages::index::message_page,
x.app_data(web::Data::new(conf.clone())) pages::index::mirrors,
.service(pages::index::index) pages::index::public_key,
// Assets pages::index::index
.service(pages::assets::wallpaper) ],
.service(pages::assets::me_img) )
.service(pages::assets::me_img_png) .manage(conf)
.service(pages::index::public_key)
.service(pages::index::mirrors)
.service(pages::index::message_page)
.service(pages::index::message_post)
}
)
.bind(("0.0.0.0", 8080))?
.run()
.await
} }

View file

@ -1,20 +1,18 @@
use crate::config; use crate::config;
use crate::config::Config; use crate::config::Config;
use actix_web::web::{self, Data};
use log::info; use log::info;
pub async fn notify(msg: &str, title: &str, config: Data<Config>) { pub async fn notify(msg: &str, title: &str, config: &Config) {
if let Some(gotify) = config.gotify_config() { if let Some(gotify) = config.gotify_config() {
gotify_notification(msg, title, gotify).await; gotify_notification(msg, title, gotify).await;
} }
if let Some(webhook) = config.webhook_config() { if let Some(webhook) = config.webhook_config() {
info!("Sending webhook notification"); info!("Sending webhook notification");
let request = let request = serde_json::json!({
serde_json::json!({ "instance": config.name(),
"instance": config.name(), "title": title,
"title": title, "msg": msg
"msg": msg });
});
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
client client
@ -29,7 +27,10 @@ async fn gotify_notification(msg: &str, title: &str, config: config::GotifySetti
info!("Sending gotify notification"); info!("Sending gotify notification");
let c = reqwest::Client::new(); let c = reqwest::Client::new();
let _ = c let _ = c
.post(format!("https://{}/message?token={}", config.host, config.token)) .post(format!(
"https://{}/message?token={}",
config.host, config.token
))
.header("title", title) .header("title", title)
.header("message", msg) .header("message", msg)
.header("priority", 5) .header("priority", 5)

View file

@ -1,19 +1,18 @@
use actix_files::NamedFile; use rocket::{fs::NamedFile, get};
use actix_web::{get, Result};
// Assets // Assets
#[get("/assets/wall")] #[get("/assets/wall")]
pub(crate) async fn wallpaper() -> Result<NamedFile> { pub async fn wallpaper() -> Option<NamedFile> {
Ok(NamedFile::open("./config/wall.avif")?) NamedFile::open("./config/wall.avif").await.ok()
} }
#[get("/assets/me")] #[get("/assets/me")]
pub(crate) async fn me_img() -> Result<NamedFile> { pub async fn me_img() -> Option<NamedFile> {
Ok(NamedFile::open("./config/me.avif")?) NamedFile::open("./config/me.avif").await.ok()
} }
#[get("/assets/me.png")] #[get("/assets/me.png")]
pub(crate) async fn me_img_png() -> Result<NamedFile> { pub async fn me_img_png() -> Option<NamedFile> {
Ok(NamedFile::open("./config/me.png")?) NamedFile::open("./config/me.png").await.ok()
} }

67
src/pages/bootstrap.rs Normal file
View file

@ -0,0 +1,67 @@
use rocket::{fs::NamedFile, get};
use std::io::Write;
async fn download_file(url: &str, file: &str) {
let content = reqwest::get(url).await.expect("couldn't download file");
std::fs::File::create(file)
.expect("could not create file")
.write_all(&content.bytes().await.expect("could not get bytes"))
.expect("could not write file");
}
#[get("/bootstrap.min.css")]
pub async fn bootstrap_css() -> Option<NamedFile> {
NamedFile::open("./cache/bootstrap.mi.await.ok()n.css")
.await
.ok()
}
#[get("/bootstrap-icons.css")]
pub async fn bootstrap_icons() -> Option<NamedFile> {
NamedFile::open("./cache/bootstrap-icon.await.ok()s.css")
.await
.ok()
}
#[get("/bootstrap.bundle.min.js")]
pub async fn bootstrap_js() -> Option<NamedFile> {
NamedFile::open("./cache/bootstrap.b.await.ok()undle.min.js")
.await
.ok()
}
#[get("/fonts/bootstrap-icons.woff2")]
pub async fn bootstrap_font1() -> Option<NamedFile> {
NamedFile::open("./cache/fonts/bootstrap-icons.woff2")
.await
.ok()
}
#[get("/fonts/bootstrap-icons.woff")]
pub async fn bootstrap_font2() -> Option<NamedFile> {
NamedFile::open("./cache/fonts/bootstrap-icons.woff")
.await
.ok()
}
/// Cache Bootstrap files locally
pub async fn cache_bootstrap() {
std::fs::create_dir_all("./cache/fonts").expect("couldn't create cache dir");
download_file(
"https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css",
"./cache/bootstrap.min.css",
)
.await;
download_file(
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css",
"./cache/bootstrap-icons.css",
)
.await;
download_file(
"https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js",
"./cache/bootstrap.bundle.min.js",
)
.await;
download_file("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/fonts/bootstrap-icons.woff2?8d200481aa7f02a2d63a331fc782cfaf", "./cache/fonts/bootstrap-icons.woff2").await;
download_file("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/fonts/bootstrap-icons.woff?8d200481aa7f02a2d63a331fc782cfaf", "./cache/fonts/bootstrap-icons.woff").await;
}

92
src/pages/html.rs Normal file
View file

@ -0,0 +1,92 @@
use crate::config::Config;
use maud::{html, PreEscaped, DOCTYPE};
use rocket::request::{FromRequest, Outcome, Request};
pub enum RequestClient {
Browser,
Other,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for RequestClient {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
if is_browser(req) {
Outcome::Success(Self::Browser)
} else {
Outcome::Success(Self::Other)
}
}
}
#[must_use]
pub fn is_browser(req: &Request) -> bool {
let ua = req
.headers()
.get("usesr-agent")
.next()
.unwrap()
.to_lowercase();
ua.contains("chrome") || ua.contains("safari") || ua.contains("firefox")
}
pub async fn build_site(
content: String,
title: &str,
disable_color: bool,
shadow: bool,
config: &Config,
) -> String {
let mut c_class = "bg-dark text-white justify-content-center text-center".to_string();
let mut c_style = String::new();
let mut g_style = "a {text-decoration: none; font-weight: bold; color: white}".to_string();
if !disable_color {
if let (Some(fg), Some(bg)) = (config.fg_color(), config.bg_color()) {
c_class = "justify-content-center text-center".to_string();
c_style = format!("background: {bg}; color: {fg};");
g_style = format!("a {{text-decoration: none; font-weight: bold; color: {fg}}}");
}
}
if std::path::Path::new("./config/wall.avif").exists() {
c_style.push_str("background-image: url('assets/wall');background-size:cover;");
}
if shadow {
c_style.push_str("text-shadow: 1px 1px 3px black;");
}
let body = html! {
body style=(c_style) class=(c_class) {
style { (g_style) };
(PreEscaped(content))
};
};
build_site_from_body(title, &body.into_string())
}
pub fn build_site_from_body(title: &str, body: &str) -> String {
html! {
(DOCTYPE)
html {
head {
meta charset="UTF-8";
title {
(title)
};
link rel="icon" href="/assets/me";
meta name="viewport" content="width=device-width, initial-scale=1.0";
link href="/bootstrap.min.css" rel="stylesheet";
link href="/bootstrap-icons.css" rel="stylesheet";
script src="/bootstrap.bundle.min.js" {};
link href="/style/global.css" rel="stylesheet";
link href="/style/site.css" rel="stylesheet";
link href="/style/common.css" rel="stylesheet";
};
(PreEscaped(body))
};
}
.into_string()
}

View file

@ -1,43 +0,0 @@
use crate::config::Config;
use actix_web::HttpResponse;
use actix_web::{web::Data, HttpRequest};
use maud::{html, PreEscaped};
pub(crate) async fn build_site(
content: String,
title: &str,
disable_color: bool,
shadow: bool,
config: &Data<Config>,
r: &HttpRequest,
) -> HttpResponse<String> {
let mut c_class = "bg-dark text-white justify-content-center text-center".to_string();
let mut c_style = String::new();
let mut g_style = "a {text-decoration: none; font-weight: bold; color: white}".to_string();
if !disable_color {
if let (Some(fg), Some(bg)) = (config.fg_color(), config.bg_color()) {
c_class = "justify-content-center text-center".to_string();
c_style = format!("background: {bg}; color: {fg};");
g_style = format!("a {{text-decoration: none; font-weight: bold; color: {fg}}}");
}
}
if std::path::Path::new("./config/wall.avif").exists() {
c_style.push_str("background-image: url('assets/wall');background-size:cover;");
}
if shadow {
c_style.push_str("text-shadow: 1px 1px 3px black;");
}
let body = html! {
body style=(c_style) class=(c_class) {
style { (g_style) };
(PreEscaped(content))
};
};
web_base::func::build_site_from_body(
&web_base::Site::from_request(&r),
title,
&body.into_string(),
)
}

View file

@ -1,34 +1,33 @@
use crate::{config, pages}; use crate::config;
use actix_web::http::header;
use actix_web::web::Form;
use actix_web::{get, post, web, Error, HttpRequest, HttpResponse, Responder, Result};
use maud::{html, PreEscaped}; use maud::{html, PreEscaped};
use rocket::{form::Form, get, post, response::Redirect, uri, FromForm, State};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)] use super::html::{build_site, RequestClient};
#[derive(Serialize, Deserialize, Debug, FromForm)]
pub struct MessageForm { pub struct MessageForm {
msg_name: String, msg_name: String,
message: String, message: String,
} }
#[post("/message")] #[post("/message", data = "<msg>")]
pub async fn message_post(r: HttpRequest, f: Form<MessageForm>) -> impl Responder { pub async fn message_post(msg: Form<MessageForm>, config: &State<config::Config>) -> Redirect {
let config: &web::Data<config::Config> = r.app_data().expect("get config failed"); crate::msg::save_message(&msg.message, &msg.msg_name.to_string());
crate::msg::save_message(&f.message, &f.msg_name.to_string()); crate::notification::notify(&msg.message, &msg.msg_name, config).await;
crate::notification::notify(&f.message, &f.msg_name, config.clone()).await; Redirect::to(uri!(message_page))
web_base::func::redirect("/message")
} }
#[get("/message")] #[get("/message")]
pub async fn message_page(r: HttpRequest) -> impl Responder { pub async fn message_page(config: &State<config::Config>) -> String {
let config: &web::Data<config::Config> = r.app_data().expect("get config failed"); let post_uri = uri!(message_post).to_string();
let resp = html! { let resp = html! {
div class="container" style="margin-top: 25px" { div class="container" style="margin-top: 25px" {
h1 { "Message" }; h1 { "Message" };
br; br;
form action=(web_base::func::get_full_url(&r)) method="post" autocomplete="off" { form action=(post_uri) method="post" autocomplete="off" {
input value="" type="text" required name="msg_name" placeholder="Name" class="form-control bg-dark text-white" style="margin-bottom: 15px"; input value="" type="text" required name="msg_name" placeholder="Name" class="form-control bg-dark text-white" style="margin-bottom: 15px";
textarea placeholder="Message" required name="message" cols="10" rows="10" class="form-control bg-dark text-white" style="margin-bottom: 15px;" {}; textarea placeholder="Message" required name="message" cols="10" rows="10" class="form-control bg-dark text-white" style="margin-bottom: 15px;" {};
input value="Send Message" type="submit" required name="submit" class="btn btn-danger text-white text-decoration-none"; input value="Send Message" type="submit" required name="submit" class="btn btn-danger text-white text-decoration-none";
@ -36,15 +35,14 @@ pub async fn message_page(r: HttpRequest) -> impl Responder {
} }
}; };
pages::html_fn::build_site(resp.into_string(), "Message", false, true, config, &r).await build_site(resp.into_string(), "Message", false, true, config).await
} }
#[get("/mirrors.txt")] #[get("/mirrors.txt")]
pub async fn mirrors(r: HttpRequest) -> Result<impl Responder, Error> { pub async fn mirrors(r: RequestClient, config: &State<config::Config>) -> Option<String> {
let config: &web::Data<config::Config> = r.app_data().expect("get config failed");
if let Ok(mirror_file) = std::fs::File::open("./config/mirrors.txt") { if let Ok(mirror_file) = std::fs::File::open("./config/mirrors.txt") {
let content = std::io::read_to_string(mirror_file).expect("could not read file"); let content = std::io::read_to_string(mirror_file).ok()?;
if web_base::func::is_browser(&r) { if matches!(r, RequestClient::Browser) {
let resp = html! { let resp = html! {
div style="margin: 25px;" { div style="margin: 25px;" {
pre { pre {
@ -52,26 +50,19 @@ pub async fn mirrors(r: HttpRequest) -> Result<impl Responder, Error> {
}; };
} }
}; };
return Ok(pages::html_fn::build_site(
resp.into_string(), return Some(build_site(resp.into_string(), "Mirrors", false, true, config).await);
"Mirrors",
false,
true,
config,
&r,
)
.await);
} }
return HttpResponse::Ok().message_body(content); return Some(content);
} }
HttpResponse::NotFound().message_body(String::new())
None
} }
#[get("/public_key")] #[get("/public_key")]
pub async fn public_key(r: HttpRequest) -> Result<impl Responder, Error> { pub async fn public_key(r: RequestClient, config: &State<config::Config>) -> Option<String> {
if web_base::func::is_browser(&r) { if matches!(r, RequestClient::Browser) {
let config: &web::Data<config::Config> = r.app_data().expect("get config failed"); let host = config.base_url();
let host = format!("http://{}", web_base::func::get_host(&r));
let key = std::io::read_to_string( let key = std::io::read_to_string(
std::fs::File::open("./config/pub.key").expect("key could not be opened"), std::fs::File::open("./config/pub.key").expect("key could not be opened"),
) )
@ -94,28 +85,19 @@ pub async fn public_key(r: HttpRequest) -> Result<impl Responder, Error> {
} }
}; };
return Ok(pages::html_fn::build_site( return Some(build_site(resp.into_string(), "Public Key", true, false, config).await);
resp.into_string(),
"Public Key",
true,
false,
config,
&r,
)
.await);
} }
if let Ok(key_f) = std::fs::File::open("./config/pub.key") { if let Ok(key_f) = std::fs::File::open("./config/pub.key") {
if let Ok(key_data) = std::io::read_to_string(key_f) { if let Ok(key_data) = std::io::read_to_string(key_f) {
return HttpResponse::Ok() return Some(key_data);
.insert_header(header::ContentType::plaintext())
.message_body(key_data);
} }
} }
HttpResponse::NotFound().message_body(String::new()) None
} }
fn build_information_block(conf: &web::Data<config::Config>) -> String { fn build_information_block(conf: &config::Config) -> String {
let name = conf.name().expect("no name found"); let name = conf.name().expect("no name found");
html! { html! {
div class="container border-dark" style="margin-top: 20px" { div class="container border-dark" style="margin-top: 20px" {
@ -128,7 +110,7 @@ fn build_information_block(conf: &web::Data<config::Config>) -> String {
.into_string() .into_string()
} }
fn build_contact_block(conf: &web::Data<config::Config>) -> String { fn build_contact_block(conf: &config::Config) -> String {
conf.email().map_or_else(String::new, |email| { conf.email().map_or_else(String::new, |email| {
let pgp_key_message = if std::path::Path::new("./config/pub.key").exists() { let pgp_key_message = if std::path::Path::new("./config/pub.key").exists() {
html! { html! {
@ -158,7 +140,7 @@ fn build_contact_block(conf: &web::Data<config::Config>) -> String {
}) })
} }
fn build_donation_block(conf: &web::Data<config::Config>) -> String { fn build_donation_block(conf: &config::Config) -> String {
conf.xmr_address().map_or_else(String::new, |xmr_addr| { conf.xmr_address().map_or_else(String::new, |xmr_addr| {
html! { html! {
div class="container" style="margin-top: 20px" { div class="container" style="margin-top: 20px" {
@ -178,10 +160,10 @@ fn build_donation_block(conf: &web::Data<config::Config>) -> String {
} }
#[get("/")] #[get("/")]
pub(crate) async fn index(conf: web::Data<config::Config>, r: HttpRequest) -> impl Responder { pub async fn index(conf: &State<config::Config>) -> String {
let information_block = build_information_block(&conf); let information_block = build_information_block(conf);
let contact_block = build_contact_block(&conf); let contact_block = build_contact_block(conf);
let donation_block = build_donation_block(&conf); let donation_block = build_donation_block(conf);
let own_index = std::fs::read_to_string("./config/index.html").unwrap_or_default(); let own_index = std::fs::read_to_string("./config/index.html").unwrap_or_default();
let content = format!( let content = format!(
" "
@ -191,5 +173,5 @@ pub(crate) async fn index(conf: web::Data<config::Config>, r: HttpRequest) -> im
{donation_block} {donation_block}
" "
); );
crate::pages::html_fn::build_site(content, "About Me", false, true, &conf, &r).await build_site(content, "About Me", false, true, conf).await
} }

View file

@ -1,3 +1,4 @@
pub mod assets; pub mod assets;
pub mod html_fn; pub mod bootstrap;
pub mod html;
pub mod index; pub mod index;