🚧 port to rust

This commit is contained in:
JMARyA 2022-11-12 00:41:51 +01:00
parent e5aa247f11
commit af3a052acb
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
24 changed files with 3125 additions and 564 deletions

5
.gitignore vendored
View file

@ -136,5 +136,6 @@ cython_debug/
.vscode .vscode
/data /data
# Cached Files # Added by cargo
/src/static
/target

2556
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "me-site"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-files = "0.6.2"
actix-web = "4.2.1"
chrono = "0.4.22"
env_logger = "0.9.3"
log = "0.4.17"
pgp = "0.9.0"
rand = "0.8.5"
reqwest = "0.11.12"
serde = {version = "1.0.147", features = ["derive"] }
serde_json = "1.0.87"

View file

@ -1,13 +1,13 @@
FROM python:3 FROM rust as builder
COPY requirements.txt / COPY . /app
RUN pip3 install -r /requirements.txt RUN cargo build --release
FROM archlinux
COPY --from=builder /app/target/release/me-site /bin/me-site
VOLUME /config VOLUME /config
COPY src /app CMD [ "./bin/me-site" ]
RUN sed -i "s/debug=True/debug=False/" /app/main.py
CMD [ "python3", "/app/main.py" ]

View file

@ -1,12 +0,0 @@
fmt:
black .
clean:
fd pycache -I -x rm -rv {}
debug:
docker-compose -f docker-compose-debug.yml up -d
debug-stop:
docker-compose -f docker-compose-debug.yml down

View file

@ -1,13 +0,0 @@
version: '3'
services:
app:
build: "."
environment:
TZ: Europe/Berlin
ports:
- 1030:1030
volumes:
- ./config:/config
- ./data:/data
- ./src:/app

View file

@ -1,4 +0,0 @@
Flask
gnupg
requests
git+https://github.com/jmarya/htmlpy#egg=htmlpy

View file

@ -1,28 +0,0 @@
from flask import request, session, Blueprint, Response, send_from_directory
from htmlpy import *
from config import CONFIG
import os
asset_pages = Blueprint("assets", __name__)
###################
### Site Assets ###
###################
def filesend(path):
filename = os.path.basename(path)
dirpath = os.path.dirname(path)
return send_from_directory(dirpath, filename)
# Profile Picture Asset
@asset_pages.route("/me", methods=["GET"])
def me_picture():
return filesend("/config/me.avif")
# Background Image
@asset_pages.route("/wall")
def wall_bg():
return filesend("/config/wall.avif")

View file

@ -1,39 +0,0 @@
import json
#####################
### Configuration ###
#####################
CONFIG = json.loads(open("/config/config.json").read())
# Colors Array
def colors():
try:
return json.loads(open("/config/colors.json").read())
except:
return None
# Foreground Color
def fg_color():
if colors() is not None:
fg = colors()["special"]["foreground"]
if "colors" in CONFIG:
if "fg" in CONFIG["colors"]:
i = CONFIG["colors"]["fg"] - 1
fg = colors()["colors"][f"color{i}"]
return fg
return None
# Background Color
def bg_color():
if colors() is not None:
bg = colors()["special"]["background"]
if "colors" in CONFIG:
if "bg" in CONFIG["colors"]:
i = CONFIG["colors"]["bg"] - 1
bg = colors()["colors"][f"color{i}"]
return bg
return None

81
src/config.rs Normal file
View file

