🚧 port to rust
This commit is contained in:
parent
e5aa247f11
commit
af3a052acb
24 changed files with 3125 additions and 564 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -136,5 +136,6 @@ cython_debug/
|
|||
.vscode
|
||||
/data
|
||||
|
||||
# Cached Files
|
||||
/src/static
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
|
2556
Cargo.lock
generated
Normal file
2556
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal 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"
|
16
Dockerfile
16
Dockerfile
|
@ -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
|
||||
|
||||
COPY src /app
|
||||
|
||||
RUN sed -i "s/debug=True/debug=False/" /app/main.py
|
||||
|
||||
CMD [ "python3", "/app/main.py" ]
|
||||
CMD [ "./bin/me-site" ]
|
||||
|
|
12
Makefile
12
Makefile
|
@ -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
|
|
@ -1,13 +0,0 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: "."
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
ports:
|
||||
- 1030:1030
|
||||
volumes:
|
||||
- ./config:/config
|
||||
- ./data:/data
|
||||
- ./src:/app
|
|
@ -1,4 +0,0 @@
|
|||
Flask
|
||||
gnupg
|
||||
requests
|
||||
git+https://github.com/jmarya/htmlpy#egg=htmlpy
|
|
@ -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")
|
|
@ -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
81
src/config.rs
Normal 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 });
|
||||
}
|
||||
}
|
35
src/fn.py
35
src/fn.py
|
@ -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
|
|
@ -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"),
|
||||
]
|
262
src/index.py
262
src/index.py
|
@ -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()
|
18
src/main.py
18
src/main.py
|
@ -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
40
src/main.rs
Normal 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
|
||||
}
|
21
src/msg.py
21
src/msg.py
|
@ -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
26
src/msg.rs
Normal 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();
|
||||
}
|
|
@ -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
25
src/notification.rs
Normal 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
71
src/pages/assets.rs
Normal 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
53
src/pages/func.rs
Normal 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
56
src/pages/html_fn.rs
Normal 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
184
src/pages/index.rs
Normal 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
4
src/pages/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod assets;
|
||||
pub mod func;
|
||||
pub mod html_fn;
|
||||
pub mod index;
|
Loading…
Add table
Reference in a new issue