commit 0b91b94ae13f78968d0e3e6b14f865bafcd6f059 Author: Torsten Schulz (local) Date: Wed Mar 4 17:04:41 2026 +0100 Initialisiere yourchat2 als eigenständigen Rust-Chatdienst und portiere die Kernfunktionen aus der Altanwendung. Die Implementierung enthält modulare Command-/State-/DB-Strukturen, DB-basierte Authentifizierung inkl. Rechte- und Raumzugriffsprüfung sowie kompatible Chat- und Dice-Commands. Made-with: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8396fa1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1438 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[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 = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[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 = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[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 = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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 = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "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", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "postgres-protocol" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[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 = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "yourchat2" +version = "0.1.0" +dependencies = [ + "futures-util", + "hex", + "openssl", + "scrypt", + "serde", + "serde_json", + "tokio", + "tokio-postgres", + "tokio-tungstenite", + "uuid", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5083a91 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "yourchat2" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "io-util", "sync", "signal", "fs"] } +uuid = { version = "1", features = ["v4"] } +tokio-tungstenite = "0.28" +futures-util = "0.3" +tokio-postgres = "0.7" +openssl = "0.10" +hex = "0.4" +scrypt = "0.11" diff --git a/README.md b/README.md new file mode 100644 index 0000000..42d6e9a --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# yourchat2 + +Rust-basierter Chat-Service/Daemon zur Entlastung des Backends von `YourPart3`. +Die Kommunikation erfolgt ueber WebSocket (Frontend) und optional TCP/Unix-Socket (Bridge/Backend). + +## Features + +- Asynchroner WebSocket-Server fuer `MultiChatDialog` (Standard `0.0.0.0:1235`) +- Zusaetzlicher TCP-Server fuer `chatTcpBridge.js` (Standard `127.0.0.1:1236`) +- Optional zusaetzlicher Unix Domain Socket +- In-Memory-Room-Management +- Token-basierte Session-Absicherung pro Verbindung +- Username-Pruefung (3-32 Zeichen, `[a-zA-Z0-9_]`) +- Verhindert Mehrfach-Login mit identischem Namen (`loggedin`) +- Optionale Allowlist fuer User via `CHAT_ALLOWED_USERS` +- Optionale DB-basierte User-Pruefung via Postgres (`CHAT_DB_URL`) +- Kompatible Antworttypen fuer die bestehende `chatTcpBridge.js` + +## Start + +```bash +cd ~/Programs/yourchat2 +cargo run --release +``` + +Der Server lauscht danach standardmaessig auf: + +- `ws://0.0.0.0:1235` (Frontend `MultiChatDialog`) +- `tcp://127.0.0.1:1236` (Backend-Bridge) + +## Konfiguration (Umgebungsvariablen) + +- `CHAT_WS_ADDR` (default: `0.0.0.0:1235`) +- `CHAT_TCP_ADDR` (default: `127.0.0.1:1236`) +- `CHAT_UNIX_SOCKET` (optional, z. B. `/run/yourchat2/yourchat2.sock`) +- `CHAT_ALLOWED_USERS` (optional, CSV-Liste erlaubter Usernamen, z. B. `alice,bob,carol`) +- `CHAT_DB_URL` (optional, PostgreSQL-Connection-String fuer Community-/Chat-User-Checks) +- `SECRET_KEY` (benoetigt fuer Entschluesselung verschluesselter Birthdate-Werte aus der DB) + +Beispiel: + +```bash +CHAT_WS_ADDR=0.0.0.0:1235 CHAT_TCP_ADDR=127.0.0.1:1236 CHAT_UNIX_SOCKET=/tmp/yourchat2.sock cargo run --release +``` + +## Protokoll (Kurzfassung) + +Alle Requests/Responses sind JSON-Objekte. +- WebSocket: ein JSON pro Frame (Text) +- TCP/Unix-Socket: newline-delimited JSON + +### Eingehende Commands + +- `{"type":"init","name":"alice","room":"lobby"}` +- `{"type":"join","room":"sports","token":"..."}` +- `{"type":"message","message":"Hi","token":"..."}` +- `{"type":"scream","message":"Hallo alle","token":"..."}` +- `{"type":"do","message":"winkt","token":"..."}` +- `{"type":"dice","message":"1d6","token":"..."}` +- `{"type":"roll","value":4,"token":"..."}` +- `{"type":"color","value":"#33aaee","token":"..."}` +- `{"type":"rooms","token":"..."}` +- `{"type":"userlist","token":"..."}` +- `{"type":"start_dice_game","rounds":3,"token":"..."}` +- `{"type":"end_dice_game","token":"..."}` +- `{"type":"reload_rooms","token":"..."}` +- `{"type":"ping"}` + +Zusatz: Bei `{"type":"message", ...}` werden Slash-Commands erkannt: +- `/join [password]` +- `/color ` +- `/dice [expr]` oder `/roll [expr]` +- `/start_dice_game ` +- `/end_dice_game` +- `/reload_rooms` +- `/do ` +- `/scream ` +- `/rooms` und `/userlist` + +Wenn `CHAT_DB_URL` gesetzt ist, prueft `init` den User gegen `community."user"`. +Falls fuer den Community-User noch kein Eintrag in `chat."user"` existiert, wird er automatisch angelegt. +Farbe und Rechte werden aus der DB geladen. +Fuer Alterspruefungen in Raeumen wird `community.user_param.value` (birthdate) per AES-256-ECB +mit einem via scrypt aus `SECRET_KEY` abgeleiteten Schluessel entschluesselt (kompatibel zur Alt-App). + +### Wichtige Antworten + +- Token: `{"type":1,"message":""}` +- Roomliste: `{"type":3,"message":[{"name":"lobby","users":2}]}` +- Room betreten: `{"type":5,"message":"room_entered","to":"lobby"}` +- Farbaenderung: `{"type":5,"message":"color_changed","userName":"alice","color":"#33aaee"}` +- Normale Message: `{"type":"message","userName":"alice","message":"Hi","color":"#33aaee"}` +- Scream: `{"type":6,"userName":"alice","message":"Hallo alle","color":"#33aaee"}` + +## Integration mit `YourPart3` + +In `YourPart3/backend/config/chatBridge.json` sollte stehen: + +```json +{ + "host": "127.0.0.1", + "port": 1236 +} +``` + +Dann verbindet sich die bestehende Bridge (`chatTcpBridge.js`) direkt mit `yourchat2`. + +## Systemd (optional) + +Es liegt eine Beispiel-Datei `yourchat2.service` im Projekt. +Nach Anpassung des User/Paths: + +```bash +sudo cp yourchat2.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now yourchat2 +``` diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..1abb83b --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,1021 @@ +use crate::types::{ChatState, ClientId, Command, RoomMeta, ServerConfig}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use uuid::Uuid; + +use crate::db; +use crate::state; + +pub async fn process_text_command( + client_id: ClientId, + raw: &str, + state: Arc>, + config: Arc, +) { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return; + } + match serde_json::from_str::(trimmed) { + Ok(command) => { + handle_command(client_id, command, state, config).await; + } + Err(err) => { + state::send_to_client( + client_id, + state, + json!({"type":"error","message": format!("invalid_json: {err}")}), + ) + .await; + } + } +} + +pub async fn handle_command( + client_id: ClientId, + command: Command, + state: Arc>, + config: Arc, +) { + let mut current = command; + loop { + match command_name(¤t).as_str() { + "init" => { + handle_init_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; + return; + } + "join" => { + handle_join_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; + return; + } + "message" => { + if let Some(next_command) = + handle_message_command(client_id, ¤t, Arc::clone(&state)).await + { + current = next_command; + continue; + } + return; + } + "scream" => { + handle_scream_command(client_id, ¤t, Arc::clone(&state)).await; + return; + } + "do" => { + handle_do_command(client_id, ¤t, Arc::clone(&state)).await; + return; + } + "dice" | "roll" => { + handle_dice_command(client_id, ¤t, Arc::clone(&state)).await; + return; + } + "color" => { + handle_color_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; + return; + } + "rooms" | "list_rooms" => { + handle_rooms_command(client_id, ¤t, Arc::clone(&state)).await; + return; + } + "userlist" => { + handle_userlist_command(client_id, ¤t, Arc::clone(&state)).await; + return; + } + "ping" => { + state::send_to_client(client_id, Arc::clone(&state), json!({"type":"pong"})).await; + return; + } + "start_dice_game" => { + handle_start_dice_game_command(client_id, ¤t, Arc::clone(&state)).await; + return; + } + "end_dice_game" => { + handle_end_dice_game_command(client_id, ¤t, Arc::clone(&state)).await; + return; + } + "reload_rooms" => { + handle_reload_rooms_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; + return; + } + _ => { + handle_unknown_command(client_id, ¤t, Arc::clone(&state)).await; + return; + } + } + } +} + +fn command_name(command: &Command) -> String { + match &command.cmd_type { + Value::String(s) => s.to_lowercase(), + Value::Number(n) => n.to_string(), + _ => String::new(), + } +} + +async fn send_error(client_id: ClientId, state: Arc>, message: &str) { + state::send_to_client(client_id, state, json!({"type":"error","message": message})).await; +} + +async fn handle_init_command( + client_id: ClientId, + command: &Command, + state: Arc>, + config: Arc, +) { + let Some(raw_name) = command.name.clone().or(command.user_name.clone()) else { + send_error(client_id, Arc::clone(&state), "missing_name").await; + return; + }; + let requested_user_name = raw_name.trim().to_string(); + if !db::is_valid_username(&requested_user_name) { + send_error(client_id, Arc::clone(&state), "invalid_username").await; + return; + } + if !db::is_allowed_user(&requested_user_name, &config) { + send_error(client_id, Arc::clone(&state), "user_not_allowed").await; + return; + } + let profile = match db::load_user_profile(&requested_user_name, &config).await { + Ok(profile) => profile, + Err(error_message) => { + send_error(client_id, Arc::clone(&state), error_message).await; + return; + } + }; + let room_name = normalize_room_name(command.room.as_deref().unwrap_or("lobby")); + let room_password = command.password.clone().unwrap_or_default(); + + let (token, user_name) = { + let mut guard = state.write().await; + if guard.logged_in_names.contains(&requested_user_name) + && guard + .clients + .iter() + .any(|(id, c)| *id != client_id && c.logged_in && c.user_name == requested_user_name) + { + drop(guard); + send_error(client_id, Arc::clone(&state), "loggedin").await; + return; + } + if !room_access_allowed( + guard.room_meta.get(&room_name), + &profile.display_name, + profile.falukant_user_id, + profile.age, + &room_password, + ) { + drop(guard); + send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; + return; + } + + let (old_room, old_name, was_logged_in, user_name, token, new_token) = { + let Some(client) = guard.clients.get_mut(&client_id) else { + return; + }; + let old_room = client.room.clone(); + let old_name = client.user_name.clone(); + let was_logged_in = client.logged_in; + + client.user_name = profile.display_name.clone(); + client.color = profile.color.clone(); + client.falukant_user_id = profile.falukant_user_id; + client.chat_user_id = profile.chat_user_id; + client.age = profile.age; + client.rights = profile.rights.clone(); + client.logged_in = true; + client.room = room_name.clone(); + + let mut new_token = None; + if client.token.is_none() { + let generated = Uuid::new_v4().to_string(); + client.token = Some(generated.clone()); + new_token = Some(generated); + } + + ( + old_room, + old_name, + was_logged_in, + client.user_name.clone(), + client.token.clone().unwrap_or_default(), + new_token, + ) + }; + + if let Some(generated) = new_token { + guard.tokens.insert(generated, client_id); + } + if was_logged_in { + guard.logged_in_names.remove(&old_name); + } + guard.logged_in_names.insert(user_name.clone()); + if !old_room.is_empty() { + if let Some(members) = guard.rooms.get_mut(&old_room) { + members.remove(&client_id); + } + } + guard.rooms.entry(room_name.clone()).or_default().insert(client_id); + (token, user_name) + }; + + state::send_to_client( + client_id, + Arc::clone(&state), + json!({"type":1, "message": token}), + ) + .await; + state::send_room_list(client_id, Arc::clone(&state)).await; + state::send_to_client( + client_id, + Arc::clone(&state), + json!({"type":5, "message":"room_entered", "to": room_name}), + ) + .await; + state::broadcast_room( + &room_name, + Arc::clone(&state), + json!({"type":"system","message": format!("{user_name} joined {room_name}")}), + Some(client_id), + ) + .await; +} + +async fn handle_join_command( + client_id: ClientId, + command: &Command, + state: Arc>, + _config: Arc, +) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + let room = normalize_room_name(command.room.as_deref().or(command.name.as_deref()).unwrap_or("lobby")); + let password = command.password.clone().unwrap_or_default(); + let (from_room, user_name) = { + let mut guard = state.write().await; + let (name_for_check, falukant_user_id, age) = { + let Some(client) = guard.clients.get(&client_id) else { + return; + }; + (client.user_name.clone(), client.falukant_user_id, client.age) + }; + if !room_access_allowed( + guard.room_meta.get(&room), + &name_for_check, + falukant_user_id, + age, + &password, + ) { + drop(guard); + send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; + return; + } + let Some(client) = guard.clients.get_mut(&client_id) else { + return; + }; + let from_room = client.room.clone(); + let user_name = client.user_name.clone(); + client.room = room.clone(); + if !from_room.is_empty() { + if let Some(members) = guard.rooms.get_mut(&from_room) { + members.remove(&client_id); + } + } + guard.rooms.entry(room.clone()).or_default().insert(client_id); + (from_room, user_name) + }; + + if !from_room.is_empty() && from_room != room { + state::broadcast_room( + &from_room, + Arc::clone(&state), + json!({"type":"system","message": format!("{user_name} left {from_room}")}), + Some(client_id), + ) + .await; + } + state::send_to_client( + client_id, + Arc::clone(&state), + json!({"type":5, "message":"room_entered", "to": room}), + ) + .await; + state::send_room_list(client_id, Arc::clone(&state)).await; +} + +async fn handle_message_command( + client_id: ClientId, + command: &Command, + state: Arc>, +) -> Option { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return None; + } + let text = command.message.clone().unwrap_or_default(); + if text.trim().is_empty() { + return None; + } + if let Some(cmd) = command_from_chat_line(&text, command.token.clone()) { + return Some(cmd); + } + let (room, user_name, color) = { + let guard = state.read().await; + let Some(client) = guard.clients.get(&client_id) else { + return None; + }; + (client.room.clone(), client.user_name.clone(), client.color.clone()) + }; + if room.is_empty() { + return None; + } + state::broadcast_room( + &room, + Arc::clone(&state), + json!({"type":"message", "userName": user_name, "message": text, "color": color}), + None, + ) + .await; + None +} + +async fn handle_scream_command(client_id: ClientId, command: &Command, state: Arc>) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + let text = command.message.clone().unwrap_or_default(); + if text.trim().is_empty() { + return; + } + let (user_name, color) = { + let guard = state.read().await; + let Some(client) = guard.clients.get(&client_id) else { + return; + }; + (client.user_name.clone(), client.color.clone()) + }; + state::broadcast_all( + Arc::clone(&state), + json!({"type":6, "userName": user_name, "message": text, "color": color}), + ) + .await; +} + +async fn handle_do_command(client_id: ClientId, command: &Command, state: Arc>) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + let text = command + .message + .clone() + .or_else(|| string_from_value(command.value.as_ref())) + .unwrap_or_default(); + if text.trim().is_empty() { + return; + } + let target = command.to.clone(); + let (room, user_name, color) = { + let guard = state.read().await; + let Some(client) = guard.clients.get(&client_id) else { + return; + }; + ( + client.room.clone(), + client.user_name.clone(), + client.color.clone().unwrap_or_else(|| "#000000".to_string()), + ) + }; + if room.is_empty() { + return; + } + if let Some(to_name) = target { + state::broadcast_room( + &room, + Arc::clone(&state), + json!({ + "type": 7, + "message": {"tr":"user_action","action": text,"to": to_name}, + "userName": user_name, + "color": color + }), + None, + ) + .await; + } else { + state::broadcast_room( + &room, + Arc::clone(&state), + json!({"type":"system", "message": format!("* {} {}", user_name, text)}), + None, + ) + .await; + } +} + +async fn handle_dice_command(client_id: ClientId, command: &Command, state: Arc>) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + let expr = command + .message + .clone() + .or_else(|| string_from_value(command.value.as_ref())) + .unwrap_or_else(|| "1d6".to_string()); + let numeric_roll = i32_from_command_value(command).or_else(|| parse_roll_value(&expr)); + let (room, user_name, color) = { + let guard = state.read().await; + let Some(client) = guard.clients.get(&client_id) else { + return; + }; + ( + client.room.clone(), + client.user_name.clone(), + client.color.clone().unwrap_or_else(|| "#000000".to_string()), + ) + }; + if room.is_empty() { + return; + } + + let mut game_events: Vec = Vec::new(); + let mut invalid_roll = false; + let mut already_rolled = false; + let mut round_snapshot: Option<(i32, HashMap)> = None; + let mut total_snapshot: Option> = None; + let mut next_round_to_start: Option = None; + let mut game_ended = false; + { + let mut guard = state.write().await; + let member_count = guard.rooms.get(&room).map(|m| m.len()).unwrap_or(0); + if let Some(game) = guard.dice_games.get_mut(&room) { + if game.running { + if let Some(rolled) = numeric_roll { + if !(1..=6).contains(&rolled) { + invalid_roll = true; + } else if game.rolled_this_round.contains_key(&client_id) { + already_rolled = true; + } else { + game.rolled_this_round.insert(client_id, rolled); + *game.total_scores.entry(client_id).or_insert(0) += rolled; + game_events.push(json!({ + "type": 8, + "message": {"tr":"dice_rolled","round": game.current_round,"value": rolled}, + "userName": user_name, + "color": color + })); + if member_count > 0 && game.rolled_this_round.len() >= member_count { + round_snapshot = Some((game.current_round, game.rolled_this_round.clone())); + if game.current_round >= game.total_rounds { + total_snapshot = Some(game.total_scores.clone()); + game.running = false; + game_ended = true; + } else { + game.current_round += 1; + game.rolled_this_round.clear(); + next_round_to_start = Some(game.current_round); + } + } + } + } else { + invalid_roll = true; + } + } + } + } + + if invalid_roll { + send_error(client_id, Arc::clone(&state), "invalid_dice_value").await; + return; + } + if already_rolled { + send_error(client_id, Arc::clone(&state), "dice_already_done").await; + return; + } + + if let Some((round_number, rolled_values)) = round_snapshot { + let guard = state.read().await; + let mut round_results: Vec = Vec::new(); + for (uid, value) in rolled_values { + if let Some(c) = guard.clients.get(&uid) { + round_results.push(json!({"userName": c.user_name, "value": value})); + } + } + game_events.push(json!({ + "type": 8, + "message": {"tr":"dice_round_ended","round": round_number,"results": round_results} + })); + } + if let Some(total_scores) = total_snapshot { + let guard = state.read().await; + let mut final_results: Vec = Vec::new(); + for (uid, score) in total_scores { + if let Some(c) = guard.clients.get(&uid) { + final_results.push(json!({"userName": c.user_name, "score": score})); + } + } + game_events.push(json!({ + "type": 8, + "message": {"tr":"dice_game_results","results": final_results} + })); + } + if let Some(next_round) = next_round_to_start { + game_events.push(json!({"type": 8, "message": {"tr":"dice_round_started","round": next_round}})); + } + if game_ended { + game_events.push(json!({"type": 8, "message": {"tr":"dice_game_ended"}})); + } + + if !game_events.is_empty() { + for event in game_events { + state::broadcast_room(&room, Arc::clone(&state), event, None).await; + } + } else { + state::broadcast_room( + &room, + Arc::clone(&state), + json!({"type":"system","message": format!("{user_name} rolled {expr}")}), + None, + ) + .await; + } +} + +async fn handle_color_command( + client_id: ClientId, + command: &Command, + state: Arc>, + config: Arc, +) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + let Some(mut color) = command + .message + .clone() + .or_else(|| string_from_value(command.value.as_ref())) + else { + return; + }; + color = normalize_color(&color); + if color.is_empty() { + send_error(client_id, Arc::clone(&state), "invalid_color").await; + return; + } + let (room, user_name, chat_user_id) = { + let mut guard = state.write().await; + let Some(client) = guard.clients.get_mut(&client_id) else { + return; + }; + client.color = Some(color.clone()); + (client.room.clone(), client.user_name.clone(), client.chat_user_id) + }; + if let Some(chat_user_id) = chat_user_id { + let _ = db::save_user_color(&config, chat_user_id, &color).await; + } + if !room.is_empty() { + state::broadcast_room( + &room, + Arc::clone(&state), + json!({"type":5, "message":"color_changed", "userName": user_name, "color": color}), + None, + ) + .await; + } +} + +async fn handle_rooms_command(client_id: ClientId, command: &Command, state: Arc>) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + state::send_room_list(client_id, Arc::clone(&state)).await; +} + +async fn handle_userlist_command(client_id: ClientId, command: &Command, state: Arc>) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + state::send_user_list(client_id, Arc::clone(&state)).await; +} + +async fn handle_start_dice_game_command( + client_id: ClientId, + command: &Command, + state: Arc>, +) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + let rounds = command + .rounds + .or_else(|| i32_from_command_value(command)) + .or_else(|| command.message.as_deref().and_then(parse_roll_value)) + .unwrap_or(0); + if !(1..=10).contains(&rounds) { + send_error(client_id, Arc::clone(&state), "invalid_rounds").await; + return; + } + let (room, user_name, is_admin) = { + let guard = state.read().await; + let Some(client) = guard.clients.get(&client_id) else { + return; + }; + ( + client.room.clone(), + client.user_name.clone(), + client.rights.contains("admin"), + ) + }; + if room.is_empty() { + return; + } + if !is_admin { + send_error(client_id, Arc::clone(&state), "permission_denied").await; + return; + } + let started = { + let mut guard = state.write().await; + let game = guard.dice_games.entry(room.clone()).or_default(); + if game.running { + false + } else { + game.running = true; + game.current_round = 1; + game.total_rounds = rounds; + game.rolled_this_round.clear(); + game.total_scores.clear(); + true + } + }; + if !started { + send_error(client_id, Arc::clone(&state), "dice_game_already_running").await; + return; + } + state::broadcast_room( + &room, + Arc::clone(&state), + json!({ + "type":8, + "message":{"tr":"dice_game_started","rounds":rounds,"round":1}, + "userName": user_name + }), + None, + ) + .await; +} + +async fn handle_end_dice_game_command( + client_id: ClientId, + command: &Command, + state: Arc>, +) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + let (room, is_admin) = { + let guard = state.read().await; + let Some(client) = guard.clients.get(&client_id) else { + return; + }; + (client.room.clone(), client.rights.contains("admin")) + }; + if room.is_empty() { + return; + } + if !is_admin { + send_error(client_id, Arc::clone(&state), "permission_denied").await; + return; + } + let ended = { + let mut guard = state.write().await; + if let Some(game) = guard.dice_games.get_mut(&room) { + if game.running { + game.running = false; + true + } else { + false + } + } else { + false + } + }; + if ended { + state::broadcast_room( + &room, + Arc::clone(&state), + json!({"type":8,"message":{"tr":"dice_game_ended"}}), + None, + ) + .await; + } else { + send_error(client_id, Arc::clone(&state), "dice_game_not_running").await; + } +} + +async fn handle_reload_rooms_command( + client_id: ClientId, + command: &Command, + state: Arc>, + config: Arc, +) { + if !state::authorize(client_id, command, Arc::clone(&state)).await { + return; + } + if let Ok(rooms) = db::load_room_configs(&config).await { + let mut guard = state.write().await; + for room in rooms { + guard.rooms.entry(room.name.clone()).or_default(); + guard.room_meta.insert(room.name.clone(), room); + } + } + state::send_room_list(client_id, Arc::clone(&state)).await; + state::send_to_client( + client_id, + Arc::clone(&state), + json!({"type":"system","message":"rooms_reloaded"}), + ) + .await; +} + +async fn handle_unknown_command(client_id: ClientId, command: &Command, state: Arc>) { + if command.password.is_some() { + send_error( + client_id, + Arc::clone(&state), + "password_protected_rooms_not_supported_yet", + ) + .await; + } else { + send_error(client_id, Arc::clone(&state), "unknown_command").await; + } +} + +fn room_access_allowed( + room_meta: Option<&RoomMeta>, + user_name: &str, + falukant_user_id: Option, + age: Option, + provided_password: &str, +) -> bool { + let Some(room) = room_meta else { + return false; + }; + + if let Some(room_password) = &room.password { + if !room_password.is_empty() && room_password != provided_password { + return false; + } + } + + if let Some(min_age) = room.min_age { + if min_age >= 18 { + let Some(fid) = falukant_user_id else { + return false; + }; + if fid <= 0 { + return false; + } + if let Some(user_age) = age { + if user_age < min_age { + return false; + } + } + } + } + if let Some(max_age) = room.max_age { + if let Some(user_age) = age { + if user_age > max_age { + return false; + } + } + } + + if !room.is_public { + if room.owner_id.is_none() { + return false; + } + if user_name.trim().is_empty() { + return false; + } + } + true +} + +fn normalize_room_name(input: &str) -> String { + let trimmed = input.trim(); + if trimmed.is_empty() { + return "lobby".to_string(); + } + trimmed.to_string() +} + +fn normalize_color(input: &str) -> String { + let mut value = input.trim().to_string(); + if value.is_empty() { + return String::new(); + } + if !value.starts_with('#') { + value = format!("#{value}"); + } + let raw = value.trim_start_matches('#'); + match raw.len() { + 3 if raw.chars().all(|c| c.is_ascii_hexdigit()) => { + let mut expanded = String::from("#"); + for ch in raw.chars() { + expanded.push(ch); + expanded.push(ch); + } + expanded + } + 6 if raw.chars().all(|c| c.is_ascii_hexdigit()) => format!("#{raw}"), + _ => String::new(), + } +} + +fn command_from_chat_line(message: &str, token: Option) -> Option { + let trimmed = message.trim(); + if !trimmed.starts_with('/') { + return None; + } + let mut parts = trimmed.splitn(2, ' '); + let command = parts.next().unwrap_or_default().to_lowercase(); + let rest = parts.next().unwrap_or("").trim().to_string(); + match command.as_str() { + "/join" => { + if rest.is_empty() { + return None; + } + let mut args = rest.splitn(2, ' '); + let room = args.next().unwrap_or_default().trim().to_string(); + let password = args.next().map(|p| p.trim().to_string()); + Some(Command { + cmd_type: Value::String("join".to_string()), + token, + name: None, + room: Some(room), + message: None, + value: None, + password, + rounds: None, + to: None, + user_name: None, + }) + } + "/color" => Some(Command { + cmd_type: Value::String("color".to_string()), + token, + name: None, + room: None, + message: Some(rest), + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/dice" => Some(Command { + cmd_type: Value::String("dice".to_string()), + token, + name: None, + room: None, + message: Some(if rest.is_empty() { "1d6".to_string() } else { rest }), + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/roll" => Some(Command { + cmd_type: Value::String("roll".to_string()), + token, + name: None, + room: None, + message: Some(rest), + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/start_dice_game" => Some(Command { + cmd_type: Value::String("start_dice_game".to_string()), + token, + name: None, + room: None, + message: Some(rest.clone()), + value: if rest.is_empty() { None } else { Some(Value::String(rest)) }, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/end_dice_game" => Some(Command { + cmd_type: Value::String("end_dice_game".to_string()), + token, + name: None, + room: None, + message: None, + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/reload_rooms" => Some(Command { + cmd_type: Value::String("reload_rooms".to_string()), + token, + name: None, + room: None, + message: None, + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/do" => Some(Command { + cmd_type: Value::String("do".to_string()), + token, + name: None, + room: None, + message: Some(rest), + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/scream" => Some(Command { + cmd_type: Value::String("scream".to_string()), + token, + name: None, + room: None, + message: Some(rest), + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/rooms" | "/list_rooms" => Some(Command { + cmd_type: Value::String("rooms".to_string()), + token, + name: None, + room: None, + message: None, + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + "/userlist" => Some(Command { + cmd_type: Value::String("userlist".to_string()), + token, + name: None, + room: None, + message: None, + value: None, + password: None, + rounds: None, + to: None, + user_name: None, + }), + _ => None, + } +} + +fn string_from_value(value: Option<&Value>) -> Option { + let value = value?; + match value { + Value::String(s) => Some(s.clone()), + Value::Number(n) => Some(n.to_string()), + Value::Bool(b) => Some(b.to_string()), + _ => None, + } +} + +fn i32_from_command_value(command: &Command) -> Option { + let value = command.value.as_ref()?; + match value { + Value::Number(n) => n.as_i64().and_then(|v| i32::try_from(v).ok()), + Value::String(s) => parse_roll_value(s), + _ => None, + } +} + +fn parse_roll_value(text: &str) -> Option { + let trimmed = text.trim(); + if trimmed.is_empty() { + return None; + } + if let Ok(value) = trimmed.parse::() { + return Some(value); + } + if let Some((_, rhs)) = trimmed.to_lowercase().split_once('d') { + return rhs.trim().parse::().ok(); + } + None +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..8e3968f --- /dev/null +++ b/src/db.rs @@ -0,0 +1,278 @@ +use crate::types::{RoomMeta, ServerConfig, UserProfile}; +use openssl::symm::{Cipher, Crypter, Mode}; +use scrypt::{scrypt, Params as ScryptParams}; +use std::collections::HashSet; +use std::env; +use std::sync::Arc; +use tokio_postgres::{Client as PgClient, NoTls}; + +pub fn parse_allowed_users() -> Option> { + let raw = env::var("CHAT_ALLOWED_USERS").ok()?; + let users = raw + .split(',') + .map(|entry| entry.trim()) + .filter(|entry| !entry.is_empty()) + .map(ToOwned::to_owned) + .collect::>(); + if users.is_empty() { + None + } else { + Some(users) + } +} + +pub async fn connect_db_from_env() -> Result>, Box> { + let Some(url) = env::var("CHAT_DB_URL").ok().filter(|v| !v.trim().is_empty()) else { + return Ok(None); + }; + let (client, connection) = tokio_postgres::connect(&url, NoTls).await?; + tokio::spawn(async move { + if let Err(err) = connection.await { + eprintln!("[yourchat2] db connection error: {err}"); + } + }); + println!("[yourchat2] postgres auth enabled"); + Ok(Some(Arc::new(client))) +} + +pub async fn load_user_profile(user_name: &str, config: &ServerConfig) -> Result { + let Some(client) = config.db_client.clone() else { + return Ok(UserProfile { + display_name: user_name.to_string(), + color: None, + falukant_user_id: None, + chat_user_id: None, + age: None, + rights: HashSet::new(), + }); + }; + + let community_user = client + .query_opt( + "SELECT id FROM community.\"user\" WHERE username = $1 LIMIT 1", + &[&user_name], + ) + .await + .map_err(|_| "db_error")?; + let Some(community_row) = community_user else { + return Err("user_not_allowed"); + }; + let falukant_user_id: i32 = community_row.get("id"); + + let chat_user = client + .query_opt( + "SELECT id, display_name, color FROM chat.\"user\" WHERE falukant_user_id = $1 LIMIT 1", + &[&falukant_user_id], + ) + .await + .map_err(|_| "db_error")?; + + let (chat_user_id, display_name, color) = if let Some(row) = chat_user { + ( + row.get::<_, i32>("id"), + row.get::<_, String>("display_name"), + row.get::<_, Option>("color"), + ) + } else { + let inserted = client + .query_one( + "INSERT INTO chat.\"user\" (falukant_user_id, display_name, color, show_gender, show_age, created_at, updated_at) VALUES ($1, $2, $3, true, true, NOW(), NOW()) RETURNING id, display_name, color", + &[&falukant_user_id, &user_name, &"#000000"], + ) + .await + .map_err(|_| "db_error")?; + ( + inserted.get::<_, i32>("id"), + inserted.get::<_, String>("display_name"), + inserted.get::<_, Option>("color"), + ) + }; + + let rights_rows = client + .query( + "SELECT r.tr FROM chat.user_rights ur JOIN chat.rights r ON ur.chat_right_id = r.id WHERE ur.chat_user_id = $1", + &[&chat_user_id], + ) + .await + .map_err(|_| "db_error")?; + let mut rights = HashSet::new(); + for row in rights_rows { + rights.insert(row.get::<_, String>("tr")); + } + + let age = load_user_age(client.clone(), falukant_user_id).await; + + Ok(UserProfile { + display_name, + color, + falukant_user_id: Some(falukant_user_id), + chat_user_id: Some(chat_user_id), + age, + rights, + }) +} + +pub fn is_allowed_user(user: &str, config: &ServerConfig) -> bool { + match &config.allowed_users { + Some(allowed) => allowed.contains(user), + None => true, + } +} + +pub fn is_valid_username(input: &str) -> bool { + let trimmed = input.trim(); + if trimmed.len() < 3 || trimmed.len() > 32 { + return false; + } + trimmed + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_') +} + +pub async fn save_user_color(config: &ServerConfig, chat_user_id: i32, color: &str) -> Result<(), &'static str> { + let Some(client) = config.db_client.clone() else { + return Ok(()); + }; + client + .execute( + "UPDATE chat.\"user\" SET color = $1, updated_at = NOW() WHERE id = $2", + &[&color, &chat_user_id], + ) + .await + .map_err(|_| "db_error")?; + Ok(()) +} + +pub async fn load_room_configs(config: &ServerConfig) -> Result, &'static str> { + let Some(client) = config.db_client.clone() else { + return Ok(vec![RoomMeta { + name: "lobby".to_string(), + is_public: true, + ..RoomMeta::default() + }]); + }; + + let rows = client + .query( + "SELECT r.title, r.password_hash, r.is_public, r.owner_id, r.min_age, r.max_age + FROM chat.room r + LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id", + &[], + ) + .await + .map_err(|_| "db_error")?; + + let mut rooms = Vec::new(); + for row in rows { + rooms.push(RoomMeta { + name: row.get::<_, String>("title"), + password: row.get::<_, Option>("password_hash"), + min_age: row.get::<_, Option>("min_age"), + max_age: row.get::<_, Option>("max_age"), + is_public: row.get::<_, bool>("is_public"), + owner_id: row.get::<_, Option>("owner_id"), + }); + } + + if rooms.is_empty() { + rooms.push(RoomMeta { + name: "lobby".to_string(), + is_public: true, + ..RoomMeta::default() + }); + } + Ok(rooms) +} + +async fn load_user_age(client: Arc, falukant_user_id: i32) -> Option { + let row = client + .query_opt( + "SELECT up.value + FROM community.user_param up + JOIN \"type\".user_param tp ON up.param_type_id = tp.id + WHERE up.user_id = $1 AND tp.description = 'birthdate' + LIMIT 1", + &[&falukant_user_id], + ) + .await + .ok()??; + let encrypted_or_plain = row.get::<_, Option>("value")?; + let normalized = normalize_birthdate_value(&encrypted_or_plain)?; + parse_age_from_yyyy_mm_dd(&normalized) +} + +fn parse_age_from_yyyy_mm_dd(input: &str) -> Option { + let parts = input.split('-').collect::>(); + if parts.len() != 3 { + return None; + } + let year = parts[0].parse::().ok()?; + let month = parts[1].parse::().ok()?; + let day = parts[2].parse::().ok()?; + + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?.as_secs(); + let days = (now / 86_400) as i64; + let z = days + 719_468; + let era = if z >= 0 { z } else { z - 146_096 } / 146_097; + let doe = z - era * 146_097; + let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let current_day = (doy - (153 * mp + 2) / 5 + 1) as u32; + let current_month = (mp + if mp < 10 { 3 } else { -9 }) as u32; + let current_year = (y + if current_month <= 2 { 1 } else { 0 }) as i32; + + let mut age = current_year - year; + if current_month < month || (current_month == month && current_day < day) { + age -= 1; + } + (age >= 0).then_some(age) +} + +fn normalize_birthdate_value(input: &str) -> Option { + if is_yyyy_mm_dd(input) { + return Some(input.to_string()); + } + let decrypted = decrypt_aes256_ecb_hex(input)?; + let trimmed = decrypted.trim(); + if is_yyyy_mm_dd(trimmed) { + Some(trimmed.to_string()) + } else { + None + } +} + +fn is_yyyy_mm_dd(input: &str) -> bool { + let parts = input.split('-').collect::>(); + if parts.len() != 3 { + return false; + } + parts[0].len() == 4 + && parts[1].len() == 2 + && parts[2].len() == 2 + && parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit())) +} + +fn decrypt_aes256_ecb_hex(encrypted_hex: &str) -> Option { + let secret = env::var("SECRET_KEY").unwrap_or_else(|_| "DEV_FALLBACK_SECRET".to_string()); + let mut key = [0u8; 32]; + let params = ScryptParams::new(14, 8, 1, 32).ok()?; + scrypt(secret.as_bytes(), b"salt", ¶ms, &mut key).ok()?; + + let encrypted = hex::decode(encrypted_hex).ok()?; + if encrypted.is_empty() { + return None; + } + + let cipher = Cipher::aes_256_ecb(); + let mut decrypter = Crypter::new(cipher, Mode::Decrypt, &key, None).ok()?; + decrypter.pad(true); + + let mut out = vec![0u8; encrypted.len() + cipher.block_size()]; + let mut count = decrypter.update(&encrypted, &mut out).ok()?; + count += decrypter.finalize(&mut out[count..]).ok()?; + out.truncate(count); + + String::from_utf8(out).ok() +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8e03c36 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,321 @@ +use futures_util::{SinkExt, StreamExt}; +use std::collections::HashSet; +use std::env; +use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}; +use tokio::net::{TcpListener, UnixListener}; +use tokio::sync::{mpsc, watch, RwLock}; +use tokio_tungstenite::{accept_async, tungstenite::Message}; + +mod commands; +mod db; +mod state; +mod types; +use types::{ChatState, ClientConn, ServerConfig}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let ws_addr = env::var("CHAT_WS_ADDR").unwrap_or_else(|_| "0.0.0.0:1235".to_string()); + let tcp_addr = env::var("CHAT_TCP_ADDR").unwrap_or_else(|_| "127.0.0.1:1236".to_string()); + let unix_socket = env::var("CHAT_UNIX_SOCKET").ok().filter(|s| !s.trim().is_empty()); + + let state = Arc::new(RwLock::new(ChatState::default())); + let db_client = db::connect_db_from_env().await?; + let config = Arc::new(ServerConfig { + allowed_users: db::parse_allowed_users(), + db_client, + }); + let rooms = db::load_room_configs(&config).await.unwrap_or_else(|_| { + vec![types::RoomMeta { + name: "lobby".to_string(), + is_public: true, + ..types::RoomMeta::default() + }] + }); + { + let mut guard = state.write().await; + for room in rooms { + guard.rooms.entry(room.name.clone()).or_default(); + guard.room_meta.insert(room.name.clone(), room); + } + } + let next_client_id = Arc::new(AtomicU64::new(1)); + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + let ws_listener = TcpListener::bind(&ws_addr).await?; + println!("[yourchat2] listening on ws://{}", ws_addr); + + let ws_state = Arc::clone(&state); + let ws_config = Arc::clone(&config); + let ws_next = Arc::clone(&next_client_id); + let mut ws_shutdown_rx = shutdown_rx.clone(); + let ws_task = tokio::spawn(async move { + loop { + tokio::select! { + changed = ws_shutdown_rx.changed() => { + if changed.is_ok() && *ws_shutdown_rx.borrow() { + break; + } + } + accepted = ws_listener.accept() => { + match accepted { + Ok((socket, addr)) => { + println!("[yourchat2] ws client connected: {}", addr); + let state = Arc::clone(&ws_state); + let config = Arc::clone(&ws_config); + let next = Arc::clone(&ws_next); + let shutdown = ws_shutdown_rx.clone(); + tokio::spawn(async move { + if let Err(err) = handle_ws_client(socket, state, config, next, shutdown).await { + eprintln!("[yourchat2] ws client error: {err}"); + } + }); + } + Err(err) => eprintln!("[yourchat2] ws accept error: {err}"), + } + } + } + } + }); + + let tcp_listener = TcpListener::bind(&tcp_addr).await?; + println!("[yourchat2] listening on tcp://{}", tcp_addr); + + let tcp_state = Arc::clone(&state); + let tcp_config = Arc::clone(&config); + let tcp_next = Arc::clone(&next_client_id); + let mut tcp_shutdown_rx = shutdown_rx.clone(); + let tcp_task = tokio::spawn(async move { + loop { + tokio::select! { + changed = tcp_shutdown_rx.changed() => { + if changed.is_ok() && *tcp_shutdown_rx.borrow() { + break; + } + } + accepted = tcp_listener.accept() => { + match accepted { + Ok((socket, addr)) => { + println!("[yourchat2] tcp client connected: {}", addr); + let state = Arc::clone(&tcp_state); + let config = Arc::clone(&tcp_config); + let next = Arc::clone(&tcp_next); + let shutdown = tcp_shutdown_rx.clone(); + tokio::spawn(async move { + if let Err(err) = handle_client(socket, state, config, next, shutdown).await { + eprintln!("[yourchat2] client error: {err}"); + } + }); + } + Err(err) => eprintln!("[yourchat2] accept error: {err}"), + } + } + } + } + }); + + let unix_task = if let Some(socket_path) = unix_socket.clone() { + let path = Path::new(&socket_path); + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + if path.exists() { + tokio::fs::remove_file(path).await?; + } + + let listener = UnixListener::bind(path)?; + println!("[yourchat2] listening on unix://{}", socket_path); + + let unix_state = Arc::clone(&state); + let unix_config = Arc::clone(&config); + let unix_next = Arc::clone(&next_client_id); + let mut unix_shutdown_rx = shutdown_rx.clone(); + Some(tokio::spawn(async move { + loop { + tokio::select! { + changed = unix_shutdown_rx.changed() => { + if changed.is_ok() && *unix_shutdown_rx.borrow() { + break; + } + } + accepted = listener.accept() => { + match accepted { + Ok((socket, _addr)) => { + let state = Arc::clone(&unix_state); + let config = Arc::clone(&unix_config); + let next = Arc::clone(&unix_next); + let shutdown = unix_shutdown_rx.clone(); + tokio::spawn(async move { + if let Err(err) = handle_client(socket, state, config, next, shutdown).await { + eprintln!("[yourchat2] unix client error: {err}"); + } + }); + } + Err(err) => eprintln!("[yourchat2] unix accept error: {err}"), + } + } + } + } + })) + } else { + None + }; + + tokio::signal::ctrl_c().await?; + println!("[yourchat2] shutdown requested"); + let _ = shutdown_tx.send(true); + + let _ = ws_task.await; + let _ = tcp_task.await; + if let Some(task) = unix_task { + let _ = task.await; + if let Some(path) = unix_socket { + let _ = tokio::fs::remove_file(path).await; + } + } + + println!("[yourchat2] stopped"); + Ok(()) +} + +async fn handle_client( + stream: S, + state: Arc>, + config: Arc, + next_client_id: Arc, + mut shutdown_rx: watch::Receiver, +) -> Result<(), Box> +where + S: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + let client_id = next_client_id.fetch_add(1, Ordering::Relaxed); + let default_name = format!("Guest-{client_id}"); + let (read_half, mut write_half) = tokio::io::split(stream); + let (tx, mut rx) = mpsc::unbounded_channel::(); + + { + let mut guard = state.write().await; + guard.clients.insert( + client_id, + ClientConn { + user_name: default_name.clone(), + room: String::new(), + color: None, + token: None, + falukant_user_id: None, + chat_user_id: None, + age: None, + rights: HashSet::new(), + logged_in: false, + tx: tx.clone(), + }, + ); + } + + let writer_task = tokio::spawn(async move { + while let Some(msg) = rx.recv().await { + if write_half.write_all(msg.as_bytes()).await.is_err() { + break; + } + if write_half.write_all(b"\n").await.is_err() { + break; + } + } + }); + + let mut lines = BufReader::new(read_half).lines(); + loop { + tokio::select! { + changed = shutdown_rx.changed() => { + if changed.is_ok() && *shutdown_rx.borrow() { + break; + } + } + line = lines.next_line() => { + match line? { + Some(raw) => { + commands::process_text_command(client_id, &raw, Arc::clone(&state), Arc::clone(&config)).await; + } + None => break, + } + } + } + } + + state::disconnect_client(client_id, state).await; + writer_task.abort(); + Ok(()) +} + +async fn handle_ws_client( + socket: tokio::net::TcpStream, + state: Arc>, + config: Arc, + next_client_id: Arc, + mut shutdown_rx: watch::Receiver, +) -> Result<(), Box> { + let ws_stream = accept_async(socket).await?; + let (mut ws_write, mut ws_read) = ws_stream.split(); + let client_id = next_client_id.fetch_add(1, Ordering::Relaxed); + let default_name = format!("Guest-{client_id}"); + let (tx, mut rx) = mpsc::unbounded_channel::(); + + { + let mut guard = state.write().await; + guard.clients.insert( + client_id, + ClientConn { + user_name: default_name, + room: String::new(), + color: None, + token: None, + falukant_user_id: None, + chat_user_id: None, + age: None, + rights: HashSet::new(), + logged_in: false, + tx: tx.clone(), + }, + ); + } + + let writer_task = tokio::spawn(async move { + while let Some(msg) = rx.recv().await { + if ws_write.send(Message::Text(msg.into())).await.is_err() { + break; + } + } + }); + + loop { + tokio::select! { + changed = shutdown_rx.changed() => { + if changed.is_ok() && *shutdown_rx.borrow() { + break; + } + } + incoming = ws_read.next() => { + match incoming { + Some(Ok(Message::Text(text))) => { + commands::process_text_command(client_id, &text, Arc::clone(&state), Arc::clone(&config)).await; + } + Some(Ok(Message::Binary(bin))) => { + if let Ok(text) = std::str::from_utf8(&bin) { + commands::process_text_command(client_id, text, Arc::clone(&state), Arc::clone(&config)).await; + } + } + Some(Ok(Message::Ping(_))) => {} + Some(Ok(Message::Close(_))) => break, + Some(Ok(_)) => {} + Some(Err(_)) | None => break, + } + } + } + } + + state::disconnect_client(client_id, state).await; + writer_task.abort(); + Ok(()) +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..55d1512 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,154 @@ +use crate::types::{ChatState, ClientId, Command, RoomInfo}; +use serde_json::{json, Value}; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub async fn authorize(client_id: ClientId, command: &Command, state: Arc>) -> bool { + let auth_error = { + let guard = state.read().await; + let Some(client) = guard.clients.get(&client_id) else { + return false; + }; + if !client.logged_in { + Some("not_initialized") + } else { + match (&client.token, &command.token) { + (Some(expected), Some(got)) if expected == got => None, + (Some(_), Some(_)) => Some("invalid_token"), + (Some(_), None) => Some("missing_token"), + _ => None, + } + } + }; + if let Some(message) = auth_error { + send_to_client(client_id, state, json!({"type":"error","message": message})).await; + false + } else { + true + } +} + +pub async fn send_room_list(client_id: ClientId, state: Arc>) { + let list = { + let guard = state.read().await; + let mut rooms: Vec = guard + .rooms + .iter() + .map(|(name, members)| RoomInfo { + name: name.clone(), + users: members.len(), + }) + .collect(); + rooms.sort_by(|a, b| a.name.cmp(&b.name)); + rooms + }; + send_to_client(client_id, state, json!({"type":3, "message": list})).await; +} + +pub async fn send_user_list(client_id: ClientId, state: Arc>) { + let users = { + let guard = state.read().await; + let Some(client) = guard.clients.get(&client_id) else { + return; + }; + if client.room.is_empty() { + Vec::::new() + } else if let Some(members) = guard.rooms.get(&client.room) { + members + .iter() + .filter_map(|id| guard.clients.get(id)) + .map(|u| json!({"name": u.user_name, "color": u.color})) + .collect::>() + } else { + Vec::::new() + } + }; + send_to_client(client_id, state, json!({"type":2, "message": users})).await; +} + +pub async fn send_to_client(client_id: ClientId, state: Arc>, payload: Value) { + let msg = payload.to_string(); + let tx = { + let guard = state.read().await; + guard.clients.get(&client_id).map(|c| c.tx.clone()) + }; + if let Some(tx) = tx { + let _ = tx.send(msg); + } +} + +pub async fn broadcast_all(state: Arc>, payload: Value) { + let msg = payload.to_string(); + let targets = { + let guard = state.read().await; + guard.clients.values().map(|c| c.tx.clone()).collect::>() + }; + for tx in targets { + let _ = tx.send(msg.clone()); + } +} + +pub async fn broadcast_room( + room: &str, + state: Arc>, + payload: Value, + exclude: Option, +) { + let msg = payload.to_string(); + let targets = { + let guard = state.read().await; + let Some(members) = guard.rooms.get(room) else { + return; + }; + members + .iter() + .filter_map(|id| { + if exclude.is_some() && exclude == Some(*id) { + return None; + } + guard.clients.get(id).map(|c| c.tx.clone()) + }) + .collect::>() + }; + for tx in targets { + let _ = tx.send(msg.clone()); + } +} + +pub async fn disconnect_client(client_id: ClientId, state: Arc>) { + let (old_room, old_name, token, was_logged_in) = { + let mut guard = state.write().await; + let Some(client) = guard.clients.remove(&client_id) else { + return; + }; + + if !client.room.is_empty() { + if let Some(members) = guard.rooms.get_mut(&client.room) { + members.remove(&client_id); + if members.is_empty() { + guard.rooms.remove(&client.room); + } + } + } + + (client.room, client.user_name, client.token, client.logged_in) + }; + + if let Some(token) = token { + let mut guard = state.write().await; + guard.tokens.remove(&token); + if was_logged_in { + guard.logged_in_names.remove(&old_name); + } + } + + if !old_room.is_empty() { + broadcast_room( + &old_room, + state, + json!({"type":"system","message": format!("{old_name} disconnected")}), + Some(client_id), + ) + .await; + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..e473aed --- /dev/null +++ b/src/types.rs @@ -0,0 +1,89 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio_postgres::Client as PgClient; + +pub(crate) type ClientId = u64; + +#[derive(Clone)] +pub(crate) struct ClientConn { + pub(crate) user_name: String, + pub(crate) room: String, + pub(crate) color: Option, + pub(crate) token: Option, + pub(crate) falukant_user_id: Option, + pub(crate) chat_user_id: Option, + pub(crate) age: Option, + pub(crate) rights: HashSet, + pub(crate) logged_in: bool, + pub(crate) tx: mpsc::UnboundedSender, +} + +#[derive(Default)] +pub(crate) struct ChatState { + pub(crate) clients: HashMap, + pub(crate) rooms: HashMap>, + pub(crate) tokens: HashMap, + pub(crate) logged_in_names: HashSet, + pub(crate) dice_games: HashMap, + pub(crate) room_meta: HashMap, +} + +#[derive(Clone, Default)] +pub(crate) struct ServerConfig { + pub(crate) allowed_users: Option>, + pub(crate) db_client: Option>, +} + +#[derive(Debug)] +pub(crate) struct UserProfile { + pub(crate) display_name: String, + pub(crate) color: Option, + pub(crate) falukant_user_id: Option, + pub(crate) chat_user_id: Option, + pub(crate) age: Option, + pub(crate) rights: HashSet, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct Command { + #[serde(rename = "type")] + pub(crate) cmd_type: Value, + pub(crate) token: Option, + pub(crate) name: Option, + pub(crate) room: Option, + pub(crate) message: Option, + pub(crate) value: Option, + pub(crate) password: Option, + pub(crate) rounds: Option, + pub(crate) to: Option, + #[serde(rename = "userName")] + pub(crate) user_name: Option, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RoomInfo { + pub(crate) name: String, + pub(crate) users: usize, +} + +#[derive(Clone, Default)] +pub(crate) struct DiceGame { + pub(crate) running: bool, + pub(crate) current_round: i32, + pub(crate) total_rounds: i32, + pub(crate) rolled_this_round: HashMap, + pub(crate) total_scores: HashMap, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct RoomMeta { + pub(crate) name: String, + pub(crate) password: Option, + pub(crate) min_age: Option, + pub(crate) max_age: Option, + pub(crate) is_public: bool, + pub(crate) owner_id: Option, +} diff --git a/yourchat2.service b/yourchat2.service new file mode 100644 index 0000000..c701815 --- /dev/null +++ b/yourchat2.service @@ -0,0 +1,18 @@ +[Unit] +Description=yourchat2 Rust chat daemon +After=network.target + +[Service] +Type=simple +User=torsten +WorkingDirectory=/home/torsten/Programs/yourchat2 +Environment=CHAT_WS_ADDR=0.0.0.0:1235 +Environment=CHAT_TCP_ADDR=127.0.0.1:1236 +# Optional: +# Environment=CHAT_UNIX_SOCKET=/run/yourchat2/yourchat2.sock +ExecStart=/home/torsten/Programs/yourchat2/target/release/yourchat2 +Restart=always +RestartSec=2 + +[Install] +WantedBy=multi-user.target