@ -0,0 +1,81 @@
use serde_json::Value;
use std::fmt::format;
#[derive(Debug)]
pub struct GotifySettings {
pub host: String,
pub token: String,
}
#[derive(Clone)]
pub struct Config {
root: Value,
color: Option<Value>,
}
fn read_json_file(f: &str) -> Option<Value> {
return serde_json::from_str(&std::fs::read_to_string(f).ok()?).ok()?;
}
impl Config {
pub fn new() -> Config {
let v = read_json_file("/config/config.json").expect("could not read config file");
let c = read_json_file("/config/colors.json");
Config { root: v, color: c }
}
pub fn name(&self) -> Option<String> {
return Option::from(self.root.get("name")?.as_str()?.to_string());
}
pub fn email(&self) -> Option<String> {
return Option::from(self.root.get("email")?.as_str()?.to_string());
}
pub fn xmr_address(&self) -> Option<String> {
return Option::from(self.root.get("xmr_address")?.as_str()?.to_string());
}
fn color_n_fg(&self) -> Option<i64> {
return Some(self.root.get("colors")?.get("fg")?.as_i64()?);
}
fn color_n_bg(&self) -> Option<i64> {
return Some(self.root.get("colors")?.get("bg")?.as_i64()?);
}
pub fn fg_color(&self) -> Option<String> {
if let Some(col) = &self.color {
let fg = col.get("special")?.get("foreground")?.as_str()?;
if let Some(fg_n) = self.color_n_fg() {
let n = fg_n - 1;
let fg = col.get("colors")?.get(format!("color{}", n))?.as_str()?;
return Some(fg.to_string());
}
return Some(fg.to_string());
} else {
return None;
}
}
pub fn bg_color(&self) -> Option<String> {
if let Some(col) = &self.color {
let fg = col.get("special")?.get("background")?.as_str()?;
if let Some(bg_n) = self.color_n_bg() {
let n = bg_n - 1;
let fg = col.get("colors")?.get(format!("color{}", n))?.as_str()?;
return Some(fg.to_string());
}
return Some(fg.to_string());
} else {
return None;
}
}
pub fn gotify_config(&self) -> Option<GotifySettings> {
let settings = self.root.get("notify")?.get("gotify")?;
let host = settings.get("host")?.as_str()?.to_string();
let token = settings.get("token")?.as_str()?.to_string();
return Some(GotifySettings { host, token });
}
}

View file

@ -1,35 +0,0 @@
import flask
from htmlpy import Link
######################
### Site Functions ###
######################
# Check if request is from onion
def is_onion(req: flask.globals.request) -> bool:
return req.host.endswith("onion")
# Check if request is from i2p
def is_i2p(req: flask.globals.request) -> bool:
return req.host.endswith("i2p")
# Return dynamic link depending on request origin
def dynamic_link(
inner, normal: str, onion: str, i2p: str, req: flask.globals.request
) -> Link:
if is_onion(req):
return Link(onion, inner)
if is_i2p(req):
return Link(i2p, inner)
return Link(normal, inner)
# Check if request is from common browsers
def is_browser(req: flask.globals.request) -> bool:
ua = req.user_agent.string.lower()
if "chrome" in ua or "safari" in ua or "firefox" in ua:
return True
return False

View file

@ -1,99 +0,0 @@
import requests
import os
from htmlpy import *
from config import colors, CONFIG, fg_color, bg_color
############################
### HTML Generation Code ###
############################
# Bootstrap Icon
def Icon(code, middle_align=False):
style = ""
if middle_align:
style = "vertical-align: middle;"
return Span(
"", global_attr=GlobalAttributes(css_class=f"bi bi-{code}", style=style)
)
# Wrapper for Base HTML
def buildSite(content, title=None, disable_color=False, shadow=True):
c_class = "bg-dark text-white justify-content-center text-center"
c_style = ""
g_style = "a {text-decoration: none; font-weight: bold; color: white}"
if not disable_color:
if colors() is not None:
c_class = "justify-content-center text-center"
fg = fg_color()
bg = bg_color()
c_style = f"background: {bg}; color: {fg};"
g_style = f"a {{text-decoration: none; font-weight: bold; color: {fg}}}"
if os.path.exists("/config/wall.avif"):
c_style += "background-image: url('assets/wall');background-size:cover;"
if shadow:
c_style += "text-shadow: 1px 1px 3px black;"
return Document(
head=Head(
[
Title(title),
Meta(
name="viewport",
content="user-scalable=no, width=device-width, initial-scale=1.0",
),
BOOTSTRAP,
]
),
body=Body(
[Style(g_style), content],
global_attr=GlobalAttributes(css_class=c_class, style=c_style),
),
)
def download_file(url, file):
os.makedirs("/app/static/", exist_ok=True)
r = requests.get(url, allow_redirects=True)
open(file, "wb").write(r.content)
# Downloads all bootstrap files to flasks static directory
def cache_bootstrap():
download_file(
"https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css",
"/app/static/bootstrap.min.css",
)
download_file(
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css",
"/app/static/bootstrap-icons.css",
)
download_file(
"https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js",
"/app/static/bootstrap.bundle.min.js",
)
os.makedirs("/app/static/fonts", exist_ok=True)
download_file(
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/fonts/bootstrap-icons.woff2?8d200481aa7f02a2d63a331fc782cfaf",
"/app/static/fonts/bootstrap-icons.woff2",
)
download_file(
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/fonts/bootstrap-icons.woff?8d200481aa7f02a2d63a331fc782cfaf",
"/app/static/fonts/bootstrap-icons.woff",
)
# Bootstrap CSS
BOOTSTRAP = [
Reference(
"/static/bootstrap.min.css",
"stylesheet",
),
Reference(
"/static/bootstrap-icons.css",
"stylesheet",
),
Script(src="/static/bootstrap.bundle.min.js"),
]

