Compare commits

...

9 Commits

Author SHA1 Message Date
93a69fd89b prepare for peehaitchpee 2025-05-02 17:18:34 +02:00
abe2171e19 add screenshot 2025-05-02 15:16:57 +02:00
6bfc272c46 add ui 2025-05-02 15:12:52 +02:00
a2a6933058 implement server logic 2025-05-02 14:11:42 +02:00
e90dfa0b0b add basic question uploading 2025-05-02 13:03:30 +02:00
4af8c825bb wip: add client base and basic router 2025-05-02 10:08:29 +02:00
96bf9303ea add basic logger 2025-05-01 00:08:09 +02:00
06b389b28f wip: add base for server 2025-04-30 23:39:21 +02:00
ce49dd6e3d add dynamic config 2025-04-30 23:38:45 +02:00
20 changed files with 1371 additions and 20 deletions

615
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -1,3 +1,7 @@
[workspace]
resolver = "2"
members = ["lib/search_and_replace", "theseus-server"]
[profile.release]
strip = true
lto = true

View File

@@ -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 $@

View File

@@ -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.
![screenshot of the client-side of Theseus](./doc/screenshot.jpg)

44
client/index.html Normal file
View 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>

View File

@@ -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
View 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
View File

153
client/style.scss Normal file
View 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;
}
}

View File

@@ -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"

View File

@@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -3,7 +3,4 @@ name = "search_and_replace"
version = "0.1.0"
edition = "2021"
[profile.release]
strip = true
[dependencies]

View File

@@ -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"

View File

@@ -0,0 +1,2 @@
[general]
dirs = ["../dst/"]

View File

@@ -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,
}

View 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>"
}
}

View 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),*)?);
};
}

View File

@@ -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);
});
}
}