diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7a71e19 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +target +src/frontend/dist + +.vscode diff --git a/.gitignore b/.gitignore index 073664d..7a71e19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/target +target +src/frontend/dist .vscode diff --git a/Cargo.lock b/Cargo.lock index c6ddacd..be02333 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,7 +47,7 @@ checksum = "d1eb7c4fcde1858a6796c18a729b661346d38e05a207e2d9028bce822fc20283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -72,11 +72,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21cc1548309245035eb18aa7f0967da6bc65587005170c56e6ef2788a4cf3f4e" dependencies = [ "include_dir", - "itertools", + "itertools 0.10.5", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -101,13 +101,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -231,6 +231,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "backend" +version = "0.1.0" +dependencies = [ + "axum 0.8.1", + "eyre", + "futures", + "hex", + "hmac", + "http-body-util", + "mongodb", + "once_cell", + "reqwest 0.12.12", + "retainer", + "serde", + "serde_json", + "sha2", + "teloxide", + "tokio", + "tower 0.5.2", + "tower-http 0.6.2", + "tracing", + "tracing-subscriber", + "twitch_api", + "twitch_oauth2", + "url", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -264,6 +292,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -272,9 +309,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bitvec" @@ -332,15 +369,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.15" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "shlex", ] @@ -364,6 +401,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-random" version = "0.1.18" @@ -476,7 +523,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -498,7 +545,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -525,7 +572,7 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -536,7 +583,7 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -549,7 +596,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -571,7 +618,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -585,9 +632,9 @@ dependencies = [ [[package]] name = "either" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" @@ -607,7 +654,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -688,6 +735,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "frontend" +version = "0.1.0" +dependencies = [ + "gloo-net 0.6.0", + "stylist", + "web-sys", + "yew", +] + [[package]] name = "funty" version = "2.0.0" @@ -750,7 +807,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -800,8 +857,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -822,6 +881,208 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net 0.5.0", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom 0.2.15", + "gloo-events", + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 1.2.0", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode", + "futures", + "gloo-utils", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "h2" version = "0.3.26" @@ -884,6 +1145,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" @@ -907,7 +1174,7 @@ dependencies = [ "ipnet", "once_cell", "rand", - "thiserror", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -930,7 +1197,7 @@ dependencies = [ "rand", "resolv-conf", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -1012,10 +1279,16 @@ dependencies = [ ] [[package]] -name = "httparse" -version = "1.10.0" +name = "http-range-header" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1081,7 +1354,7 @@ dependencies = [ "rustls 0.23.23", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.1", + "tokio-rustls 0.26.2", "tower-service", ] @@ -1271,7 +1544,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1301,6 +1574,26 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "implicit-clone" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e4c5423e5b38b6f175b42561a08f6ddbc8ed3a3275f48eb350ba2dfe9a6b60" +dependencies = [ + "implicit-clone-derive", + "indexmap 2.7.1", +] + +[[package]] +name = "implicit-clone-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" +dependencies = [ + "quote", + "syn 2.0.99", +] + [[package]] name = "include_dir" version = "0.7.4" @@ -1348,6 +1641,18 @@ dependencies = [ "serde", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -1376,10 +1681,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.14" +name = "itertools" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" @@ -1421,6 +1735,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +dependencies = [ + "proc-macro2", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -1455,7 +1778,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1469,7 +1792,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1480,7 +1803,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1491,7 +1814,7 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1544,6 +1867,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.5" @@ -1604,7 +1933,7 @@ dependencies = [ "stringprep", "strsim 0.11.1", "take_mut", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-rustls 0.24.1", "tokio-util", @@ -1622,7 +1951,7 @@ dependencies = [ "macro_magic", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1642,6 +1971,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1667,6 +2006,16 @@ 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", + "libc", +] + [[package]] name = "object" version = "0.36.7" @@ -1688,7 +2037,7 @@ version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "cfg-if", "foreign-types", "libc", @@ -1705,7 +2054,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1772,22 +2121,22 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1803,10 +2152,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.31" +name = "pinned" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror 1.0.69", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "powerfmt" @@ -1823,6 +2183,26 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +dependencies = [ + "proc-macro2", + "syn 2.0.99", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1849,9 +2229,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -1864,9 +2244,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ "proc-macro2", ] @@ -1922,7 +2302,7 @@ version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -2036,9 +2416,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.11" +version = "0.17.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73" +checksum = "ed9b823fa29b721a59671b41d6b06e66b29e0628e207e8b1c3ceeda701ec928d" dependencies = [ "cc", "cfg-if", @@ -2079,7 +2459,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", @@ -2158,15 +2538,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" @@ -2199,7 +2579,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation", "core-foundation-sys", "libc", @@ -2218,9 +2598,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" @@ -2231,6 +2611,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_bytes" version = "0.11.16" @@ -2248,14 +2639,14 @@ checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "indexmap 2.7.1", "itoa", @@ -2266,9 +2657,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" dependencies = [ "itoa", "serde", @@ -2335,7 +2726,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -2438,6 +2829,52 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "stylist" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684929eeaa18b44296533430c1453f6ea0ebff8cc7182185657fc7887ad5b9d4" +dependencies = [ + "fastrand", + "instant", + "once_cell", + "serde", + "stylist-core", + "stylist-macros", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "stylist-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c59bd4f35e91ac75facd4b904916abddcbfca73ce70674e5babc47617dc50f7" +dependencies = [ + "nom", + "once_cell", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", +] + +[[package]] +name = "stylist-macros" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a93326fb80057248f81d95d9c648eab0338f353ceae4263a5e345de836fa9" +dependencies = [ + "itertools 0.11.0", + "litrs", + "log", + "nom", + "proc-macro-error", + "proc-macro2", + "quote", + "stylist-core", + "syn 2.0.99", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2457,9 +2894,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" dependencies = [ "proc-macro2", "quote", @@ -2489,7 +2926,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -2509,7 +2946,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation", "system-configuration-sys 0.6.0", ] @@ -2552,27 +2989,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "telegram-twitch-notifier" -version = "0.1.0" -dependencies = [ - "axum 0.8.1", - "eyre", - "futures", - "http-body-util", - "mongodb", - "once_cell", - "reqwest 0.12.12", - "retainer", - "teloxide", - "tokio", - "tower-http 0.6.2", - "tracing", - "tracing-subscriber", - "twitch_api", - "twitch_oauth2", -] - [[package]] name = "teloxide" version = "0.13.0" @@ -2594,7 +3010,7 @@ dependencies = [ "serde_json", "teloxide-core", "teloxide-macros", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-util", @@ -2626,7 +3042,7 @@ dependencies = [ "serde_with 1.14.0", "take_mut", "takecell", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", "url", @@ -2666,7 +3082,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -2677,7 +3102,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", ] [[package]] @@ -2780,7 +3216,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -2805,9 +3241,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls 0.23.23", "tokio", @@ -2838,6 +3274,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokise" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf97738ce15b9e9cc1671ea29b0f6c56538719e1a092d19cc2134bf144e40e" +dependencies = [ + "futures", + "gloo", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.7.1", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2871,7 +3341,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "bytes", "http 1.2.0", "http-body 1.0.1", @@ -2888,11 +3358,20 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "bytes", + "futures-util", "http 1.2.0", "http-body 1.0.1", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -2930,7 +3409,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -2994,7 +3473,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "sha2", - "thiserror", + "thiserror 1.0.69", "tower 0.4.13", "twitch_oauth2", "twitch_types", @@ -3019,7 +3498,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "twitch_types", "url", ] @@ -3065,9 +3544,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" @@ -3194,7 +3673,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "wasm-bindgen-shared", ] @@ -3229,7 +3708,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3493,6 +3972,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -3509,7 +3997,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -3533,6 +4021,43 @@ dependencies = [ "tap", ] +[[package]] +name = "yew" +version = "0.21.0" +source = "git+https://github.com/yewstack/yew/#a3a3ffc6d8804b7136d23aa923fb10df2e61c4ac" +dependencies = [ + "console_error_panic_hook", + "futures", + "gloo", + "implicit-clone", + "indexmap 2.7.1", + "js-sys", + "rustversion", + "serde", + "slab", + "thiserror 2.0.12", + "tokio", + "tokise", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-macro" +version = "0.21.0" +source = "git+https://github.com/yewstack/yew/#a3a3ffc6d8804b7136d23aa923fb10df2e61c4ac" +dependencies = [ + "once_cell", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "yoke" version = "0.7.5" @@ -3553,7 +4078,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "synstructure", ] @@ -3575,7 +4100,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -3595,7 +4120,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "synstructure", ] @@ -3624,5 +4149,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] diff --git a/Cargo.toml b/Cargo.toml index cf86a60..1030cf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,3 @@ -[package] -name = "telegram-twitch-notifier" -version = "0.1.0" -edition = "2024" - -[dependencies] -once_cell = "1.20.3" -eyre = { version = "0.6" } - -tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] } -futures = "0.3.31" - -teloxide = { version = "0.13.0", features = ["macros", "webhooks-axum", "cache-me", "throttle"] } -twitch_api = { version = "0.7.0", features = ["reqwest", "helix", "eventsub", "hmac"] } -twitch_oauth2 = "0.15.1" - -axum = { version = "0.8.1", features = ["http2"] } -tower-http = { version = "0.6.2", features = ["trace"] } -http-body-util = "0.1.2" - -retainer = "0.3.0" - -reqwest = "0.12.12" - -tracing = "0.1.37" -tracing-subscriber = "0.3.16" -mongodb = "3.2.1" +[workspace] +members = ["src/backend", "src/frontend"] +resolver = "2" diff --git a/docker/Dockerfile b/docker/Dockerfile index 5dc868f..ba524e3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,15 +1,30 @@ -FROM rust:bullseye AS builder +FROM rust:bullseye AS builder_backend WORKDIR /app COPY . . -RUN cargo build --release --bin telegram-twitch-notifier +RUN cargo build --release --bin backend + + +FROM rust:bullseye AS builder_frontend + +RUN rustup target add wasm32-unknown-unknown + +RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash +RUN cargo binstall trunk wasm-bindgen-cli + +WORKDIR /app + +COPY . . + +WORKDIR /app/src/frontend + +RUN trunk build --release FROM debian:bullseye-slim - RUN apt-get update \ && apt-get install -y openssl ca-certificates curl jq \ && rm -rf /var/lib/apt/lists/* @@ -18,6 +33,7 @@ RUN update-ca-certificates WORKDIR /app -COPY --from=builder /app/target/release/telegram-twitch-notifier /usr/local/bin +COPY --from=builder_backend /app/target/release/backend /usr/local/bin +COPY --from=builder_frontend /app/src/frontend/dist /app/static -CMD ["/usr/local/bin/telegram-twitch-notifier"] +CMD ["/usr/local/bin/backend"] diff --git a/src/backend/Cargo.toml b/src/backend/Cargo.toml new file mode 100644 index 0000000..cec134f --- /dev/null +++ b/src/backend/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2024" + +[dependencies] +once_cell = "1.20.3" +eyre = { version = "0.6" } + +tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] } +futures = "0.3.31" + +teloxide = { version = "0.13.0", features = ["macros", "webhooks-axum", "cache-me", "throttle"] } +twitch_api = { version = "0.7.0", features = ["reqwest", "helix", "eventsub", "hmac"] } +twitch_oauth2 = "0.15.1" + +axum = { version = "0.8.1", features = ["http2"] } +tower = { version = "0.5.2" } +tower-http = { version = "0.6.2", features = ["fs", "trace"] } +http-body-util = "0.1.2" + +retainer = "0.3.0" + +reqwest = "0.12.12" + +tracing = "0.1.37" +tracing-subscriber = "0.3.16" +mongodb = "3.2.1" + +url = "2.5.4" +hmac = "0.12.1" +sha2 = "0.10.8" +hex = "0.4.3" + +serde = { version = "1.0.218", features = ["derive"] } +serde_json = "1.0.140" diff --git a/src/config.rs b/src/backend/src/config.rs similarity index 79% rename from src/config.rs rename to src/backend/src/config.rs index 204857a..9f17635 100644 --- a/src/config.rs +++ b/src/backend/src/config.rs @@ -1,11 +1,15 @@ use once_cell::sync::Lazy; pub struct Config { - pub bot_token: String, + // Telegram + pub telegram_bot_token: String, pub telegram_webhook_url: String, pub telegram_webhook_port: u16, + pub telegram_mini_app_port: u16, + + // Twitch pub twitch_client_id: String, pub twitch_client_secret: String, @@ -14,13 +18,14 @@ pub struct Config { pub twitch_webhook_url: String, pub twitch_webhook_port: u16, + // Common pub mongodb_connection_string: String, } impl Config { fn load() -> Self { Self { - bot_token: std::env::var("BOT_TOKEN").expect("BOT_TOKEN is not set"), + telegram_bot_token: std::env::var("BOT_TOKEN").expect("BOT_TOKEN is not set"), telegram_webhook_url: std::env::var("TELEGRAM_WEBHOOK_URL") .expect("TELEGRAM_WEBHOOK_URL is not set"), @@ -28,6 +33,10 @@ impl Config { .expect("TELEGRAM_WEBHOOK_PORT is not set") .parse() .expect("TELEGRAM_WEBHOOK_PORT is not a valid u16"), + telegram_mini_app_port: std::env::var("TELEGRAM_MINI_APP_PORT") + .expect("TELEGRAM_MINI_APP_PORT is not set") + .parse() + .expect("TELEGRAM_MINI_APP_PORT is not a valid u16"), twitch_client_id: std::env::var("TWITCH_CLIENT_ID") .expect("TWITCH_CLIENT_ID is not set"), diff --git a/src/main.rs b/src/backend/src/main.rs similarity index 95% rename from src/main.rs rename to src/backend/src/main.rs index fa220c4..c915f1a 100644 --- a/src/main.rs +++ b/src/backend/src/main.rs @@ -1,7 +1,9 @@ pub mod config; +pub mod repositories; pub mod subscription_manager; pub mod telegram_bot; pub mod twitch_webhook; +pub mod web_app; use std::sync::Arc; diff --git a/src/backend/src/repositories/mod.rs b/src/backend/src/repositories/mod.rs new file mode 100644 index 0000000..338c636 --- /dev/null +++ b/src/backend/src/repositories/mod.rs @@ -0,0 +1 @@ +pub mod subscriptions; diff --git a/src/backend/src/repositories/subscriptions.rs b/src/backend/src/repositories/subscriptions.rs new file mode 100644 index 0000000..cdf4ee1 --- /dev/null +++ b/src/backend/src/repositories/subscriptions.rs @@ -0,0 +1,122 @@ +use futures::StreamExt as _; +use mongodb::{ + Client, Collection, + bson::{Document, doc, oid::ObjectId}, +}; +use serde::Serialize; + +use crate::config::CONFIG; + +pub struct SubscriptionRepository {} + +#[derive(Serialize)] +pub struct Subscription { + pub id: ObjectId, + pub streamer: String, + pub telegram_user_id: u64, +} + +impl From for Subscription { + fn from(doc: Document) -> Self { + Self { + id: doc.get_object_id("_id").unwrap(), + streamer: doc.get_str("streamer").unwrap().to_string(), + telegram_user_id: doc.get_i64("telegram_user_id").unwrap() as u64, + } + } +} + +impl SubscriptionRepository { + async fn get_collection() -> mongodb::error::Result> { + let client = Client::with_uri_str(CONFIG.mongodb_connection_string.clone()).await?; + + let database = client.database("telegram-twitch-notifier"); + + Ok(database.collection("subscriptions")) + } + + pub async fn get_by_id(id: ObjectId) -> mongodb::error::Result> { + let collection = Self::get_collection().await?; + + let doc = collection.find_one(doc! { "_id": id }).await?; + + match doc { + Some(doc) => Ok(Some(Subscription::from(doc))), + None => Ok(None), + } + } + + pub async fn get_or_create( + streamer: String, + telegram_user_id: u64, + ) -> mongodb::error::Result { + let collection = Self::get_collection().await?; + + let existing = collection + .find_one(doc! { + "streamer": streamer.clone(), + "telegram_user_id": telegram_user_id as i64, + }) + .await?; + + if let Some(v) = existing { + return Ok(Subscription::from(v)); + } + + let created = collection + .insert_one(doc! { + "streamer": streamer, + "telegram_user_id": telegram_user_id as i64, + }) + .await?; + + let inserted_id = created.inserted_id.as_object_id().unwrap(); + + Ok(SubscriptionRepository::get_by_id(inserted_id.clone()) + .await? + .unwrap()) + } + + pub async fn delete(streamer: String, telegram_user_id: u64) -> mongodb::error::Result<()> { + let collection = Self::get_collection().await?; + + collection + .delete_one(doc! { + "streamer": streamer, + "telegram_user_id": telegram_user_id as i64, + }) + .await?; + + Ok(()) + } + + pub async fn all_by_user(telegram_user_id: u64) -> mongodb::error::Result> { + let collection = Self::get_collection().await?; + + let mut subs = collection + .find(doc! { "telegram_user_id": telegram_user_id as i64 }) + .await?; + + let mut result = Vec::new(); + + while let Some(sub) = subs.next().await { + result.push(Subscription::from(sub?)); + } + + Ok(result) + } + + pub async fn all() -> mongodb::error::Result> { + let collection = Self::get_collection().await?; + + let mut subs = collection.find(doc! {}).await?; + + let mut result = Vec::new(); + + while let Some(sub) = subs.next().await { + result.push(Subscription::from(sub?)); + } + + Ok(result) + } +} diff --git a/src/backend/src/subscription_manager.rs b/src/backend/src/subscription_manager.rs new file mode 100644 index 0000000..c950deb --- /dev/null +++ b/src/backend/src/subscription_manager.rs @@ -0,0 +1,60 @@ +use std::collections::{HashMap, HashSet}; + +use tokio::sync::RwLock; + +use crate::repositories::subscriptions::SubscriptionRepository; + +pub struct SubscriptionManager { + pub subscriptions: RwLock>>, +} + +impl SubscriptionManager { + pub fn new() -> Self { + Self { + subscriptions: RwLock::new(HashMap::new()), + } + } + + pub async fn load(&self) -> mongodb::error::Result<()> { + let subs = SubscriptionRepository::all().await?; + + for sub in subs { + self.subscriptions + .write() + .await + .entry(sub.streamer.clone()) + .or_insert(HashSet::new()) + .insert(sub.telegram_user_id); + } + + Ok(()) + } + + pub async fn subscribe(&self, telegram_user_id: u64, username: String) { + tracing::debug!("Subscribing {} to {}", telegram_user_id, username); + + let inserted = self + .subscriptions + .write() + .await + .entry(username.clone()) + .or_insert(HashSet::new()) + .insert(telegram_user_id); + + if !inserted { + return; + } + + SubscriptionRepository::get_or_create(username, telegram_user_id) + .await + .expect("Failed to create subscription"); + } + + pub async fn unsubscribe(&self, telegram_user_id: u64, username: String) { + tracing::debug!("Unsubscribing {} from {}", telegram_user_id, username); + + SubscriptionRepository::delete(username, telegram_user_id) + .await + .expect("Failed to delete subscription"); + } +} diff --git a/src/telegram_bot.rs b/src/backend/src/telegram_bot.rs similarity index 98% rename from src/telegram_bot.rs rename to src/backend/src/telegram_bot.rs index f55bb3d..3d6aa63 100644 --- a/src/telegram_bot.rs +++ b/src/backend/src/telegram_bot.rs @@ -127,7 +127,7 @@ pub async fn get_commands() -> Vec { } pub fn get_telegram_bot() -> Bot { - OriginBot::new(CONFIG.bot_token.clone()) + OriginBot::new(CONFIG.telegram_bot_token.clone()) .throttle(Limits::default()) .cache_me() } diff --git a/src/twitch_webhook.rs b/src/backend/src/twitch_webhook.rs similarity index 99% rename from src/twitch_webhook.rs rename to src/backend/src/twitch_webhook.rs index 0ff85d3..c2c857e 100644 --- a/src/twitch_webhook.rs +++ b/src/backend/src/twitch_webhook.rs @@ -255,7 +255,7 @@ impl TwitchWebhookServer { } } - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } } diff --git a/src/backend/src/web_app/auth.rs b/src/backend/src/web_app/auth.rs new file mode 100644 index 0000000..f4287ae --- /dev/null +++ b/src/backend/src/web_app/auth.rs @@ -0,0 +1,74 @@ +use axum::{ + extract::Request, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use futures::future::BoxFuture; +use tower::{Layer, Service}; + +use crate::config::CONFIG; + +use super::validation::validate; + +#[derive(Clone)] +pub struct UserId(pub u64); + +#[derive(Clone)] +pub struct AuthLayer; + +impl Layer for AuthLayer { + type Service = AuthMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + AuthMiddleware { inner } + } +} + +#[derive(Clone)] +pub struct AuthMiddleware { + inner: S, +} + +impl Service for AuthMiddleware +where + S: Service + Clone + Send + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + let init_data = { + let header = req.headers().get("X-Init-Data"); + + match header { + Some(header) => { + let header = header.to_str().unwrap(); + header + } + None => return Box::pin(async { Ok(StatusCode::UNAUTHORIZED.into_response()) }), + } + }; + + let user_id = match validate(init_data, &CONFIG.telegram_bot_token) { + Some(user_id) => user_id, + None => return Box::pin(async { Ok(StatusCode::UNAUTHORIZED.into_response()) }), + }; + + req.extensions_mut().insert(UserId(user_id)); + + let future = self.inner.call(req); + Box::pin(async move { + let response: Response = future.await?; + Ok(response) + }) + } +} diff --git a/src/backend/src/web_app/mod.rs b/src/backend/src/web_app/mod.rs new file mode 100644 index 0000000..784ad8f --- /dev/null +++ b/src/backend/src/web_app/mod.rs @@ -0,0 +1,33 @@ +pub mod auth; +pub mod subscriptions; +pub mod validation; + +use std::net::SocketAddr; + +use axum::Router; +use subscriptions::get_api_router; +use tokio::net::TcpListener; +use tower_http::services::ServeFile; + +use crate::config::CONFIG; + +fn get_app() -> Router { + Router::new() + .nest_service("/assets", ServeFile::new("assets")) + .nest("/api", get_api_router()) + .fallback_service(ServeFile::new("assets/index.html")) +} + +pub async fn start_web_app() -> Result<(), eyre::Report> { + let app = get_app(); + + let address = SocketAddr::new([0, 0, 0, 0].into(), CONFIG.telegram_mini_app_port); + + let _ = axum::serve( + TcpListener::bind(address).await.unwrap(), + app.into_make_service(), + ) + .await; + + Ok(()) +} diff --git a/src/backend/src/web_app/subscriptions.rs b/src/backend/src/web_app/subscriptions.rs new file mode 100644 index 0000000..9ef3489 --- /dev/null +++ b/src/backend/src/web_app/subscriptions.rs @@ -0,0 +1,47 @@ +use axum::{ + Extension, Json, Router, + extract::Path, + http::StatusCode, + response::IntoResponse, + routing::{delete, get, post}, +}; + +use crate::repositories::subscriptions::SubscriptionRepository; + +use super::auth::{AuthLayer, UserId}; + +async fn get_subscriptions(Extension(UserId(user_id)): Extension) -> impl IntoResponse { + let subs = SubscriptionRepository::all_by_user(user_id).await.unwrap(); + + Json(subs).into_response() +} + +async fn create_subscription( + Path(streamer): Path, + Extension(UserId(user_id)): Extension, +) -> impl IntoResponse { + let sub = SubscriptionRepository::get_or_create(streamer, user_id) + .await + .unwrap(); + + Json(sub).into_response() +} + +async fn delete_subscription( + Path(streamer): Path, + Extension(UserId(user_id)): Extension, +) -> impl IntoResponse { + SubscriptionRepository::delete(streamer, user_id) + .await + .unwrap(); + + StatusCode::NO_CONTENT +} + +pub fn get_api_router() -> Router { + Router::new() + .route("/subscriptions/", get(get_subscriptions)) + .route("/subscriptions/:streamer/", post(create_subscription)) + .route("/subscriptions/:streamer/", delete(delete_subscription)) + .layer(AuthLayer) +} diff --git a/src/backend/src/web_app/validation.rs b/src/backend/src/web_app/validation.rs new file mode 100644 index 0000000..633d74c --- /dev/null +++ b/src/backend/src/web_app/validation.rs @@ -0,0 +1,89 @@ +use hmac::{Hmac, Mac}; +use serde::Deserialize; +use sha2::Sha256; +use url::form_urlencoded; + +type HmacSha256 = Hmac; + +#[derive(Debug, Clone, Deserialize)] +pub struct User { + pub id: u64, +} + +pub fn parse(init_data: &str) -> Option { + if init_data.is_empty() { + return None; + } + + if init_data.contains(';') || !init_data.contains('=') { + return None; + } + + let pairs = form_urlencoded::parse(init_data.as_bytes()); + + for (key, value) in pairs { + if key == "user" { + let user_data = serde_json::from_str::(&value).ok(); + + return match user_data { + Some(user) => Some(user.id), + None => None, + }; + } + } + + None +} + +fn extract_hash(init_data: &str) -> Option<(String, String)> { + let (base_data, hash) = if let Some(pos) = init_data.find("&hash=") { + let (base, hash_part) = init_data.split_at(pos); + let hash = &hash_part[6..]; // Skip "&hash=" + (base.to_string(), hash.to_string()) + } else { + return None; + }; + + if !hash.chars().all(|c| c.is_ascii_hexdigit()) || hash.len() != 64 { + return None; + } + + Some((base_data, hash)) +} + +fn sign(data: &str, token: &str) -> Result { + let secret_key = { + let mut mac = HmacSha256::new_from_slice(token.as_bytes()).unwrap(); + mac.update(b"WebAppData"); + mac.finalize().into_bytes() + }; + + let token_bytes = { + let mut mac = HmacSha256::new_from_slice(data.as_bytes()).unwrap(); + mac.update(&secret_key); + mac.finalize().into_bytes() + }; + + Ok(hex::encode(token_bytes)) +} + +pub fn validate(init_data: &str, token: &str) -> Option { + if init_data.is_empty() || !init_data.contains('=') { + return None; + } + + let (base_data, hash) = match extract_hash(init_data) { + Some(v) => v, + None => return None, + }; + let expected_hash = match sign(&base_data, token) { + Ok(v) => v, + Err(_) => return None, + }; + + if hash != expected_hash { + return None; + } + + parse(&base_data) +} diff --git a/src/frontend/Cargo.toml b/src/frontend/Cargo.toml new file mode 100644 index 0000000..807e570 --- /dev/null +++ b/src/frontend/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "frontend" +version = "0.1.0" +edition = "2024" + +[dependencies] +yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] } +stylist = "0.13" +web-sys = { version = "0.3.77" } +gloo-net = "0.6.0" diff --git a/src/frontend/Trunk.toml b/src/frontend/Trunk.toml new file mode 100644 index 0000000..a3d4d1b --- /dev/null +++ b/src/frontend/Trunk.toml @@ -0,0 +1,6 @@ +[build] +public_url = "/assets/" + +[serve] +address = "127.0.0.1" +port = 8000 diff --git a/src/frontend/index.html b/src/frontend/index.html new file mode 100644 index 0000000..2c0c23c --- /dev/null +++ b/src/frontend/index.html @@ -0,0 +1,10 @@ + + + + + Yew App + + + + + diff --git a/src/frontend/src/main.rs b/src/frontend/src/main.rs new file mode 100644 index 0000000..2e04e9c --- /dev/null +++ b/src/frontend/src/main.rs @@ -0,0 +1,53 @@ +use stylist::style; +use yew::prelude::*; + +#[derive(Clone, PartialEq, Properties)] +struct SubscriptionProps { + username: String, +} + +#[function_component] +fn Subscription(props: &SubscriptionProps) -> Html { + html! { +
+ { props.username.clone() } +
+ } +} + +#[function_component] +fn Settings() -> Html { + let subscriptions = vec!["kurbezz"]; + + let header_style = style!( + r#" + font-size: 24px; + "# + ) + .expect("Failed to mount style"); + + html! { +
+

{ "Settings" }

+
+ { + subscriptions + .iter() + .map(|sub| html! { }) + .collect::() + } +
+
+ } +} + +#[function_component] +fn App() -> Html { + html! { + + } +} + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/src/subscription_manager.rs b/src/subscription_manager.rs deleted file mode 100644 index 072c936..0000000 --- a/src/subscription_manager.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use futures::StreamExt; -use mongodb::{ - Client, Collection, - bson::{Document, doc}, -}; -use tokio::sync::RwLock; - -use crate::config::CONFIG; - -pub struct SubscriptionManager { - pub subscriptions: RwLock>>, -} - -impl SubscriptionManager { - pub fn new() -> Self { - Self { - subscriptions: RwLock::new(HashMap::new()), - } - } - - async fn get_collection() -> mongodb::error::Result> { - let client = Client::with_uri_str(CONFIG.mongodb_connection_string.clone()).await?; - - let database = client.database("telegram-twitch-notifier"); - - Ok(database.collection("subscriptions")) - } - - pub async fn load(&self) -> mongodb::error::Result<()> { - let collection = Self::get_collection().await?; - - let mut subs = collection.find(doc! {}).await?; - - while let Some(sub) = subs.next().await { - let sub = sub?; - - let username = sub.get_str("streamer").unwrap(); - let telegram_user_id = sub.get_i64("telegram_user_id").unwrap() as u64; - - self.subscribe(telegram_user_id, username.to_string()).await; - } - - Ok(()) - } - - pub async fn subscribe(&self, telegram_user_id: u64, username: String) { - tracing::debug!("Subscribing {} to {}", telegram_user_id, username); - - let inserted = self - .subscriptions - .write() - .await - .entry(username.clone()) - .or_insert(HashSet::new()) - .insert(telegram_user_id); - - if !inserted { - return; - } - - Self::get_collection() - .await - .unwrap() - .insert_one(doc! { - "streamer": username, - "telegram_user_id": telegram_user_id as i64, - }) - .await - .unwrap(); - } - - pub async fn unsubscribe(&self, telegram_user_id: u64, username: String) { - tracing::debug!("Unsubscribing {} from {}", telegram_user_id, username); - - self.subscriptions - .write() - .await - .entry(username.clone()) - .and_modify(|set| { - set.remove(&telegram_user_id); - }); - - Self::get_collection() - .await - .unwrap() - .delete_one(doc! { - "streamer": username, - "telegram_user_id": telegram_user_id as i64, - }) - .await - .unwrap(); - } -}