View file

@ -1,262 +0,0 @@
from os.path import exists
import gnupg
from flask import request, Blueprint, Response, redirect
from htmlpy import *
from config import CONFIG, colors, fg_color, bg_color
from html_fn import buildSite, Icon
from msg import encrypt, save_message
from notification import notify
from fn import is_browser
main_pages = Blueprint("main", __name__)
# Color Scheme
@main_pages.route("/color")
def color():
if colors() is None:
return ""
colors_p = [Heading(1, "Color 0")]
for i in range(0, 16):
c = colors()["colors"][f"color{i}"]
colors_p.append(
Heading(
1, f"Color {i+1}", global_attr=GlobalAttributes(style=f"color: {c}")
)
)
site = buildSite([], "Colors")
bg = colors()["special"]["background"]
fg = colors()["special"]["foreground"]
cur = colors()["special"]["cursor"]
site.body = Body(
[Div([colors_p], global_attr=GlobalAttributes(css_class="container"))],
global_attr=GlobalAttributes(
style=f"background: {bg}; color: {fg}",
css_class="justify-content-center text-center",
),
)
return site.to_code()
# Mirrors
@main_pages.route("/mirrors.txt", methods=["GET"])
def mirrors():
# TODO : Autogenerate GPG Sign Message
if exists("/config/mirrors.txt"):
txt = open("/config/mirrors.txt").read()
if is_browser(request):
site = buildSite(
Div(
f"<pre>{txt}</pre>",
global_attr=GlobalAttributes(style="margin: 25px;"),
),
"Mirrors",
)
return site.to_code()
return txt
else:
return Response(response="", status=404)
# Message Sending Page
@main_pages.route("/message", methods=["GET", "POST"])
def send_message():
if request.method == "POST":
# If POST handle message
msg = request.form["message"]
name = request.form["msg_name"]
if msg is not None:
cipher = encrypt(msg)
save_message(cipher, name)
notify(f"New Message from {name}")
return redirect(request.url_root)
else:
# Return Message Form
return buildSite(
[
Div(
[
Heading(1, "Message"),
LineBreak(),
Form(
destination=request.url,
inner=[
Input(
"",
placeholder="Name",
name="msg_name",
global_attr=GlobalAttributes(
css_class="form-control bg-dark text-white",
style="margin-bottom: 15px",
),
),
TextArea(
inner="",
placeholder="Message",
name="message",
global_attr=GlobalAttributes(
css_class="form-control bg-dark text-white",
style="margin-bottom: 15px;",
),
),
Input(
type=InputType.Submit,
value="Send Message",
name="submit",
global_attr=GlobalAttributes(
css_class="btn btn-danger text-white text-decoration-none"
),
),
],
),
],
global_attr=GlobalAttributes(
css_class="container", style="margin-top: 25px"
),
)
],
"Send a message",
).to_code()
# Public Key Page
@main_pages.route("/public_key", methods=["GET"])
def public_key():
try:
ret = open("/config/pub.key").read()
pgp = gnupg.GPG()
pgp.import_keys(open("/config/pub.key").read())
key_id = (
str(pgp.list_keys()[0]["uids"][0]).replace("<", "[ ").replace(">", " ]")
)
if is_browser(request):
# If request is from common browser return stylized html
return buildSite(
[
Div(
Div(
[
Bold("To Import: "),
Span(
f'curl -sL "{request.base_url}"|gpg --import',
global_attr=GlobalAttributes(
style="display: block;font-family: monospace,monospace;margin-top: 10px; font-size: 20px;overflow-wrap: break-word;"
),
),
],
global_attr=GlobalAttributes(css_class="alert alert-info"),
),
global_attr=GlobalAttributes(
css_class="container", style="margin-top: 25px"
),
),
Heading(
4,
key_id,
global_attr=GlobalAttributes(
css_class="container card",
style="padding-top: 10px; padding-bottom: 10px; background: black; margin-bottom: 15px;",
),
),
Div(
Paragraph(ret.replace("\n", "<br>")),
global_attr=GlobalAttributes(
css_class="container card bg-primary"
),
),
],
"Public Key",
disable_color=True,
shadow=False,
).to_code()
# Return raw key
resp = Response(response=ret, status=200, mimetype="application/pgp-keys")
return resp
except:
# If key does not exist return error
return Response(response="", status=502)
# Contact
def build_contact_block():
return Div(
[
Heading(1, [Icon("person-lines-fill"), "Contact"]),
ThematicBreak(),
[
Link("/public_key", "My PGP Key"),
LineBreak(),
Link("/message", "Write a message"),
LineBreak(),
LineBreak(),
]
if exists("/config/pub.key")
else None,
Link(f"mailto:{CONFIG['email']}", CONFIG["email"]),
LineBreak(),
],
global_attr=GlobalAttributes(css_class="container border-dark"),
)
# Donations
def build_donation_block():
return Div(
[
Heading(1, [Icon("cash-coin", True), "Donation"]),
ThematicBreak(),
Paragraph(
[
Bold("Monero: "),
Span(
CONFIG["xmr_address"],
global_attr=GlobalAttributes(
style="color: orange;overflow-wrap: break-word;"
),
),
]
)
if "xmr_address" in CONFIG
else None,
],
global_attr=GlobalAttributes(css_class="container", style="margin-top: 20px"),
)
# Basic Information
def build_information_block():
return Div(
[
Image(
"/assets/me",
200,
200,
"Me",
global_attr=GlobalAttributes(css_class="rounded"),
),
LineBreak(),
LineBreak(),
Heading(1, CONFIG["name"]),
ThematicBreak(),
],
global_attr=GlobalAttributes(
css_class="container border-dark", style="margin-top: 20px"
),
)
# Main
@main_pages.route("/", methods=["GET"])
def index():
return buildSite(
[build_information_block(), build_contact_block(), build_donation_block()],
"About Me",
).to_code()

