Compare commits
9 Commits
1065bd1956
...
master
Author | SHA1 | Date | |
---|---|---|---|
93a69fd89b
|
|||
abe2171e19
|
|||
6bfc272c46
|
|||
a2a6933058
|
|||
e90dfa0b0b
|
|||
4af8c825bb
|
|||
96bf9303ea
|
|||
06b389b28f
|
|||
ce49dd6e3d
|
615
Cargo.lock
generated
615
Cargo.lock
generated
@@ -17,6 +17,63 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"itoa",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
"memchr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@@ -44,24 +101,79 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -74,6 +186,15 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -140,9 +261,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
@@ -231,6 +352,169 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_locid_transform_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform_data"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locid_transform",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_provider_macros",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_macros"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
@@ -247,12 +531,28 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
@@ -263,6 +563,12 @@ dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
@@ -289,6 +595,15 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
@@ -327,6 +642,12 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -372,6 +693,24 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -402,6 +741,18 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
@@ -411,6 +762,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
@@ -445,6 +802,12 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
@@ -456,16 +819,41 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theseus-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"chrono",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -588,6 +976,29 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@@ -603,6 +1014,123 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -678,9 +1206,88 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5"
|
||||
checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
@@ -1,3 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["lib/search_and_replace", "theseus-server"]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
|
14
Makefile
14
Makefile
@@ -4,11 +4,17 @@ SRCS_RUST_THESEUS_SERVER := $(shell find theseus-server -type f)
|
||||
build: \
|
||||
dst/release/theseus-server
|
||||
|
||||
include config/make.mk
|
||||
include client/make.mk
|
||||
|
||||
.PHONY: run
|
||||
run: dst/dev.toml
|
||||
run: dst/dev.toml $(TARGETS_CLIENT)
|
||||
cargo run --package theseus-server -- dst/dev.toml
|
||||
|
||||
dst/release/theseus-server: $(SRCS_RUST_THESEUS_SERVER)
|
||||
cargo build --package theseus-server --release
|
||||
.PHONY: clean
|
||||
clean: client_clean
|
||||
rm -rf dst
|
||||
|
||||
include config/make.mk
|
||||
dst/release/theseus-server: $(SRCS_RUST_THESEUS_SERVER) $(TARGETS_CLIENT) dst/prod.toml
|
||||
cargo build --package theseus-server --release
|
||||
touch $@
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# theseus
|
||||
|
||||
Strong, performance-first server for receiving and pre-filtering questions.
|
||||
Strong, performance-first server for receiving and pre-filtering questions.
|
||||
|
||||

