commit 293dbb5ad193a4ed15d735132d1f44db0d251bb2 Author: Snídaňový Mistr Date: Mon Dec 11 16:18:43 2023 +0100 Nahrát existující kód diff --git a/Cargo.lock b/Cargo.lock new file mode 100755 index 0000000..599eafe --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3354 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-files" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "askama_escape", + "bitflags 1.3.2", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64 0.21.2", + "bitflags 1.3.2", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.28", +] + +[[package]] +name = "actix-multipart" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee489e3c01eae4d1c35b03c4493f71cb40d93f66b14558feb1b1a807671cc4e" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec592f234db8a253cf80531246a4407c8a70530423eea80688a6c5a44a110e7" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2 0.4.9", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.4.9", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + +[[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 = "anyhow" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6f84b74db2535ebae81eede2f39b947dcbf01d093ae5f791e5dd414a1bf289" + +[[package]] +name = "askama" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47cbc3cf73fa8d9833727bbee4835ba5c421a0d65b72daf9a7b5d0e0f9cfb57e" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22fbe0413545c098358e56966ff22cdd039e10215ae213cfbd65032b119fc94" +dependencies = [ + "basic-toml", + "mime", + "mime_guess", + "nom", + "proc-macro2", + "quote", + "serde", + "syn 2.0.28", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "basic-toml" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838d03a705d72b12389b8930bd14cacf493be1380bfb15720d4d12db5ab03ac" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "captcha" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db21780337b425f968a2c3efa842eeaa4fe53d2bcb1eb27d2877460a862fb0ab" +dependencies = [ + "base64 0.13.1", + "hound", + "image", + "lodepng", + "rand", + "serde_json", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "pure-rust-locales", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "const-oid" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cow-utils" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79bb3adfaf5f75d24b01aee375f7555907840fa2800e5ec8fa3b9e2031830173" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "der" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "educe" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079044df30bb07de7d846d41a184c4b00e66ebdac93ee459253474f3a47e50ae" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-ordinalize" +version = "3.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f76552f53cefc9a7f64987c3701b99d982f7690606fd67de1d09712fbf52f1" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "exr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fallible_collections" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd" +dependencies = [ + "hashbrown 0.13.2", +] + +[[package]] +name = "fancy-regex" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "hound" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "html-minifier" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f74036e4be1b97eaf78bcf795d55002eccf613d5c4705bf78d3b92649ab00f" +dependencies = [ + "cow-utils", + "educe", + "html-escape", + "minifier", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09" +dependencies = [ + "base64 0.21.2", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lodepng" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ad39f75bbaa4b10bb6f2316543632a8046a5bcf9c785488d79720b21f044f8" +dependencies = [ + "crc32fast", + "fallible_collections", + "flate2", + "libc", + "rgb", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minifier" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb022374af2f446981254e6bf9efb6e2c9e1a53176d395fca02792fd4435729" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "nekrochan" +version = "0.1.0" +dependencies = [ + "actix-files", + "actix-multipart", + "actix-web", + "anyhow", + "askama", + "captcha", + "chrono", + "dotenv", + "encoding", + "enumflags2", + "env_logger", + "fancy-regex", + "glob", + "html-minifier", + "image", + "ipnetwork", + "jsonwebtoken", + "lazy_static", + "log", + "num-traits", + "pwhash", + "rand", + "redis", + "serde", + "serde_json", + "serde_qs", + "sha256", + "sqlx", + "thiserror", + "tokio", + "toml", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "parse-size" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + +[[package]] +name = "paste" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" + +[[package]] +name = "pem" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +dependencies = [ + "base64 0.21.2", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pure-rust-locales" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed02a829e62dc2715ceb8afb4f80e298148e1345749ceb369540fe0eb3368432" + +[[package]] +name = "pwhash" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56" +dependencies = [ + "blowfish", + "byteorder", + "hmac 0.10.1", + "md-5 0.9.1", + "rand", + "sha-1", + "sha2 0.9.9", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redis" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "serde", + "serde_json", + "sha1_smol", + "socket2 0.4.9", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys", +] + +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +dependencies = [ + "byteorder", + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.166" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.166" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_json" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_plain" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6018081315db179d0ce57b1fe4b62a12a0028c9cf9bbef868c9cf477b3c34ae" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha256" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a975c1bc0941703000eaf232c4d8ce188d8d5408d6344b6b2c8c6262772828" +dependencies = [ + "hex", + "sha2 0.10.7", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ef53c86d2066e04f0ac6b1364f16d13d82388e2d07f11a5c71782345555761" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a22fd81e9c1ad53c562edb869ff042b215d4eadefefc4784bacfbfd19835945" +dependencies = [ + "ahash 0.8.3", + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.0", + "ipnetwork", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.7", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bb7c096a202b8164c175614cbfb79fe0e1e0a3d50e0374526183ef2974e4a2" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d644623ab9699014e5b3cb61a040d16caa50fd477008f63f1399ae35498a58" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.7", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8264c59b28b6858796acfcedc660aa4c9075cc6e4ec8eb03cdca2a3e725726db" +dependencies = [ + "atoi", + "base64 0.21.2", + "bitflags 2.3.3", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa", + "log", + "md-5 0.10.5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cab6147b81ca9213a7578f1b4c9d24c449a53953cd2222a7b5d7cd29a5c3139" +dependencies = [ + "atoi", + "base64 0.21.2", + "bitflags 2.3.3", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "ipnetwork", + "itoa", + "log", + "md-5 0.10.5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fba60afa64718104b71eec6984f8779d4caffff3b30cde91a75843c7efc126" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", +] + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3ce25f50619af8b0aec2eb23deebe84249e19e2ddd393a6e16e3300a6dadfd" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.3", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +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.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100755 index 0000000..658d361 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "nekrochan" +version = "0.1.0" +edition = "2021" + +[dependencies] +actix-files = "0.6.2" +actix-multipart = "0.6.0" +actix-web = { version = "4.3.1", features = ["cookies"] } +askama = "0.12.0" +anyhow = "1.0.71" +captcha = "0.0.9" +chrono = { version = "0.4.31", features = ["serde", "unstable-locales"] } +dotenv = "0.15.0" +enumflags2 = "0.7.7" +encoding = "0.2.33" +env_logger = "0.10.0" +fancy-regex = "0.12.0" +glob = "0.3.1" +image = "0.24.7" +ipnetwork = "0.20.0" +jsonwebtoken = "9.1.0" +lazy_static = "1.4.0" +log = "0.4.19" +num-traits = "0.2.16" +pwhash = "1.0.0" +rand = "0.8.5" +redis = { version = "0.24.0", features = ["aio", "json", "tokio-comp"] } +serde = "1.0.166" +serde_json = "1.0.100" +serde_qs = "0.12.0" +sha256 = "1.1.4" +sqlx = { version = "0.7.0", features = [ + "runtime-tokio", + "postgres", + "json", + "chrono", + "ipnetwork", +] } +thiserror = "1.0.41" +tokio = { version = "1.29.1", features = ["rt-multi-thread", "macros"] } +toml = "0.8.6" + +[build-dependencies] +anyhow = "1.0.74" +glob = "0.3.1" +html-minifier = "4.0.0" + +[profile.dev] +opt-level = 1 diff --git a/askama.toml b/askama.toml new file mode 100644 index 0000000..7c682a8 --- /dev/null +++ b/askama.toml @@ -0,0 +1,2 @@ +[general] +dirs = ["templates_min"] diff --git a/build.rs b/build.rs new file mode 100755 index 0000000..285279a --- /dev/null +++ b/build.rs @@ -0,0 +1,37 @@ +use anyhow::Error; +use glob::glob; +use html_minifier::minify; +use std::{ + fs::{read_to_string, File}, + io::Write, + process::Command, +}; + +fn main() -> Result<(), Error> { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=migrations"); + println!("cargo:rerun-if-changed=templates"); + + Command::new("rm").args(["-rf", "templates_min"]).output()?; + + Command::new("cp") + .args(["-r", "templates", "templates_min"]) + .output()?; + + let templates = glob("templates_min/**/*.html")?; + + for path in templates { + let path = path?; + + if !path.is_file() { + continue; + } + + let html = read_to_string(&path)?; + let minified = minify(html)?.replace('\n', ""); + + File::create(path)?.write_all(minified.as_bytes())?; + } + + Ok(()) +} diff --git a/migrations/20230710121446_create_tables.down.sql b/migrations/20230710121446_create_tables.down.sql new file mode 100755 index 0000000..efacdfc --- /dev/null +++ b/migrations/20230710121446_create_tables.down.sql @@ -0,0 +1 @@ +DROP TABLE accounts, boards, bans; diff --git a/migrations/20230710121446_create_tables.up.sql b/migrations/20230710121446_create_tables.up.sql new file mode 100755 index 0000000..cccea3b --- /dev/null +++ b/migrations/20230710121446_create_tables.up.sql @@ -0,0 +1,30 @@ +CREATE TABLE accounts ( + username VARCHAR(32) NOT NULL PRIMARY KEY, + password VARCHAR(64) NOT NULL, + owner BOOLEAN NOT NULL DEFAULT false, + permissions JSONB NOT NULL DEFAULT '0'::jsonb, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE boards ( + id VARCHAR(16) NOT NULL PRIMARY KEY, + name VARCHAR(32) NOT NULL, + description VARCHAR(128) NOT NULL, + banners JSONB NOT NULL, + config JSONB NOT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE bans ( + id SERIAL NOT NULL PRIMARY KEY, + ip_range INET NOT NULL, + reason TEXT NOT NULL, + board VARCHAR(16) DEFAULT NULL REFERENCES boards(id), + issued_by VARCHAR(32) NOT NULL REFERENCES accounts(username), + appealable BOOLEAN NOT NULL DEFAULT true, + appeal TEXT DEFAULT NULL, + expires TIMESTAMPTZ DEFAULT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO accounts (username, password, owner, permissions) VALUES ('admin', '$2y$10$XcxAe19B1eWC15sfnDRyiuiNLZIhdL7PMTnTmtTfglJIz0zOpN3oa', true, '16383'::jsonb); diff --git a/src/auth.rs b/src/auth.rs new file mode 100755 index 0000000..5ced9fa --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,39 @@ +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +use crate::{ctx::Ctx, error::NekrochanError}; + +#[derive(Serialize, Deserialize)] +pub struct Claims { + pub sub: String, +} + +impl Claims { + pub fn new(sub: String) -> Self { + Self { sub } + } + + pub fn encode(&self, ctx: &Ctx) -> Result { + let header = Header::default(); + let key = EncodingKey::from_secret(ctx.cfg.secrets.auth_token.as_bytes()); + + let auth = encode(&header, &self, &key)?; + + Ok(auth) + } + + pub fn decode(ctx: &Ctx, auth: &str) -> Result { + let key = DecodingKey::from_secret(ctx.cfg.secrets.auth_token.as_bytes()); + + let mut validation = Validation::default(); + validation.required_spec_claims = HashSet::from_iter(["sub".to_owned()]); + validation.validate_exp = false; + + let claims = decode(auth, &key, &validation) + .map_err(|_| NekrochanError::InvalidAuthError)? + .claims; + + Ok(claims) + } +} diff --git a/src/cfg.rs b/src/cfg.rs new file mode 100755 index 0000000..5e90339 --- /dev/null +++ b/src/cfg.rs @@ -0,0 +1,71 @@ +use anyhow::Error; +use serde::{Deserialize, Serialize}; +use tokio::fs::read_to_string; + +#[derive(Deserialize, Clone)] +pub struct Cfg { + pub server: ServerCfg, + pub site: SiteCfg, + pub secrets: SecretsCfg, + pub files: FilesCfg, + pub board_defaults: BoardCfg, +} + +impl Cfg { + pub async fn load(path: &str) -> Result { + let cfg_string = read_to_string(path).await?; + let cfg: Cfg = toml::from_str(&cfg_string)?; + + Ok(cfg) + } +} + +#[derive(Deserialize, Clone)] +pub struct ServerCfg { + pub port: u16, + pub database_url: String, + pub cache_url: String, +} + +#[derive(Deserialize, Clone)] +pub struct SiteCfg { + pub name: String, + pub description: String, + pub site_banner: Option, +} + +#[derive(Deserialize, Clone)] +pub struct SecretsCfg { + pub auth_token: String, + pub secure_trip: String, + pub user_id: String, +} + +#[derive(Deserialize, Clone)] +pub struct FilesCfg { + pub videos: bool, + pub thumb_size: u32, + pub max_size_mb: usize, + pub max_height: u32, + pub max_width: u32, + pub cleanup_interval: u64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct BoardCfg { + pub anon_name: String, + pub page_size: i64, + pub page_count: i64, + pub file_limit: usize, + pub bump_limit: i32, + pub reply_limit: i32, + pub locked: bool, + pub user_ids: bool, + pub flags: bool, + pub thread_captcha: String, + pub reply_captcha: String, + pub require_thread_content: bool, + pub require_thread_file: bool, + pub require_reply_content: bool, + pub require_reply_file: bool, +} diff --git a/src/ctx.rs b/src/ctx.rs new file mode 100755 index 0000000..a7b74c8 --- /dev/null +++ b/src/ctx.rs @@ -0,0 +1,36 @@ +use anyhow::Error; +use redis::{aio::MultiplexedConnection, Client}; +use sqlx::PgPool; +use std::net::SocketAddr; + +use crate::cfg::Cfg; + +#[derive(Clone)] +pub struct Ctx { + pub cfg: Cfg, + db: PgPool, + cache: MultiplexedConnection, +} + +impl Ctx { + pub async fn new(cfg: Cfg) -> Result { + let db = PgPool::connect(&cfg.server.database_url).await?; + let cache = Client::open(cfg.server.cache_url.as_str())? + .get_multiplexed_async_connection() + .await?; + + Ok(Self { cfg, db, cache }) + } + + pub fn bind_addr(&self) -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], self.cfg.server.port)) + } + + pub fn db(&self) -> &PgPool { + &self.db + } + + pub fn cache(&self) -> MultiplexedConnection { + self.cache.clone() + } +} diff --git a/src/db/account.rs b/src/db/account.rs new file mode 100755 index 0000000..c6bdf2e --- /dev/null +++ b/src/db/account.rs @@ -0,0 +1,117 @@ +use redis::{AsyncCommands, JsonAsyncCommands}; +use sqlx::{query, query_as, types::Json}; + +use super::models::Account; +use crate::{ctx::Ctx, error::NekrochanError, perms::PermissionWrapper}; + +impl Account { + pub async fn create( + ctx: &Ctx, + username: String, + password: String, + ) -> Result { + let account = + query_as("INSERT INTO accounts (username, password) VALUES ($1, $2) RETURNING *") + .bind(&username) + .bind(password) + .fetch_one(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("accounts:{}", username), ".", &account) + .await?; + + Ok(account) + } + + pub async fn read(ctx: &Ctx, username: String) -> Result, NekrochanError> { + let account: Option = ctx + .cache() + .json_get(format!("accounts:{}", username), ".") + .await?; + + let account = match account { + Some(json) => Some(serde_json::from_str(&json)?), + None => None, + }; + + Ok(account) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let accounts = query_as("SELECT * FROM accounts ORDER BY owner DESC, created DESC") + .fetch_all(ctx.db()) + .await?; + + Ok(accounts) + } + + pub async fn update_password(&self, ctx: &Ctx, password: String) -> Result<(), NekrochanError> { + query("UPDATE accounts SET password = $1 WHERE username = $2") + .bind(&password) + .bind(&self.username) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("accounts:{}", self.username), "password", &password) + .await?; + + Ok(()) + } + + pub async fn update_permissions( + &self, + ctx: &Ctx, + permissions: u64, + ) -> Result<(), NekrochanError> { + query("UPDATE accounts SET permissions = $1 WHERE username = $2") + .bind(Json(permissions)) + .bind(&self.username) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set( + format!("accounts:{}", self.username), + "permissions", + &permissions, + ) + .await?; + + Ok(()) + } + + pub async fn update_owner(&self, ctx: &Ctx, owner: bool) -> Result<(), NekrochanError> { + query("UPDATE accounts SET owner = $1 WHERE username = $2") + .bind(owner) + .bind(&self.username) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("accounts:{}", self.username), "owner", &owner) + .await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query("DELETE FROM accounts WHERE username = $1") + .bind(&self.username) + .execute(ctx.db()) + .await?; + + ctx.cache() + .del(format!("accounts:{}", self.username)) + .await?; + + Ok(()) + } +} + +impl Account { + pub fn perms(&self) -> PermissionWrapper { + PermissionWrapper::new(self.permissions.0, self.owner) + } +} diff --git a/src/db/ban.rs b/src/db/ban.rs new file mode 100755 index 0000000..0bc1f4c --- /dev/null +++ b/src/db/ban.rs @@ -0,0 +1,107 @@ +use chrono::{DateTime, Utc}; +use ipnetwork::IpNetwork; +use sqlx::{query, query_as}; +use std::{collections::HashMap, net::IpAddr}; + +use super::models::Ban; +use crate::{ctx::Ctx, error::NekrochanError}; + +impl Ban { + pub async fn create( + ctx: &Ctx, + account: String, + board: Option, + ip_range: IpNetwork, + reason: String, + appealable: bool, + expires: Option>, + ) -> Result { + let ban = query_as("INSERT INTO bans (ip_range, reason, board, issued_by, appealable, expires) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *") + .bind(ip_range) + .bind(reason) + .bind(board) + .bind(account) + .bind(appealable) + .bind(expires) + .fetch_one(ctx.db()) + .await?; + + Ok(ban) + } + + pub async fn read(ctx: &Ctx, board: String, ip: IpAddr) -> Result, NekrochanError> { + let ban = query_as("SELECT * FROM bans WHERE (expires > CURRENT_TIMESTAMP OR expires IS NULL) AND (board = $1 OR board IS NULL) AND (ip_range >> $2 OR ip_range = $2)") + .bind(board) + .bind(ip) + .fetch_optional(ctx.db()) + .await?; + + Ok(ban) + } + + pub async fn read_global(ctx: &Ctx, ip: IpNetwork) -> Result, NekrochanError> { + let ban = query_as("SELECT * FROM bans WHERE (expires > CURRENT_TIMESTAMP OR expires IS NULL) AND board IS NULL AND (ip_range >> $1 OR ip_range = $1)") + .bind(ip) + .fetch_optional(ctx.db()) + .await?; + + Ok(ban) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let bans = + query_as("SELECT * FROM bans WHERE (expires > CURRENT_TIMESTAMP OR expires IS NULL) ORDER BY created DESC") + .fetch_all(ctx.db()) + .await?; + + Ok(bans) + } + + pub async fn read_by_id(ctx: &Ctx, id: i32) -> Result, NekrochanError> { + let ban = query_as("SELECT * FROM bans WHERE id = $1") + .bind(id) + .fetch_optional(ctx.db()) + .await?; + + Ok(ban) + } + + pub async fn read_by_ip( + ctx: &Ctx, + ip: IpAddr, + ) -> Result, Ban>, NekrochanError> { + let bans: Vec = query_as("SELECT * FROM bans WHERE (expires > CURRENT_TIMESTAMP OR expires IS NULL) AND (ip_range >> $1 OR ip_range = $1)") + .bind(ip) + .fetch_all(ctx.db()) + .await?; + + let mut ban_map = HashMap::new(); + + for ban in bans.into_iter() { + let board = ban.board.clone(); + + ban_map.insert(board, ban); + } + + Ok(ban_map) + } + + pub async fn update_appeal(&self, ctx: &Ctx, appeal: String) -> Result<(), NekrochanError> { + query("UPDATE bans SET appeal = $1 WHERE id = $2") + .bind(appeal) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query("DELETE FROM bans WHERE id = $1") + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } +} diff --git a/src/db/board.rs b/src/db/board.rs new file mode 100755 index 0000000..9e19396 --- /dev/null +++ b/src/db/board.rs @@ -0,0 +1,281 @@ +use captcha::{gen, Difficulty}; +use rand::{seq::SliceRandom, thread_rng}; +use redis::{cmd, AsyncCommands, JsonAsyncCommands}; +use sha256::digest; +use sqlx::{query, query_as, types::Json}; +use std::collections::HashMap; + +use super::models::{Board, File}; +use crate::{cfg::BoardCfg, ctx::Ctx, error::NekrochanError, CAPTCHA}; + +impl Board { + pub async fn create( + ctx: &Ctx, + id: String, + name: String, + description: String, + ) -> Result { + let banners = Json(Vec::::new()); + let config = Json(ctx.cfg.board_defaults.clone()); + + let board: Board = query_as("INSERT INTO boards (id, name, description, banners, config) VALUES ($1, $2, $3, $4, $5) RETURNING *") + .bind(id) + .bind(name) + .bind(description) + .bind(banners) + .bind(config) + .fetch_one(ctx.db()) + .await?; + + query(&format!( + r#"CREATE TABLE posts_{} ( + id SERIAL NOT NULL PRIMARY KEY, + board VARCHAR(16) NOT NULL DEFAULT '{}' REFERENCES boards(id), + thread INT DEFAULT NULL REFERENCES posts_{}(id), + name VARCHAR(32) NOT NULL, + user_id VARCHAR(6) NOT NULL DEFAULT '000000', + tripcode VARCHAR(12) DEFAULT NULL, + capcode VARCHAR(32) DEFAULT NULL, + email VARCHAR(256) DEFAULT NULL, + content TEXT NOT NULL, + content_nomarkup TEXT NOT NULL, + files JSONB NOT NULL, + password VARCHAR(64) DEFAULT NULL, + country VARCHAR(2) NOT NULL, + ip INET NOT NULL, + bumps INT NOT NULL DEFAULT 0, + replies INT NOT NULL DEFAULT 0, + sticky BOOLEAN NOT NULL DEFAULT false, + locked BOOLEAN NOT NULL DEFAULT false, + reported TIMESTAMPTZ DEFAULT NULL, + reports JSONB NOT NULL DEFAULT '[]'::json, + bumped TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + )"#, + board.id, board.id, board.id + )) + .execute(ctx.db()) + .await?; + + ctx.cache() + .set(format!("board_threads:{}", board.id), 0) + .await?; + + ctx.cache().lpush("board_ids", &board.id).await?; + + ctx.cache() + .json_set(format!("boards:{}", board.id), ".", &board) + .await?; + + cmd("SORT") + .arg("board_ids") + .arg("ALPHA") + .arg("STORE") + .arg("board_ids") + .query_async(&mut ctx.cache()) + .await?; + + update_overboard(ctx, Self::read_all(ctx).await?).await?; + + Ok(board) + } + + pub async fn read(ctx: &Ctx, id: String) -> Result, NekrochanError> { + let board: Option = ctx.cache().json_get(format!("boards:{}", id), ".").await?; + + let board = match board { + Some(json) => Some(serde_json::from_str(&json)?), + None => None, + }; + + Ok(board) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let mut boards = Vec::new(); + let ids: Vec = ctx.cache().lrange("board_ids", 0, -1).await?; + + for id in ids.into_iter() { + if let Some(board) = Self::read(ctx, id).await? { + boards.push(board); + } + } + + Ok(boards) + } + + pub async fn read_all_map(ctx: &Ctx) -> Result, NekrochanError> { + let mut boards = HashMap::new(); + let ids: Vec = ctx.cache().lrange("board_ids", 0, -1).await?; + + for id in ids.into_iter() { + if let Some(board) = Self::read(ctx, id.clone()).await? { + boards.insert(id, board); + } + } + + Ok(boards) + } + + pub async fn update_name(&self, ctx: &Ctx, name: String) -> Result<(), NekrochanError> { + query("UPDATE boards SET name = $1 WHERE id = $2") + .bind(&name) + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("boards:{}", self.id), "name", &name) + .await?; + + Ok(()) + } + + pub async fn update_description( + &self, + ctx: &Ctx, + description: String, + ) -> Result<(), NekrochanError> { + query("UPDATE boards SET description = $1 WHERE id = $2") + .bind(&description) + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("boards:{}", self.id), "description", &description) + .await?; + + Ok(()) + } + + pub async fn update_banners( + &self, + ctx: &Ctx, + banners: Vec, + ) -> Result<(), NekrochanError> { + query("UPDATE boards SET banners = $1 WHERE id = $2") + .bind(Json(&banners)) + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("boards:{}", self.id), "banners", &banners) + .await?; + + Ok(()) + } + + pub async fn update_config(&self, ctx: &Ctx, config: BoardCfg) -> Result<(), NekrochanError> { + query("UPDATE boards SET config = $1 WHERE id = $2") + .bind(Json(&config)) + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("boards:{}", self.id), "config", &config) + .await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + let boards = Self::read_all(ctx) + .await? + .into_iter() + .filter(|board| board.id != self.id) + .collect(); + + update_overboard(ctx, boards).await?; + + query(&format!("DROP TABLE posts_{}", self.id)) + .execute(ctx.db()) + .await?; + + query("DELETE FROM boards WHERE id = $1") + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache().del(format!("boards:{}", self.id)).await?; + ctx.cache().lrem("board_ids", 0, &self.id).await?; + + Ok(()) + } +} + +impl Board { + pub fn random_banner(&self) -> Option { + self.banners + .choose(&mut thread_rng()) + .map(|banner| banner.to_owned()) + } + + pub fn thread_captcha(&self) -> Option<(String, String)> { + let captcha = match self.config.thread_captcha.as_str() { + "easy" => gen(Difficulty::Easy), + "medium" => gen(Difficulty::Medium), + "hard" => gen(Difficulty::Hard), + _ => return None, + }; + + let base64 = captcha.as_base64()?; + + let board = self.id.clone(); + let difficulty = self.config.thread_captcha.clone(); + let id = digest(base64.as_bytes()); + + let key = (board, difficulty, id.clone()); + let solution = captcha.chars_as_string(); + + CAPTCHA.write().ok()?.insert(key, solution); + + Some((id, base64)) + } + + pub fn reply_captcha(&self) -> Option<(String, String)> { + let captcha = match self.config.reply_captcha.as_str() { + "easy" => gen(Difficulty::Easy), + "medium" => gen(Difficulty::Medium), + "hard" => gen(Difficulty::Hard), + _ => return None, + }; + + let base64 = captcha.as_base64()?; + + let board = self.id.clone(); + let difficulty = self.config.thread_captcha.clone(); + let id = digest(base64.as_bytes()); + + let key = (board, difficulty, id.clone()); + let solution = captcha.chars_as_string(); + + CAPTCHA.write().ok()?.insert(key, solution); + + Some((id, base64)) + } +} + +async fn update_overboard(ctx: &Ctx, boards: Vec) -> Result<(), NekrochanError> { + query("DROP VIEW IF EXISTS overboard") + .execute(ctx.db()) + .await?; + + if boards.is_empty() { + return Ok(()); + } + + let unions = boards + .into_iter() + .map(|board| format!("SELECT * FROM posts_{}", board.id)) + .collect::>() + .join(" UNION "); + + query(&format!("CREATE VIEW overboard AS {unions}")) + .execute(ctx.db()) + .await?; + + Ok(()) +} diff --git a/src/db/cache.rs b/src/db/cache.rs new file mode 100644 index 0000000..3a1f96f --- /dev/null +++ b/src/db/cache.rs @@ -0,0 +1,72 @@ +use anyhow::Error; +use redis::{cmd, AsyncCommands, JsonAsyncCommands}; +use sha256::digest; +use sqlx::query_as; + +use super::models::{Account, Board, Post}; +use crate::ctx::Ctx; + +pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> { + cmd("FLUSHDB").query_async(&mut ctx.cache()).await?; + + let accounts: Vec = query_as("SELECT * FROM accounts") + .fetch_all(ctx.db()) + .await?; + + for account in accounts.iter() { + ctx.cache() + .json_set(format!("accounts:{}", account.username), ".", &account) + .await?; + } + + let boards: Vec = query_as("SELECT * FROM boards").fetch_all(ctx.db()).await?; + + for board in boards.iter() { + ctx.cache() + .json_set(format!("boards:{}", board.id), ".", board) + .await?; + + ctx.cache().lpush("board_ids", &board.id).await?; + } + + cmd("SORT") + .arg("board_ids") + .arg("ALPHA") + .arg("STORE") + .arg("board_ids") + .query_async(&mut ctx.cache()) + .await?; + + ctx.cache().set("total_threads", 0).await?; + + for board in boards.iter() { + let (thread_count,): (i64,) = query_as(&format!( + "SELECT COUNT(id) FROM posts_{} WHERE thread IS NULL", + board.id + )) + .fetch_one(ctx.db()) + .await?; + + ctx.cache().incr("total_threads", thread_count).await?; + ctx.cache() + .set(format!("board_threads:{}", board.id), thread_count) + .await?; + } + + for board in boards.iter() { + let posts = Post::read_all(ctx, board.id.clone()).await?; + + for post in posts.into_iter() { + let ip_key = format!("by_ip:{}", post.ip); + let content_key = format!("by_content:{}", digest(post.content_nomarkup)); + + let member = format!("{}/{}", post.board, post.id); + let score = post.created.timestamp_micros(); + + ctx.cache().zadd(ip_key, &member, score).await?; + ctx.cache().zadd(content_key, &member, score).await?; + } + } + + Ok(()) +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100755 index 0000000..0cdd13a --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,7 @@ +pub mod cache; +pub mod models; + +mod account; +mod ban; +mod board; +mod post; diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100755 index 0000000..20664da --- /dev/null +++ b/src/db/models.rs @@ -0,0 +1,92 @@ +use std::net::IpAddr; + +use chrono::{DateTime, Utc}; +use ipnetwork::IpNetwork; +use serde::{Deserialize, Serialize}; +use sqlx::{types::Json, FromRow}; + +use crate::cfg::BoardCfg; + +#[derive(FromRow, Clone, Serialize, Deserialize)] +pub struct Account { + pub username: String, + pub password: String, + pub owner: bool, + pub permissions: Json, + pub created: DateTime, +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Board { + pub id: String, + pub name: String, + pub description: String, + pub banners: Json>, + pub config: Json, + pub created: DateTime, +} + +#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] +pub struct Ban { + pub id: i32, + pub ip_range: IpNetwork, + pub reason: String, + pub board: Option, + pub issued_by: String, + pub appealable: bool, + pub appeal: Option, + pub expires: Option>, + pub created: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Report { + pub reason: String, + pub reporter_country: String, + pub reporter_ip: IpAddr, +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct LogRecord { + pub id: i32, + pub message: String, + pub created: DateTime, +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Post { + pub id: i32, + pub board: String, + pub thread: Option, + pub name: String, + pub user_id: String, + pub tripcode: Option, + pub capcode: Option, + pub email: Option, + pub content: String, + pub content_nomarkup: String, + pub files: Json>, + pub password: String, + pub country: String, + pub ip: IpAddr, + pub bumps: i32, + pub replies: i32, + pub sticky: bool, + pub locked: bool, + pub reported: Option>, + pub reports: Json>, + pub bumped: DateTime, + pub created: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct File { + pub original_name: String, + pub format: String, + pub thumb_format: Option, + pub spoiler: bool, + pub width: u32, + pub height: u32, + pub timestamp: i64, + pub size: usize, +} diff --git a/src/db/post.rs b/src/db/post.rs new file mode 100755 index 0000000..efe87ba --- /dev/null +++ b/src/db/post.rs @@ -0,0 +1,451 @@ +use chrono::Utc; +use redis::AsyncCommands; +use sha256::digest; +use sqlx::{query, query_as, types::Json}; +use std::net::IpAddr; + +use super::models::{Board, File, Post, Report}; +use crate::{ctx::Ctx, error::NekrochanError}; + +impl Post { + #[allow(clippy::too_many_arguments)] + pub async fn create( + ctx: &Ctx, + board: &Board, + thread: Option, + name: String, + tripcode: Option, + capcode: Option, + email: Option, + content: String, + content_nomarkup: String, + files: Vec, + password: String, + country: String, + ip: IpAddr, + bumpy_bump: bool, + ) -> Result { + let post: Post = query_as(&format!( + r#"INSERT INTO posts_{} + (thread, name, tripcode, capcode, email, content, content_nomarkup, files, password, country, ip) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + RETURNING *"#, board.id) + ) + .bind(thread) + .bind(name) + .bind(tripcode) + .bind(capcode) + .bind(email) + .bind(content) + .bind(content_nomarkup) + .bind(Json(files)) + .bind(password) + .bind(country) + .bind(ip) + .fetch_one(ctx.db()) + .await?; + + if let Some(thread) = thread { + query(&format!( + "UPDATE posts_{} SET replies = replies + 1 WHERE id = $1", + board.id + )) + .bind(thread) + .execute(ctx.db()) + .await?; + + if bumpy_bump { + query(&format!( + "UPDATE posts_{} SET bumps = bumps + 1, bumped = CURRENT_TIMESTAMP WHERE id = $1", + board.id + )) + .bind(thread) + .execute(ctx.db()) + .await?; + } + } else { + delete_old_threads(ctx, board).await?; + + ctx.cache().incr("total_threads", 1).await?; + ctx.cache() + .incr(format!("board_threads:{}", board.id), 1) + .await?; + } + + let ip_key = format!("by_ip:{}", ip); + let content_key = format!("by_content:{}", digest(post.content_nomarkup.as_bytes())); + let member = format!("{}/{}", board.id, post.id); + let score = post.created.timestamp_micros(); + + ctx.cache().zadd(ip_key, &member, score).await?; + ctx.cache().zadd(content_key, &member, score).await?; + + Ok(post) + } + + pub async fn create_report( + &self, + ctx: &Ctx, + reason: String, + reporter_country: String, + reporter_ip: IpAddr, + ) -> Result<(), NekrochanError> { + let mut reports = self.reports.clone(); + + reports.push(Report { + reason, + reporter_country, + reporter_ip, + }); + + query(&format!( + "UPDATE posts_{} SET reported = CURRENT_TIMESTAMP, reports = $1 WHERE id = $2", + self.board + )) + .bind(reports) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn read(ctx: &Ctx, board: String, id: i32) -> Result, NekrochanError> { + let post = query_as("SELECT * FROM overboard WHERE board = $1 AND id = $2") + .bind(board) + .bind(id) + .fetch_optional(ctx.db()) + .await?; + + Ok(post) + } + + pub async fn read_board_page( + ctx: &Ctx, + board: &Board, + page: i64, + ) -> Result, NekrochanError> { + let posts = query_as(&format!( + r#"SELECT * FROM posts_{} + WHERE thread IS NULL + ORDER BY sticky DESC, bumped DESC + LIMIT $1 + OFFSET $2"#, + board.id + )) + .bind(board.config.0.page_size) + .bind((page - 1) * board.config.0.page_size) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_board_catalog(ctx: &Ctx, board: String) -> Result, NekrochanError> { + let posts = query_as(&format!( + r#"SELECT * FROM posts_{} + WHERE thread IS NULL + ORDER BY sticky DESC, bumped DESC"#, + board + )) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_overboard_page(ctx: &Ctx, page: i64) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * FROM overboard + WHERE thread IS NULL + ORDER BY bumped DESC + LIMIT $1 + OFFSET $2"#, + ) + .bind(15) + .bind((page - 1) * 15) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_overboard_catalog(ctx: &Ctx) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * FROM overboard + WHERE thread IS NULL + ORDER BY bumped DESC"#, + ) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_latest(ctx: &Ctx) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * FROM overboard + ORDER BY created DESC + LIMIT $1"#, + ) + .bind(15) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_reports(ctx: &Ctx) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * FROM overboard + WHERE reports != '[]'::jsonb + ORDER BY jsonb_array_length(reports), reported DESC"#, + ) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_files(ctx: &Ctx) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * + FROM overboard + WHERE files != '[]'::jsonb + ORDER BY created DESC + LIMIT 3"#, + ) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_replies(&self, ctx: &Ctx) -> Result, NekrochanError> { + let replies = query_as(&format!( + "SELECT * FROM posts_{} WHERE thread = $1 ORDER BY created ASC", + self.board + )) + .bind(self.id) + .fetch_all(ctx.db()) + .await?; + + Ok(replies) + } + + pub async fn read_all(ctx: &Ctx, board: String) -> Result, NekrochanError> { + let posts = query_as(&format!("SELECT * FROM posts_{}", board)) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn update_user_id(&self, ctx: &Ctx, user_id: String) -> Result<(), NekrochanError> { + query(&format!( + "UPDATE posts_{} SET user_id = $1 WHERE id = $2", + self.board, + )) + .bind(user_id) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn update_sticky(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query(&format!( + "UPDATE posts_{} SET sticky = NOT sticky WHERE id = $1", + self.board + )) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn update_lock(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query(&format!( + "UPDATE posts_{} SET locked = NOT locked WHERE id = $1", + self.board + )) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn update_content( + &self, + ctx: &Ctx, + content: String, + content_nomarkup: String, + ) -> Result<(), NekrochanError> { + query(&format!( + "UPDATE posts_{} SET content = $1, content_nomarkup = $2 WHERE id = $3", + self.board + )) + .bind(content) + .bind(&content_nomarkup) + .bind(self.id) + .execute(ctx.db()) + .await?; + + let old_key = format!("by_content:{}", digest(self.content_nomarkup.as_bytes())); + let new_key = format!("by_content:{}", digest(content_nomarkup.as_bytes())); + let member = format!("{}/{}", self.board, self.id); + let score = Utc::now().timestamp_micros(); + + ctx.cache().zrem(old_key, &member).await?; + ctx.cache().zadd(new_key, &member, score).await?; + + Ok(()) + } + + pub async fn update_spoiler(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + let mut files = self.files.clone(); + + for file in files.iter_mut() { + file.spoiler = !file.spoiler; + } + + query(&format!( + "UPDATE posts_{} SET files = $1 WHERE id = $2", + self.board + )) + .bind(Json(files)) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + let to_be_deleted: Vec = query_as(&format!( + "SELECT * FROM posts_{} WHERE id = $1 OR thread = $1", + self.board + )) + .bind(self.id) + .fetch_all(ctx.db()) + .await?; + + for post in to_be_deleted.iter() { + let id = post.id; + let url = post.post_url(); + + let live_quote = format!(">>{id}"); + let dead_quote = format!(">>{id}"); + + query(&format!( + "UPDATE posts_{} SET content = REPLACE(content, $1, $2)", + self.board + )) + .bind(live_quote) + .bind(dead_quote) + .execute(ctx.db()) + .await?; + + let ip_key = format!("by_ip:{}", post.ip); + let content_key = format!("by_content:{}", digest(post.content_nomarkup.as_bytes())); + + let member = format!("{}/{}", post.board, post.id); + + ctx.cache().zrem(ip_key, &member).await?; + ctx.cache().zrem(content_key, &member).await?; + } + + let in_list = to_be_deleted + .iter() + .map(|post| (post.id)) + .collect::>(); + + query(&format!( + "DELETE FROM posts_{} WHERE id = ANY($1)", + self.board + )) + .bind(&in_list) + .execute(ctx.db()) + .await?; + + if self.thread.is_none() { + ctx.cache().decr("total_threads", 1).await?; + ctx.cache() + .decr(format!("board_threads:{}", self.board), 1) + .await?; + } + + Ok(()) + } + + pub async fn delete_files(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + for file in self.files.iter() { + file.delete().await; + } + + query(&format!( + "UPDATE posts_{} SET files = '[]'::jsonb WHERE id = $1", + self.board + )) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn delete_reports(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query(&format!( + "UPDATE posts_{} SET reported = NULL, reports = '[]'::jsonb WHERE id = $1", + self.board + )) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } +} + +impl Post { + pub fn post_url(&self) -> String { + if let Some(thread) = self.thread { + format!("/boards/{}/{}#{}", self.board, thread, self.id) + } else { + format!("/boards/{}/{}#{}", self.board, self.id, self.id) + } + } + + pub fn post_url_notarget(&self) -> String { + format!("/boards/{}/{}", self.board, self.id) + } +} + +async fn delete_old_threads(ctx: &Ctx, board: &Board) -> Result<(), NekrochanError> { + let old_threads: Vec = query_as(&format!( + r#"SELECT * FROM posts_{} + WHERE thread IS NULL AND id NOT IN ( + SELECT id + FROM ( + SELECT id + FROM posts_{} + WHERE thread IS NULL + ORDER BY sticky DESC, bumped DESC + LIMIT $1 + ) catty + )"#, + board.id, board.id + )) + .bind(board.config.0.page_size * board.config.0.page_count) + .fetch_all(ctx.db()) + .await?; + + for thread in old_threads.iter() { + thread.delete(ctx).await?; + } + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100755 index 0000000..a87dd04 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,233 @@ +use actix_web::{http::StatusCode, ResponseError}; +use log::error; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NekrochanError { + #[error("Chyba při zpracovávání souboru '{}': {}", .0, .1)] + FileError(String, &'static str), + #[error("Uživatelské jméno musí mít 1-32 znaků.")] + UsernameFormatError, + #[error("Heslo musí mít alespoň 8 znaků.")] + PasswordFormatError, + #[error("ID musí mít 1-16 znaků.")] + IdFormatError, + #[error("Jméno nástěnky musí mít 1-32 znaků.")] + BoardNameFormatError, + #[error("Popis musí mít 1-128 znaků.")] + DescriptionFormatError, + #[error("Jméno nesmí mít více než 32 znaků.")] + PostNameFormatError, + #[error("Capcode nesmí mít více než 32 znaků.")] + CapcodeFormatError, + #[error("E-mail nesmí mít více než 256 znaků.")] + EmailFormatError, + #[error("Obsah nesmí mít více než 4000 znaků")] + ContentFormatError, + #[error("Nástěnka /{}/ neexistuje.", .0)] + BoardNotFound(String), + #[error("Účet '{}' neexistuje.", .0)] + AccountNotFound(String), + #[error("Příspěvek /{}/{} neexistuje.", .0, .1)] + PostNotFound(String, i32), + #[error("Nedostatečná oprávnění.")] + InsufficientPermissionError, + #[error("Nesprávné přihlašovací údaje.")] + IncorrectCredentialError, + #[error("Neplatná strana.")] + InvalidPageError, + #[error("Neplatný autentizační token. Vymaž soubory cookie.")] + InvalidAuthError, + #[error("Pro přístup se musíš přihlásit.")] + NotLoggedInError, + #[error("Účet vlastníka nemůže být vymazán.")] + OwnerDeletionError, + #[error("Reverzní proxy nevrátilo vyžadovanou hlavičku '{}'.", .0)] + HeaderError(&'static str), + #[error("Nástěnka /{}/ je uzamčená.", .0)] + BoardLockError(String), + #[error("Toto vlákno je uzamčené.")] + ThreadLockError, + #[error("Nelze vytvořit odpověď na odpověď.")] + ReplyReplyError, + #[error("Vlákno dosáhlo limitu odpovědí.")] + ReplyLimitError, + #[error("Příspěvek musí mít obsah.")] + NoContentError, + #[error("Příspěvek musí mít soubor.")] + NoFileError, + #[error("Příspěvek musí mít obsah nebo soubor.")] + EmptyPostError, + #[error("Na této nástěnce se musí vyplnit CAPTCHA.")] + RequiredCaptchaError, + #[error("Nesprávné řešení CAPTCHA.")] + IncorrectCaptchaError, + #[error("Tato CAPTCHA neexistuje nebo už byla vyřešena.")] + SolvedCaptchaError, + #[error("Nebyly vybrány žádné příspěvky.")] + NoPostsError, + #[error("Maximální počet souborů na této nástěnce je {}.", .0)] + FileLimitError(usize), + #[error("Nesprávné heslo pro příspěvek #{}.", .0)] + IncorrectPasswordError(i32), + // 500 + #[error("Nadnástěnka nebyla inicializována.")] + OverboardError, + #[error("Server se připojil k 41 procentům.")] + InternalError, +} + +impl From for NekrochanError { + fn from(e: askama::Error) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: ipnetwork::IpNetworkError) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: jsonwebtoken::errors::Error) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: pwhash::error::Error) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: fancy_regex::Error) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: redis::RedisError) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: serde_json::Error) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: serde_qs::Error) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: sqlx::Error) -> Self { + let overboard_err = match e.as_database_error() { + Some(e) => e.message() == "relation \"overboard\" does not exist", + None => false, + }; + + if !overboard_err { + error!("{e:#?}"); + + Self::InternalError + } else { + Self::OverboardError + } + } +} + +impl From for NekrochanError { + fn from(e: std::io::Error) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: std::net::AddrParseError) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl From> for NekrochanError { + fn from(_: std::sync::PoisonError) -> Self { + error!("CAPTCHA RwLock got poisoned or something."); + + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: tokio::task::JoinError) -> Self { + error!("Internal server error: {e:#?}"); + + Self::InternalError + } +} + +impl ResponseError for NekrochanError { + fn status_code(&self) -> StatusCode { + match self { + NekrochanError::FileError(_, _) => StatusCode::BAD_REQUEST, + NekrochanError::UsernameFormatError => StatusCode::BAD_REQUEST, + NekrochanError::PasswordFormatError => StatusCode::BAD_REQUEST, + NekrochanError::IdFormatError => StatusCode::BAD_REQUEST, + NekrochanError::BoardNameFormatError => StatusCode::BAD_REQUEST, + NekrochanError::DescriptionFormatError => StatusCode::BAD_REQUEST, + NekrochanError::PostNameFormatError => StatusCode::BAD_REQUEST, + NekrochanError::CapcodeFormatError => StatusCode::BAD_REQUEST, + NekrochanError::EmailFormatError => StatusCode::BAD_REQUEST, + NekrochanError::ContentFormatError => StatusCode::BAD_REQUEST, + NekrochanError::BoardNotFound(_) => StatusCode::NOT_FOUND, + NekrochanError::AccountNotFound(_) => StatusCode::NOT_FOUND, + NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND, + NekrochanError::InsufficientPermissionError => StatusCode::FORBIDDEN, + NekrochanError::IncorrectCredentialError => StatusCode::UNAUTHORIZED, + NekrochanError::InvalidPageError => StatusCode::NOT_FOUND, + NekrochanError::InvalidAuthError => StatusCode::NOT_FOUND, + NekrochanError::NotLoggedInError => StatusCode::UNAUTHORIZED, + NekrochanError::OwnerDeletionError => StatusCode::FORBIDDEN, + NekrochanError::HeaderError(_) => StatusCode::BAD_GATEWAY, + NekrochanError::BoardLockError(_) => StatusCode::FORBIDDEN, + NekrochanError::ThreadLockError => StatusCode::FORBIDDEN, + NekrochanError::ReplyReplyError => StatusCode::BAD_REQUEST, + NekrochanError::ReplyLimitError => StatusCode::FORBIDDEN, + NekrochanError::NoContentError => StatusCode::BAD_REQUEST, + NekrochanError::NoFileError => StatusCode::BAD_REQUEST, + NekrochanError::EmptyPostError => StatusCode::BAD_REQUEST, + NekrochanError::RequiredCaptchaError => StatusCode::BAD_REQUEST, + NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED, + NekrochanError::SolvedCaptchaError => StatusCode::BAD_REQUEST, + NekrochanError::NoPostsError => StatusCode::BAD_REQUEST, + NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST, + NekrochanError::IncorrectPasswordError(_) => StatusCode::UNAUTHORIZED, + NekrochanError::OverboardError => StatusCode::INTERNAL_SERVER_ERROR, + NekrochanError::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/src/files.rs b/src/files.rs new file mode 100755 index 0000000..fe5e5ef --- /dev/null +++ b/src/files.rs @@ -0,0 +1,350 @@ +use actix_multipart::form::tempfile::TempFile; +use anyhow::Error; +use chrono::Utc; +use glob::glob; +use image::io::Reader as ImageReader; +use std::{collections::HashSet, process::Command}; +use tokio::{ + fs::{remove_file, rename}, + task::spawn_blocking, +}; + +use crate::{ + cfg::Cfg, + ctx::Ctx, + db::models::{Board, File, Post}, + error::NekrochanError, +}; + +impl File { + pub async fn new( + cfg: &Cfg, + temp_file: TempFile, + spoiler: bool, + thumb: bool, + ) -> Result { + let original_name = temp_file.file_name.unwrap_or_else(|| "unknown".into()); + + let mime = temp_file + .content_type + .ok_or(NekrochanError::FileError( + original_name.clone(), + "žádný mime typ", + ))? + .to_string(); + + let (video, format) = match mime.as_str() { + "image/jpeg" => (false, "jpg"), + "image/pjpeg" => (false, "jpg"), + "image/png" => (false, "png"), + "image/bmp" => (false, "bmp"), + "image/gif" => (false, "gif"), + "image/webp" => (false, "webp"), + "image/apng" => (false, "apng"), + "video/mpeg" => (true, "mpeg"), + "video/quicktime" => (true, "mov"), + "video/mp4" => (true, "mp4"), + "video/webm" => (true, "webm"), + "video/x-matroska" => (true, "mkv"), + "video/ogg" => (true, "ogg"), + _ => { + return Err(NekrochanError::FileError( + original_name, + "nepodporovaný formát", + )) + } + }; + + if video && !cfg.files.videos { + return Err(NekrochanError::FileError( + original_name, + "videa nejsou podporovaná", + )); + } + + let size = temp_file.size; + + if size / 1_000_000 > cfg.files.max_size_mb { + return Err(NekrochanError::FileError( + original_name, + "soubor je příliš velký", + )); + } + + let timestamp = Utc::now().timestamp_micros(); + let format = format.to_owned(); + + let new_name = format!("{timestamp}.{format}"); + + let (thumb_format, thumb_name) = if thumb { + let format = if video { "png".into() } else { format.clone() }; + + (Some(format.clone()), Some(format!("{timestamp}.{format}"))) + } else { + (None, None) + }; + + rename(temp_file.file.path(), format!("/tmp/{new_name}")).await?; + + let (width, height) = if video { + process_video(cfg, original_name.clone(), new_name.clone(), thumb_name).await? + } else { + process_image(cfg, original_name.clone(), new_name.clone(), thumb_name).await? + }; + + rename(format!("/tmp/{new_name}"), format!("uploads/{new_name}")).await?; + + let file = File { + original_name, + format, + thumb_format, + spoiler, + width, + height, + timestamp, + size, + }; + + Ok(file) + } + + pub async fn delete(&self) { + remove_file(format!("./uploads/{}.{}", self.timestamp, self.format)) + .await + .ok(); + + if let Some(thumb_format) = &self.thumb_format { + remove_file(format!( + "./uploads/thumb/{}.{}", + self.timestamp, thumb_format + )) + .await + .ok(); + } + } + + pub fn file_url(&self) -> String { + format!("/uploads/{}.{}", self.timestamp, self.format) + } + + pub fn thumb_url(&self) -> String { + if self.spoiler { + "/static/spoiler.png".into() + } else if let Some(thumb_format) = &self.thumb_format { + format!("/uploads/thumb/{}.{}", self.timestamp, thumb_format) + } else { + self.file_url() + } + } +} + +async fn process_image( + cfg: &Cfg, + original_name: String, + new_name: String, + thumb_name: Option, +) -> Result<(u32, u32), NekrochanError> { + let original_name_ = original_name.clone(); + + let img = spawn_blocking(move || { + ImageReader::open(format!("/tmp/{new_name}"))? + .decode() + .map_err(|_| { + NekrochanError::FileError(original_name_, "nepodařilo se dekódovat obrázek") + }) + }) + .await??; + + let (width, height) = (img.width(), img.height()); + + if width > cfg.files.max_width || height > cfg.files.max_height { + return Err(NekrochanError::FileError( + original_name, + "rozměry obrázku jsou příliš velké", + )); + } + + let thumb_name = match thumb_name { + Some(thumb_name) => thumb_name, + None => return Ok((width, height)), + }; + + let thumb_w = if width > cfg.files.thumb_size { + cfg.files.thumb_size + } else { + width + }; + + let thumb_h = if height > cfg.files.thumb_size { + cfg.files.thumb_size + } else { + height + }; + + spawn_blocking(move || { + let thumb = img.thumbnail(thumb_w, thumb_h); + + thumb + .save(format!("./uploads/thumb/{thumb_name}")) + .map_err(|_| { + NekrochanError::FileError(original_name, "nepodařilo se vytvořit náhled obrázku") + }) + }) + .await??; + + Ok((width, height)) +} + +async fn process_video( + cfg: &Cfg, + original_name: String, + new_name: String, + thumb_name: Option, +) -> Result<(u32, u32), NekrochanError> { + let new_name_ = new_name.clone(); + + let ffprobe_out = spawn_blocking(move || { + Command::new("ffprobe") + .args([ + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=width,height", + "-of", + "csv=s=x:p=0", + &format!("/tmp/{new_name_}"), + ]) + .output() + }) + .await??; + + if !ffprobe_out.status.success() { + return Err(NekrochanError::FileError( + original_name, + "nepodařilo se získat rozměry videa", + )); + } + + let invalid_dimensions = "ffprobe vrátil neplatné rozměry"; + + let out_string = String::from_utf8_lossy(&ffprobe_out.stdout); + + let (width, height) = out_string + .trim() + .split_once('x') + .ok_or(NekrochanError::FileError( + original_name.clone(), + invalid_dimensions, + ))?; + + let (width, height) = ( + width + .parse() + .map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?, + height + .parse() + .map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?, + ); + + if width > cfg.files.max_width || height > cfg.files.max_height { + return Err(NekrochanError::FileError( + original_name, + "rozměry videa jsou příliš velké", + )); + } + + let thumb_name = match thumb_name { + Some(thumb_name) => thumb_name, + None => return Ok((width, height)), + }; + + let thumb_size = cfg.files.thumb_size; + + let output = spawn_blocking(move || { + Command::new("ffmpeg") + .args([ + "-i", + &format!("/tmp/{new_name}"), + "-ss", + "00:00:00.50", + "-vframes", + "1", + "-vf", + &format!( + "scale={}", + if width > height { + format!("{thumb_size}:-2") + } else { + format!("-2:{thumb_size}") + } + ), + &format!("./uploads/thumb/{thumb_name}"), + ]) + .output() + }) + .await??; + + if !output.status.success() { + return Err(NekrochanError::FileError( + original_name, + "nepodařilo se vytvořit náhled videa", + )); + } + + Ok((width, height)) +} + +pub async fn cleanup_files(ctx: &Ctx) -> Result<(), Error> { + let mut keep = HashSet::new(); + let mut keep_thumbs = HashSet::new(); + + let boards = Board::read_all(ctx).await?; + + for board in boards.into_iter() { + for file in board.banners.0 { + keep.insert(format!("{}.{}", file.timestamp, file.format)); + } + + let posts = Post::read_all(ctx, board.id.clone()).await?; + + for post in posts.into_iter() { + for file in post.files.0 { + keep.insert(format!("{}.{}", file.timestamp, file.format)); + + if let Some(thumb_format) = file.thumb_format { + keep_thumbs.insert(format!("{}.{}", file.timestamp, thumb_format)); + } + } + } + } + + for file in glob("uploads/*.*")? { + let file = file?; + let file_name = file.file_name(); + + if let Some(file_name) = file_name { + let check = file_name.to_string_lossy().to_string(); + + if !keep.contains(&check) { + remove_file(file).await?; + } + } + } + + for file in glob("uploads/thumb/*.*")? { + let file = file?; + let file_name = file.file_name(); + + if let Some(file_name) = file_name { + let check = file_name.to_string_lossy().to_string(); + + if !keep_thumbs.contains(&check) { + remove_file(file).await?; + } + } + } + + Ok(()) +} diff --git a/src/filters.rs b/src/filters.rs new file mode 100644 index 0000000..fc7771d --- /dev/null +++ b/src/filters.rs @@ -0,0 +1,131 @@ +use chrono::{DateTime, Locale, Utc}; +use fancy_regex::{Captures, Regex}; +use lazy_static::lazy_static; +use std::{collections::HashSet, fmt::Display}; + +use crate::markup::SPOILER_REGEX; + +lazy_static! { + static ref MARKUP_QUOTE_REGEX: Regex = + Regex::new(r#">>(\d+)<\/a>"#).unwrap(); +} + +pub fn czech_humantime(time: &DateTime) -> askama::Result { + let duration = (Utc::now() - *time).abs(); + + let minutes = duration.num_minutes(); + let hours = duration.num_hours(); + let days = duration.num_days(); + let weeks = duration.num_weeks(); + let months = duration.num_days() / 30; + let years = duration.num_days() / 365; + + let mut time = "Teď".into(); + + if minutes > 0 { + time = format!( + "{} {}", + minutes, + czech_plural("minuta|minuty|minut", minutes)? + ); + } + + if hours > 0 { + time = format!("{} {}", hours, czech_plural("hodina|hodiny|hodin", hours)?); + } + + if days > 0 { + time = format!("{} {}", days, czech_plural("den|dny|dnů", days)?); + } + + if weeks > 0 { + time = format!("{} {}", weeks, czech_plural("týden|týdny|týdnů", weeks)?); + } + + if months > 0 { + time = format!( + "{} {}", + months, + czech_plural("měsíc|měsíce|měsíců", months)? + ); + } + + if years > 0 { + time = format!("{} {}", years, czech_plural("rok|roky|let", years)?); + } + + Ok(time) +} + +pub fn czech_datetime(time: &DateTime) -> askama::Result { + let time = time + .format_localized("%d.%m.%Y (%a) %H:%M:%S UTC", Locale::cs_CZ) + .to_string(); + + Ok(time) +} + +pub fn czech_plural(plurals: &str, count: impl Display) -> askama::Result { + let plurals = plurals.split('|').collect::>(); + let count = count.to_string().parse::().unwrap(); + + let one = plurals[0]; + let few = plurals[1]; + let other = plurals[2]; + + if count == 1 { + Ok(one.into()) + } else if count < 5 { + Ok(few.into()) + } else { + Ok(other.into()) + } +} + +pub fn inline_post(input: impl Display) -> askama::Result { + let input = input.to_string(); + + if input.is_empty() { + return Ok("(bez obsahu)".into()); + } + + let collapsed = input.split_whitespace().collect::>().join(" "); + let spoilered = SPOILER_REGEX + .replace_all(&collapsed, "(spoiler)") + .to_string(); + + let truncated = askama::filters::truncate(spoilered, 64)?; + + Ok(truncated) +} + +pub fn get_page(input: &usize, page_size: &i64) -> askama::Result { + let page = crate::paginate(*page_size, *input as i64); + + Ok(page) +} + +pub fn add_yous( + input: impl Display, + board: &String, + yous: &HashSet, +) -> askama::Result { + let input = input.to_string(); + + let output = MARKUP_QUOTE_REGEX.replace_all(&input, |captures: &Captures| { + let quote = &captures[0]; + let id = &captures[1]; + + format!( + "{}{}", + quote, + if yous.contains(&format!("{}/{}", board, id)) { + " (Ty)" + } else { + "" + } + ) + }); + + Ok(output.to_string()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100755 index 0000000..da498ec --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,43 @@ +use error::NekrochanError; +use lazy_static::lazy_static; +use std::{collections::HashMap, sync::RwLock}; + +lazy_static! { + pub static ref CAPTCHA: RwLock> = + RwLock::new(HashMap::new()); +} + +pub mod auth; +pub mod cfg; +pub mod ctx; +pub mod db; +pub mod error; +pub mod files; +pub mod filters; +pub mod markup; +pub mod perms; +pub mod qsform; +pub mod trip; +pub mod web; + +pub fn paginate(page_size: i64, count: i64) -> i64 { + count / page_size + (count % page_size).signum() +} + +pub fn check_page( + page: i64, + pages: i64, + page_limit: impl Into>, +) -> Result<(), NekrochanError> { + if page <= 0 || (page > pages && page != 1) { + return Err(NekrochanError::InvalidPageError); + } + + if let Some(page_limit) = page_limit.into() { + if page > page_limit { + return Err(NekrochanError::InvalidPageError); + } + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 0000000..257e2bb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,154 @@ +use actix_files::{Files, NamedFile}; +use actix_web::{ + body::MessageBody, + dev::ServiceResponse, + get, + http::StatusCode, + middleware::{ErrorHandlerResponse, ErrorHandlers}, + post, + web::Data, + App, HttpResponse, HttpServer, ResponseError, +}; +use anyhow::Error; +use askama::Template; +use log::{error, info}; +use nekrochan::{ + cfg::Cfg, + ctx::Ctx, + db::cache::init_cache, + error::NekrochanError, + files::cleanup_files, + web::{self, template_response}, +}; +use sqlx::migrate; +use std::{env::var, time::Duration}; +use tokio::time::sleep; + +#[tokio::main] +async fn main() { + dotenv::dotenv().ok(); + env_logger::init(); + + if let Err(err) = run().await { + error!("{err:?}"); + } +} + +async fn run() -> Result<(), Error> { + let cfg_path = var("NEKROCHAN_CONFIG").unwrap_or_else(|_| "Nekrochan.toml".into()); + + let cfg = Cfg::load(&cfg_path).await?; + let ctx = Ctx::new(cfg).await?; + + migrate!().run(ctx.db()).await?; + init_cache(&ctx).await?; + + let ctx_ = ctx.clone(); + + tokio::spawn(async move { + loop { + match cleanup_files(&ctx_).await { + Ok(()) => info!("Routine file cleanup successful."), + Err(err) => error!("Routine file cleanup failed: {err:?}"), + }; + + sleep(Duration::from_secs(ctx_.cfg.files.cleanup_interval)).await; + } + }); + + let bind_addr = ctx.bind_addr(); + + HttpServer::new(move || { + App::new() + .app_data(Data::new(ctx.clone())) + .service(web::index::index) + .service(web::board::board) + .service(web::board_catalog::board_catalog) + .service(web::overboard::overboard) + .service(web::overboard_catalog::overboard_catalog) + .service(web::thread::thread) + .service(web::actions::create_post::create_post) + .service(web::actions::user_post_actions::user_post_actions) + .service(web::actions::staff_post_actions::staff_post_actions) + .service(web::actions::report_posts::report_posts) + .service(web::login::login_get) + .service(web::login::login_post) + .service(web::logout::logout) + .service(web::staff::account::account) + .service(web::staff::accounts::accounts) + .service(web::staff::boards::boards) + .service(web::staff::bans::bans) + .service(web::staff::reports::reports) + .service(web::staff::permissions::permissions) + .service(web::staff::banners::banners) + .service(web::staff::board_config::board_config) + .service(web::staff::actions::change_password::change_password) + .service(web::staff::actions::transfer_ownership::transfer_ownership) + .service(web::staff::actions::delete_account::delete_account) + .service(web::staff::actions::remove_accounts::remove_accounts) + .service(web::staff::actions::create_account::create_account) + .service(web::staff::actions::remove_boards::remove_boards) + .service(web::staff::actions::update_boards::update_boards) + .service(web::staff::actions::create_board::create_board) + .service(web::staff::actions::remove_bans::remove_bans) + .service(web::staff::actions::update_permissions::update_permissions) + .service(web::staff::actions::remove_banners::remove_banners) + .service(web::staff::actions::add_banners::add_banners) + .service(web::staff::actions::update_board_config::update_board_config) + .service(debug) + .service(favicon) + .service(Files::new("/static", "./static")) + .service(Files::new("/uploads", "./uploads").disable_content_disposition()) + .wrap(ErrorHandlers::new().default_handler(error_handler)) + }) + .bind(bind_addr)? + .run() + .await?; + + Ok(()) +} + +#[get("/favicon.ico")] +async fn favicon() -> Result { + let favicon = NamedFile::open("./static/favicon.ico")?; + + Ok(favicon) +} + +#[post("/debug")] +async fn debug(req: String) -> HttpResponse { + println!("{req}"); + + HttpResponse::new(StatusCode::OK) +} + +#[derive(Template)] +#[template(path = "error.html")] +struct ErrorTempalate { + error_code: u16, + error_message: String, +} + +fn error_handler(res: ServiceResponse) -> actix_web::Result> +where + B: MessageBody, + ::Error: ResponseError + 'static, +{ + let (req, res) = res.into_parts(); + + let error_code = res.status().as_u16(); + let error_message = match res.into_body().try_into_bytes().ok() { + Some(bytes) => String::from_utf8(bytes.to_vec()).unwrap_or_default(), + None => String::default(), + }; + + let template = ErrorTempalate { + error_code, + error_message, + }; + + let res = template_response(template)?; + let res = ServiceResponse::new(req, res).map_into_right_body(); + + Ok(ErrorHandlerResponse::Response(res)) +} diff --git a/src/markup.rs b/src/markup.rs new file mode 100644 index 0000000..64b8660 --- /dev/null +++ b/src/markup.rs @@ -0,0 +1,216 @@ +use std::collections::HashMap; + +use fancy_regex::{Captures, Regex}; +use lazy_static::lazy_static; +use sqlx::query_as; + +use crate::{ + ctx::Ctx, + db::models::Post, + error::NekrochanError, + perms::PermissionWrapper, + trip::{secure_tripcode, tripcode}, +}; + +lazy_static! { + pub static ref NAME_REGEX: Regex = + Regex::new(r"^([^#].*?)?(?:(##([^ ].*?)|#([^#].*?)))?(##( .*?)?)?$").unwrap(); + pub static ref QUOTE_REGEX: Regex = Regex::new(r">>(\d+)").unwrap(); + pub static ref GREENTEXT_REGEX: Regex = + Regex::new(r"(?m)^>((?!>\d+|>>/\w+(/\d*)?|>>#/).*)") + .unwrap(); + pub static ref ORANGETEXT_REGEX: Regex = Regex::new(r"(?m)^<(.+)").unwrap(); + pub static ref REDTEXT_REGEX: Regex = Regex::new(r"(?m)==(.+?)==").unwrap(); + pub static ref BLUETEXT_REGEX: Regex = Regex::new(r"(?m)--(.+?)--").unwrap(); + pub static ref GLOWTEXT_REGEX: Regex = Regex::new(r"(?m)\%\%(.+?)\%\%").unwrap(); + pub static ref UH_OH_TEXT_REGEX: Regex = Regex::new(r"(?m)\(\(\((.+?)\)\)\)").unwrap(); + pub static ref SPOILER_REGEX: Regex = Regex::new(r"(?m)\|\|([\s\S]+?)\|\|").unwrap(); + pub static ref URL_REGEX: Regex = + Regex::new(r"https?\://[^\s<>\[\]{}|\\^]+").unwrap(); +} + +pub fn parse_name( + ctx: &Ctx, + perms: &PermissionWrapper, + anon_name: &str, + name: &str, +) -> Result<(String, Option, Option), NekrochanError> { + let captures = match NAME_REGEX.captures(name)? { + Some(captures) => captures, + None => return Ok((anon_name.to_owned(), None, None)), + }; + + let name = match captures.get(1) { + Some(name) => { + let name = name.as_str().to_owned(); + + if name.len() > 32 { + return Err(NekrochanError::PostNameFormatError); + } + + name + } + None => anon_name.to_owned(), + }; + + let tripcode = match captures.get(2) { + Some(_) => { + let strip = captures.get(3); + let itrip = captures.get(4); + + if let Some(strip) = strip { + let trip = secure_tripcode(strip.as_str(), &ctx.cfg.secrets.secure_trip); + + Some(format!("!!{trip}")) + } else if let Some(itrip) = itrip { + let trip = tripcode(itrip.as_str()); + + Some(format!("!{trip}")) + } else { + None + } + } + None => None, + }; + + if !(perms.owner() || perms.capcodes()) { + return Ok((name, tripcode, None)); + } + + fn capcode_fallback(owner: bool) -> Option { + if owner { + Some("Admin".into()) + } else { + Some("Uklízeč".into()) + } + } + + let capcode = match captures.get(5) { + Some(_) => match captures.get(6) { + Some(capcode) => { + let capcode: String = capcode.as_str().trim().into(); + + if capcode.is_empty() { + capcode_fallback(perms.owner()) + } else { + if capcode.len() > 32 { + return Err(NekrochanError::CapcodeFormatError); + } + + Some(capcode) + } + } + None => capcode_fallback(perms.owner()), + }, + None => None, + }; + + Ok((name, tripcode, capcode)) +} + +pub async fn markup( + ctx: &Ctx, + board: &String, + op: Option, + text: &str, +) -> Result { + let text = escape_html(text); + let quoted_posts = get_quoted_posts(ctx, board, &text).await?; + + let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| { + let id_raw = &captures[1]; + let id = match id_raw.parse() { + Ok(id) => id, + Err(_) => return format!(">>{id_raw}"), + }; + + let post = quoted_posts.get(&id); + + match post { + Some(post) => format!( + ">>{}{}", + post.post_url(), + post.id, + if op == Some(post.id) { + " (OP)" + } else { + "" + } + ), + None => format!(">>{id}"), + } + }); + + let text = GREENTEXT_REGEX.replace_all(&text, ">$1"); + let text = ORANGETEXT_REGEX.replace_all(&text, "<$1"); + let text = REDTEXT_REGEX.replace_all(&text, "$1"); + let text = BLUETEXT_REGEX.replace_all(&text, "$1"); + let text = GLOWTEXT_REGEX.replace_all(&text, "$1"); + let text = SPOILER_REGEX.replace_all(&text, "$1"); + + let text = UH_OH_TEXT_REGEX.replace_all(&text, |captures: &Captures| { + format!( + "((( {} )))", + captures[1].trim() + ) + }); + + let text = URL_REGEX.replace_all(&text, |captures: &Captures| { + let url = &captures[0]; + + format!("{url}") + }); + + Ok(text.to_string()) +} + +fn escape_html(text: &str) -> String { + text.replace('&', "&") + .replace('\'', "'") + .replace('/', "/") + .replace('`', "`") + .replace('=', "=") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +async fn get_quoted_posts( + ctx: &Ctx, + board: &String, + text: &str, +) -> Result, NekrochanError> { + let mut quoted_ids: Vec = Vec::new(); + + for quote in QUOTE_REGEX.captures_iter(text) { + let id_raw = "e.unwrap()[1]; + let id = match id_raw.parse() { + Ok(id) => id, + Err(_) => continue, + }; + + quoted_ids.push(id); + } + + if quoted_ids.is_empty() { + return Ok(HashMap::new()); + } + + let in_list = quoted_ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(","); + + let quoted_posts = query_as(&format!( + "SELECT * FROM posts_{} WHERE id IN ({})", + board, in_list + )) + .fetch_all(ctx.db()) + .await? + .into_iter() + .map(|post: Post| (post.id, post)) + .collect::>(); + + Ok(quoted_posts) +} diff --git a/src/perms.rs b/src/perms.rs new file mode 100755 index 0000000..28ccb24 --- /dev/null +++ b/src/perms.rs @@ -0,0 +1,85 @@ +use enumflags2::{bitflags, BitFlags}; + +#[bitflags] +#[repr(u64)] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Permissions { + EditPosts, + ManagePosts, + Capcodes, + StaffLog, + Reports, + Bans, + BoardBanners, + BoardConfig, + BypassBans, + BypassBoardLock, + BypassThreadLock, + BypassCaptcha, +} + +pub struct PermissionWrapper(BitFlags, bool); + +impl PermissionWrapper { + pub fn new(perms: u64, owner: bool) -> Self { + Self(BitFlags::from_bits_truncate(perms), owner) + } +} + +impl PermissionWrapper { + pub fn integer(&self) -> u64 { + self.0.bits() + } + + pub fn owner(&self) -> bool { + self.1 + } + + pub fn edit_posts(&self) -> bool { + self.0.contains(Permissions::EditPosts) + } + + pub fn manage_posts(&self) -> bool { + self.0.contains(Permissions::ManagePosts) + } + + pub fn capcodes(&self) -> bool { + self.0.contains(Permissions::Capcodes) + } + + pub fn staff_log(&self) -> bool { + self.0.contains(Permissions::StaffLog) + } + + pub fn reports(&self) -> bool { + self.0.contains(Permissions::Reports) + } + + pub fn bans(&self) -> bool { + self.0.contains(Permissions::Bans) + } + + pub fn board_banners(&self) -> bool { + self.0.contains(Permissions::BoardBanners) + } + + pub fn board_config(&self) -> bool { + self.0.contains(Permissions::BoardConfig) + } + + pub fn bypass_bans(&self) -> bool { + self.0.contains(Permissions::BypassBans) + } + + pub fn bypass_board_lock(&self) -> bool { + self.0.contains(Permissions::BypassBoardLock) + } + + pub fn bypass_thread_lock(&self) -> bool { + self.0.contains(Permissions::BypassThreadLock) + } + + pub fn bypass_captcha(&self) -> bool { + self.0.contains(Permissions::BypassCaptcha) + } +} diff --git a/src/qsform.rs b/src/qsform.rs new file mode 100644 index 0000000..ccfaa53 --- /dev/null +++ b/src/qsform.rs @@ -0,0 +1,43 @@ +use actix_web::{dev::Payload, http::StatusCode, FromRequest, HttpRequest, ResponseError}; +use serde::Deserialize; +use serde_qs::Config; +use std::{future::Future, pin::Pin}; +use thiserror::Error; + +pub struct QsForm(pub T) +where + T: for<'de> Deserialize<'de>; + +#[derive(Debug, Error)] +pub enum QsFormError { + #[error("{}", .0)] + FutureError(#[from] actix_web::Error), + #[error("{}", .0)] + ParseError(#[from] serde_qs::Error), +} + +impl ResponseError for QsFormError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl FromRequest for QsForm +where + T: for<'de> Deserialize<'de>, +{ + type Error = QsFormError; + type Future = Pin, Self::Error>>>>; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let data = String::from_request(req, payload); + + Box::pin(async move { + let data = data.await?; + let config = Config::new(10, false); + let form: T = config.deserialize_str(&data)?; + + Ok(QsForm(form)) + }) + } +} diff --git a/src/trip.rs b/src/trip.rs new file mode 100755 index 0000000..0818739 --- /dev/null +++ b/src/trip.rs @@ -0,0 +1,54 @@ +use encoding::{all::WINDOWS_31J, EncoderTrap, Encoding}; +use pwhash::{ + bcrypt::{self, BcryptSetup}, + unix::crypt, +}; + +pub fn tripcode(password: &str) -> String { + let password = WINDOWS_31J.encode(password, EncoderTrap::Replace).unwrap(); + + let salt = [password.as_ref(), "H.".as_bytes()].concat(); + let salt = &salt[1..3]; + let salt = salt + .iter() + .map(|c| match c { + 46..=122 => *c, + _ => 46, + } as char) + .map(|c| match c { + ':' => 'A', + ';' => 'B', + '<' => 'C', + '=' => 'D', + '>' => 'E', + '?' => 'F', + '@' => 'G', + '[' => 'a', + '\\' => 'b', + ']' => 'c', + '^' => 'd', + '_' => 'e', + '`' => 'f', + _ => c, + }) + .collect::(); + + let trip = crypt(password, &salt).unwrap(); + + trip[3..].to_owned() +} + +pub fn secure_tripcode(password: &str, tripcode_secret: &str) -> String { + let trip = bcrypt::hash_with( + BcryptSetup { + salt: Some(tripcode_secret), + ..Default::default() + }, + password, + ) + .unwrap(); + + let trip = &trip[trip.len() - 10..]; + + trip.into() +} diff --git a/src/web/actions/create_post.rs b/src/web/actions/create_post.rs new file mode 100644 index 0000000..3d8f7c4 --- /dev/null +++ b/src/web/actions/create_post.rs @@ -0,0 +1,250 @@ +use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; +use actix_web::{ + cookie::Cookie, http::StatusCode, post, web::Data, HttpRequest, HttpResponse, + HttpResponseBuilder, +}; +use pwhash::bcrypt::hash; +use sha256::digest; + +use crate::{ + ctx::Ctx, + db::models::{Ban, Board, File, Post}, + error::NekrochanError, + markup::{markup, parse_name}, + perms::PermissionWrapper, + web::{ + ban_response, + tcx::{account_from_auth_opt, ip_from_req}, + }, + CAPTCHA, +}; + +#[derive(MultipartForm)] +pub struct PostForm { + pub board: Text, + pub thread: Option>, + pub name: Text, + pub email: Text, + pub content: Text, + #[multipart(rename = "files[]")] + pub files: Vec, + pub spoiler_files: Option>, + pub password: Text, + pub captcha_id: Option>, + pub captcha_solution: Option>, +} + +#[post("/actions/create-post")] +pub async fn create_post( + ctx: Data, + req: HttpRequest, + MultipartForm(form): MultipartForm, +) -> Result { + let perms = match account_from_auth_opt(&ctx, &req).await? { + Some(account) => account.perms(), + None => PermissionWrapper::new(0, false), + }; + + let (ip, country) = ip_from_req(&req)?; + + let board = form.board.0; + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + if let Some(ban) = Ban::read(&ctx, board.id.clone(), ip).await? { + if !(perms.owner() || perms.bypass_bans()) { + return ban_response(&ctx, &req, ban).await; + } + } + + if board.config.0.locked && !(perms.owner() || perms.bypass_board_lock()) { + return Err(NekrochanError::BoardLockError(board.id.clone())); + } + + let mut bumpy_bump = true; + let mut noko = true; + + let thread = match form.thread { + Some(Text(thread)) => { + let thread = Post::read(&ctx, board.id.clone(), thread) + .await? + .ok_or(NekrochanError::PostNotFound(board.id.clone(), thread))?; + + if thread.thread.is_some() { + return Err(NekrochanError::ReplyReplyError); + } + + if thread.locked && !(perms.owner() || perms.bypass_thread_lock()) { + return Err(NekrochanError::ThreadLockError); + } + + if thread.replies >= board.config.0.reply_limit { + return Err(NekrochanError::ReplyLimitError); + } + + if thread.bumps >= board.config.0.bump_limit { + bumpy_bump = false; + } + + Some(thread) + } + None => None, + }; + + let difficulties = ["easy", "medium", "hard"]; + + let difficulty = if thread.is_none() { + if difficulties.contains(&board.config.0.thread_captcha.as_str()) { + Some(board.config.0.thread_captcha.clone()) + } else { + None + } + } else if difficulties.contains(&board.config.0.reply_captcha.as_str()) { + Some(board.config.0.reply_captcha.clone()) + } else { + None + }; + + if let Some(difficulty) = difficulty { + let board = board.id.clone(); + let id = form + .captcha_id + .ok_or(NekrochanError::RequiredCaptchaError)? + .0; + + let key = (board, difficulty, id); + let solution = form + .captcha_solution + .ok_or(NekrochanError::RequiredCaptchaError)?; + + let actual_solution = CAPTCHA + .write()? + .remove(&key) + .ok_or(NekrochanError::SolvedCaptchaError)?; + + if solution.trim() != actual_solution { + return Err(NekrochanError::IncorrectCaptchaError); + } + } + + let name_raw = form.name.trim(); + let (name, tripcode, capcode) = parse_name(&ctx, &perms, &board.config.0.anon_name, name_raw)?; + + let email_raw = form.email.trim(); + + let email = if !email_raw.is_empty() { + if email_raw.len() > 256 { + return Err(NekrochanError::EmailFormatError); + } + + if email_raw == "sage" || email_raw == "nonokosage" { + bumpy_bump = false; + } + + if email_raw == "nonoko" || email_raw == "nonokosage" { + noko = false; + } + + Some(email_raw.into()) + } else { + None + }; + + let content_nomarkup = form.content.0.trim().to_owned(); + + if content_nomarkup.is_empty() && (thread.is_none() && board.config.0.require_thread_content) + || (thread.is_some() && board.config.0.require_reply_content) + { + return Err(NekrochanError::NoContentError); + } + + if content_nomarkup.len() > 4000 { + return Err(NekrochanError::ContentFormatError); + } + + let content = markup( + &ctx, + &board.id, + thread.as_ref().map(|t| t.id), + &content_nomarkup, + ) + .await?; + + if form.files.len() > board.config.0.file_limit { + return Err(NekrochanError::FileLimitError(board.config.0.file_limit)); + } + + let mut files = Vec::new(); + + for file in form.files.into_iter() { + if file.size == 0 { + continue; + } + + let spoiler = form.spoiler_files.is_some(); + let file = File::new(&ctx.cfg, file, spoiler, true).await?; + + files.push(file); + } + + if content_nomarkup.is_empty() && files.is_empty() { + return Err(NekrochanError::EmptyPostError); + } + + let password_raw = form.password.trim(); + + if password_raw.len() < 8 { + return Err(NekrochanError::PasswordFormatError); + } + + let password = hash(password_raw)?; + let thread_id = thread.as_ref().map(|t| t.id); + + let post = Post::create( + &ctx, + &board, + thread_id, + name, + tripcode, + capcode, + email, + content, + content_nomarkup, + files, + password, + country, + ip, + bumpy_bump, + ) + .await?; + + let ts = thread + .as_ref() + .map(|thread| thread.created.timestamp_micros()) + .unwrap_or_else(|| post.created.timestamp_micros()); + + let hash_input = format!("{}:{}:{}", ip, ts, ctx.cfg.secrets.user_id); + let user_hash = digest(hash_input); + let user_id = user_hash[..6].to_owned(); + + post.update_user_id(&ctx, user_id).await?; + + let mut res = HttpResponseBuilder::new(StatusCode::SEE_OTHER); + + let name_cookie = Cookie::build("name", name_raw).path("/").finish(); + let password_cookie = Cookie::build("password", password_raw).path("/").finish(); + + res.cookie(name_cookie); + res.cookie(password_cookie); + + let res = if noko { + res.append_header(("Location", post.post_url().as_str())) + .finish() + } else { + res.append_header(("Location", format!("/boards/{}", post.board).as_str())) + .finish() + }; + + Ok(res) +} diff --git a/src/web/actions/mod.rs b/src/web/actions/mod.rs new file mode 100644 index 0000000..a6a58d5 --- /dev/null +++ b/src/web/actions/mod.rs @@ -0,0 +1,38 @@ +use askama::Template; + +use super::tcx::TemplateCtx; +use crate::{ctx::Ctx, db::models::Post}; + +pub mod create_post; +pub mod staff_post_actions; +pub mod report_posts; +pub mod user_post_actions; + +#[derive(Template)] +#[template(path = "action.html")] +pub struct ActionTemplate { + pub tcx: TemplateCtx, + pub response: String, +} + +pub async fn get_posts_from_ids(ctx: &Ctx, ids: Vec) -> Vec { + let mut posts = Vec::new(); + + for id in ids.into_iter() { + if let Some((board, id)) = parse_id(id) { + if let Ok(Some(post)) = Post::read(ctx, board, id).await { + posts.push(post); + } + } + } + + posts +} + +fn parse_id(id: String) -> Option<(String, i32)> { + let (board, id) = id.split_once('/')?; + let board = board.to_owned(); + let id = id.parse().ok()?; + + Some((board, id)) +} diff --git a/src/web/actions/report_posts.rs b/src/web/actions/report_posts.rs new file mode 100644 index 0000000..db4a2dc --- /dev/null +++ b/src/web/actions/report_posts.rs @@ -0,0 +1,106 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; +use std::fmt::Write; + +use crate::{ + ctx::Ctx, + db::models::{Ban, Board}, + error::NekrochanError, + qsform::QsForm, + web::{ + actions::{get_posts_from_ids, ActionTemplate}, + ban_response, + tcx::{ip_from_req, TemplateCtx}, + template_response, + }, +}; + +#[derive(Deserialize)] +pub struct ReportPostsForm { + #[serde(default)] + pub posts: Vec, + pub report_reason: String, +} + +#[post("/actions/report-posts")] +pub async fn report_posts( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let (reporter_ip, reporter_country) = ip_from_req(&req)?; + let bans = Ban::read_by_ip(&ctx, reporter_ip).await?; + + if let Some(ban) = bans.get(&None) { + if !(tcx.perms.owner() || tcx.perms.bypass_bans()) { + return ban_response(&ctx, &req, ban.clone()).await; + } + } + + let boards = Board::read_all_map(&ctx).await?; + let posts = get_posts_from_ids(&ctx, form.posts).await; + + let mut response = String::new(); + let mut posts_reported = 0; + + let reason = form.report_reason.trim(); + + for post in posts.iter() { + let board = &boards[&post.board]; + + if bans.contains_key(&Some(board.id.clone())) + && !(tcx.perms.owner() || tcx.perms.bypass_bans()) + { + writeln!(&mut response, "[Chyba] Jsi zabanován z /{}/.", board.id).ok(); + continue; + } + + if board.config.0.locked && !(tcx.perms.owner() || tcx.perms.bypass_board_lock()) { + writeln!( + &mut response, + "[Chyba] {}", + NekrochanError::BoardLockError(board.id.to_owned()) + ) + .ok(); + + continue; + } + + if post + .reports + .iter() + .any(|report| report.reporter_ip == reporter_ip) + { + writeln!( + &mut response, + "[Chyba] Příspěvek #{} jsi už nahlásil.", + post.id + ) + .ok(); + continue; + } + + post.create_report( + &ctx, + reason.to_owned(), + reporter_country.clone(), + reporter_ip, + ) + .await?; + + posts_reported += 1; + } + + if posts_reported != 0 { + writeln!( + &mut response, + "[Úspěch] Nahlášeny příspěvky: {posts_reported}" + ) + .ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(template) +} diff --git a/src/web/actions/staff_post_actions.rs b/src/web/actions/staff_post_actions.rs new file mode 100644 index 0000000..e8f9105 --- /dev/null +++ b/src/web/actions/staff_post_actions.rs @@ -0,0 +1,219 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use chrono::{Duration, Utc}; +use ipnetwork::IpNetwork; +use serde::Deserialize; +use std::{collections::HashSet, fmt::Write}; + +use crate::{ + ctx::Ctx, + db::models::Ban, + error::NekrochanError, + qsform::QsForm, + web::{ + actions::{get_posts_from_ids, ActionTemplate}, + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Deserialize)] +pub struct JannyPostActionsForm { + #[serde(default)] + pub posts: Vec, + #[serde(rename = "staff_remove_posts")] + pub remove_posts: Option, + #[serde(rename = "staff_remove_files")] + pub remove_files: Option, + #[serde(rename = "staff_toggle_spoiler")] + pub toggle_spoiler: Option, + pub toggle_sticky: Option, + pub toggle_lock: Option, + pub ban_user: Option, + pub global_ban: Option, + pub unappealable_ban: Option, + pub ban_reason: Option, + pub ban_duration: Option, + pub ban_range: Option, +} + +#[post("/actions/staff-post-actions")] +pub async fn staff_post_actions( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let account = account_from_auth(&ctx, &req).await?; + let posts = get_posts_from_ids(&ctx, form.posts).await; + + let mut response = String::new(); + + let mut posts_removed = 0; + let mut files_removed = 0; + let mut spoilers_toggled = 0; + let mut stickies_toggled = 0; + let mut locks_toggled = 0; + let mut bans_issued = 0; + + for post in posts.iter() { + if (form.remove_posts.is_some() + || form.remove_files.is_some() + || form.toggle_spoiler.is_some() + || form.toggle_sticky.is_some() + || form.toggle_lock.is_some()) + && !(account.perms().owner() || account.perms().manage_posts()) + { + writeln!( + &mut response, + "[Chyba] Nemáš oprávnění spravovat příspěvky." + ) + .ok(); + + continue; + } + + if form.remove_posts.is_some() { + post.delete(&ctx).await?; + posts_removed += 1; + } + + if form.remove_files.is_some() { + post.delete_files(&ctx).await?; + files_removed += post.files.0.len(); + } + + if form.toggle_spoiler.is_some() { + post.update_spoiler(&ctx).await?; + spoilers_toggled += post.files.0.len(); + } + + if form.toggle_sticky.is_some() { + post.update_sticky(&ctx).await?; + stickies_toggled += post.files.0.len(); + } + + if form.toggle_lock.is_some() { + post.update_lock(&ctx).await?; + locks_toggled += post.files.0.len(); + } + } + + let mut already_banned = HashSet::new(); + + for post in posts.into_iter() { + if let (Some(_), Some(ban_reason), Some(ban_duration), Some(ban_range)) = ( + form.ban_user.clone(), + form.ban_reason.clone(), + form.ban_duration, + form.ban_range.clone(), + ) { + if !(account.perms().owner() || account.perms().bans()) { + writeln!(&mut response, "[Chyba] Nemáš oprávnění vydat ban.").ok(); + continue; + } + + if already_banned.contains(&post.ip) { + continue; + } + + let account = account.username.clone(); + + let board = if form.global_ban.is_none() { + Some(post.board.clone()) + } else { + None + }; + + let prefix = if post.ip.is_ipv4() { + match ban_range.as_str() { + "ip" => 32, + "lan" => 24, + "isp" => 16, + _ => 32, + } + } else { + match ban_range.as_str() { + "ip" => 128, + "lan" => 48, + "isp" => 24, + _ => 128, + } + }; + + let ip_range = IpNetwork::new(post.ip, prefix)?; + let reason = ban_reason.trim().into(); + let appealable = form.unappealable_ban.is_none(); + + let expires = if ban_duration != 0 { + Some(Utc::now() + Duration::days(ban_duration)) + } else { + None + }; + + Ban::create(&ctx, account, board, ip_range, reason, appealable, expires).await?; + + let content_nomarkup = format!( + "{}\n\n==(UŽIVATEL BYL ZA TENTO PŘÍSPĚVEK ZABANOVÁN)==", + post.content_nomarkup + ); + + let content = format!( + "{}\n\n(UŽIVATEL BYL ZA TENTO PŘÍSPĚVEK ZABANOVÁN)", + post.content + ); + + post.update_content(&ctx, content, content_nomarkup).await?; + + already_banned.insert(post.ip); + bans_issued += 1; + } + } + + if posts_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněny příspěvky: {posts_removed}" + ) + .ok(); + } + + if files_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněny soubory: {files_removed}" + ) + .ok(); + } + + if spoilers_toggled != 0 { + writeln!( + &mut response, + "[Úspěch] Přepnuty spoilery: {spoilers_toggled}" + ) + .ok(); + } + + if stickies_toggled != 0 { + writeln!( + &mut response, + "[Úspěch] Připnuta/odepnuta vlákna: {stickies_toggled}" + ) + .ok(); + } + + if locks_toggled != 0 { + writeln!( + &mut response, + "[Úspěch] Zamčena/odemčena vlákna: {locks_toggled}" + ) + .ok(); + } + + if bans_issued != 0 { + writeln!(&mut response, "[Úspěch] Uděleny bany: {bans_issued}").ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(template) +} diff --git a/src/web/actions/user_post_actions.rs b/src/web/actions/user_post_actions.rs new file mode 100644 index 0000000..83ef9e2 --- /dev/null +++ b/src/web/actions/user_post_actions.rs @@ -0,0 +1,135 @@ +use std::fmt::Write; + +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use pwhash::bcrypt::verify; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, + db::models::{Ban, Board}, + error::NekrochanError, + qsform::QsForm, + web::{ + actions::{get_posts_from_ids, ActionTemplate}, + ban_response, + tcx::{ip_from_req, TemplateCtx}, + template_response, + }, +}; + +#[derive(Deserialize)] +pub struct UserPostActionsForm { + #[serde(default)] + pub posts: Vec, + pub remove_posts: Option, + pub remove_files: Option, + pub toggle_spoiler: Option, + pub password: String, +} + +#[post("/actions/user-post-actions")] +pub async fn user_post_actions( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let (ip, _) = ip_from_req(&req)?; + let bans = Ban::read_by_ip(&ctx, ip).await?; + + if let Some(ban) = bans.get(&None) { + if !(tcx.perms.owner() || tcx.perms.bypass_bans()) { + return ban_response(&ctx, &req, ban.clone()).await; + } + } + + let posts = get_posts_from_ids(&ctx, form.posts).await; + let boards = Board::read_all_map(&ctx).await?; + + let mut response = String::new(); + + let mut posts_removed = 0; + let mut files_removed = 0; + let mut spoilers_toggled = 0; + + for post in posts.iter() { + let board = &boards[&post.board]; + + if bans.contains_key(&Some(board.id.clone())) + && !(tcx.perms.owner() || tcx.perms.bypass_bans()) + { + writeln!(&mut response, "[Chyba] Jsi zabanován z /{}/.", board.id).ok(); + continue; + } + + if board.config.0.locked && !(tcx.perms.owner() || tcx.perms.bypass_board_lock()) { + writeln!( + &mut response, + "[Chyba] {}", + NekrochanError::BoardLockError(board.id.to_owned()) + ) + .ok(); + + continue; + } + + if !verify(&form.password, &post.password) { + writeln!( + &mut response, + "[Chyba] {}", + NekrochanError::IncorrectPasswordError(post.id) + ) + .ok(); + continue; + } + + if form.remove_posts.is_some() { + post.delete(&ctx).await?; + posts_removed += 1; + } + + if form.remove_files.is_some() { + if (post.thread.is_none() && board.config.0.require_thread_file) + || (post.thread.is_some() && board.config.0.require_reply_file) + { + writeln!(&mut response, "[Chyba] Soubor je na tomto místě potřebný.").ok(); + } else { + post.delete_files(&ctx).await?; + files_removed += post.files.0.len(); + } + } + + if form.toggle_spoiler.is_some() { + post.update_spoiler(&ctx).await?; + spoilers_toggled += post.files.0.len(); + } + } + + if posts_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněny příspěvky: {posts_removed}" + ) + .ok(); + } + + if files_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněny soubory: {files_removed}" + ) + .ok(); + } + + if spoilers_toggled != 0 { + writeln!( + &mut response, + "[Úspěch] Přepnuty spoilery: {spoilers_toggled}" + ) + .ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(template) +} diff --git a/src/web/board.rs b/src/web/board.rs new file mode 100755 index 0000000..0d5d60c --- /dev/null +++ b/src/web/board.rs @@ -0,0 +1,74 @@ +use actix_web::{ + get, + web::{Data, Path, Query}, + HttpRequest, HttpResponse, +}; +use askama::Template; +use redis::AsyncCommands; +use serde::Deserialize; + +use crate::{ + check_page, + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, paginate, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Deserialize)] +pub struct BoardQuery { + page: i64, +} + +#[derive(Template)] +#[template(path = "board.html")] +struct BoardTemplate { + tcx: TemplateCtx, + board: Board, + threads: Vec<(Post, Vec)>, + page: i64, + pages: i64, +} + +#[get("/boards/{board}")] +pub async fn board( + ctx: Data, + req: HttpRequest, + path: Path, + query: Option>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let board = path.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let count = ctx + .cache() + .get(format!("board_threads:{}", board.id)) + .await?; + let page = query.map(|q| q.page).unwrap_or(1); + let pages = paginate(board.config.0.page_size, count); + + check_page(page, pages, board.config.0.page_count)?; + + let mut threads = Vec::new(); + + for thread in Post::read_board_page(&ctx, &board, page).await?.into_iter() { + let replies = thread.read_replies(&ctx).await?; + + threads.push((thread, replies)) + } + + let template = BoardTemplate { + tcx, + board, + threads, + page, + pages, + }; + + template_response(template) +} diff --git a/src/web/board_catalog.rs b/src/web/board_catalog.rs new file mode 100644 index 0000000..1f3fb22 --- /dev/null +++ b/src/web/board_catalog.rs @@ -0,0 +1,46 @@ +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "board_catalog.html")] +struct BoardCatalogTemplate { + tcx: TemplateCtx, + board: Board, + threads: Vec, +} + +#[get("/boards/{board}/catalog")] +pub async fn board_catalog( + ctx: Data, + req: HttpRequest, + path: Path, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let board = path.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let threads = Post::read_board_catalog(&ctx, board.id.clone()).await?; + + let template = BoardCatalogTemplate { + tcx, + board, + threads, + }; + + template_response(template) +} diff --git a/src/web/index.rs b/src/web/index.rs new file mode 100755 index 0000000..bda31ec --- /dev/null +++ b/src/web/index.rs @@ -0,0 +1,24 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use super::tcx::TemplateCtx; +use crate::{ctx::Ctx, db::models::Post, error::NekrochanError, filters, web::template_response}; + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate { + tcx: TemplateCtx, + posts: Vec, + files: Vec, +} + +#[get("/")] +pub async fn index(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let posts = Post::read_latest(&ctx).await.unwrap_or_else(|_| Vec::new()); + let files = Post::read_files(&ctx).await.unwrap_or_else(|_| Vec::new()); + + let template = IndexTemplate { tcx, posts, files }; + + template_response(template) +} diff --git a/src/web/login.rs b/src/web/login.rs new file mode 100755 index 0000000..3778194 --- /dev/null +++ b/src/web/login.rs @@ -0,0 +1,59 @@ +use actix_web::{ + cookie::Cookie, get, http::StatusCode, post, web::Data, HttpRequest, HttpResponse, + HttpResponseBuilder, +}; +use askama::Template; +use pwhash::bcrypt::verify; +use serde::Deserialize; + +use crate::{ + auth::Claims, + ctx::Ctx, + db::models::Account, + error::NekrochanError, + qsform::QsForm, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "login.html")] +struct LogInTemplate { + tcx: TemplateCtx, +} + +#[get("/login")] +pub async fn login_get(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let template = LogInTemplate { tcx }; + + template_response(template) +} + +#[derive(Deserialize)] +pub struct LogInForm { + username: String, + password: String, +} + +#[post("/login")] +pub async fn login_post( + ctx: Data, + QsForm(form): QsForm, +) -> Result { + let account = Account::read(&ctx, form.username.clone()) + .await? + .ok_or(NekrochanError::IncorrectCredentialError)?; + + if !verify(form.password, &account.password) { + return Err(NekrochanError::IncorrectCredentialError); + } + + let auth = Claims::new(account.username).encode(&ctx)?; + + let res = HttpResponseBuilder::new(StatusCode::SEE_OTHER) + .append_header(("Location", "/staff/account")) + .cookie(Cookie::new("auth", auth)) + .finish(); + + Ok(res) +} diff --git a/src/web/logout.rs b/src/web/logout.rs new file mode 100755 index 0000000..f9c8d06 --- /dev/null +++ b/src/web/logout.rs @@ -0,0 +1,13 @@ +use actix_web::{cookie::Cookie, get, http::StatusCode, HttpResponse, HttpResponseBuilder}; + +#[get("/logout")] +pub async fn logout() -> HttpResponse { + let mut auth = Cookie::named("auth"); + + auth.make_removal(); + + HttpResponseBuilder::new(StatusCode::SEE_OTHER) + .append_header(("Location", "/")) + .cookie(auth) + .finish() +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100755 index 0000000..9c0a270 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,45 @@ +use actix_web::{http::StatusCode, HttpRequest, HttpResponse, HttpResponseBuilder}; +use askama::Template; + +pub mod actions; +pub mod board; +pub mod board_catalog; +pub mod index; +pub mod login; +pub mod logout; +pub mod overboard; +pub mod overboard_catalog; +pub mod staff; +pub mod tcx; +pub mod thread; + +use self::tcx::TemplateCtx; +use crate::{ctx::Ctx, db::models::Ban, error::NekrochanError, filters}; + +#[derive(Template)] +#[template(path = "banned.html")] +struct BannedTemplate { + tcx: TemplateCtx, + ban: Ban, +} + +pub async fn ban_response( + ctx: &Ctx, + req: &HttpRequest, + ban: Ban, +) -> Result { + let tcx = TemplateCtx::new(ctx, req).await?; + + template_response(BannedTemplate { tcx, ban }) +} + +pub fn template_response(template: T) -> Result +where + T: Template, +{ + let res = HttpResponseBuilder::new(StatusCode::OK) + .append_header(("Content-Type", "text/html")) + .body(template.render()?); + + Ok(res) +} diff --git a/src/web/overboard.rs b/src/web/overboard.rs new file mode 100644 index 0000000..7bb37ae --- /dev/null +++ b/src/web/overboard.rs @@ -0,0 +1,67 @@ +use actix_web::{ + get, + web::{Data, Query}, + HttpRequest, HttpResponse, +}; +use askama::Template; +use redis::AsyncCommands; +use serde::Deserialize; +use std::collections::HashMap; + +use crate::{ + check_page, + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, paginate, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Deserialize)] +pub struct OverboardQuery { + page: i64, +} + +#[derive(Template)] +#[template(path = "overboard.html")] +struct OverboardTemplate { + tcx: TemplateCtx, + boards: HashMap, + threads: Vec<(Post, Vec)>, + page: i64, + pages: i64, +} + +#[get("/overboard")] +pub async fn overboard( + ctx: Data, + req: HttpRequest, + query: Option>, +) -> Result { + let boards = Board::read_all_map(&ctx).await?; + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let count = ctx.cache().get("total_threads").await?; + let page = query.map(|q| q.page).unwrap_or(1); + let pages = paginate(15, count); + + check_page(page, pages, None)?; + + let mut threads = Vec::new(); + + for thread in Post::read_overboard_page(&ctx, page).await?.into_iter() { + let replies = thread.read_replies(&ctx).await?; + + threads.push((thread, replies)) + } + + let template = OverboardTemplate { + tcx, + boards, + threads, + page, + pages, + }; + + template_response(template) +} diff --git a/src/web/overboard_catalog.rs b/src/web/overboard_catalog.rs new file mode 100644 index 0000000..5758167 --- /dev/null +++ b/src/web/overboard_catalog.rs @@ -0,0 +1,30 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Post, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "overboard_catalog.html")] +struct OverboardCatalogTemplate { + tcx: TemplateCtx, + threads: Vec, +} + +#[get("/overboard/catalog")] +pub async fn overboard_catalog( + ctx: Data, + req: HttpRequest, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let threads = Post::read_overboard_catalog(&ctx).await?; + + let template = OverboardCatalogTemplate { tcx, threads }; + + template_response(template) +} diff --git a/src/web/staff/account.rs b/src/web/staff/account.rs new file mode 100755 index 0000000..0924bea --- /dev/null +++ b/src/web/staff/account.rs @@ -0,0 +1,28 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Account, + error::NekrochanError, + web::{ + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Template)] +#[template(path = "staff/account.html")] +struct AccountTemplate { + tcx: TemplateCtx, + account: Account, +} + +#[get("/staff/account")] +pub async fn account(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let account = account_from_auth(&ctx, &req).await?; + let template = AccountTemplate { tcx, account }; + + template_response(template) +} diff --git a/src/web/staff/accounts.rs b/src/web/staff/accounts.rs new file mode 100755 index 0000000..0a47f57 --- /dev/null +++ b/src/web/staff/accounts.rs @@ -0,0 +1,26 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Account, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/accounts.html")] +struct AccountsTemplate { + tcx: TemplateCtx, + accounts: Vec, +} + +#[get("/staff/accounts")] +pub async fn accounts(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let accounts = Account::read_all(&ctx).await?; + let template = AccountsTemplate { tcx, accounts }; + + template_response(template) +} diff --git a/src/web/staff/actions/add_banners.rs b/src/web/staff/actions/add_banners.rs new file mode 100755 index 0000000..7adf441 --- /dev/null +++ b/src/web/staff/actions/add_banners.rs @@ -0,0 +1,55 @@ +use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; + +use crate::{ + ctx::Ctx, + db::models::{Board, File}, + error::NekrochanError, + web::tcx::account_from_auth, +}; + +#[derive(MultipartForm)] +pub struct AddBannersForm { + board: Text, + #[multipart(rename = "files[]")] + files: Vec, +} + +#[post("/staff/actions/add-banners")] +pub async fn add_banners( + ctx: Data, + req: HttpRequest, + MultipartForm(form): MultipartForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().board_banners()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let board = form.board.0; + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let mut new_banners = board.banners.0.clone(); + let added_banners = form.files; + + let mut cfg = ctx.cfg.clone(); + + cfg.files.videos = false; + + for banner in added_banners.into_iter() { + let file = File::new(&cfg, banner, false, false).await?; + + new_banners.push(file) + } + + board.update_banners(&ctx, new_banners).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", format!("/staff/banners/{}", board.id).as_str())) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/change_password.rs b/src/web/staff/actions/change_password.rs new file mode 100755 index 0000000..0c7f62c --- /dev/null +++ b/src/web/staff/actions/change_password.rs @@ -0,0 +1,38 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use pwhash::bcrypt::{hash, verify}; +use serde::Deserialize; + +use crate::{ctx::Ctx, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth}; + +#[derive(Deserialize)] +pub struct ChangePasswordForm { + old_password: String, + new_password: String, +} + +#[post("/staff/actions/change-password")] +pub async fn change_password( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !verify(form.old_password, &account.password) { + return Err(NekrochanError::IncorrectCredentialError); + } + + if form.new_password.len() < 8 { + return Err(NekrochanError::PasswordFormatError); + } + + let password = hash(form.new_password)?; + + account.update_password(&ctx, password).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/account")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/create_account.rs b/src/web/staff/actions/create_account.rs new file mode 100755 index 0000000..a72c58d --- /dev/null +++ b/src/web/staff/actions/create_account.rs @@ -0,0 +1,48 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use pwhash::bcrypt::hash; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Account, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct CreateAccountForm { + username: String, + password: String, +} + +#[post("/staff/actions/create-account")] +pub async fn create_account( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let username = form.username.trim().to_owned(); + let password = form.password.trim().to_owned(); + + if username.is_empty() || username.len() > 32 { + return Err(NekrochanError::UsernameFormatError); + } + + if password.len() < 8 { + return Err(NekrochanError::PasswordFormatError); + } + + let password = hash(password)?; + + let _ = Account::create(&ctx, username, password).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/accounts")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/create_board.rs b/src/web/staff/actions/create_board.rs new file mode 100755 index 0000000..237bc20 --- /dev/null +++ b/src/web/staff/actions/create_board.rs @@ -0,0 +1,50 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct CreateBoardForm { + id: String, + name: String, + description: String, +} + +#[post("/staff/actions/create-board")] +pub async fn create_board( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let id = form.id.trim().to_owned(); + let name = form.name.trim().to_owned(); + let description = form.description.trim().to_owned(); + + if id.is_empty() || id.len() > 16 { + return Err(NekrochanError::IdFormatError); + } + + if name.is_empty() || name.len() > 32 { + return Err(NekrochanError::BoardNameFormatError); + } + + if description.is_empty() || description.len() > 128 { + return Err(NekrochanError::DescriptionFormatError); + } + + let _ = Board::create(&ctx, id, name, description).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/boards")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/delete_account.rs b/src/web/staff/actions/delete_account.rs new file mode 100755 index 0000000..1857a9b --- /dev/null +++ b/src/web/staff/actions/delete_account.rs @@ -0,0 +1,23 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; + +use crate::{ctx::Ctx, error::NekrochanError, web::tcx::account_from_auth}; + +#[post("/staff/actions/delete-account")] +pub async fn delete_account( + ctx: Data, + req: HttpRequest, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if account.perms().owner() { + return Err(NekrochanError::OwnerDeletionError); + } + + account.delete(&ctx).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/logout")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/mod.rs b/src/web/staff/actions/mod.rs new file mode 100755 index 0000000..2921538 --- /dev/null +++ b/src/web/staff/actions/mod.rs @@ -0,0 +1,13 @@ +pub mod add_banners; +pub mod change_password; +pub mod create_account; +pub mod create_board; +pub mod delete_account; +pub mod remove_accounts; +pub mod remove_banners; +pub mod remove_bans; +pub mod remove_boards; +pub mod transfer_ownership; +pub mod update_board_config; +pub mod update_boards; +pub mod update_permissions; diff --git a/src/web/staff/actions/remove_accounts.rs b/src/web/staff/actions/remove_accounts.rs new file mode 100755 index 0000000..27c2833 --- /dev/null +++ b/src/web/staff/actions/remove_accounts.rs @@ -0,0 +1,42 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Account, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct RemoveAccountsForm { + #[serde(default)] + accounts: Vec, +} + +#[post("/staff/actions/remove-accounts")] +pub async fn remove_accounts( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + for account in form.accounts.into_iter() { + if let Some(account) = Account::read(&ctx, account).await? { + if account.owner { + return Err(NekrochanError::OwnerDeletionError); + } + + account.delete(&ctx).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/accounts")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/remove_banners.rs b/src/web/staff/actions/remove_banners.rs new file mode 100755 index 0000000..b4481ef --- /dev/null +++ b/src/web/staff/actions/remove_banners.rs @@ -0,0 +1,50 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct RemoveBannersForm { + board: String, + #[serde(default)] + banners: Vec, +} + +#[post("/staff/actions/remove-banners")] +pub async fn remove_banners( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().board_banners()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let board = form.board; + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let old_banners = board.banners.0.clone(); + let mut new_banners = Vec::new(); + + for (i, banner) in old_banners.into_iter().enumerate() { + if form.banners.contains(&i) { + banner.delete().await; + } else { + new_banners.push(banner); + } + } + + board.update_banners(&ctx, new_banners).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", format!("/staff/banners/{}", board.id).as_str())) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/remove_bans.rs b/src/web/staff/actions/remove_bans.rs new file mode 100755 index 0000000..2025d20 --- /dev/null +++ b/src/web/staff/actions/remove_bans.rs @@ -0,0 +1,37 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Ban, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct RemoveBansForm { + #[serde(default)] + bans: Vec, +} + +#[post("/staff/actions/remove-bans")] +pub async fn remove_bans( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().bans()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + for ban in form.bans.into_iter() { + if let Some(ban) = Ban::read_by_id(&ctx, ban).await? { + ban.delete(&ctx).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/bans")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/remove_boards.rs b/src/web/staff/actions/remove_boards.rs new file mode 100755 index 0000000..c444028 --- /dev/null +++ b/src/web/staff/actions/remove_boards.rs @@ -0,0 +1,37 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct RemoveBoardsForm { + #[serde(default)] + boards: Vec, +} + +#[post("/staff/actions/remove-boards")] +pub async fn remove_boards( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + for board in form.boards.into_iter() { + if let Some(board) = Board::read(&ctx, board).await? { + board.delete(&ctx).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/boards")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/transfer_ownership.rs b/src/web/staff/actions/transfer_ownership.rs new file mode 100755 index 0000000..bbd3103 --- /dev/null +++ b/src/web/staff/actions/transfer_ownership.rs @@ -0,0 +1,39 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Account, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct TransferOwnershipForm { + account: String, +} + +#[post("/staff/actions/transfer-ownership")] +pub async fn transfer_ownership( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let old_owner = account_from_auth(&ctx, &req).await?; + + if !old_owner.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let new_owner = form.account; + let new_owner = Account::read(&ctx, new_owner.clone()) + .await? + .ok_or(NekrochanError::AccountNotFound(new_owner))?; + + old_owner.update_owner(&ctx, false).await?; + new_owner.update_owner(&ctx, true).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/account")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/update_board_config.rs b/src/web/staff/actions/update_board_config.rs new file mode 100755 index 0000000..d4af5fd --- /dev/null +++ b/src/web/staff/actions/update_board_config.rs @@ -0,0 +1,87 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + cfg::BoardCfg, ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct UpdateBoardConfigForm { + board: String, + anon_name: String, + page_size: i64, + page_count: i64, + file_limit: usize, + bump_limit: i32, + reply_limit: i32, + locked: Option, + user_ids: Option, + flags: Option, + thread_captcha: String, + reply_captcha: String, + require_thread_content: Option, + require_thread_file: Option, + require_reply_content: Option, + require_reply_file: Option, +} + +#[post("/staff/actions/update-board-config")] +pub async fn update_board_config( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().board_config()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let board = form.board; + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let anon_name = form.anon_name; + let page_size = form.page_size; + let page_count = form.page_count; + let file_limit = form.file_limit; + let bump_limit = form.bump_limit; + let reply_limit = form.reply_limit; + let locked = form.locked.is_some(); + let user_ids = form.user_ids.is_some(); + let flags = form.flags.is_some(); + let thread_captcha = form.thread_captcha; + let reply_captcha = form.reply_captcha; + let require_thread_content = form.require_thread_content.is_some(); + let require_thread_file = form.require_thread_file.is_some(); + let require_reply_content = form.require_reply_content.is_some(); + let require_reply_file = form.require_reply_file.is_some(); + + let config = BoardCfg { + anon_name, + page_size, + page_count, + file_limit, + bump_limit, + reply_limit, + locked, + user_ids, + flags, + thread_captcha, + reply_captcha, + require_thread_content, + require_thread_file, + require_reply_content, + require_reply_file, + }; + + board.update_config(&ctx, config).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/boards")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/update_boards.rs b/src/web/staff/actions/update_boards.rs new file mode 100755 index 0000000..287e05c --- /dev/null +++ b/src/web/staff/actions/update_boards.rs @@ -0,0 +1,57 @@ +use actix_web::{ + post, + web::{Bytes, Data}, + HttpRequest, HttpResponse, +}; +use serde::Deserialize; +use serde_qs::Config; + +use crate::{ctx::Ctx, db::models::Board, error::NekrochanError, web::tcx::account_from_auth}; + +#[derive(Deserialize)] +pub struct UpdateBoardsForm { + #[serde(default)] + boards: Vec, + name: String, + description: String, +} + +#[post("/staff/actions/update-boards")] +pub async fn update_boards( + ctx: Data, + req: HttpRequest, + bytes: Bytes, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let config = Config::new(10, false); + let form: UpdateBoardsForm = config.deserialize_bytes(&bytes)?; + + let name = form.name.trim().to_owned(); + let description = form.description.trim().to_owned(); + + if name.is_empty() || name.len() > 32 { + return Err(NekrochanError::BoardNameFormatError); + } + + if description.is_empty() || description.len() > 128 { + return Err(NekrochanError::DescriptionFormatError); + } + + for board in form.boards.into_iter() { + if let Some(board) = Board::read(&ctx, board).await? { + board.update_name(&ctx, name.clone()).await?; + board.update_description(&ctx, description.clone()).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/boards")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/update_permissions.rs b/src/web/staff/actions/update_permissions.rs new file mode 100755 index 0000000..28116bb --- /dev/null +++ b/src/web/staff/actions/update_permissions.rs @@ -0,0 +1,103 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use enumflags2::BitFlags; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Account, error::NekrochanError, perms::Permissions, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct UpdatePermissionsForm { + account: String, + edit_posts: Option, + manage_posts: Option, + capcodes: Option, + staff_log: Option, + reports: Option, + bans: Option, + board_banners: Option, + board_config: Option, + bypass_bans: Option, + bypass_board_lock: Option, + bypass_thread_lock: Option, + bypass_captcha: Option, +} + +#[post("/staff/actions/update-permissions")] +pub async fn update_permissions( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let updated_account = form.account; + let updated_account = Account::read(&ctx, updated_account.clone()) + .await? + .ok_or(NekrochanError::AccountNotFound(updated_account))?; + + let mut permissions = BitFlags::empty(); + + if form.edit_posts.is_some() { + permissions |= Permissions::EditPosts + } + + if form.manage_posts.is_some() { + permissions |= Permissions::ManagePosts + } + + if form.capcodes.is_some() { + permissions |= Permissions::Capcodes + } + + if form.staff_log.is_some() { + permissions |= Permissions::StaffLog + } + + if form.reports.is_some() { + permissions |= Permissions::Reports + } + + if form.bans.is_some() { + permissions |= Permissions::Bans + } + + if form.board_banners.is_some() { + permissions |= Permissions::BoardBanners + } + + if form.board_config.is_some() { + permissions |= Permissions::BoardConfig + } + + if form.bypass_bans.is_some() { + permissions |= Permissions::BypassBans + } + + if form.bypass_board_lock.is_some() { + permissions |= Permissions::BypassBoardLock + } + + if form.bypass_thread_lock.is_some() { + permissions |= Permissions::BypassThreadLock + } + + if form.bypass_captcha.is_some() { + permissions |= Permissions::BypassCaptcha + } + + updated_account + .update_permissions(&ctx, permissions.bits()) + .await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/accounts")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/banners.rs b/src/web/staff/banners.rs new file mode 100755 index 0000000..3def9b4 --- /dev/null +++ b/src/web/staff/banners.rs @@ -0,0 +1,46 @@ +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Board, + error::NekrochanError, + web::{ + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Template)] +#[template(path = "staff/banners.html")] +struct BannersTemplate { + tcx: TemplateCtx, + board: Board, +} + +#[get("/staff/banners/{board}")] +pub async fn banners( + ctx: Data, + req: HttpRequest, + board: Path, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().board_banners()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let board = board.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let template = BannersTemplate { tcx, board }; + + template_response(template) +} diff --git a/src/web/staff/bans.rs b/src/web/staff/bans.rs new file mode 100755 index 0000000..38c3f57 --- /dev/null +++ b/src/web/staff/bans.rs @@ -0,0 +1,35 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Ban, + error::NekrochanError, + filters, + web::{ + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Template)] +#[template(path = "staff/bans.html")] +struct BansTemplate { + tcx: TemplateCtx, + bans: Vec, +} + +#[get("/staff/bans")] +pub async fn bans(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().bans()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let bans = Ban::read_all(&ctx).await?; + let template = BansTemplate { tcx, bans }; + + template_response(template) +} diff --git a/src/web/staff/board_config.rs b/src/web/staff/board_config.rs new file mode 100755 index 0000000..5061680 --- /dev/null +++ b/src/web/staff/board_config.rs @@ -0,0 +1,46 @@ +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Board, + error::NekrochanError, + web::{ + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Template)] +#[template(path = "staff/board-config.html")] +struct BannersTemplate { + tcx: TemplateCtx, + board: Board, +} + +#[get("/staff/board-config/{board}")] +pub async fn board_config( + ctx: Data, + req: HttpRequest, + board: Path, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().board_config()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let board = board.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let template = BannersTemplate { tcx, board }; + + template_response(template) +} diff --git a/src/web/staff/boards.rs b/src/web/staff/boards.rs new file mode 100755 index 0000000..9038cdb --- /dev/null +++ b/src/web/staff/boards.rs @@ -0,0 +1,38 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Board, + error::NekrochanError, + filters, + web::{ + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Template)] +#[template(path = "staff/boards.html")] +struct BoardsTemplate { + tcx: TemplateCtx, + boards: Vec, +} + +#[get("/staff/boards")] +pub async fn boards(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() + || account.perms().board_config() + || account.perms().board_banners()) + { + return Err(NekrochanError::InsufficientPermissionError); + } + + let boards = Board::read_all(&ctx).await?; + let template = BoardsTemplate { tcx, boards }; + + template_response(template) +} diff --git a/src/web/staff/mod.rs b/src/web/staff/mod.rs new file mode 100755 index 0000000..d1425f1 --- /dev/null +++ b/src/web/staff/mod.rs @@ -0,0 +1,9 @@ +pub mod account; +pub mod accounts; +pub mod actions; +pub mod banners; +pub mod bans; +pub mod board_config; +pub mod boards; +pub mod permissions; +pub mod reports; diff --git a/src/web/staff/permissions.rs b/src/web/staff/permissions.rs new file mode 100755 index 0000000..8ef13d2 --- /dev/null +++ b/src/web/staff/permissions.rs @@ -0,0 +1,42 @@ +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Account, + error::NekrochanError, + web::{ + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Template)] +#[template(path = "staff/permissions.html")] +struct PermissionsTemplate { + tcx: TemplateCtx, + account: Account, +} + +#[get("/staff/permissions/{account}")] +pub async fn permissions( + ctx: Data, + req: HttpRequest, + path: Path, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let _ = account_from_auth(&ctx, &req).await?; + + let account = path.into_inner(); + let account = Account::read(&ctx, account.clone()) + .await? + .ok_or(NekrochanError::AccountNotFound(account))?; + + let template = PermissionsTemplate { tcx, account }; + + template_response(template) +} diff --git a/src/web/staff/reports.rs b/src/web/staff/reports.rs new file mode 100755 index 0000000..d6df7c7 --- /dev/null +++ b/src/web/staff/reports.rs @@ -0,0 +1,23 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + error::NekrochanError, + web::{tcx::TemplateCtx, template_response}, +}; + +#[allow(dead_code)] +#[derive(Template)] +#[template(path = "staff/reports.html")] +struct ReportsTemplate { + tcx: TemplateCtx, +} + +#[get("/staff/reports")] +async fn reports(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let template = ReportsTemplate { tcx }; + + template_response(template) +} diff --git a/src/web/tcx.rs b/src/web/tcx.rs new file mode 100755 index 0000000..ae9fec8 --- /dev/null +++ b/src/web/tcx.rs @@ -0,0 +1,113 @@ +use actix_web::HttpRequest; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use redis::AsyncCommands; +use std::{ + collections::HashSet, + net::{IpAddr, Ipv4Addr}, +}; + +use crate::{ + auth::Claims, cfg::Cfg, ctx::Ctx, db::models::Account, error::NekrochanError, + perms::PermissionWrapper, +}; + +pub struct TemplateCtx { + pub cfg: Cfg, + pub boards: Vec, + pub logged_in: bool, + pub perms: PermissionWrapper, + pub name: Option, + pub password: String, + pub ip: IpAddr, + pub yous: HashSet, +} + +impl TemplateCtx { + pub async fn new(ctx: &Ctx, req: &HttpRequest) -> Result { + let cfg = ctx.cfg.to_owned(); + let boards = ctx.cache().lrange("board_ids", 0, -1).await?; + + let account = account_from_auth_opt(ctx, req).await?; + let logged_in = account.is_some(); + + let perms = match &account { + Some(account) => account.perms(), + None => PermissionWrapper::new(0, false), + }; + + let name = req.cookie("name").map(|cookie| cookie.value().into()); + let password_cookie = req.cookie("password").map(|cookie| cookie.value().into()); + + let password: String = match password_cookie { + Some(password) => password, + None => thread_rng() + .sample_iter(&Alphanumeric) + .take(8) + .map(char::from) + .collect(), + }; + + let (ip, _) = ip_from_req(req)?; + let yous = ctx.cache().zrange(format!("yous:{ip}"), 0, -1).await?; + + let tcx = Self { + cfg, + boards, + logged_in, + perms, + name, + password, + ip, + yous, + }; + + Ok(tcx) + } +} + +pub async fn account_from_auth(ctx: &Ctx, req: &HttpRequest) -> Result { + let account = account_from_auth_opt(ctx, req) + .await? + .ok_or(NekrochanError::NotLoggedInError)?; + + Ok(account) +} + +pub async fn account_from_auth_opt( + ctx: &Ctx, + req: &HttpRequest, +) -> Result, NekrochanError> { + let account = match req.cookie("auth") { + Some(auth) => { + let claims = Claims::decode(ctx, auth.value())?; + let account = Account::read(ctx, claims.sub) + .await? + .ok_or(NekrochanError::InvalidAuthError)?; + + Some(account) + } + None => None, + }; + + Ok(account) +} + +pub fn ip_from_req(req: &HttpRequest) -> Result<(IpAddr, String), NekrochanError> { + let ip = IpAddr::V4(Ipv4Addr::UNSPECIFIED); + + // let ip = req + // .headers() + // .get("X-Real-IP") + // .ok_or(NekrochanError::HeaderError("X-Real-IP"))? + // .to_str() + // .map_err(|_| NekrochanError::HeaderError("X-Real-IP"))? + // .parse::()?; + + let country = req + .headers() + .get("X-Country-Code") + .map(|hdr| hdr.to_str().unwrap_or("xx").to_ascii_lowercase()) + .unwrap_or_else(|| "xx".into()); + + Ok((ip, country)) +} diff --git a/src/web/thread.rs b/src/web/thread.rs new file mode 100644 index 0000000..5647fe9 --- /dev/null +++ b/src/web/thread.rs @@ -0,0 +1,59 @@ +use actix_web::{ + get, + http::StatusCode, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "thread.html")] +struct ThreadTemplate { + tcx: TemplateCtx, + board: Board, + thread: Post, + replies: Vec, +} + +#[get("/boards/{board}/{thread}")] +pub async fn thread( + ctx: Data, + req: HttpRequest, + path: Path<(String, i32)>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let (board, id) = path.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let thread = Post::read(&ctx, board.id.clone(), id) + .await? + .ok_or(NekrochanError::PostNotFound(board.id.clone(), id))?; + + if thread.thread.is_some() { + return Ok(HttpResponse::build(StatusCode::PERMANENT_REDIRECT) + .append_header(("Location", thread.post_url())) + .finish()); + } + + let replies = thread.read_replies(&ctx).await?; + + let template = ThreadTemplate { + tcx, + board, + thread, + replies, + }; + + template_response(template) +} diff --git a/static/banner.gif b/static/banner.gif new file mode 100755 index 0000000..86d986c Binary files /dev/null and b/static/banner.gif differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100755 index 0000000..6c1d133 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/flags/ad.png b/static/flags/ad.png new file mode 100755 index 0000000..625ca84 Binary files /dev/null and b/static/flags/ad.png differ diff --git a/static/flags/ae.png b/static/flags/ae.png new file mode 100755 index 0000000..ef3a1ec Binary files /dev/null and b/static/flags/ae.png differ diff --git a/static/flags/af.png b/static/flags/af.png new file mode 100755 index 0000000..a4742e2 Binary files /dev/null and b/static/flags/af.png differ diff --git a/static/flags/ag.png b/static/flags/ag.png new file mode 100755 index 0000000..556d550 Binary files /dev/null and b/static/flags/ag.png differ diff --git a/static/flags/ai.png b/static/flags/ai.png new file mode 100755 index 0000000..74ed29d Binary files /dev/null and b/static/flags/ai.png differ diff --git a/static/flags/al.png b/static/flags/al.png new file mode 100755 index 0000000..92354cb Binary files /dev/null and b/static/flags/al.png differ diff --git a/static/flags/am.png b/static/flags/am.png new file mode 100755 index 0000000..344a2a8 Binary files /dev/null and b/static/flags/am.png differ diff --git a/static/flags/an.png b/static/flags/an.png new file mode 100755 index 0000000..633e4b8 Binary files /dev/null and b/static/flags/an.png differ diff --git a/static/flags/ao.png b/static/flags/ao.png new file mode 100755 index 0000000..bcbd1d6 Binary files /dev/null and b/static/flags/ao.png differ diff --git a/static/flags/ar.png b/static/flags/ar.png new file mode 100755 index 0000000..e5ef8f1 Binary files /dev/null and b/static/flags/ar.png differ diff --git a/static/flags/as.png b/static/flags/as.png new file mode 100755 index 0000000..32f30e4 Binary files /dev/null and b/static/flags/as.png differ diff --git a/static/flags/at.png b/static/flags/at.png new file mode 100755 index 0000000..0f15f34 Binary files /dev/null and b/static/flags/at.png differ diff --git a/static/flags/au.png b/static/flags/au.png new file mode 100755 index 0000000..a01389a Binary files /dev/null and b/static/flags/au.png differ diff --git a/static/flags/aw.png b/static/flags/aw.png new file mode 100755 index 0000000..a3579c2 Binary files /dev/null and b/static/flags/aw.png differ diff --git a/static/flags/ax.png b/static/flags/ax.png new file mode 100755 index 0000000..1eea80a Binary files /dev/null and b/static/flags/ax.png differ diff --git a/static/flags/az.png b/static/flags/az.png new file mode 100755 index 0000000..4ee9fe5 Binary files /dev/null and b/static/flags/az.png differ diff --git a/static/flags/ba.png b/static/flags/ba.png new file mode 100755 index 0000000..c774992 Binary files /dev/null and b/static/flags/ba.png differ diff --git a/static/flags/bb.png b/static/flags/bb.png new file mode 100755 index 0000000..0df19c7 Binary files /dev/null and b/static/flags/bb.png differ diff --git a/static/flags/bd.png b/static/flags/bd.png new file mode 100755 index 0000000..076a8bf Binary files /dev/null and b/static/flags/bd.png differ diff --git a/static/flags/be.png b/static/flags/be.png new file mode 100755 index 0000000..d86ebc8 Binary files /dev/null and b/static/flags/be.png differ diff --git a/static/flags/bf.png b/static/flags/bf.png new file mode 100755 index 0000000..ab5ce8f Binary files /dev/null and b/static/flags/bf.png differ diff --git a/static/flags/bg.png b/static/flags/bg.png new file mode 100755 index 0000000..0469f06 Binary files /dev/null and b/static/flags/bg.png differ diff --git a/static/flags/bh.png b/static/flags/bh.png new file mode 100755 index 0000000..ea8ce68 Binary files /dev/null and b/static/flags/bh.png differ diff --git a/static/flags/bi.png b/static/flags/bi.png new file mode 100755 index 0000000..5cc2e30 Binary files /dev/null and b/static/flags/bi.png differ diff --git a/static/flags/bj.png b/static/flags/bj.png new file mode 100755 index 0000000..1cc8b45 Binary files /dev/null and b/static/flags/bj.png differ diff --git a/static/flags/bm.png b/static/flags/bm.png new file mode 100755 index 0000000..c0c7aea Binary files /dev/null and b/static/flags/bm.png differ diff --git a/static/flags/bn.png b/static/flags/bn.png new file mode 100755 index 0000000..8fb0984 Binary files /dev/null and b/static/flags/bn.png differ diff --git a/static/flags/bo.png b/static/flags/bo.png new file mode 100755 index 0000000..ce7ba52 Binary files /dev/null and b/static/flags/bo.png differ diff --git a/static/flags/br.png b/static/flags/br.png new file mode 100755 index 0000000..9b1a553 Binary files /dev/null and b/static/flags/br.png differ diff --git a/static/flags/bs.png b/static/flags/bs.png new file mode 100755 index 0000000..639fa6c Binary files /dev/null and b/static/flags/bs.png differ diff --git a/static/flags/bt.png b/static/flags/bt.png new file mode 100755 index 0000000..1d512df Binary files /dev/null and b/static/flags/bt.png differ diff --git a/static/flags/bv.png b/static/flags/bv.png new file mode 100755 index 0000000..160b6b5 Binary files /dev/null and b/static/flags/bv.png differ diff --git a/static/flags/bw.png b/static/flags/bw.png new file mode 100755 index 0000000..fcb1039 Binary files /dev/null and b/static/flags/bw.png differ diff --git a/static/flags/by.png b/static/flags/by.png new file mode 100755 index 0000000..504774e Binary files /dev/null and b/static/flags/by.png differ diff --git a/static/flags/bz.png b/static/flags/bz.png new file mode 100755 index 0000000..be63ee1 Binary files /dev/null and b/static/flags/bz.png differ diff --git a/static/flags/ca.png b/static/flags/ca.png new file mode 100755 index 0000000..1f20419 Binary files /dev/null and b/static/flags/ca.png differ diff --git a/static/flags/cc.png b/static/flags/cc.png new file mode 100755 index 0000000..aed3d3b Binary files /dev/null and b/static/flags/cc.png differ diff --git a/static/flags/cd.png b/static/flags/cd.png new file mode 100755 index 0000000..5e48942 Binary files /dev/null and b/static/flags/cd.png differ diff --git a/static/flags/cf.png b/static/flags/cf.png new file mode 100755 index 0000000..da687bd Binary files /dev/null and b/static/flags/cf.png differ diff --git a/static/flags/cg.png b/static/flags/cg.png new file mode 100755 index 0000000..a859792 Binary files /dev/null and b/static/flags/cg.png differ diff --git a/static/flags/ch.png b/static/flags/ch.png new file mode 100755 index 0000000..242ec01 Binary files /dev/null and b/static/flags/ch.png differ diff --git a/static/flags/ci.png b/static/flags/ci.png new file mode 100755 index 0000000..3f2c62e Binary files /dev/null and b/static/flags/ci.png differ diff --git a/static/flags/ck.png b/static/flags/ck.png new file mode 100755 index 0000000..746d3d6 Binary files /dev/null and b/static/flags/ck.png differ diff --git a/static/flags/cl.png b/static/flags/cl.png new file mode 100755 index 0000000..29c6d61 Binary files /dev/null and b/static/flags/cl.png differ diff --git a/static/flags/cm.png b/static/flags/cm.png new file mode 100755 index 0000000..f65c5bd Binary files /dev/null and b/static/flags/cm.png differ diff --git a/static/flags/cn.png b/static/flags/cn.png new file mode 100755 index 0000000..8914414 Binary files /dev/null and b/static/flags/cn.png differ diff --git a/static/flags/co.png b/static/flags/co.png new file mode 100755 index 0000000..a118ff4 Binary files /dev/null and b/static/flags/co.png differ diff --git a/static/flags/cr.png b/static/flags/cr.png new file mode 100755 index 0000000..c7a3731 Binary files /dev/null and b/static/flags/cr.png differ diff --git a/static/flags/cs.png b/static/flags/cs.png new file mode 100755 index 0000000..8254790 Binary files /dev/null and b/static/flags/cs.png differ diff --git a/static/flags/cu.png b/static/flags/cu.png new file mode 100755 index 0000000..083f1d6 Binary files /dev/null and b/static/flags/cu.png differ diff --git a/static/flags/cv.png b/static/flags/cv.png new file mode 100755 index 0000000..a63f7ea Binary files /dev/null and b/static/flags/cv.png differ diff --git a/static/flags/cx.png b/static/flags/cx.png new file mode 100755 index 0000000..48e31ad Binary files /dev/null and b/static/flags/cx.png differ diff --git a/static/flags/cy.png b/static/flags/cy.png new file mode 100755 index 0000000..5b1ad6c Binary files /dev/null and b/static/flags/cy.png differ diff --git a/static/flags/cz.png b/static/flags/cz.png new file mode 100755 index 0000000..c8403dd Binary files /dev/null and b/static/flags/cz.png differ diff --git a/static/flags/de.png b/static/flags/de.png new file mode 100755 index 0000000..ac4a977 Binary files /dev/null and b/static/flags/de.png differ diff --git a/static/flags/dj.png b/static/flags/dj.png new file mode 100755 index 0000000..582af36 Binary files /dev/null and b/static/flags/dj.png differ diff --git a/static/flags/dk.png b/static/flags/dk.png new file mode 100755 index 0000000..e2993d3 Binary files /dev/null and b/static/flags/dk.png differ diff --git a/static/flags/dm.png b/static/flags/dm.png new file mode 100755 index 0000000..5fbffcb Binary files /dev/null and b/static/flags/dm.png differ diff --git a/static/flags/do.png b/static/flags/do.png new file mode 100755 index 0000000..5a04932 Binary files /dev/null and b/static/flags/do.png differ diff --git a/static/flags/dz.png b/static/flags/dz.png new file mode 100755 index 0000000..335c239 Binary files /dev/null and b/static/flags/dz.png differ diff --git a/static/flags/ec.png b/static/flags/ec.png new file mode 100755 index 0000000..0caa0b1 Binary files /dev/null and b/static/flags/ec.png differ diff --git a/static/flags/ee.png b/static/flags/ee.png new file mode 100755 index 0000000..0c82efb Binary files /dev/null and b/static/flags/ee.png differ diff --git a/static/flags/eg.png b/static/flags/eg.png new file mode 100755 index 0000000..8a3f7a1 Binary files /dev/null and b/static/flags/eg.png differ diff --git a/static/flags/eh.png b/static/flags/eh.png new file mode 100755 index 0000000..90a1195 Binary files /dev/null and b/static/flags/eh.png differ diff --git a/static/flags/er.png b/static/flags/er.png new file mode 100755 index 0000000..13065ae Binary files /dev/null and b/static/flags/er.png differ diff --git a/static/flags/es.png b/static/flags/es.png new file mode 100755 index 0000000..c2de2d7 Binary files /dev/null and b/static/flags/es.png differ diff --git a/static/flags/et.png b/static/flags/et.png new file mode 100755 index 0000000..2e893fa Binary files /dev/null and b/static/flags/et.png differ diff --git a/static/flags/fam.png b/static/flags/fam.png new file mode 100755 index 0000000..cf50c75 Binary files /dev/null and b/static/flags/fam.png differ diff --git a/static/flags/fi.png b/static/flags/fi.png new file mode 100755 index 0000000..14ec091 Binary files /dev/null and b/static/flags/fi.png differ diff --git a/static/flags/fj.png b/static/flags/fj.png new file mode 100755 index 0000000..cee9988 Binary files /dev/null and b/static/flags/fj.png differ diff --git a/static/flags/fk.png b/static/flags/fk.png new file mode 100755 index 0000000..ceaeb27 Binary files /dev/null and b/static/flags/fk.png differ diff --git a/static/flags/fm.png b/static/flags/fm.png new file mode 100755 index 0000000..066bb24 Binary files /dev/null and b/static/flags/fm.png differ diff --git a/static/flags/fo.png b/static/flags/fo.png new file mode 100755 index 0000000..cbceb80 Binary files /dev/null and b/static/flags/fo.png differ diff --git a/static/flags/fr.png b/static/flags/fr.png new file mode 100755 index 0000000..8332c4e Binary files /dev/null and b/static/flags/fr.png differ diff --git a/static/flags/ga.png b/static/flags/ga.png new file mode 100755 index 0000000..0e0d434 Binary files /dev/null and b/static/flags/ga.png differ diff --git a/static/flags/gb.png b/static/flags/gb.png new file mode 100755 index 0000000..ff701e1 Binary files /dev/null and b/static/flags/gb.png differ diff --git a/static/flags/gd.png b/static/flags/gd.png new file mode 100755 index 0000000..9ab57f5 Binary files /dev/null and b/static/flags/gd.png differ diff --git a/static/flags/ge.png b/static/flags/ge.png new file mode 100755 index 0000000..728d970 Binary files /dev/null and b/static/flags/ge.png differ diff --git a/static/flags/gf.png b/static/flags/gf.png new file mode 100755 index 0000000..8332c4e Binary files /dev/null and b/static/flags/gf.png differ diff --git a/static/flags/gh.png b/static/flags/gh.png new file mode 100755 index 0000000..4e2f896 Binary files /dev/null and b/static/flags/gh.png differ diff --git a/static/flags/gi.png b/static/flags/gi.png new file mode 100755 index 0000000..e76797f Binary files /dev/null and b/static/flags/gi.png differ diff --git a/static/flags/gl.png b/static/flags/gl.png new file mode 100755 index 0000000..ef12a73 Binary files /dev/null and b/static/flags/gl.png differ diff --git a/static/flags/gm.png b/static/flags/gm.png new file mode 100755 index 0000000..0720b66 Binary files /dev/null and b/static/flags/gm.png differ diff --git a/static/flags/gn.png b/static/flags/gn.png new file mode 100755 index 0000000..ea660b0 Binary files /dev/null and b/static/flags/gn.png differ diff --git a/static/flags/gp.png b/static/flags/gp.png new file mode 100755 index 0000000..dbb086d Binary files /dev/null and b/static/flags/gp.png differ diff --git a/static/flags/gq.png b/static/flags/gq.png new file mode 100755 index 0000000..ebe20a2 Binary files /dev/null and b/static/flags/gq.png differ diff --git a/static/flags/gr.png b/static/flags/gr.png new file mode 100755 index 0000000..8651ade Binary files /dev/null and b/static/flags/gr.png differ diff --git a/static/flags/gs.png b/static/flags/gs.png new file mode 100755 index 0000000..7ef0bf5 Binary files /dev/null and b/static/flags/gs.png differ diff --git a/static/flags/gt.png b/static/flags/gt.png new file mode 100755 index 0000000..c43a70d Binary files /dev/null and b/static/flags/gt.png differ diff --git a/static/flags/gu.png b/static/flags/gu.png new file mode 100755 index 0000000..92f37c0 Binary files /dev/null and b/static/flags/gu.png differ diff --git a/static/flags/gw.png b/static/flags/gw.png new file mode 100755 index 0000000..b37bcf0 Binary files /dev/null and b/static/flags/gw.png differ diff --git a/static/flags/gy.png b/static/flags/gy.png new file mode 100755 index 0000000..22cbe2f Binary files /dev/null and b/static/flags/gy.png differ diff --git a/static/flags/hk.png b/static/flags/hk.png new file mode 100755 index 0000000..d5c380c Binary files /dev/null and b/static/flags/hk.png differ diff --git a/static/flags/hm.png b/static/flags/hm.png new file mode 100755 index 0000000..a01389a Binary files /dev/null and b/static/flags/hm.png differ diff --git a/static/flags/hn.png b/static/flags/hn.png new file mode 100755 index 0000000..96f8388 Binary files /dev/null and b/static/flags/hn.png differ diff --git a/static/flags/hr.png b/static/flags/hr.png new file mode 100755 index 0000000..696b515 Binary files /dev/null and b/static/flags/hr.png differ diff --git a/static/flags/ht.png b/static/flags/ht.png new file mode 100755 index 0000000..416052a Binary files /dev/null and b/static/flags/ht.png differ diff --git a/static/flags/hu.png b/static/flags/hu.png new file mode 100755 index 0000000..7baafe4 Binary files /dev/null and b/static/flags/hu.png differ diff --git a/static/flags/id.png b/static/flags/id.png new file mode 100755 index 0000000..c6bc0fa Binary files /dev/null and b/static/flags/id.png differ diff --git a/static/flags/ie.png b/static/flags/ie.png new file mode 100755 index 0000000..26baa31 Binary files /dev/null and b/static/flags/ie.png differ diff --git a/static/flags/il.png b/static/flags/il.png new file mode 100755 index 0000000..2ca772d Binary files /dev/null and b/static/flags/il.png differ diff --git a/static/flags/in.png b/static/flags/in.png new file mode 100755 index 0000000..e4d7e81 Binary files /dev/null and b/static/flags/in.png differ diff --git a/static/flags/io.png b/static/flags/io.png new file mode 100755 index 0000000..3e74b6a Binary files /dev/null and b/static/flags/io.png differ diff --git a/static/flags/iq.png b/static/flags/iq.png new file mode 100755 index 0000000..878a351 Binary files /dev/null and b/static/flags/iq.png differ diff --git a/static/flags/ir.png b/static/flags/ir.png new file mode 100755 index 0000000..c5fd136 Binary files /dev/null and b/static/flags/ir.png differ diff --git a/static/flags/is.png b/static/flags/is.png new file mode 100755 index 0000000..b8f6d0f Binary files /dev/null and b/static/flags/is.png differ diff --git a/static/flags/it.png b/static/flags/it.png new file mode 100755 index 0000000..89692f7 Binary files /dev/null and b/static/flags/it.png differ diff --git a/static/flags/jm.png b/static/flags/jm.png new file mode 100755 index 0000000..7be119e Binary files /dev/null and b/static/flags/jm.png differ diff --git a/static/flags/jo.png b/static/flags/jo.png new file mode 100755 index 0000000..11bd497 Binary files /dev/null and b/static/flags/jo.png differ diff --git a/static/flags/jp.png b/static/flags/jp.png new file mode 100755 index 0000000..325fbad Binary files /dev/null and b/static/flags/jp.png differ diff --git a/static/flags/ke.png b/static/flags/ke.png new file mode 100755 index 0000000..51879ad Binary files /dev/null and b/static/flags/ke.png differ diff --git a/static/flags/kg.png b/static/flags/kg.png new file mode 100755 index 0000000..0a818f6 Binary files /dev/null and b/static/flags/kg.png differ diff --git a/static/flags/kh.png b/static/flags/kh.png new file mode 100755 index 0000000..30f6bb1 Binary files /dev/null and b/static/flags/kh.png differ diff --git a/static/flags/ki.png b/static/flags/ki.png new file mode 100755 index 0000000..2dcce4b Binary files /dev/null and b/static/flags/ki.png differ diff --git a/static/flags/km.png b/static/flags/km.png new file mode 100755 index 0000000..812b2f5 Binary files /dev/null and b/static/flags/km.png differ diff --git a/static/flags/kn.png b/static/flags/kn.png new file mode 100755 index 0000000..febd5b4 Binary files /dev/null and b/static/flags/kn.png differ diff --git a/static/flags/kp.png b/static/flags/kp.png new file mode 100755 index 0000000..d3d509a Binary files /dev/null and b/static/flags/kp.png differ diff --git a/static/flags/kr.png b/static/flags/kr.png new file mode 100755 index 0000000..9c0a78e Binary files /dev/null and b/static/flags/kr.png differ diff --git a/static/flags/kw.png b/static/flags/kw.png new file mode 100755 index 0000000..96546da Binary files /dev/null and b/static/flags/kw.png differ diff --git a/static/flags/ky.png b/static/flags/ky.png new file mode 100755 index 0000000..15c5f8e Binary files /dev/null and b/static/flags/ky.png differ diff --git a/static/flags/kz.png b/static/flags/kz.png new file mode 100755 index 0000000..45a8c88 Binary files /dev/null and b/static/flags/kz.png differ diff --git a/static/flags/la.png b/static/flags/la.png new file mode 100755 index 0000000..e28acd0 Binary files /dev/null and b/static/flags/la.png differ diff --git a/static/flags/lb.png b/static/flags/lb.png new file mode 100755 index 0000000..d0d452b Binary files /dev/null and b/static/flags/lb.png differ diff --git a/static/flags/lc.png b/static/flags/lc.png new file mode 100755 index 0000000..a47d065 Binary files /dev/null and b/static/flags/lc.png differ diff --git a/static/flags/li.png b/static/flags/li.png new file mode 100755 index 0000000..6469909 Binary files /dev/null and b/static/flags/li.png differ diff --git a/static/flags/lk.png b/static/flags/lk.png new file mode 100755 index 0000000..088aad6 Binary files /dev/null and b/static/flags/lk.png differ diff --git a/static/flags/lr.png b/static/flags/lr.png new file mode 100755 index 0000000..89a5bc7 Binary files /dev/null and b/static/flags/lr.png differ diff --git a/static/flags/ls.png b/static/flags/ls.png new file mode 100755 index 0000000..33fdef1 Binary files /dev/null and b/static/flags/ls.png differ diff --git a/static/flags/lt.png b/static/flags/lt.png new file mode 100755 index 0000000..c8ef0da Binary files /dev/null and b/static/flags/lt.png differ diff --git a/static/flags/lu.png b/static/flags/lu.png new file mode 100755 index 0000000..4cabba9 Binary files /dev/null and b/static/flags/lu.png differ diff --git a/static/flags/lv.png b/static/flags/lv.png new file mode 100755 index 0000000..49b6998 Binary files /dev/null and b/static/flags/lv.png differ diff --git a/static/flags/ly.png b/static/flags/ly.png new file mode 100755 index 0000000..b163a9f Binary files /dev/null and b/static/flags/ly.png differ diff --git a/static/flags/ma.png b/static/flags/ma.png new file mode 100755 index 0000000..f386770 Binary files /dev/null and b/static/flags/ma.png differ diff --git a/static/flags/mc.png b/static/flags/mc.png new file mode 100755 index 0000000..1aa830f Binary files /dev/null and b/static/flags/mc.png differ diff --git a/static/flags/md.png b/static/flags/md.png new file mode 100755 index 0000000..4e92c18 Binary files /dev/null and b/static/flags/md.png differ diff --git a/static/flags/me.png b/static/flags/me.png new file mode 100755 index 0000000..ac72535 Binary files /dev/null and b/static/flags/me.png differ diff --git a/static/flags/mg.png b/static/flags/mg.png new file mode 100755 index 0000000..d2715b3 Binary files /dev/null and b/static/flags/mg.png differ diff --git a/static/flags/mh.png b/static/flags/mh.png new file mode 100755 index 0000000..fb523a8 Binary files /dev/null and b/static/flags/mh.png differ diff --git a/static/flags/mk.png b/static/flags/mk.png new file mode 100755 index 0000000..db173aa Binary files /dev/null and b/static/flags/mk.png differ diff --git a/static/flags/ml.png b/static/flags/ml.png new file mode 100755 index 0000000..2cec8ba Binary files /dev/null and b/static/flags/ml.png differ diff --git a/static/flags/mm.png b/static/flags/mm.png new file mode 100755 index 0000000..f464f67 Binary files /dev/null and b/static/flags/mm.png differ diff --git a/static/flags/mn.png b/static/flags/mn.png new file mode 100755 index 0000000..9396355 Binary files /dev/null and b/static/flags/mn.png differ diff --git a/static/flags/mo.png b/static/flags/mo.png new file mode 100755 index 0000000..deb801d Binary files /dev/null and b/static/flags/mo.png differ diff --git a/static/flags/mp.png b/static/flags/mp.png new file mode 100755 index 0000000..298d588 Binary files /dev/null and b/static/flags/mp.png differ diff --git a/static/flags/mq.png b/static/flags/mq.png new file mode 100755 index 0000000..010143b Binary files /dev/null and b/static/flags/mq.png differ diff --git a/static/flags/mr.png b/static/flags/mr.png new file mode 100755 index 0000000..319546b Binary files /dev/null and b/static/flags/mr.png differ diff --git a/static/flags/ms.png b/static/flags/ms.png new file mode 100755 index 0000000..d4cbb43 Binary files /dev/null and b/static/flags/ms.png differ diff --git a/static/flags/mt.png b/static/flags/mt.png new file mode 100755 index 0000000..00af948 Binary files /dev/null and b/static/flags/mt.png differ diff --git a/static/flags/mu.png b/static/flags/mu.png new file mode 100755 index 0000000..b7fdce1 Binary files /dev/null and b/static/flags/mu.png differ diff --git a/static/flags/mv.png b/static/flags/mv.png new file mode 100755 index 0000000..5073d9e Binary files /dev/null and b/static/flags/mv.png differ diff --git a/static/flags/mw.png b/static/flags/mw.png new file mode 100755 index 0000000..13886e9 Binary files /dev/null and b/static/flags/mw.png differ diff --git a/static/flags/mx.png b/static/flags/mx.png new file mode 100755 index 0000000..5bc58ab Binary files /dev/null and b/static/flags/mx.png differ diff --git a/static/flags/my.png b/static/flags/my.png new file mode 100755 index 0000000..9034cba Binary files /dev/null and b/static/flags/my.png differ diff --git a/static/flags/mz.png b/static/flags/mz.png new file mode 100755 index 0000000..76405e0 Binary files /dev/null and b/static/flags/mz.png differ diff --git a/static/flags/na.png b/static/flags/na.png new file mode 100755 index 0000000..63358c6 Binary files /dev/null and b/static/flags/na.png differ diff --git a/static/flags/nc.png b/static/flags/nc.png new file mode 100755 index 0000000..2cad283 Binary files /dev/null and b/static/flags/nc.png differ diff --git a/static/flags/ne.png b/static/flags/ne.png new file mode 100755 index 0000000..d85f424 Binary files /dev/null and b/static/flags/ne.png differ diff --git a/static/flags/nf.png b/static/flags/nf.png new file mode 100755 index 0000000..f9bcdda Binary files /dev/null and b/static/flags/nf.png differ diff --git a/static/flags/ng.png b/static/flags/ng.png new file mode 100755 index 0000000..3eea2e0 Binary files /dev/null and b/static/flags/ng.png differ diff --git a/static/flags/ni.png b/static/flags/ni.png new file mode 100755 index 0000000..3969aaa Binary files /dev/null and b/static/flags/ni.png differ diff --git a/static/flags/nl.png b/static/flags/nl.png new file mode 100755 index 0000000..fe44791 Binary files /dev/null and b/static/flags/nl.png differ diff --git a/static/flags/no.png b/static/flags/no.png new file mode 100755 index 0000000..160b6b5 Binary files /dev/null and b/static/flags/no.png differ diff --git a/static/flags/np.png b/static/flags/np.png new file mode 100755 index 0000000..aeb058b Binary files /dev/null and b/static/flags/np.png differ diff --git a/static/flags/nr.png b/static/flags/nr.png new file mode 100755 index 0000000..705fc33 Binary files /dev/null and b/static/flags/nr.png differ diff --git a/static/flags/nu.png b/static/flags/nu.png new file mode 100755 index 0000000..c3ce4ae Binary files /dev/null and b/static/flags/nu.png differ diff --git a/static/flags/nz.png b/static/flags/nz.png new file mode 100755 index 0000000..10d6306 Binary files /dev/null and b/static/flags/nz.png differ diff --git a/static/flags/om.png b/static/flags/om.png new file mode 100755 index 0000000..2ffba7e Binary files /dev/null and b/static/flags/om.png differ diff --git a/static/flags/pa.png b/static/flags/pa.png new file mode 100755 index 0000000..9b2ee9a Binary files /dev/null and b/static/flags/pa.png differ diff --git a/static/flags/pe.png b/static/flags/pe.png new file mode 100755 index 0000000..62a0497 Binary files /dev/null and b/static/flags/pe.png differ diff --git a/static/flags/pf.png b/static/flags/pf.png new file mode 100755 index 0000000..771a0f6 Binary files /dev/null and b/static/flags/pf.png differ diff --git a/static/flags/pg.png b/static/flags/pg.png new file mode 100755 index 0000000..10d6233 Binary files /dev/null and b/static/flags/pg.png differ diff --git a/static/flags/ph.png b/static/flags/ph.png new file mode 100755 index 0000000..b89e159 Binary files /dev/null and b/static/flags/ph.png differ diff --git a/static/flags/pk.png b/static/flags/pk.png new file mode 100755 index 0000000..e9df70c Binary files /dev/null and b/static/flags/pk.png differ diff --git a/static/flags/pl.png b/static/flags/pl.png new file mode 100755 index 0000000..d413d01 Binary files /dev/null and b/static/flags/pl.png differ diff --git a/static/flags/pm.png b/static/flags/pm.png new file mode 100755 index 0000000..ba91d2c Binary files /dev/null and b/static/flags/pm.png differ diff --git a/static/flags/pn.png b/static/flags/pn.png new file mode 100755 index 0000000..aa9344f Binary files /dev/null and b/static/flags/pn.png differ diff --git a/static/flags/pr.png b/static/flags/pr.png new file mode 100755 index 0000000..82d9130 Binary files /dev/null and b/static/flags/pr.png differ diff --git a/static/flags/ps.png b/static/flags/ps.png new file mode 100755 index 0000000..f5f5477 Binary files /dev/null and b/static/flags/ps.png differ diff --git a/static/flags/pt.png b/static/flags/pt.png new file mode 100755 index 0000000..ece7980 Binary files /dev/null and b/static/flags/pt.png differ diff --git a/static/flags/pw.png b/static/flags/pw.png new file mode 100755 index 0000000..6178b25 Binary files /dev/null and b/static/flags/pw.png differ diff --git a/static/flags/py.png b/static/flags/py.png new file mode 100755 index 0000000..cb8723c Binary files /dev/null and b/static/flags/py.png differ diff --git a/static/flags/qa.png b/static/flags/qa.png new file mode 100755 index 0000000..ed4c621 Binary files /dev/null and b/static/flags/qa.png differ diff --git a/static/flags/re.png b/static/flags/re.png new file mode 100755 index 0000000..8332c4e Binary files /dev/null and b/static/flags/re.png differ diff --git a/static/flags/ro.png b/static/flags/ro.png new file mode 100755 index 0000000..57e74a6 Binary files /dev/null and b/static/flags/ro.png differ diff --git a/static/flags/rs.png b/static/flags/rs.png new file mode 100755 index 0000000..9439a5b Binary files /dev/null and b/static/flags/rs.png differ diff --git a/static/flags/ru.png b/static/flags/ru.png new file mode 100755 index 0000000..47da421 Binary files /dev/null and b/static/flags/ru.png differ diff --git a/static/flags/rw.png b/static/flags/rw.png new file mode 100755 index 0000000..5356491 Binary files /dev/null and b/static/flags/rw.png differ diff --git a/static/flags/sa.png b/static/flags/sa.png new file mode 100755 index 0000000..b4641c7 Binary files /dev/null and b/static/flags/sa.png differ diff --git a/static/flags/sb.png b/static/flags/sb.png new file mode 100755 index 0000000..a9937cc Binary files /dev/null and b/static/flags/sb.png differ diff --git a/static/flags/sc.png b/static/flags/sc.png new file mode 100755 index 0000000..39ee371 Binary files /dev/null and b/static/flags/sc.png differ diff --git a/static/flags/sd.png b/static/flags/sd.png new file mode 100755 index 0000000..eaab69e Binary files /dev/null and b/static/flags/sd.png differ diff --git a/static/flags/se.png b/static/flags/se.png new file mode 100755 index 0000000..1994653 Binary files /dev/null and b/static/flags/se.png differ diff --git a/static/flags/sg.png b/static/flags/sg.png new file mode 100755 index 0000000..dd34d61 Binary files /dev/null and b/static/flags/sg.png differ diff --git a/static/flags/sh.png b/static/flags/sh.png new file mode 100755 index 0000000..4b1d2a2 Binary files /dev/null and b/static/flags/sh.png differ diff --git a/static/flags/si.png b/static/flags/si.png new file mode 100755 index 0000000..bb1476f Binary files /dev/null and b/static/flags/si.png differ diff --git a/static/flags/sj.png b/static/flags/sj.png new file mode 100755 index 0000000..160b6b5 Binary files /dev/null and b/static/flags/sj.png differ diff --git a/static/flags/sk.png b/static/flags/sk.png new file mode 100755 index 0000000..7ccbc82 Binary files /dev/null and b/static/flags/sk.png differ diff --git a/static/flags/sl.png b/static/flags/sl.png new file mode 100755 index 0000000..12d812d Binary files /dev/null and b/static/flags/sl.png differ diff --git a/static/flags/sm.png b/static/flags/sm.png new file mode 100755 index 0000000..3df2fdc Binary files /dev/null and b/static/flags/sm.png differ diff --git a/static/flags/sn.png b/static/flags/sn.png new file mode 100755 index 0000000..eabb71d Binary files /dev/null and b/static/flags/sn.png differ diff --git a/static/flags/so.png b/static/flags/so.png new file mode 100755 index 0000000..4a1ea4b Binary files /dev/null and b/static/flags/so.png differ diff --git a/static/flags/sr.png b/static/flags/sr.png new file mode 100755 index 0000000..5eff927 Binary files /dev/null and b/static/flags/sr.png differ diff --git a/static/flags/st.png b/static/flags/st.png new file mode 100755 index 0000000..2978557 Binary files /dev/null and b/static/flags/st.png differ diff --git a/static/flags/sv.png b/static/flags/sv.png new file mode 100755 index 0000000..2498799 Binary files /dev/null and b/static/flags/sv.png differ diff --git a/static/flags/sy.png b/static/flags/sy.png new file mode 100755 index 0000000..f5ce30d Binary files /dev/null and b/static/flags/sy.png differ diff --git a/static/flags/sz.png b/static/flags/sz.png new file mode 100755 index 0000000..914ee86 Binary files /dev/null and b/static/flags/sz.png differ diff --git a/static/flags/tc.png b/static/flags/tc.png new file mode 100755 index 0000000..8fc1156 Binary files /dev/null and b/static/flags/tc.png differ diff --git a/static/flags/td.png b/static/flags/td.png new file mode 100755 index 0000000..667f21f Binary files /dev/null and b/static/flags/td.png differ diff --git a/static/flags/tf.png b/static/flags/tf.png new file mode 100755 index 0000000..80529a4 Binary files /dev/null and b/static/flags/tf.png differ diff --git a/static/flags/tg.png b/static/flags/tg.png new file mode 100755 index 0000000..3aa00ad Binary files /dev/null and b/static/flags/tg.png differ diff --git a/static/flags/th.png b/static/flags/th.png new file mode 100755 index 0000000..dd8ba91 Binary files /dev/null and b/static/flags/th.png differ diff --git a/static/flags/tj.png b/static/flags/tj.png new file mode 100755 index 0000000..617bf64 Binary files /dev/null and b/static/flags/tj.png differ diff --git a/static/flags/tk.png b/static/flags/tk.png new file mode 100755 index 0000000..67b8c8c Binary files /dev/null and b/static/flags/tk.png differ diff --git a/static/flags/tl.png b/static/flags/tl.png new file mode 100755 index 0000000..77da181 Binary files /dev/null and b/static/flags/tl.png differ diff --git a/static/flags/tm.png b/static/flags/tm.png new file mode 100755 index 0000000..828020e Binary files /dev/null and b/static/flags/tm.png differ diff --git a/static/flags/tn.png b/static/flags/tn.png new file mode 100755 index 0000000..183cdd3 Binary files /dev/null and b/static/flags/tn.png differ diff --git a/static/flags/to.png b/static/flags/to.png new file mode 100755 index 0000000..f89b8ba Binary files /dev/null and b/static/flags/to.png differ diff --git a/static/flags/tr.png b/static/flags/tr.png new file mode 100755 index 0000000..be32f77 Binary files /dev/null and b/static/flags/tr.png differ diff --git a/static/flags/tt.png b/static/flags/tt.png new file mode 100755 index 0000000..2a11c1e Binary files /dev/null and b/static/flags/tt.png differ diff --git a/static/flags/tv.png b/static/flags/tv.png new file mode 100755 index 0000000..28274c5 Binary files /dev/null and b/static/flags/tv.png differ diff --git a/static/flags/tw.png b/static/flags/tw.png new file mode 100755 index 0000000..f31c654 Binary files /dev/null and b/static/flags/tw.png differ diff --git a/static/flags/tz.png b/static/flags/tz.png new file mode 100755 index 0000000..c00ff79 Binary files /dev/null and b/static/flags/tz.png differ diff --git a/static/flags/ua.png b/static/flags/ua.png new file mode 100755 index 0000000..09563a2 Binary files /dev/null and b/static/flags/ua.png differ diff --git a/static/flags/ug.png b/static/flags/ug.png new file mode 100755 index 0000000..33f4aff Binary files /dev/null and b/static/flags/ug.png differ diff --git a/static/flags/um.png b/static/flags/um.png new file mode 100755 index 0000000..c1dd965 Binary files /dev/null and b/static/flags/um.png differ diff --git a/static/flags/us.png b/static/flags/us.png new file mode 100755 index 0000000..10f451f Binary files /dev/null and b/static/flags/us.png differ diff --git a/static/flags/uy.png b/static/flags/uy.png new file mode 100755 index 0000000..31d948a Binary files /dev/null and b/static/flags/uy.png differ diff --git a/static/flags/uz.png b/static/flags/uz.png new file mode 100755 index 0000000..fef5dc1 Binary files /dev/null and b/static/flags/uz.png differ diff --git a/static/flags/va.png b/static/flags/va.png new file mode 100755 index 0000000..b31eaf2 Binary files /dev/null and b/static/flags/va.png differ diff --git a/static/flags/vc.png b/static/flags/vc.png new file mode 100755 index 0000000..8fa17b0 Binary files /dev/null and b/static/flags/vc.png differ diff --git a/static/flags/ve.png b/static/flags/ve.png new file mode 100755 index 0000000..00c90f9 Binary files /dev/null and b/static/flags/ve.png differ diff --git a/static/flags/vg.png b/static/flags/vg.png new file mode 100755 index 0000000..4156907 Binary files /dev/null and b/static/flags/vg.png differ diff --git a/static/flags/vi.png b/static/flags/vi.png new file mode 100755 index 0000000..ed26915 Binary files /dev/null and b/static/flags/vi.png differ diff --git a/static/flags/vn.png b/static/flags/vn.png new file mode 100755 index 0000000..ec7cd48 Binary files /dev/null and b/static/flags/vn.png differ diff --git a/static/flags/vu.png b/static/flags/vu.png new file mode 100755 index 0000000..b3397bc Binary files /dev/null and b/static/flags/vu.png differ diff --git a/static/flags/wf.png b/static/flags/wf.png new file mode 100755 index 0000000..9f95587 Binary files /dev/null and b/static/flags/wf.png differ diff --git a/static/flags/ws.png b/static/flags/ws.png new file mode 100755 index 0000000..c169508 Binary files /dev/null and b/static/flags/ws.png differ diff --git a/static/flags/xx.png b/static/flags/xx.png new file mode 100755 index 0000000..ba351ca Binary files /dev/null and b/static/flags/xx.png differ diff --git a/static/flags/ye.png b/static/flags/ye.png new file mode 100755 index 0000000..468dfad Binary files /dev/null and b/static/flags/ye.png differ diff --git a/static/flags/yt.png b/static/flags/yt.png new file mode 100755 index 0000000..c298f37 Binary files /dev/null and b/static/flags/yt.png differ diff --git a/static/flags/za.png b/static/flags/za.png new file mode 100755 index 0000000..57c58e2 Binary files /dev/null and b/static/flags/za.png differ diff --git a/static/flags/zm.png b/static/flags/zm.png new file mode 100755 index 0000000..c25b07b Binary files /dev/null and b/static/flags/zm.png differ diff --git a/static/flags/zw.png b/static/flags/zw.png new file mode 100755 index 0000000..53c9725 Binary files /dev/null and b/static/flags/zw.png differ diff --git a/static/icons/download.png b/static/icons/download.png new file mode 100755 index 0000000..ee2c90f Binary files /dev/null and b/static/icons/download.png differ diff --git a/static/icons/locked.png b/static/icons/locked.png new file mode 100755 index 0000000..af561c1 Binary files /dev/null and b/static/icons/locked.png differ diff --git a/static/icons/sticky.png b/static/icons/sticky.png new file mode 100755 index 0000000..21bfc96 Binary files /dev/null and b/static/icons/sticky.png differ diff --git a/static/spoiler.png b/static/spoiler.png new file mode 100755 index 0000000..013467b Binary files /dev/null and b/static/spoiler.png differ diff --git a/static/style.css b/static/style.css new file mode 100755 index 0000000..12c0f87 --- /dev/null +++ b/static/style.css @@ -0,0 +1,442 @@ +body { + min-height: 100vh; + font-family: var(--font); + font-size: 10pt; + background: var(--bg); + color: var(--text); + margin: 0; +} + +a { + color: var(--link-color); +} + +a:hover { + color: var(--link-hover); +} + +details { + display: inline-block; + margin-bottom: 8px; +} + +details:last-child { + margin-bottom: 0; +} + +img, +video { + max-height: 90vh; +} + +hr { + border-top: 1px solid var(--hr-color); + border-left: none; + border-right: none; + border-bottom: none; +} + +summary { + cursor: pointer; +} + +.form-table .label { + font-weight: bold; + background-color: var(--table-head); + border: 1px solid var(--table-border); + padding: 4px; +} + +.form-table td { + padding: 0; +} + +.form-table input[type="text"], +.form-table input[type="password"], +.form-table input[type="number"], +.form-table textarea, +.form-table select, +.input-wrapper { + /* FUCK IE7 */ + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + display: block; + width: 100%; + color: var(--text); + background-color: var(--input-color); + border-radius: 0; + border: 1px solid var(--input-border); + padding: 4px; +} + +.form-table input[type="checkbox"] { + display: block; + margin: 0 auto; +} + +.form-table textarea { + height: 8rem; + resize: none; +} + +.form-table .submit .button { + display: block; + width: 100%; +} + +.reply-mode { + font-weight: bold; + font-size: 18px; + text-align: center; + background-color: var(--table-head); + border: 1px solid var(--table-border); + padding: 8px; +} + +.container > form > .form-table { + margin: 8px auto; +} + +.table-wrap { + overflow-x: auto; + margin: 8px 0; +} + +.data-table { + width: 100%; + background-color: var(--table-primary); + border-spacing: 0; + border: 1px solid var(--table-border); +} + +.data-table tr:nth-child(2n + 1) { + background-color: var(--table-secondary); +} + +.data-table th { + background-color: var(--table-head); +} + +.data-table td, +.data-table th { + padding: 4px; + text-align: center; +} + +.data-table .banner { + margin-top: 0; + margin-bottom: 0; +} + +.box { + background-color: var(--box-color); + border-right: 1px solid var(--box-border); + border-bottom: 1px solid var(--box-border); + padding: 8px; +} + +.box:target { + background-color: var(--hl-box-color); + border-right: 1px solid var(--hl-box-border); + border-bottom: 1px solid var(--hl-box-border); +} + +.infobox { + margin: 8px 0; + border: 1px solid var(--table-border); +} + +.infobox-head { + background-color: var(--table-head); + font-weight: bold; + padding: 4px; +} + +.infobox-content { + background-color: var(--table-primary); + padding: 4px; +} + +.infobox-content .post-files { + margin: 0; +} + +.button { + cursor: pointer; + border-radius: 0; + color: var(--text); + background-color: var(--input-color); + border: 1px solid var(--input-border); + padding: 8px; +} + +.captcha { + width: 100%; + image-rendering: pixelated; + border: 1px solid var(--input-border); +} + +.main { + margin: 8px; +} + +.container { + margin: 0 auto; + max-width: 720px; +} + +.title { + color: var(--title-color); + font-family: var(--title-font); + text-align: center; + letter-spacing: -2px; + margin: 0; +} + +.description { + font-weight: bold; + margin: 0; +} + +.small { + font-size: 8pt; + font-weight: normal; +} + +.center { + text-align: center; +} + +.inline-block { + display: inline-block; +} + +.banner { + display: block; + max-width: 300px; + margin: 8px auto; + border: 1px solid var(--box-border); +} + +.board-links { + padding: 2px; + border-bottom: 1px solid var(--box-border); +} + +.link-separator::after { + content: " / "; +} + +.link-group::before { + content: " [ "; +} + +.link-group::after { + content: " ] "; +} + +.board-links::after { + content: ""; + display: block; + clear: both; +} + +.post { + margin-bottom: 8px; + padding: 8px; +} + +.post.box { + display: inline-block; + min-width: 400px; +} + +.post:last-of-type { + margin-bottom: 0; +} + +.post::after { + display: block; + content: ""; + clear: both; +} + +.board-links a, +.pagination a, +.post-number { + text-decoration: none; +} + +.post-header input[type="checkbox"] { + margin: 0; +} + +.catalog { + text-align: center; +} + +.catalog-entry { + display: inline-block; + width: 200px; + height: 250px; + overflow: scroll; + margin: 4px; + padding: 8px; +} + +.catalog-entry .thumb { + display: block; + max-width: 100%; + max-height: 50%; + box-shadow: 0 0 3px #000; + margin: 4px auto; + padding: 2px; +} + +.catalog-entry .post-content { + margin: 8px; +} + +.name { + font-weight: bold; + color: var(--name-color); +} + +.tripcode { + color: var(--trip-color); +} + +.capcode { + color: var(--capcode-color); + font-weight: bold; +} + +.user-id { + text-shadow: #000 0 0 1px, #000 0 0 1px, #000 0 0 1px, #000 0 0 1px, + #000 0 0 1px, #000 0 0 1px; + color: #ffffff; + border: 1px solid var(--box-border); + padding: 0 2px; +} + +.post-files { + float: left; + margin: 0 8px 8px 8px; +} + +.multi-files { + float: none; +} + +.post-file { + display: inline-block; + vertical-align: top; + text-align: center; + font-size: 8pt; + padding: 4px; +} + +.thumb { + max-width: 150px; + max-height: 150px; +} + +.post-content { + font-family: inherit; + white-space: pre-wrap; + word-break: break-word; + margin: 1rem 2rem; +} + +.post-content a { + color: var(--post-link-color); +} + +.post-content a:hover { + color: var(--post-link-hover); +} + +.quote { + text-decoration: underline; +} + +.dead-quote { + color: var(--dead-quote-color); + text-decoration: line-through; +} + +.greentext { + color: var(--greentext-color); +} + +.orangetext { + color: var(--orangetext-color); +} + +.redtext { + color: var(--redtext-color); + font-weight: bold; +} + +.bluetext { + color: var(--bluetext-color); + font-weight: bold; +} + +.glowtext { + text-shadow: 0 0 40px #00fe20, 0 0 2px #00fe20; +} + +.uh-oh-text { + color: var(--uh-oh-text); + background-color: var(--uh-oh-color); +} + +.spoiler { + color: var(--text); + background-color: var(--text); +} + +.spoiler:hover { + background-color: transparent; +} + +.icon { + height: 1em; + vertical-align: middle; + image-rendering: pixelated; +} + +.posts-omitted { + margin-top: 0; + margin-bottom: 8px; +} + +.recent-posts { + list-style-type: none; + margin: 0; + padding: 0; +} + +@media only screen and (max-width: 600px) { + .thumb { + max-width: 100px; + max-height: 100px; + } + + .post.box { + display: block; + min-width: unset; + } + + .thread > br { + display: none; + } + + .catalog-entry { + width: 140px; + height: 220px; + } +} diff --git a/static/themes/yotsuba-b.css b/static/themes/yotsuba-b.css new file mode 100644 index 0000000..976f686 --- /dev/null +++ b/static/themes/yotsuba-b.css @@ -0,0 +1,38 @@ +:root { + /* General */ + --bg: linear-gradient(#d6daf0 3rem, #eef2ff 230px); + --text: #000000; + --font: Arial, Helvetica, sans-serif; + /* Text */ + --link-color: #34345c; + --link-hover: #dd0000; + --post-link-color: #34345c; + --post-link-hover: #dd0000; + --title-color: #af0a0f; + --title-font: tahoma; + --hr-color: #d3d3d3; + --name-color: #117743; + --trip-color: #117743; + --capcode-color: #cc1105; + /* Tables */ + --table-head: #9988ee; + --table-border: #000000; + --table-primary: #ffffff; + --table-secondary: #d6daf0; + /* Forms */ + --input-color: #ffffff; + --input-border: #808080; + /* Box™ */ + --box-color: #d6daf0; + --box-border: #b7c5d9; + --hl-box-color: #d6bad0; + --hl-box-border: #ba9dbf; + /* Formatting */ + --dead-quote-color: #2e2e2e; + --greentext-color: #008000; + --orangetext-color: #ff5100; + --redtext-color: #af0a0f; + --bluetext-color: #0000ff; + --uh-oh-color: #ffffff; + --uh-oh-text: #0038b8; +} diff --git a/static/themes/yotsuba.css b/static/themes/yotsuba.css new file mode 100644 index 0000000..b49dfea --- /dev/null +++ b/static/themes/yotsuba.css @@ -0,0 +1,38 @@ +:root { + /* General */ + --bg: linear-gradient(#fed6af90 3rem, #ffe 230px); + --text: #800000; + --font: Arial, Helvetica, sans-serif; + /* Text */ + --link-color: #800000; + --link-hover: #dd0000; + --post-link-color: #000080; + --post-link-hover: #dd0000; + --title-color: #af0a0f; + --title-font: tahoma; + --hr-color: #d9bfb7; + --name-color: #117743; + --trip-color: #117743; + --capcode-color: #cc1105; + /* Tables */ + --table-head: #ffccaa; + --table-border: #800000; + --table-primary: #ffffff; + --table-secondary: #ffffff; + /* Forms */ + --input-color: #ffffff; + --input-border: #808080; + /* Box™ */ + --box-color: #f0e0d6; + --box-border: #d9bfb7; + --hl-box-color: #f0c0b0; + --hl-box-border: #d99f91; + /* Formatting */ + --dead-quote-color: #2e2e2e; + --greentext-color: #008000; + --orangetext-color: #ff5100; + --redtext-color: #af0a0f; + --bluetext-color: #0000ff; + --uh-oh-color: #ffffff; + --uh-oh-text: #0038b8; +} diff --git a/templates/action.html b/templates/action.html new file mode 100644 index 0000000..156fc43 --- /dev/null +++ b/templates/action.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block title %}Výsledek{% endblock %} + +{% block content %} +
+
+
Výsledek
+
+ {% if !response.is_empty() %} + {{ response|linebreaksbr|safe }} + {% else %} + Nic se nezměnilo. + {% endif %} +
+
+
+{% endblock %} diff --git a/templates/banned.html b/templates/banned.html new file mode 100644 index 0000000..bc7aa15 --- /dev/null +++ b/templates/banned.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} + +{% block title %}Máš ban!{% endblock %} + +{% block content %} +
+

Jsi trans, btw.

+
+
Máš ban!
+
+ + Byl jsi zabanován + + {% if let Some(board) = ban.board %} + z /{{ board }}/ + {% else %} + ze všech nástěněk + {% endif %} + + z následujícího důvodu: + +
+ {{ ban.reason }} +
+ Udělil {{ ban.issued_by }} +
+
+ + Tvůj ban platí pro IP adresu/rozsah {{ ban.ip_range }} a + {% if let Some(expires) = ban.expires %} + vyprší {{ expires|czech_datetime }} ({{ expires|czech_humantime }}). + {% else %} + je trvalý. + {% endif %} + + {% if ban.appealable %} +
+
+ {% if let Some(appeal) = ban.appeal %} + Tvé odvolání: +
+ {{ appeal }} +
+ {% else %} + Můžeš se pokusit svůj ban odvolat: +
+ + + + + + + + +
Odvolání
+ +
+
+ {% endif %} + {% endif %} +
+
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100755 index 0000000..ed59932 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,34 @@ + + + + + + {% block title %}{% endblock %} + + + + + + + + +
{% block content %}{% endblock %}
+ + diff --git a/templates/board.html b/templates/board.html new file mode 100755 index 0000000..f2239dc --- /dev/null +++ b/templates/board.html @@ -0,0 +1,50 @@ +{% import "./macros/pagination.html" as pagination %} +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post-form.html" as post_form %} +{% import "./macros/post.html" as post %} + +{% extends "base.html" %} + +{% block title %}/{{ board.id }}/ - {{ board.name }}{% endblock %} + +{% block content %} +
+
+ {% if let Some(banner) = board.random_banner() %} + + {% endif %} +

/{{ board.id }}/ - {{ board.name }}

+

{{ board.description }}

+ Katalog +
+ {% call post_form::post_form(board, tcx.perms, false, 0) %} +
+
+
+ {% for (thread, replies) in threads %} +
+ {% call post::post(board, thread, false) %} + {% let count = replies.len() %} + {% if count > 5 %} +

+ {% let omitted = count - 5 %} + {{ "Vynechán|Vynechány|Vynecháno"|czech_plural(omitted) }} {{ omitted }} {{ "příspěvek|příspěvky|příspěvků"|czech_plural(omitted) }}. Zobrazit celé vlákno. +

+ {% for reply_post in replies.iter().rev().take(5).rev() %} + {% call post::post(board, reply_post, true) %} +
+ {% endfor %} + {% else %} + {% for reply_post in replies %} + {% call post::post(board, reply_post, true) %} +
+ {% endfor %} + {% endif %} +
+
+ {% endfor %} + {% call pagination::pagination("/boards/{}"|format(board.id), pages, page) %} +
+ {% call post_actions::post_actions(tcx.perms) %} +
+{% endblock %} diff --git a/templates/board_catalog.html b/templates/board_catalog.html new file mode 100755 index 0000000..2e2150b --- /dev/null +++ b/templates/board_catalog.html @@ -0,0 +1,29 @@ +{% import "./macros/catalog-entry.html" as catalog_entry %} +{% import "./macros/post-actions.html" as post_actions %} + +{% extends "base.html" %} + +{% block title %}Katalog (/{{ board.id }}/){% endblock %} + +{% block content %} +
+
+ {% if let Some(banner) = board.random_banner() %} + + {% endif %} +

Katalog (/{{ board.id }}/)

+

{{ board.description }}

+ Index +
+
+
+
+
+ {% for thread in threads %} + {% call catalog_entry::catalog_entry(thread, loop.index, board.config.0.page_size.into()) %} + {% endfor %} +
+
+ {% call post_actions::post_actions(tcx.perms) %} +
+{% endblock %} diff --git a/templates/error.html b/templates/error.html new file mode 100755 index 0000000..0d41e8f --- /dev/null +++ b/templates/error.html @@ -0,0 +1,28 @@ + + + + + + Chyba + + + + +
+
+

Je konec...

+ +
+
+ Chyba {{ error_code }} +
+ {% if !error_message.is_empty() %} +
+ {{ error_message }} +
+ {% endif %} +
+
+
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100755 index 0000000..3d13377 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% block title %}{{ tcx.cfg.site.name }}{% endblock %} + +{% block content %} +
+
+ {% if let Some(banner) = tcx.cfg.site.site_banner %} + + {% endif %} +

{{ tcx.cfg.site.name }}

+

{{ tcx.cfg.site.description }}

+
+ {% if !posts.is_empty() %} +
+
+ Nejnovější příspěvky +
+
+ +
+
+ {% endif %} + {% if !files.is_empty() %} +
+
+ Nejnovější soubory +
+
+
+ {% for post in files %} +
+ + + +
+ {% endfor %} +
+
+
+ {% endif %} +
+{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100755 index 0000000..3735568 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block title %}Přihlásit se{% endblock %} + +{% block content %} +
+

Přihlásit se

+
+ + + + + + + + + + + + +
Jméno
Heslo
+
+
+{% endblock %} diff --git a/templates/macros/catalog-entry.html b/templates/macros/catalog-entry.html new file mode 100644 index 0000000..42de933 --- /dev/null +++ b/templates/macros/catalog-entry.html @@ -0,0 +1,23 @@ +{% macro catalog_entry(post, index, page_size) %} +
+ + /{{ post.board }}/{{ post.id }} + {% if let Some(file) = post.files.0.get(0) %} + + + + {% else %} +

[Link]

+ {% endif %} + + R: {{ post.replies }} / P: {{ index|get_page(page_size) }} + {% if post.sticky %} + + {% endif %} + {% if post.locked %} + + {% endif %} + +
{{ post.content|add_yous(post.board, tcx.yous)|safe }}
+
+{% endmacro %} diff --git a/templates/macros/pagination.html b/templates/macros/pagination.html new file mode 100644 index 0000000..444b24b --- /dev/null +++ b/templates/macros/pagination.html @@ -0,0 +1,15 @@ +{% macro pagination(base, pages, current) %} + +{% endmacro %} diff --git a/templates/macros/post-actions.html b/templates/macros/post-actions.html new file mode 100644 index 0000000..d21a8d4 --- /dev/null +++ b/templates/macros/post-actions.html @@ -0,0 +1,188 @@ +{% macro post_actions(perms) %} +
+ Uživatelské akce + + + + + + + + + + + + + + + + + + + + + +
Odstranit příspěvky +
+ +
+
Odstranit soubory +
+ +
+
Přidat/odstranit spoiler +
+ +
+
Heslo + +
+ +
+ + + + + + + + +
Důvod hlášení
+ +
+
+
+{% if perms.owner() || perms.manage_posts() || perms.bans() || perms.edit_posts() %} +
+ Uklízečské akce + + {% if perms.owner() || perms.manage_posts() %} + + + + + + + + + + + + + + + + + + + + + {% endif %} + {% if perms.owner() || perms.bans() %} + + + + + + + + + + + + + + + + + + + + + + + + + {% endif %} + + + +
Odstranit příspěvky +
+ +
+
Odstranit soubory +
+ +
+
Přidat/odstranit spoiler +
+ +
+
Připnout/odepnout +
+ +
+
Uzamknout/odemknout +
+ +
+
Zabanovat uživatele +
+ +
+
Globální ban +
+ +
+
Neodvolatelný ban +
+ +
+
Důvod banu
Délka banu (dny)
Rozsah banu + +
+ +
+ {% if perms.owner() || perms.edit_posts() %} + + + + +
+ +
+ {% endif %} +
+{% endif %} +{% endmacro %} diff --git a/templates/macros/post-form.html b/templates/macros/post-form.html new file mode 100644 index 0000000..6afbf02 --- /dev/null +++ b/templates/macros/post-form.html @@ -0,0 +1,87 @@ +{% macro post_form(board, perms, reply, reply_to) %} +
+ + {% if reply %} + + {% endif %} + + {% if reply %} + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + {% if !(perms.bypass_captcha() || perms.owner()) %} + {% if reply %} + {% if let Some((id, base64)) = board.reply_captcha() %} + + + + + {% endif %} + {% else %} + {% if let Some((id, base64)) = board.thread_captcha() %} + + + + + {% endif %} + {% endif %} + {% endif %} + + {% if reply %} + + {% else %} + + {% endif %} + +
Režim: Odpověď [Zpět]
Jméno + +
Email + +
Obsah + +
Soubory +
+ 1 %} multiple="multiple"{% endif %}{% if (!reply && board.config.0.require_thread_file) || (reply && board.config.0.require_reply_file) %} required="required"{% endif %}> +
+
Spoiler? +
+ +
+
Heslo (na odstranění) + +
CAPTCHA + + + +
CAPTCHA + + + +
+
+{% endmacro %} diff --git a/templates/macros/post.html b/templates/macros/post.html new file mode 100644 index 0000000..d4720a8 --- /dev/null +++ b/templates/macros/post.html @@ -0,0 +1,59 @@ +{% macro post(board, post, boxed) %} +
+
+ + {% if let Some(email) = post.email %} + {{ post.name }} + {% else %} + {{ post.name }} + {% endif %} + {% if let Some(tripcode) = post.tripcode %} + {{ tripcode }} + {% endif %} + {% if let Some(capcode) = post.capcode %} + ## {{ capcode }} + {% endif %} + {% if tcx.ip == post.ip %} + {# Technically not a tripcode or something but same styling #} + (Ty) + {% endif %} + {% if board.config.0.flags %} + + {% endif %} + {{ post.created|czech_humantime }} + {% if board.config.0.user_ids %} + {{ post.user_id }} + {% endif %} + Č.{{ post.id }} + {% if post.sticky %} + + {% endif %} + {% if post.locked %} + + {% endif %} + + {% if !boxed %} + [Odpovědět] + {% endif %} +
+ {% if !post.files.0.is_empty() %} +
+ {% for file in post.files.0 %} +
+ + {% if file.spoiler %}[Spoiler]{% else %}{{ file.original_name }}{% endif %} + + +
+ ({{ file.size|filesizeformat }}, {{ file.width }}x{{ file.height }}) +
+ + + +
+ {% endfor %} +
+ {% endif %} +
{{ post.content|add_yous(post.board, tcx.yous)|safe }}
+
+{% endmacro %} diff --git a/templates/macros/staff-nav.html b/templates/macros/staff-nav.html new file mode 100644 index 0000000..171dc61 --- /dev/null +++ b/templates/macros/staff-nav.html @@ -0,0 +1,18 @@ +{% macro staff_nav(perms) %} + +{% endmacro %} diff --git a/templates/overboard.html b/templates/overboard.html new file mode 100644 index 0000000..d35520d --- /dev/null +++ b/templates/overboard.html @@ -0,0 +1,49 @@ +{% import "./macros/pagination.html" as pagination %} +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post.html" as post %} + +{% extends "base.html" %} + +{% block title %}Index nadnástěnky{% endblock %} + +{% block content %} +
+
+ {% if let Some(banner) = tcx.cfg.site.site_banner %} + + {% endif %} +

Index nadnástěnky

+

Nově naťuknutá vlákna ze všech nástěnek

+ Katalog +
+
+
+
+ {% for (thread, replies) in threads %} +
+ Vlákno z /{{ thread.board }}/ + {% call post::post(boards[thread.board.as_str()], thread, false) %} + {% let count = replies.len() %} + {% if count > 5 %} +

+ {% let omitted = count - 5 %} + {{ "Vynechán|Vynechány|Vynecháno"|czech_plural(omitted) }} {{ omitted }} {{ "příspěvek|příspěvky|příspěvků"|czech_plural(omitted) }}. Zobrazit celé vlákno. +

+ {% for reply_post in replies.iter().rev().take(5).rev() %} + {% call post::post(boards[reply_post.board.as_str()], reply_post, true) %} +
+ {% endfor %} + {% else %} + {% for reply_post in replies %} + {% call post::post(boards[reply_post.board.as_str()], reply_post, true) %} +
+ {% endfor %} + {% endif %} +
+
+ {% endfor %} + {% call pagination::pagination("/overboard", pages, page) %} +
+ {% call post_actions::post_actions(tcx.perms) %} +
+{% endblock %} diff --git a/templates/overboard_catalog.html b/templates/overboard_catalog.html new file mode 100755 index 0000000..5dd507c --- /dev/null +++ b/templates/overboard_catalog.html @@ -0,0 +1,29 @@ +{% import "./macros/catalog-entry.html" as catalog_entry %} +{% import "./macros/post-actions.html" as post_actions %} + +{% extends "base.html" %} + +{% block title %}Katalog nadnístěnky{% endblock %} + +{% block content %} +
+
+ {% if let Some(banner) = tcx.cfg.site.site_banner %} + + {% endif %} +

Katalog nadnástěnky

+

Nově naťuknutá vlákna ze všech nástěnek

+ Index +
+
+
+
+
+ {% for thread in threads %} + {% call catalog_entry::catalog_entry(thread, loop.index, 15) %} + {% endfor %} +
+
+ {% call post_actions::post_actions(tcx.perms) %} +
+{% endblock %} diff --git a/templates/staff/account.html b/templates/staff/account.html new file mode 100755 index 0000000..a81f9ad --- /dev/null +++ b/templates/staff/account.html @@ -0,0 +1,68 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Účet ({{ account.username }}){% endblock %} + +{% block content %} +

Účet ({{ account.username }})

+{% call staff_nav::staff_nav(tcx.perms) %} +
+

Změnit heslo

+
+ + + + + + + + + + + + +
Staré heslo
Nové heslo
+
+
+{% if tcx.perms.owner() %} +

Předat vlastnictví

+
+ + + + + + + + + + + + +
Účet
Potvrdit +
+ +
+
+
+
+{% endif %} +

Vymazat účet

+
+ + + + + + + + +
Potvrdit +
+ +
+
+
+
+{% endblock %} diff --git a/templates/staff/accounts.html b/templates/staff/accounts.html new file mode 100755 index 0000000..aa178fe --- /dev/null +++ b/templates/staff/accounts.html @@ -0,0 +1,57 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Účty{% endblock %} + +{% block content %} +

Účty

+{% call staff_nav::staff_nav(tcx.perms) %} +
+

Účty

+
+
+ + + + + + + + + {% for account in accounts %} + + + + + + + + {% endfor %} +
JménoVlastníkVytvořenOprávnění
{{ account.username }}{% if account.owner %}Ano{% else %}Ne{% endif %}{{ account.created|czech_datetime }}{{ account.permissions.0 }} [Zobrazit]
+
+ {% if tcx.perms.owner() %} + + {% endif %} +
+
+{% if tcx.perms.owner() %} +

Vytvořit účet

+
+ + + + + + + + + + + + +
Jméno
Heslo
+
+
+{% endif %} +{% endblock %} diff --git a/templates/staff/banners.html b/templates/staff/banners.html new file mode 100755 index 0000000..ce00b12 --- /dev/null +++ b/templates/staff/banners.html @@ -0,0 +1,50 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Bannery (/{{ board.id }}/){% endblock %} + +{% block content %} +

Bannery (/{{ board.id }}/)

+{% call staff_nav::staff_nav(tcx.perms) %} +
+

Bannery

+
+ + +
+ + + + + + {% for banner in board.banners.0 %} + + + + + {% endfor %} +
Banner
+ +
+
+ + +
+
+

Přidat bannery

+
+ + + + + + + + + + +
Bannery
+
+
+{% endblock %} diff --git a/templates/staff/bans.html b/templates/staff/bans.html new file mode 100755 index 0000000..1f45038 --- /dev/null +++ b/templates/staff/bans.html @@ -0,0 +1,47 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Bany{% endblock %} + +{% block content %} +

Bany

+{% call staff_nav::staff_nav(tcx.perms) %} +
+

Bany

+
+
+ + + + + + + + + + + + + {% for ban in bans %} + + + + + + + + + + + + {% endfor %} +
IPNástěnkaDůvodUdělilOdvolatelnýOdvoláníUdělěnVyprší
{{ ban.ip_range.network() }}-{{ ban.ip_range.broadcast() }}{% if let Some(board) = ban.board %}/{{ board }}/{% else %}Všechny{% endif %}
{{ ban.reason }}
{{ ban.issued_by }}{% if ban.appealable %}Ano{% else %}Ne{% endif %}{% if let Some(appeal) = ban.appeal %}
{{ appeal }}
{% else %}-{% endif %}
{{ ban.created|czech_datetime }}{% if let Some(expires) = ban.expires %}{{ expires|czech_humantime }}{% else %}Nikdy{% endif %}
+
+ + {% if tcx.perms.owner() || tcx.perms.bans() %} + + {% endif %} +
+
+{% endblock %} diff --git a/templates/staff/board-config.html b/templates/staff/board-config.html new file mode 100755 index 0000000..39c42e4 --- /dev/null +++ b/templates/staff/board-config.html @@ -0,0 +1,154 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Nastavení (/{{ board.id }}/){% endblock %} + +{% block content %} +

Nastavení (/{{ board.id }}/)

+{% call staff_nav::staff_nav(tcx.perms) %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Uzamknout nástěnku +
+ +
+
Výchozí jméno
Velikost stránky
Počet stránek
Limit souborů
Limit naťuknutí
Limit odpovědí
Identifikátory (anti-samefag) +
+ +
+
Vlajky +
+ +
+
CAPTCHA (vlákno) + +
CAPTCHA (odpověď) + +
Vyžadovat obsah ve vlákně +
+ +
+
Vyžadovat soubor ve vlákně +
+ +
+
Vyžadovat obsah v odpovědi +
+ +
+
Vyžadovat soubor v odpovědi +
+ +
+
+
+
+{% endblock %} diff --git a/templates/staff/boards.html b/templates/staff/boards.html new file mode 100755 index 0000000..26e9b48 --- /dev/null +++ b/templates/staff/boards.html @@ -0,0 +1,82 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Nástěnky{% endblock %} + +{% block content %} +

Nástěnky

+{% call staff_nav::staff_nav(tcx.perms) %} +
+

Nástěnky

+
+
+ + + + + + + + + + + {% for board in boards %} + + + + + + + + + + {% endfor %} +
IDJménoPopisVytvořenaBanneryNastavení
/{{ board.id }}/{{ board.name }}{{ board.description }}{{ board.created|czech_datetime }}{% if tcx.perms.owner() || tcx.perms.board_banners() %}[Zobrazit]{% else %}-{% endif %}{% if tcx.perms.owner() || tcx.perms.board_config() %}[Zobrazit]{% else %}-{% endif %}
+
+ + {% if tcx.perms.owner() %} + + {% endif %} + +
+ + {% if tcx.perms.owner() || tcx.perms.board_config() %} + + + + + + + + + + + + +
Jméno
Popis
+ {% endif %} +
+
+

Vytvořit nástěnku

+
+ + + + + + + + + + + + + + + + +
ID
Jméno
Popis
+
+
+{% endblock %} diff --git a/templates/staff/logs.html b/templates/staff/logs.html new file mode 100755 index 0000000..986a422 --- /dev/null +++ b/templates/staff/logs.html @@ -0,0 +1,30 @@ +{% import "../macros/pagination.html" as pagination %} +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Záznamy{% endblock %} + +{% block content %} +

Záznamy

+{% call staff_nav::staff_nav(tcx.perms) %} +
+

Záznamy

+
+ + + + + + {% for record in records %} + + + + + {% endfor %} +
ZprávaDatum
{{ record.message }}{{ record.created|czech_datetime }}
+
+
+{% call pagination::pagination("/staff/logs", pages, page) %} +
+{% endblock %} diff --git a/templates/staff/permissions.html b/templates/staff/permissions.html new file mode 100755 index 0000000..4cfce88 --- /dev/null +++ b/templates/staff/permissions.html @@ -0,0 +1,135 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Oprávnění ({{ account.username }}){% endblock %} + +{% block content %} +

Oprávnění ({{ account.username }})

+{% call staff_nav::staff_nav(tcx.perms) %} +
+{% if account.perms().owner() %} +

Tento uživatel je vlastník, změny nebudou mít žádný vliv.

+
+{% endif %} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if tcx.perms.owner() %} + + + + {% endif %} +
Upravit příspěvky +
+ +
+
Spravovat příspěvky +
+ +
+
Používat capcode +
+ +
+
Záznamy +
+ +
+
Hlášení +
+ +
+
Bany +
+ +
+
Bannery nástěnek +
+ +
+
Nastavení nástěnek +
+ +
+
Obejít ban +
+ +
+
Obejít uzamčení nástěnky +
+ +
+
Obejít uzamčení vlákna +
+ +
+
Obejít CAPTCHA +
+ +
+
+
+
+{% endblock %} diff --git a/templates/staff/reports.html b/templates/staff/reports.html new file mode 100755 index 0000000..37c63e6 --- /dev/null +++ b/templates/staff/reports.html @@ -0,0 +1,14 @@ +{% import "../macros/pagination.html" as pagination %} +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Hlášení{% endblock %} + +{% block content %} +

Hlášení

+{% call staff_nav::staff_nav(tcx.perms) %} +
+

Dočasně nedostupné

+
+{% endblock %} diff --git a/templates/thread.html b/templates/thread.html new file mode 100644 index 0000000..a40a1f6 --- /dev/null +++ b/templates/thread.html @@ -0,0 +1,33 @@ +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post-form.html" as post_form %} +{% import "./macros/post.html" as post %} + +{% extends "base.html" %} + +{% block title %}/{{ board.id }}/ - {{ thread.content_nomarkup|inline_post }}{% endblock %} + +{% block content %} +
+
+ {% if let Some(banner) = board.random_banner() %} + + {% endif %} +

/{{ board.id }}/ - {{ board.name }}

+

{{ board.description }}

+ Katalog +
+ {% call post_form::post_form(board, tcx.perms, true, thread.id) %} +
+
+
+
+ {% call post::post(board, thread, false) %} + {% for reply_post in replies %} + {% call post::post(board, reply_post, true) %} +
+ {% endfor %} +
+
+ {% call post_actions::post_actions(tcx.perms) %} +
+{% endblock %}