feat: implement main logic
This commit is contained in:
parent
d8da31f8b6
commit
e1fca0eb1e
217
Cargo.lock
generated
217
Cargo.lock
generated
@ -11,6 +11,56 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama"
|
name = "askama"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@ -143,6 +193,15 @@ version = "2.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
|
checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
@ -184,12 +243,96 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_complete"
|
||||||
|
version = "4.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@ -254,6 +397,16 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -266,6 +419,18 @@ version = "0.17.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@ -380,6 +545,12 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@ -463,6 +634,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@ -624,6 +801,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@ -662,6 +850,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@ -686,7 +880,12 @@ dependencies = [
|
|||||||
"askama",
|
"askama",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"clap_complete",
|
||||||
|
"hex",
|
||||||
|
"mime",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
@ -808,12 +1007,30 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|||||||
12
Cargo.toml
12
Cargo.toml
@ -5,6 +5,13 @@ edition = "2024"
|
|||||||
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
strip = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
askama = "0.16.0"
|
askama = "0.16.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@ -12,3 +19,8 @@ toml = "0.8"
|
|||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
clap_complete = "4.5"
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
mime = "0.3"
|
||||||
|
|||||||
18
TODO.md
18
TODO.md
@ -1,13 +1,17 @@
|
|||||||
# ToDo list
|
# ToDo list
|
||||||
* [ ] Сделать в embed шаблоны
|
* [ ] Поправить стили
|
||||||
* [ ] Сделать автоматическую локализацию по ip или заголовкам http `Accept-Language`
|
* [ ] alert copied
|
||||||
|
* [X] сделать путь до ключа
|
||||||
|
* [X] Сделать стили и засунуть их в embed (с уникальным хешем в имени)
|
||||||
|
* [X] Сделать js и засунуть их в embed (с уникальным хешем в имени)
|
||||||
|
* [ ] Добавить валидирование конфига
|
||||||
|
* [ ] Сделать default config чтобы выводился по аргументу cli
|
||||||
|
* [X] Сделать автоматическую локализацию по ip или заголовкам http `Accept-Language`
|
||||||
* [X] Сделать локализацию по параметру `/?lang=en`
|
* [X] Сделать локализацию по параметру `/?lang=en`
|
||||||
* [X] Сделать авто выбор верстки в зависимости от User-Agent
|
* [X] Сделать авто выбор верстки в зависимости от User-Agent
|
||||||
* [ ] Сделать стили и засунуть их в embed (с уникальным хешем в имени)
|
* [X] Отрефачить
|
||||||
* [ ] Сделать скрипты и засунуть их в embed (с уникальным хешем в имени)
|
* [X] Сделать определение хоста по которому был запрос и засунуть его в
|
||||||
* [ ] Отрефачить
|
|
||||||
* [ ] Сделать определение хоста по которому был запрос и засунуть его в
|
|
||||||
аргумент `build_html_template()`
|
аргумент `build_html_template()`
|
||||||
* [x] Сделать текущий год на клиенте или сервере (решить) (на сервере
|
* [x] Сделать текущий год на клиенте или сервере (решить) (на сервере
|
||||||
предпочтительно, чтобы им нельзя было манипулировать на клиенте)
|
предпочтительно, чтобы им нельзя было манипулировать на клиенте)
|
||||||
* [ ] Вынести конфиг в параметры
|
* [X] Вынести конфиг в параметры
|
||||||
|
|||||||
@ -1,64 +1,17 @@
|
|||||||
const translations = {
|
|
||||||
en: document.querySelectorAll('[data-lang="en"]'),
|
|
||||||
ru: document.querySelectorAll('[data-lang="ru"]'),
|
|
||||||
};
|
|
||||||
|
|
||||||
function setLanguage(lang) {
|
|
||||||
document.documentElement.lang = lang;
|
|
||||||
localStorage.setItem("lang", lang);
|
|
||||||
|
|
||||||
const langToHide = lang === "en" ? "ru" : "en";
|
|
||||||
|
|
||||||
translations[langToHide].forEach((el) => (el.style.display = "none"));
|
|
||||||
translations[lang].forEach((el) => (el.style.display = ""));
|
|
||||||
|
|
||||||
document.getElementById(`lang-${lang}`).classList.add("active");
|
|
||||||
document.getElementById(`lang-${langToHide}`).classList.remove("active");
|
|
||||||
|
|
||||||
if (window.event) event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTheme(theme) {
|
|
||||||
if (theme === "new") {
|
|
||||||
document.documentElement.setAttribute("data-theme", "new");
|
|
||||||
} else {
|
|
||||||
document.documentElement.removeAttribute("data-theme");
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById("theme-old")
|
|
||||||
.classList.toggle("active", theme === "old");
|
|
||||||
document
|
|
||||||
.getElementById("theme-new")
|
|
||||||
.classList.toggle("active", theme === "new");
|
|
||||||
|
|
||||||
if (window.event) event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyGPGKey() {
|
function copyGPGKey() {
|
||||||
var e = window.event;
|
var e = window.event;
|
||||||
if (e) { e.preventDefault(); e.stopPropagation(); }
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
||||||
const keyText = document.getElementById('gpg-key').textContent;
|
const keyText = document.getElementById('gpg-key').textContent;
|
||||||
navigator.clipboard.writeText(keyText).then(() => {
|
navigator.clipboard.writeText(keyText).then(() => {
|
||||||
const btnEn = document.getElementById('copy-gpg');
|
// const btnEn = document.getElementById('copy-gpg');
|
||||||
const btnRu = document.getElementById('copy-gpg-ru');
|
// const btnRu = document.getElementById('copy-gpg-ru');
|
||||||
const prevEn = btnEn.textContent;
|
// const prevEn = btnEn.textContent;
|
||||||
const prevRu = btnRu.textContent;
|
// const prevRu = btnRu.textContent;
|
||||||
btnEn.textContent = 'Copied!';
|
// btnEn.textContent = 'Copied!';
|
||||||
btnRu.textContent = 'Скопировано!';
|
// btnRu.textContent = 'Скопировано!';
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
btnEn.textContent = prevEn;
|
// btnEn.textContent = prevEn;
|
||||||
btnRu.textContent = prevRu;
|
// btnRu.textContent = prevRu;
|
||||||
}, 2000);
|
// }, 2000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const savedLang = localStorage.getItem("lang") || "en";
|
|
||||||
const savedTheme = localStorage.getItem("theme") || "old";
|
|
||||||
|
|
||||||
setLanguage(savedLang);
|
|
||||||
setTheme(savedTheme);
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
[page]
|
[page]
|
||||||
title.en = "Hi, I'm Vlad"
|
title.en = "Hi, I'm Vlad"
|
||||||
title.ru = "Владислав Кан"
|
title.ru = "Владислав Кан"
|
||||||
|
title.ua = "Владислав Кан"
|
||||||
meta_title = "thek4n"
|
meta_title = "thek4n"
|
||||||
meta_description = "thek4n - developer"
|
meta_description = "thek4n - developer"
|
||||||
description.en = "Developer"
|
description.en = "Developer"
|
||||||
description.ru = "Разработчик"
|
description.ru = "Разработчик"
|
||||||
|
description.ua = "Разработчик"
|
||||||
copyright_author = "Vladislav Kan <thek4n@yandex.ru>"
|
copyright_author = "Vladislav Kan <thek4n@yandex.ru>"
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +22,10 @@ title = "english"
|
|||||||
key = "ru"
|
key = "ru"
|
||||||
title = "русский"
|
title = "русский"
|
||||||
|
|
||||||
|
[[languages]]
|
||||||
|
key = "ua"
|
||||||
|
title = "український"
|
||||||
|
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
@ -27,12 +33,29 @@ title = "русский"
|
|||||||
[page.aboutme]
|
[page.aboutme]
|
||||||
title.en = "About Me"
|
title.en = "About Me"
|
||||||
title.ru = "Обо Мне"
|
title.ru = "Обо Мне"
|
||||||
|
title.ua = "Про мене"
|
||||||
text.en = '''
|
text.en = '''
|
||||||
About me text...
|
I am a Software Engineer 👨💻.
|
||||||
|
|
||||||
|
I'm working on developing of various IT products from embedded ⚙️ to web 🌐.
|
||||||
|
|
||||||
|
I enjoy reading different tech articles and books 📘.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
text.ru = '''
|
text.ru = '''
|
||||||
Обо мне текст...
|
Я инженер-программист 👨💻.
|
||||||
|
|
||||||
|
Я занимаюсь разработкой различных ИТ-продуктов, от embedded ⚙️ до веб-приложений 🌐.
|
||||||
|
|
||||||
|
Мне нравится читать различные технические статьи и книги 📘.
|
||||||
|
'''
|
||||||
|
|
||||||
|
text.ua = '''
|
||||||
|
Я інженер-програміст.
|
||||||
|
|
||||||
|
Я розробляю різні ІТ-продукти, від embedded ⚙️ до веб-додатків 🌐
|
||||||
|
|
||||||
|
Мені подобається читати Різні технічні статті та книги 📘
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@ -42,19 +65,22 @@ text.ru = '''
|
|||||||
[page.gpg]
|
[page.gpg]
|
||||||
title.en = "GPG Public Key"
|
title.en = "GPG Public Key"
|
||||||
title.ru = "Публичный ключ GPG"
|
title.ru = "Публичный ключ GPG"
|
||||||
url = "/gpgkey.txt"
|
title.ua = "Публічний ключ GPG"
|
||||||
url_title = "thek4n.ru/gpgkey.txt"
|
|
||||||
showkey_title.en = "Show key"
|
showkey_title.en = "Show key"
|
||||||
showkey_title.ru = "Показать ключ"
|
showkey_title.ru = "Показать ключ"
|
||||||
|
showkey_title.ua = "Показати ключ"
|
||||||
|
|
||||||
availat_title.en = "Available at"
|
availat_title.en = "Available at"
|
||||||
availat_title.ru = "Доступен по"
|
availat_title.ru = "Доступен по"
|
||||||
|
availat_title.ua = "Доступний по"
|
||||||
|
|
||||||
copy_title.en = "Show key"
|
copy_title.en = "Copy key"
|
||||||
copy_title.ru = "Показать ключ"
|
copy_title.ru = "Скопировать ключ"
|
||||||
|
copy_title.ua = "Скопіювати ключ"
|
||||||
|
|
||||||
copied_title.en = "Copied!"
|
copied_title.en = "Copied!"
|
||||||
copied_title.ru = "Скопировано!"
|
copied_title.ru = "Скопировано!"
|
||||||
|
copied_title.ua = "Скопійовано!"
|
||||||
|
|
||||||
value = '''
|
value = '''
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
@ -116,6 +142,7 @@ Fs/cX/nWHveKYtN9iWsgHnYwm7hvg3A2k6JMwec3Yp0NmeJinfhJaihqxg==
|
|||||||
[page.projects]
|
[page.projects]
|
||||||
title.en = "Projects"
|
title.en = "Projects"
|
||||||
title.ru = "Проекты"
|
title.ru = "Проекты"
|
||||||
|
title.ua = "Проекти"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
title = "ip.thek4n.ru"
|
title = "ip.thek4n.ru"
|
||||||
@ -124,6 +151,7 @@ source_url = "https://gitea.thek4n.ru/thek4n/ip.thek4n.ru"
|
|||||||
source_url_title = "thek4n/ip.thek4n.ru"
|
source_url_title = "thek4n/ip.thek4n.ru"
|
||||||
description.en = "check my ip service"
|
description.en = "check my ip service"
|
||||||
description.ru = "сервис определения ip"
|
description.ru = "сервис определения ip"
|
||||||
|
description.ua = "сервіс визначення ip"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
title = "paste.thek4n.ru"
|
title = "paste.thek4n.ru"
|
||||||
@ -132,6 +160,7 @@ source_url = "https://gitea.thek4n.ru/thek4n/paste.thek4n.ru"
|
|||||||
source_url_title = "thek4n/paste.thek4n.ru"
|
source_url_title = "thek4n/paste.thek4n.ru"
|
||||||
description.en = "copy/paste and url shortener service"
|
description.en = "copy/paste and url shortener service"
|
||||||
description.ru = "укоротитель ссылок"
|
description.ru = "укоротитель ссылок"
|
||||||
|
description.ua = "укоротитель посилань"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
title = "thek4n.ru"
|
title = "thek4n.ru"
|
||||||
@ -140,6 +169,7 @@ source_url = "https://gitea.thek4n.ru/thek4n/thek4n.ru"
|
|||||||
source_url_title = "thek4n/thek4n.ru"
|
source_url_title = "thek4n/thek4n.ru"
|
||||||
description.en = "this site source"
|
description.en = "this site source"
|
||||||
description.ru = "исходники этого сайта"
|
description.ru = "исходники этого сайта"
|
||||||
|
description.ua = "джерела цього сайту"
|
||||||
|
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
@ -148,21 +178,25 @@ description.ru = "исходники этого сайта"
|
|||||||
[page.contacts]
|
[page.contacts]
|
||||||
title.en = "Contacts"
|
title.en = "Contacts"
|
||||||
title.ru = "Контакты"
|
title.ru = "Контакты"
|
||||||
|
title.ua = "Контакти"
|
||||||
|
|
||||||
[[contacts]]
|
[[contacts]]
|
||||||
site_name.en = "Telegram"
|
site_name.en = "Telegram"
|
||||||
site_name.ru = "Телеграм"
|
site_name.ru = "Телеграм"
|
||||||
|
site_name.ua = "Телеграм"
|
||||||
title = "@thek4n"
|
title = "@thek4n"
|
||||||
url = "https://t.me/thek4n"
|
url = "https://t.me/thek4n"
|
||||||
|
|
||||||
[[contacts]]
|
[[contacts]]
|
||||||
site_name.en = "Github"
|
site_name.en = "Github"
|
||||||
site_name.ru = "Github"
|
site_name.ru = "Github"
|
||||||
|
site_name.ua = "Github"
|
||||||
title = "github.com/thek4n"
|
title = "github.com/thek4n"
|
||||||
url = "https://github.com/thek4n"
|
url = "https://github.com/thek4n"
|
||||||
|
|
||||||
[[contacts]]
|
[[contacts]]
|
||||||
site_name.en = "Email"
|
site_name.en = "Email"
|
||||||
site_name.ru = "Почта"
|
site_name.ru = "Почта"
|
||||||
|
site_name.ua = "Пошта"
|
||||||
title = "thek4n@yandex.ru"
|
title = "thek4n@yandex.ru"
|
||||||
url = "mailto:thek4n@yandex.ru"
|
url = "mailto:thek4n@yandex.ru"
|
||||||
|
|||||||
63
src/client/detector.rs
Normal file
63
src/client/detector.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::http::header::USER_AGENT;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum ClientType {
|
||||||
|
Browser,
|
||||||
|
CliLike,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_client_type(headers: &HeaderMap) -> ClientType {
|
||||||
|
let user_agent = headers
|
||||||
|
.get(USER_AGENT)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let user_agent_lower = user_agent.to_lowercase();
|
||||||
|
|
||||||
|
let accept = headers
|
||||||
|
.get("accept")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let accept_lower = accept.to_lowercase();
|
||||||
|
|
||||||
|
let is_likely_cli = accept_lower.is_empty()
|
||||||
|
|| accept_lower == "*/*"
|
||||||
|
|| accept_lower == "application/json"
|
||||||
|
|| accept_lower == "text/plain";
|
||||||
|
|
||||||
|
let is_likely_browser = accept_lower.contains("text/html")
|
||||||
|
&& (accept_lower.contains("application/xhtml+xml")
|
||||||
|
|| accept_lower.contains("application/xml")
|
||||||
|
|| accept_lower.contains("*/*"));
|
||||||
|
|
||||||
|
if user_agent_lower.contains("curl")
|
||||||
|
|| user_agent_lower.contains("wget")
|
||||||
|
|| user_agent_lower.contains("httpie")
|
||||||
|
|| user_agent_lower.contains("python-requests")
|
||||||
|
|| user_agent_lower.contains("go-http-client")
|
||||||
|
|| (is_likely_cli && !user_agent_lower.contains("mozilla/"))
|
||||||
|
{
|
||||||
|
return ClientType::CliLike;
|
||||||
|
}
|
||||||
|
|
||||||
|
if user_agent_lower.contains("mozilla/")
|
||||||
|
&& (user_agent_lower.contains("chrome")
|
||||||
|
|| user_agent_lower.contains("safari")
|
||||||
|
|| user_agent_lower.contains("firefox")
|
||||||
|
|| user_agent_lower.contains("edg"))
|
||||||
|
&& is_likely_browser
|
||||||
|
{
|
||||||
|
return ClientType::Browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_likely_browser {
|
||||||
|
ClientType::Browser
|
||||||
|
} else if is_likely_cli {
|
||||||
|
ClientType::CliLike
|
||||||
|
} else {
|
||||||
|
ClientType::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/client/mod.rs
Normal file
4
src/client/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mod detector;
|
||||||
|
|
||||||
|
pub use detector::ClientType;
|
||||||
|
pub use detector::detect_client_type;
|
||||||
12
src/config/mod.rs
Normal file
12
src/config/mod.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
mod models;
|
||||||
|
|
||||||
|
pub use models::*;
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn load_config(path: PathBuf) -> Result<Config, Box<dyn std::error::Error>> {
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
let config: Config = toml::from_str(&contents)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
70
src/config/models.rs
Normal file
70
src/config/models.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub page: PageConfig,
|
||||||
|
pub languages: Vec<LanguageConfig>,
|
||||||
|
pub projects: Option<Vec<ProjectConfig>>,
|
||||||
|
pub contacts: Option<Vec<ContactConfig>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct PageConfig {
|
||||||
|
pub title: HashMap<String, String>,
|
||||||
|
pub meta_title: String,
|
||||||
|
pub meta_description: String,
|
||||||
|
pub description: HashMap<String, String>,
|
||||||
|
pub copyright_author: String,
|
||||||
|
pub aboutme: Option<AboutMeConfig>,
|
||||||
|
pub gpg: Option<GPGConfig>,
|
||||||
|
pub projects: Option<ProjectsSectionConfig>,
|
||||||
|
pub contacts: Option<ContactsSectionConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct AboutMeConfig {
|
||||||
|
pub title: HashMap<String, String>,
|
||||||
|
pub text: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct GPGConfig {
|
||||||
|
pub title: HashMap<String, String>,
|
||||||
|
pub showkey_title: HashMap<String, String>,
|
||||||
|
pub availat_title: HashMap<String, String>,
|
||||||
|
pub copy_title: HashMap<String, String>,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ProjectsSectionConfig {
|
||||||
|
pub title: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ContactsSectionConfig {
|
||||||
|
pub title: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ProjectConfig {
|
||||||
|
pub title: String,
|
||||||
|
pub url: String,
|
||||||
|
pub source_url: String,
|
||||||
|
pub source_url_title: String,
|
||||||
|
pub description: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ContactConfig {
|
||||||
|
pub site_name: HashMap<String, String>,
|
||||||
|
pub title: String,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct LanguageConfig {
|
||||||
|
pub key: String,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
3
src/handlers/mod.rs
Normal file
3
src/handlers/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod root;
|
||||||
|
|
||||||
|
pub use root::{gpg_handler, root_handler};
|
||||||
105
src/handlers/root.rs
Normal file
105
src/handlers/root.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
use askama::Template;
|
||||||
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
http::{HeaderMap, Request},
|
||||||
|
response::{Html, IntoResponse},
|
||||||
|
};
|
||||||
|
use chrono::Datelike;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::client::ClientType;
|
||||||
|
use crate::client::detect_client_type;
|
||||||
|
use crate::config::load_config;
|
||||||
|
use crate::language::detect_user_language;
|
||||||
|
use crate::templates::{build_html_template, build_text_template};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct RootParams {
|
||||||
|
pub lang: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_base_url(request: &Request<axum::body::Body>) -> String {
|
||||||
|
let protocol = request
|
||||||
|
.headers()
|
||||||
|
.get("x-forwarded-proto")
|
||||||
|
.and_then(|p| p.to_str().ok())
|
||||||
|
.or_else(|| request.uri().scheme_str())
|
||||||
|
.unwrap_or("http");
|
||||||
|
|
||||||
|
let host = request
|
||||||
|
.headers()
|
||||||
|
.get("x-forwarded-host")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.or_else(|| request.headers().get("host").and_then(|h| h.to_str().ok()))
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
|
format!("{}://{}", protocol, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn root_handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Query(params): Query<RootParams>,
|
||||||
|
request: Request<axum::body::Body>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let config = match load_config(state.config_path.clone()) {
|
||||||
|
Ok(cfg) => cfg,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load config: {}", e);
|
||||||
|
return "Internal Server Error".into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let accept_language = headers
|
||||||
|
.get(axum::http::header::ACCEPT_LANGUAGE)
|
||||||
|
.and_then(|v| v.to_str().ok());
|
||||||
|
|
||||||
|
let lang = detect_user_language(params.lang, accept_language, &config.languages);
|
||||||
|
|
||||||
|
let year = chrono::Utc::now().year().to_string();
|
||||||
|
let base_url = get_base_url(&request);
|
||||||
|
|
||||||
|
match detect_client_type(&headers) {
|
||||||
|
ClientType::Browser => {
|
||||||
|
match build_html_template(
|
||||||
|
&config,
|
||||||
|
&lang,
|
||||||
|
&base_url,
|
||||||
|
&year,
|
||||||
|
&state.css_url,
|
||||||
|
&state.script_js_url,
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
{
|
||||||
|
Ok(html) => Html(html).into_response(),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to render HTML template: {}", e);
|
||||||
|
"Internal Server Error".into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientType::CliLike | ClientType::Unknown => {
|
||||||
|
let text = build_text_template(&config, &lang, &base_url, &year)
|
||||||
|
.render()
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Failed to render text template: {}", e);
|
||||||
|
"Internal Server Error".to_string()
|
||||||
|
});
|
||||||
|
format!("{}\n", text).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn gpg_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let config = match load_config(state.config_path.clone()) {
|
||||||
|
Ok(cfg) => cfg,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load config: {}", e);
|
||||||
|
return "Internal Server Error".into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config.page.gpg.unwrap().value.into_response()
|
||||||
|
}
|
||||||
67
src/language/detector.rs
Normal file
67
src/language/detector.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
use crate::config::LanguageConfig;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
struct LanguageWeight {
|
||||||
|
code: String,
|
||||||
|
weight: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_user_language(
|
||||||
|
query_lang: Option<String>,
|
||||||
|
accept_language_header: Option<&str>,
|
||||||
|
supported_languages: &[LanguageConfig],
|
||||||
|
) -> String {
|
||||||
|
if let Some(lang) = query_lang {
|
||||||
|
if supported_languages.iter().any(|l| l.key == lang) {
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(accept_lang) = accept_language_header {
|
||||||
|
let languages = parse_accept_language(accept_lang);
|
||||||
|
|
||||||
|
for lang_weight in languages {
|
||||||
|
if supported_languages
|
||||||
|
.iter()
|
||||||
|
.any(|l| l.key == lang_weight.code)
|
||||||
|
{
|
||||||
|
return lang_weight.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
supported_languages
|
||||||
|
.first()
|
||||||
|
.map(|l| l.key.clone())
|
||||||
|
.unwrap_or_else(|| "en".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_accept_language(header: &str) -> Vec<LanguageWeight> {
|
||||||
|
let mut languages: Vec<LanguageWeight> = Vec::new();
|
||||||
|
|
||||||
|
for part in header.split(',') {
|
||||||
|
let trimmed = part.trim();
|
||||||
|
let parts: Vec<&str> = trimmed.split(';').collect();
|
||||||
|
|
||||||
|
let lang_code = parts[0];
|
||||||
|
let main_lang = lang_code.split('-').next().unwrap_or(lang_code);
|
||||||
|
|
||||||
|
let weight = if parts.len() > 1 {
|
||||||
|
parts[1]
|
||||||
|
.trim()
|
||||||
|
.strip_prefix("q=")
|
||||||
|
.and_then(|q| q.parse::<f32>().ok())
|
||||||
|
.unwrap_or(1.0)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
languages.push(LanguageWeight {
|
||||||
|
code: main_lang.to_string(),
|
||||||
|
weight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
languages.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap());
|
||||||
|
languages
|
||||||
|
}
|
||||||
3
src/language/mod.rs
Normal file
3
src/language/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod detector;
|
||||||
|
|
||||||
|
pub use detector::detect_user_language;
|
||||||
567
src/main.rs
567
src/main.rs
@ -1,477 +1,134 @@
|
|||||||
use askama::Template;
|
mod client;
|
||||||
use axum::{
|
mod config;
|
||||||
Router,
|
mod handlers;
|
||||||
extract::Query,
|
mod language;
|
||||||
http::HeaderMap,
|
mod templates;
|
||||||
http::header::{ACCEPT, USER_AGENT},
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
routing::get,
|
|
||||||
};
|
|
||||||
use chrono::Datelike;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
use axum::Router;
|
||||||
struct Config {
|
use axum::extract::State;
|
||||||
page: PageConfig,
|
use axum::response::IntoResponse;
|
||||||
languages: Vec<LanguageConfig>,
|
use axum::routing::get;
|
||||||
projects: Option<Vec<ProjectConfig>>,
|
use clap::Parser;
|
||||||
contacts: Option<Vec<ContactConfig>>,
|
use handlers::{gpg_handler, root_handler};
|
||||||
}
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
const STATIC_JS: &'static str = include_str!("../assets/script.js");
|
||||||
struct PageConfig {
|
const STATIC_CSS: &'static str = include_str!("../assets/style.css");
|
||||||
title: HashMap<String, String>, // теперь HashMap
|
|
||||||
meta_title: String,
|
|
||||||
meta_description: String,
|
|
||||||
description: HashMap<String, String>, // теперь HashMap
|
|
||||||
copyright_author: String,
|
|
||||||
aboutme: Option<AboutMeConfig>,
|
|
||||||
gpg: Option<GPGConfig>,
|
|
||||||
projects: Option<ProjectsSectionConfig>,
|
|
||||||
contacts: Option<ContactsSectionConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Parser, Debug)]
|
||||||
struct AboutMeConfig {
|
#[command(author, version, about, long_about = None)]
|
||||||
title: HashMap<String, String>,
|
struct Args {
|
||||||
text: HashMap<String, String>,
|
/// Path to configuration file
|
||||||
}
|
#[arg(short, long)]
|
||||||
|
config: std::path::PathBuf,
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
/// Network port to use
|
||||||
struct GPGConfig {
|
#[arg(short, long, value_parser = port_in_range, value_name = "PORT", default_value = "8080")]
|
||||||
title: HashMap<String, String>,
|
port: u16,
|
||||||
url: String,
|
|
||||||
url_title: String,
|
|
||||||
showkey_title: HashMap<String, String>,
|
|
||||||
availat_title: HashMap<String, String>,
|
|
||||||
copy_title: HashMap<String, String>,
|
|
||||||
value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
/// Host to listen
|
||||||
struct ProjectsSectionConfig {
|
#[arg(long, default_value = "127.0.0.1")]
|
||||||
title: HashMap<String, String>,
|
host: String,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct ContactsSectionConfig {
|
|
||||||
title: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct ProjectConfig {
|
|
||||||
title: String,
|
|
||||||
url: String,
|
|
||||||
source_url: String,
|
|
||||||
source_url_title: String,
|
|
||||||
description: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct ContactConfig {
|
|
||||||
site_name: HashMap<String, String>,
|
|
||||||
title: String,
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct LanguageConfig {
|
|
||||||
key: String,
|
|
||||||
title: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template structures matching the HTML
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "html_index.jinja2")]
|
|
||||||
struct HtmlPageTemplate<'a> {
|
|
||||||
title: &'a str,
|
|
||||||
description: &'a str,
|
|
||||||
copyright_author: &'a str,
|
|
||||||
base_url: &'a str,
|
|
||||||
meta: PageMeta<'a>,
|
|
||||||
year: &'a str,
|
|
||||||
aboutme: AboutMeSection<'a>,
|
|
||||||
gpg_section: GPGSection<'a>,
|
|
||||||
contacts_section: ContactsSection<'a>,
|
|
||||||
projects_section: ProjectsSection<'a>,
|
|
||||||
languages: Vec<Language<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "text_index.jinja2", escape = "none")]
|
|
||||||
struct TextPageTemplate<'a> {
|
|
||||||
title: &'a str,
|
|
||||||
description: &'a str,
|
|
||||||
copyright_author: &'a str,
|
|
||||||
base_url: &'a str,
|
|
||||||
year: &'a str,
|
|
||||||
aboutme_section: AboutMeSection<'a>,
|
|
||||||
gpg_section: GPGSection<'a>,
|
|
||||||
contacts_section: ContactsSection<'a>,
|
|
||||||
projects_section: ProjectsSection<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct PageMeta<'a> {
|
|
||||||
title: &'a str,
|
|
||||||
description: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct AboutMeSection<'a> {
|
|
||||||
title: &'a str,
|
|
||||||
text: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct GPGSection<'a> {
|
|
||||||
title: &'a str,
|
|
||||||
availat_title: &'a str,
|
|
||||||
url: &'a str,
|
|
||||||
url_title: &'a str,
|
|
||||||
showkey_title: &'a str,
|
|
||||||
copy_title: &'a str,
|
|
||||||
value: &'a str,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ContactsSection<'a> {
|
pub struct AppState {
|
||||||
title: &'a str,
|
config_path: std::path::PathBuf,
|
||||||
contacts: Vec<Contact<'a>>,
|
css_content: &'static str,
|
||||||
|
js_content: &'static str,
|
||||||
|
css_url: String,
|
||||||
|
script_js_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
const PORT_RANGE: RangeInclusive<usize> = 1..=65535;
|
||||||
struct Contact<'a> {
|
|
||||||
site_name: &'a str,
|
|
||||||
url: &'a str,
|
|
||||||
title: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
fn port_in_range(s: &str) -> Result<u16, String> {
|
||||||
struct ProjectsSection<'a> {
|
let port: usize = s
|
||||||
title: &'a str,
|
.parse()
|
||||||
projects: Vec<Project<'a>>,
|
.map_err(|_| format!("`{s}` isn't a port number"))?;
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
if PORT_RANGE.contains(&port) {
|
||||||
struct Project<'a> {
|
Ok(port as u16)
|
||||||
title: &'a str,
|
|
||||||
url: &'a str,
|
|
||||||
source_url: &'a str,
|
|
||||||
source_url_title: &'a str,
|
|
||||||
description: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Language<'a> {
|
|
||||||
key: &'a str,
|
|
||||||
current: bool,
|
|
||||||
value: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
|
|
||||||
let contents = fs::read_to_string(path)?;
|
|
||||||
let config: Config = toml::from_str(&contents)?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_aboutme_section<'a>(config: &'a Config, lang_key: &'a str) -> AboutMeSection<'a> {
|
|
||||||
AboutMeSection {
|
|
||||||
title: config
|
|
||||||
.page
|
|
||||||
.aboutme
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|a| a.title.get(lang_key))
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
text: config
|
|
||||||
.page
|
|
||||||
.aboutme
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|a| a.text.get(lang_key))
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_gpg_section<'a>(config: &'a Config, lang_key: &'a str) -> GPGSection<'a> {
|
|
||||||
let gpg_config = config.page.gpg.as_ref().unwrap();
|
|
||||||
GPGSection {
|
|
||||||
title: gpg_config
|
|
||||||
.title
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
availat_title: gpg_config
|
|
||||||
.availat_title
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
url: &gpg_config.url,
|
|
||||||
url_title: &gpg_config.url_title,
|
|
||||||
showkey_title: gpg_config
|
|
||||||
.showkey_title
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
copy_title: gpg_config
|
|
||||||
.copy_title
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
value: &gpg_config.value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_contacts_section<'a>(config: &'a Config, lang_key: &'a str) -> ContactsSection<'a> {
|
|
||||||
let contacts_config = config.page.contacts.as_ref().unwrap();
|
|
||||||
let contacts: Vec<Contact<'a>> = config
|
|
||||||
.contacts
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.map(|contact| Contact {
|
|
||||||
site_name: contact
|
|
||||||
.site_name
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
url: &contact.url,
|
|
||||||
title: &contact.title,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
ContactsSection {
|
|
||||||
title: contacts_config
|
|
||||||
.title
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
contacts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_projects_section<'a>(config: &'a Config, lang_key: &'a str) -> ProjectsSection<'a> {
|
|
||||||
let projects_config = config.page.projects.as_ref().unwrap();
|
|
||||||
let projects: Vec<Project<'a>> = config
|
|
||||||
.projects
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.map(|project| Project {
|
|
||||||
title: &project.title,
|
|
||||||
url: &project.url,
|
|
||||||
source_url: &project.source_url,
|
|
||||||
source_url_title: &project.source_url_title,
|
|
||||||
description: project
|
|
||||||
.description
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
ProjectsSection {
|
|
||||||
title: projects_config
|
|
||||||
.title
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
projects,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_languages<'a>(config: &'a Config, lang_key: &'a str) -> Vec<Language<'a>> {
|
|
||||||
config
|
|
||||||
.languages
|
|
||||||
.iter()
|
|
||||||
.map(|lang| Language {
|
|
||||||
key: &lang.key,
|
|
||||||
current: lang.key == lang_key,
|
|
||||||
value: &lang.title,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_html_template<'a>(
|
|
||||||
config: &'a Config,
|
|
||||||
lang_key: &'a str,
|
|
||||||
base_url: &'a str,
|
|
||||||
year: &'a str,
|
|
||||||
) -> HtmlPageTemplate<'a> {
|
|
||||||
let meta = PageMeta {
|
|
||||||
title: &config.page.meta_title,
|
|
||||||
description: &config.page.meta_description,
|
|
||||||
};
|
|
||||||
|
|
||||||
HtmlPageTemplate {
|
|
||||||
title: config
|
|
||||||
.page
|
|
||||||
.title
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
description: config
|
|
||||||
.page
|
|
||||||
.description
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
copyright_author: &config.page.copyright_author,
|
|
||||||
base_url,
|
|
||||||
meta,
|
|
||||||
year,
|
|
||||||
aboutme: build_aboutme_section(config, lang_key),
|
|
||||||
gpg_section: build_gpg_section(config, lang_key),
|
|
||||||
contacts_section: build_contacts_section(config, lang_key),
|
|
||||||
projects_section: build_projects_section(config, lang_key),
|
|
||||||
languages: build_languages(config, lang_key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_text_template<'a>(
|
|
||||||
config: &'a Config,
|
|
||||||
lang_key: &'a str,
|
|
||||||
base_url: &'a str,
|
|
||||||
year: &'a str,
|
|
||||||
) -> TextPageTemplate<'a> {
|
|
||||||
TextPageTemplate {
|
|
||||||
title: config
|
|
||||||
.page
|
|
||||||
.title
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
description: config
|
|
||||||
.page
|
|
||||||
.description
|
|
||||||
.get(lang_key)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
copyright_author: &config.page.copyright_author,
|
|
||||||
base_url,
|
|
||||||
year,
|
|
||||||
aboutme_section: build_aboutme_section(config, lang_key),
|
|
||||||
gpg_section: build_gpg_section(config, lang_key),
|
|
||||||
contacts_section: build_contacts_section(config, lang_key),
|
|
||||||
projects_section: build_projects_section(config, lang_key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
enum ClientType {
|
|
||||||
Browser,
|
|
||||||
CliLike, // curl, wget, httpie и т.д.
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detect_client_type(headers: &HeaderMap) -> ClientType {
|
|
||||||
let user_agent = headers
|
|
||||||
.get(USER_AGENT)
|
|
||||||
.and_then(|v| v.to_str().ok())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
let user_agent_lower = user_agent.to_lowercase();
|
|
||||||
|
|
||||||
// Проверка Accept header
|
|
||||||
let accept = headers
|
|
||||||
.get(ACCEPT)
|
|
||||||
.and_then(|v| v.to_str().ok())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
let accept_lower = accept.to_lowercase();
|
|
||||||
|
|
||||||
// CLI утилиты обычно имеют простой Accept или не имеют его вовсе
|
|
||||||
let is_likely_cli = accept_lower.is_empty()
|
|
||||||
|| accept_lower == "*/*"
|
|
||||||
|| accept_lower == "application/json"
|
|
||||||
|| accept_lower == "text/plain";
|
|
||||||
|
|
||||||
// Браузеры всегда запрашивают text/html в первую очередь
|
|
||||||
let is_likely_browser = accept_lower.contains("text/html")
|
|
||||||
&& (accept_lower.contains("application/xhtml+xml")
|
|
||||||
|| accept_lower.contains("application/xml")
|
|
||||||
|| accept_lower.contains("*/*"));
|
|
||||||
|
|
||||||
// Комбинированная проверка: User-Agent + Accept
|
|
||||||
if user_agent_lower.contains("curl")
|
|
||||||
|| user_agent_lower.contains("wget")
|
|
||||||
|| user_agent_lower.contains("httpie")
|
|
||||||
|| user_agent_lower.contains("python-requests")
|
|
||||||
|| user_agent_lower.contains("go-http-client")
|
|
||||||
|| (is_likely_cli && !user_agent_lower.contains("mozilla/"))
|
|
||||||
{
|
|
||||||
return ClientType::CliLike;
|
|
||||||
}
|
|
||||||
|
|
||||||
if user_agent_lower.contains("mozilla/")
|
|
||||||
&& (user_agent_lower.contains("chrome")
|
|
||||||
|| user_agent_lower.contains("safari")
|
|
||||||
|| user_agent_lower.contains("firefox")
|
|
||||||
|| user_agent_lower.contains("edg"))
|
|
||||||
&& is_likely_browser
|
|
||||||
{
|
|
||||||
return ClientType::Browser;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback с использованием только Accept header
|
|
||||||
if is_likely_browser {
|
|
||||||
ClientType::Browser
|
|
||||||
} else if is_likely_cli {
|
|
||||||
ClientType::CliLike
|
|
||||||
} else {
|
} else {
|
||||||
ClientType::Unknown
|
Err(format!(
|
||||||
}
|
"port not in range {}-{}",
|
||||||
}
|
PORT_RANGE.start(),
|
||||||
|
PORT_RANGE.end()
|
||||||
#[derive(Debug, Deserialize)]
|
))
|
||||||
pub struct RootParams {
|
|
||||||
pub lang: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn root_handler(headers: HeaderMap, Query(params): Query<RootParams>) -> impl IntoResponse {
|
|
||||||
let query_lang = params.lang.unwrap_or("en".to_string());
|
|
||||||
|
|
||||||
let config = load_config("docs/config_example.toml").unwrap();
|
|
||||||
|
|
||||||
let lang_exists = config
|
|
||||||
.languages
|
|
||||||
.iter()
|
|
||||||
.any(|lang_config| lang_config.key == query_lang);
|
|
||||||
|
|
||||||
let lang = if lang_exists {
|
|
||||||
query_lang.as_str()
|
|
||||||
} else {
|
|
||||||
"en"
|
|
||||||
};
|
|
||||||
|
|
||||||
let year = chrono::Utc::now().year().to_string();
|
|
||||||
|
|
||||||
match detect_client_type(&headers) {
|
|
||||||
ClientType::Browser => Html(
|
|
||||||
build_html_template(&config, lang, "https://thek4n.ru", year.as_str())
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
ClientType::CliLike | ClientType::Unknown => format!(
|
|
||||||
"{}\n",
|
|
||||||
build_text_template(&config, lang, "https://thek4n.ru", year.as_str())
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let app = Router::new().route("/", get(root_handler));
|
let args = Args::parse();
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
let css_hash = calculate_sha256(STATIC_CSS);
|
||||||
|
let js_hash = calculate_sha256(STATIC_JS);
|
||||||
|
|
||||||
axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app)
|
let css_path = format!("/style-{css_hash}.js");
|
||||||
|
let js_path = format!("/script-{js_hash}.js");
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
config_path: args.config,
|
||||||
|
css_content: STATIC_CSS,
|
||||||
|
js_content: STATIC_JS,
|
||||||
|
css_url: css_path.clone(),
|
||||||
|
script_js_url: js_path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(root_handler))
|
||||||
|
.route("/gpgkey.txt", get(gpg_handler))
|
||||||
|
.route(css_path.as_str(), get(static_css_handler))
|
||||||
|
.route(js_path.as_str(), get(static_js_handler))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let addr = format!("{}:{}", args.host, args.port)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.expect("Fail to get address")
|
||||||
|
.next()
|
||||||
|
.ok_or("failed to resolve address")
|
||||||
|
.expect("");
|
||||||
|
|
||||||
|
println!("Server running on http://{}", addr);
|
||||||
|
|
||||||
|
axum::serve(
|
||||||
|
tokio::net::TcpListener::bind(addr)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.expect("Failed to bind to address"),
|
||||||
|
app,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Server failed to start");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn static_css_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let headers = [
|
||||||
|
(axum::http::header::CONTENT_TYPE, mime::CSS.as_ref()),
|
||||||
|
(axum::http::header::CACHE_CONTROL, "public, max-age=86400"),
|
||||||
|
];
|
||||||
|
|
||||||
|
(headers, state.css_content).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn static_js_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let headers = [
|
||||||
|
(
|
||||||
|
axum::http::header::CONTENT_TYPE,
|
||||||
|
mime::APPLICATION_JAVASCRIPT.as_ref(),
|
||||||
|
),
|
||||||
|
(axum::http::header::CACHE_CONTROL, "public, max-age=86400"),
|
||||||
|
];
|
||||||
|
|
||||||
|
(headers, state.js_content).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_sha256(input: &str) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(input.as_bytes());
|
||||||
|
let result = hasher.finalize();
|
||||||
|
|
||||||
|
hex::encode(result)[..6].to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
236
src/templates/builders.rs
Normal file
236
src/templates/builders.rs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use askama::Template;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "html_index.jinja2")]
|
||||||
|
pub struct HtmlPageTemplate<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub description: &'a str,
|
||||||
|
pub copyright_author: &'a str,
|
||||||
|
pub base_url: &'a str,
|
||||||
|
pub meta: PageMeta<'a>,
|
||||||
|
pub year: &'a str,
|
||||||
|
pub aboutme: AboutMeSection<'a>,
|
||||||
|
pub gpg_section: GPGSection<'a>,
|
||||||
|
pub contacts_section: ContactsSection<'a>,
|
||||||
|
pub projects_section: ProjectsSection<'a>,
|
||||||
|
pub languages: Vec<Language<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "text_index.jinja2", escape = "none")]
|
||||||
|
pub struct TextPageTemplate<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub description: &'a str,
|
||||||
|
pub copyright_author: &'a str,
|
||||||
|
pub base_url: &'a str,
|
||||||
|
pub year: &'a str,
|
||||||
|
pub aboutme_section: AboutMeSection<'a>,
|
||||||
|
pub gpg_section: GPGSection<'a>,
|
||||||
|
pub contacts_section: ContactsSection<'a>,
|
||||||
|
pub projects_section: ProjectsSection<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct PageMeta<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub description: &'a str,
|
||||||
|
pub css_url: &'a str,
|
||||||
|
pub script_js_url: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct AboutMeSection<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub text: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct GPGSection<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub availat_title: &'a str,
|
||||||
|
pub showkey_title: &'a str,
|
||||||
|
pub copy_title: &'a str,
|
||||||
|
pub value: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ContactsSection<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub contacts: Vec<Contact<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Contact<'a> {
|
||||||
|
pub site_name: &'a str,
|
||||||
|
pub url: &'a str,
|
||||||
|
pub title: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProjectsSection<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub projects: Vec<Project<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Project<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub url: &'a str,
|
||||||
|
pub source_url: &'a str,
|
||||||
|
pub source_url_title: &'a str,
|
||||||
|
pub description: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Language<'a> {
|
||||||
|
pub key: &'a str,
|
||||||
|
pub current: bool,
|
||||||
|
pub value: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательная функция для безопасного получения значения из HashMap
|
||||||
|
fn get_localized_string<'a>(map: &'a HashMap<String, String>, key: &str) -> &'a str {
|
||||||
|
map.get(key).map(|s| s.as_str()).unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_html_template<'a>(
|
||||||
|
config: &'a Config,
|
||||||
|
lang_key: &'a str,
|
||||||
|
base_url: &'a str,
|
||||||
|
year: &'a str,
|
||||||
|
css_url: &'a str,
|
||||||
|
script_js_url: &'a str,
|
||||||
|
) -> HtmlPageTemplate<'a> {
|
||||||
|
let meta = PageMeta {
|
||||||
|
title: &config.page.meta_title,
|
||||||
|
description: &config.page.meta_description,
|
||||||
|
css_url,
|
||||||
|
script_js_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
HtmlPageTemplate {
|
||||||
|
title: get_localized_string(&config.page.title, lang_key),
|
||||||
|
description: get_localized_string(&config.page.description, lang_key),
|
||||||
|
copyright_author: &config.page.copyright_author,
|
||||||
|
base_url,
|
||||||
|
meta,
|
||||||
|
year,
|
||||||
|
aboutme: build_aboutme_section(config, lang_key),
|
||||||
|
gpg_section: build_gpg_section(config, lang_key),
|
||||||
|
contacts_section: build_contacts_section(config, lang_key),
|
||||||
|
projects_section: build_projects_section(config, lang_key),
|
||||||
|
languages: build_languages(config, lang_key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_text_template<'a>(
|
||||||
|
config: &'a Config,
|
||||||
|
lang_key: &'a str,
|
||||||
|
base_url: &'a str,
|
||||||
|
year: &'a str,
|
||||||
|
) -> TextPageTemplate<'a> {
|
||||||
|
TextPageTemplate {
|
||||||
|
title: get_localized_string(&config.page.title, lang_key),
|
||||||
|
description: get_localized_string(&config.page.description, lang_key),
|
||||||
|
copyright_author: &config.page.copyright_author,
|
||||||
|
base_url,
|
||||||
|
year,
|
||||||
|
aboutme_section: build_aboutme_section(config, lang_key),
|
||||||
|
gpg_section: build_gpg_section(config, lang_key),
|
||||||
|
contacts_section: build_contacts_section(config, lang_key),
|
||||||
|
projects_section: build_projects_section(config, lang_key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_aboutme_section<'a>(config: &'a Config, lang_key: &'a str) -> AboutMeSection<'a> {
|
||||||
|
AboutMeSection {
|
||||||
|
title: config
|
||||||
|
.page
|
||||||
|
.aboutme
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| get_localized_string(&a.title, lang_key))
|
||||||
|
.unwrap_or(""),
|
||||||
|
text: config
|
||||||
|
.page
|
||||||
|
.aboutme
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| get_localized_string(&a.text, lang_key))
|
||||||
|
.unwrap_or(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_gpg_section<'a>(config: &'a Config, lang_key: &'a str) -> GPGSection<'a> {
|
||||||
|
let gpg_config = config.page.gpg.as_ref().expect("GPG config is required");
|
||||||
|
|
||||||
|
GPGSection {
|
||||||
|
title: get_localized_string(&gpg_config.title, lang_key),
|
||||||
|
availat_title: get_localized_string(&gpg_config.availat_title, lang_key),
|
||||||
|
showkey_title: get_localized_string(&gpg_config.showkey_title, lang_key),
|
||||||
|
copy_title: get_localized_string(&gpg_config.copy_title, lang_key),
|
||||||
|
value: &gpg_config.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_contacts_section<'a>(config: &'a Config, lang_key: &'a str) -> ContactsSection<'a> {
|
||||||
|
let contacts_config = config
|
||||||
|
.page
|
||||||
|
.contacts
|
||||||
|
.as_ref()
|
||||||
|
.expect("Contacts config is required");
|
||||||
|
let contacts = config
|
||||||
|
.contacts
|
||||||
|
.as_ref()
|
||||||
|
.expect("Contacts list is required")
|
||||||
|
.iter()
|
||||||
|
.map(|contact| Contact {
|
||||||
|
site_name: get_localized_string(&contact.site_name, lang_key),
|
||||||
|
url: &contact.url,
|
||||||
|
title: &contact.title,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
ContactsSection {
|
||||||
|
title: get_localized_string(&contacts_config.title, lang_key),
|
||||||
|
contacts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_projects_section<'a>(config: &'a Config, lang_key: &'a str) -> ProjectsSection<'a> {
|
||||||
|
let projects_config = config
|
||||||
|
.page
|
||||||
|
.projects
|
||||||
|
.as_ref()
|
||||||
|
.expect("Projects config is required");
|
||||||
|
let projects = config
|
||||||
|
.projects
|
||||||
|
.as_ref()
|
||||||
|
.expect("Projects list is required")
|
||||||
|
.iter()
|
||||||
|
.map(|project| Project {
|
||||||
|
title: &project.title,
|
||||||
|
url: &project.url,
|
||||||
|
source_url: &project.source_url,
|
||||||
|
source_url_title: &project.source_url_title,
|
||||||
|
description: get_localized_string(&project.description, lang_key),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
ProjectsSection {
|
||||||
|
title: get_localized_string(&projects_config.title, lang_key),
|
||||||
|
projects,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_languages<'a>(config: &'a Config, lang_key: &'a str) -> Vec<Language<'a>> {
|
||||||
|
config
|
||||||
|
.languages
|
||||||
|
.iter()
|
||||||
|
.map(|lang| Language {
|
||||||
|
key: &lang.key,
|
||||||
|
current: lang.key == lang_key,
|
||||||
|
value: &lang.title,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
3
src/templates/mod.rs
Normal file
3
src/templates/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod builders;
|
||||||
|
|
||||||
|
pub use builders::{build_html_template, build_text_template};
|
||||||
@ -6,7 +6,7 @@
|
|||||||
<title>{{ meta.title }}</title>
|
<title>{{ meta.title }}</title>
|
||||||
<meta name="description" content="{{ meta.description }}">
|
<meta name="description" content="{{ meta.description }}">
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="{{ meta.css_url }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
@ -39,28 +39,6 @@
|
|||||||
<p>{{ aboutme.text }}</p>
|
<p>{{ aboutme.text }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>{{ gpg_section.title }}</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span>{{ gpg_section.availat_title }}</span>
|
|
||||||
<a href="{{ gpg_section.url }}" target="_blank" rel="noopener noreferrer" >{{ gpg_section.url_title }}</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<details class="gpg-details">
|
|
||||||
<summary>
|
|
||||||
<span class="summary-row">
|
|
||||||
{{ gpg_section.showkey_title }}
|
|
||||||
<a id="copy-gpg" class="copy-link" href="#" onclick="copyGPGKey()">{{ gpg_section.copy_title }}</a>
|
|
||||||
</span>
|
|
||||||
<code>
|
|
||||||
{{ gpg_section.value|linebreaksbr }}
|
|
||||||
</code>
|
|
||||||
</summary>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>{{ contacts_section.title }}</h2>
|
<h2>{{ contacts_section.title }}</h2>
|
||||||
{% for contact in contacts_section.contacts %}
|
{% for contact in contacts_section.contacts %}
|
||||||
@ -72,11 +50,30 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>{{ gpg_section.title }}</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<span>{{ gpg_section.availat_title }}</span>
|
||||||
|
<a href="{{ base_url }}/gpgkey.txt" target="_blank" rel="noopener noreferrer" >{{ base_url }}/gpgkey.txt</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details class="gpg-details">
|
||||||
|
<summary>
|
||||||
|
<span class="summary-row">
|
||||||
|
{{ gpg_section.showkey_title }}
|
||||||
|
<a id="copy-gpg" class="copy-link" href="#" onclick="copyGPGKey()">{{ gpg_section.copy_title }}</a>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<pre id="gpg-key">{{ gpg_section.value|linebreaksbr }}</pre>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© {{ year }} {{ copyright_author }}</p>
|
<p>© {{ year }} {{ copyright_author }}</p>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="{{ meta.script_js_url }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -7,18 +7,16 @@
|
|||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
|
|
||||||
{{ contacts_section.title }}:
|
|
||||||
{%- for contact in contacts_section.contacts %}
|
|
||||||
> {{ contact.site_name }}: {{ contact.title }}
|
|
||||||
{%- endfor %}
|
|
||||||
|
|
||||||
|
|
||||||
{{ gpg_section.title }}: {{ base_url }}{{ gpg_section.url }}
|
|
||||||
|
|
||||||
|
|
||||||
{{ aboutme_section.title }}:
|
{{ aboutme_section.title }}:
|
||||||
{{ aboutme_section.text }}
|
{{ aboutme_section.text }}
|
||||||
|
|
||||||
|
|
||||||
|
{{ contacts_section.title }}:
|
||||||
|
{%- for contact in contacts_section.contacts %}
|
||||||
|
- {{ contact.site_name }}: {{ contact.title }}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
{{ gpg_section.title }}: {{ base_url }}/gpgkey.txt
|
||||||
|
|
||||||
|
|
||||||
© {{ year }} {{ copyright_author }}
|
© {{ year }} {{ copyright_author }}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user