diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b122e04..36bb0d0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,30 @@ { - "image": "mcr.microsoft.com/devcontainers/universal:2", - "features": { - "ghcr.io/devcontainers-extra/features/gleam:1": {}, - "ghcr.io/prulloac/devcontainer-features/bun:1": {}, - "ghcr.io/itsmechlark/features/postgresql:1": {} - } + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers-extra/features/gleam:1": { + "version": "v1.7.0-rc2" + }, + "ghcr.io/devcontainers/features/rust:1": { + "version": "latest" + }, + "ghcr.io/itsmechlark/features/postgresql:1": { + "version": "latest" + }, + "ghcr.io/lumenpink/devcontainer-features/bun:0": {}, + "ghcr.io/devcontainers-extra/features/asdf-package:1": { + "plugin": "erlang", + "version": "latest", + "pluginRepo": "https://github.com/asdf-vm/asdf-erlang.git" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "gleam.gleam", + "ms-ossdata.vscode-postgresql", + "rust-lang.rust-analyzer", + "Yasuo-Higano.gleam-qol" + ] + } + } } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..88ef350 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ + +* text=auto +*.svg linguist-detectable diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9817a23..68581f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,29 +1,30 @@ name: tests on: - push: - branches: - - master - - main - pull_request: + push: + branches: + - master + - main + pull_request: jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 - with: - profile: minimal - toolchain: stable - components: clippy - override: true - - uses: erlef/setup-beam@v1 - with: - otp-version: "26.0.2" - gleam-version: "1.4.1" - rebar3-version: "3" - # elixir-version: "1.15.4" - - uses: oven-sh/setup-bun@v2 - with: - bun-version: "1.1.30" - - run: bash ./build.sh --test + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 + with: + profile: minimal + toolchain: stable + components: clippy + override: true + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.4.1" + rebar3-version: "3" + # elixir-version: "1.15.4" + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.1.30" + # For now at least. + - run: bash ./build.sh --test --backend=gleam diff --git a/.gitignore b/.gitignore index 15373fb..3e510ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,27 @@ *.beam *.ez /target/ + +# Ignore Gleam backend build artifacts backend/build backend/priv/generated + +# Ignore Rust backend build artifacts +backend-rs/target +backend-rs/generated + +# Ignore Gleam frontend build artifacts +frontend/prelude.mjs + erl_crash.dump /test *.log -frontend-ts/node_modules/ -frontend/node_modules/ -backend/node_modules/ +node_modules package-lock.json rsffi/target rsffi/test +node_modules # Ignore Editor files .idea/ .vscode/ -node_modules diff --git a/backend-rs/Cargo.lock b/backend-rs/Cargo.lock new file mode 100644 index 0000000..fe4f212 --- /dev/null +++ b/backend-rs/Cargo.lock @@ -0,0 +1,3666 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64 0.22.1", + "bitflags 2.6.0", + "brotli", + "bytes", + "bytestring", + "derive_more 0.99.18", + "encoding_rs", + "flate2", + "futures-core", + "h2 0.3.26", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-identity" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b8ddc6f6a8b19c4016aaa13519968da9969bc3bc1c1c883cdb0f25dd6c8cf7" +dependencies = [ + "actix-service", + "actix-session", + "actix-utils", + "actix-web", + "derive_more 1.0.0", + "futures-core", + "serde", + "tracing", +] + +[[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.68", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.7", + "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-session" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe6976a74f34f1b6d07a6c05aadc0ed0359304a7781c367fa5b4029418db08f" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "derive_more 1.0.0", + "rand", + "serde", + "serde_json", + "tracing", +] + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more 0.99.18", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.7", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +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 = "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.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.0", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io 2.3.3", + "async-lock 3.4.0", + "blocking", + "futures-lite 2.3.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +dependencies = [ + "async-lock 3.4.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.2", + "rustix 0.38.34", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[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 = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "build_const" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.5", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[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 = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc-any" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73" +dependencies = [ + "debug-helper", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.11.2", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.68", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.68", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", + "unicode-xid", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.1.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[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 = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "handlebars" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd4ccde012831f9a071a637b0d4e31df31c0f6c525784b35ae76a9ac6bc1e315" +dependencies = [ + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body", + "hyper", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lumina-urls" +version = "0.1.0" +dependencies = [ + "regex", + "reqwest", + "scraper", + "serde", + "serde_json", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "magic-crypt" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844b6169eeaae32ae8a61855964331a67f12d2afba9170303fbd3e3c2a861a52" +dependencies = [ + "aes", + "base64 0.22.1", + "cbc", + "crc-any", + "des", + "md-5", + "sha2", + "tiger", +] + +[[package]] +name = "markdown" +version = "1.0.0-alpha.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e61c5c85b392273c4d4ea546e6399ace3e3db172ab01b6de8f3d398d1dbd2ec" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "postgres" +version = "0.19.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c918733159f4d55d2ceb262950f00b0aebd6af4aa97b5a47bb0655120475ed" +dependencies = [ + "bytes", + "fallible-iterator 0.2.0", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" +dependencies = [ + "bytes", + "fallible-iterator 0.2.0", + "postgres-protocol", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +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 = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.6.0", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[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.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.102.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b80b33679ff7a0ea53d37f3b39de77ea0c75b12c5805ac43ec0c33b3051af1b" +dependencies = [ + "ahash", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors", + "tendril", +] + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags 2.6.0", + "cssparser", + "derive_more 0.99.18", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.10.1", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +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 = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strawmelonjuice-lumina" +version = "0.1.0" +dependencies = [ + "actix-identity", + "actix-session", + "actix-web", + "async-std", + "build_const", + "chrono", + "colored", + "console", + "dotenv", + "futures", + "handlebars", + "home", + "indicatif", + "log", + "lumina-urls", + "magic-crypt", + "markdown", + "password-hash", + "pbkdf2", + "postgres", + "rand", + "regex", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "simplelog", + "time", + "tokio", + "toml", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand 2.1.0", + "rustix 0.38.34", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "tiger" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579abbce4ad73b04386dbeb34369c9873a8f9b749c7b99cbf479a2949ff715ed" +dependencies = [ + "digest", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +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.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.7", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf 0.11.2", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand", + "socket2 0.5.7", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-id" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "value-bag" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" + +[[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 = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.68", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", + "web-sys", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[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-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.11+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/backend-rs/Cargo.toml b/backend-rs/Cargo.toml new file mode 100644 index 0000000..294ad0d --- /dev/null +++ b/backend-rs/Cargo.toml @@ -0,0 +1,83 @@ +[workspace] +members = ["libs/lumina-urls"] +[workspace.dependencies] +serde = { version = "1.0.204", features = ["derive"] } +toml = "0.8.14" +reqwest = { version = "0.12.5", features = ["blocking"] } +serde_json = "1.0.120" +regex = "1.10.5" +[workspace.package] +authors = ['MLC "Strawmelonjuice" Bloeiman'] +license = "BSD-3-Clause" +repository = "https://github.com/strawmelonjuice/lumina" +edition = "2021" +license-file = "./LICENSE" + +[package] +name = "strawmelonjuice-lumina" +authors.workspace = true +publish = false +license.workspace = true +# license-file.workspace = true +repository.workspace = true +version = "0.1.0" +edition.workspace = true +[[bin]] +name = "lumina-server" +path = "./src/server.rs" + +[profile.dev] +opt-level = 3 +debug = true +strip = "none" +debug-assertions = true +overflow-checks = true +lto = false +panic = 'unwind' +incremental = true +codegen-units = 256 + +[profile.release] +opt-level = 3 +lto = true +panic = 'abort' + +[dependencies] +regex = { workspace = true } +serde = { workspace = true } +reqwest = { workspace = true, features = ["blocking"] } +serde_json = { workspace = true } +toml = { workspace = true } +lumina-urls = { path = "libs/lumina-urls" } +futures = "0.3.30" +async-std = "1.12.0" +colored = "2.0.4" +log = "0.4.21" +rusqlite = { version = "0.32.1", features = ["bundled"] } +time = "0.3.34" +password-hash = "0.5.0" +tokio = { version = "1.38.0", features = [ + "rt", + "rt-multi-thread", + "macros", + "time", +] } +pbkdf2 = "0.12.2" +actix-session = { version = "0.10.1", features = ["cookie-session"] } +actix-identity = "0.8.0" +actix-web = "4.7.0" +handlebars = "6.2.0" +postgres = "0.19.9" +home = "0.5.9" +magic-crypt = "4.0.1" +rand = "0.8.5" +build_const = "0.2.2" +markdown = "1.0.0-alpha.18" +chrono = "0.4.38" +simplelog = "0.12.2" +dotenv = "0.15.0" +indicatif = "0.17.8" +console = "0.15.8" +[build-dependencies] +build_const = "0.2.2" +markdown = "1.0.0-alpha.18" diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Black.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Black.ttf new file mode 100644 index 0000000..113cd3b Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Black.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-BlackItalic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-BlackItalic.ttf new file mode 100644 index 0000000..1c49fb2 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-BlackItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Bold.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Bold.ttf new file mode 100644 index 0000000..e3593fb Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Bold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-BoldItalic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-BoldItalic.ttf new file mode 100644 index 0000000..305b0b8 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-BoldItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraBold.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraBold.ttf new file mode 100644 index 0000000..83744c1 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraBold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraBoldItalic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000..54bcaca Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraBoldItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraLight.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraLight.ttf new file mode 100644 index 0000000..2d4e331 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraLight.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraLightItalic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraLightItalic.ttf new file mode 100644 index 0000000..ef666ad Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ExtraLightItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Italic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Italic.ttf new file mode 100644 index 0000000..27d32ed Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Italic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Light.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Light.ttf new file mode 100644 index 0000000..663d1de Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Light.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-LightItalic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-LightItalic.ttf new file mode 100644 index 0000000..d1b1fc5 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-LightItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Medium.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Medium.ttf new file mode 100644 index 0000000..001ebe7 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Medium.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-MediumItalic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-MediumItalic.ttf new file mode 100644 index 0000000..b7640be Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-MediumItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Regular.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Regular.ttf new file mode 100644 index 0000000..6f80647 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Regular.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-SemiBold.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-SemiBold.ttf new file mode 100644 index 0000000..0c93b7e Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-SemiBold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-SemiBoldItalic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-SemiBoldItalic.ttf new file mode 100644 index 0000000..e1a2989 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-SemiBoldItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Thin.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Thin.ttf new file mode 100644 index 0000000..c925f94 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-Thin.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ThinItalic.ttf b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ThinItalic.ttf new file mode 100644 index 0000000..dd39092 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Fira_Sans/FiraSans-ThinItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Fira_Sans/OFL.txt b/backend-rs/frontend_assets/fonts/Fira_Sans/OFL.txt new file mode 100644 index 0000000..0d0213a --- /dev/null +++ b/backend-rs/frontend_assets/fonts/Fira_Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/backend-rs/frontend_assets/fonts/Gantari/Gantari-Italic-VariableFont_wght.ttf b/backend-rs/frontend_assets/fonts/Gantari/Gantari-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..1ad7f7a Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/Gantari-Italic-VariableFont_wght.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/Gantari-VariableFont_wght.ttf b/backend-rs/frontend_assets/fonts/Gantari/Gantari-VariableFont_wght.ttf new file mode 100644 index 0000000..757a2f7 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/Gantari-VariableFont_wght.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/OFL.txt b/backend-rs/frontend_assets/fonts/Gantari/OFL.txt new file mode 100644 index 0000000..3598793 --- /dev/null +++ b/backend-rs/frontend_assets/fonts/Gantari/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Gantari Project Authors (https://github.com/Lafontype/Gantari) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/backend-rs/frontend_assets/fonts/Gantari/README.txt b/backend-rs/frontend_assets/fonts/Gantari/README.txt new file mode 100644 index 0000000..fd663ff --- /dev/null +++ b/backend-rs/frontend_assets/fonts/Gantari/README.txt @@ -0,0 +1,81 @@ +Gantari Variable Font +===================== + +This download contains Gantari as both variable fonts and static fonts. + +Gantari is a variable font with this axis: + wght + +This means all the styles are contained in these files: + Gantari/Gantari-VariableFont_wght.ttf + Gantari/Gantari-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Gantari: + Gantari/static/Gantari-Thin.ttf + Gantari/static/Gantari-ExtraLight.ttf + Gantari/static/Gantari-Light.ttf + Gantari/static/Gantari-Regular.ttf + Gantari/static/Gantari-Medium.ttf + Gantari/static/Gantari-SemiBold.ttf + Gantari/static/Gantari-Bold.ttf + Gantari/static/Gantari-ExtraBold.ttf + Gantari/static/Gantari-Black.ttf + Gantari/static/Gantari-ThinItalic.ttf + Gantari/static/Gantari-ExtraLightItalic.ttf + Gantari/static/Gantari-LightItalic.ttf + Gantari/static/Gantari-Italic.ttf + Gantari/static/Gantari-MediumItalic.ttf + Gantari/static/Gantari-SemiBoldItalic.ttf + Gantari/static/Gantari-BoldItalic.ttf + Gantari/static/Gantari-ExtraBoldItalic.ttf + Gantari/static/Gantari-BlackItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Black.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Black.ttf new file mode 100644 index 0000000..60ef02f Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Black.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-BlackItalic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-BlackItalic.ttf new file mode 100644 index 0000000..9d69214 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-BlackItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Bold.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Bold.ttf new file mode 100644 index 0000000..9df1a90 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Bold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-BoldItalic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-BoldItalic.ttf new file mode 100644 index 0000000..b42ccf2 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-BoldItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraBold.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraBold.ttf new file mode 100644 index 0000000..d0c9aaa Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraBold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraBoldItalic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraBoldItalic.ttf new file mode 100644 index 0000000..3585212 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraBoldItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraLight.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraLight.ttf new file mode 100644 index 0000000..62d8ee9 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraLight.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraLightItalic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraLightItalic.ttf new file mode 100644 index 0000000..961fead Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ExtraLightItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Italic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Italic.ttf new file mode 100644 index 0000000..432d15a Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Italic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Light.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Light.ttf new file mode 100644 index 0000000..53817f0 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Light.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-LightItalic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-LightItalic.ttf new file mode 100644 index 0000000..62a83fd Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-LightItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Medium.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Medium.ttf new file mode 100644 index 0000000..00b9668 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Medium.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-MediumItalic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-MediumItalic.ttf new file mode 100644 index 0000000..b4ba9c5 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-MediumItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Regular.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Regular.ttf new file mode 100644 index 0000000..452ba40 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Regular.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-SemiBold.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-SemiBold.ttf new file mode 100644 index 0000000..b160fce Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-SemiBold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-SemiBoldItalic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-SemiBoldItalic.ttf new file mode 100644 index 0000000..06ec92d Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-SemiBoldItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Thin.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Thin.ttf new file mode 100644 index 0000000..5ebba10 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-Thin.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ThinItalic.ttf b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ThinItalic.ttf new file mode 100644 index 0000000..15dbcb4 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Gantari/static/Gantari-ThinItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/JosefinSans-Italic-VariableFont_wght.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/JosefinSans-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..d2d2dd6 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/JosefinSans-Italic-VariableFont_wght.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/JosefinSans-VariableFont_wght.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/JosefinSans-VariableFont_wght.ttf new file mode 100644 index 0000000..00ea1e7 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/JosefinSans-VariableFont_wght.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/OFL.txt b/backend-rs/frontend_assets/fonts/Josefin_Sans/OFL.txt new file mode 100644 index 0000000..cbc2217 --- /dev/null +++ b/backend-rs/frontend_assets/fonts/Josefin_Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2010 The Josefin Sans Project Authors (https://github.com/ThomasJockin/JosefinSansFont-master), with Reserved Font Name "Josefin Sans". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/README.txt b/backend-rs/frontend_assets/fonts/Josefin_Sans/README.txt new file mode 100644 index 0000000..713c43f --- /dev/null +++ b/backend-rs/frontend_assets/fonts/Josefin_Sans/README.txt @@ -0,0 +1,77 @@ +Josefin Sans Variable Font +========================== + +This download contains Josefin Sans as both variable fonts and static fonts. + +Josefin Sans is a variable font with this axis: + wght + +This means all the styles are contained in these files: + Josefin_Sans/JosefinSans-VariableFont_wght.ttf + Josefin_Sans/JosefinSans-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Josefin Sans: + Josefin_Sans/static/JosefinSans-Thin.ttf + Josefin_Sans/static/JosefinSans-ExtraLight.ttf + Josefin_Sans/static/JosefinSans-Light.ttf + Josefin_Sans/static/JosefinSans-Regular.ttf + Josefin_Sans/static/JosefinSans-Medium.ttf + Josefin_Sans/static/JosefinSans-SemiBold.ttf + Josefin_Sans/static/JosefinSans-Bold.ttf + Josefin_Sans/static/JosefinSans-ThinItalic.ttf + Josefin_Sans/static/JosefinSans-ExtraLightItalic.ttf + Josefin_Sans/static/JosefinSans-LightItalic.ttf + Josefin_Sans/static/JosefinSans-Italic.ttf + Josefin_Sans/static/JosefinSans-MediumItalic.ttf + Josefin_Sans/static/JosefinSans-SemiBoldItalic.ttf + Josefin_Sans/static/JosefinSans-BoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Bold.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Bold.ttf new file mode 100644 index 0000000..190dce0 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Bold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-BoldItalic.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-BoldItalic.ttf new file mode 100644 index 0000000..798ce0e Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-BoldItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ExtraLight.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ExtraLight.ttf new file mode 100644 index 0000000..0bd55fa Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ExtraLight.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ExtraLightItalic.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ExtraLightItalic.ttf new file mode 100644 index 0000000..fc12b6d Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ExtraLightItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Italic.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Italic.ttf new file mode 100644 index 0000000..0d34f6b Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Italic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Light.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Light.ttf new file mode 100644 index 0000000..7be10de Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Light.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-LightItalic.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-LightItalic.ttf new file mode 100644 index 0000000..d63ec91 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-LightItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Medium.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Medium.ttf new file mode 100644 index 0000000..4a4c6bf Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Medium.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-MediumItalic.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-MediumItalic.ttf new file mode 100644 index 0000000..e2c97a3 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-MediumItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Regular.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Regular.ttf new file mode 100644 index 0000000..c116243 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Regular.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-SemiBold.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-SemiBold.ttf new file mode 100644 index 0000000..d09830d Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-SemiBold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-SemiBoldItalic.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-SemiBoldItalic.ttf new file mode 100644 index 0000000..511426a Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-SemiBoldItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Thin.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Thin.ttf new file mode 100644 index 0000000..a620236 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-Thin.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ThinItalic.ttf b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ThinItalic.ttf new file mode 100644 index 0000000..9eddd2a Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Josefin_Sans/static/JosefinSans-ThinItalic.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Syne/OFL.txt b/backend-rs/frontend_assets/fonts/Syne/OFL.txt new file mode 100644 index 0000000..146e2e0 --- /dev/null +++ b/backend-rs/frontend_assets/fonts/Syne/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2017 The Syne Project Authors (https://gitlab.com/bonjour-monde/fonderie/syne-typeface) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/backend-rs/frontend_assets/fonts/Syne/README.txt b/backend-rs/frontend_assets/fonts/Syne/README.txt new file mode 100644 index 0000000..ea7dcea --- /dev/null +++ b/backend-rs/frontend_assets/fonts/Syne/README.txt @@ -0,0 +1,67 @@ +Syne Variable Font +================== + +This download contains Syne as both a variable font and static fonts. + +Syne is a variable font with this axis: + wght + +This means all the styles are contained in a single file: + Syne/Syne-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Syne: + Syne/static/Syne-Regular.ttf + Syne/static/Syne-Medium.ttf + Syne/static/Syne-SemiBold.ttf + Syne/static/Syne-Bold.ttf + Syne/static/Syne-ExtraBold.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/backend-rs/frontend_assets/fonts/Syne/Syne-VariableFont_wght.ttf b/backend-rs/frontend_assets/fonts/Syne/Syne-VariableFont_wght.ttf new file mode 100644 index 0000000..4e94628 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Syne/Syne-VariableFont_wght.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Syne/static/Syne-Bold.ttf b/backend-rs/frontend_assets/fonts/Syne/static/Syne-Bold.ttf new file mode 100644 index 0000000..ced7219 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Syne/static/Syne-Bold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Syne/static/Syne-ExtraBold.ttf b/backend-rs/frontend_assets/fonts/Syne/static/Syne-ExtraBold.ttf new file mode 100644 index 0000000..331b59b Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Syne/static/Syne-ExtraBold.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Syne/static/Syne-Medium.ttf b/backend-rs/frontend_assets/fonts/Syne/static/Syne-Medium.ttf new file mode 100644 index 0000000..dea196c Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Syne/static/Syne-Medium.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Syne/static/Syne-Regular.ttf b/backend-rs/frontend_assets/fonts/Syne/static/Syne-Regular.ttf new file mode 100644 index 0000000..ebca922 Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Syne/static/Syne-Regular.ttf differ diff --git a/backend-rs/frontend_assets/fonts/Syne/static/Syne-SemiBold.ttf b/backend-rs/frontend_assets/fonts/Syne/static/Syne-SemiBold.ttf new file mode 100644 index 0000000..8ee519c Binary files /dev/null and b/backend-rs/frontend_assets/fonts/Syne/static/Syne-SemiBold.ttf differ diff --git a/backend-rs/frontend_assets/handlebars/home-side.handlebars b/backend-rs/frontend_assets/handlebars/home-side.handlebars new file mode 100644 index 0000000..59eb99e --- /dev/null +++ b/backend-rs/frontend_assets/handlebars/home-side.handlebars @@ -0,0 +1,30 @@ + + +
+
+

Timelines

+ + + + + + +
+
+

Bubbles

+
+
\ No newline at end of file diff --git a/backend-rs/frontend_assets/handlebars/postrender.handlebars b/backend-rs/frontend_assets/handlebars/postrender.handlebars new file mode 100644 index 0000000..025ba93 --- /dev/null +++ b/backend-rs/frontend_assets/handlebars/postrender.handlebars @@ -0,0 +1,71 @@ +
+
+ {{#if (numequal posttype 2)}} + {{!-- Display title for article posts --}} +

{title}

+ {{/if}} + {{#if (numequal posttype 3)}} + {{!-- Display media in media posts --}} +
+ +
+

{{content}}

+
+
+ {{/if}} + + {{#if (numequal posttype 1)}} +
+
+

{{content}}

+
+
+ {{/if}} +
+
+ {{#if tags}} +

Tags on this post

+
    + {{#each tags}} + {{!-- Displaying tags -- if any -- in their boxy little list. --}} +
  • {{this}}
  • + {{/each}} +
+ {{/if}} +
+
+
+ +
+ {{{button_push}}} +
+ Push up +
+
+ +
+ {{{button_comment}}} +
+ Comment +
+
+ +
+ {{{button_boost}}} +
+ Bump +
+
+
+
+
diff --git a/backend-rs/frontend_assets/handlebars/timeline.handlebars b/backend-rs/frontend_assets/handlebars/timeline.handlebars new file mode 100644 index 0000000..16a57e0 --- /dev/null +++ b/backend-rs/frontend_assets/handlebars/timeline.handlebars @@ -0,0 +1,17 @@ + + + + + + Home - {{Username}}@{{InstanceName}} + + + + + + + \ No newline at end of file diff --git a/backend-rs/frontend_assets/html/examplepost.html b/backend-rs/frontend_assets/html/examplepost.html new file mode 100644 index 0000000..afe4c89 --- /dev/null +++ b/backend-rs/frontend_assets/html/examplepost.html @@ -0,0 +1,595 @@ +

Example text post (manually edited static html)

+
+
+ + +
+
+

+ Guys... Do you think Dave Mustaine would like Gerard Way? I + don't know... +

+
+
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ C +
+
+
+ +
+ B +
+
+
+
+
+
+

Example image/media post (manually edited static html)

+
+
+ +
+ +
+

+ Just got my new image placeholder delivered 💖✨✨✨ +

+
+
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ C +
+
+
+ +
+ B +
+
+
+
+
+
+

Example article post (manually edited static html)

+
+
+ +

My latin learning progress

+ + + +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. + Posuere lorem ipsum dolor sit amet. Porttitor eget dolor morbi + non arcu risus quis varius. Fames ac turpis egestas sed tempus. + Pulvinar etiam non quam lacus suspendisse faucibus interdum + posuere. Euismod in pellentesque massa placerat duis ultricies + lacus sed. Nullam eget felis eget nunc lobortis mattis aliquam + faucibus purus. Nullam eget felis eget nunc lobortis. Amet + consectetur adipiscing elit pellentesque habitant morbi + tristique senectus. Sed turpis tincidunt id aliquet risus + feugiat in. Ut consequat semper viverra nam. Sociis natoque + penatibus et magnis dis parturient montes nascetur. Imperdiet + massa tincidunt nunc pulvinar sapien et. Lorem mollis aliquam ut + porttitor leo a diam. +

+ +

+ In fermentum et sollicitudin ac. Tellus id interdum velit + laoreet id. Nibh cras pulvinar mattis nunc. + Interdum velit laoreet id donec ultrices tincidunt arcu. Ac + tortor dignissim convallis aenean. + Sit amet nulla facilisi morbi tempus. Nec feugiat in fermentum + posuere urna nec tincidunt praesent. Nunc mi ipsum faucibus + vitae aliquet nec ullamcorper sit amet. At augue eget arcu + dictum varius. Quam pellentesque nec nam aliquam sem et tortor + consequat. Duis tristique sollicitudin nibh sit amet commodo. +

+
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ C +
+
+
+ +
+ B +
+
+
+
+
+
diff --git a/backend-rs/frontend_assets/html/home.html b/backend-rs/frontend_assets/html/home.html new file mode 100644 index 0000000..2a1ca58 --- /dev/null +++ b/backend-rs/frontend_assets/html/home.html @@ -0,0 +1,431 @@ + + + + + Home - Lumina(@{{iid}}) + + + + + + + + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

+ Failed to load post editor. +

+
+ + diff --git a/backend-rs/frontend_assets/html/index.html b/backend-rs/frontend_assets/html/index.html new file mode 100644 index 0000000..e7cf938 --- /dev/null +++ b/backend-rs/frontend_assets/html/index.html @@ -0,0 +1,139 @@ + + + + + + Welcome - Lumina(@{{iid}}) + + + + + + + + +
+
+

Lumina

+

+ Hello, this is an Lumina instance you landed on! The + specific ID of this instance is + localhost. +

+

What is Lumina?

+

haha, something that is not ready for you to see!

+
+
+ + diff --git a/backend-rs/frontend_assets/html/login.html b/backend-rs/frontend_assets/html/login.html new file mode 100644 index 0000000..abd9719 --- /dev/null +++ b/backend-rs/frontend_assets/html/login.html @@ -0,0 +1,196 @@ + + + + + + + Login - Lumina(@{{iid}}) + + + + + + + + +
+
+
+ + +
+
+ +
+ +
+
+ +

+ Do you not have an account on this instance yet? + + Sign up. +

+
+
+ + diff --git a/backend-rs/frontend_assets/html/signup.html b/backend-rs/frontend_assets/html/signup.html new file mode 100644 index 0000000..51dbc55 --- /dev/null +++ b/backend-rs/frontend_assets/html/signup.html @@ -0,0 +1,210 @@ + + + + + + + Sign up - Lumina(@{{iid}}) + + + + + + + + +
+
+
+ + +
+
+ + + This email may be the only way to get back in to your + account if you're locked out! +
+
+ + + + +
+ +

+ Do you already have an account? + Log in. +

+
+
+ + diff --git a/backend-rs/frontend_assets/html/writer.html b/backend-rs/frontend_assets/html/writer.html new file mode 100644 index 0000000..b1820d4 --- /dev/null +++ b/backend-rs/frontend_assets/html/writer.html @@ -0,0 +1,142 @@ + + +
+
+
+ Post editor +
+ +
+ +
+
+
+
+ +
+
+ +
+
+ + The post editor supports simple MarkDown + +
+
    +
  • Use * or _ to make text italic
  • +
  • Use ** to make text bold
  • +
  • Use ` to use a code block
  • + +
+

The article editor supports more advanced MarkDown.

+
+
+
+ + +
+
diff --git a/backend-rs/frontend_assets/png/luminalogo-0.png b/backend-rs/frontend_assets/png/luminalogo-0.png new file mode 100644 index 0000000..6115bb1 Binary files /dev/null and b/backend-rs/frontend_assets/png/luminalogo-0.png differ diff --git a/backend-rs/frontend_assets/png/luminalogo-1.png b/backend-rs/frontend_assets/png/luminalogo-1.png new file mode 100644 index 0000000..46bc764 Binary files /dev/null and b/backend-rs/frontend_assets/png/luminalogo-1.png differ diff --git a/backend-rs/frontend_assets/styles/initial_customstyles.css b/backend-rs/frontend_assets/styles/initial_customstyles.css new file mode 100644 index 0000000..63863d4 --- /dev/null +++ b/backend-rs/frontend_assets/styles/initial_customstyles.css @@ -0,0 +1 @@ +/* This CSS file is here to override theme choices and colorschemes. If not defined, defaults compiled into Lumina are used. */ diff --git a/backend-rs/frontend_assets/styles/main.pcss b/backend-rs/frontend_assets/styles/main.pcss new file mode 100644 index 0000000..61ec9b6 --- /dev/null +++ b/backend-rs/frontend_assets/styles/main.pcss @@ -0,0 +1,117 @@ +/*! + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +@font-face { + font-family: "Josefin Sans"; + src: url(/fonts/Josefin_Sans/JosefinSans-VariableFont_wght.ttf); +} + +@font-face { + font-family: "Fira Sans"; + src: url(/fonts/Fira_Sans/FiraSans-Regular.ttf); +} + +@font-face { + font-family: "Gantari"; + src: url(/fonts/Gantari/Gantari-VariableFont_wght.ttf); +} + +@font-face { + font-family: "Syne"; + src: url(/fonts/Syne/Syne-VariableFont_wght.ttf); +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + font-family: "Josefin Sans", "Fira Sans", serif; + } + + h1, + h2, + h3, + h4, + h5, + h6, + .special { + font-family: "Gantari", "Syne", cursive; + } + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: revert; + font-weight: revert; + } +} + +body { + overscroll-behavior-y: contain; +} + +/*noinspection ALL*/ +#homegrid { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 20%; +} + +.articlecontent > p { + font-size: 1.125rem /* 18px */; + line-height: 1.75rem /* 28px */; +} + +.postbttn { + position: relative; + display: inline-block; + border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ +} +.postbttn .postbttndescription { + visibility: hidden; + width: 120px; + pointer-events: none; + text-align: center; + padding: 5px 0; + border-radius: 6px; + position: absolute; + z-index: 1; +} + +.postbttn:hover .postbttndescription { + visibility: visible; + top: -5px; + right: 105%; +} +@media (prefers-color-scheme: dark) { +} +.postbttn .postbttndescription::after { + content: " "; + position: absolute; + bottom: 10%; + left: 100%; + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent transparent black; +} +@media (prefers-color-scheme: dark) { + .postbttn .postbttndescription::after { + border-color: transparent transparent transparent rgb(224 206 199); + } +} + +.editor-container:focus-within > div { + display: none; +} + +.editor-container:focus-within :is(#editor-short-input, #editor-long-input) { + display: initial; +} diff --git a/backend-rs/frontend_assets/svg/add.svg b/backend-rs/frontend_assets/svg/add.svg new file mode 100644 index 0000000..f99e161 --- /dev/null +++ b/backend-rs/frontend_assets/svg/add.svg @@ -0,0 +1,7 @@ + + + diff --git a/backend-rs/frontend_assets/svg/arrow-right.svg b/backend-rs/frontend_assets/svg/arrow-right.svg new file mode 100644 index 0000000..64641fd --- /dev/null +++ b/backend-rs/frontend_assets/svg/arrow-right.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/avatar1.svg b/backend-rs/frontend_assets/svg/avatar1.svg new file mode 100644 index 0000000..c2cc461 --- /dev/null +++ b/backend-rs/frontend_assets/svg/avatar1.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/avatar2.svg b/backend-rs/frontend_assets/svg/avatar2.svg new file mode 100644 index 0000000..1d1917c --- /dev/null +++ b/backend-rs/frontend_assets/svg/avatar2.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/avatar3.svg b/backend-rs/frontend_assets/svg/avatar3.svg new file mode 100644 index 0000000..9edc0ab --- /dev/null +++ b/backend-rs/frontend_assets/svg/avatar3.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/avatar4.svg b/backend-rs/frontend_assets/svg/avatar4.svg new file mode 100644 index 0000000..b539d7d --- /dev/null +++ b/backend-rs/frontend_assets/svg/avatar4.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/avatar5.svg b/backend-rs/frontend_assets/svg/avatar5.svg new file mode 100644 index 0000000..11a6c5d --- /dev/null +++ b/backend-rs/frontend_assets/svg/avatar5.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/avatar6.svg b/backend-rs/frontend_assets/svg/avatar6.svg new file mode 100644 index 0000000..863c617 --- /dev/null +++ b/backend-rs/frontend_assets/svg/avatar6.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/bell.svg b/backend-rs/frontend_assets/svg/bell.svg new file mode 100644 index 0000000..9cffa12 --- /dev/null +++ b/backend-rs/frontend_assets/svg/bell.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/boost.svg b/backend-rs/frontend_assets/svg/boost.svg new file mode 100644 index 0000000..a5f3a35 --- /dev/null +++ b/backend-rs/frontend_assets/svg/boost.svg @@ -0,0 +1,7 @@ + + + diff --git a/backend-rs/frontend_assets/svg/comment.svg b/backend-rs/frontend_assets/svg/comment.svg new file mode 100644 index 0000000..613caf0 --- /dev/null +++ b/backend-rs/frontend_assets/svg/comment.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/backend-rs/frontend_assets/svg/green_check.svg b/backend-rs/frontend_assets/svg/green_check.svg new file mode 100644 index 0000000..02c0aa0 --- /dev/null +++ b/backend-rs/frontend_assets/svg/green_check.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/luminalogo-0.svg b/backend-rs/frontend_assets/svg/luminalogo-0.svg new file mode 100644 index 0000000..a2bdce7 --- /dev/null +++ b/backend-rs/frontend_assets/svg/luminalogo-0.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + diff --git a/backend-rs/frontend_assets/svg/luminalogo-1.svg b/backend-rs/frontend_assets/svg/luminalogo-1.svg new file mode 100644 index 0000000..f01f774 --- /dev/null +++ b/backend-rs/frontend_assets/svg/luminalogo-1.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend-rs/frontend_assets/svg/push.svg b/backend-rs/frontend_assets/svg/push.svg new file mode 100644 index 0000000..4237cd3 --- /dev/null +++ b/backend-rs/frontend_assets/svg/push.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/backend-rs/frontend_assets/svg/red_cross.svg b/backend-rs/frontend_assets/svg/red_cross.svg new file mode 100644 index 0000000..a3a3f2f --- /dev/null +++ b/backend-rs/frontend_assets/svg/red_cross.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/svg/spinner.svg b/backend-rs/frontend_assets/svg/spinner.svg new file mode 100644 index 0000000..bfa3527 --- /dev/null +++ b/backend-rs/frontend_assets/svg/spinner.svg @@ -0,0 +1,6 @@ + + diff --git a/backend-rs/frontend_assets/toml/initial_config.toml b/backend-rs/frontend_assets/toml/initial_config.toml new file mode 100644 index 0000000..9a91a82 --- /dev/null +++ b/backend-rs/frontend_assets/toml/initial_config.toml @@ -0,0 +1,39 @@ +version = "0.3-a" +[server] +# What port to bind to (the server is designed with apache2 reverse-proxy in mind, so 80 is not necessarily default.) +port = 8085 +# What adress to bind to? +adress = "127.0.0.1" +# Secret key for cookie encryption +cookiekey = "Uneaten email in the washing machines to dry the popcorn" +# For development purposes, please set this to true on prod (and use SSL!) +secure = true + +[interinstance] +# Instance ID, equals, the domain name this instance is open on. +iid = "example.com" +# Specifies instances to send sync requests to. Note that these are only answered if both servers have each other listed. If not, the admin's will get a request to add them, but don't necessarily have to. +synclist = [ + # Of course, by default, the home domain is included, however! You can just remove it if you want to! + { name = "peonies.xyz", level = "full", key = "A secret kept between two friends (servers :) Do not re-use these keys!)" }, +] +# Ignored instances are no longer allowed to send requests to join this instance's synclist. +ignorelist = ["example.com"] +[interinstance.syncing] +# Specifies the interval between syncs. Minimum is 30. +syncintervall = 120 + +[database] +# What kind of database to use, currently only supporting "sqlite". +method = "sqlite" +# Encryption key for sensitive data like passwords. Changing it will immediately mark all existing passwords as false. +cryptkey = "In these parts, passwords are encrypted. Pardners. How do you do?" + +[database.sqlite] +# The database file to use for sqlite. +file = "instance-db.sqlite" + +[logging] +file-loglevel = 3 +console-loglevel = 2 +file = "instance-logging.log" diff --git a/backend-rs/libs/lumina-urls/Cargo.toml b/backend-rs/libs/lumina-urls/Cargo.toml new file mode 100644 index 0000000..c4135b5 --- /dev/null +++ b/backend-rs/libs/lumina-urls/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "lumina-urls" +description = "Parses urls and returns their information, used in Lumina for embeds" +authors.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +version = "0.1.0" + +[dependencies] +scraper = "0.19.0" +regex = { workspace = true } +serde = {workspace = true} +reqwest = { workspace = true, features = ["blocking"] } +serde_json= {workspace = true} \ No newline at end of file diff --git a/backend-rs/libs/lumina-urls/src/lib.rs b/backend-rs/libs/lumina-urls/src/lib.rs new file mode 100644 index 0000000..e1da4cb --- /dev/null +++ b/backend-rs/libs/lumina-urls/src/lib.rs @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +use crate::PageType::General; +use crate::UrlMetaInfo::{Fail, Succes}; +use scraper::{Html, Selector}; + +pub enum PageType { + General, + Article { + summary: Option, + author: Option, + published_time: Option, + modified_time: Option, + expiration_time: Option, + section: Option, + tag: Option, + }, + Media { + mime_type: Option, + media_url: Option, + }, +} +pub struct UrlMetaTags { + pub title: String, + pub description: String, + pub thumbnail: Option, + pub page: PageType, +} +pub enum UrlMetaInfo { + Succes(Box), + Fail, + Unset, +} + +impl UrlMetaInfo { + pub fn get(mut self, url: String) -> UrlMetaInfo { + match self { + Succes(_) | Fail => self, + UrlMetaInfo::Unset => { + let html = match reqwest::blocking::get(url) { + Ok(w) => match w.text() { + Ok(o) => o, + Err(_) => { + self = Fail; + return self; + } + }, + Err(_) => { + self = Fail; + return self; + } + }; + let document = Html::parse_document(&html); + let title = onesome( + { + let selector = Selector::parse("title").unwrap(); + document + .select(&selector) + .next() + .map(|elm| String::from(elm.value().name())) + }, + extract("og:title", &document), + ) + .unwrap_or(String::from("Could not get a title at this time.")); + self = Succes(Box::from(UrlMetaTags { + title, + description: onesome( + extract("description", &document), + extract("og:description", &document), + ) + .unwrap_or(String::from("Could not get a description at this time.")), + thumbnail: onesome(extract("og:image", &document), extract("image", &document)), + page: General, + })); + self + } + } + } +} + +fn extract(name: &str, document: &Html) -> Option { + let selector = Selector::parse(format!(r#"meta[name="{}"]"#, name).as_str()).unwrap(); + match document.select(&selector).next() { + Some(elm) => elm.value().attr("content").map(String::from), + None => None, + } +} + +/// Out of two `Option`'s, picks the one containing a `Some(T)`. +/// If both have `Some`, `a` will be preferred. If both have `None`, `None` will be returned. +fn onesome(a: Option, b: Option) -> Option { + match a { + Some(aa) => Some(aa), + None => b, + } +} diff --git a/backend-rs/src/api_fe.rs b/backend-rs/src/api_fe.rs new file mode 100644 index 0000000..b8b095e --- /dev/null +++ b/backend-rs/src/api_fe.rs @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +//! ## API's to the front-end. +//! This module contains the API endpoints for the frontend, most of them being Actix request factories. + +use actix_session::Session; +use actix_web::http::StatusCode; +use actix_web::web::Data; +use actix_web::{HttpRequest, HttpResponse}; +use colored::Colorize; +use serde::{Deserialize, Serialize}; +use tokio::sync::{Mutex}; + +use crate::assets::STR_ASSETS_HOME_SIDE_HANDLEBARS; +use crate::database::users::auth::{check, AuthResponse}; +use crate::database::users::{add, SafeUser}; +use crate::database::{self}; +use crate::{LuminaConfig, ServerVars}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +struct FEJSonObj { + // pub pulled: i128, + pub instance: FEJsonObjInstanceInfo, + pub user: SafeUser, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +struct FEJsonObjInstanceInfo { + pub iid: String, + pub last_sync: i64, +} + +pub enum ShieldValue { + Notsafe(HttpResponse), + Safe, +} +async fn shield( + session: Session, + server_vars_mutex: &Data>, + halt: HttpResponse, +) -> ShieldValue { + let server_vars = ServerVars::grab(server_vars_mutex).await; + let config = server_vars.clone().config; + let id_ = session.get::("userid").unwrap_or(None); + let id = id_.unwrap_or(-100); + let safe = checksessionvalidity(id, &session, &config); + if !safe { + session.purge(); + ShieldValue::Notsafe(halt) + } else { + ShieldValue::Safe + } +} + +pub(crate) fn checksessionvalidity(id: i64, session: &Session, config: &LuminaConfig) -> bool { + match id { + -100 => false, + _ => match session.get::("validity") { + Ok(s) => matches!(s, Some(a) if a == config.clone().erun.session_valid), + Err(_) => false, + }, + } +} + +pub(crate) async fn update( + server_vars_mutex: Data>, + session: Session, + req: HttpRequest, +) -> HttpResponse { + let server_vars: ServerVars = ServerVars::grab(&server_vars_mutex).await; + let config: LuminaConfig = server_vars.clone().config; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + let username_a = session.get::("username"); + let username_b = username_a.unwrap_or(None).unwrap_or(String::from("")); + let username_c = if username_b != *"" { + format!("/{}", username_b.green()) + } else { + String::from("") + }; + info!( + "{}\t{:>45.47}\t\t{}{:<26}", + "Request/200".bright_green(), + "/api/fe/update".magenta(), + ip.yellow(), + username_c + ); + let mut d: FEJSonObj = FEJSonObj { + // pulled: -1, + instance: FEJsonObjInstanceInfo { + iid: config.lumina_synchronisation_iid.clone(), + last_sync: -1, + }, + user: SafeUser { + username: "unset".to_string(), + id: -1, + email: "unset".to_string(), + }, + }; + let userd_maybe = database::fetch::user(&config, ("username", username_b)).unwrap_or(None); + if let Some(userd) = userd_maybe { + d.user = SafeUser { + username: userd.username, + id: userd.id, + email: userd.email, + }; + }; + return HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body(serde_json::to_string(&d).unwrap()); +} +#[derive(Deserialize)] +pub(super) struct AuthReqData { + username: String, + password: String, +} + +pub(crate) async fn auth( + server_vars_mutex: Data>, + session: Session, + req: HttpRequest, + data: actix_web::web::Json, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let config = server_vars.clone().config; + server_vars.tell("Auth request received."); + let result = check( + data.username.clone(), + data.password.clone(), + &server_vars_mutex, + ) + .await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + match result { + AuthResponse::Success(user_id) => { + let user = database::fetch::user(&config, ("id", user_id.to_string())) + .unwrap() + .unwrap(); + let username = user.username; + info!("User '{0}' logged in succesfully from {1}", username, ip); + session.insert("userid", user.id).unwrap(); + session.insert("username", username).unwrap(); + session + .insert("validity", config.clone().erun.session_valid) + .unwrap(); + HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body(r#"{"Ok": true, "Errorvalue": ""}"#) + } + _ => HttpResponse::build(StatusCode::UNAUTHORIZED) + .content_type("text/json; charset=utf-8") + .body(r#"{"Ok": false}"#), + } +} + +#[derive(Deserialize)] +pub(super) struct AuthCreateUserReqData { + username: String, + email: String, + password: String, +} +pub(crate) async fn newaccount( + server_vars_mutex: Data>, + session: Session, + req: HttpRequest, + data: actix_web::web::Json, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let config = server_vars.clone().config; + server_vars.tell("User creation request: received."); + let result = add( + data.username.clone(), + data.email.clone(), + data.password.clone(), + &config.clone(), + ); + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + match result { + Ok(user_id) => { + let user = database::fetch::user(&config, ("id", user_id.to_string())) + .unwrap() + .unwrap(); + let username = user.username; + session.insert("userid", user.id).unwrap(); + session.insert("username", username).unwrap(); + session + .insert("validity", config.clone().erun.session_valid) + .unwrap(); + server_vars.tell(format!( + "User creation request: approved for {} @ {}", + user.id, ip + )); + HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body(r#"{"Ok": true}"#) + } + Err(e) => { + server_vars.tell(format!("User creation request: denied - {e}")); + HttpResponse::build(StatusCode::EXPECTATION_FAILED) + .content_type("text/json; charset=utf-8") + .body(format!(r#"{{"Ok": false, "Errorvalue": "{}"}}"#, e)) + } + } +} +#[derive(Deserialize)] +pub struct FEPageServeRequest { + location: String, +} +#[derive(Serialize)] +struct FEPageServeResponse { + main: String, + side: String, + message: Vec, +} +pub(crate) async fn pageservresponder( + server_vars_mutex: Data>, + session: Session, + // req: HttpRequest, + data: actix_web::web::Json, +) -> HttpResponse { + match shield( + session, + &server_vars_mutex, + HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body( + serde_json::to_string(&FEPageServeResponse { + main: String::from("It seems your session has expired."), + side: String::new(), + message: vec![1], + }) + .unwrap(), + ), + ) + .await + { + ShieldValue::Notsafe(o) => o, + ShieldValue::Safe => { + // These three WILL be used in the future, when pages actually get dynamic. + let location = data.location.clone(); + let server_vars = server_vars_mutex.lock().await.clone(); + let config: LuminaConfig = server_vars.clone().config; + let o: FEPageServeResponse = match location.as_str() { + "home" => FEPageServeResponse { + main: String::from( + r#" +

welcome to instance

+

+ as you can see, there is no such thing as a homepage. lumina is + not ready for anything yet. +

+ + "#, + ), + side: String::from(STR_ASSETS_HOME_SIDE_HANDLEBARS), + message: vec![], + }, + "test" => FEPageServeResponse { + message: vec![], + side: String::new(), + main: { + let mut s = format!( + "

Post fetched from DB (dynamically rendered using HandleBars)

\n{}\n", + &database::fetch::post(&config, ("pid", "1".to_string())) + .unwrap() + .unwrap() + .to_formatted(&config) + .to_html() + ); + s.push_str(include_str!("../frontend_assets/html/examplepost.html")); + s + }, + }, + "notifications-centre" => FEPageServeResponse { + main: String::from("Notifications should show up here!"), + side: String::from(""), + message: vec![33], + }, + "editor" => FEPageServeResponse { + main: String::from(crate::assets::STR_ASSETS_EDITOR_WINDOW_HTML), + side: String::from(""), + message: vec![34], + }, + + _ => { + return HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body( + serde_json::to_string(&FEPageServeResponse { + main: String::from( + "This page does not exist according to the instance server.", + ), + side: String::new(), + message: vec![2], + }) + .unwrap(), + ) + } + }; + HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body(serde_json::to_string(&o).unwrap()) + } + } +} +#[derive(Deserialize)] +pub struct FEUsernameCheckRequest { + u: String, +} + +pub(crate) async fn check_username( + server_vars_mutex: Data>, + data: actix_web::web::Json, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let config = server_vars.clone().config; + let username = data.u.clone(); + if database::users::char_check_username(username.clone()) { + return HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body(r#"{"Ok": false, "Why": "InvalidChars"}"#.to_string()); + } + if username.len() < 4 { + return HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body(r#"{"Ok": false, "Why": "TooShort"}"#.to_string()); + } + if database::fetch::user(&config.clone(), ("username", username.clone())) + .unwrap_or(None) + .is_some() + { + return HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body(r#"{"Ok": false, "Why": "userExists"}"#.to_string()); + }; + return HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body(r#"{"Ok": true}"#); +} + +#[derive(Deserialize)] +pub struct EditorContent { + a: String, +} +#[derive(Serialize, Deserialize)] +struct EditorResponse { + #[serde(rename = "Ok")] + ok: bool, + #[serde(rename = "htmlContent")] + html_content: String, +} +pub(crate) async fn render_editor_articlepost( + server_vars_mutex: Data>, + data: actix_web::web::Json, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let _config = server_vars.clone().config; + let unprocessed_md = data.a.clone(); + + let processed_md = + match markdown::to_html_with_options(unprocessed_md.as_str(), &markdown::Options::gfm()) { + Ok(html) => html, + Err(_) => { + return HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body( + serde_json::to_string(&EditorResponse { + ok: false, + html_content: String::from("Markdown processing failed."), + }) + .unwrap(), + ); + } + }; + let readied_html = processed_md + .replace(r#""#, r#""#) + .replace(r#"
"#, r#"
"#); + return HttpResponse::build(StatusCode::OK) + .content_type("text/json; charset=utf-8") + .body( + serde_json::to_string(&EditorResponse { + ok: true, + html_content: readied_html, + }) + .unwrap(), + ); +} + +mod media; diff --git a/backend-rs/src/api_fe/media.rs b/backend-rs/src/api_fe/media.rs new file mode 100644 index 0000000..e0a8d55 --- /dev/null +++ b/backend-rs/src/api_fe/media.rs @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +//! This module handles media-related API endpoints, such as uploading media. \ No newline at end of file diff --git a/backend-rs/src/api_ii.rs b/backend-rs/src/api_ii.rs new file mode 100644 index 0000000..b0431c2 --- /dev/null +++ b/backend-rs/src/api_ii.rs @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ +//! ## Inter-instance API's +//! This module contains the inter-instance api's. It will mostly be used for pulling from other instances and syncing. + +use std::time::Duration; + +use crate::{database, LuminaConfig, ServerVars, SynclistItem}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use tokio::time::sleep; + +pub(crate) async fn main(vars: ServerVars) { + let progressbars = MultiProgress::new(); + let config: LuminaConfig = vars.config.clone(); + let mut round = 0; + let mut sync_interval = config.lumina_synchronisation_interval; + if sync_interval < 30 { + sync_interval = 120 + }; + // count before starting to sync + let initial_waiting_time = sync_interval / 3; + println!( + "{}", + vars.format_tell(format!( + "Syncer: Syncing will start in {initial_waiting_time} seconds." + )) + .as_str() + ); + let waiting_counter = progressbars.add(ProgressBar::new(initial_waiting_time)); + waiting_counter.enable_steady_tick(Duration::from_secs(1)); + waiting_counter.set_style( + ProgressStyle::with_template("{bar:60.white} {pos:>7}/{len:7} {msg}") + .unwrap() + .progress_chars("██░"), + ); + let mut remaining_waiting_time = initial_waiting_time; + for _ in 0..initial_waiting_time { + remaining_waiting_time -= 1; + waiting_counter.inc(1); + waiting_counter + .set_message(vars.format_tell(format!("{} seconds left.", remaining_waiting_time))); + sleep(Duration::from_secs(1)).await; + } + + let number_of_instances: u64 = database::get_instance_sync_list(&config).unwrap_or(vec![]).len() as u64; + if number_of_instances == 0 { + waiting_counter.println(vars.format_tell("Syncer: No instances to sync from are listed. Syncer will close until further notice to preserve CPU threads.")); + waiting_counter.finish_and_clear(); + return; + } + let _s = if number_of_instances == 1 { "" } else { "s" }; + waiting_counter.println(vars.format_tell(format!( + "Syncer: Syncing from {number_of_instances} listed instance{_s} starting now." + ))); + waiting_counter.finish_and_clear(); + loop { + vars.tell(format!("Syncer: Syncing from {number_of_instances} listed instance{_s}, round {round}. Syncings are done every {sync_interval} seconds.")); + round += 1; + let sync_progress_through_instances = + progressbars.add(ProgressBar::new(number_of_instances)); + // bar.println(vars.format_tell("Syncing:")); + sync_progress_through_instances.set_style( + ProgressStyle::with_template("{bar:60.white} {pos:>7}/{len:7} {msg}") + .unwrap() + .progress_chars("██░"), + ); + for instance in database::get_instance_sync_list(&config).unwrap_or_default().into_iter() { + sync_progress_through_instances.inc(1); + sync_progress_through_instances + .set_message(vars.format_tell(format!("Syncing: {}", instance.name))); + // let sync_this_instance = MultiProgress::new().insert_after(&sync_progress_through_instances, ProgressBar::new(100)); + // sync_this_instance.set_style(ProgressStyle::with_template("{bar:60.white} {pos:>7}/{len:7} {msg}").unwrap().progress_chars("██░")); + sync_instance(instance.clone(), config.clone()); + sleep(Duration::from_secs(3)).await; + // sync_this_instance.finish(); + } + sync_progress_through_instances.finish_and_clear(); + vars.tell(String::from("Syncer: Sync done.")); + sleep(Duration::from_secs(sync_interval)).await; + } +} + +fn sync_instance(synclist_item: SynclistItem, config: LuminaConfig) { + // Syncing code here. + let _ = (synclist_item, config); + // todo!("Syncing code here.") +} diff --git a/backend-rs/src/assets.rs b/backend-rs/src/assets.rs new file mode 100644 index 0000000..7fd2f7e --- /dev/null +++ b/backend-rs/src/assets.rs @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +pub const STR_ASSETS_INDEX_HTML: &str = include_str!("../frontend_assets/html/index.html"); +pub const STR_ASSETS_LOGIN_HTML: &str = include_str!("../frontend_assets/html/login.html"); +pub const STR_ASSETS_SIGNUP_HTML: &str = include_str!("../frontend_assets/html/signup.html"); +pub const STR_ASSETS_HOME_HTML: &str = include_str!("../frontend_assets/html/home.html"); +pub const STR_CLEAN_CONFIG_TOML: &str = include_str!("../frontend_assets/toml/initial_config.toml"); +pub const STR_CLEAN_CUSTOMSTYLES_CSS: &str = + include_str!("../frontend_assets/styles/initial_customstyles.css"); + +pub const STR_ASSETS_APPJS: &str = include_str!("../generated/js/app.js"); +pub const STR_ASSETS_APPJS_MAP: &str = include_str!("../generated/js/app.js.map"); + +pub const STR_GENERATED_MAIN_MIN_CSS: &str = include_str!("../generated/css/main.min.css"); + +type Fontbytes = &'static [u8]; +pub struct Fonts { + pub josefin_sans: Fontbytes, + pub fira_sans: Fontbytes, + pub gantari: Fontbytes, + pub syne: Fontbytes, +} + +pub fn fonts() -> Fonts { + Fonts { + josefin_sans: include_bytes!( + "../frontend_assets/fonts/Josefin_Sans/JosefinSans-VariableFont_wght.ttf" + ), + fira_sans: include_bytes!("../frontend_assets/fonts/Fira_Sans/FiraSans-Regular.ttf"), + gantari: include_bytes!("../frontend_assets/fonts/Gantari/Gantari-VariableFont_wght.ttf"), + syne: include_bytes!("../frontend_assets/fonts/Syne/Syne-VariableFont_wght.ttf"), + } +} + +pub const STR_ASSETS_LOGO_SVG: &str = include_str!("../frontend_assets/svg/luminalogo-1.svg"); + +pub const STR_ASSETS_BTN_NEW_SVG: &str = include_str!("../frontend_assets/svg/add.svg"); + +pub const STR_ASSETS_BTN_PUSH_SVG: &str = include_str!("../frontend_assets/svg/push.svg"); + +pub const STR_ASSETS_BTN_COMMENT_SVG: &str = include_str!("../frontend_assets/svg/comment.svg"); + +pub const STR_ASSETS_BTN_BOOST_SVG: &str = include_str!("../frontend_assets/svg/boost.svg"); + +pub const STR_ASSETS_GREEN_CHECK_SVG: &str = include_str!("../frontend_assets/svg/green_check.svg"); + +pub const STR_ASSETS_SPINNER_SVG: &str = include_str!("../frontend_assets/svg/spinner.svg"); + +pub const STR_ASSETS_RED_CROSS_SVG: &str = include_str!("../frontend_assets/svg/red_cross.svg"); +pub const STR_ASSETS_ANON_SVG: &str = include_str!("../frontend_assets/svg/avatar1.svg"); + +pub fn vec_string_assets_anons_svg() -> Vec { + vec![ + STR_ASSETS_ANON_SVG.to_string(), + include_str!("../frontend_assets/svg/avatar2.svg").to_string(), + include_str!("../frontend_assets/svg/avatar3.svg").to_string(), + include_str!("../frontend_assets/svg/avatar4.svg").to_string(), + include_str!("../frontend_assets/svg/avatar5.svg").to_string(), + include_str!("../frontend_assets/svg/avatar6.svg").to_string(), + ] +} + +pub const BYTES_ASSETS_LOGO_PNG: &[u8] = include_bytes!("../frontend_assets/png/luminalogo-1.png"); + +pub const STR_ASSETS_EDITOR_WINDOW_HTML: &str = include_str!("../frontend_assets/html/writer.html"); + +pub const STR_ASSETS_HOME_SIDE_HANDLEBARS: &str = + include_str!("../frontend_assets/handlebars/home-side.handlebars"); +pub const STR_ASSETS_POST_RENDERS_HANDLEBARS: &str = + include_str!("../frontend_assets/handlebars/postrender.handlebars"); diff --git a/backend-rs/src/config.rs b/backend-rs/src/config.rs new file mode 100644 index 0000000..7922f43 --- /dev/null +++ b/backend-rs/src/config.rs @@ -0,0 +1,237 @@ +use crate::ERun; +use colored::Colorize; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct LuminaConfig { + pub(crate) lumina_server_port: u16, + pub(crate) lumina_server_addr: String, + pub(crate) lumina_server_https: bool, + pub(crate) lumina_synchronisation_iid: String, + pub(crate) lumina_synchronisation_interval: u64, + pub(crate) db_custom_salt: String, + pub(crate) db_connection_info: LuminaDBConnectionInfo, + pub(crate) logging: Option, + /// Run time information + pub(crate) erun: ERun, +} + +#[derive(Clone, Debug)] +pub enum LuminaDBConnectionInfo { + LuminaDBConnectionInfoPOSTGRES(postgres::Config), + LuminaDBConnectionInfoSQLite(PathBuf), +} + +#[derive(Default, Debug, Clone, PartialEq)] +pub struct LuminaLogConfig { + pub file_loglevel: Option, + pub term_loglevel: Option, + pub logfile: Option, +} +#[derive(Debug)] +enum DBType { + POSTGRES, + SQLITE, +} +impl LuminaConfig { + /// Loads the configuration from environment variables after reading an env file if it exists. + pub fn new(erun: ERun) -> Self { + if erun.cd.join(".env").exists() { + if dotenv::from_path(erun.cd.join(".env")).is_ok() { + info!( + "Loaded environment variables from '{}' file.", + erun.cd.join(".env").display() + ); + }; + } else { + info!( + "No '.env' file found in the directory '{}'. Using only environment variables.", + erun.cd.display() + ); + } + + let db_type: DBType = { + match std::env::var("_LUMINA_DB_TYPE_") + .unwrap_or(String::from("SQLITE")) + .to_uppercase() + .as_str() + { + "POSTGRES" => DBType::POSTGRES, + _ => { + warn!("{}", "Using SQLITE database, this is not recommended for production as it is not scalable.".yellow()); + info!("{}","To use a different database, set the '_LUMINA_DB_TYPE_' environment variable to 'POSTGRES'.".purple()); + DBType::SQLITE + } + } + }; + let db_connection_info = match db_type { + DBType::SQLITE => { + let keyname = "_LUMINA_SQLITE_FILE_"; + let def_val = String::from("instance.db"); + match std::env::var(keyname) { + Ok(val) => { + LuminaDBConnectionInfo::LuminaDBConnectionInfoSQLite(erun.cd.join(val)) + } + Err(_) => { + warn!("The key '{}' was not found in the environment variables. Using default: {}", keyname, def_val); + LuminaDBConnectionInfo::LuminaDBConnectionInfoSQLite(erun.cd.join(def_val)) + } + } + } + DBType::POSTGRES => { + let mut pg_config = postgres::Config::new(); + { + match std::env::var("_LUMINA_POSTGRES_USERNAME_") { + Ok(val) => { + pg_config.user(&val); + } + Err(_) => { + error!("No Postgres database user provided under environment variable '_LUMINA_POSTGRES_USER_'. Exiting."); + std::process::exit(1); + } + } + } + match std::env::var("_LUMINA_POSTGRES_HOST_") { + Ok(val) => { + pg_config.host(&val); + } + Err(_) => { + warn!("No Postgres database host provided under environment variable '_LUMINA_POSTGRES_HOST_'. Using default value 'localhost'."); + pg_config.host("localhost"); + } + }; + match std::env::var("_LUMINA_POSTGRES_PASSWORD_") { + Ok(val) => { + pg_config.password(&val); + } + Err(_) => { + info!("No Postgres database password provided under environment variable '_LUMINA_POSTGRES_PASSWORD_'. Using passwordless connection."); + } + }; + match std::env::var("_LUMINA_POSTGRES_DATABASE_") { + Ok(val) => { + pg_config.dbname(&val); + } + Err(_) => { + error!("No Postgres database name provided under environment variable '_LUMINA_POSTGRES_DATABASE_'. Exiting."); + std::process::exit(1); + } + }; + { + match std::env::var("_LUMINA_POSTGRES_PORT_") { + Ok(val) => match val.parse::() { + Ok(v) => { + pg_config.port(v); + } + Err(_) => { + error!("The value '{}' for the key '_LUMINA_POSTGRES_PORT_' is not a valid port number. Exiting.", val); + std::process::exit(1); + } + }, + Err(_) => { + warn!("No Postgres database port provided under environment variable '_LUMINA_POSTGRES_PORT_'. Using default value '5432'."); + pg_config.port(5432); + } + } + } + + LuminaDBConnectionInfo::LuminaDBConnectionInfoPOSTGRES(pg_config) + } + }; + + LuminaConfig { + lumina_server_port: { + let keyname = "_LUMINA_SERVER_PORT_"; + let def_val = String::from("8085"); + match std::env::var(keyname) { + Ok(val) => match val.parse::() { + Ok(v) => v, + Err(_) => { + error!("The value '{}' for the key '{}' is not a valid port number. Exiting.", val, keyname); + std::process::exit(1); + } + }, + Err(_) => { + warn!("The key '{}' was not found in the environment variables. Using default value.", keyname); + def_val.parse::().unwrap() + } + } + }, + lumina_server_addr: { + let keyname = "_LUMINA_SERVER_ADDR_"; + let def_val = String::from("localhost"); + match std::env::var(keyname) { + Ok(val) => val, + Err(_) => { + warn!("The key '{}' was not found in the environment variables. Using default value.", keyname); + def_val + } + } + }, + lumina_server_https: { + let keyname = "_LUMINA_SERVER_HTTPS_"; + let def_val = String::from("true"); + match std::env::var(keyname) { + Ok(val) => { + match val.parse::() { + Ok(v) => v, + Err(_) => { + error!("The value '{}' for the key '{}' is not a valid boolean. Exiting.", val, keyname); + std::process::exit(1); + } + } + } + Err(_) => { + warn!("The key '{}' was not found in the environment variables. Using default value.", keyname); + def_val.parse::().unwrap() + } + } + }, + lumina_synchronisation_iid: { + let keyname = "_LUMINA_SYNCHRONISATION_IID_"; + let def_val = String::from("localhost"); + match std::env::var(keyname) { + Ok(val) => val, + Err(_) => { + warn!("The key '{}' was not found in the environment variables. Using default value.", keyname); + def_val + } + } + }, + lumina_synchronisation_interval: { + let keyname = "_LUMINA_SYNCHRONISATION_INTERVAL_"; + let def_val = String::from("30"); + match std::env::var(keyname) { + Ok(val) => { + match val.parse::() { + Ok(v) => v, + Err(_) => { + error!("The value '{}' for the key '{}' is not a valid number. Exiting.", val, keyname); + std::process::exit(1); + } + } + } + Err(_) => { + warn!("The key '{}' was not found in the environment variables. Using default value.", keyname); + def_val.parse::().unwrap() + } + } + }, + db_custom_salt: { + let keyname = "_LUMINA_DB_SALT_"; + let def_val = String::from("sally_sal"); + match std::env::var(keyname) { + Ok(val) => val, + Err(_) => { + warn!("The key '{}' was not found in the environment variables. Using default value.", keyname); + def_val + } + } + }, + db_connection_info, + // Uses the default value of Logging::default() fer now. + logging: None, + erun, + } + } +} diff --git a/backend-rs/src/database.rs b/backend-rs/src/database.rs new file mode 100644 index 0000000..aecab48 --- /dev/null +++ b/backend-rs/src/database.rs @@ -0,0 +1,676 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ +#![allow(dead_code)] + +use crate::config::LuminaDBConnectionInfo; +use crate::post::PostInfo; +use crate::{LuminaConfig, SynclistItem}; +use colored::Colorize; +use rusqlite::{params, Connection}; +use serde::{Deserialize, Serialize}; +use std::any::type_name; +use std::io::Error; +use std::process; +use LuminaDBConnectionInfo::{LuminaDBConnectionInfoPOSTGRES, LuminaDBConnectionInfoSQLite}; +use users::User; + +/// Basic exchangable user information. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct IIExchangedUserInfo { + /// User ID + pub(crate) id: i64, + /// Known username + pub(crate) username: String, + /// Instance ID + pub(crate) instance: String, +} + +impl LuminaConfig { + /// Create a database connection to either SQLite or POSTGRES. + pub(crate) fn db_connect(&self) -> LuminaDBConnection { + match self.db_connection_info.clone() { + LuminaDBConnectionInfoSQLite(file) => match Connection::open(file.clone()) { + Ok(d) => LuminaDBConnection::SQLite(d), + Err(_e) => { + error!( + "Could not create a database connection to <{}>", + file.display().to_string().yellow() + ); + process::exit(1); + } + }, + LuminaDBConnectionInfoPOSTGRES(pg_config) => { + let f = pg_config.connect(postgres::tls::NoTls).unwrap(); + LuminaDBConnection::POSTGRES(f) + } + } + } +} + +pub enum LuminaDBConnection { + SQLite(Connection), + POSTGRES(postgres::Client), +} + +impl LuminaDBConnection { + pub(crate) fn initial_dbconf(&mut self) { + fn emergencyabort() { + error!("Could not configure the database correctly!"); + process::exit(1); + } + let qs = [ + "CREATE TABLE IF NOT EXISTS external_posts( + host_id INTEGER PRIMARY KEY, + source_id INTEGER NOT NULL, + source_instance TEXT NOT NULL + );", + "CREATE TABLE IF NOT EXISTS interinstance_relations( + instance_id TEXT PRIMARY KEY, + synclevel TEXT NOT NULL, + last_contact INTEGER NOT NULL + );", + "CREATE TABLE IF NOT EXISTS local_posts( + host_id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + privacy INTEGER NOT NULL + );", + "CREATE TABLE IF NOT EXISTS posts_pool( + postid INTEGER PRIMARY KEY, + kind TEXT NOT NULL, + content TEXT NOT NULL, + from_local INTEGER NOT NULL + );", + "CREATE TABLE IF NOT EXISTS users( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + password TEXT NOT NULL, + email TEXT NOT NULL + );", + ]; + + for query in qs { + match self.execute0(query) { + Ok(_) => {} + Err(_e) => emergencyabort(), + } + } + } + pub(crate) fn execute0(&mut self, query: &str) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => { + conn.execute(query, ()).map(|_r| ()).map_err(|_e| ()) + } + LuminaDBConnection::POSTGRES(client) => { + client.execute(query, &[]).map(|_r| ()).map_err(|_e| ()) + } + } + } + pub(crate) fn execute1(&mut self, query: &str, param: impl AsRef) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute(query, &[¶m.as_ref()]) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute(query, &[¶m.as_ref()]) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + pub(crate) fn execute2( + &mut self, + query: &str, + params: (impl AsRef, impl AsRef), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute(query, &[¶ms.0.as_ref(), ¶ms.1.as_ref()]) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute(query, &[¶ms.0.as_ref(), ¶ms.1.as_ref()]) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + pub(crate) fn execute3( + &mut self, + query: &str, + params: (impl AsRef, impl AsRef, impl AsRef), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + pub(crate) fn execute4( + &mut self, + query: &str, + params: ( + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + ), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + pub(crate) fn execute5( + &mut self, + query: &str, + params: ( + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + ), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + pub fn execute6( + &mut self, + query: &str, + params: ( + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + ), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + + pub fn execute7( + &mut self, + query: &str, + params: ( + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + ), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + &(params.6.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + &(params.6.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + pub fn execute8( + &mut self, + query: &str, + params: ( + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + ), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + &(params.6.as_ref()), + &(params.7.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + &(params.6.as_ref()), + &(params.7.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + pub fn execute9( + &mut self, + query: &str, + params: ( + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + ), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + &(params.6.as_ref()), + &(params.7.as_ref()), + &(params.8.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + &(params.6.as_ref()), + &(params.7.as_ref()), + &(params.8.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + } + } + pub fn execute10( + &mut self, + query: &str, + params: ( + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + impl AsRef, + ), + ) -> Result<(), ()> { + match self { + LuminaDBConnection::SQLite(conn) => conn + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + &(params.6.as_ref()), + &(params.7.as_ref()), + &(params.8.as_ref()), + &(params.9.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + LuminaDBConnection::POSTGRES(client) => client + .execute( + query, + &[ + &(params.0.as_ref()), + &(params.1.as_ref()), + &(params.2.as_ref()), + &(params.3.as_ref()), + &(params.4.as_ref()), + &(params.5.as_ref()), + &(params.6.as_ref()), + &(params.7.as_ref()), + &(params.8.as_ref()), + &(params.9.as_ref()), + ], + ) + .map(|_r| ()) + .map_err(|_e| ()), + } + } +} + +pub trait DatabaseItem {} +impl DatabaseItem for PostInfo {} + +pub enum UniversalFetchAnswer { + Post(PostInfo), + User(User), + None, + Err(Error), +} +impl UniversalFetchAnswer { + /// Unwraps a post from a fetch answer. + /// # Panics + /// Will panic if the fetch answer is not a post. + /// # Returns + /// * `PostInfo` - The post metadata. + pub fn unwrap_post(self) -> PostInfo { + match self { + UniversalFetchAnswer::Post(s) => s, + UniversalFetchAnswer::None => { + panic!("Attempted to unwrap a post from a None value!"); + } + UniversalFetchAnswer::Err(_) => { + panic!("Attempted to unwrap a post from an error!"); + } + _ => { + panic!("Attempted to unwrap a post from a different type of fetch!"); + } + } + } + /// Unwraps a user from a fetch answer. + /// # Panics + /// Panics if the fetch answer is not a user. + /// # Returns + /// * `BasicUserInfo` - The user metadata. + pub fn unwrap_user(self) -> User { + match self { + UniversalFetchAnswer::User(s) => s, + UniversalFetchAnswer::None => { + panic!("Attempted to unwrap a user from a None value!"); + } + UniversalFetchAnswer::Err(_) => { + panic!("Attempted to unwrap a user from an error!"); + } + _ => { + panic!("Attempted to unwrap a user from a different type of fetch!"); + } + } + } +} + +/// # `storage::unifetch<>()` +/// Fetches well-known data types from the database. +/// # Arguments +/// * `config` - A reference to the LuminaConfig struct. +/// * `discriminator` - A tuple containing the discriminator and the value to search for. +/// # Returns +/// * `UniversalFetchAnswer` - The fetched data. Wrapped in an enum that can be unwrapped to the specific type. +/// # Panics +/// Panics if the database type is not supported. +/// # Note +/// **Discouraged: Use the specific fetch functions instead.** +/// # Example +/// ```rust +/// let config = LuminaConfig::default(); +/// let discriminator = ("username", "admin"); +/// let user = storage::unifetch::(&config, discriminator).unwrap_user(); +/// ``` +pub fn unifetch( + config: &LuminaConfig, + discriminator: (impl AsRef, impl AsRef), +) -> UniversalFetchAnswer { + if type_name::() == type_name::() { + let sres = fetch::post(config, discriminator); + let res = match sres { + Ok(s) => s, + Err(e) => return UniversalFetchAnswer::Err(e), + }; + match res { + Some(s) => UniversalFetchAnswer::Post(s), + None => UniversalFetchAnswer::None, + } + } else if type_name::() == type_name::() { + let sres = fetch::user(config, discriminator); + let res = match sres { + Ok(s) => s, + Err(e) => return UniversalFetchAnswer::Err(e), + }; + match res { + Some(s) => UniversalFetchAnswer::User(s), + None => UniversalFetchAnswer::None, + } + } else { + error!("Unknown or unsupported database type! Only SQLITE is supported as of now."); + process::exit(1); + } +} + +pub fn fetch_posts_in_range( + config: &LuminaConfig, + start: i64, + end: i64, +) -> Result, Error> { + match config.db_connect() { + LuminaDBConnection::SQLite(conn) => { + let mut stmt = conn + .prepare("SELECT * FROM PostsStore WHERE timestamp BETWEEN ?1 AND ?2") + .unwrap(); + + let post_rows = stmt + .query_map(params![start, end], |row| Ok(row_to_post_info(row))) + .unwrap(); + + let mut posts = Vec::new(); + for post_row in post_rows { + posts.push(post_row.unwrap()); + } + + Ok(posts) + } + LuminaDBConnection::POSTGRES(_) => { + todo!("Postgres not implemented yet."); + } + } +} + +fn row_to_post_info(row: &rusqlite::Row) -> PostInfo { + PostInfo { + lpid: row.get(0).unwrap(), + pid: row.get(1).unwrap(), + instance: row.get(2).unwrap(), + author_id: row.get(3).unwrap(), + timestamp: row.get(4).unwrap(), + content_type: row.get(5).unwrap(), + content: row.get(6).unwrap(), + tags: row.get(7).unwrap(), + } +} + +pub(crate) mod fetch; +pub(crate) mod users; + +pub fn get_instance_sync_list(config: &LuminaConfig) -> Result, Error> { + match config.db_connect() { + LuminaDBConnection::SQLite(conn) => { + let mut stmt = conn + .prepare("SELECT * FROM interinstance_relations") + .unwrap(); + + let sync_list_rows = stmt + .query_map((), |row| { + Ok(SynclistItem { + name: row.get(0).unwrap(), + level: row.get(1).unwrap(), + last_contact: row.get(2).unwrap(), + }) + }) + .unwrap(); + + let mut sync_list = Vec::new(); + for sync_list_row in sync_list_rows { + sync_list.push(sync_list_row.unwrap()); + } + + Ok(sync_list) + } + LuminaDBConnection::POSTGRES(mut client) => { + let rows = client + .query("SELECT * FROM interinstance_relations", &[]) + .unwrap(); + + let mut sync_list = Vec::new(); + for row in rows { + sync_list.push(SynclistItem { + name: row.get(0), + level: row.get(1), + last_contact: row.get(2), + }); + } + + Ok(sync_list) + } + } +} \ No newline at end of file diff --git a/backend-rs/src/database/fetch.rs b/backend-rs/src/database/fetch.rs new file mode 100644 index 0000000..06283b0 --- /dev/null +++ b/backend-rs/src/database/fetch.rs @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +use crate::database::{LuminaDBConnection}; +use crate::post::PostInfo; +use crate::{database, LuminaConfig}; +use std::io::{Error, ErrorKind}; +use crate::database::users::User; + +/// Fetches user from database +pub fn user( + config: &LuminaConfig, + discriminator: (impl AsRef, impl AsRef), +) -> Result, Error> { + match config.db_connect() { + LuminaDBConnection::SQLite(conn) => { + let mut stmt = conn + .prepare( + format!( + r#"select * from Users where {0} = '{1}'"#, + discriminator.0.as_ref(), + discriminator.1.as_ref() + ) + .trim(), + ) + .unwrap(); + debug!("{:?}", stmt); + let mut res = stmt + .query_map((), |row| { + Ok({ + serde_json::to_string(&User { + id: row.get(0)?, + username: row.get(1)?, + password: row.get(2)?, + email: row.get(3)?, + }) + .unwrap() + }) + }) + .unwrap(); + // println!("{:?}", res.nth(0)); + match res.next() { + None => Ok(None), + Some(r) => match r { + Ok(s) => { + let res: User = serde_json::from_str(&s).unwrap(); + Ok(Some(res)) + } + Err(f) => { + eprintln!("{:?}", f); + Err(Error::new(ErrorKind::Other, "Unparseable data.")) + } + }, + } + } + LuminaDBConnection::POSTGRES(_) => { + todo!("Postgres not implemented yet."); + } + } +} + +/// Fetches post from database +pub fn post( + config: &LuminaConfig, + discriminator: (impl AsRef, impl AsRef), +) -> Result, Error> { + match config.db_connect() { + LuminaDBConnection::SQLite(conn) => { + + + let mut stmt = conn + .prepare( + format!( + r#"select * from PostsStore where {0} = '{1}'"#, + discriminator.0.as_ref(), + discriminator.1.as_ref() + ) + .trim(), + ) + .unwrap(); + debug!("{:?}", stmt); + let mut res = stmt + .query_map((), |row| { + Ok({ + let s = database::row_to_post_info(row); + serde_json::to_string(&s).unwrap() + }) + }) + .unwrap(); + // println!("{:?}", res.nth(0)); + match res.next() { + None => Ok(None), + Some(r) => match r { + Ok(s) => { + let res: PostInfo = serde_json::from_str(&s)?; + Ok(Some(res)) + } + Err(f) => { + eprintln!("{:?}", f); + Err(Error::new(ErrorKind::Other, "Unparseable data.")) + } + }, + } + } + LuminaDBConnection::POSTGRES(_) => { + todo!("Postgres not implemented yet."); + } + } +} diff --git a/backend-rs/src/database/users.rs b/backend-rs/src/database/users.rs new file mode 100644 index 0000000..21de748 --- /dev/null +++ b/backend-rs/src/database/users.rs @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +use std::io::{Error, ErrorKind}; + +use magic_crypt::{new_magic_crypt, MagicCryptTrait}; +use serde::{Deserialize, Serialize}; +use crate::{database, LuminaConfig}; + +use super::{DatabaseItem, IIExchangedUserInfo}; +/// The minimum length of a username. +pub const MINIMUM_USERNAME_LENGTH: usize = 3; + +/// Checks if a username contains valid characters. +/// +/// # Arguments +/// +/// * `username` - A string slice that holds the username. +/// +/// # Returns +/// +/// * `bool` - Returns true if the username contains invalid characters, false otherwise. +pub(crate) fn char_check_username(username: String) -> bool { + username.chars().any(|c| { + match c { + ' ' | '\\' | '/' | '@' | '\n' | '\r' | '\t' | '\x0b' | '\'' | '"' | '(' | ')' | '`' + | '%' | '?' | '!' => true, + '#' => ( + // Make sure, if a # is in the username, only 4 numbers may follow it. + || { + let split_username = username.split('#'); + let array_split_username: Vec<&str> = split_username.collect(); + let lastbit = username.replacen(array_split_username[0], "", 1); + let firstbit = username.replacen(&*lastbit, "", 1); + let vec_split_username: Vec<&str> = vec![&*firstbit, &*lastbit]; + // println!("array: {:?}", array_split_username); + // println!("vec: {:?}", vec_split_username); + if vec_split_username.is_empty() || array_split_username[1].is_empty() { + return true; + }; + (!array_split_username[1].chars().all(char::is_numeric)) + || !(vec_split_username[1].len() == 5 || vec_split_username[1].len() == 7) + } + )(), + _ => false, + } + }) || !username + .replace(['_', '-', '.'], "") + .replacen('#', "", 1) + .chars() + .all(char::is_alphanumeric) +} + +/// Adds a new user to the database. +/// +/// # Arguments +/// +/// * `username` - A string slice that holds the username. +/// * `email` - A string slice that holds the email. +/// * `password` - A string slice that holds the password. +/// * `config` - A reference to the LuminaConfig struct. +/// +/// # Returns +/// +/// * `Result` - Returns the user id if the user is successfully added, otherwise returns an error. +pub(crate) fn add( + username: String, + email: String, + password: String, + config: &LuminaConfig, +) -> Result { + if char_check_username(username.clone()) { + return Err(Error::new( + ErrorKind::Other, + "Invalid characters in username!", + )); + } + if username.len() < MINIMUM_USERNAME_LENGTH { + return Err(Error::new(ErrorKind::Other, "Username is too short.")); + } + use regex::Regex; + let email_regex = Regex::new( + r"^([a-z0-9_+]([a-z0-9_+.]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})", + ) + .unwrap(); + if !email_regex.is_match(&email) { + return Err(Error::new(ErrorKind::Other, "Email is invalid.")); + }; + if password.len() < 8 { + return Err(Error::new(ErrorKind::Other, "Password is too short.")); + } + let mcrypt = new_magic_crypt!(config.clone().db_custom_salt, 256); + let conn = &config.clone(); + let onusername = database::fetch::user(&config.clone(), ("username", username.clone()))?; + let onemail = database::fetch::user(&config.clone(), ("email", email.clone()))?; + let res: Option = match onusername { + Some(s) => Some(s), + None => onemail, + }; + let password_encrypted = mcrypt.encrypt_str_to_base64(password); + match res { + Some(_) => Err(Error::new(ErrorKind::Other, "User already exists!")), + None => { + match conn.db_connect().execute3( + "INSERT INTO `users` (username, password, email) VALUES (?1,?2,?3)", + (username.clone(), password_encrypted, email), + ) { + Ok(_) => { + match database::fetch::user(&config.clone(), ("username", username.clone()))? { + Some(q) => Ok(q.id), + None => Err(Error::new( + ErrorKind::Other, + "Unknown database check error.", + )), + } + } + Err(_) => { + error!("Unknown database write error!"); + Err(Error::new( + ErrorKind::Other, + "Unknown database write error.", + )) + } + } + } + } +} + +pub(crate) mod auth { + use crate::database::users::User; + use crate::ServerVars; + use actix_web::web::Data; + use colored::Colorize; + use magic_crypt::{new_magic_crypt, MagicCryptTrait}; + use tokio::sync::Mutex; + + /// # AuthResponse + /// Tells the server what the database knows of a user. If it exists, and if the password provided was correct. + pub enum AuthResponse { + Success(i64), + UserNoneExistant, + Fail(FailReason), + } + /// # FailReason + /// The reason why the authentication failed. + pub enum FailReason { + Unspecified, + PasswordIncorrect, + InvalidUsername, + } + + /// # `storage::users::auth::check()` + /// Authenticates a user by plain username/email and password. + pub(crate) async fn check( + identifyer: String, + password: String, + server_vars_mutex: &Data>, + ) -> AuthResponse { + let server_vars = ServerVars::grab(server_vars_mutex).await; + if identifyer.chars().any(|c| { + matches!( + c, + ' ' | '\\' | '/' | '\n' | '\r' | '\t' | '\x0b' | '\'' | '"' | '(' | ')' | '`' + ) + }) { + // Invalid characters in username. + return AuthResponse::Fail(FailReason::InvalidUsername); + } + let config: crate::LuminaConfig = server_vars.clone().config.clone(); + let mcrypt = new_magic_crypt!(config.clone().db_custom_salt, 256); + let errorresponse = |e| { + error!("{}: \n\t\tRan into an error:\n {}", "Auth".purple(), e); + AuthResponse::Fail(FailReason::Unspecified) + }; + let onusername = + match super::database::fetch::user(&config.clone(), ("username", identifyer.clone())) { + Ok(a) => a, + Err(e) => return errorresponse(e), + }; + let onemail = + match super::database::fetch::user(&config.clone(), ("email", identifyer.clone())) { + Ok(a) => a, + Err(e) => return errorresponse(e), + }; + let a_some: Option = match onusername { + Some(s) => Some(s), + None => onemail, + }; + match a_some { + Some(u) => { + if u.password == mcrypt.encrypt_str_to_base64(password) { + server_vars.tell(format!( + "{}\t\t\t{}", + "Auth".purple(), + format!("User {} successfully authorised.", u.username.blue()).green() + )); + AuthResponse::Success(u.id) + } else { + server_vars.tell(format!( + "{}\t\t\t{}", + "Auth".purple(), + format!("User {}: Wrong password entered.", identifyer.blue()).bright_red() + )); + AuthResponse::Fail(FailReason::PasswordIncorrect) + } + } + None => { + server_vars.tell(format!( + "{}\t\t\t{}", + "Auth".purple(), + format!("User {} does not exist.", identifyer.blue()).bright_yellow() + )); + AuthResponse::UserNoneExistant + } + } + } +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct SafeUser { + pub id: i64, + pub username: String, + pub email: String, +} + +/// Basic user-identifying information. +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + /// User ID + pub(crate) id: i64, + /// Known username + pub(crate) username: String, + /// Hashed password + pub(crate) password: String, + /// Given email + pub(crate) email: String, +} + +impl User { + pub fn to_exchangable(&self, config: &LuminaConfig) -> IIExchangedUserInfo { + IIExchangedUserInfo { + id: self.id, + username: self.username.clone(), + instance: config.lumina_synchronisation_iid.clone(), + } + } +} + +impl DatabaseItem for User {} \ No newline at end of file diff --git a/backend-rs/src/post.rs b/backend-rs/src/post.rs new file mode 100644 index 0000000..ad361aa --- /dev/null +++ b/backend-rs/src/post.rs @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +use handlebars::*; +use serde::{Deserialize, Serialize}; + +use crate::assets::STR_ASSETS_POST_RENDERS_HANDLEBARS; +use crate::database::{unifetch, IIExchangedUserInfo}; +use crate::database::users::User; +use crate::LuminaConfig; + +#[derive(Debug, Serialize, Deserialize)] +pub struct PostInfo { + /// Local Post ID + pub lpid: i64, + /// Post ID + pub pid: i64, + /// Instance ID - Might not be necessary on local posts. Hence, being Option<>; + pub instance: Option, + /// Author ID + pub author_id: i64, + /// Timestamp (UNIX) + pub timestamp: i64, + /// Content type + // 1: Textual post (json) + // 2: Article textual post (json) + // 3: Media post (json) + // 4: Repost (json) + pub content_type: i32, + /// Content in JSON, deserialised depending on content_type. + pub content: String, + /// Tags + pub tags: String, // Json> +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PostPreRenderData { + pub(crate) posttype: i32, + pub(crate) author: IIExchangedUserInfo, + pub(crate) content: String, + pub(crate) timestamp: i64, + pub(crate) title: Option, + pub(crate) embeds: Option>, + pub(crate) tags: Vec, + pub(crate) local: bool, + button_push: String, + button_comment: String, + button_boost: String, +} +impl PostPreRenderData { + pub fn to_html(&self) -> String { + let mut handlebars = Handlebars::new(); + handlebars_helper!(num_is_equal: |x: u64, y: u64| x == y); + handlebars.register_helper("numequal", Box::new(num_is_equal)); + + handlebars + .render_template(STR_ASSETS_POST_RENDERS_HANDLEBARS, self) + .unwrap_or_else(|e| { + eprintln!("Error rendering post: {}", e); + String::from("Error rendering post.") + }) + } +} + +impl PostInfo { + pub fn to_formatted(&self, config: &LuminaConfig) -> PostPreRenderData { + let author_u: User = + unifetch::(config, ("id", self.author_id.to_string().as_str())) + .unwrap_user(); + + let author = IIExchangedUserInfo { + id: self.author_id, + username: author_u.username, + instance: self + .instance + .clone() + .unwrap_or(config.lumina_synchronisation_iid.clone()), + }; + let content; + + let mut embeds = None; + + let mut title = None; + + match self.content_type { + 1 => { + #[derive(Serialize, Deserialize)] + struct TextPost { + content: String, + } + + content = serde_json::from_str::(&self.content) + .unwrap() + .content; + } + 2 => { + #[derive(Serialize, Deserialize)] + struct ArticlePost { + header: String, + body: String, + } + + let article = serde_json::from_str::(&self.content).unwrap(); + content = markdown::to_html(&article.body); + title = Some(article.header); + } + 3 => { + #[derive(Serialize, Deserialize)] + struct MediaPost { + caption: String, + media_id: Vec, + } + + let media = serde_json::from_str::(&self.content).unwrap(); + content = markdown::to_html(&media.caption); + embeds = Some(media.media_id.iter().map(|x| x.to_string()).collect()); + } + 4 => { + #[derive(Serialize, Deserialize)] + struct Repost { + repost_id: i64, + source_instance: String, + } + + let repost = serde_json::from_str::(&self.content).unwrap(); + content = format!( + "Reposted from {}", + repost.source_instance, repost.repost_id, repost.source_instance + ); + } + _ => { + error!("Unknown content type!"); + panic!("Unknown content type!"); + } + }; + + let timestamp = self.timestamp; + + let tags = serde_json::from_str::>(&self.tags).unwrap(); + + let local = self.instance.is_none(); + + PostPreRenderData { + posttype: self.content_type, + author, + content, + timestamp, + title, + embeds, + tags, + local, + button_push: crate::assets::STR_ASSETS_BTN_PUSH_SVG + .replace(r#"height="120""#, r#"height="100%""#) + .replace(r#"width="120""#, r#"width="100%""#), + // + button_comment: crate::assets::STR_ASSETS_BTN_COMMENT_SVG + .replace(r#"height="120""#, r#"height="100%""#) + .replace(r#"width="120""#, r#"width="100%""#), + button_boost: crate::assets::STR_ASSETS_BTN_BOOST_SVG + .replace(r#"height="120""#, r#"height="100%""#) + .replace(r#"width="120""#, r#"width="100%""#), + } + } +} diff --git a/backend-rs/src/serve.rs b/backend-rs/src/serve.rs new file mode 100644 index 0000000..3000e6c --- /dev/null +++ b/backend-rs/src/serve.rs @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ +const SPECIALDATES: &str = r#" +/*Pride month banner*/ +body:has(.monthclass-6)::before { + margin: 0; + content: "Happy Pride Month! 💖🏳️‍🌈"; + justify-content: center; + align-items: center; + height: 1.4em; + color: black; + width: 100VW; + border-radius: 0; + display: inline-flex; + background-image: linear-gradient(to right, rgb(237, 34, 36), rgb(243, 91, 34), rgb(249, 150, 33), rgb(245, 193, 30), rgb(241, 235, 27) 27%, rgb(241, 235, 27), rgb(241, 235, 27) 33%, rgb(99, 199, 32), rgb(12, 155, 73), rgb(33, 135, 141), rgb(57, 84, 165), rgb(97, 55, 155), rgb(147, 40, 142)) +} + + + +body:has(.monthclass-6) { +--bs: 300% 100%; + +} +body:has(.monthclass-6):hover::before { +animation: prideBannerAnimation 10s linear infinite; + } +@keyframes prideBannerAnimation { + 0% { } + 25% { background-position: 0 0; + background-size: var(--bs); + background-repeat: repeat;} + 30% { background-position: 50% 0; + content: "Protect LGBTQ+ Rights! 🏳️‍🌈✊"; + background-size: var(--bs); + background-repeat: repeat; + } + 50% { background-position: 100% 0; +content: "Protect LGBTQ+ Rights! 🏳️‍🌈✊"; + background-size: var(--bs); + background-repeat: repeat; + } + + 75% { background-position: 0 0; + background-size: var(--bs); + background-repeat: repeat; + } + 80% { background-position: 50% 0; + content: "Protect LGBTQ+ Rights! 🏳️‍🌈 ✊"; + background-size: var(--bs); + background-repeat: repeat; + } + 100% { } +} +body:has(.monthclass-6):active::before { + animation: none; + animation-delay: 3s; + animation-duration: 999s; + animation-name: transrights; + animation-iteration-count: 1; + animation-timing-function: ease-in-out; +} +@keyframes transrights { + 0% { + content: "Protect trans Rights! ✊ 🩵🩷🤍🩷🩵"; + background-image: linear-gradient(to right, rgb(85, 205, 252), rgb(179, 157, 233), rgb(247, 168, 184), rgb(246, 216, 221), rgb(255, 255, 255) 45%, rgb(255, 255, 255), rgb(255, 255, 255) 55%, rgb(246, 216, 221), rgb(247, 168, 184), rgb(179, 157, 233), rgb(85, 205, 252)); + } +} +/*29th of februari is nonexistent in non-leap years*/ +body:has(.dayclass-29.monthclass-2)::before { + margin-top: .8em; + margin-bottom: .8em; + content: "[This day does not exist]"; + justify-content: center; + align-items: center; + height: 2.4em; + flex: none; + color: yellow; + width: 100%; + display: inline-flex; + background-color: black; + text-shadow: 22px 4px 2px rgba(255,255,0,0.6); + box-shadow: 2px 2px 10px 8px #3d3a3a; + animation-name: glitched; + animation-duration: 3s; + animation-iteration-count: infinite; + animation-timing-function: linear; + animation-direction: alternate; +} +@keyframes glitched { + 0% { + transform: skew(-20deg); + left: -4px; + } + 10% { + transform: skew(-20deg); + left: -4px; + } + 11% { + transform: skew(0deg); + left: 2px; + } + 50% { + transform: skew(0deg); + } + 51% { + transform: skew(10deg); + } + 59% { + transform: skew(10deg); + } + 60% { + transform: skew(0deg); + } + 100% { + transform: skew(0deg); + } +}"#; + +use actix_session::Session; +use actix_web::http::header::LOCATION; +use actix_web::http::StatusCode; +use actix_web::web::Data; +use actix_web::{HttpRequest, HttpResponse, Responder}; +use colored::Colorize; +use tokio::sync::{Mutex, MutexGuard}; + +use crate::database::{self, IIExchangedUserInfo}; +use crate::{LuminaConfig, ServerVars}; +use chrono::Datelike; +use crate::database::users::User; + +pub(super) async fn notfound( + server_vars_mutex: Data>, + req: HttpRequest, + session: Session, +) -> HttpResponse { + let _server_vars = ServerVars::grab(&server_vars_mutex).await; + let username_ = session.get::("username"); + let username = username_.unwrap_or(None).unwrap_or(String::from("")); + let username_b = if username != *"" { + format!("/{}", username.green()) + } else { + String::from("") + }; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + + warn!( + "{}\t{:>45.47}\t\t{}{:<26}", + "Request/404".bright_red(), + req.path().red(), + ip.yellow(), + username_b + ); + HttpResponse::NotFound().body("") +} + +pub(super) async fn root( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars: ServerVars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/".bright_magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + + HttpResponse::build(StatusCode::OK) + .content_type("text/html; charset=utf-8") + .body(replaceable( + crate::assets::STR_ASSETS_INDEX_HTML, + &server_vars, + )) +} + +fn replaceable(string: &str, server_vars: &ServerVars) -> String { + // "Compress" that CSS too, however, be careful. Two consecutive spaces can be removed, but one on its own might mean something! + let specialdates = SPECIALDATES.replace(['\r', '\n'], "").replace(" ", ""); + let current_date = chrono::Utc::now(); + let mut stylesheet: String = String::new(); + stylesheet.push_str(""); + let s = string + .replace( + "{{iid}}", + &server_vars + .clone() + .config + .lumina_synchronisation_iid + .clone(), + ) + .replace( + "monthclass-month", + format!("monthclass-{}", current_date.month()).as_str(), + ) + .replace( + "dayclass-day", + format!("dayclass-{}", current_date.day()).as_str(), + ) + .replace("", &stylesheet); + s.clone() +} + +pub(super) async fn login( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars: ServerVars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/login".bright_magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + + HttpResponse::build(StatusCode::OK) + .content_type("text/html; charset=utf-8") + .body(replaceable( + crate::assets::STR_ASSETS_LOGIN_HTML, + &server_vars, + )) +} + +pub(super) async fn signup( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars: ServerVars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/signup".bright_magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + + HttpResponse::build(StatusCode::OK) + .content_type("text/html; charset=utf-8") + .body(replaceable( + crate::assets::STR_ASSETS_SIGNUP_HTML, + &server_vars, + )) +} + +pub(super) async fn appjs( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/app.js".magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + HttpResponse::build(StatusCode::OK) + .content_type("text/javascript; charset=utf-8") + .body(crate::assets::STR_ASSETS_APPJS) +} + +pub(super) async fn appjsmap( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/app.js.map".magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + HttpResponse::build(StatusCode::OK) + .content_type("text/javascript; charset=utf-8") + .body(crate::assets::STR_ASSETS_APPJS_MAP) +} + +pub(super) async fn red_cross_svg( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/red-cross.svg".magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + HttpResponse::build(StatusCode::OK) + .content_type("image/svg+xml; charset=utf-8") + .body(crate::assets::STR_ASSETS_RED_CROSS_SVG) +} + +pub(super) async fn spinner_svg( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/spinner.svg".magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + HttpResponse::build(StatusCode::OK) + .content_type("image/svg+xml; charset=utf-8") + .body(crate::assets::STR_ASSETS_SPINNER_SVG) +} + +pub(super) async fn green_check_svg( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/green-check.svg".magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + HttpResponse::build(StatusCode::OK) + .content_type("image/svg+xml; charset=utf-8") + .body(crate::assets::STR_ASSETS_GREEN_CHECK_SVG) +} + +pub(super) async fn logo_svg( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/logo.svg".magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + HttpResponse::build(StatusCode::OK) + .content_type("image/svg+xml; charset=utf-8") + .body(crate::assets::STR_ASSETS_LOGO_SVG) +} + +pub(super) async fn logo_png( + server_vars_mutex: Data>, + req: HttpRequest, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + "/logo.png".magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + HttpResponse::build(StatusCode::OK) + .content_type("image/png; charset=utf-8") + .body(crate::assets::BYTES_ASSETS_LOGO_PNG) +} + +pub(super) async fn homepage( + server_vars_mutex: Data>, + session: Session, + req: HttpRequest, +) -> HttpResponse { + fence( + session, + server_vars_mutex, + req, + |_, server_vars, user, request| { + let coninfo = request.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{}\t{:>45.47}\t\t{}/{:<25}", + "Request/200".bright_green(), + "/home".bright_magenta(), + ip.yellow(), + user.username.green() + )); + HttpResponse::build(StatusCode::OK) + .content_type("text/html; charset=utf-8") + .body(replaceable( + crate::assets::STR_ASSETS_HOME_HTML, + &server_vars, + )) + }, + ) + .await +} + +pub(super) async fn logout( + server_vars_mutex: Data>, + session: Session, + req: HttpRequest, +) -> impl Responder { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let username_ = session.get::("username"); + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + match username_.unwrap_or(None) { + Some(username) => { + server_vars.tell(format!( + "{}\t{:>45.47}\t\t{}/{:<25}", + "Request/200".bright_green(), + "/session/logout".bright_magenta(), + ip.yellow(), + username.green() + )); + session.purge(); + HttpResponse::build(StatusCode::TEMPORARY_REDIRECT) + .append_header((LOCATION, "/login")) + .finish() + } + None => HttpResponse::build(StatusCode::TEMPORARY_REDIRECT) + .append_header((LOCATION, "/login")) + .finish(), + } +} + +/// Fence is a function serving kind of like middleware usually would. But actix middleware kinda sucks balls. So. +pub(crate) async fn fence( + session: Session, + server_vars_mutex: Data>, + req: HttpRequest, + next: fn( + config: LuminaConfig, + vars: ServerVars, + user: IIExchangedUserInfo, + req: HttpRequest, + ) -> HttpResponse, +) -> HttpResponse { + let server_vars: MutexGuard = server_vars_mutex.lock().await; + let config = server_vars.clone().config; + let id_ = session.get::("userid").unwrap_or(Some(-100)); + let id = id_.unwrap_or(-100); + debug!("Session validity: {:?}", session.get::("validity")); + debug!("Session contents: {:?}", session.entries()); + debug!("User ID: {:?}", id); + let safe = crate::api_fe::checksessionvalidity(id, &session, &config); + if !safe { + session.purge(); + HttpResponse::build(StatusCode::TEMPORARY_REDIRECT) + .append_header((LOCATION, "/login")) + .finish() + } else { + let user: User = database::fetch::user(&config, ("id", id.to_string())) + .unwrap() + .unwrap(); + + next( + config.clone(), + server_vars.clone().to_owned(), + user.to_exchangable(&config), + req, + ) + } +} diff --git a/backend-rs/src/server.rs b/backend-rs/src/server.rs new file mode 100644 index 0000000..5785c64 --- /dev/null +++ b/backend-rs/src/server.rs @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +#[macro_use] +extern crate log; +extern crate simplelog; + +use std::fmt::Debug; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::{env, fs, path::Path, process}; + +use actix_session::storage::CookieSessionStore; +use actix_session::{Session, SessionMiddleware}; +use actix_web::cookie::Key; +use actix_web::{get, HttpRequest, HttpResponse}; +use actix_web::{ + web::{self, Data}, + App, HttpServer, +}; +use colored::Colorize; +use rand::prelude::*; +use simplelog::*; +use tokio::sync::{Mutex, MutexGuard}; + +use crate::config::{LuminaConfig, LuminaLogConfig}; +use crate::serve::notfound; +use assets::{fonts, vec_string_assets_anons_svg, STR_CLEAN_CUSTOMSTYLES_CSS}; + +/// ## API's to the front-end. +mod api_fe; +/// # Inter-instance API's +mod api_ii; +/// ## Definition of assets, so file paths refactoring goes easier. +pub mod assets; +/// # Actions on the database +mod database; +/// # Actions on posts +mod post; +/// # Actions for gentle logging ("telling") +/// Logging doesn't need this, but for prettyness these are added as implementations on ServerVars. +mod tell; + +#[derive(Clone)] +struct ServerVars { + config: crate::config::LuminaConfig, +} + +#[derive(Clone)] +pub struct SynclistItem { + pub name: String, // The name of the instance to sync with, equal to the domain name it is public on. + pub level: String, // The level of syncing to do. "full" is the only one being implemented right now. + pub last_contact: i64, // The last time the instance was contacted. +} +impl ServerVars { + /// This function grabs the server variables from the provided mutex. + /// + /// # Arguments + /// + /// * `server_vars_mutex` - A reference to a `Data>` containing the server variables. + /// + /// # Returns + /// + /// * `ServerVars` - A clone of the server variables stored in the mutex. These are cloned so that the mutex can be unlocked without having to wait for the calling function to finish. + /// + /// # Errors + /// + /// This function does not return any errors. + /// + /// # Panics + /// + /// This function does not panic. + /// + /// # Examples + /// + /// ``` + /// // This is the shield function from 'api_fe', it implements grab in the simplest way. + /// async fn shield( + /// session: Session, + /// server_vars_mutex: &Data>, + /// halt: HttpResponse, + /// ) -> Option { + /// let server_vars = ServerVars::grab(server_vars_mutex).await; + /// let config = server_vars.clone().config; + /// ... + /// ``` + pub(crate) async fn grab(server_vars_mutex: &Data>) -> ServerVars { + let vars: MutexGuard = server_vars_mutex.lock().await; + vars.clone() + } +} + +#[derive(Default, Debug, Clone, PartialEq)] +pub struct ERun { + pub cd: PathBuf, + pub customcss: String, + pub session_valid: i64, +} + +pub struct LogSets { + pub file_loglevel: LevelFilter, + pub term_loglevel: LevelFilter, + pub logfile: PathBuf, +} + +#[tokio::main] +async fn main() { + let v = (|| { + if env::args().nth(1).unwrap_or(String::from("")) != *"" { + return PathBuf::from(env::args().nth(1).unwrap()); + }; + match home::home_dir() { + Some(path) => path.join(".luminainstance/"), + None => PathBuf::from(Path::new(".")), + } + })(); + + let vs = v + .canonicalize() + .unwrap_or(v.to_path_buf()) + .to_string_lossy() + .replace("\\\\?\\", "") + .to_string(); + if !v.exists() { + match fs::create_dir_all(v.clone()) { + Ok(_) => {} + Err(e) => { + eprintln!( + "Could not write necessary files! Error: {}", + e.to_string().bright_red() + ); + process::exit(1); + } + } + } + if !v.is_dir() { + eprintln!( + "Unable to load or write config! Error: {}", + format!("`{}` is not a directory.", vs).bright_red() + ); + process::exit(1); + } + + let erun: ERun = { + let sty_f = v.clone().join("./custom-styles.css"); + if (!sty_f.is_file()) || (!sty_f.exists()) { + let mut output = match File::create(sty_f.clone()) { + Ok(p) => p, + Err(a) => { + eprintln!( + "Error: Could not create blank style customisation file. The system returned: {}", + a + ); + process::exit(1); + } + }; + + match write!(output, "{}", STR_CLEAN_CUSTOMSTYLES_CSS) { + Ok(p) => p, + Err(a) => { + eprintln!( + "Error: Could not create blank style customisation file. The system returned: {}", + a + ); + process::exit(1); + } + }; + }; + // read the styf file to a string + let styf = match fs::read_to_string(sty_f.clone()) { + Ok(p) => p, + Err(a) => { + eprintln!( + "Error: Could not read custom style file. The system returned: {}", + a + ); + process::exit(1); + } + }; + + ERun { + cd: v.clone(), + customcss: styf, + session_valid: rand::thread_rng().gen_range(0..1000000), + } + }; + let logsets: LogSets = { + fn matchlogmode(o: u8) -> LevelFilter { + match o { + 0 => LevelFilter::Off, + 1 => LevelFilter::Error, + 2 => LevelFilter::Warn, + 3 => LevelFilter::Info, + 4 => LevelFilter::Debug, + 5 => LevelFilter::Trace, + _ => { + eprintln!( + "{} Could not set loglevel `{}`! Ranges are 0-5 (quiet to verbose)", + "error:".red(), + o + ); + process::exit(1); + } + } + } + let temp: Option = None; + match temp { + None => LogSets { + file_loglevel: LevelFilter::Info, + term_loglevel: LevelFilter::Warn, + logfile: erun.cd.join("./instance.log"), + }, + Some(d) => LogSets { + file_loglevel: match d.file_loglevel { + Some(l) => matchlogmode(l), + None => LevelFilter::Info, + }, + term_loglevel: match d.term_loglevel { + Some(l) => matchlogmode(l), + None => LevelFilter::Warn, + }, + logfile: match d.logfile { + Some(s) => erun.cd.join(s.as_str()), + None => erun.cd.join("./instance.log"), + }, + }, + } + }; + CombinedLogger::init(vec![ + TermLogger::new( + logsets.term_loglevel, + Config::default(), + TerminalMode::Mixed, + ColorChoice::Auto, + ), + WriteLogger::new( + logsets.file_loglevel, + Config::default(), + File::create(&logsets.logfile).unwrap(), + ), + ]) + .unwrap(); + + let config: LuminaConfig = LuminaConfig::new(erun.clone()); + + let server_p: ServerVars = ServerVars { + config: config.clone(), + }; + let server_q: Data> = Data::new(Mutex::new(server_p.clone())); + server_p.tell(format!( + "Logging to {}", + logsets + .logfile + .canonicalize() + .unwrap() + .to_string_lossy() + .replace("\\\\?\\", "") + )); + config.db_connect().initial_dbconf(); + let keydouble = config.db_custom_salt.repeat(10); + let keybytes = keydouble.as_bytes(); + if keybytes.len() < 32 { + error!( + "Error: Cookie key must be at least 32 (doubled) bytes long. \"{}\" yields only {} bytes.", + config.db_custom_salt.blue(), + format!("{}",keybytes.len()).blue() + ); + process::exit(1); + } + let secret_key: Key = Key::from(keybytes); + let main_server = match HttpServer::new(move || { + App::new() + .wrap(SessionMiddleware::new( + CookieSessionStore::default(), + secret_key.clone(), + )) + .default_service(web::to(notfound)) + .route("/", web::get().to(serve::root)) + .route("/home", web::get().to(serve::homepage)) + .route("/login", web::get().to(serve::login)) + .route("/signup", web::get().to(serve::signup)) + .route("/session/logout", web::get().to(serve::logout)) + .route("/home/", web::get().to(serve::homepage)) + .route("/login/", web::get().to(serve::login)) + .route("/signup/", web::get().to(serve::signup)) + .route("/session/logout/", web::get().to(serve::logout)) + .route("/app.js", web::get().to(serve::appjs)) + .route("/app.js.map", web::get().to(serve::appjsmap)) + .route( + "/api/fe/fetch-page", + web::post().to(api_fe::pageservresponder), + ) + .route( + "/api/fe/editor_fetch_markdownpreview", + web::post().to(api_fe::render_editor_articlepost), + ) + .route("/api/fe/update", web::get().to(api_fe::update)) + .route("/api/fe/auth/", web::post().to(api_fe::auth)) + .route("/api/fe/auth", web::post().to(api_fe::auth)) + .route("/api/fe/auth-create/", web::post().to(api_fe::newaccount)) + .route("/api/fe/auth-create", web::post().to(api_fe::newaccount)) + .route( + "/api/fe/auth-create/check-username", + web::post().to(api_fe::check_username), + ) + .route("/red-cross.svg", web::get().to(serve::red_cross_svg)) + .route("/spinner.svg", web::get().to(serve::spinner_svg)) + .route("/green-check.svg", web::get().to(serve::green_check_svg)) + .route("/logo.svg", web::get().to(serve::logo_svg)) + .route("/favicon.ico", web::get().to(serve::logo_png)) + .route("/logo.png", web::get().to(serve::logo_png)) + .service(avatar) + .service(serve_fonts) + .app_data(web::Data::clone(&server_q)) + }) + .bind((config.lumina_server_addr.clone(), config.lumina_server_port)) + { + Ok(o) => { + server_p.tell(format!( + "Running on http://{0}:{1}, which should be bound to {2}://{3}", + config.lumina_server_addr, + config.lumina_server_port, + if config.lumina_server_https { + "https" + } else { + "http" + }, + config.lumina_synchronisation_iid + )); + o + } + Err(s) => { + error!( + "Could not bind to {}:{}, error message: {}", + config.lumina_server_addr, config.lumina_server_port, s + ); + process::exit(1); + } + } + .run(); + let _ = futures::join!(api_ii::main(server_p.clone()), main_server, close()); +} + +async fn close() { + // This function is uh, pruned mostly, because it affected others. + let _ = tokio::signal::ctrl_c().await; + println!("\n\n\nBye!\n"); + process::exit(0); + // let msg = format!("Type [{}] and then [{}] to exit or use '{}' to show more available Lumina server runtime commands.", "q".blue(), "return".bright_magenta(), "help".bright_blue()).bright_yellow(); + // println!("{}", msg); + // let mut input = String::new(); + // let mut waiting = true; + // while waiting { + // input.clear(); + // let _ = std::io::stdout().flush(); + // std::io::stdin() + // .read_line(&mut input) + // .expect("Failed to read input"); + // if input == *"\r\n" { + // waiting = false; + // } + // input = input.replace(['\n', '\r'], ""); + // let split_input = input.as_str().split(' ').collect::>(); + // match split_input[0].to_lowercase().as_str() { + // "q" | "x" | "exit" => { + // println!("Bye!"); + // process::exit(0); + // } + // "au" | "adduser" => { + // if split_input.len() < 2 { + // println!("Usage: adduser "); + // } else { + // match database::users::add( + // split_input[1].to_string(), + // split_input[2].to_string(), + // split_input[3].to_string(), + // &config.clone(), + // ) { + // Ok(o) => println!( + // "{}", + // format!( + // "Added user {} with password {} and ID {}.", + // split_input[1].bright_magenta(), + // split_input[2].bright_magenta(), + // o.to_string().bright_magenta(), + // ) + // .green() + // ), + // Err(e) => println!( + // "{}", + // format!( + // "Could not add user {} with password {}: {}", + // split_input[1], + // split_input[2], + // e + // ) + // .red() + // ), + // } + // } + // } + // "h" | "help" => println!( + // "\n{}\n\t{} {}{}{} {}{}{} {}{}{}{}", + // "Lumina server runtime command line - Help\n".bright_yellow(), + // "au | adduser".white(), + // "<".red(), "username".bright_yellow().on_red(), ">".red(), + // "<".red(), "password".bright_yellow().on_red(), ">".red(), + // "<".red(), "email".bright_yellow().on_red(), ">".red(), + // format!("\n\t\tAdds a new user to the database.\n\t{}\n\t\tDisplays this help message.\n\t{}\n\t\tShut down the server.", "h | help".white(), "q | x | exit".white()).green() + // ), + // _ => println!("{}", msg), + // } + // } +} + +mod config; +mod serve; + +#[doc = r"Font file server"] +#[get("/fonts/{a:.*}")] +pub(crate) async fn serve_fonts( + req: HttpRequest, + server_vars_mutex: Data>, + session: Session, +) -> HttpResponse { + // let reqx = req.clone(); + let fnt: String = req.match_info().get("a").unwrap().parse().unwrap(); + let fonts = fonts(); + let fontbytes: &[u8] = match fnt.as_str() { + "Josefin_Sans/JosefinSans-VariableFont_wght.ttf" => fonts.josefin_sans, + "Fira_Sans/FiraSans-Regular.ttf" => fonts.fira_sans, + "Gantari/Gantari-VariableFont_wght.ttf" => fonts.gantari, + "Syne/Syne-VariableFont_wght.ttf" => fonts.syne, + _ => { + return notfound(server_vars_mutex, req, session).await; + } + }; + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + format!("/fonts/{}", fnt).magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + HttpResponse::Ok() + .append_header(("Accept-Charset", "UTF-8")) + .content_type("font/ttf") + .body(fontbytes) +} + +#[get("/user/avatar/{a:.*}")] +pub(crate) async fn avatar( + req: HttpRequest, + server_vars_mutex: Data>, + session: Session, +) -> HttpResponse { + let server_vars = ServerVars::grab(&server_vars_mutex).await; + let user: String = req.match_info().get("a").unwrap().parse().unwrap(); + + // For now unused. Will be used once users can have avatars. + let _ = (user, session); + let coninfo = req.connection_info(); + let ip = coninfo.realip_remote_addr().unwrap_or(""); + server_vars.tell(format!( + "{2}\t{:>45.47}\t\t{}", + req.path().magenta(), + ip.yellow(), + "Request/200".bright_green() + )); + let index: usize = Rng::gen_range(&mut thread_rng(), 0..=5); + let cont: String = { + let oo = &vec_string_assets_anons_svg()[index]; + + oo.clone().to_string() + }; + HttpResponse::Ok() + .append_header(("Accept-Charset", "UTF-8")) + .content_type("image/svg+xml") + .body(cont) +} diff --git a/backend-rs/src/tell.rs b/backend-rs/src/tell.rs new file mode 100644 index 0000000..7348632 --- /dev/null +++ b/backend-rs/src/tell.rs @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + */ + +//! ## Actions for gentle logging ("telling") +//! Logging doesn't need this, but for prettyness these are added as implementations on ServerVars. + +use std::time::SystemTime; + +use colored::Colorize; +use time::{format_description, OffsetDateTime}; + +use crate::{config::LuminaLogConfig, ServerVars}; + +const DATE_FORMAT_STR: &str = "[hour]:[minute]:[second]"; + +#[doc = r"A function that either prints as an [info] log, or prints as [log], depending on configuration. This because loglevel 3 is a bit too verbose, while loglevel 2 is too quiet."] +impl ServerVars { + pub(crate) fn tell(&self, rmsg: impl AsRef) { + let msg = rmsg.as_ref(); + match &self.config.logging.clone() { + None => { + let dt1: OffsetDateTime = SystemTime::now().into(); + let dt_fmt = format_description::parse(DATE_FORMAT_STR).unwrap(); + let times = dt1.format(&dt_fmt).unwrap(); + println!("{} {} {}", times, "[LOG] ".magenta(), msg); + info!("{}", msg); + } + Some(l) => { + l.clone().to_owned().tell(rmsg); + } + } + } + + pub fn format_tell(&self, rmsg: impl AsRef) -> String { + let msg = rmsg.as_ref(); + let dt1: OffsetDateTime = SystemTime::now().into(); + let dt_fmt = format_description::parse(DATE_FORMAT_STR).unwrap(); + let times = dt1.format(&dt_fmt).unwrap(); + format!("{} {} {}", times, "[LOG] ".magenta(), msg) + } +} +impl LuminaLogConfig { + fn tell(self, rmsg: impl AsRef) { + let msg = rmsg.as_ref(); + let a = self; + match a.term_loglevel { + None => { + let dt1: OffsetDateTime = SystemTime::now().into(); + let dt_fmt = format_description::parse(DATE_FORMAT_STR).unwrap(); + let times = dt1.format(&dt_fmt).unwrap(); + println!("{} {} {}", times, "[LOG] ".magenta(), msg); + info!("{}", msg); + } + Some(s) => { + // If the log level is set to erroronly or info-too, just return it as info. The only other case is really just 2, but I am funny. + if s >= 3 || s <= 1 { + info!("{}", msg); + } else { + { + let dt1: OffsetDateTime = SystemTime::now().into(); + let dt_fmt = format_description::parse(DATE_FORMAT_STR).unwrap(); + let times = dt1.format(&dt_fmt).unwrap(); + println!("{} {} {}", times, "[LOG] ".magenta(), msg); + info!("{}", msg); + } + } + } + } + } +} diff --git a/backend/gleam.toml b/backend/gleam.toml index 284c820..8911146 100644 --- a/backend/gleam.toml +++ b/backend/gleam.toml @@ -24,7 +24,7 @@ simplifile = "2.1.0" wisp = ">= 0.14.0 and < 0.17.0" mist = ">= 1.2.0 and < 2.0.0" gleamyshell = ">= 2.0.1 and < 3.0.0" -gleamy_lights = "2.2.1" +gleamy_lights = ">= 2.3.0 and < 3.0.0" birl = ">= 1.7.1 and < 2.0.0" wisp_kv_sessions = ">= 0.0.3 and < 1.0.0" gleam_http = ">= 3.7.0 and < 4.0.0" diff --git a/backend/manifest.toml b/backend/manifest.toml index b0adc55..07dee84 100644 --- a/backend/manifest.toml +++ b/backend/manifest.toml @@ -17,7 +17,7 @@ packages = [ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, { name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" }, - { name = "gleamy_lights", version = "2.2.1", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "79B5057BDB169F27CB8770FB354293D7219318217A456CB2E7A20BF3B15E2075" }, + { name = "gleamy_lights", version = "2.3.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "8A3D43BCA0D935F7CC787F4D0D1771F822B3366114C08B93CC8D00747618499A" }, { name = "gleamyshell", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleamyshell", source = "hex", outer_checksum = "9FCDB2E7C2C64CF8973049323586AEE164B72D06EBCE1AFBEA88D7B5E6B88AAE" }, { name = "gleescript", version = "1.4.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "gleam_erlang", "gleam_stdlib", "simplifile", "snag", "tom"], otp_app = "gleescript", source = "hex", outer_checksum = "8CDDD29F91064E69950A91A40061785F10275ADB70A0520075591F61A724C455" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, @@ -34,7 +34,7 @@ packages = [ { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, { name = "pog", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "pgo"], otp_app = "pog", source = "hex", outer_checksum = "00D57120936AFBF486BE357C472E483C1F0CA507FF9C3668075E87C733CA53F8" }, { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../shared" }, + { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "../shared" }, { name = "simplifile", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "BDD04F5D31D6D34E2EDFAEF0B68A6297AEC939888C3BFCE61133DE13857F6DA2" }, { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, { name = "sqlight", version = "0.9.1", build_tools = ["gleam"], requirements = ["esqlite", "gleam_stdlib"], otp_app = "sqlight", source = "hex", outer_checksum = "A495F2892627B2268CCBCC5107EDC1E1AD9547D5F4F21A5DB04CEA72B8931B00" }, @@ -53,7 +53,7 @@ gleam_erlang = { version = ">= 0.26.0 and < 1.0.0" } gleam_http = { version = ">= 3.7.0 and < 4.0.0" } gleam_json = { version = ">= 1.0.1 and < 2.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 0.40.0" } -gleamy_lights = { version = "2.2.1" } +gleamy_lights = { version = ">= 2.3.0 and < 3.0.0" } gleamyshell = { version = ">= 2.0.1 and < 3.0.0" } gleescript = { version = ">= 1.4.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index f74f04a..0000000 --- a/backend/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "packageManager": "bun@1.1.30+", - "devDependencies": { - "tailwindcss": "latest", - "autoprefixer": "latest", - "postcss": "latest", - "typescript": "^5.6.3", - "postcss-cli": "^11.0.0", - "clean-css-cli": "^5.6.3", - "bun": "latest" - } -} diff --git a/backend/src/lumina/web/routing.gleam b/backend/src/lumina/web/routing.gleam index 1a5cda7..b4a6be5 100644 --- a/backend/src/lumina/web/routing.gleam +++ b/backend/src/lumina/web/routing.gleam @@ -121,7 +121,11 @@ pub fn handle_request(req: Request, ctx: Context) -> Response { ["api", "fe", "fetch-page"] -> api_fe.pagesrverresponder(req, ctx) ["api", "fe", "editor_fetch_markdownpreview"] -> api_fe.editor_preview_markdown(req, ctx) - _ -> wisp.bad_request() + _ -> + wisp.not_found() + |> wisp.set_body(wisp.Text( + "Invalid POST request" |> string_builder.from_string, + )) } } _ -> { diff --git a/backend/src/lumina/web/routing/api_fe.gleam b/backend/src/lumina/web/routing/api_fe.gleam index fca9ca4..af0fcb8 100644 --- a/backend/src/lumina/web/routing/api_fe.gleam +++ b/backend/src/lumina/web/routing/api_fe.gleam @@ -14,6 +14,10 @@ import gleam/string import gleam/string_builder import lumina/data/context.{type Context} import lumina/shared/shared_fejsonobject +import lumina/shared/shared_fepage_com.{ + type FEPageServeRequest, type FEPageServeResponse, FEPageServeRequest, + FEPageServeResponse, +} import lumina/shared/shared_users import lumina/users import lumina/web/pages @@ -37,15 +41,15 @@ pub fn get_update(req: Request, ctx: Context) -> Response { None } } - let username = case uid { + let user = case uid { Some(id) -> { case users.fetch(ctx, id) { - Some(user) -> user.username - None -> "unset" + Some(user) -> user |> users.to_safe_user + None -> shared_users.SafeUser(id: -1, username: "unset", email: "unset") } } None -> { - "unset" + shared_users.SafeUser(id: -1, username: "unset", email: "unset") } } let clientdata = @@ -55,7 +59,11 @@ pub fn get_update(req: Request, ctx: Context) -> Response { iid: "localhost", last_sync: 0, ), - user: shared_users.SafeUser(id: -1, username: username, email: "unset"), + user: shared_users.SafeUser( + id: user.id, + username: user.username, + email: user.email, + ), ) json.object([ #( @@ -79,6 +87,7 @@ pub fn get_update(req: Request, ctx: Context) -> Response { } pub fn auth(req: wisp.Request, ctx: context.Context) { + wisp.log_info("Authentication request received.") use form <- wisp.require_form(req) case form.values { [#("password", password), #("username", username)] -> { @@ -86,14 +95,27 @@ pub fn auth(req: wisp.Request, ctx: context.Context) { case users.auth(username, password, ctx) { Ok(Some(id)) -> { // If the user is authenticated, we can store their user ID in the session. - let assert Ok(_) = - wisp_kv_sessions.set( - ctx.session_config, - req, - "uid", - id, - fn(in: Int) { json.int(in) |> json.to_string }, - ) + let handle_session_setting = fn(continue: fn() -> Response) { + case + wisp_kv_sessions.set( + ctx.session_config, + req, + "uid", + id, + fn(in: Int) { json.int(in) |> json.to_string }, + ) + { + Ok(_) -> continue() + Error(e) -> { + wisp.log_critical( + "Error in setting session: " <> string.inspect(e), + ) + wisp.internal_server_error() + } + } + } + + use <- handle_session_setting() // Then send them on string_builder.from_string("{\"Ok\": true, \"Errorvalue\": \"\"}") |> wisp.json_response(200) @@ -101,7 +123,9 @@ pub fn auth(req: wisp.Request, ctx: context.Context) { Error(e) -> case e { users.PasswordIncorrect -> { - string_builder.from_string("{\"Ok\": false}") + string_builder.from_string( + "{\"Ok\": false, \"Errorvalue\": \"No user known with that username-password combination.\"}", + ) |> wisp.json_response(401) } users.InvalidIdentifier -> { @@ -110,7 +134,9 @@ pub fn auth(req: wisp.Request, ctx: context.Context) { } users.NonexistentUser -> { wisp.log_warning("Nonexistent user in auth") - string_builder.from_string("{\"Ok\": false}") + string_builder.from_string( + "{\"Ok\": false, \"Errorvalue\": \"No user known with that username-password combination.\"}", + ) |> wisp.json_response(401) } users.Unspecified -> { @@ -137,6 +163,9 @@ pub fn auth(req: wisp.Request, ctx: context.Context) { _ -> { wisp.log_warning("Invalid form data in auth") wisp.bad_request() + |> wisp.set_body(wisp.Text( + "Invalid form data" |> string_builder.from_string, + )) } } } @@ -199,14 +228,6 @@ pub fn create_user(req: wisp.Request, ctx: context.Context) { } } -type FEPageServeRequest { - FEPageServeRequest(location: String) -} - -type FEPageServeResponse { - FEPageServeResponse(main: String, side: String, message: List(Int)) -} - pub fn pagesrverresponder(req: wisp.Request, ctx: context.Context) { let pagesrverresponseencoder = fn(response: FEPageServeResponse) { json.object([ diff --git a/build.sh b/build.sh index 8423daf..fdd9030 100755 --- a/build.sh +++ b/build.sh @@ -1,16 +1,22 @@ #!/usr/bin/env bash LOCA=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +GEN_ASSETS="$LOCA/backend/priv/generated" SECONDS=0 QUIET=false TESTS=false PACK=false BUNFLAGS="" CARGOFLAGS="" +BCARGOFLAGS="" TEST_FE_TS=false TEST_FE_GLEAM=false # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +if [[ "$*" == *"--backend=rust"* ]]; then + GEN_ASSETS="$LOCA/backend-rs/generated" +fi + if [[ "$*" == *"--quiet"* ]]; then QUIET=true fi @@ -20,9 +26,11 @@ if [[ "$*" == *"--test"* ]]; then fi if [[ "$*" == *"--pack"* ]]; then PACK=true + BCARGOFLAGS="--release" fi if [[ "$*" == *"--run-packed"* ]]; then PACK=true + BCARGOFLAGS="--release" fi if [ "$QUIET" = true ]; then echo "[quiet mode]" >&2 @@ -111,8 +119,9 @@ res_succ() { # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- res_noti 2 "Starting build process..." -rm -rf "$LOCA/backend/priv/generated/js" -mkdir -p "$LOCA/backend/priv/generated/js" +rm -rf "$GEN_ASSETS/js" +mkdir -p "$GEN_ASSETS/js" +bun install $BUNFLAGS if [[ "$*" == *"--frontend=typescript"* ]]; then noti "Building front-end (TS)..." @@ -120,8 +129,14 @@ if [[ "$*" == *"--frontend=typescript"* ]]; then cd "$LOCA/frontend-ts/" || exit 1 bun install $BUNFLAGS noti "Transpiling and copying to Lumina server..." - bun $BUNFLAGS build "$LOCA/frontend-ts/app.ts" --minify --target=browser --outdir "$LOCA/backend/priv/generated/js/" --sourcemap=linked - bun $BUNFLAGS "$LOCA/tobundle.ts" -- js-1 "$LOCA/backend/priv/generated/js/app.js" + bun $BUNFLAGS build "$LOCA/frontend-ts/app.ts" --minify --target=browser --outdir "$GEN_ASSETS/js/" --sourcemap=linked || { + errnoti "Error while building the frontend." + exit 1 + } + bun $BUNFLAGS "$LOCA/tobundle.ts" -- js-1 "$GEN_ASSETS/js/app.js" || { + errnoti "Error while bundling the frontend." + exit 1 + } else if [[ "$*" == *"--frontend=gleam"* ]]; then noti "Building front-end (Gleam)..." @@ -136,8 +151,14 @@ else fi noti "Copying to Lumina server..." echo "import { main } from \"./frontend.mjs\";main();" >"$LOCA/frontend/build/dev/javascript/frontend/app.js" - bun $BUNFLAGS build "$LOCA/frontend/build/dev/javascript/frontend/app.js" --minify --target=browser --outdir "$LOCA/backend/priv/generated/js/" --sourcemap=none - bun $BUNFLAGS "$LOCA/tobundle.ts" -- js-1 "$LOCA/backend/priv/generated/js/app.js" + bun $BUNFLAGS build "$LOCA/frontend/build/dev/javascript/frontend/app.js" --target=browser --outdir "$GEN_ASSETS/js/" --sourcemap=linked || { + errnoti "Error while bundling the frontend." + exit 1 + } + bun $BUNFLAGS "$LOCA/tobundle.ts" -- js-1 "$GEN_ASSETS/js/app.js" || { + errnoti "Error while bundling the frontend." + exit 1 + } else errnoti "Invalid or missing frontend option, expected either \"--frontend=typescript\" or \"--frontend=gleam\"." if [ "$TESTS" = false ]; then @@ -151,42 +172,71 @@ else fi # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- noti "Front-end should be done. Continuing to generated assets." -cd "$LOCA/backend/" || exit 1 +cd "$LOCA/" || exit 1 bun install $BUNFLAGS -rm -rf "$LOCA/backend/priv/generated/css/" -mkdir -p "$LOCA/backend/priv/generated/css/" +rm -rf "$GEN_ASSETS/css/" +mkdir -p "$GEN_ASSETS/css/" noti "Generating CSS... (TailwindCSS)" -bun x postcss -o "$LOCA/backend/priv/generated/css/main.css" "$LOCA/backend/assets/styles/main.pcss" -u autoprefixer -u tailwindcss -bun "$LOCA/tobundle.ts" -- css-1 "$LOCA/backend/priv/generated/css/main.css" +bun x postcss -o "$GEN_ASSETS/css/main.css" "$LOCA/backend/assets/styles/main.pcss" -u autoprefixer -u tailwindcss +bun "$LOCA/tobundle.ts" -- css-1 "$GEN_ASSETS/css/main.css" noti "Minifying CSS and copying to Lumina server..." -bun x cleancss -O1 specialComments:all --inline none "$LOCA/backend/priv/generated/css/main.css" -o "$LOCA/backend/priv/generated/css/main.min.css" +bun x cleancss -O1 specialComments:all --inline none "$GEN_ASSETS/css/main.css" -o "$GEN_ASSETS/css/main.min.css" # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -noti "Compiling Rust libraries..." -cd "$LOCA/rsffi/" || exit 1 -if cargo build --release; then - success "\t--> Rust libraries build success." -else - errnoti "\t--> Rust libraries compilation ran into an error." - exit 1 -fi -rm -rf "$LOCA/backend/priv/generated/libs/" -mkdir -p "$LOCA/backend/priv/generated/libs/" -noti "Copying Rust libraries to Lumina server..." -cp "$LOCA/rsffi/target/release/librsffi.so" "$LOCA/backend/priv/generated/libs/rsffi.so" -# --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -noti "Starting on Lumina server compilation" -cd "$LOCA/backend/" || exit 1 -if gleam build --target erlang; then - success "\t--> Back-end build success." +if [[ "$*" == *"--backend=rust"* ]]; then + noti "Building Rust backend..." + cd "$LOCA/backend-rs/" || exit 1 + if cargo build $BCARGOFLAGS; then + success "\t--> Rust backend build success." + else + errnoti "\t--> Rust backend compilation ran into an error." + exit 1 + fi else - errnoti "\t--> Compilation ran into an error!" - exit 1 + if [[ "$*" == *"--backend=gleam"* ]]; then + noti "Compiling Rust libraries..." + cd "$LOCA/rsffi/" || exit 1 + if cargo build --release; then + success "\t--> Rust libraries build success." + else + errnoti "\t--> Rust libraries compilation ran into an error." + exit 1 + fi + rm -rf "$GEN_ASSETS/libs/" + mkdir -p "$GEN_ASSETS/libs/" + noti "Copying Rust libraries to Lumina server..." + cp "$LOCA/rsffi/target/release/librsffi.so" "$GEN_ASSETS/libs/rsffi.so" + # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + noti "Starting on Lumina server compilation" + cd "$LOCA/backend/" || exit 1 + if gleam build --target erlang; then + success "\t--> Back-end build success." + else + errnoti "\t--> Compilation ran into an error!" + exit 1 + fi + else + errnoti "Invalid or missing backend option, expected either \"--backend=gleam\" or \"--backend=rust\"." + if [ "$TESTS" = false ]; then + exit 1 + else + noti "This option is not needed for tests, running both backend tests without it." + fi + fi fi build_duration=$SECONDS res_noti 1 "Build completed, took $((build_duration / 60)) minutes and $((build_duration % 60)) seconds." if [[ "$*" == *"--run"* ]]; then noti "'--run' detected. Running Lumina directly!" - gleam run -- start + if [[ "$*" == *"--backend=rust"* ]]; then + if [ "$PACK" = true ]; then + "$LOCA/backend-rs/target/release/lumina-server" || exit 1 + else + "$LOCA/backend-rs/target/debug/lumina-server" || exit 1 + fi + else + cd "$LOCA/backend/" || exit 1 + gleam run -- start + fi else if [[ "$*" == *"--pack"* ]]; then noti "'--pack' detected. Packaging for deployment." diff --git a/bun.lockb b/bun.lockb old mode 100644 new mode 100755 index 8949e5d..94e8ec8 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/frontend/TODO.md b/frontend/TODO.md new file mode 100644 index 0000000..b515aaa --- /dev/null +++ b/frontend/TODO.md @@ -0,0 +1,25 @@ +# FE-gleam To-do list + +- [x] Client-server communication + - [x] Periodic updates (FEJSON) + - [x] Fetching FEJSON from server + - [x] Applying auto-actions to FEJSON + - [ ] Verify compile moment thru FEJSON + - [ ] User action reporting + - [ ] New post creation + - [ ] Log out +- [ ] Sub page loading + - [x] Loading sub pages (main, side) + - [ ] Migrating to Handlebars parsing on the client side, instead of server-leveraged strings. + - [ ] Loading special sub pages (editor, settings) + - [ ] Sub page logic +- [ ] Editor + - [ ] Editor UI + - [ ] Editor tab switching + - [ ] Editor Markdown parsing +- [x] Sign in + - [x] Sign in logic +- [x] Sign up + - [x] Sign up logic + - [x] Username check +- [ ] Mobile navigation enhancements diff --git a/frontend/gleam.toml b/frontend/gleam.toml index 0d22315..68ba92e 100644 --- a/frontend/gleam.toml +++ b/frontend/gleam.toml @@ -16,12 +16,14 @@ runtime = "bun" [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" plinth = ">= 0.5.0 and < 1.0.0" -gleamy_lights = ">= 2.2.0 and < 3.0.0" +gleamy_lights = ">= 2.3.0 and < 3.0.0" shared = { path = "../shared/" } gleam_http = ">= 3.7.0 and < 4.0.0" gleam_fetch = ">= 1.0.1 and < 2.0.0" +lustre = ">= 4.5.0 and < 5.0.0" gleam_javascript = ">= 0.13.0 and < 1.0.0" -gleam_json = ">= 1.0.1 and < 2.0.0" +gleam_json = ">= 2.0.0 and < 3.0.0" +pprint = ">= 1.0.4 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/frontend/manifest.toml b/frontend/manifest.toml index fd83fe7..130ba5f 100644 --- a/frontend/manifest.toml +++ b/frontend/manifest.toml @@ -4,26 +4,32 @@ packages = [ { name = "conversation", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "908B46F60444442785A495197D482558AD8B849C3714A38FAA1940358CC8CCCD" }, { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, + { name = "glam", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "66EC3BCD632E51EED029678F8DF419659C1E57B1A93D874C5131FE220DFAD2B2" }, { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, - { name = "gleam_fetch", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "4AE60B21A9A664137A79B1BEB93F751CB27F1DDED4086CA00C0260F5FFACBD80" }, - { name = "gleam_http", version = "3.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "EA66440C2269F7CED0F6845E5BD0DB68095775D627FA709A841CA78A398D6D56" }, + { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" }, + { name = "gleam_fetch", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2FCFC0A85A6D5A83076889EEDD02D6779C02FEF6FDE0263C4D3B46D0785EAB7A" }, + { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" }, { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_stdlib", version = "0.43.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "69EF22E78FDCA9097CBE7DF91C05B2A8B5436826D9F66680D879182C0860A747" }, - { name = "gleamy_lights", version = "2.2.1", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "79B5057BDB169F27CB8770FB354293D7219318217A456CB2E7A20BF3B15E2075" }, + { name = "gleam_json", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "093214EB186A88D301795A94F0A8128C2E24CF1423997ED31A6C6CC67FC3E1A1" }, + { name = "gleam_otp", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "FA0EB761339749B4E82D63016C6A18C4E6662DA05BAB6F1346F9AF2E679E301A" }, + { name = "gleam_stdlib", version = "0.51.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "14AFA8D3DDD7045203D422715DBB822D1725992A31DF35A08D97389014B74B68" }, + { name = "gleamy_lights", version = "2.3.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "8A3D43BCA0D935F7CC787F4D0D1771F822B3366114C08B93CC8D00747618499A" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, - { name = "plinth", version = "0.5.1", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "076D792629D0EF917FC47AD054A9AA35F4B33729DAC06726717B1A60BFBE8E11" }, - { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../shared" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, + { name = "lustre", version = "4.6.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "BDF833368F6C8F152F948D5B6A79866E9881CB80CB66C0685B3327E7DCBFB12F" }, + { name = "plinth", version = "0.5.6", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "7A9BEAB98F4BC6859803D5ABCD0F3B4C3578A747F12BF32F9E234F8325F5F0E0" }, + { name = "pprint", version = "1.0.4", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "C310A98BDC0995644847C3C8702DE19656D6BCD638B2A8A358B97824379ECAA1" }, + { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "../shared" }, ] [requirements] gleam_fetch = { version = ">= 1.0.1 and < 2.0.0" } gleam_http = { version = ">= 3.7.0 and < 4.0.0" } gleam_javascript = { version = ">= 0.13.0 and < 1.0.0" } -gleam_json = { version = ">= 1.0.1 and < 2.0.0" } +gleam_json = { version = ">= 2.0.0 and < 3.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } -gleamy_lights = { version = ">= 2.2.0 and < 3.0.0" } +gleamy_lights = { version = ">= 2.3.0 and < 3.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 4.5.0 and < 5.0.0" } plinth = { version = ">= 0.5.0 and < 1.0.0" } +pprint = { version = ">= 1.0.4 and < 2.0.0" } shared = { path = "../shared/" } diff --git a/frontend/src/elementactions_ffi.ts b/frontend/src/elementactions_ffi.ts new file mode 100644 index 0000000..3afc0c1 --- /dev/null +++ b/frontend/src/elementactions_ffi.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + * + */ + +/** + * TypeScript FFI module providing DOM manipulation and window utilities for the Gleam frontend. + * This module serves as a bridge between Gleam and browser-specific JavaScript functionality. + */ + +export function disableElement(element: HTMLElement) { + element.setAttribute("disabled", ""); +} +export function elementHidden(element: HTMLElement): boolean { + return element.classList.contains("hidden"); +} +export function enableElement(element: HTMLElement) { + element.removeAttribute("disabled"); +} +export function unHideElement(element: HTMLElement) { + if (element.classList.contains("hidden")) { + element.classList.remove("hidden"); + } +} +export function hideElement(element: HTMLElement) { + if (!element.classList.contains("hidden")) { + element.classList.add("hidden"); + } +} +export function getWindowHost() { + return window.location.host; +} +export function goWindowBack() { + return window.history.back(); +} +export function setWindowLocationHash(to: string) { + return (window.location.hash = to); +} +export function getWindowLocationHash() { + return window.location.hash; +} diff --git a/frontend/src/fejson_ffi.ts b/frontend/src/fejson_ffi.ts new file mode 100644 index 0000000..310b486 --- /dev/null +++ b/frontend/src/fejson_ffi.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman + * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + * + */ + +export function setJsonObj(jsonObj: { + instance_iid: string; + instance_lastsync: number; + pulled: number; + user_id: number; + user_username: string; + user_email: string; +}) { + window.fejson = { + instance: { + iid: jsonObj.instance_iid, + lastsync: jsonObj.instance_lastsync, + }, + pulled: jsonObj.pulled, + user: { + id: jsonObj.user_id, + username: jsonObj.user_username, + email: jsonObj.user_email, + }, + }; +} + +export function getJsonObj() { + if (!window.fejson) { + return { + instance_iid: -1, + instance_lastsync: -1, + pulled: 0, + user_id: -1, + user_username: "unset", + user_email: "unset", + }; + } + return { + instance_iid: window.fejson.instance.iid, + instance_lastsync: window.fejson.instance.lastsync, + pulled: window.fejson.pulled, + user_id: window.fejson.user.id, + user_username: window.fejson.user.username, + user_email: window.fejson.user.email, + }; +} + +export function dateToTimestamp(): number { + return Date.now(); +} + +const MAX_QUEUE_SIZE = 1000; + +export function queueFejsonFunction(func: () => null) { + if (!window.fejsonqueue) { + window.fejsonqueue = []; + } + if (window.fejsonqueue.length >= MAX_QUEUE_SIZE) { + console.warn("Function queue size limit reached"); + return; + } + window.fejsonqueue.push(func); +} + +export function getQueuedFejsonFunctions(): Array<() => null> { + return window.fejsonqueue || []; +} + +interface fejsonObject { + pulled: number; + instance: { + iid: string; + lastsync: number; + }; + user: { username: string; id: number; email: string }; +} +declare global { + export interface Window { + mobileMenuToggle: () => null; + on_mobile_swipe_down: Array<(evt: TouchEvent) => null>; + on_mobile_swipe_up: Array<(evt: TouchEvent) => null>; + on_mobile_swipe_right: Array<(evt: TouchEvent) => null>; + on_mobile_swipe_left: Array<(evt: TouchEvent) => null>; + fejsonqueue: Array<() => null>; + fejson: fejsonObject; + } +} diff --git a/frontend/src/ffi.mjs b/frontend/src/ffi.mjs deleted file mode 100644 index 97e38c2..0000000 --- a/frontend/src/ffi.mjs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman -// Licensed under the BSD 3-Clause License. See the LICENSE file for more info. - -export function setJsonObj(jsonObj) { - window.fejson = { - instance: { - iid: jsonObj.instance_iid, - lastsync: jsonObj.instance_lastsync, - }, - pulled: jsonObj.pulled, - user: { - id: jsonObj.user_id, - username: jsonObj.user_username, - email: jsonObj.user_email, - }, - }; -} - -export function getJsonObj() { - if (!window.fejson) { - return { - instance_iid: -1, - instance_lastsync: -1, - pulled: -1, - user_id: -1, - user_username: "unset", - user_email: "unset", - }; - } - return { - instance_iid: window.fejson.instance.iid, - instance_lastsync: window.fejson.instance.lastsync, - pulled: window.fejson.pulled, - user_id: window.fejson.user.id, - user_username: window.fejson.user.username, - user_email: window.fejson.user.email, - }; -} - -export function dateToTimestamp() { - return Number.parseInt(Date.now()); -} diff --git a/frontend/src/frontend.gleam b/frontend/src/frontend.gleam index 65a1cf2..bb11f70 100644 --- a/frontend/src/frontend.gleam +++ b/frontend/src/frontend.gleam @@ -1,6 +1,7 @@ // Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman // Licensed under the BSD 3-Clause License. See the LICENSE file for more info. +import frontend/other/element_actions import frontend/other/fejson import gleam/bool import gleam/dynamic @@ -8,14 +9,18 @@ import gleam/fetch.{type FetchError} import gleam/http/request import gleam/http/response.{type Response} import gleam/int +import gleam/javascript/array.{type Array} import gleam/javascript/promise import gleam/json +import gleam/list import gleam/string -import gleamy_lights/helper as web_io +import gleamy_lights/console import gleamy_lights/premixed import gleamy_lights/premixed/gleam_colours import lumina/shared/shared_fejsonobject import lumina/shared/shared_users +import plinth/browser/document +import plinth/browser/element import plinth/browser/window import plinth/javascript/global @@ -30,23 +35,154 @@ pub fn main() { window.add_event_listener("load", fn(_) { let path = window.pathname() case path { - "/" -> site.index_render() - "/home" -> site.home_render() - "/login" -> login.render() - "/signup" -> signup.render() - _ -> web_io.println("404: Page not found") + "/" | "" -> site.index_render() + "/home" | "/home/" -> site.home_render() + "/login" | "/login/" -> login.render() + "/signup" | "/signup/" -> signup.render() + _ -> console.error("404: Page not found") } }) - web_io.println( + console.log( "Hello from the " <> gleam_colours.text_faff_pink("Gleam") <> " frontend rewrite!", ) global.set_interval(4000, update_fejson) + global.set_timeout(0, update_fejson) + // fejson.register_fejson_function(fn() { + // console.log( + // "FEJson instance info: " + // <> premixed.text_lightblue(string.inspect(fejson.get().instance)) + // <> ", last sync: " + // <> premixed.text_lightblue(string.inspect(fejson.get().pulled)) + // <> ".", + // ) + // }) + fejson.register_fejson_function(fn() { + let src = "/user/avatar/" <> int.to_string(fejson.get().user.id) + document.query_selector_all(".ownuseravatarsrc") + |> array.to_list + |> list.each(fn(a) { + use <- + bool.guard( + when: { fejson.get().user.id == -1 }, + return: Nil, + otherwise: _, + ) + + use <- + bool.guard( + when: { a |> element.get_attribute("src") == Ok(src) }, + return: Nil, + otherwise: _, + ) + a + |> element.set_attribute("src", src) + }) + }) + fejson.register_fejson_function(fn() { + let href = "/user/" <> fejson.get().user.username <> "/me" + document.query_selector_all(".ownuserprofilelink") + |> array.to_list + |> list.each(fn(a) { + use <- + bool.guard( + when: { fejson.get().user.id == -1 }, + return: Nil, + otherwise: _, + ) + + use <- + bool.guard( + when: { a |> element.get_attribute("href") == Ok(href) }, + return: Nil, + otherwise: _, + ) + a + |> element.set_attribute("href", href) + }) + }) + + fejson.register_fejson_function(fn() { + let username = fejson.get().user.username + document.query_selector_all(".ownusernametext") + |> array.to_list + |> list.each(fn(a) { + use <- + bool.guard( + when: { fejson.get().user.id == -1 }, + return: Nil, + otherwise: _, + ) + + use <- + bool.guard( + when: { a |> element.inner_text() == username }, + return: Nil, + otherwise: _, + ) + a |> element.set_inner_text(username) + }) + }) + + fejson.register_fejson_function(fn() { + let display_name = fejson.get().user.username + document.query_selector_all(".settodisplayname") + |> array.to_list + |> list.each(fn(a) { + use <- + bool.guard( + when: { fejson.get().user.id == -1 }, + return: Nil, + otherwise: _, + ) + use <- + bool.guard( + when: { a |> element.inner_text() == display_name }, + return: Nil, + otherwise: _, + ) + + console.warn("Display names not implemented yet, using username instead.") + a |> element.set_inner_text(display_name) + }) + }) + global.set_timeout(80, fn() { + global.set_interval(80, fn() { run_fejson_functions() }) + }) + document.add_event_listener("DOMContentLoaded", fn(_) { + mobile_menu_toggle() + let assert Ok(button) = document.get_element_by_id("btn-mobile-menu") + button + |> element.add_event_listener("click", fn(_) { mobile_menu_toggle() }) + Nil + }) +} + +fn mobile_menu_toggle() { + console.info("Toggling mobile menu") + let assert Ok(mobile_menu) = document.get_element_by_id("mobile-menu") + let assert Ok(mobile_menu_button_open) = + document.get_element_by_id("btn-mobile-menu-open") + let assert Ok(mobile_menu_button_close) = + document.get_element_by_id("btn-mobile-menu-close") + case mobile_menu |> element_actions.element_hidden { + True -> { + element_actions.show_element(mobile_menu) + element_actions.hide_element(mobile_menu_button_open) + element_actions.show_element(mobile_menu_button_close) + } + False -> { + element_actions.hide_element(mobile_menu) + element_actions.show_element(mobile_menu_button_open) + element_actions.hide_element(mobile_menu_button_close) + } + } } fn update_fejson() { let origi = fejson.get() + let first_pull = origi.pulled == 0 use <- bool.guard( when: { { fejson.timestamp() - origi.pulled } |> int.absolute_value @@ -71,7 +207,6 @@ fn update_fejson() { |> promise.await(fn(a: Result(Response(String), FetchError)) { case a { Ok(b) -> { - // {"instance":{"iid":"localhost","last_sync":0},"user":{"id":-1,"username":"unset"}} let now = fn(_) { fejson.timestamp() |> Ok() } case json.decode( @@ -101,7 +236,7 @@ fn update_fejson() { { Ok(c) -> then(c) Error(e) -> - web_io.println( + console.warn( premixed.text_lightblue("FEJson fetch ") <> premixed.text_error_red(" decoding failed") <> ", error:\n" @@ -113,7 +248,7 @@ fn update_fejson() { } } Error(e) -> - web_io.println( + console.error( premixed.text_lightblue("FEJson fetch ") <> premixed.text_error_red(" fetch failed") <> ", error:\n" @@ -125,4 +260,25 @@ fn update_fejson() { } use data <- f() fejson.set(data) + // run_fejson_functions() + // instead: + case first_pull { + True -> run_fejson_functions() + False -> Nil + } +} + +/// FE json usually updates every 30 seconds, that means some stuff might change. These functions are ran periodically as well, to keep the frontend in sync with the backend. +/// They'll be fetched from the window object using FFI. Then ran here. +fn run_fejson_functions() { + use <- + bool.guard(when: { fejson.get().pulled == 0 }, return: Nil, otherwise: _) + fetch_fejson_functions() + |> array.to_list + |> list.each(fn(f) { f() }) + + Nil } + +@external(javascript, "./fejson_ffi.ts", "getQueuedFejsonFunctions") +fn fetch_fejson_functions() -> Array(fn() -> Nil) diff --git a/frontend/src/frontend/other/element_actions.gleam b/frontend/src/frontend/other/element_actions.gleam new file mode 100644 index 0000000..9c688aa --- /dev/null +++ b/frontend/src/frontend/other/element_actions.gleam @@ -0,0 +1,58 @@ +// Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman +// Licensed under the BSD 3-Clause License. See the LICENSE file for more info. +import gleam/string +import plinth/browser/element.{type Element} +import plinth/browser/window +import gleam/http/request.{type Request} +import gleam/http + +@external(javascript, "../../elementactions_ffi.ts", "disableElement") +pub fn disable_element(a: Element) -> nil + +@external(javascript, "../../elementactions_ffi.ts", "enableElement") +pub fn enable_element(a: Element) -> nil + +@external(javascript, "../../elementactions_ffi.ts", "hideElement") +pub fn hide_element(a: Element) -> nil + +@external(javascript, "../../elementactions_ffi.ts", "unHideElement") +pub fn show_element(a: Element) -> nil + +@external(javascript, "../../elementactions_ffi.ts", "elementHidden") +pub fn element_hidden(a: Element) -> bool + +@external(javascript, "../../elementactions_ffi.ts", "getWindowHost") +pub fn get_window_host() -> String + +@external(javascript, "../../elementactions_ffi.ts", "goWindowBack") +pub fn go_back() -> nil + +@external(javascript, "../../elementactions_ffi.ts", "setWindowLocationHash") +pub fn set_window_location_hash(new: String) -> nil + +@external(javascript, "../../elementactions_ffi.ts", "getWindowLocationHash") +fn int_get_window_location_hash() -> String + +pub fn get_window_location_hash() -> String { + // Remove the leading '#' from the hash if it exists + + let s = int_get_window_location_hash() + case string.starts_with(s, "#") { + True -> s |> string.drop_start(1) + False -> s + } +} + +pub fn phone_home() -> Request(String) { + request.new() + |> request.set_scheme({ + let origin = window.origin() + case origin { + "http://" <> _ -> http.Http + "https://" <> _ -> http.Https + _ -> http.Https + } + }) + + |> request.set_host(get_window_host()) +} diff --git a/frontend/src/frontend/other/fejson.gleam b/frontend/src/frontend/other/fejson.gleam index bf074c1..6b15381 100644 --- a/frontend/src/frontend/other/fejson.gleam +++ b/frontend/src/frontend/other/fejson.gleam @@ -45,11 +45,14 @@ pub fn set(obj: FEJsonObj) -> nil { )) } -@external(javascript, "../../ffi.mjs", "getJsonObj") +@external(javascript, "../../fejson_ffi.ts", "getJsonObj") fn get_flat() -> FEJsonObjFlat -@external(javascript, "../../ffi.mjs", "setJsonObj") +@external(javascript, "../../fejson_ffi.ts", "setJsonObj") fn set_flat(obj: FEJsonObjFlat) -> nil -@external(javascript, "../../ffi.mjs", "dateToTimestamp") +@external(javascript, "../../fejson_ffi.ts", "dateToTimestamp") pub fn timestamp() -> Int + +@external(javascript, "../../fejson_ffi.ts", "queueFejsonFunction") +pub fn register_fejson_function(a: fn() -> nil) -> nil diff --git a/frontend/src/frontend/other/formdata.gleam b/frontend/src/frontend/other/formdata.gleam new file mode 100644 index 0000000..8da2c1b --- /dev/null +++ b/frontend/src/frontend/other/formdata.gleam @@ -0,0 +1,63 @@ +// Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman +// Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + +import gleam/http/request.{type Request} +import gleam/int +import gleam/list + +/// Encode a list of key-value pairs into a multipart form data string. +/// +/// This function replaces the body and content-type header of a request with the encoded form data. +pub fn encode( + req: Request(String), + data: List(#(String, String)), +) -> Request(String) { + let boundary = + "---------------------------" + <> int.to_string(int.random(1_000_000_000_000_000)) + req + |> request.set_body( + encoder( + data, + // |> dict.to_list + boundary, + "--" <> boundary, + ) + <> "--\r\n", + ) + |> request.prepend_header( + "content-type", + "multipart/form-data; boundary=" <> boundary, + ) +} + +fn encoder( + rest: List(#(String, String)), + boundary: String, + complete: String, +) -> String { + case list.first(rest) { + Error(_) -> complete + Ok(#(key, value)) -> { + case list.rest(rest) { + Ok(new_rest) -> + encoder( + new_rest, + boundary, + complete + <> "\r\n" + <> "Content-Disposition: form-data; name=\"" + <> key + <> "\"" + <> "\r\n" + <> "\r\n" + <> value + <> "\r\n" + <> "--" + <> boundary, + ) + Error(_) -> complete + } + } + } +} diff --git a/frontend/src/frontend/page/login.gleam b/frontend/src/frontend/page/login.gleam index c77d691..7f56b3a 100644 --- a/frontend/src/frontend/page/login.gleam +++ b/frontend/src/frontend/page/login.gleam @@ -1,23 +1,223 @@ // Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman // Licensed under the BSD 3-Clause License. See the LICENSE file for more info. -import gleamy_lights/helper as web_io +import frontend/other/element_actions +import gleam/dynamic.{field} +import gleam/fetch +import gleam/http.{Post} +import gleam/http/request +import gleam/javascript/promise +import gleam/json +import gleam/result +import gleam/string +import gleamy_lights/console import gleamy_lights/premixed import plinth/browser/document import plinth/browser/element +import plinth/browser/window +import plinth/javascript/global +import plinth/javascript/storage +import pprint // import plinth/browser/event.{type Event} pub fn render() { - web_io.println( + console.log( "Detected you are on the " <> premixed.text_lime("login page") <> ".", ) let assert Ok(submitbutton) = document.get_element_by_id("submitbutton") - element.add_event_listener(submitbutton, "click", try_login) - // Just to show we now can use the element. - element.set_attribute(submitbutton, "data-identified-as", "Login") + element.add_event_listener(submitbutton, "click", fn(_) { + try_login(submitbutton) + Nil + }) + let assert Ok(local_storage) = storage.local() + case storage.get_item(local_storage, "AutologinUsername") { + Ok(username) -> { + case storage.get_item(local_storage, "AutologinPassword") { + Ok(password) -> { + let assert Ok(d) = document.get_element_by_id("Aaa1") + d + |> element.set_inner_text("Logging in automatically...") + authentication_request(username, password, True) + Nil + } + _ -> Nil + } + } + _ -> Nil + } + Nil } -fn try_login(_a) { - web_io.println("Trying to login...") +fn try_login(submitbutton: element.Element) { + console.log("Trying authentication...") + submitbutton + |> element.set_inner_html( + "
", + ) + submitbutton |> element_actions.disable_element + global.set_timeout(9600, fn() { + submitbutton |> element.set_inner_text("Retry") + submitbutton |> element_actions.enable_element + }) + { + let assert Ok(d) = document.get_element_by_id("Aaa1") + d + } + |> element.set_inner_text("Checking credentials...") + + // timeout to allow spinner to show up + global.set_timeout(500, fn() { + let username = { + let assert Ok(d) = document.get_element_by_id("username") + let assert Ok(v) = d |> element.value + v + } + let password = { + let assert Ok(d) = document.get_element_by_id("password") + let assert Ok(v) = d |> element.value + v + } + authentication_request(username, password, False) + }) +} + +fn authentication_request( + username: String, + password: String, + is_autologin: Bool, +) { + let req = + element_actions.phone_home() + |> request.set_method(Post) + |> request.set_path("/api/fe/auth/") + |> request.set_body( + json.object([ + #("username", json.string(username)), + #("password", json.string(password)), + ]) + |> json.to_string, + ) + |> request.set_header("Content-Type", "application/json") + + // |> formdata.encode([#("username", username), #("password", password)]) + // |> request.prepend_header("accept", "application/vnd.hmrc.1.0+json") + + fetch.send(req) + |> promise.try_await(fetch.read_json_body) + |> promise.await(fn(resp) { + let assert Ok(resp) = resp + // We don't care about the status code, we just want to know if the request was successful. + // let assert 200 = resp.status + // let assert Ok("application/json; charset=utf-8") = + // response.get_header(resp, "content-type") + case + resp.body + |> dynamic.decode2( + AuthResponse, + field("Ok", of: dynamic.bool), + field("Errorvalue", of: dynamic.string), + ) + { + Error(e) -> { + console.error( + "Error decoding server auth response: " + <> string.inspect(e) + <> "\n\nGot: " + <> pprint.format(resp.body), + ) + panic + } + Ok(authorisation_response) -> { + continue_after_login(authorisation_response, is_autologin) + } + } + promise.resolve(Ok(Nil)) + }) +} + +/// This function is called after the user has send a login request. It checks if the login was successful and continues the user to the next page. +/// If the login was not successful, it will show an error message. +/// If the user checked the "Remember me" checkbox, it will save the login data in the local storage. +fn continue_after_login( + authorisation_response: AuthResponse, + is_autologin: Bool, +) { + console.log("Login answer was received, let's unpack it.") + case authorisation_response { + AuthResponse(True, _) -> { + console.info("Spoiler alert: Login is succesful.") + let timeout = case is_autologin { + True -> { + 0 + } + False -> { + let assert Ok(d) = document.get_element_by_id("Aaa1") + d + |> element.set_inner_text( + "Login successful, you will be forwarded now.", + ) + 2000 + } + } + + case + { + let assert Ok(autologincheckbox) = + document.get_element_by_id("autologin") + autologincheckbox |> element.get_checked + } + { + True -> { + let username = { + let assert Ok(d) = document.get_element_by_id("username") + let assert Ok(v) = d |> element.value + v + } + let password = { + let assert Ok(d) = document.get_element_by_id("password") + let assert Ok(v) = d |> element.value + v + } + let assert Ok(storage) = storage.local() + let assert Ok(_) = + [ + storage.set_item(storage, "AutologinUsername", username), + storage.set_item(storage, "AutologinPassword", password), + ] + |> result.all() + Nil + } + False -> { + Nil + } + } + global.set_timeout(timeout, fn() { + window.set_location( + window.self(), + "/home/" + <> { + case window.get_hash() { + // const loginPageList = ["home", "notifications", "test"]; + Ok("home") -> "#home" + Ok("notifications") -> "#notifications" + Ok("test") -> "#test" + _ -> "" + } + }, + ) + }) + } + AuthResponse(False, error) -> { + let assert Ok(d) = document.get_element_by_id("Aaa1") + d |> element.set_inner_text("Login failed: " <> error) + let assert Ok(submitbutton) = document.get_element_by_id("submitbutton") + submitbutton |> element.set_inner_text("Retry") + submitbutton |> element_actions.enable_element + } + } +} + +type AuthResponse { + AuthResponse(ok: Bool, error_value: String) } diff --git a/frontend/src/frontend/page/signup.gleam b/frontend/src/frontend/page/signup.gleam index 1f167ea..5119357 100644 --- a/frontend/src/frontend/page/signup.gleam +++ b/frontend/src/frontend/page/signup.gleam @@ -1,8 +1,245 @@ // Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman // Licensed under the BSD 3-Clause License. See the LICENSE file for more info. -import gleamy_lights/helper as web_io +import frontend/other/element_actions +import gleam/dynamic.{field} +import gleam/dynamic/decode +import gleam/fetch +import gleam/http.{Post} +import gleam/http/request +import gleam/javascript/promise +import gleam/json +import gleam/option.{type Option, None, Some} +import gleam/string +import gleamy_lights/console +import gleamy_lights/premixed +import plinth/browser/document +import plinth/browser/element +import plinth/browser/window +import plinth/javascript/global -pub fn render() { - web_io.println("Login page") +pub fn render() -> Nil { + console.log( + "Detected you are on the " <> premixed.text_lilac("signup page") <> ".", + ) + let assert Ok(usernamebox) = document.get_element_by_id("username") + usernamebox + |> element.add_event_listener("change", fn(_) { checkusername(usernamebox) }) + + let assert Ok(passwordbox) = document.get_element_by_id("password") + passwordbox + |> element.add_event_listener("change", fn(_) { checkpassword(passwordbox) }) + let assert Ok(submitbutton) = document.get_element_by_id("submitbutton") + element.add_event_listener(submitbutton, "click", fn(_) { + try_signup(submitbutton) + Nil + }) + + Nil +} + +fn checkusername(usernamebox) -> Nil { + let assert Ok(entered_username) = usernamebox |> element.value() + console.log( + "Checking if the username " <> entered_username <> " is available...", + ) + element_actions.phone_home() + |> request.set_method(Post) + |> request.set_path("/api/fe/auth-create/check-username") + |> request.set_body( + json.object([#("u", json.string(entered_username))]) + |> json.to_string, + ) + |> request.set_header("Content-Type", "application/json") + |> fetch.send() + |> promise.try_await(fetch.read_json_body) + |> promise.await(fn(resp) { + let assert Ok(resp) = resp + let assert 200 = resp.status + let check_response = case + resp.body + |> decode.run(username_check_response_decoder()) + { + Ok(v) -> v + Error(e) -> { + console.error( + { + "Error decoding the response: " + <> string.inspect(e) |> premixed.text_error_red + } + <> "\n\n" + <> string.inspect(resp.body), + ) + UsernameCheckResponse(False, None) + } + } + case check_response { + UsernameCheckResponse(True, _) -> { + let assert Ok(user_name_label) = + document.get_element_by_id("usernameLabel") + element.set_inner_html( + user_name_label, + "Username  ⬅ Username is available! ", + ) + } + UsernameCheckResponse(False, Some(why)) -> { + let assert Ok(user_name_label) = + document.get_element_by_id("usernameLabel") + element.set_inner_html(user_name_label, { + case why { + "InvalidChars" -> + "Username  ⬅ There are characters in this username that are not allowed! " + "TooShort" -> + "Username  ⬅ That username is too short! " + "userExists" -> + "Username  ⬅ Someone already claimed that username! \"X\"" + _ -> + "Username  ⬅ Username is not available! \"X\"" + } + }) + } + _ -> { + Nil + } + } + promise.resolve(Ok(Nil)) + }) + Nil +} + +type UsernameCheckResponse { + UsernameCheckResponse(ok: Bool, why: Option(String)) +} + +fn username_check_response_decoder() -> decode.Decoder(UsernameCheckResponse) { + use ok <- decode.field("Ok", decode.bool) + use why <- decode.optional_field("Why", None, decode.optional(decode.string)) + decode.success(UsernameCheckResponse(ok:, why:)) +} + +fn checkpassword(passwordbox) -> Nil { + let assert Ok(entered_username) = passwordbox |> element.value() + // This is not yet implemented in the backend + console.log( + "Checking if the password " <> entered_username <> " is available...", + ) + console.log(premixed.text_error_red( + "Password check feature is not yet implemented. Also see for this.", + )) + Nil +} + +fn try_signup(submitbutton: element.Element) { + console.log("Trying registration...") + submitbutton + |> element.set_inner_html( + "
", + ) + submitbutton |> element_actions.disable_element + global.set_timeout(9600, fn() { + submitbutton |> element.set_inner_text("Retry") + submitbutton |> element_actions.enable_element + }) + { + let assert Ok(d) = document.get_element_by_id("Aaa1") + d + } + |> element.set_inner_text("Checking credentials...") + + // timeout to allow spinner to show up + global.set_timeout(500, fn() { + let username = { + let assert Ok(d) = document.get_element_by_id("username") + let assert Ok(v) = d |> element.value + v + } + let password = { + let assert Ok(d) = document.get_element_by_id("password") + let assert Ok(v) = d |> element.value + v + } + let email = { + let assert Ok(d) = document.get_element_by_id("email") + let assert Ok(v) = d |> element.value + v + } + registration_request(username, email, password) + }) +} + +fn registration_request(username: String, email: String, password: String) { + let req = + element_actions.phone_home() + |> request.set_method(Post) + |> request.set_path("/api/fe/auth-create/") + |> request.set_body( + json.object([ + #("username", json.string(username)), + #("password", json.string(password)), + #("email", json.string(email)), + ]) + |> json.to_string, + ) + |> request.set_header("Content-Type", "application/json") + // |> request.prepend_header("accept", "application/vnd.hmrc.1.0+json") + + fetch.send(req) + |> promise.try_await(fetch.read_json_body) + |> promise.await(fn(resp) { + let assert Ok(resp) = resp + // We don't care about the status code, we just want to know if the request was successful. + // let assert 200 = resp.status + // let assert Ok("application/json; charset=utf-8") = response.get_header(resp, "content-type") + let assert Ok(registration_response) = + resp.body + |> dynamic.decode2( + RegistrationResponse, + field("Ok", of: dynamic.bool), + field("Errorvalue", of: dynamic.string), + ) + registration_response |> continue_after_signup() + promise.resolve(Ok(Nil)) + }) +} + +type RegistrationResponse { + RegistrationResponse(ok: Bool, error_value: String) +} + +fn continue_after_signup(registration_response) { + case registration_response { + RegistrationResponse(True, _) -> { + let assert Ok(d) = document.get_element_by_id("Aaa1") + d + |> element.set_inner_text( + "Sign-up successful! You will be forwarded now.", + ) + + global.set_timeout(3000, fn() { + window.set_location( + window.self(), + "/home/" + <> { + case window.get_hash() { + // const loginPageList = ["home", "notifications", "test"]; + Ok("home") -> "#home" + Ok("notifications") -> "#notifications" + Ok("test") -> "#test" + _ -> "" + } + }, + ) + }) + } + RegistrationResponse(False, _) -> { + let assert Ok(d) = document.get_element_by_id("Aaa1") + d + |> element.set_inner_text( + "
", + ) + let assert Ok(submitbutton) = document.get_element_by_id("submitbutton") + submitbutton |> element.set_inner_text("Sign up") + submitbutton |> element_actions.enable_element + } + } } diff --git a/frontend/src/frontend/page/site.gleam b/frontend/src/frontend/page/site.gleam index b08eaf7..e92bb85 100644 --- a/frontend/src/frontend/page/site.gleam +++ b/frontend/src/frontend/page/site.gleam @@ -1,18 +1,437 @@ // Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman // Licensed under the BSD 3-Clause License. See the LICENSE file for more info. -import gleamy_lights/helper as web_io +import gleam/bool +import gleam/list +import lumina/shared/shared_fepage_com.{ + type FEPageServeRequest, type FEPageServeResponse, FEPageServeRequest, + FEPageServeResponse, +} +import plinth/javascript/global + +import frontend/other/element_actions +import gleam/dict.{type Dict} +import gleam/dynamic +import gleam/fetch +import gleam/http +import gleam/http/request +import gleam/http/response +import gleam/javascript/promise +import gleam/json +import gleam/string +import gleamy_lights/console import gleamy_lights/premixed import gleamy_lights/premixed/gleam_colours +import plinth/browser/document +import plinth/browser/element +import plinth/browser/window pub fn home_render() { - web_io.println( - "Detected you are on the " <> premixed.text_pink("home page") <> ".", - ) + console.log( + "Detected you are on the " <> premixed.text_pink("home page") <> ".", + ) + let sub_page_list: SubPageList = + dict.new() + |> dict.insert( + "home", + SubPageMeta( + { + let assert Ok(a) = document.query_selector("#home-nav") + a + }, + { + let assert Ok(a) = document.query_selector("#mobile-home-nav") + a + }, + fn() { + let assert Ok(a) = document.query_selector("#mobiletimelineswitcher") + a |> element_actions.show_element() + Nil + }, + "home", + True, + ), + ) + |> dict.insert( + "test", + SubPageMeta( + { + let assert Ok(a) = document.query_selector("#test-nav") + a + }, + { + let assert Ok(a) = document.query_selector("#mobile-test-nav") + a + }, + fn() { + let assert Ok(a) = document.query_selector("#mobiletimelineswitcher") + a |> element_actions.hide_element() + Nil + }, + "test", + True, + ), + ) + |> dict.insert( + "editor", + SubPageMeta( + { + let assert Ok(a) = document.query_selector("#home-nav") + a + }, + { + let assert Ok(a) = document.query_selector("#mobile-home-nav") + a + }, + fn() { editor_unfold() }, + "editor", + False, + ), + ) + |> dict.insert( + "notifications", + SubPageMeta( + { + let assert Ok(a) = document.query_selector("#notifications-nav") + a + }, + { + let assert Ok(a) = + document.query_selector("#mobile-notifications-nav") + a + }, + fn() { + let assert Ok(a) = document.query_selector("#mobiletimelineswitcher") + a |> element_actions.hide_element() + Nil + }, + "notifications", + True, + ), + ) + console.log( + "Subpage list: " <> premixed.text_lightblue(string.inspect(sub_page_list)), + ) + let hash = element_actions.get_window_location_hash() + switch_subpage(hash, "Initial load", sub_page_list) + editor_fold() + { + let assert Ok(a) = + document.get_element_by_id("switchpageNotificationsTrigger") + a + |> element.add_event_listener("click", fn(_) { + switch_subpage("notifications", "special click event", sub_page_list) + Nil + }) + } + { + let assert Ok(a) = document.get_element_by_id("editorTrigger") + a + |> element.add_event_listener("click", fn(_) { + trigger_editor() + Nil + }) + } + // TODO: Add keyboard shortcuts + // document.addEventListener("keydown", (event) => { + // if (document.body.dataset.editorOpen !== "true") { + // if (event.key === "e") { + // event.preventDefault(); + // triggerEditor(); + // } + // if (event.key === "h") { + // event.preventDefault(); + // window.location.hash = "home"; + // } + // if (event.key === "n") { + // event.preventDefault(); + // window.location.hash = "notifications"; + // } + // } + // }); + + Nil +} + +fn editor_unfold() { + let assert Ok(mobiletimelineswitcher) = + document.query_selector("#mobiletimelineswitcher") + let assert Ok(posteditor) = document.query_selector("div#posteditor") + let errormsg = + "

Failed to load post editor.

" + mobiletimelineswitcher |> element_actions.hide_element() + posteditor |> element_actions.show_element() + case document.body() |> element.dataset_get("editorOpen") { + Ok("initial") -> { + fetch_editor() + Nil + } + _ -> Nil + } + + todo +} + +fn trigger_editor() { + let hash = element_actions.get_window_location_hash() + + case document.body() |> element.dataset_get("editorOpen") { + Ok("true") -> { + console.info( + "triggerEditor: got called, but editor is already open. Refolding editor instead.", + ) + editor_fold() + } + _ -> { + case hash == "editor" { + True -> { + // Editor glitched out, going back to retry... + console.log("triggerEditor: retrying...") + element_actions.go_back() + global.set_timeout(600, fn() { + element_actions.set_window_location_hash("editor") + Nil + }) + Nil + } + False -> { + element_actions.set_window_location_hash("editor") + } + } + } + } +} + +fn editor_fold() { + let assert Ok(posteditor) = document.query_selector("div#posteditor") + posteditor |> element_actions.hide_element() + case document.body() |> element.dataset_get("editorOpen") { + Ok(_) -> + document.body() |> element.set_attribute("data-editor-open", "false") + Error(_) -> + document.body() |> element.set_attribute("data-editor-open", "initial") + } +} + +fn fetch_page(page: String, then: fn(Result(FEPageServeResponse, Nil)) -> Nil) { + { + let req = + { + let assert Ok(a) = request.to(window.origin() <> "/api/fe/fetch-page") + a + } + |> request.set_body("{\"location\": \"" <> page <> "\"}") + |> request.set_header("Content-Type", "application/json") + |> request.set_method(http.Post) + use resp <- promise.try_await(fetch.send(req)) + use resp <- promise.try_await(fetch.read_text_body(resp)) + promise.resolve(Ok(resp)) + } + |> promise.await(fn(a: Result(response.Response(String), fetch.FetchError)) { + case a { + Ok(b) -> { + case + json.decode( + from: b.body, + using: dynamic.decode3( + FEPageServeResponse, + dynamic.field("main", dynamic.string), + dynamic.field("side", dynamic.string), + dynamic.field("message", dynamic.list(dynamic.int)), + ), + ) + { + Ok(c) -> then(Ok(c)) + Error(_) -> then(Error(Nil)) + } + } + Error(_) -> then(Error(Nil)) + } + promise.resolve(Nil) + }) +} + +fn fetch_editor() { + use presp <- fetch_page("editor", _) + case presp { + Ok(resp) -> { + console.log("Page: " <> premixed.text_lightblue(string.inspect(resp))) + let resp: FEPageServeResponse = resp + let message_list = resp.message + case + bool.and( + message_list |> list.contains(1) |> bool.negate, + message_list |> list.contains(2) |> bool.negate, + ) + { + True -> { + // document.querySelector("div#posteditor").innerHTML = + // response.data.main; + // window.history.back(); + { + let assert Ok(a) = document.query_selector("div#posteditor") + a + } + |> element.set_inner_html(resp.main) + element_actions.go_back() + Nil + } + False -> { + // document.querySelector("div#posteditor").innerHTML = + // errormsg; + { + let assert Ok(a) = document.query_selector("div#posteditor") + a + } + |> element.set_inner_html( + "

Failed to load post editor.

", + ) + Nil + } + } + } + Error(_) -> { + Nil + } + } } pub fn index_render() { - web_io.println( - "Detected you are on the " <> gleam_colours.text_faff_pink("first page") <> ".", - ) + console.log( + "Detected you are on the " + <> gleam_colours.text_faff_pink("first page") + <> ".", + ) +} + +type SubPageList = + Dict(String, SubPageMeta) + +type SubPageMeta { + SubPageMeta( + desktop: element.Element, + mobile: element.Element, + f: fn() -> Nil, + location: String, + navigator: Bool, + ) +} + +fn switch_subpage(to_page: String, reason: String, sub_page_list: SubPageList) { + let error_out = fn() { + let assert Ok(a) = document.query_selector("main div#mainright") + a |> element.set_inner_html("There was an error loading this page.") + let assert Ok(a) = document.query_selector("main div#mainleft") + a |> element.set_inner_html("") + Nil + } + let to = case to_page { + "" -> { + // The next line might cause some errors. + // In the TypeScript version, the hash is only changed at the end of the function. + // It also keeps the other URL parameters intact. + element_actions.set_window_location_hash("home") + "home" + } + _ -> to_page + } + console.info("Switching page to " <> to <> ". Reason: " <> reason) + dict.each(sub_page_list, fn(k, v) { + case v { + SubPageMeta(desktop, mobile, _, location, True) -> { + case k == to { + False -> { + desktop + |> element.set_attribute( + "class", + "px-3 py-2 text-sm font-medium bg-orange-200 border-2 rounded-md text-brown-800 dark:text-orange-200 border-emerald-600 dark:bg-yellow-700 dark:border-zinc-400 hover:bg-gray-700 hover:text-white", + ) + mobile + |> element.set_attribute( + "class", + "block rounded-md px-3 py-2 text-base font-medium bg-orange-200 text-brown-800 dark:text-orange-200 border-emerald-600 dark:bg-yellow-700 dark:border-zinc-400 hover:bg-gray-700 hover:text-white", + ) + } + True -> Nil + } + + [desktop, mobile] + |> list.each(fn(h) { + case h |> element.dataset_get("listening") { + Ok("true") -> Nil + _ -> { + h + |> element.add_event_listener("click", fn(_) { + switch_subpage(location, "Click event", sub_page_list) + Nil + }) + h |> element.set_attribute("data-listening", "true") + } + } + }) + } + SubPageMeta(_, _, _, _, False) -> { + Nil + } + } + }) + case dict.get(sub_page_list, to) { + Ok(SubPageMeta(desktop, mobile, f, location, _)) -> { + mobile + |> element.set_attribute( + "class", + "bg-red-400 dark:bg-red-900 text-white block rounded-md px-3 py-2 text-base font-medium", + ) + desktop + |> element.set_attribute( + "class", + "border-2 px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-md", + ) + use resp <- fetch_page(location) + case resp { + Error(_) -> { + error_out() + } + Ok(respons) -> { + let msg_list = respons.message + case msg_list |> list.contains(1) { + False -> { + case msg_list |> list.contains(34) |> bool.negate { + True -> { + let assert Ok(a) = + document.query_selector("main div#mainright") + a |> element.set_inner_html(respons.main) + case msg_list |> list.contains(33) |> bool.negate { + True -> { + let assert Ok(a) = + document.query_selector("main div#mainleft") + a |> element.set_inner_html(respons.side) + } + False -> Nil + } + } + False -> { + Nil + } + } + } + True -> { + window.set_location( + window.self(), + "/login#" + <> { + let a = element_actions.get_window_location_hash() + let assert Ok(b) = a |> string.split("?") |> list.first() + b + }, + ) + } + } + } + } + document.body() |> element.set_attribute("data-current-page", to) + f() + } + Error(_) -> promise.resolve(error_out()) + } } diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..0e5dd4d --- /dev/null +++ b/nodemon.json @@ -0,0 +1,4 @@ +{ + "ext": "ts,rs,gleam,svg,handlebars,hb,pcss,css", + "ignore": ["./*/build/**", "./backend/priv/generated/**"] +} diff --git a/package.json b/package.json index a88da8c..a5ac3bb 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,13 @@ "packageManager": "bun@1.1.30+", "devDependencies": { "@biomejs/biome": "latest", + "@types/bun": "latest", + "tailwindcss": "latest", + "autoprefixer": "latest", + "postcss": "latest", + "typescript": "^5.6.3", + "postcss-cli": "^11.0.0", + "clean-css-cli": "^5.6.3", "bun": "latest" }, "dependencies": { @@ -10,17 +17,17 @@ "nodemon": "^3.1.7" }, "scripts": { - "build": "bash ./build.sh --frontend-ts", - "build-with-gleam": "bash ./build.sh --frontend-gleam", + "build": "bash ./build.sh --frontend=typescript", + "build-with-gleam": "bash ./build.sh --frontend=gleam", "format": "bun run format:gleam && bun run format:ts && bun run minify:css", "format:gleam": "echo \"Incomplete script.\"", "format:ts": "bun x biome format . --write", "minify": "bun run minify:css", "start": "cd ./backend/ && gleam run ", "dev-ts": "bun run build && bun run start", - "dev-gleam": "bun run build-with-gleam && bun run start", - "watch-ts": "bun x nodemon -e ts,gleam,svg,handlebars,hb,pcss,css --ignore './shared/build/**' --ignore './frontend/build/**' --ignore './backend/build/**' --ignore './backend/priv/generated/**' --exec bun run dev-ts ", - "watch-gleam": "bun x nodemon -e mjs,gleam,svg,handlebars,hb,pcss,css --ignore './shared/build/**' --ignore './frontend/build/**' --ignore './backend/build/**' --ignore './backend/priv/generated/**' --exec bun run dev-gleam ", + "dev-gleam": "bun tobundle.ts setup-prelude && bun run build-with-gleam && bun run start", + "watch-ts": "bun x nodemon --exec bun run dev-ts", + "watch-gleam": "bun x nodemon --exec bun run dev-gleam", "clean": "rm -rf ./backend/node_modules/ && rm -rf ./frontend/node_modules/ && rm -rf ./frontend-ts/node_modules/ && rm -rf ./backend/priv/generated && mkdir -p ./backend/priv/generated/css && mkdir -p ./backend/priv/generated/js && cd backend && gleam clean && cd ../frontend && gleam clean && cd ../shared && gleam clean" }, "trustedDependencies": ["@biomejs/biome", "bun"] diff --git a/shared/gleam.toml b/shared/gleam.toml index c01d91c..1719a8f 100644 --- a/shared/gleam.toml +++ b/shared/gleam.toml @@ -13,7 +13,7 @@ version = "1.0.0" # https://gleam.run/writing-gleam/gleam-toml/. [dependencies] -gleam_stdlib = ">= 0.34.0 and < 2.0.0" +# gleam_stdlib = ">= 0.34.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/shared/manifest.toml b/shared/manifest.toml index 4721e5c..5505982 100644 --- a/shared/manifest.toml +++ b/shared/manifest.toml @@ -2,10 +2,9 @@ # You typically do not need to edit this file packages = [ - { name = "gleam_stdlib", version = "0.43.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "69EF22E78FDCA9097CBE7DF91C05B2A8B5436826D9F66680D879182C0860A747" }, + { name = "gleam_stdlib", version = "0.44.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "A6E55E309A6778206AAD4038D9C49E15DF71027A1DB13C6ADA06BFDB6CF1260E" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, ] [requirements] -gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/shared/src/lumina/shared/shared_fepage_com.gleam b/shared/src/lumina/shared/shared_fepage_com.gleam new file mode 100644 index 0000000..baaaab8 --- /dev/null +++ b/shared/src/lumina/shared/shared_fepage_com.gleam @@ -0,0 +1,12 @@ +// Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman +// Licensed under the BSD 3-Clause License. See the LICENSE file for more info. + +/// A request from the client to the server to serve a page. +pub type FEPageServeRequest { + FEPageServeRequest(location: String) +} + +/// A response from the server to a request to serve a page. +pub type FEPageServeResponse { + FEPageServeResponse(main: String, side: String, message: List(Int)) +} diff --git a/backend/tailwind.config.js b/tailwind.config.js similarity index 62% rename from backend/tailwind.config.js rename to tailwind.config.js index 509376a..39a483f 100644 --- a/backend/tailwind.config.js +++ b/tailwind.config.js @@ -1,14 +1,8 @@ -/* - * Copyright (c) 2024, MLC 'Strawmelonjuice' Bloeiman - * - * Licensed under the BSD 3-Clause License. See the LICENSE file for more info. - */ - -/** @type {import('tailwindcss').LuminaConfig} */ module.exports = { content: [ - "./src-frontend/**/*.{handlebars,ts}", - "./src/lumina/web/**/*.gleam", + "./*frontend/**/*.{handlebars,ts,gleam}", + "./backend/src/lumina/web/**/*.gleam", + "./backend-rs/src/**/*.rs", ], theme: { fontFamily: { diff --git a/tobundle.ts b/tobundle.ts index ef01d97..82cfb93 100644 --- a/tobundle.ts +++ b/tobundle.ts @@ -1,4 +1,3 @@ -// @ts-ignore import Bun from "bun"; switch (process.argv[2]) { case "js-1": diff --git a/watch.sh b/watch.sh new file mode 100644 index 0000000..b953510 --- /dev/null +++ b/watch.sh @@ -0,0 +1 @@ +watchexec -w . -e ts,rs,gleam,svg,handlebars,hb,pcss,css -r -- bash ./build.sh --run $@