|
||||
|
44
client/index.html
Normal file
44
client/index.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Ask Stallman</title>
|
||||
<link href="style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="inner">
|
||||
<h1>Ask Stallman</h1>
|
||||
</div>
|
||||
</header>
|
||||
{%- if message.len() != 0 -%}
|
||||
<div class="notification notification-{{ ntfy_class }}">
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div class="question">
|
||||
<form method="post">
|
||||
<input class="input" type="text" name="question" placeholder="Your question" autocomplete="off" maxlength="1024">
|
||||
<input class="submit" type="submit" value="Send question">
|
||||
</form>
|
||||
</div>
|
||||
<div class="info">
|
||||
<p>We kindly ask you to consider this before sending:</p>
|
||||
<ul>
|
||||
<li>
|
||||
Please write your question in English. Dr Stallman will be reading it
|
||||
unedited during the discussion block.
|
||||
</li>
|
||||
<li>Keep it kind. We are all here to learn something.</li>
|
||||
<li>
|
||||
Don't spam. We try to keep algorithmic moderation as permissive as possible.
|
||||
So we ask you to moderate yourself.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<footer>
|
||||
<p><a href="https://zumepro.cz" target="_blank">Zumepro</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,31 @@
|
||||
TARGETS_CLIENT_PAGES := index.html not_found.html
|
||||
TARGETS_CLIENT_SCRIPTS := script.js
|
||||
TARGETS_CLIENT_STYLES := style.css
|
||||
TARGETS_CLIENT := $(TARGETS_CLIENT_PAGES:%=dst/%) \
|
||||
$(TARGETS_CLIENT_SCRIPTS:%=dst/%) \
|
||||
$(TARGETS_CLIENT_STYLES:%=dst/%)
|
||||
|
||||
.PHONY: client_clean
|
||||
client_clean:
|
||||
rm -rf client/node_modules
|
||||
|
||||
client/node_modules:
|
||||
cd client && bun install
|
||||
|
||||
dst/%.html: client/%.html client/node_modules
|
||||
@mkdir -p $(@D)
|
||||
cat $< | bun run --cwd client html-minifier \
|
||||
--collapse-inline-tag-whitespace \
|
||||
--collapse-boolean-attributes \
|
||||
--collapse-whitespace \
|
||||
--remove-attribute-quotes \
|
||||
--remove-comments \
|
||||
--remove-redundant-attributes > $@
|
||||
|
||||
dst/%.css: client/%.scss client/node_modules
|
||||
@mkdir -p $(@D)
|
||||
bun run --cwd client sass $(notdir $<) --style compressed > $@
|
||||
|
||||
dst/%.js: client/%.ts client/node_modules
|
||||
@mkdir -p $(@D)
|
||||
bun build $< --minify --outfile $@
|
||||
|
21
client/not_found.html
Normal file
21
client/not_found.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Not Found | Ask Stallman</title>
|
||||
<link href="style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Ask Stallman</h1>
|
||||
</header>
|
||||
<div class="info">
|
||||
<h1>Not Found</h1>
|
||||
<p>This page was not found. Please check the URL.</p>
|
||||
</div>
|
||||
<footer>
|
||||
<p><a href="https://zumepro.cz">Zumepro</a> 2025</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
0
client/script.ts
Normal file
0
client/script.ts
Normal file
153
client/style.scss
Normal file
153
client/style.scss
Normal file
@@ -0,0 +1,153 @@
|
||||
$color-primary: white;
|
||||
$color-primary-background: black;
|
||||
$color-subtle: #908caa;
|
||||
$color-muted: #6e6a86;
|
||||
$color-highlight: #f6c177;
|
||||
$color-iris: #c4a7e7;
|
||||
$color-error: #eb6f92;
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: $color-primary;
|
||||
background-color: $color-primary-background;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-muted;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: $color-muted;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 7em;
|
||||
background-color: $color-primary-background;
|
||||
box-shadow: 0 0 10em rgba($color-subtle, .5);
|
||||
border-bottom: 1px solid rgba($color-subtle, .3);
|
||||
}
|
||||
|
||||
@keyframes notification-anim {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.notification {
|
||||
animation: notification-anim 1s forwards;
|
||||
display: block;
|
||||
margin: 2em 0;
|
||||
width: min(90%, 50em);
|
||||
color: $color-primary;
|
||||
border: 2px dashed $color-primary;
|
||||
overflow: hidden;
|
||||
padding: 1em 1em;
|
||||
border-radius: .5em;
|
||||
box-sizing: border-box;
|
||||
background-color: $color-primary-background;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 4em;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
color: $color-error;
|
||||
border-color: $color-error;
|
||||
}
|
||||
|
||||
.question {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
|
||||
form {
|
||||
width: min(90%, 50em);
|
||||
|
||||
input {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.input {
|
||||
transition: .5s ease box-shadow, .5s ease border-color;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1em 2em;
|
||||
color: $color-primary;
|
||||
background-color: transparent;
|
||||
border: 1px solid $color-iris;
|
||||
border-radius: 2em;
|
||||
outline: none;
|
||||
box-shadow: 0 0 5em rgba($color-subtle, .1);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
box-shadow: 0 0 5em $color-subtle;
|
||||
border-color: rgba($color-iris, .5);
|
||||
}
|
||||
|
||||
.submit {
|
||||
transition: .5s ease color, .5s ease background-color, .5s ease box-shadow;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 2em auto;
|
||||
margin-top: 5em;
|
||||
padding: .5em 1em;
|
||||
border-radius: 2em;
|
||||
box-sizing: border-box;
|
||||
color: $color-primary-background;
|
||||
background-color: $color-primary;
|
||||
border: 2px solid $color-primary;
|
||||
outline: none;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.submit:hover {
|
||||
color: $color-primary;
|
||||
background-color: $color-primary-background;
|
||||
}
|
||||
|
||||
.submit:focus {
|
||||
color: $color-primary;
|
||||
background-color: $color-primary-background;
|
||||
box-shadow: 0 0 2em $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
width: min(90%, 50em);
|
||||
margin: 5em auto;
|
||||
z-index: 2;
|
||||
|
||||
ul {
|
||||
padding: 1em;
|
||||
|
||||
li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 5em;
|
||||
display: flex;
|
||||
width: min(100%, 50em);
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
padding: 1em 2em;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid $color-muted;
|
||||
opacity: .6;
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
@@ -1,2 +1,12 @@
|
||||
[server]
|
||||
bind_to = "[::1]:8080"
|
||||
max_question_body_size = 25
|
||||
|
||||
[performance]
|
||||
memory_limit = 50
|
||||
|
||||
[maintenance]
|
||||
interval = 10
|
||||
|
||||
[push]
|
||||
endpoint = "http://[::1]:8081/api.php?cmd=newmodmessages"
|
||||
|
@@ -1,2 +1,7 @@
|
||||
dst/dev.toml: config/dev.toml
|
||||
@mkdir -p $(@D)
|
||||
ln -f $< $@
|
||||
|
||||
dst/prod.toml: config/prod.toml
|
||||
@mkdir -p $(@D)
|
||||
ln -f $< $@
|
||||
|
12
config/prod.toml
Normal file
12
config/prod.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[server]
|
||||
bind_to = "[::1]:8080"
|
||||
max_question_body_size = 2048
|
||||
|
||||
[performance]
|
||||
memory_limit = 536870912
|
||||
|
||||
[maintenance]
|
||||
interval = 60
|
||||
|
||||
[push]
|
||||
endpoint = "http://[::1]:9091/api.php?cmd=newmodmessages"
|
BIN
doc/screenshot.jpg
Normal file
BIN
doc/screenshot.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
@@ -3,7 +3,4 @@ name = "search_and_replace"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
[dependencies]
|
||||
|
@@ -4,9 +4,13 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
askama = "0.14.0"
|
||||
chrono = "0.4.41"
|
||||
http-body-util = "0.1.3"
|
||||
hyper = { version = "1.6.0", features = ["full"] }
|
||||
hyper-util = { version = "0.1.11", features = ["full"] }
|
||||
serde = "1.0.219"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
toml = "0.8.22"
|
||||
url = "2.5.4"
|
||||
|
2
theseus-server/askama.toml
Normal file
2
theseus-server/askama.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[general]
|
||||
dirs = ["../dst/"]
|
@@ -9,7 +9,7 @@ macro_rules! argdef {
|
||||
pub fn usage() -> &'static str {
|
||||
concat!("usage: ", $program_name, " ", $("[", stringify!($arg_id), "] "),*)
|
||||
}
|
||||
|
||||
|
||||
$(pub fn $arg_id(&self) -> &str { &self.$arg_id })*
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ macro_rules! argdef {
|
||||
}
|
||||
|
||||
argdef!{
|
||||
|
||||
|
||||
args Args("theseus-server") {
|
||||
config_path,
|
||||
}
|
||||
|
40
theseus-server/src/config.rs
Normal file
40
theseus-server/src/config.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
macro_rules! def_config {
|
||||
($(config $config_id: ident
|
||||
{ $([$namespace: ident $struct_name: ident]$($var_id: ident = $var_type: ty),*$(,)?)* })*
|
||||
) => {
|
||||
$(
|
||||
#[derive(Deserialize)]
|
||||
pub struct $config_id {
|
||||
$(pub $namespace: $struct_name),*
|
||||
}
|
||||
|
||||
$(
|
||||
#[derive(Deserialize)]
|
||||
pub struct $struct_name { $(pub $var_id: $var_type),* }
|
||||
)*
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
def_config! {
|
||||
|
||||
config Config {
|
||||
|
||||
[server Server]
|
||||
bind_to = std::net::SocketAddr, // recommended format: "<host>:<port>"
|
||||
max_question_body_size = u64 // in bytes
|
||||
|
||||
[performance Performance]
|
||||
memory_limit = usize,
|
||||
|
||||
[maintenance Maintenance]
|
||||
interval = u64, // in seconds
|
||||
|
||||
[push Push]
|
||||
endpoint = String, // recommended format: "<proto>://<host>:<port>/<path>"
|
||||
|
||||
}
|
||||
|
||||
}
|
25
theseus-server/src/logger.rs
Normal file
25
theseus-server/src/logger.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
(info $text: literal$(, $($arg: expr),*$(,)?)?) => {
|
||||
let now = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
|
||||
println!(concat!("[{}] INFO ", $text), now, $($($arg),*)?);
|
||||
};
|
||||
|
||||
(debug $text: literal$(, $($arg: expr),*$(,)?)?) => {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let now = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
|
||||
println!(concat!("[{}] DEBUG ", $text), now, $($($arg),*)?);
|
||||
}
|
||||
};
|
||||
|
||||
(err $text: literal$(, $($arg: expr),*$(,)?)?) => {
|
||||
let now = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
|
||||
println!(concat!("[{}] ERROR ", $text), now, $($($arg),*)?);
|
||||
};
|
||||
|
||||
(fatal $text: literal$(, $($arg: expr),*$(,)?)?) => {
|
||||
let now = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
|
||||
println!(concat!("[{}] !!FATAL ERROR!! ", $text), now, $($($arg),*)?);
|
||||
};
|
||||
}
|
@@ -1,16 +1,367 @@
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
use askama::Template;
|
||||
use http_body_util::{combinators::BoxBody, BodyExt, Full};
|
||||
use hyper::{body::{Bytes, Incoming}, Error, Request, Response};
|
||||
use hyper::{
|
||||
body::{Body, Bytes, Incoming},
|
||||
header::HeaderValue, server::conn::http1, service::service_fn, Error, Method, Request, Response
|
||||
};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::{net::{TcpListener, TcpStream}, sync::Mutex};
|
||||
|
||||
mod logger;
|
||||
mod args;
|
||||
mod config;
|
||||
|
||||
const CTYPE_FORM: HeaderValue = HeaderValue::from_static("application/x-www-form-urlencoded");
|
||||
|
||||
macro_rules! response {
|
||||
(@server) => { "zumepro/ask_stallman" };
|
||||
|
||||
(file $req: expr, $source: literal, $mime: literal) => {
|
||||
response!(
|
||||
$req, OK include_str!(concat!("../../dst/", $source)),
|
||||
CONTENT_TYPE: $mime,
|
||||
CACHE_CONTROL: "max-age=180, public",
|
||||
)
|
||||
};
|
||||
|
||||
(main_page $req: expr, $message_class: literal, $message: literal) => {
|
||||
response!( $req, OK MainPage {
|
||||
ntfy_class: $message_class, message: $message, prefill_question: None
|
||||
}.render()?, CONTENT_TYPE: "text/html")
|
||||
};
|
||||
|
||||
(main_page $req: expr, $message_class: literal, $message: literal$(, $prefill: expr)?) => {
|
||||
response!($req, OK MainPage {
|
||||
ntfy_class: $message_class, message: $message, prefill_question: Some($prefill)
|
||||
}.render()?, CONTENT_TYPE: "text/html")
|
||||
};
|
||||
|
||||
($req: expr, $status: ident $body: expr) => {{
|
||||
log!(info "{} \"{} {:?}\"", hyper::StatusCode::$status, $req.method(), $req.uri());
|
||||
let mut res = Response::new(Full::new(Bytes::from($body)).map_err(|n| match n {}).boxed());
|
||||
res.headers_mut().append(
|
||||
hyper::header::SERVER, hyper::header::HeaderValue::from_static(response!(@server))
|
||||
);
|
||||
*res.status_mut() = hyper::StatusCode::$status;
|
||||
res
|
||||
}};
|
||||
|
||||
($req: expr, $status: ident $body: expr, $($hkey: ident : $hval: literal),*$(,)?) => {{
|
||||
log!(info "{} \"{} {:?}\"", hyper::StatusCode::$status, $req.method(), $req.uri());
|
||||
let mut res = Response::new(Full::new(Bytes::from($body)).map_err(|n| match n {}).boxed());
|
||||
*res.status_mut() = hyper::StatusCode::$status;
|
||||
res.headers_mut().append(
|
||||
hyper::header::SERVER, hyper::header::HeaderValue::from_static(response!(@server))
|
||||
);
|
||||
$(res.headers_mut()
|
||||
.append(hyper::header::$hkey, hyper::header::HeaderValue::from_static($hval));)*
|
||||
res
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(askama::Template, Default)]
|
||||
#[template(path = "index.html")]
|
||||
struct MainPage<'a> {
|
||||
ntfy_class: &'a str,
|
||||
message: &'a str,
|
||||
prefill_question: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RouterError {
|
||||
Templating(askama::Error),
|
||||
NotImplemented,
|
||||
IO(Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RouterError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Templating(e) => write!(f, "templating: {}", e),
|
||||
Self::IO(e) => write!(f, "io: {}", e),
|
||||
Self::NotImplemented => write!(f, "not implemented"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RouterError {
|
||||
fn cause(&self) -> Option<&dyn std::error::Error> {
|
||||
match self {
|
||||
Self::Templating(e) => Some(e),
|
||||
Self::IO(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Templating(e) => Some(e),
|
||||
Self::IO(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
match self {
|
||||
Self::Templating(_) => "could not construct template",
|
||||
Self::IO(_) => "i/o error",
|
||||
Self::NotImplemented => "reached a code block not yet implemented",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for RouterError {
|
||||
fn from(value: Error) -> Self {
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<askama::Error> for RouterError {
|
||||
fn from(value: askama::Error) -> Self {
|
||||
Self::Templating(value)
|
||||
}
|
||||
}
|
||||
|
||||
async fn router(
|
||||
_: Request<Incoming>,
|
||||
) -> Result<Response<BoxBody<Bytes, Error>>, Error> {
|
||||
Ok(Response::new(Full::new("test".into()).map_err(|n| match n {}).boxed()))
|
||||
req: Request<Incoming>,
|
||||
state: Arc<SharedState>,
|
||||
) -> Result<Response<BoxBody<Bytes, Error>>, RouterError> {
|
||||
match (req.method(), req.uri().path()) {
|
||||
// pages
|
||||
(&Method::GET, "/") => Ok(response!(main_page req, "", "")),
|
||||
(&Method::POST, "/") => new_question(req, state).await,
|
||||
|
||||
// assets
|
||||
(&Method::GET, "/script.js") => Ok(response!(file req, "script.js", "text/javascript")),
|
||||
(&Method::GET, "/style.css") => Ok(response!(file req, "style.css", "text/css")),
|
||||
|
||||
_ => Ok(response!(req,
|
||||
NOT_FOUND include_str!("../../dst/not_found.html"),
|
||||
CONTENT_TYPE: "text/html",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_form<'a>(bytes: &'a [u8]) -> Result<std::borrow::Cow<'a, str>, ()> {
|
||||
let parsed = url::form_urlencoded::parse(bytes);
|
||||
for field in parsed {
|
||||
if field.0 == "question" {
|
||||
return Ok(field.1);
|
||||
}
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
|
||||
async fn new_question(
|
||||
mut req: Request<Incoming>,
|
||||
state: Arc<SharedState>,
|
||||
) -> Result<Response<BoxBody<Bytes, Error>>, RouterError> {
|
||||
// check size
|
||||
let body_size = req.body().size_hint().upper().unwrap_or(u64::MAX);
|
||||
if body_size > state.config.server.max_question_body_size {
|
||||
return Ok(response!(main_page req, "error", "Question was too long to add."));
|
||||
}
|
||||
|
||||
// check headers
|
||||
if req.headers().get(hyper::header::CONTENT_TYPE) != Some(&CTYPE_FORM) {
|
||||
return Ok(response!(main_page req,
|
||||
"error", "Your browser sent a POST request without form data. Please try again."
|
||||
));
|
||||
}
|
||||
|
||||
// get question
|
||||
let body = (&mut req).collect().await?.to_bytes();
|
||||
let Ok(question) = parse_form(&body) else {
|
||||
return Ok(response!(main_page req,
|
||||
"error", "The question your browser sent was in invalid format. Please try again."
|
||||
));
|
||||
};
|
||||
let question = question.to_string();
|
||||
if question.len() == 0 {
|
||||
return Ok(response!(main_page req,
|
||||
"info", "You sent an empty question. Please try again."
|
||||
));
|
||||
}
|
||||
|
||||
// insert question
|
||||
match state.questions.lock().await.add_new(question) {
|
||||
Ok(()) => {},
|
||||
Err(()) => return Ok(response!(main_page req,
|
||||
"error", "We got too many questions in total. So we are not accepting new ones \
|
||||
anymore. We are so sorry. :(")
|
||||
)
|
||||
}
|
||||
|
||||
Ok(response!(main_page req, "info", "Your question was successfully added."))
|
||||
}
|
||||
|
||||
struct PushEndpoint<'a> {
|
||||
uri: &'a hyper::Uri,
|
||||
host: &'a str,
|
||||
port: u16,
|
||||
addr: String,
|
||||
authority: String,
|
||||
path_and_query: String,
|
||||
}
|
||||
|
||||
impl<'a> PushEndpoint<'a> {
|
||||
fn new(uri: &'a hyper::Uri) -> Result<PushEndpoint<'a>, &'static str> {
|
||||
let host = uri.host().ok_or("no host provided")?;
|
||||
let port = uri.port_u16().ok_or("no port provided")?;
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let authority = uri.authority().ok_or("no authority provided")?.to_string();
|
||||
let mut path_and_query: String = uri.path().to_string();
|
||||
match uri.query() {
|
||||
Some(v) => path_and_query.push_str(&format!("?{}", v)),
|
||||
None => {},
|
||||
}
|
||||
Ok(Self { uri, host, port, addr, authority, path_and_query })
|
||||
}
|
||||
}
|
||||
|
||||
async fn maintenance(state: Arc<SharedState>) {
|
||||
let Ok(uri): Result<hyper::Uri, _> = state.config.push.endpoint.parse() else {
|
||||
log!(fatal "could not parse uri: {:?}", state.config.push.endpoint);
|
||||
return;
|
||||
};
|
||||
let uri = match PushEndpoint::new(&uri) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log!(fatal "could not parse endpoint address: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let interval = std::time::Duration::from_secs(state.config.maintenance.interval);
|
||||
let mut questions: HashSet<String> = HashSet::default();
|
||||
log!(debug "started maintenance routine with {:?} interval", interval);
|
||||
loop {
|
||||
log!(debug "----- MAINTENANCE -----");
|
||||
let questions_new = state.questions.lock().await.consume_to_push();
|
||||
questions.extend(questions_new);
|
||||
log!(debug "pushing {} questions", questions.len());
|
||||
match push_questions(&questions, &uri).await {
|
||||
Ok(()) => questions = HashSet::default(),
|
||||
Err(()) => { log!(debug "push failed - will try again"); },
|
||||
};
|
||||
log!(debug "----- /MAINTENANCE -----");
|
||||
tokio::time::sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn make_push_request<'a>(
|
||||
questions: &HashSet<String>, uri: &PushEndpoint<'a>
|
||||
) -> Result<hyper::Request<Full<Bytes>>, ()> {
|
||||
let Ok(body) = serde_json::to_string(questions) else { return Err(()); };
|
||||
hyper::Request::builder()
|
||||
.uri(&uri.path_and_query)
|
||||
.method(hyper::Method::POST)
|
||||
.header(hyper::header::HOST, &uri.authority)
|
||||
.header(hyper::header::CONTENT_TYPE, "application/json")
|
||||
.body(Full::new(Bytes::from(body))).map_err(|_| ())
|
||||
}
|
||||
|
||||
async fn connect_to_push_endpoint(
|
||||
addr: &String,
|
||||
) -> Result<hyper::client::conn::http1::SendRequest<Full<Bytes>>, ()> {
|
||||
let stream = TcpStream::connect(addr).await.map_err(|_| ())?;
|
||||
let (sender, conn) = hyper::client::conn::http1::handshake(
|
||||
TokioIo::new(stream)
|
||||
).await.map_err(|_| ())?;
|
||||
tokio::task::spawn(async move {
|
||||
conn.await
|
||||
});
|
||||
Ok(sender)
|
||||
}
|
||||
|
||||
async fn push_questions<'a>(questions: &HashSet<String>, uri: &PushEndpoint<'a>) -> Result<(), ()> {
|
||||
if questions.len() == 0 {
|
||||
log!(debug "skipping push - no new questions");
|
||||
return Ok(());
|
||||
}
|
||||
let Ok(mut conn) = connect_to_push_endpoint(&uri.addr).await else {
|
||||
log!(err "could not connect to push endpoint");
|
||||
return Err(());
|
||||
};
|
||||
let Ok(req) = make_push_request(questions, uri) else {
|
||||
log!(err "could not construct push questions request");
|
||||
return Err(());
|
||||
};
|
||||
let res = match conn.send_request(req).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log!(err "could not send questions request to push endpoint: {:?}", e);
|
||||
return Err(());
|
||||
},
|
||||
};
|
||||
if res.status() != hyper::StatusCode::OK {
|
||||
log!(err "got non-200 response from push endpoint");
|
||||
return Err(());
|
||||
}
|
||||
log!(info "successfully pushed new questions");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_config(path: &str) -> Result<config::Config, String> {
|
||||
let Ok(file_contents) = std::fs::read_to_string(path) else {
|
||||
return Err("could not read the config file".to_string());
|
||||
};
|
||||
toml::from_str(&file_contents)
|
||||
.map_err(|e| format!("invalid config file structure or fields: {:?}", e))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Questions {
|
||||
total_size: usize,
|
||||
max_size: usize,
|
||||
to_push: HashSet<String>,
|
||||
pushed: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Questions {
|
||||
fn with_capacity(max_size: usize) -> Self {
|
||||
Self {
|
||||
max_size,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn add_new(&mut self, question: String) -> Result<(), ()> {
|
||||
if self.pushed.contains(&question) || self.to_push.contains(&question) { return Ok(()); }
|
||||
if self.total_size + question.len() > self.max_size {
|
||||
return Err(());
|
||||
}
|
||||
self.total_size += question.len();
|
||||
self.to_push.insert(question);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn consume_to_push(&mut self) -> HashSet<String> {
|
||||
for question in self.to_push.iter() {
|
||||
self.pushed.insert(question.clone());
|
||||
}
|
||||
std::mem::take(&mut self.to_push)
|
||||
}
|
||||
}
|
||||
|
||||
struct SharedState {
|
||||
config: config::Config,
|
||||
questions: Mutex<Questions>,
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
fn new(config: config::Config) -> Self {
|
||||
let memory_limit = config.performance.memory_limit;
|
||||
Self {
|
||||
config,
|
||||
questions: Mutex::new(Questions::with_capacity(memory_limit)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// load config
|
||||
let args = match args::Args::try_from(&mut std::env::args()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@@ -18,5 +369,42 @@ async fn main() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
println!("{}", args.config_path());
|
||||
let config = match load_config(args.config_path()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log!(fatal "{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// shared state
|
||||
let state = Arc::new(SharedState::new(config));
|
||||
|
||||
// server initialization
|
||||
let Ok(listener) = TcpListener::bind(state.config.server.bind_to).await else {
|
||||
log!(fatal "unable to bind to {:?}", state.config.server.bind_to);
|
||||
return;
|
||||
};
|
||||
|
||||
// server runtime
|
||||
let state_maintenance = state.clone();
|
||||
tokio::task::spawn(async move { maintenance(state_maintenance).await; });
|
||||
log!(info "listening on {:?}", state.config.server.bind_to);
|
||||
loop {
|
||||
let Ok((stream, addr)) = listener.accept().await else {
|
||||
log!(fatal "unable to accept new connections");
|
||||
return;
|
||||
};
|
||||
log!(debug "new connection from {:?}", addr);
|
||||
let io = TokioIo::new(stream);
|
||||
let state_clone = state.clone();
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(_) = http1::Builder::new().serve_connection(io, service_fn(move |req| {
|
||||
router(req, state_clone.clone())
|
||||
})).await {
|
||||
log!(debug "transport error to {:?}", addr);
|
||||
}
|
||||
log!(debug "closing connection to {:?}", addr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user