View file

@ -1,18 +0,0 @@
from flask import Flask, session, request
from config import CONFIG
app = Flask(__name__)
app.secret_key = CONFIG["secret_key"]
from index import main_pages
from assets import asset_pages
app.register_blueprint(main_pages)
app.register_blueprint(asset_pages, url_prefix="/assets")
if __name__ == "__main__":
from html_fn import cache_bootstrap
cache_bootstrap()
app.run(host="0.0.0.0", port=1030, debug=True, threaded=True)

40
src/main.rs Normal file
View file

@ -0,0 +1,40 @@
mod config;
mod msg;
mod notification;
mod pages;
use actix_web::*;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "info");
std::env::set_var("RUST_BACKTRACE", "1");
env_logger::init();
let conf = config::Config::new();
pages::assets::cache_bootstrap().await;
HttpServer::new(move || {
let logger = actix_web::middleware::Logger::default();
App::new()
.wrap(logger)
.app_data(web::Data::new(conf.clone()))
.service(pages::index::index)
// Assets
.service(pages::assets::bootstrap_js)
.service(pages::assets::bootstrap_css)
.service(pages::assets::bootstrap_icons)
.service(pages::assets::bootstrap_font1)
.service(pages::assets::bootstrap_font2)
.service(pages::assets::wallpaper)
.service(pages::assets::me_img)
.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,21 +0,0 @@
import datetime
import os
import gnupg
################
### Messages ###
################
# Encrypt msg with GPG
def encrypt(msg):
pgp = gnupg.GPG()
pgp.import_keys(open("/config/pub.key").read())
return str(pgp.encrypt(msg, pgp.list_keys()[0]["fingerprint"]))
# Save msg in `/data/messages`
def save_message(msg, name=""):
os.makedirs("/data/messages", exist_ok=True)
dt = datetime.datetime.now().strftime("%Y-%m-%d.%H-%M")
f = open(f"/data/messages/{name}-{dt}.asc", "w")
f.write(msg)

26
src/msg.rs Normal file
View file

@ -0,0 +1,26 @@
use pgp::crypto::SymmetricKeyAlgorithm;
use pgp::{Deserializable, Message, SignedPublicKey};
use std::io::Write;
pub fn encrypt(msg: String) -> String {
// err: Encryption is done twice
let pub_key =
SignedPublicKey::from_string(&*std::fs::read_to_string("/config/pub.key").unwrap())
.unwrap()
.0;
let mut rng = rand::thread_rng();
let c = Message::new_literal("msg", &msg)
.encrypt_to_keys(&mut rng, SymmetricKeyAlgorithm::AES128, &[&pub_key])
.unwrap()
.to_armored_string(None)
.unwrap();
return c;
}
pub fn save_msg(msg: String, name: &str) {
std::fs::create_dir_all("/data/messages").expect("couldn't create msg dir");
let time = chrono::offset::Utc::now();
let time = time.format("%Y-%m-%d.%H-%M").to_string();
let mut f = std::fs::File::create(format!("/data/messages/{name}-{time}.asc")).unwrap();
f.write_all(encrypt(msg).as_bytes()).unwrap();
}

View file

@ -1,23 +0,0 @@
from config import CONFIG
import requests
#####################
### Notifications ###
#####################
# Send Notification to all handlers
def notify(msg, title=None):
try:
gotify_notification(msg, title)
except:
pass
# Gotify Notification Handler
def gotify_notification(msg, title=None):
token = CONFIG["notify"]["gotify"]["token"]
url = CONFIG["notify"]["gotify"]["host"]
requests.post(
f"https://{url}/message?token={token}",
{"title": title, "message": msg, "priority": "5"},
)

25
src/notification.rs Normal file
View file

@ -0,0 +1,25 @@
use crate::config;
use crate::config::Config;
use actix_web::web::Data;
use log::*;
pub async fn notify(msg: &str, title: &str, config: Data<Config>) {
if let Some(gotify) = config.gotify_config() {
gotify_notification(msg, title, gotify).await;
}
}
async fn gotify_notification(msg: &str, title: &str, config: config::GotifySettings) {
info!("Sending gotify notification");
let c = reqwest::Client::new();
let res = c
.post(format!(
"https://{}/message?token={}",
config.host, config.token
))
.header("title", title)
.header("message", msg)
.header("priority", 5)
.send()
.await;
}

71
src/pages/assets.rs Normal file
View file

@ -0,0 +1,71 @@
use actix_files::NamedFile;
use actix_web::*;
use std::io::Write;
// Bootstrap
async fn download_file(url: &str, file: &str) {
let content = reqwest::get(url).await.expect("couldn't download file");
std::fs::File::create(file)
.unwrap()
.write_all(&content.bytes().await.unwrap())
.unwrap();
}
pub(crate) 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;
}
#[get("/bootstrap.min.css")]
pub(crate) async fn bootstrap_css() -> Result<NamedFile> {
Ok(NamedFile::open("./cache/bootstrap.min.css")?)
}
#[get("/bootstrap-icons.css")]
pub(crate) async fn bootstrap_icons() -> Result<NamedFile> {
Ok(NamedFile::open("./cache/bootstrap-icons.css")?)
}
#[get("/bootstrap.bundle.min.js")]
pub(crate) async fn bootstrap_js() -> Result<NamedFile> {
Ok(NamedFile::open("./cache/bootstrap.bundle.min.js")?)
}
#[get("/fonts/bootstrap-icons.woff2")]
pub(crate) async fn bootstrap_font1(_: HttpRequest) -> Result<NamedFile> {
Ok(NamedFile::open("./cache/fonts/bootstrap-icons.woff2")?)
}
#[get("/fonts/bootstrap-icons.woff")]
pub(crate) async fn bootstrap_font2(_: HttpRequest) -> Result<NamedFile> {
Ok(NamedFile::open("./cache/fonts/bootstrap-icons.woff")?)
}
// Assets
#[get("/assets/wall")]
pub(crate) async fn wallpaper() -> Result<NamedFile> {
Ok(NamedFile::open("/config/wall.avif")?)
}
#[get("/assets/me")]
pub(crate) async fn me_img() -> Result<NamedFile> {
Ok(NamedFile::open("/config/me.avif")?)
}

53
src/pages/func.rs Normal file
View file

@ -0,0 +1,53 @@
use reqwest::get;
pub fn is_browser(req: &actix_web::HttpRequest) -> bool {
let ua = req
.headers()
.get("user-agent")
.unwrap()
.to_str()
.unwrap()
.to_lowercase();
if ua.contains("chrome") || ua.contains("safari") || ua.contains("firefox") {
return true;
}
return false;
}
pub fn get_host(r: &actix_web::HttpRequest) -> String {
let res = r.headers().get("HOST").unwrap().to_str().unwrap();
return res.to_string();
}
pub fn get_host_address(r: &actix_web::HttpRequest) -> String {
let res = r.headers().get("HOST").unwrap().to_str().unwrap();
let res: Vec<&str> = res.split(":").collect();
let res = res.first().unwrap();
return res.to_string();
}
pub fn is_onion(r: &actix_web::HttpRequest) -> bool {
return get_host_address(r).ends_with("onion");
}
pub fn is_i2p(r: &actix_web::HttpRequest) -> bool {
return get_host_address(r).ends_with("i2p");
}
pub fn dynamic_link(
inner: &str,
normal_link: &str,
onion: Option<&str>,
i2p: Option<&str>,
req: &actix_web::HttpRequest,
) -> String {
if is_onion(req) {
let href = onion.unwrap_or(normal_link);
return format!("<a href={href}> {inner} </a>");
}
if is_i2p(req) {
let href = i2p.unwrap_or(normal_link);
return format!("<a href={href}> {inner} </a>");
}
return format!("<a href={normal_link}> {inner} </a>");
}

56
src/pages/html_fn.rs Normal file
View file

@ -0,0 +1,56 @@
use crate::config;
use crate::config::Config;
use actix_web::web::Data;
use actix_web::*;
pub(crate) async fn build_site(
content: String,
title: &str,
disable_color: bool,
shadow: bool,
config: &Data<Config>,
) -> HttpResponse<String> {
const BOOTSTRAP: &str = r#"
<link href="/bootstrap.min.css" rel="stylesheet">
<link href="/bootstrap-icons.css" rel="stylesheet">
<link href="/bootstrap.bundle.min.js" rel="stylesheet">
"#;
let mut c_class = "bg-dark text-white justify-content-center text-center".to_string();
let mut c_style = "".to_string();
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 r = format!(
"
<!DOCTYPE html>
<html>
<head>
<title> {title} </title>
<meta name=\"viewport\" content=\"user-scalable=no, width=device-width, initial-scale=1.0\">
{BOOTSTRAP}
</head>
<body style=\"{c_style}\" class=\"{c_class}\">
<style>
{g_style}
</style>
{content}
</body>
</html>
"
);
return HttpResponse::Ok().message_body(r).unwrap();
}

184
src/pages/index.rs Normal file
View file

@ -0,0 +1,184 @@
use crate::pages::html_fn::build_site;
use crate::{config, pages};
use actix_web::http::header;
use actix_web::web::{Form, Json};
use actix_web::*;
use serde::{Deserialize, Serialize};
use std::io::Read;
#[derive(Serialize, Deserialize, Debug)]
pub struct MessageForm {
msg_name: String,
message: String,
}
#[post("/message")]
pub async fn message_post(r: HttpRequest, f: Form<MessageForm>) -> impl Responder {
let config: &web::Data<config::Config> = r.app_data().unwrap();
let cipher = crate::msg::encrypt(f.message.to_string());
crate::msg::save_msg(cipher, &f.msg_name.to_string());
crate::notification::notify(
&format!("New Message from {}", f.msg_name.to_string()),
"New Message",
config.clone(),
);
return HttpResponse::Found()
.header("Location", "/message")
.finish();
}
#[get("/message")]
pub async fn message_page(r: HttpRequest) -> impl Responder {
let config: &web::Data<config::Config> = r.app_data().unwrap();
let host = pages::func::get_host(&r);
let resp = format!(
r#"
<div class="container" style="margin-top: 25px"><h1>Message</h1>
<br>
<form action="http://{host}/message" 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">
<textarea placeholder="Message" required name="message" cols="10" rows="10" class="form-control bg-dark text-white" style="margin-bottom: 15px;">
</textarea>
<input value="Send Message" type="submit" required name="submit" class="btn btn-danger text-white text-decoration-none">
</form>
</div>
"#
);
return pages::html_fn::build_site(resp, "Message", false, true, config).await;
}
#[get("/mirrors.txt")]
pub async fn mirrors(r: HttpRequest) -> impl Responder {
let config: &web::Data<config::Config> = r.app_data().unwrap();
if let Ok(mirror_file) = std::fs::File::open("/config/mirrors.txt") {
let content = std::io::read_to_string(mirror_file).unwrap();
if pages::func::is_browser(&r) {
let resp = format!(
r#"
<div style="margin: 25px;">
<pre> {content} </pre>
</div>
"#
);
return pages::html_fn::build_site(resp, "Mirrors", false, true, config).await;
}
let res: HttpResponse<String> = HttpResponse::Ok().message_body(content).unwrap();
return res;
}
let res: HttpResponse<String> = HttpResponse::NotFound()
.message_body("".to_string())
.unwrap();
return res;
}
#[get("/public_key")]
pub async fn public_key(r: HttpRequest) -> impl Responder {
if pages::func::is_browser(&r) {
let config: &web::Data<config::Config> = r.app_data().unwrap();
let host = format!("http://{}", pages::func::get_host(&r));
let key = std::io::read_to_string(std::fs::File::open("/config/pub.key").unwrap())
.unwrap()
.replace("\n", "<br>");
let resp = format!(
r#"
<div class="container" style="margin-top: 25px">
<div class="alert alert-info">
<b>To Import: </b>
<span style="display: block;font-family: monospace,monospace;margin-top: 10px; font-size: 20px;overflow-wrap: break-word;">
curl -sL "{host}/public_key"|gpg --import</span>
</div></div>
<div class="container card bg-primary"><p> {key} </p></div>
"#
);
return pages::html_fn::build_site(resp, "Public Key", true, false, config).await;
}
if let Ok(key_f) = std::fs::File::open("/config/pub.key") {
if let Ok(key_data) = std::io::read_to_string(key_f) {
let res: HttpResponse<String> = HttpResponse::Ok()
.insert_header(header::ContentType::plaintext())
.message_body(key_data)
.unwrap();
return res;
}
}
let res: HttpResponse<String> = HttpResponse::NotFound()
.message_body("".to_string())
.unwrap();
return res;
}
fn build_information_block(conf: &web::Data<config::Config>) -> String {
let name = conf.name().unwrap();
format!(
r#"
<div class="container border-dark" style="margin-top: 20px">
<img src="/assets/me" height=200 width=200 alt="Me" class="rounded">
<br><br>
<h1> {name} </h1>
<hr>
</div>
"#
)
}
fn build_contact_block(conf: &web::Data<config::Config>) -> String {
if let Some(email) = conf.email() {
let pgp_key_message = match std::path::Path::new("/config/pub.key").exists() {
true => {
r#"
<a href="/public_key"> My PGP Key </a>
<br>
<a href="/message"> Write a message </a>
<br><br>
"#
}
false => "",
};
return format!(
r#"
<div class="container border-dark">
<h1> <span class="bi bi-person-lines-fill" style="vertical-align: middle;"> </span> Contact </h1>
<tr>
{pgp_key_message}
<a href="mailto:{email}"> {email} </a>
<hr>
</div>
"#
);
} else {
return "".to_string();
}
}
fn build_donation_block(conf: &web::Data<config::Config>) -> String {
if let Some(xmr_addr) = conf.xmr_address() {
format!(
r#"
<div class="container" style="margin-top: 20px">
<h1> <span class="bi bi-cash-coin"> </span> Donation </h1>
<tr>
<p> <b> Monero: </b> <span style="color: orange;overflow-wrap: break-word;"> {xmr_addr} </span> </p>
</div>
"#
)
} else {
return "".to_string();
}
}
#[get("/")]
pub(crate) async fn index(conf: web::Data<config::Config>) -> impl Responder {
let information_block = build_information_block(&conf);
let contact_block = build_contact_block(&conf);
let donation_block = build_donation_block(&conf);
let content = format!(
"
{information_block}
{contact_block}
{donation_block}
"
);
let r = crate::pages::html_fn::build_site(content, "About Me", false, true, &conf).await;
return r;
}

4
src/pages/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod assets;
pub mod func;
pub mod html_fn;
pub mod index;