diff --git a/.vscode/launch.json b/.vscode/launch.json index 2a0c72c66..7ae641e51 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,7 @@ { "version": "0.2.0", "configurations": [ + { "type": "lldb", "request": "launch", @@ -12,7 +13,7 @@ "kind": "bin" } }, - "args": [], + "args": ["--noui"], "env": { "RUST_LOG": "trace,actix_web=off,aws_sigv4=off,aws_smithy_checksums=off,actix_http=off,mio=off,reqwest=off,aws_runtime=off,aws_smithy_http_client=off,rustls=off,actix_server=off,hyper_util=off,aws_smithy_runtime=off,aws_smithy_runtime_api=off,tracing=off,aws_sdk_s3=off" diff --git a/Cargo.lock b/Cargo.lock index a3aa52d25..19f3713f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-core", "futures-sink", @@ -45,7 +45,7 @@ dependencies = [ "actix-service", "actix-utils", "base64 0.22.1", - "bitflags", + "bitflags 2.10.0", "brotli", "bytes", "bytestring", @@ -80,7 +80,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -118,7 +118,7 @@ dependencies = [ "parse-size", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -201,7 +201,7 @@ dependencies = [ "bytes", "bytestring", "cfg-if", - "cookie", + "cookie 0.16.2", "derive_more 2.0.1", "encoding_rs", "foldhash 0.1.5", @@ -235,7 +235,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -468,6 +468,27 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -478,6 +499,129 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.2", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock 3.4.1", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix 1.1.2", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock 3.4.1", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.2", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -497,9 +641,15 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -508,7 +658,30 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", ] [[package]] @@ -1055,7 +1228,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1066,14 +1239,23 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.108", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] name = "blake2" @@ -1102,6 +1284,37 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bmrng" version = "0.5.2" @@ -1123,6 +1336,7 @@ dependencies = [ "aes-gcm", "anyhow", "argon2", + "async-lock 2.8.0", "async-stream", "async-trait", "aws-config", @@ -1154,6 +1368,7 @@ dependencies = [ "once_cell", "pdf-extract", "qdrant-client", + "r2d2", "rand 0.9.2", "ratatui", "redis", @@ -1166,6 +1381,10 @@ dependencies = [ "sha2", "smartstring", "sysinfo", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", "tempfile", "time", "tokio", @@ -1217,6 +1436,12 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + [[package]] name = "byteorder" version = "1.5.0" @@ -1228,6 +1453,9 @@ name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] [[package]] name = "bytes-utils" @@ -1277,6 +1505,73 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.8", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -1328,12 +1623,33 @@ dependencies = [ "nom 7.1.3", ] +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + [[package]] name = "cff-parser" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31f5b6e9141c036f3ff4ce7b2f7e432b0f00dee416ddcd4f17741d189ddc2e9d" +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -1398,7 +1714,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] @@ -1444,7 +1760,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.1.14", + "unicode-width 0.2.0", ] [[package]] @@ -1517,6 +1833,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.16.1" @@ -1594,6 +1919,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1620,6 +1955,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1674,7 +2033,16 @@ checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" dependencies = [ "chrono", "once_cell", - "winnow", + "winnow 0.6.26", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", ] [[package]] @@ -1689,7 +2057,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "mio", "parking_lot", @@ -1705,7 +2073,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "derive_more 2.0.1", "document-features", @@ -1765,6 +2133,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.108", +] + [[package]] name = "csv" version = "1.4.0" @@ -1786,6 +2181,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.108", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1822,7 +2227,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 2.0.108", ] [[package]] @@ -1836,7 +2241,7 @@ dependencies = [ "indexmap 2.12.0", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1854,7 +2259,7 @@ dependencies = [ "indexmap 2.12.0", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1888,7 +2293,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.108", ] [[package]] @@ -1902,7 +2307,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.108", ] [[package]] @@ -1913,7 +2318,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1924,7 +2329,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1956,6 +2361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -1966,7 +2372,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1987,7 +2393,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1997,7 +2403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.108", ] [[package]] @@ -2010,7 +2416,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.108", ] [[package]] @@ -2031,7 +2437,7 @@ dependencies = [ "convert_case 0.7.1", "proc-macro2", "quote", - "syn", + "syn 2.0.108", "unicode-xid", ] @@ -2041,13 +2447,14 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e7624a3bb9fffd82fff016be9a7f163d20e5a89eb8d28f9daaa6b30fff37500" dependencies = [ - "bitflags", + "bitflags 2.10.0", "byteorder", "chrono", "diesel_derives", - "downcast-rs", + "downcast-rs 2.0.2", "itoa", "pq-sys", + "r2d2", "serde_json", "uuid", ] @@ -2062,7 +2469,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -2071,7 +2478,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" dependencies = [ - "syn", + "syn 2.0.108", ] [[package]] @@ -2085,6 +2492,45 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2093,7 +2539,39 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -2111,6 +2589,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "downcast-rs" version = "2.0.2" @@ -2130,6 +2614,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + [[package]] name = "dsl_auto_type" version = "0.2.0" @@ -2141,7 +2634,22 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.108", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", ] [[package]] @@ -2150,6 +2658,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecb" version = "0.1.2" @@ -2213,6 +2727,26 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.8", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -2228,6 +2762,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -2257,6 +2818,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -2276,6 +2848,33 @@ dependencies = [ "num-traits", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + [[package]] name = "eyre" version = "0.6.12" @@ -2292,6 +2891,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.12.1" @@ -2302,6 +2910,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -2348,7 +2966,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -2357,6 +2996,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2382,6 +3027,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.31" @@ -2430,6 +3085,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2438,7 +3106,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -2471,6 +3139,114 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -2481,6 +3257,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -2490,7 +3277,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2524,12 +3311,102 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "group" version = "0.12.1" @@ -2541,6 +3418,58 @@ dependencies = [ "subtle", ] +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "h2" version = "0.3.27" @@ -2624,6 +3553,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -2650,6 +3585,18 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + [[package]] name = "http" version = "0.2.12" @@ -2878,6 +3825,16 @@ dependencies = [ "cc", ] +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -3056,6 +4013,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -3066,6 +4024,8 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -3090,6 +4050,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + [[package]] name = "inout" version = "0.1.4" @@ -3110,7 +4079,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -3129,6 +4098,25 @@ dependencies = [ "serde", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -3177,6 +4165,29 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "jiff" version = "0.2.15" @@ -3198,7 +4209,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -3243,6 +4254,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "jsonwebtoken" version = "9.3.1" @@ -3256,6 +4289,29 @@ dependencies = [ "serde_json", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.12.0", + "selectors", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -3296,12 +4352,46 @@ dependencies = [ "url", ] +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.9" @@ -3312,6 +4402,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + [[package]] name = "libwebrtc" version = "0.3.19" @@ -3380,7 +4480,7 @@ dependencies = [ "chrono", "futures-util", "lazy_static", - "libloading", + "libloading 0.8.9", "libwebrtc", "livekit-api", "livekit-protocol", @@ -3491,7 +4591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7184fdea2bc3cd272a1acec4030c321a8f9875e877b3f92a53f2f6033fdc289" dependencies = [ "aes", - "bitflags", + "bitflags 2.10.0", "cbc", "ecb", "encoding_rs", @@ -3548,6 +4648,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "mailparse" version = "0.15.0" @@ -3559,6 +4665,31 @@ dependencies = [ "quoted_printable", ] +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "matchers" version = "0.2.0" @@ -3568,6 +4699,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "matchit" version = "0.7.3" @@ -3590,6 +4727,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -3620,7 +4766,7 @@ checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -3648,6 +4794,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + [[package]] name = "multimap" version = "0.10.1" @@ -3671,6 +4838,61 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -3763,13 +4985,194 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-core-foundation", ] [[package]] @@ -3782,6 +5185,102 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-javascript-core", + "objc2-security", +] + [[package]] name = "object" version = "0.32.2" @@ -3821,15 +5320,27 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -3844,7 +5355,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -3865,6 +5376,22 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "ouroboros" version = "0.18.5" @@ -3886,7 +5413,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -3912,6 +5439,37 @@ dependencies = [ "sha2", ] +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -3969,6 +5527,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbjson" version = "0.6.0" @@ -4061,6 +5625,140 @@ dependencies = [ "indexmap 2.12.0", ] +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -4078,7 +5776,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -4093,6 +5791,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -4109,6 +5818,46 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.12.0", + "quick-xml 0.38.3", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + [[package]] name = "polyval" version = "0.6.2" @@ -4183,6 +5932,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -4190,9 +5945,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.108", ] +[[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 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -4210,7 +6024,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", "version_check", "yansi", ] @@ -4252,7 +6066,7 @@ dependencies = [ "prost 0.12.6", "prost-types 0.12.6", "regex", - "syn", + "syn 2.0.108", "tempfile", ] @@ -4266,7 +6080,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -4279,7 +6093,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -4331,6 +6145,24 @@ dependencies = [ "tonic", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -4407,6 +6239,31 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.5" @@ -4428,6 +6285,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4448,6 +6315,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -4466,6 +6342,24 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rangemap" version = "1.6.0" @@ -4478,7 +6372,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cassowary", "compact_str", "crossterm 0.28.1", @@ -4493,6 +6387,12 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "redis" version = "0.27.6" @@ -4523,7 +6423,38 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -4621,13 +6552,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rhai" version = "1.22.2" source = "git+https://github.com/therealprof/rhai.git?branch=features%2Fuse-web-time#fbf0f4198f2cad20e07ef7c1ceca10b43d69a04b" dependencies = [ "ahash", - "bitflags", + "bitflags 2.10.0", "getrandom 0.2.16", "num-traits", "once_cell", @@ -4645,7 +6601,7 @@ source = "git+https://github.com/therealprof/rhai.git?branch=features%2Fuse-web- dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -4689,7 +6645,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4702,7 +6658,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -4841,6 +6797,72 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.108", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -4883,7 +6905,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4896,7 +6918,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4913,11 +6935,33 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -4929,6 +6973,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4946,7 +7002,18 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -4971,6 +7038,35 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4983,6 +7079,69 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -5078,6 +7237,18 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.11" @@ -5121,6 +7292,54 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics", + "foreign-types 0.5.0", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "spki" version = "0.6.0" @@ -5156,6 +7375,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -5192,7 +7436,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.108", ] [[package]] @@ -5201,6 +7445,28 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.108" @@ -5229,7 +7495,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -5252,7 +7518,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5267,6 +7533,369 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5" +dependencies = [ + "anyhow", + "bytes", + "cookie 0.18.1", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http 1.3.1", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.17", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.108", + "tauri-utils", + "thiserror 2.0.17", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "toml 0.9.8", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" +dependencies = [ + "cookie 0.18.1", + "dpi", + "gtk", + "http 1.3.1", + "jni", + "objc2 0.6.3", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" +dependencies = [ + "gtk", + "http 1.3.1", + "jni", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http 1.3.1", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.17", + "toml 0.9.8", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +dependencies = [ + "embed-resource", + "toml 0.9.8", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -5280,6 +7909,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -5308,7 +7948,7 @@ checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -5343,7 +7983,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -5354,7 +7994,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -5445,6 +8085,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.1", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -5456,7 +8097,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -5525,6 +8166,102 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.12.0", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.12.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tonic" version = "0.12.3" @@ -5600,7 +8337,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http 1.3.1", @@ -5644,7 +8381,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -5696,6 +8433,28 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tray-icon" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5736,12 +8495,70 @@ dependencies = [ "pom", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -5874,6 +8691,18 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -5916,6 +8745,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -5928,6 +8763,26 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -5947,6 +8802,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -6007,7 +8868,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.108", "wasm-bindgen-shared", ] @@ -6033,6 +8894,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs 1.2.1", + "rustix 1.1.2", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.10.0", + "rustix 1.1.2", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.82" @@ -6053,6 +8974,50 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + [[package]] name = "webpki-roots" version = "1.0.3" @@ -6092,6 +9057,42 @@ dependencies = [ "zip 0.6.6", ] +[[package]] +name = "webview2-com" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +dependencies = [ + "thiserror 2.0.17", + "windows", + "windows-core 0.61.2", +] + [[package]] name = "weezl" version = "0.1.10" @@ -6129,6 +9130,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + [[package]] name = "windows" version = "0.61.3" @@ -6196,7 +9212,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -6207,7 +9223,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -6381,6 +9397,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6519,6 +9544,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.6.26" @@ -6528,6 +9562,25 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -6540,6 +9593,72 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wry" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie 0.18.1", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http 1.3.1", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + [[package]] name = "xmlparser" version = "0.13.6" @@ -6581,10 +9700,72 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", "synstructure", ] +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock 3.4.1", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -6602,7 +9783,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -6622,7 +9803,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", "synstructure", ] @@ -6643,7 +9824,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -6676,7 +9857,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -6787,3 +9968,44 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.108", + "winnow 0.7.13", +] diff --git a/Cargo.toml b/Cargo.toml index ae5cb8387..8e11c6c84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,17 +37,15 @@ license = "AGPL-3.0" repository = "https://github.com/GeneralBots/BotServer" [features] -default = [ "vectordb", "desktop"] +default = [] + vectordb = ["qdrant-client"] email = ["imap"] -desktop = [] +desktop = ["tauri"] +official = ["tauri"] [dependencies] -color-eyre = "0.6.5" -crossterm = "0.29.0" -ratatui = "0.29.0" -scopeguard = "1.2.0" -once_cell = "1.18.0" + actix-cors = "0.7" actix-multipart = "0.7" actix-web = "4.9" @@ -55,6 +53,7 @@ actix-ws = "0.3" aes-gcm = "0.10" anyhow = "1.0" argon2 = "0.5" +async-lock = "2.8.0" async-stream = "0.3" async-trait = "0.1" aws-config = "1.8.8" @@ -62,9 +61,11 @@ aws-sdk-s3 = { version = "1.109.0", features = ["behavior-version-latest"] } base64 = "0.22" bytes = "1.8" chrono = { version = "0.4", features = ["serde"] } +color-eyre = "0.6.5" cron = "0.15.0" +crossterm = "0.29.0" csv = "1.3" -diesel = { version = "2.1", features = ["postgres", "uuid", "chrono", "serde_json"] } +diesel = { version = "2.1", features = ["postgres", "uuid", "chrono", "serde_json", "r2d2"] } dotenvy = "0.15" downloader = "0.2" env_logger = "0.11" @@ -81,18 +82,25 @@ mailparse = "0.15" mockito = "1.7.0" native-tls = "0.2" num-format = "0.4" +once_cell = "1.18.0" pdf-extract = "0.10.0" qdrant-client = { version = "1.12", optional = true } +r2d2 = "0.8.10" rand = "0.9.2" +ratatui = "0.29.0" redis = { version = "0.27", features = ["tokio-comp"] } regex = "1.11" reqwest = { version = "0.12", features = ["json", "stream"] } rhai = { git = "https://github.com/therealprof/rhai.git", branch = "features/use-web-time" } +scopeguard = "1.2.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10.9" smartstring = "1.0" sysinfo = "0.37.2" +tauri = { version = "2", features = ["unstable"], optional = true } +tauri-plugin-dialog = { version = "2", optional = true } +tauri-plugin-opener = { version = "2", optional = true } tempfile = "3" time = "0.3.44" tokio = { version = "1.41", features = ["full"] } @@ -104,6 +112,11 @@ urlencoding = "2.1" uuid = { version = "1.11", features = ["serde", "v4"] } zip = "2.2" +[build-dependencies] +tauri-build = { version = "2", features = [] } + + + [profile.release] lto = true opt-level = "z" diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a735d5089..000000000 --- a/TODO.md +++ /dev/null @@ -1,78 +0,0 @@ -# Documentation Completion Checklist - -- [x] Created Chapter 01 files (README, installation, first-conversation, sessions) -- [ ] Fill Chapter 02 files (README, gbai, gbdialog, gbkb, gbot, gbtheme, gbdrive) – already have content -- [ ] Complete Chapter 03 files - - [ ] README.md - - [ ] vector-collections.md - - [ ] indexing.md - - [ ] qdrant.md - - [ ] semantic-search.md - - [ ] context-compaction.md - - [ ] caching.md (if needed) -- [ ] Complete Chapter 04 files - - [ ] README.md - - [ ] structure.md - - [ ] web-interface.md - - [ ] css.md - - [ ] html.md -- [ ] Complete Chapter 05 files - - [ ] README.md - - [ ] basics.md - - [ ] templates.md - - [ ] template-start.md - - [ ] template-auth.md - - [ ] template-summary.md - - [ ] template-enrollment.md - - [ ] keywords.md - - [ ] All keyword pages (talk, hear, set-user, set-context, llm, get-bot-memory, set-bot-memory, set-kb, add-kb, add-website, add-tool, list-tools, remove-tool, clear-tools, get, find, set, on, set-schedule, create-site, create-draft, website-of, print, wait, format, first, last, for-each, exit-for) -- [ ] Complete Chapter 06 files - - [ ] README.md - - [ ] architecture.md - - [ ] building.md - - [ ] crates.md - - [ ] services.md - - [ ] custom-keywords.md - - [ ] dependencies.md -- [ ] Complete Chapter 07 files - - [ ] README.md - - [ ] config-csv.md - - [ ] parameters.md - - [ ] answer-modes.md - - [ ] llm-config.md - - [ ] context-config.md - - [ ] minio.md -- [ ] Complete Chapter 08 files - - [ ] README.md - - [ ] tool-definition.md - - [ ] param-declaration.md - - [ ] compilation.md - - [ ] mcp-format.md - - [ ] openai-format.md - - [ ] get-integration.md - - [ ] external-apis.md -- [ ] Complete Chapter 09 files - - [ ] README.md - - [ ] core-features.md - - [ ] conversation.md - - [ ] ai-llm.md - - [ ] knowledge-base.md - - [ ] automation.md - - [ ] email.md - - [ ] web-automation.md - - [ ] storage.md - - [ ] channels.md -- [ ] Complete Chapter 10 files - - [ ] README.md - - [ ] setup.md - - [ ] standards.md - - [ ] testing.md - - [ ] pull-requests.md - - [ ] documentation.md -- [ ] Complete Appendix I files - - [ ] README.md - - [ ] schema.md - - [ ] tables.md - - [ ] relationships.md -- [ ] Verify SUMMARY.md links -- [ ] Run mdbook build to ensure no errors diff --git a/add-req.sh b/add-req.sh index 4f7b7ad1f..7223b1530 100755 --- a/add-req.sh +++ b/add-req.sh @@ -21,24 +21,24 @@ for file in "${prompts[@]}"; do done dirs=( - #"auth" - #"automation" - #"basic" + "auth" + "automation" + "basic" "bootstrap" "bot" #"channels" - #"config" + "config" #"context" "drive_monitor" #"email" - "file" - # "kb" + #"file" + #"kb" "llm" #"llm_models" #"org" - "package_manager" + #"package_manager" #"riot_compiler" - #"session" + "session" "shared" #"tests" #"tools" @@ -48,12 +48,6 @@ dirs=( #"web_automation" ) -filter_rust_file() { - sed -E '/^\s*\/\//d' "$1" | \ - sed -E '/info!\s*\(/d' | \ - sed -E '/debug!\s*\(/d' | \ - sed -E '/trace!\s*\(/d' -} for dir in "${dirs[@]}"; do find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read -r file; do diff --git a/docs/src/chapter-06/building.md b/docs/src/chapter-06/building.md index 2b25b59ec..a9b54ec4a 100644 --- a/docs/src/chapter-06/building.md +++ b/docs/src/chapter-06/building.md @@ -74,6 +74,8 @@ cargo install cargo-audit cargo install cargo-edit apt install -y libpq-dev apt install -y valkey-cli +valkey-cli config set stop-writes-on-bgsave-error no + # Util diff --git a/fix-errors.sh b/fix-errors.sh index b003f0a63..0315f6f6d 100755 --- a/fix-errors.sh +++ b/fix-errors.sh @@ -1,9 +1,15 @@ #!/bin/bash +set -e # Exit on error + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$SCRIPT_DIR" OUTPUT_FILE="/tmp/prompt.out" +# Check required commands +command -v cargo >/dev/null 2>&1 || { echo "cargo is required but not installed" >&2; exit 1; } +command -v xclip >/dev/null 2>&1 || { echo "xclip is required but not installed" >&2; exit 1; } + echo "Please, fix this consolidated LLM Context" > "$OUTPUT_FILE" prompts=( @@ -12,6 +18,14 @@ prompts=( "./Cargo.toml" ) +# Validate files exist +for file in "${prompts[@]}"; do + if [ ! -f "$file" ]; then + echo "Required file not found: $file" >&2 + exit 1 + fi +done + for file in "${prompts[@]}"; do cat "$file" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE" @@ -43,11 +57,15 @@ dirs=( # "whatsapp" ) for dir in "${dirs[@]}"; do - find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read file; do - echo $file >> "$OUTPUT_FILE" - cat "$file" >> "$OUTPUT_FILE" - echo "" >> "$OUTPUT_FILE" - done + if [ -d "$PROJECT_ROOT/src/$dir" ]; then + find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read -r file; do + if [ -f "$file" ]; then + echo "$file" >> "$OUTPUT_FILE" + cat "$file" >> "$OUTPUT_FILE" + echo "" >> "$OUTPUT_FILE" + fi + done + fi done # Also append the specific files you mentioned @@ -63,15 +81,18 @@ cargo build --message-format=short 2>&1 | grep -E 'error' >> "$OUTPUT_FILE" # Calculate and display token count (approximation: words * 1.3) -WORD_COUNT=$(wc -w < "$OUTPUT_FILE") -TOKEN_COUNT=$(echo "$WORD_COUNT * 1.3 / 1" | bc) -FILE_SIZE=$(wc -c < "$OUTPUT_FILE") +WORD_COUNT=$(wc -w < "$OUTPUT_FILE") || { echo "Error counting words" >&2; exit 1; } +TOKEN_COUNT=$(echo "$WORD_COUNT * 1.3 / 1" | bc) || { echo "Error calculating tokens" >&2; exit 1; } +FILE_SIZE=$(wc -c < "$OUTPUT_FILE") || { echo "Error getting file size" >&2; exit 1; } echo "" >> "$OUTPUT_FILE" - echo "Approximate token count: $TOKEN_COUNT" echo "Context size: $FILE_SIZE bytes" -cat "$OUTPUT_FILE" | xclip -selection clipboard +if ! cat "$OUTPUT_FILE" | xclip -selection clipboard; then + echo "Error copying to clipboard" >&2 + exit 1 +fi + echo "Content copied to clipboard (xclip)" rm -f "$OUTPUT_FILE" diff --git a/migrations/6.0.5.sql b/migrations/6.0.5.sql index 6be7e3bf8..79b7979d3 100644 --- a/migrations/6.0.5.sql +++ b/migrations/6.0.5.sql @@ -25,29 +25,6 @@ CREATE INDEX IF NOT EXISTS idx_system_automations_bot_id ON public.system_automations (bot_id); -ALTER TABLE public.system_automations -ADD CONSTRAINT system_automations_bot_kind_param_unique -UNIQUE (bot_id, kind, param); - --- Migration 6.0.10: Add unique constraint for system_automations upsert --- Description: Creates a unique constraint matching the ON CONFLICT target in set_schedule.rs - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname = 'system_automations_bot_kind_param_unique' - ) THEN - ALTER TABLE public.system_automations - ADD CONSTRAINT system_automations_bot_kind_param_unique - UNIQUE (bot_id, kind, param); - END IF; -END -$$; - --- Migration 6.0.6: Add unique constraint for system_automations --- Fixes error: "there is no unique or exclusion constraint matching the ON CONFLICT specification" - ALTER TABLE public.system_automations ADD CONSTRAINT system_automations_bot_kind_param_unique UNIQUE (bot_id, kind, param); diff --git a/prompts/dev/platform/README.md b/prompts/dev/platform/README.md index 01d3a6b42..5af0fa983 100644 --- a/prompts/dev/platform/README.md +++ b/prompts/dev/platform/README.md @@ -3,17 +3,19 @@ ### Fallback Strategy (After 3 attempts / 10 minutes): When initial attempts fail, sequentially try these LLMs: 1. **DeepSeek-V3-0324** (good architect, adventure, reliable, let little errors just to be fixed by gpt-*) -1. **DeepSeek-V3.1** (slower) 1. **gpt-5-chat** (slower, let warnings...) 1. **gpt-oss-120b** 1. **Claude (Web)**: Copy only the problem statement and create unit tests. Create/extend UI. -1. **Llama-3.3-70B-Instruct** (alternative) ### Development Workflow: - **One requirement at a time** with sequential commits -- **On error**: Stop and consult Claude for guidance +- **On unresolved error**: Stop and use add-req.sh, and consult Claude for guidance. with DeepThining in DeepSeek also, with Web turned on. - **Change progression**: Start with DeepSeek, conclude with gpt-oss-120b - If a big req. fail, specify a @code file that has similar pattern or sample from official docs. - **Final validation**: Use prompt "cargo check" with gpt-oss-120b - Be humble, one requirement, one commit. But sometimes, freedom of caos is welcome - when no deadlines are set. +- Fix manually in case of dangerous trouble. - Keep in the source codebase only deployed and tested source, no lab source code in main project. At least, use optional features to introduce new behaviour gradually in PRODUCTION. +- Transform good articles into prompts for the coder. +- Switch to libraries that have LLM affinity. +- Ensure 'continue' on LLMs, they can EOF and say are done, but got more to output. \ No newline at end of file diff --git a/prompts/dev/platform/ide.md b/prompts/dev/platform/ide.md index bf28abcae..20f20af26 100644 --- a/prompts/dev/platform/ide.md +++ b/prompts/dev/platform/ide.md @@ -1,5 +1,6 @@ -- On code return identifiers/chars in english language. -- Do not emmit any comment, and remove any existants in Rust/html. +- On code return identifiers/characters in English language, no invalid tokens! +- Do not emit any comments, and remove any existing ones in Rust/HTML. - Compact the code emission where possible. -- On change code, ensure cargo check cycle to remove warnings and errors. -- Never use defaults or magic values in code (never unwrap_or_else or similars) \ No newline at end of file +- On code change, ensure cargo check cycle to remove warnings and errors. +- Never use defaults or magic values in code (never unwrap_or_else or similar) +- Check borrow, clone, types, common Rust errors! Return 0 warning code! \ No newline at end of file diff --git a/prompts/dev/platform/shared.md b/prompts/dev/platform/shared.md index 026dbd0fd..60f0040fc 100644 --- a/prompts/dev/platform/shared.md +++ b/prompts/dev/platform/shared.md @@ -1,15 +1,16 @@ MOST IMPORTANT CODE GENERATION RULES: +- KISS, NO TALK, SECURED ENTERPRISE GRADE THREAD SAFE CODE ONLY. - Use rustc 1.90.0 (1159e78c4 2025-09-14). - Check for warnings related to use of mut where is dispensable. - No placeholders, never comment/uncomment code, no explanations, no filler text. - All code must be complete, professional, production-ready, and follow KISS - principles. - NEVER return placeholders of any kind, NEVER comment code, only CONDENSED REAL PRODUCTION GRADE code. -- REMOTE ALL COMMENTS FROM GENERATED CODE. DO NOT COMMENT AT ALL, NO TALK! +- REMOTE ALL COMMENTS FROM GENERATED CODE. DO NOT COMMENT AT ALL, NO TALK, just say you are finished! - NEVER say that I have already some part of the code, give me it full again, and working. - Always increment logging with (all-in-one-line) info!, debug!, trace! to give birth to the console. - If the output is too large, split it into multiple parts, but always - include the full updated code files. - Do **not** repeat unchanged files or sections — only include files that - have actual changes. -- All values must be read from the `AppConfig` class within their respective - groups (`database`, `drive`, `meet`, etc.); never use hardcoded or magic - values. +- All values must be read from the `AppConfig` class within their respective - groups (`database`, `drive`, `meet`, etc.); never use hardcoded or commercial names like S3, Azure or something like that, preffer Drive, Cloud, instead, never use url or magic - values like api.openai - no unrwap_or_defaul at all!!! - Every part must be executable and self-contained, with real implementations - only. - DO NOT WRITE ANY ERROR HANDLING CODE LET IT CRASH. - Never generate two ore more trace mensages that are equal! @@ -18,3 +19,5 @@ MOST IMPORTANT CODE GENERATION RULES: - NEVER return a untouched file in output. Just files that need to be updated. - Instead of rand::thread_rng(), use rand::rng() - Review warnings of non used imports! Give me 0 warnings, please. +- Ensure you remove these: ommon errors borrow of moved value, unused variable, use of moved value. + diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 781260fe4..cef00d2d1 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,139 +1,124 @@ - use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::error; use std::collections::HashMap; use std::sync::Arc; use uuid::Uuid; use crate::shared::state::AppState; - pub struct AuthService {} - impl AuthService { - pub fn new() -> Self { - Self {} - } + pub fn new() -> Self { + Self {} + } } - #[actix_web::get("/api/auth")] async fn auth_handler( - _req: HttpRequest, - data: web::Data, - web::Query(params): web::Query>, + _req: HttpRequest, + data: web::Data, + web::Query(params): web::Query>, ) -> Result { - let bot_name = params.get("bot_name").cloned().unwrap_or_default(); - let _token = params.get("token").cloned(); - - let user_id = { - let mut sm = data.session_manager.lock().await; - sm.get_or_create_anonymous_user(None).map_err(|e| { - error!("Failed to create anonymous user: {}", e); - actix_web::error::ErrorInternalServerError("Failed to create user") - })? - }; - - let (bot_id, bot_name) = tokio::task::spawn_blocking({ - let bot_name = bot_name.clone(); - let conn_arc = Arc::clone(&data.conn); - move || { - let mut db_conn = conn_arc.lock().unwrap(); - use crate::shared::models::schema::bots::dsl::*; - use diesel::prelude::*; - - match bots - .filter(name.eq(&bot_name)) - .filter(is_active.eq(true)) - .select((id, name)) - .first::<(Uuid, String)>(&mut *db_conn) - .optional() - { - Ok(Some((id_val, name_val))) => Ok((id_val, name_val)), - Ok(None) => { - match bots - .filter(is_active.eq(true)) - .select((id, name)) - .first::<(Uuid, String)>(&mut *db_conn) - .optional() - { - Ok(Some((id_val, name_val))) => Ok((id_val, name_val)), - Ok(None) => Err("No active bots found".to_string()), - Err(e) => Err(format!("DB error: {}", e)), - } - } - Err(e) => Err(format!("DB error: {}", e)), - } - } - }) - .await - .map_err(|e| { - error!("Spawn blocking failed: {}", e); - actix_web::error::ErrorInternalServerError("DB thread error") - })? - .map_err(|e| { - error!("{}", e); - actix_web::error::ErrorInternalServerError(e) - })?; - - let session = { - let mut sm = data.session_manager.lock().await; - sm.get_or_create_user_session(user_id, bot_id, "Auth Session") - .map_err(|e| { - error!("Failed to create session: {}", e); - actix_web::error::ErrorInternalServerError(e.to_string()) - })? - .ok_or_else(|| { - error!("Failed to create session"); - actix_web::error::ErrorInternalServerError("Failed to create session") - })? - }; - - let auth_script_path = format!("./work/{}.gbai/{}.gbdialog/auth.ast", bot_name, bot_name); - - if tokio::fs::metadata(&auth_script_path).await.is_ok() { - let auth_script = match tokio::fs::read_to_string(&auth_script_path).await { - Ok(content) => content, - Err(e) => { - error!("Failed to read auth script: {}", e); - return Ok(HttpResponse::Ok().json(serde_json::json!({ - "user_id": session.user_id, - "session_id": session.id, - "status": "authenticated" - }))); - } - }; - - let script_service = crate::basic::ScriptService::new(Arc::clone(&data), session.clone()); - - match tokio::time::timeout( - std::time::Duration::from_secs(5), - async { - script_service - .compile(&auth_script) - .and_then(|ast| script_service.run(&ast)) - } - ).await { - Ok(Ok(result)) => { - if result.to_string() == "false" { - error!("Auth script returned false"); - return Ok(HttpResponse::Unauthorized() - .json(serde_json::json!({"error": "Authentication failed"}))); - } - } - Ok(Err(e)) => { - error!("Auth script execution error: {}", e); - } - Err(_) => { - error!("Auth script timeout"); - } - } - } - - Ok(HttpResponse::Ok().json(serde_json::json!({ - "user_id": session.user_id, - "session_id": session.id, - "status": "authenticated" - }))) + let bot_name = params.get("bot_name").cloned().unwrap_or_default(); + let _token = params.get("token").cloned(); + let user_id = { + let mut sm = data.session_manager.lock().await; + sm.get_or_create_anonymous_user(None).map_err(|e| { + error!("Failed to create anonymous user: {}", e); + actix_web::error::ErrorInternalServerError("Failed to create user") + })? + }; + let (bot_id, bot_name) = tokio::task::spawn_blocking({ + let bot_name = bot_name.clone(); + let conn = data.conn.clone(); + move || { + let mut db_conn = conn.get().map_err(|e| format!("Failed to get database connection: {}", e))?; + use crate::shared::models::schema::bots::dsl::*; + use diesel::prelude::*; + match bots + .filter(name.eq(&bot_name)) + .filter(is_active.eq(true)) + .select((id, name)) + .first::<(Uuid, String)>(&mut db_conn) + .optional() + { + Ok(Some((id_val, name_val))) => Ok((id_val, name_val)), + Ok(None) => { + match bots + .filter(is_active.eq(true)) + .select((id, name)) + .first::<(Uuid, String)>(&mut db_conn) + .optional() + { + Ok(Some((id_val, name_val))) => Ok((id_val, name_val)), + Ok(None) => Err("No active bots found".to_string()), + Err(e) => Err(format!("DB error: {}", e)), + } + } + Err(e) => Err(format!("DB error: {}", e)), + } + } + }) + .await + .map_err(|e| { + error!("Spawn blocking failed: {}", e); + actix_web::error::ErrorInternalServerError("DB thread error") + })? + .map_err(|e| { + error!("{}", e); + actix_web::error::ErrorInternalServerError(e) + })?; + let session = { + let mut sm = data.session_manager.lock().await; + sm.get_or_create_user_session(user_id, bot_id, "Auth Session") + .map_err(|e| { + error!("Failed to create session: {}", e); + actix_web::error::ErrorInternalServerError(e.to_string()) + })? + .ok_or_else(|| { + error!("Failed to create session"); + actix_web::error::ErrorInternalServerError("Failed to create session") + })? + }; + let auth_script_path = format!("./work/{}.gbai/{}.gbdialog/auth.ast", bot_name, bot_name); + if tokio::fs::metadata(&auth_script_path).await.is_ok() { + let auth_script = match tokio::fs::read_to_string(&auth_script_path).await { + Ok(content) => content, + Err(e) => { + error!("Failed to read auth script: {}", e); + return Ok(HttpResponse::Ok().json(serde_json::json!({ + "user_id": session.user_id, + "session_id": session.id, + "status": "authenticated" + }))); + } + }; + let script_service = crate::basic::ScriptService::new(Arc::clone(&data), session.clone()); + match tokio::time::timeout( + std::time::Duration::from_secs(5), + async { + script_service + .compile(&auth_script) + .and_then(|ast| script_service.run(&ast)) + } + ).await { + Ok(Ok(result)) => { + if result.to_string() == "false" { + error!("Auth script returned false"); + return Ok(HttpResponse::Unauthorized() + .json(serde_json::json!({"error": "Authentication failed"}))); + } + } + Ok(Err(e)) => { + error!("Auth script execution error: {}", e); + } + Err(_) => { + error!("Auth script timeout"); + } + } + } + Ok(HttpResponse::Ok().json(serde_json::json!({ + "user_id": session.user_id, + "session_id": session.id, + "status": "authenticated" + }))) } - - #[cfg(test)] pub mod auth_test; diff --git a/src/automation/compact_prompt.rs b/src/automation/compact_prompt.rs index bfa80ee51..4a38fd0d2 100644 --- a/src/automation/compact_prompt.rs +++ b/src/automation/compact_prompt.rs @@ -3,167 +3,100 @@ use crate::llm_models; use crate::shared::models::Automation; use crate::shared::state::AppState; use diesel::prelude::*; -use log::{error, info, trace}; +use log::{error, trace}; use std::collections::HashSet; use std::sync::Arc; use tokio::time::{interval, Duration}; use uuid::Uuid; - pub fn start_compact_prompt_scheduler(state: Arc) { - tokio::spawn(async move { - // Initial 30 second delay before first run - tokio::time::sleep(Duration::from_secs(30)).await; - let mut interval = interval(Duration::from_secs(60)); - loop { - interval.tick().await; - if let Err(e) = execute_compact_prompt(Arc::clone(&state)).await { - error!("Prompt compaction failed: {}", e); - } - } - }); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(30)).await; + let mut interval = interval(Duration::from_secs(60)); + loop { + interval.tick().await; + if let Err(e) = execute_compact_prompt(Arc::clone(&state)).await { + error!("Prompt compaction failed: {}", e); + } + } + }); } - async fn execute_compact_prompt(state: Arc) -> Result<(), Box> { - use crate::shared::models::system_automations::dsl::{is_active, system_automations}; - - let automations: Vec = { - let mut conn = state - .conn - .lock() - .map_err(|e| format!("Failed to acquire lock: {}", e))?; - - system_automations - .filter(is_active.eq(true)) - .load::(&mut *conn)? - }; - - for automation in automations { - if let Err(e) = compact_prompt_for_bot(&state, &automation).await { - error!( - "Failed to compact prompt for bot {}: {}", - automation.bot_id, e - ); - } - } - - Ok(()) + use crate::shared::models::system_automations::dsl::{is_active, system_automations}; + let automations: Vec = { + let mut conn = state.conn.get().map_err(|e| format!("Failed to acquire lock: {}", e))?; + system_automations.filter(is_active.eq(true)).load::(&mut *conn)? + }; + for automation in automations { + if let Err(e) = compact_prompt_for_bot(&state, &automation).await { + error!("Failed to compact prompt for bot {}: {}", automation.bot_id, e); + } + } + Ok(()) } - -async fn compact_prompt_for_bot( - state: &Arc, - automation: &Automation, -) -> Result<(), Box> { - // Skip if already compacting this bot - use once_cell::sync::Lazy; - use scopeguard::guard; - static IN_PROGRESS: Lazy>> = Lazy::new(|| { - tokio::sync::Mutex::new(HashSet::new()) - }); - - { - let mut in_progress = IN_PROGRESS.lock().await; - if in_progress.contains(&automation.bot_id) { - trace!("Skipping compaction for bot {} - already in progress", automation.bot_id); - return Ok(()); - } - in_progress.insert(automation.bot_id); - } - - // Ensure cleanup happens when function exits - let bot_id = automation.bot_id; - let _cleanup = guard((), |_| { - tokio::spawn(async move { - let mut in_progress = IN_PROGRESS.lock().await; - in_progress.remove(&bot_id); - trace!("Released compaction lock for bot {}", bot_id); - }); - }); - - info!("Executing prompt compaction for bot: {}", automation.bot_id); - - let config_manager = ConfigManager::new(Arc::clone(&state.conn)); - let compact_threshold = config_manager - .get_config(&automation.bot_id, "prompt-compact", None)? - .parse::() - .unwrap_or(0); - - // Compact if threshold is negative (always compact) or positive (conditional) - if compact_threshold == 0 { - return Ok(()); - } else if compact_threshold < 0 { - info!("Compaction forced for bot {} (threshold = {})", automation.bot_id, compact_threshold); - } - - // Get sessions without holding lock - let sessions = { - let mut session_manager = state.session_manager.lock().await; - session_manager.get_user_sessions(Uuid::nil())? - }; - - for session in sessions { - if session.bot_id != automation.bot_id { - continue; - } - - // Get history without holding lock - let history = { - let mut session_manager = state.session_manager.lock().await; - session_manager.get_conversation_history(session.id, session.user_id)? - }; - - info!( - "Compacting prompt for session {}: {} messages", - session.id, - history.len() - ); - - // Compact entire conversation history when threshold is reached - let mut compacted = String::new(); - for (role, content) in &history { - compacted.push_str(&format!("{}: {}\n", role, content)); - } - - // Clone needed references for async task - let llm_provider = state.llm_provider.clone(); - let compacted_clone = compacted.clone(); - - // Run LLM summarization with proper tracing and filtering - trace!("Starting summarization for session {}", session.id); - let summarized = match llm_provider.summarize(&compacted_clone).await { - Ok(summary) => { - trace!("Successfully summarized session {} ({} chars)", - session.id, summary.len()); - // Use handler to filter content - let handler = llm_models::get_handler( - &config_manager.get_config( - &automation.bot_id, - "llm-model", - None - ).unwrap_or_default() - ); - let filtered = handler.process_content(&summary); - format!("SUMMARY: {}", filtered) - }, - Err(e) => { - error!("Failed to summarize conversation for session {}: {}", session.id, e); - trace!("Using fallback summary for session {}", session.id); - format!("SUMMARY: {}", compacted) // Fallback - } - }; - info!( - "Prompt compacted {}: {} messages", - session.id, - history.len() - ); - - // Instead of clearing messages, insert a compacted marker message - { - let mut session_manager = state.session_manager.lock().await; - // Save a special compacted message type (9) - session_manager.save_message(session.id, session.user_id, 9, &summarized, 1)?; - trace!("Inserted compacted message for session {}", session.id); - } - } - - Ok(()) +async fn compact_prompt_for_bot(state: &Arc, automation: &Automation) -> Result<(), Box> { + use once_cell::sync::Lazy; + use scopeguard::guard; + static IN_PROGRESS: Lazy>> = Lazy::new(|| tokio::sync::Mutex::new(HashSet::new())); + { + let mut in_progress = IN_PROGRESS.lock().await; + if in_progress.contains(&automation.bot_id) { + return Ok(()); + } + in_progress.insert(automation.bot_id); + } + let bot_id = automation.bot_id; + let _cleanup = guard((), |_| { + tokio::spawn(async move { + let mut in_progress = IN_PROGRESS.lock().await; + in_progress.remove(&bot_id); + }); + }); + let config_manager = ConfigManager::new(state.conn.clone()); + let compact_threshold = config_manager + .get_config(&automation.bot_id, "prompt-compact", None)? + .parse::() + .unwrap_or(0); + if compact_threshold == 0 { + return Ok(()); + } else if compact_threshold < 0 { + trace!("Negative compact threshold detected for bot {}, skipping", automation.bot_id); + } + let sessions = { + let mut session_manager = state.session_manager.lock().await; + session_manager.get_user_sessions(Uuid::nil())? + }; + for session in sessions { + if session.bot_id != automation.bot_id { + continue; + } + let history = { + let mut session_manager = state.session_manager.lock().await; + session_manager.get_conversation_history(session.id, session.user_id)? + }; + trace!("Compacting prompt for session {}: {} messages", session.id, history.len()); + let mut compacted = String::new(); + for (role, content) in &history { + compacted.push_str(&format!("{}: {}\n", role, content)); + } + let llm_provider = state.llm_provider.clone(); + let compacted_clone = compacted.clone(); + let summarized = match llm_provider.summarize(&compacted_clone).await { + Ok(summary) => { + trace!("Successfully summarized conversation for session {}, summary length: {}", session.id, summary.len()); + let handler = llm_models::get_handler(&config_manager.get_config(&automation.bot_id, "llm-model", None).unwrap_or_default()); + let filtered = handler.process_content(&summary); + format!("SUMMARY: {}", filtered) + }, + Err(e) => { + error!("Failed to summarize conversation for session {}: {}", session.id, e); + format!("SUMMARY: {}", compacted) + } + }; + trace!("Prompt compacted {}: {} messages", session.id, history.len()); + { + let mut session_manager = state.session_manager.lock().await; + session_manager.save_message(session.id, session.user_id, 9, &summarized, 1)?; + } + } + Ok(()) } diff --git a/src/automation/mod.rs b/src/automation/mod.rs index bd62d4b97..dfd67e182 100644 --- a/src/automation/mod.rs +++ b/src/automation/mod.rs @@ -5,28 +5,21 @@ use crate::shared::state::AppState; use chrono::Utc; use cron::Schedule; use diesel::prelude::*; -use log::{error, info}; +use log::{error, trace}; use std::str::FromStr; use std::sync::Arc; use tokio::time::{interval, Duration}; mod compact_prompt; - pub struct AutomationService { state: Arc, } - impl AutomationService { pub fn new(state: Arc) -> Self { - // Start the compact prompt scheduler crate::automation::compact_prompt::start_compact_prompt_scheduler(Arc::clone(&state)); Self { state } } - pub async fn spawn(self) -> Result<(), Box> { - info!("Automation service started"); - let mut ticker = interval(Duration::from_secs(60)); - loop { ticker.tick().await; if let Err(e) = self.check_scheduled_tasks().await { @@ -34,29 +27,24 @@ impl AutomationService { } } } - async fn check_scheduled_tasks(&self) -> Result<(), Box> { use crate::shared::models::system_automations::dsl::{ id, is_active, kind, last_triggered as lt_column, system_automations, }; - let mut conn = self .state .conn - .lock() - .map_err(|e| format!("Failed to acquire lock: {}", e))?; - + .get() + .map_err(|e| format!("Failed to acquire database connection: {}", e))?; let automations: Vec = system_automations .filter(is_active.eq(true)) .filter(kind.eq(TriggerKind::Scheduled as i32)) - .load::(&mut *conn)?; - + .load::(&mut conn)?; for automation in automations { if let Some(schedule_str) = &automation.schedule { if let Ok(parsed_schedule) = Schedule::from_str(schedule_str) { let now = Utc::now(); let next_run = parsed_schedule.upcoming(Utc).next(); - if let Some(next_time) = next_run { let time_until_next = next_time - now; if time_until_next.num_minutes() < 1 { @@ -65,44 +53,36 @@ impl AutomationService { continue; } } - self.execute_automation(&automation).await?; - diesel::update(system_automations.filter(id.eq(automation.id))) .set(lt_column.eq(Some(now))) - .execute(&mut *conn)?; + .execute(&mut conn)?; } } } } } - Ok(()) } - async fn execute_automation( &self, automation: &Automation, ) -> Result<(), Box> { - info!("Executing automation: {}", automation.param); - let bot_name: String = { use crate::shared::models::schema::bots::dsl::*; let mut conn = self .state .conn - .lock() - .map_err(|e| format!("Lock failed: {}", e))?; + .get() + .map_err(|e| format!("Failed to acquire database connection: {}", e))?; bots.filter(id.eq(automation.bot_id)) .select(name) - .first(&mut *conn)? + .first(&mut conn)? }; - let script_path = format!( "./work/{}.gbai/{}.gbdialog/{}.ast", bot_name, bot_name, automation.param ); - let script_content = match tokio::fs::read_to_string(&script_path).await { Ok(content) => content, Err(e) => { @@ -110,16 +90,13 @@ impl AutomationService { return Ok(()); } }; - let session = { let mut sm = self.state.session_manager.lock().await; let admin_user = uuid::Uuid::nil(); sm.get_or_create_user_session(admin_user, automation.bot_id, "Automation")? .ok_or("Failed to create session")? }; - let script_service = ScriptService::new(Arc::clone(&self.state), session); - match script_service.compile(&script_content) { Ok(ast) => { if let Err(e) = script_service.run(&ast) { @@ -130,74 +107,58 @@ impl AutomationService { error!("Script compilation failed: {}", e); } } - Ok(()) } - async fn execute_compact_prompt( &self, automation: &Automation, ) -> Result<(), Box> { - info!("Executing prompt compaction for bot: {}", automation.bot_id); - - let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); + let config_manager = ConfigManager::new(self.state.conn.clone()); let compact_threshold = config_manager .get_config(&automation.bot_id, "prompt-compact", None)? .parse::() .unwrap_or(0); - if compact_threshold == 0 { return Ok(()); } - let mut session_manager = self.state.session_manager.lock().await; let sessions = session_manager.get_user_sessions(uuid::Uuid::nil())?; - for session in sessions { if session.bot_id != automation.bot_id { continue; } - let history = session_manager.get_conversation_history(session.id, session.user_id)?; - if history.len() > compact_threshold { - info!( + trace!( "Compacting prompt for session {}: {} messages", session.id, history.len() ); - let mut compacted = String::new(); for (role, content) in &history[..history.len() - compact_threshold] { compacted.push_str(&format!("{}: {}\n", role, content)); } - let summarized = format!("SUMMARY: {}", compacted); - session_manager.save_message(session.id, session.user_id, 3, &summarized, 1)?; } } - Ok(()) } } - -pub async fn execute_compact_prompt(state: Arc) -> Result<(), Box> { +pub async fn execute_compact_prompt( + state: Arc, +) -> Result<(), Box> { use crate::shared::models::system_automations::dsl::{is_active, system_automations}; use diesel::prelude::*; - use log::info; - let state_clone = state.clone(); -let service = AutomationService::new(state_clone); - + let service = AutomationService::new(state_clone); let mut conn = state .conn - .lock() - .map_err(|e| format!("Failed to acquire lock: {}", e))?; + .get() + .map_err(|e| format!("Failed to acquire database connection: {}", e))?; let automations: Vec = system_automations .filter(is_active.eq(true)) - .load::(&mut *conn)?; - + .load::(&mut conn)?; for automation in automations { if let Err(e) = service.execute_compact_prompt(&automation).await { error!( @@ -206,7 +167,5 @@ let service = AutomationService::new(state_clone); ); } } - - info!("Prompt compaction cycle completed"); Ok(()) } diff --git a/src/basic/compiler/mod.rs b/src/basic/compiler/mod.rs index 0f9ab8415..7379472d1 100644 --- a/src/basic/compiler/mod.rs +++ b/src/basic/compiler/mod.rs @@ -1,18 +1,17 @@ -use crate::shared::state::AppState; use crate::basic::keywords::set_schedule::execute_set_schedule; +use crate::shared::models::TriggerKind; +use crate::shared::state::AppState; +use diesel::ExpressionMethods; +use diesel::QueryDsl; +use diesel::RunQueryDsl; use log::warn; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use diesel::QueryDsl; -use diesel::ExpressionMethods; use std::collections::HashSet; -use diesel::RunQueryDsl; -use crate::shared::models::TriggerKind; use std::error::Error; use std::fs; use std::path::Path; use std::sync::Arc; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ParamDeclaration { pub name: String, @@ -21,7 +20,6 @@ pub struct ParamDeclaration { pub description: String, pub required: bool, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDefinition { pub name: String, @@ -29,14 +27,12 @@ pub struct ToolDefinition { pub parameters: Vec, pub source_file: String, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MCPTool { pub name: String, pub description: String, pub input_schema: MCPInputSchema, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MCPInputSchema { #[serde(rename = "type")] @@ -44,7 +40,6 @@ pub struct MCPInputSchema { pub properties: HashMap, pub required: Vec, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MCPProperty { #[serde(rename = "type")] @@ -53,21 +48,18 @@ pub struct MCPProperty { #[serde(skip_serializing_if = "Option::is_none")] pub example: Option, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenAITool { #[serde(rename = "type")] pub tool_type: String, pub function: OpenAIFunction, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenAIFunction { pub name: String, pub description: String, pub parameters: OpenAIParameters, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenAIParameters { #[serde(rename = "type")] @@ -75,7 +67,6 @@ pub struct OpenAIParameters { pub properties: HashMap, pub required: Vec, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenAIProperty { #[serde(rename = "type")] @@ -84,13 +75,11 @@ pub struct OpenAIProperty { #[serde(skip_serializing_if = "Option::is_none")] pub example: Option, } - pub struct BasicCompiler { state: Arc, bot_id: uuid::Uuid, previous_schedules: HashSet, } - impl BasicCompiler { pub fn new(state: Arc, bot_id: uuid::Uuid) -> Self { Self { @@ -99,7 +88,6 @@ impl BasicCompiler { previous_schedules: HashSet::new(), } } - pub fn compile_file( &mut self, source_path: &str, @@ -107,46 +95,35 @@ impl BasicCompiler { ) -> Result> { let source_content = fs::read_to_string(source_path) .map_err(|e| format!("Failed to read source file: {}", e))?; - let tool_def = self.parse_tool_definition(&source_content, source_path)?; - let file_name = Path::new(source_path) .file_stem() .and_then(|s| s.to_str()) .ok_or("Invalid file name")?; - let ast_path = format!("{}/{}.ast", output_dir, file_name); let ast_content = self.preprocess_basic(&source_content, source_path, self.bot_id)?; - fs::write(&ast_path, &ast_content) .map_err(|e| format!("Failed to write AST file: {}", e))?; - let (mcp_json, tool_json) = if !tool_def.parameters.is_empty() { let mcp = self.generate_mcp_tool(&tool_def)?; let openai = self.generate_openai_tool(&tool_def)?; - let mcp_path = format!("{}/{}.mcp.json", output_dir, file_name); let tool_path = format!("{}/{}.tool.json", output_dir, file_name); - let mcp_json_str = serde_json::to_string_pretty(&mcp)?; fs::write(&mcp_path, mcp_json_str) .map_err(|e| format!("Failed to write MCP JSON: {}", e))?; - let tool_json_str = serde_json::to_string_pretty(&openai)?; fs::write(&tool_path, tool_json_str) .map_err(|e| format!("Failed to write tool JSON: {}", e))?; - (Some(mcp), Some(openai)) } else { (None, None) }; - Ok(CompilationResult { mcp_tool: mcp_json, _openai_tool: tool_json, }) } - pub fn parse_tool_definition( &self, source: &str, @@ -156,16 +133,13 @@ impl BasicCompiler { let mut description = String::new(); let lines: Vec<&str> = source.lines().collect(); let mut i = 0; - while i < lines.len() { let line = lines[i].trim(); - if line.starts_with("PARAM ") { if let Some(param) = self.parse_param_line(line)? { params.push(param); } } - if line.starts_with("DESCRIPTION ") { let desc_start = line.find('"').unwrap_or(0); let desc_end = line.rfind('"').unwrap_or(line.len()); @@ -173,16 +147,13 @@ impl BasicCompiler { description = line[desc_start + 1..desc_end].to_string(); } } - i += 1; } - let tool_name = Path::new(source_path) .file_stem() .and_then(|s| s.to_str()) .unwrap_or("unknown") .to_string(); - Ok(ToolDefinition { name: tool_name, description, @@ -190,7 +161,6 @@ impl BasicCompiler { source_file: source_path.to_string(), }) } - fn parse_param_line( &self, line: &str, @@ -199,15 +169,12 @@ impl BasicCompiler { if !line.starts_with("PARAM ") { return Ok(None); } - let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 4 { warn!("Invalid PARAM line: {}", line); return Ok(None); } - let name = parts[1].to_string(); - let as_index = parts.iter().position(|&p| p == "AS"); let param_type = if let Some(idx) = as_index { if idx + 1 < parts.len() { @@ -218,7 +185,6 @@ impl BasicCompiler { } else { "string".to_string() }; - let example = if let Some(like_pos) = line.find("LIKE") { let rest = &line[like_pos + 4..].trim(); if let Some(start) = rest.find('"') { @@ -233,7 +199,6 @@ impl BasicCompiler { } else { None }; - let description = if let Some(desc_pos) = line.find("DESCRIPTION") { let rest = &line[desc_pos + 11..].trim(); if let Some(start) = rest.find('"') { @@ -248,7 +213,6 @@ impl BasicCompiler { } else { "".to_string() }; - Ok(Some(ParamDeclaration { name, param_type: self.normalize_type(¶m_type), @@ -257,7 +221,6 @@ impl BasicCompiler { required: true, })) } - fn normalize_type(&self, basic_type: &str) -> String { match basic_type.to_lowercase().as_str() { "string" | "text" => "string".to_string(), @@ -270,14 +233,12 @@ impl BasicCompiler { _ => "string".to_string(), } } - fn generate_mcp_tool( &self, tool_def: &ToolDefinition, ) -> Result> { let mut properties = HashMap::new(); let mut required = Vec::new(); - for param in &tool_def.parameters { properties.insert( param.name.clone(), @@ -291,7 +252,6 @@ impl BasicCompiler { required.push(param.name.clone()); } } - Ok(MCPTool { name: tool_def.name.clone(), description: tool_def.description.clone(), @@ -302,14 +262,12 @@ impl BasicCompiler { }, }) } - fn generate_openai_tool( &self, tool_def: &ToolDefinition, ) -> Result> { let mut properties = HashMap::new(); let mut required = Vec::new(); - for param in &tool_def.parameters { properties.insert( param.name.clone(), @@ -323,7 +281,6 @@ impl BasicCompiler { required.push(param.name.clone()); } } - Ok(OpenAITool { tool_type: "function".to_string(), function: OpenAIFunction { @@ -337,38 +294,45 @@ impl BasicCompiler { }, }) } - - fn preprocess_basic(&mut self, source: &str, source_path: &str, bot_id: uuid::Uuid) -> Result> { + fn preprocess_basic( + &mut self, + source: &str, + source_path: &str, + bot_id: uuid::Uuid, + ) -> Result> { let bot_uuid = bot_id; let mut result = String::new(); let mut has_schedule = false; - let script_name = Path::new(source_path) .file_stem() .and_then(|s| s.to_str()) .unwrap_or("unknown") .to_string(); - { - let mut conn = self.state.conn.lock().unwrap(); + let mut conn = self + .state + .conn + .get() + .map_err(|e| format!("Failed to get database connection: {}", e))?; use crate::shared::models::system_automations::dsl::*; - - diesel::delete(system_automations - .filter(bot_id.eq(bot_uuid)) - .filter(kind.eq(TriggerKind::Scheduled as i32)) - .filter(param.eq(&script_name)) + diesel::delete( + system_automations + .filter(bot_id.eq(bot_uuid)) + .filter(kind.eq(TriggerKind::Scheduled as i32)) + .filter(param.eq(&script_name)), ) - .execute(&mut *conn) + .execute(&mut conn) .ok(); } - for line in source.lines() { let trimmed = line.trim(); - - if trimmed.is_empty() || trimmed.starts_with("'") || trimmed.starts_with("//") || trimmed.starts_with("REM") { + if trimmed.is_empty() + || trimmed.starts_with("'") + || trimmed.starts_with("//") + || trimmed.starts_with("REM") + { continue; } - let normalized = trimmed .replace("SET SCHEDULE", "SET_SCHEDULE") .replace("ADD TOOL", "ADD_TOOL") @@ -387,54 +351,58 @@ impl BasicCompiler { .replace("GET BOT MEMORY", "GET_BOT_MEMORY") .replace("SET BOT MEMORY", "SET_BOT_MEMORY") .replace("CREATE DRAFT", "CREATE_DRAFT"); - if normalized.starts_with("SET_SCHEDULE") { has_schedule = true; let parts: Vec<&str> = normalized.split('"').collect(); if parts.len() >= 3 { let cron = parts[1]; - let mut conn = self.state.conn.lock().unwrap(); - if let Err(e) = execute_set_schedule(&mut *conn, cron, &script_name, bot_id) { - log::error!("Failed to schedule SET_SCHEDULE during preprocessing: {}", e); + let mut conn = self + .state + .conn + .get() + .map_err(|e| format!("Failed to get database connection: {}", e))?; + if let Err(e) = execute_set_schedule(&mut conn, cron, &script_name, bot_id) { + log::error!( + "Failed to schedule SET_SCHEDULE during preprocessing: {}", + e + ); } } else { log::warn!("Malformed SET_SCHEDULE line ignored: {}", normalized); } continue; } - if normalized.starts_with("PARAM ") || normalized.starts_with("DESCRIPTION ") { continue; } - result.push_str(&normalized); result.push('\n'); } - if self.previous_schedules.contains(&script_name) && !has_schedule { - let mut conn = self.state.conn.lock().unwrap(); + let mut conn = self + .state + .conn + .get() + .map_err(|e| format!("Failed to get database connection: {}", e))?; use crate::shared::models::system_automations::dsl::*; - - diesel::delete(system_automations - .filter(bot_id.eq(bot_uuid)) - .filter(kind.eq(TriggerKind::Scheduled as i32)) - .filter(param.eq(&script_name)) + diesel::delete( + system_automations + .filter(bot_id.eq(bot_uuid)) + .filter(kind.eq(TriggerKind::Scheduled as i32)) + .filter(param.eq(&script_name)), ) - .execute(&mut *conn) + .execute(&mut conn) .map_err(|e| log::error!("Failed to remove schedule for {}: {}", script_name, e)) .ok(); } - if has_schedule { self.previous_schedules.insert(script_name); } else { self.previous_schedules.remove(&script_name); } - Ok(result) } } - #[derive(Debug)] pub struct CompilationResult { pub mcp_tool: Option, diff --git a/src/basic/keywords/add_suggestion.rs b/src/basic/keywords/add_suggestion.rs index b7688bc98..a66622f68 100644 --- a/src/basic/keywords/add_suggestion.rs +++ b/src/basic/keywords/add_suggestion.rs @@ -1,20 +1,19 @@ -use crate::shared::state::AppState; use crate::shared::models::UserSession; -use log::{trace, debug, error, info}; +use crate::shared::state::AppState; +use log::{error, trace}; use rhai::{Dynamic, Engine}; use serde_json::json; use std::sync::Arc; - -pub fn clear_suggestions_keyword(state: Arc, user_session: UserSession, engine: &mut Engine) { +pub fn clear_suggestions_keyword( + state: Arc, + user_session: UserSession, + engine: &mut Engine, +) { let cache = state.cache.clone(); - engine .register_custom_syntax(&["CLEAR_SUGGESTIONS"], true, move |_context, _inputs| { - info!("CLEAR_SUGGESTIONS command executed"); - if let Some(cache_client) = &cache { let redis_key = format!("suggestions:{}:{}", user_session.user_id, user_session.id); - let mut conn = match cache_client.get_connection() { Ok(conn) => conn, Err(e) => { @@ -22,81 +21,83 @@ pub fn clear_suggestions_keyword(state: Arc, user_session: UserSession return Ok(Dynamic::UNIT); } }; - - // Delete the suggestions list - let result: Result = redis::cmd("DEL") - .arg(&redis_key) - .query(&mut conn); - + let result: Result = + redis::cmd("DEL").arg(&redis_key).query(&mut conn); match result { Ok(deleted) => { - trace!("Cleared suggestions from Redis key {}, deleted: {}", redis_key, deleted); + trace!( + "Cleared {} suggestions from session {}", + deleted, + user_session.id + ); } Err(e) => error!("Failed to clear suggestions from Redis: {}", e), } } else { - debug!("No Cache client configured; suggestions not cleared"); + trace!("No cache configured, suggestions not cleared"); } - Ok(Dynamic::UNIT) }) .unwrap(); } - -pub fn add_suggestion_keyword(state: Arc, user_session: UserSession, engine: &mut Engine) { +pub fn add_suggestion_keyword( + state: Arc, + user_session: UserSession, + engine: &mut Engine, +) { let cache = state.cache.clone(); - engine - .register_custom_syntax(&["ADD_SUGGESTION", "$expr$", "AS", "$expr$"], true, move |context, inputs| { - let context_name = context.eval_expression_tree(&inputs[0])?.to_string(); - let button_text = context.eval_expression_tree(&inputs[1])?.to_string(); - - info!("ADD_SUGGESTION command executed: context='{}', text='{}'", context_name, button_text); - - if let Some(cache_client) = &cache { - let redis_key = format!("suggestions:{}:{}", user_session.user_id, user_session.id); - let suggestion = json!({ "context": context_name, "text": button_text }); - - let mut conn = match cache_client.get_connection() { - Ok(conn) => conn, - Err(e) => { - error!("Failed to connect to cache: {}", e); - return Ok(Dynamic::UNIT); - } - }; - - // Append suggestion to Redis list - RPUSH returns the new length as i64 - let result: Result = redis::cmd("RPUSH") - .arg(&redis_key) - .arg(suggestion.to_string()) - .query(&mut conn); - - match result { - Ok(length) => { - trace!("Suggestion added successfully to Redis key {}, new length: {}", redis_key, length); - - // Also register context as inactive initially - let active_key = format!("active_context:{}:{}", user_session.user_id, user_session.id); - let hset_result: Result = redis::cmd("HSET") - .arg(&active_key) - .arg(&context_name) - .arg("inactive") - .query(&mut conn); - - match hset_result { - Ok(fields_added) => { - trace!("Context state set to inactive for {}, fields added: {}", context_name, fields_added) - }, - Err(e) => error!("Failed to set context state: {}", e), + .register_custom_syntax( + &["ADD_SUGGESTION", "$expr$", "AS", "$expr$"], + true, + move |context, inputs| { + let context_name = context.eval_expression_tree(&inputs[0])?.to_string(); + let button_text = context.eval_expression_tree(&inputs[1])?.to_string(); + if let Some(cache_client) = &cache { + let redis_key = + format!("suggestions:{}:{}", user_session.user_id, user_session.id); + let suggestion = json!({ "context": context_name, "text": button_text }); + let mut conn = match cache_client.get_connection() { + Ok(conn) => conn, + Err(e) => { + error!("Failed to connect to cache: {}", e); + return Ok(Dynamic::UNIT); } + }; + let result: Result = redis::cmd("RPUSH") + .arg(&redis_key) + .arg(suggestion.to_string()) + .query(&mut conn); + match result { + Ok(length) => { + trace!( + "Added suggestion to session {}, total suggestions: {}", + user_session.id, + length + ); + let active_key = format!( + "active_context:{}:{}", + user_session.user_id, user_session.id + ); + let hset_result: Result = redis::cmd("HSET") + .arg(&active_key) + .arg(&context_name) + .arg("inactive") + .query(&mut conn); + match hset_result { + Ok(_fields_added) => { + trace!("Set context state for session {}", user_session.id); + } + Err(e) => error!("Failed to set context state: {}", e), + } + } + Err(e) => error!("Failed to add suggestion to Redis: {}", e), } - Err(e) => error!("Failed to add suggestion to Redis: {}", e), + } else { + trace!("No cache configured, suggestion not added"); } - } else { - debug!("No Cache client configured; suggestion will not persist"); - } - - Ok(Dynamic::UNIT) - }) + Ok(Dynamic::UNIT) + }, + ) .unwrap(); } diff --git a/src/basic/keywords/add_tool.rs b/src/basic/keywords/add_tool.rs index 4695b6506..531af825f 100644 --- a/src/basic/keywords/add_tool.rs +++ b/src/basic/keywords/add_tool.rs @@ -1,223 +1,115 @@ use crate::shared::models::UserSession; use crate::shared::state::AppState; use diesel::prelude::*; -use log::{error, info, warn}; +use log::{error, trace, warn}; use rhai::{Dynamic, Engine}; use std::sync::Arc; use uuid::Uuid; - pub fn add_tool_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - - engine - .register_custom_syntax(&["ADD_TOOL", "$expr$"], false, move |context, inputs| { - let tool_path = context.eval_expression_tree(&inputs[0])?; - let tool_path_str = tool_path.to_string().trim_matches('"').to_string(); - - info!( - "ADD_TOOL command executed: {} for session: {}", - tool_path_str, user_clone.id - ); - - // Extract tool name from path (e.g., "enrollment.bas" -> "enrollment") - let tool_name = tool_path_str - .strip_prefix(".gbdialog/") - .unwrap_or(&tool_path_str) - .strip_suffix(".bas") - .unwrap_or(&tool_path_str) - .to_string(); - - // Validate tool name - if tool_name.is_empty() { - return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "Invalid tool name".into(), - rhai::Position::NONE, - ))); - } - - let state_for_task = Arc::clone(&state_clone); - let user_for_task = user_clone.clone(); - let tool_name_for_task = tool_name.clone(); - - // Spawn async task to associate tool with session - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build(); - - let send_err = if let Ok(rt) = rt { - let result = rt.block_on(async move { - associate_tool_with_session( - &state_for_task, - &user_for_task, - &tool_name_for_task, - ) - .await - }); - tx.send(result).err() - } else { - tx.send(Err("Failed to build tokio runtime".to_string())) - .err() - }; - - if send_err.is_some() { - error!("Failed to send result from thread"); - } - }); - - match rx.recv_timeout(std::time::Duration::from_secs(10)) { - Ok(Ok(message)) => { - info!("ADD_TOOL completed: {}", message); - Ok(Dynamic::from(message)) - } - Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - e.into(), - rhai::Position::NONE, - ))), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "ADD_TOOL timed out".into(), - rhai::Position::NONE, - ))) - } - Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - format!("ADD_TOOL failed: {}", e).into(), - rhai::Position::NONE, - ))), - } - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["ADD_TOOL", "$expr$"], false, move |context, inputs| { + let tool_path = context.eval_expression_tree(&inputs[0])?; + let tool_path_str = tool_path.to_string().trim_matches('"').to_string(); + trace!("ADD_TOOL command executed: {} for session: {}", tool_path_str, user_clone.id); + let tool_name = tool_path_str.strip_prefix(".gbdialog/").unwrap_or(&tool_path_str).strip_suffix(".bas").unwrap_or(&tool_path_str).to_string(); + if tool_name.is_empty() { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime("Invalid tool name".into(), rhai::Position::NONE))); + } + let state_for_task = Arc::clone(&state_clone); + let user_for_task = user_clone.clone(); + let tool_name_for_task = tool_name.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build(); + let send_err = if let Ok(rt) = rt { + let result = rt.block_on(async move { + associate_tool_with_session(&state_for_task, &user_for_task, &tool_name_for_task).await + }); + tx.send(result).err() + } else { + tx.send(Err("Failed to build tokio runtime".to_string())).err() + }; + if send_err.is_some() { + error!("Failed to send result from thread"); + } + }); + match rx.recv_timeout(std::time::Duration::from_secs(10)) { + Ok(Ok(message)) => { + Ok(Dynamic::from(message)) + } + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime("ADD_TOOL timed out".into(), rhai::Position::NONE))) + } + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("ADD_TOOL failed: {}", e).into(), rhai::Position::NONE))), + } + }) + .unwrap(); } - -/// Associate a compiled tool with the current session -/// The tool must already be compiled and present in the basic_tools table -async fn associate_tool_with_session( - state: &AppState, - user: &UserSession, - tool_name: &str, -) -> Result { - use crate::shared::models::schema::{basic_tools, session_tool_associations}; - - let mut conn = state.conn.lock().map_err(|e| { - error!("Failed to acquire database lock: {}", e); - format!("Database connection error: {}", e) - })?; - - // First, verify the tool exists and is active for this bot - let tool_exists: Result = basic_tools::table - .filter(basic_tools::bot_id.eq(user.bot_id.to_string())) - .filter(basic_tools::tool_name.eq(tool_name)) - .filter(basic_tools::is_active.eq(1)) - .select(diesel::dsl::count(basic_tools::id)) - .first::(&mut *conn) - .map(|count| count > 0); - - match tool_exists { - Ok(true) => { - info!( - "Tool '{}' exists and is active for bot '{}'", - tool_name, user.bot_id - ); - } - Ok(false) => { - warn!( - "Tool '{}' does not exist or is not active for bot '{}'", - tool_name, user.bot_id - ); - return Err(format!( - "Tool '{}' is not available. Make sure the tool file is compiled and active.", - tool_name - )); - } - Err(e) => { - error!("Failed to check tool existence: {}", e); - return Err(format!("Database error while checking tool: {}", e)); - } - } - - // Generate a unique ID for the association - let association_id = Uuid::new_v4().to_string(); - let session_id_str = user.id.to_string(); - let added_at = chrono::Utc::now().to_rfc3339(); - - // Insert the tool association (ignore if already exists due to UNIQUE constraint) - let insert_result: Result = - diesel::insert_into(session_tool_associations::table) - .values(( - session_tool_associations::id.eq(&association_id), - session_tool_associations::session_id.eq(&session_id_str), - session_tool_associations::tool_name.eq(tool_name), - session_tool_associations::added_at.eq(&added_at), - )) - .on_conflict(( - session_tool_associations::session_id, - session_tool_associations::tool_name, - )) - .do_nothing() - .execute(&mut *conn); - - match insert_result { - Ok(rows_affected) => { - if rows_affected > 0 { - info!( - "Tool '{}' newly associated with session '{}' (user: {}, bot: {})", - tool_name, user.id, user.user_id, user.bot_id - ); - Ok(format!( - "Tool '{}' is now available in this conversation", - tool_name - )) - } else { - info!( - "Tool '{}' was already associated with session '{}'", - tool_name, user.id - ); - Ok(format!( - "Tool '{}' is already available in this conversation", - tool_name - )) - } - } - Err(e) => { - error!( - "Failed to associate tool '{}' with session '{}': {}", - tool_name, user.id, e - ); - Err(format!("Failed to add tool to session: {}", e)) - } - } +async fn associate_tool_with_session(state: &AppState, user: &UserSession, tool_name: &str) -> Result { + use crate::shared::models::schema::{basic_tools, session_tool_associations}; + let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?; + let tool_exists: Result = basic_tools::table + .filter(basic_tools::bot_id.eq(user.bot_id.to_string())) + .filter(basic_tools::tool_name.eq(tool_name)) + .filter(basic_tools::is_active.eq(1)) + .select(diesel::dsl::count(basic_tools::id)) + .first::(&mut *conn) + .map(|count| count > 0); + match tool_exists { + Ok(true) => { + trace!("Tool '{}' exists and is active for bot '{}'", tool_name, user.bot_id); + } + Ok(false) => { + warn!("Tool '{}' does not exist or is not active for bot '{}'", tool_name, user.bot_id); + return Err(format!("Tool '{}' is not available. Make sure the tool file is compiled and active.", tool_name)); + } + Err(e) => { + error!("Failed to check tool existence: {}", e); + return Err(format!("Database error while checking tool: {}", e)); + } + } + let association_id = Uuid::new_v4().to_string(); + let session_id_str = user.id.to_string(); + let added_at = chrono::Utc::now().to_rfc3339(); + let insert_result: Result = diesel::insert_into(session_tool_associations::table) + .values(( + session_tool_associations::id.eq(&association_id), + session_tool_associations::session_id.eq(&session_id_str), + session_tool_associations::tool_name.eq(tool_name), + session_tool_associations::added_at.eq(&added_at), + )) + .on_conflict((session_tool_associations::session_id, session_tool_associations::tool_name)) + .do_nothing() + .execute(&mut *conn); + match insert_result { + Ok(rows_affected) => { + if rows_affected > 0 { + trace!("Tool '{}' newly associated with session '{}' (user: {}, bot: {})", tool_name, user.id, user.user_id, user.bot_id); + Ok(format!("Tool '{}' is now available in this conversation", tool_name)) + } else { + trace!("Tool '{}' was already associated with session '{}'", tool_name, user.id); + Ok(format!("Tool '{}' is already available in this conversation", tool_name)) + } + } + Err(e) => { + error!("Failed to associate tool '{}' with session '{}': {}", tool_name, user.id, e); + Err(format!("Failed to add tool to session: {}", e)) + } + } } - -/// Get all tools associated with a session -pub fn get_session_tools( - conn: &mut PgConnection, - session_id: &Uuid, -) -> Result, diesel::result::Error> { - use crate::shared::models::schema::session_tool_associations; - - let session_id_str = session_id.to_string(); - - session_tool_associations::table - .filter(session_tool_associations::session_id.eq(&session_id_str)) - .select(session_tool_associations::tool_name) - .load::(conn) +pub fn get_session_tools(conn: &mut PgConnection, session_id: &Uuid) -> Result, diesel::result::Error> { + use crate::shared::models::schema::session_tool_associations; + let session_id_str = session_id.to_string(); + session_tool_associations::table + .filter(session_tool_associations::session_id.eq(&session_id_str)) + .select(session_tool_associations::tool_name) + .load::(conn) } - -/// Clear all tool associations for a session -pub fn clear_session_tools( - conn: &mut PgConnection, - session_id: &Uuid, -) -> Result { - use crate::shared::models::schema::session_tool_associations; - - let session_id_str = session_id.to_string(); - - diesel::delete( - session_tool_associations::table - .filter(session_tool_associations::session_id.eq(&session_id_str)), - ) - .execute(conn) +pub fn clear_session_tools(conn: &mut PgConnection, session_id: &Uuid) -> Result { + use crate::shared::models::schema::session_tool_associations; + let session_id_str = session_id.to_string(); + diesel::delete(session_tool_associations::table.filter(session_tool_associations::session_id.eq(&session_id_str))).execute(conn) } diff --git a/src/basic/keywords/add_website.rs b/src/basic/keywords/add_website.rs index f74885c27..26b925da1 100644 --- a/src/basic/keywords/add_website.rs +++ b/src/basic/keywords/add_website.rs @@ -1,91 +1,51 @@ use crate::shared::models::UserSession; use crate::shared::state::AppState; -use log::{error, info}; +use log::{error, trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; - pub fn add_website_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - - engine - .register_custom_syntax(&["ADD_WEBSITE", "$expr$"], false, move |context, inputs| { - let url = context.eval_expression_tree(&inputs[0])?; - let url_str = url.to_string().trim_matches('"').to_string(); - - info!( - "ADD_WEBSITE command executed: {} for user: {}", - url_str, user_clone.user_id - ); - - // Validate URL - let is_valid = url_str.starts_with("http://") || url_str.starts_with("https://"); - - if !is_valid { - return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "Invalid URL format. Must start with http:// or https://".into(), - rhai::Position::NONE, - ))); - } - - let state_for_task = Arc::clone(&state_clone); - let user_for_task = user_clone.clone(); - let url_for_task = url_str.clone(); - - // Spawn async task to crawl and index website - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build(); - - let send_err = if let Ok(rt) = rt { - let result = rt.block_on(async move { - crawl_and_index_website(&state_for_task, &user_for_task, &url_for_task) - .await - }); - tx.send(result).err() - } else { - tx.send(Err("Failed to build tokio runtime".to_string())) - .err() - }; - - if send_err.is_some() { - error!("Failed to send result from thread"); - } - }); - - match rx.recv_timeout(std::time::Duration::from_secs(120)) { - Ok(Ok(message)) => { - info!("ADD_WEBSITE completed: {}", message); - Ok(Dynamic::from(message)) - } - Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - e.into(), - rhai::Position::NONE, - ))), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "ADD_WEBSITE timed out".into(), - rhai::Position::NONE, - ))) - } - Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - format!("ADD_WEBSITE failed: {}", e).into(), - rhai::Position::NONE, - ))), - } - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["ADD_WEBSITE", "$expr$"], false, move |context, inputs| { + let url = context.eval_expression_tree(&inputs[0])?; + let url_str = url.to_string().trim_matches('"').to_string(); + trace!("ADD_WEBSITE command executed: {} for user: {}", url_str, user_clone.user_id); + let is_valid = url_str.starts_with("http://") || url_str.starts_with("https://"); + if !is_valid { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime("Invalid URL format. Must start with http:// or https://".into(), rhai::Position::NONE))); + } + let state_for_task = Arc::clone(&state_clone); + let user_for_task = user_clone.clone(); + let url_for_task = url_str.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build(); + let send_err = if let Ok(rt) = rt { + let result = rt.block_on(async move { + crawl_and_index_website(&state_for_task, &user_for_task, &url_for_task).await + }); + tx.send(result).err() + } else { + tx.send(Err("Failed to build tokio runtime".to_string())).err() + }; + if send_err.is_some() { + error!("Failed to send result from thread"); + } + }); + match rx.recv_timeout(std::time::Duration::from_secs(120)) { + Ok(Ok(message)) => { + Ok(Dynamic::from(message)) + } + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime("ADD_WEBSITE timed out".into(), rhai::Position::NONE))) + } + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("ADD_WEBSITE failed: {}", e).into(), rhai::Position::NONE))), + } + }) + .unwrap(); } - -/// Crawl website and index content -async fn crawl_and_index_website( - _state: &AppState, - user: &UserSession, - url: &str, -) -> Result { - info!("Crawling website: {} for user: {}", url, user.user_id); - Err("Web automation functionality has been removed from this build".to_string()) +async fn crawl_and_index_website(_state: &AppState, _user: &UserSession, _url: &str) -> Result { + Err("Web automation functionality has been removed from this build".to_string()) } diff --git a/src/basic/keywords/bot_memory.rs b/src/basic/keywords/bot_memory.rs index 6de022045..10b249686 100644 --- a/src/basic/keywords/bot_memory.rs +++ b/src/basic/keywords/bot_memory.rs @@ -1,7 +1,7 @@ use crate::shared::models::UserSession; use crate::shared::state::AppState; use diesel::prelude::*; -use log::{error, info, trace}; +use log::{error, trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; use uuid::Uuid; @@ -9,122 +9,84 @@ use uuid::Uuid; pub fn set_bot_memory_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); - engine - .register_custom_syntax( - &["SET_BOT_MEMORY", "$expr$", ",", "$expr$"], - false, - move |context, inputs| { - let key = context.eval_expression_tree(&inputs[0])?.to_string(); - let value = context.eval_expression_tree(&inputs[1])?.to_string(); - - let state_for_spawn = Arc::clone(&state_clone); - let user_clone_spawn = user_clone.clone(); - let key_clone = key.clone(); - let value_clone = value.clone(); - - tokio::spawn(async move { - use crate::shared::models::bot_memories; - - let mut conn = match state_for_spawn.conn.lock() { - Ok(conn) => conn, - Err(e) => { - error!( - "Failed to acquire database connection for SET BOT MEMORY: {}", - e - ); - return; - } - }; - - let bot_uuid = match Uuid::parse_str(&user_clone_spawn.bot_id.to_string()) { - Ok(uuid) => uuid, - Err(e) => { - error!("Invalid bot ID format: {}", e); - return; - } - }; - - let now = chrono::Utc::now(); - - let existing_memory: Option = bot_memories::table - .filter(bot_memories::bot_id.eq(bot_uuid)) - .filter(bot_memories::key.eq(&key_clone)) - .select(bot_memories::id) - .first(&mut *conn) - .optional() - .unwrap_or(None); - - if let Some(memory_id) = existing_memory { - let update_result = diesel::update( - bot_memories::table.filter(bot_memories::id.eq(memory_id)), - ) - .set(( - bot_memories::value.eq(&value_clone), - bot_memories::updated_at.eq(now), - )) + .register_custom_syntax(&["SET_BOT_MEMORY", "$expr$", ",", "$expr$"], false, move |context, inputs| { + let key = context.eval_expression_tree(&inputs[0])?.to_string(); + let value = context.eval_expression_tree(&inputs[1])?.to_string(); + let state_for_spawn = Arc::clone(&state_clone); + let user_clone_spawn = user_clone.clone(); + let key_clone = key.clone(); + let value_clone = value.clone(); + tokio::spawn(async move { + use crate::shared::models::bot_memories; + let mut conn = match state_for_spawn.conn.get() { + Ok(conn) => conn, + Err(e) => { + error!("Failed to acquire database connection for SET BOT MEMORY: {}", e); + return; + } + }; + let bot_uuid = match Uuid::parse_str(&user_clone_spawn.bot_id.to_string()) { + Ok(uuid) => uuid, + Err(e) => { + error!("Invalid bot ID format: {}", e); + return; + } + }; + let now = chrono::Utc::now(); + let existing_memory: Option = bot_memories::table + .filter(bot_memories::bot_id.eq(bot_uuid)) + .filter(bot_memories::key.eq(&key_clone)) + .select(bot_memories::id) + .first(&mut *conn) + .optional() + .unwrap_or(None); + if let Some(memory_id) = existing_memory { + let update_result = diesel::update(bot_memories::table.filter(bot_memories::id.eq(memory_id))) + .set((bot_memories::value.eq(&value_clone), bot_memories::updated_at.eq(now))) .execute(&mut *conn); - - match update_result { - Ok(_) => { - info!( - "Updated bot memory for key: {} with value length: {}", - key_clone, - value_clone.len() - ); - } - Err(e) => { - error!("Failed to update bot memory: {}", e); - } + match update_result { + Ok(_) => { + trace!("Updated bot memory for key: {} with value length: {}", key_clone, value_clone.len()); } - } else { - let new_memory = crate::shared::models::BotMemory { - id: Uuid::new_v4(), - bot_id: bot_uuid, - key: key_clone.clone(), - value: value_clone.clone(), - created_at: now, - updated_at: now, - }; - - let insert_result = diesel::insert_into(bot_memories::table) - .values(&new_memory) - .execute(&mut *conn); - - match insert_result { - Ok(_) => { - info!( - "Created new bot memory for key: {} with value length: {}", - key_clone, - value_clone.len() - ); - } - Err(e) => { - error!("Failed to insert bot memory: {}", e); - } + Err(e) => { + error!("Failed to update bot memory: {}", e); } } - }); - - Ok(Dynamic::UNIT) - }, - ) + } else { + let new_memory = crate::shared::models::BotMemory { + id: Uuid::new_v4(), + bot_id: bot_uuid, + key: key_clone.clone(), + value: value_clone.clone(), + created_at: now, + updated_at: now, + }; + let insert_result = diesel::insert_into(bot_memories::table).values(&new_memory).execute(&mut *conn); + match insert_result { + Ok(_) => { + trace!("Created new bot memory for key: {} with value length: {}", key_clone, value_clone.len()); + } + Err(e) => { + error!("Failed to insert bot memory: {}", e); + } + } + } + }); + Ok(Dynamic::UNIT) + }) .unwrap(); } pub fn get_bot_memory_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); - engine.register_fn("GET_BOT_MEMORY", move |key_param: String| -> String { use crate::shared::models::bot_memories; - let state = Arc::clone(&state_clone); - - let conn_result = state.conn.lock(); + let conn_result = state.conn.get(); if let Ok(mut conn) = conn_result { let bot_uuid = user_clone.bot_id; - let memory_value: Option = bot_memories::table .filter(bot_memories::bot_id.eq(bot_uuid)) .filter(bot_memories::key.eq(&key_param)) @@ -132,8 +94,6 @@ pub fn get_bot_memory_keyword(state: Arc, user: UserSession, engine: & .first(&mut *conn) .optional() .unwrap_or(None); - - trace!("GET_MEMORY for key '{}' returned value: {:?}", key_param, memory_value); memory_value.unwrap_or_default() } else { String::new() diff --git a/src/basic/keywords/clear_tools.rs b/src/basic/keywords/clear_tools.rs index 297fb646d..0ad1c589e 100644 --- a/src/basic/keywords/clear_tools.rs +++ b/src/basic/keywords/clear_tools.rs @@ -1,103 +1,63 @@ use crate::basic::keywords::add_tool::clear_session_tools; use crate::shared::models::UserSession; use crate::shared::state::AppState; -use log::{error, info}; +use log::{error, trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; - pub fn clear_tools_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - - engine - .register_custom_syntax(&["CLEAR_TOOLS"], false, move |_context, _inputs| { - info!( - "CLEAR_TOOLS command executed for session: {}", - user_clone.id - ); - - let state_for_task = Arc::clone(&state_clone); - let user_for_task = user_clone.clone(); - - // Spawn async task to clear all tool associations from session - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build(); - - let send_err = if let Ok(rt) = rt { - let result = rt.block_on(async move { - clear_all_tools_from_session(&state_for_task, &user_for_task).await - }); - tx.send(result).err() - } else { - tx.send(Err("Failed to build tokio runtime".to_string())) - .err() - }; - - if send_err.is_some() { - error!("Failed to send result from thread"); - } - }); - - match rx.recv_timeout(std::time::Duration::from_secs(10)) { - Ok(Ok(message)) => { - info!("CLEAR_TOOLS completed: {}", message); - Ok(Dynamic::from(message)) - } - Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - e.into(), - rhai::Position::NONE, - ))), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "CLEAR_TOOLS timed out".into(), - rhai::Position::NONE, - ))) - } - Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - format!("CLEAR_TOOLS failed: {}", e).into(), - rhai::Position::NONE, - ))), - } - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["CLEAR_TOOLS"], false, move |_context, _inputs| { + trace!("CLEAR_TOOLS command executed for session: {}", user_clone.id); + let state_for_task = Arc::clone(&state_clone); + let user_for_task = user_clone.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build(); + let send_err = if let Ok(rt) = rt { + let result = rt.block_on(async move { + clear_all_tools_from_session(&state_for_task, &user_for_task).await + }); + tx.send(result).err() + } else { + tx.send(Err("Failed to build tokio runtime".to_string())).err() + }; + if send_err.is_some() { + error!("Failed to send result from thread"); + } + }); + match rx.recv_timeout(std::time::Duration::from_secs(10)) { + Ok(Ok(message)) => { + Ok(Dynamic::from(message)) + } + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime("CLEAR_TOOLS timed out".into(), rhai::Position::NONE))) + } + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("CLEAR_TOOLS failed: {}", e).into(), rhai::Position::NONE))), + } + }) + .unwrap(); } - -/// Clear all tool associations from the current session -async fn clear_all_tools_from_session( - state: &AppState, - user: &UserSession, -) -> Result { - let mut conn = state.conn.lock().map_err(|e| { - error!("Failed to acquire database lock: {}", e); - format!("Database connection error: {}", e) - })?; - - // Clear all tool associations for this session - let delete_result = clear_session_tools(&mut *conn, &user.id); - - match delete_result { - Ok(rows_affected) => { - if rows_affected > 0 { - info!( - "Cleared {} tool(s) from session '{}' (user: {}, bot: {})", - rows_affected, user.id, user.user_id, user.bot_id - ); - Ok(format!( - "All {} tool(s) have been removed from this conversation", - rows_affected - )) - } else { - info!("No tools were associated with session '{}'", user.id); - Ok("No tools were active in this conversation".to_string()) - } - } - Err(e) => { - error!("Failed to clear tools from session '{}': {}", user.id, e); - Err(format!("Failed to clear tools from session: {}", e)) - } - } +async fn clear_all_tools_from_session(state: &AppState, user: &UserSession) -> Result { + let mut conn = state.conn.get().map_err(|e| { + error!("Failed to acquire database lock: {}", e); + format!("Database connection error: {}", e) + })?; + let delete_result = clear_session_tools(&mut *conn, &user.id); + match delete_result { + Ok(rows_affected) => { + if rows_affected > 0 { + trace!("Cleared {} tool(s) from session '{}' (user: {}, bot: {})", rows_affected, user.id, user.user_id, user.bot_id); + Ok(format!("All {} tool(s) have been removed from this conversation", rows_affected)) + } else { + Ok("No tools were active in this conversation".to_string()) + } + } + Err(e) => { + error!("Failed to clear tools from session '{}': {}", user.id, e); + Err(format!("Failed to clear tools from session: {}", e)) + } + } } diff --git a/src/basic/keywords/create_draft.rs b/src/basic/keywords/create_draft.rs index c3d81d97d..0f15c3cb3 100644 --- a/src/basic/keywords/create_draft.rs +++ b/src/basic/keywords/create_draft.rs @@ -3,64 +3,45 @@ use crate::shared::state::AppState; use crate::shared::models::UserSession; use rhai::Dynamic; use rhai::Engine; - pub fn create_draft_keyword(state: &AppState, user: UserSession, engine: &mut Engine) { - let state_clone = state.clone(); - - engine - .register_custom_syntax( - &["CREATE_DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"], - true, - move |context, inputs| { - let to = context.eval_expression_tree(&inputs[0])?.to_string(); - let subject = context.eval_expression_tree(&inputs[1])?.to_string(); - let reply_text = context.eval_expression_tree(&inputs[2])?.to_string(); - - let fut = execute_create_draft(&state_clone, &to, &subject, &reply_text); - let result = - tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) - .map_err(|e| format!("Draft creation error: {}", e))?; - - Ok(Dynamic::from(result)) - }, - ) - .unwrap(); + let state_clone = state.clone(); + engine + .register_custom_syntax(&["CREATE_DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"], true, move |context, inputs| { + let to = context.eval_expression_tree(&inputs[0])?.to_string(); + let subject = context.eval_expression_tree(&inputs[1])?.to_string(); + let reply_text = context.eval_expression_tree(&inputs[2])?.to_string(); + let fut = execute_create_draft(&state_clone, &to, &subject, &reply_text); + let result = tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) + .map_err(|e| format!("Draft creation error: {}", e))?; + Ok(Dynamic::from(result)) + }, + ) + .unwrap(); } - -async fn execute_create_draft( - state: &AppState, - to: &str, - subject: &str, - reply_text: &str, -) -> Result { - let get_result = fetch_latest_sent_to(&state.config.clone().unwrap().email, to).await; - let email_body = if let Ok(get_result_str) = get_result { - if !get_result_str.is_empty() { - let email_separator = "


"; - let formatted_reply_text = reply_text.to_string(); - let formatted_old_text = get_result_str.replace("\n", "
"); - let fixed_reply_text = formatted_reply_text.replace("FIX", "Fixed"); - format!( - "{}{}{}", - fixed_reply_text, email_separator, formatted_old_text - ) - } else { - reply_text.to_string() - } - } else { - reply_text.to_string() - }; - - let draft_request = SaveDraftRequest { - to: to.to_string(), - subject: subject.to_string(), - cc: None, - text: email_body, - }; - - let save_result = save_email_draft(&state.config.clone().unwrap().email, &draft_request).await; - match save_result { - Ok(_) => Ok("Draft saved successfully".to_string()), - Err(e) => Err(e.to_string()), - } +async fn execute_create_draft(state: &AppState, to: &str, subject: &str, reply_text: &str) -> Result { + let get_result = fetch_latest_sent_to(&state.config.clone().unwrap().email, to).await; + let email_body = if let Ok(get_result_str) = get_result { + if !get_result_str.is_empty() { + let email_separator = "


"; + let formatted_reply_text = reply_text.to_string(); + let formatted_old_text = get_result_str.replace("\n", "
"); + let fixed_reply_text = formatted_reply_text.replace("FIX", "Fixed"); + format!("{}{}{}", fixed_reply_text, email_separator, formatted_old_text) + } else { + reply_text.to_string() + } + } else { + reply_text.to_string() + }; + let draft_request = SaveDraftRequest { + to: to.to_string(), + subject: subject.to_string(), + cc: None, + text: email_body, + }; + let save_result = save_email_draft(&state.config.clone().unwrap().email, &draft_request).await; + match save_result { + Ok(_) => Ok("Draft saved successfully".to_string()), + Err(e) => Err(e.to_string()), + } } diff --git a/src/basic/keywords/create_site.rs b/src/basic/keywords/create_site.rs index 3690824ce..772477538 100644 --- a/src/basic/keywords/create_site.rs +++ b/src/basic/keywords/create_site.rs @@ -1,87 +1,50 @@ -use log::info; use rhai::Dynamic; use rhai::Engine; use std::error::Error; use std::fs; use std::io::Read; use std::path::PathBuf; - use crate::shared::models::UserSession; use crate::shared::state::AppState; - pub fn create_site_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) { - let state_clone = state.clone(); - engine - .register_custom_syntax( - &["CREATE_SITE", "$expr$", ",", "$expr$", ",", "$expr$"], - true, - move |context, inputs| { - if inputs.len() < 3 { - return Err("Not enough arguments for CREATE SITE".into()); - } - - let alias = context.eval_expression_tree(&inputs[0])?; - let template_dir = context.eval_expression_tree(&inputs[1])?; - let prompt = context.eval_expression_tree(&inputs[2])?; - - let config = state_clone - .config - .as_ref() - .expect("Config must be initialized") - .clone(); - - let fut = create_site(&config, alias, template_dir, prompt); - let result = - tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) - .map_err(|e| format!("Site creation failed: {}", e))?; - - Ok(Dynamic::from(result)) - }, - ) - .unwrap(); + let state_clone = state.clone(); + engine + .register_custom_syntax(&["CREATE_SITE", "$expr$", ",", "$expr$", ",", "$expr$"], true, move |context, inputs| { + if inputs.len() < 3 { + return Err("Not enough arguments for CREATE SITE".into()); + } + let alias = context.eval_expression_tree(&inputs[0])?; + let template_dir = context.eval_expression_tree(&inputs[1])?; + let prompt = context.eval_expression_tree(&inputs[2])?; + let config = state_clone.config.as_ref().expect("Config must be initialized").clone(); + let fut = create_site(&config, alias, template_dir, prompt); + let result = tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) + .map_err(|e| format!("Site creation failed: {}", e))?; + Ok(Dynamic::from(result)) + }, + ) + .unwrap(); } - -async fn create_site( - config: &crate::config::AppConfig, - alias: Dynamic, - template_dir: Dynamic, - prompt: Dynamic, -) -> Result> { - let base_path = PathBuf::from(&config.site_path); - let template_path = base_path.join(template_dir.to_string()); - let alias_path = base_path.join(alias.to_string()); - - fs::create_dir_all(&alias_path).map_err(|e| e.to_string())?; - - let mut combined_content = String::new(); - - for entry in fs::read_dir(&template_path).map_err(|e| e.to_string())? { - let entry = entry.map_err(|e| e.to_string())?; - let path = entry.path(); - - if path.extension().map_or(false, |ext| ext == "html") { - let mut file = fs::File::open(&path).map_err(|e| e.to_string())?; - let mut contents = String::new(); - file.read_to_string(&mut contents) - .map_err(|e| e.to_string())?; - - combined_content.push_str(&contents); - combined_content.push_str("\n\n--- TEMPLATE SEPARATOR ---\n\n"); - } - } - - let _full_prompt = format!( - "TEMPLATE FILES:\n{}\n\nPROMPT: {}\n\nGenerate a new HTML file cloning all previous TEMPLATE (keeping only the local _assets libraries use, no external resources), but turning this into this prompt:", - combined_content, - prompt.to_string() - ); - - info!("Asking LLM to create site."); - let llm_result = "".to_string(); // TODO: - - let index_path = alias_path.join("index.html"); - fs::write(index_path, llm_result).map_err(|e| e.to_string())?; - - info!("Site created at: {}", alias_path.display()); - Ok(alias_path.to_string_lossy().into_owned()) +async fn create_site(config: &crate::config::AppConfig, alias: Dynamic, template_dir: Dynamic, prompt: Dynamic) -> Result> { + let base_path = PathBuf::from(&config.site_path); + let template_path = base_path.join(template_dir.to_string()); + let alias_path = base_path.join(alias.to_string()); + fs::create_dir_all(&alias_path).map_err(|e| e.to_string())?; + let mut combined_content = String::new(); + for entry in fs::read_dir(&template_path).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.extension().map_or(false, |ext| ext == "html") { + let mut file = fs::File::open(&path).map_err(|e| e.to_string())?; + let mut contents = String::new(); + file.read_to_string(&mut contents).map_err(|e| e.to_string())?; + combined_content.push_str(&contents); + combined_content.push_str("\n\n--- TEMPLATE SEPARATOR ---\n\n"); + } + } + let _full_prompt = format!("TEMPLATE FILES:\n{}\n\nPROMPT: {}\n\nGenerate a new HTML file cloning all previous TEMPLATE (keeping only the local _assets libraries use, no external resources), but turning this into this prompt:", combined_content, prompt.to_string()); + let llm_result = "".to_string(); + let index_path = alias_path.join("index.html"); + fs::write(index_path, llm_result).map_err(|e| e.to_string())?; + Ok(alias_path.to_string_lossy().into_owned()) } diff --git a/src/basic/keywords/find.rs b/src/basic/keywords/find.rs index 6cf2bd324..6e25f2ce6 100644 --- a/src/basic/keywords/find.rs +++ b/src/basic/keywords/find.rs @@ -1,36 +1,29 @@ -use diesel::pg::PgConnection; -use diesel::prelude::*; -use log::{error, info}; -use rhai::Dynamic; -use rhai::Engine; -use serde_json::{json, Value}; - use crate::shared::models::UserSession; use crate::shared::state::AppState; use crate::shared::utils; use crate::shared::utils::to_array; - +use diesel::pg::PgConnection; +use diesel::prelude::*; +use log::error; +use log::trace; +use rhai::Dynamic; +use rhai::Engine; +use serde_json::{json, Value}; pub fn find_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) { let connection = state.conn.clone(); - engine .register_custom_syntax(&["FIND", "$expr$", ",", "$expr$"], false, { move |context, inputs| { let table_name = context.eval_expression_tree(&inputs[0])?; let filter = context.eval_expression_tree(&inputs[1])?; - let mut binding = connection.lock().unwrap(); - - // Use the current async context instead of creating a new runtime + let mut binding = connection.get().map_err(|e| format!("DB error: {}", e))?; let binding2 = table_name.to_string(); let binding3 = filter.to_string(); - - // Since execute_find is async but we're in a sync context, we need to block on it let result = tokio::task::block_in_place(|| { tokio::runtime::Handle::current() .block_on(async { execute_find(&mut binding, &binding2, &binding3).await }) }) .map_err(|e| format!("DB error: {}", e))?; - if let Some(results) = result.get("results") { let array = to_array(utils::json_value_to_dynamic(results)); Ok(Dynamic::from(array)) @@ -41,56 +34,37 @@ pub fn find_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) { }) .unwrap(); } - pub async fn execute_find( conn: &mut PgConnection, table_str: &str, filter_str: &str, ) -> Result { - // Changed to String error like your Actix code - info!( + trace!( "Starting execute_find with table: {}, filter: {}", - table_str, filter_str + table_str, + filter_str ); - let (where_clause, params) = utils::parse_filter(filter_str).map_err(|e| e.to_string())?; - let query = format!( "SELECT * FROM {} WHERE {} LIMIT 10", table_str, where_clause ); - info!("Executing query: {}", query); - - - // Execute raw SQL and get raw results - let raw_result = diesel::sql_query(&query) + let _raw_result = diesel::sql_query(&query) .bind::(¶ms[0]) .execute(conn) .map_err(|e| { error!("SQL execution error: {}", e); e.to_string() })?; - - info!("Query executed successfully, affected {} rows", raw_result); - - // For now, create placeholder results since we can't easily deserialize dynamic rows let mut results = Vec::new(); - - // This is a simplified approach - in a real implementation you'd need to: - // 1. Query the table schema to know column types - // 2. Build a proper struct or use a more flexible approach - // 3. Or use a different database library that supports dynamic queries better - - // Placeholder result for demonstration let json_row = serde_json::json!({ - "note": "Dynamic row deserialization not implemented - need table schema" + "note": "Dynamic row deserialization not implemented - need table schema" }); results.push(json_row); - Ok(json!({ - "command": "find", - "table": table_str, - "filter": filter_str, - "results": results + "command": "find", + "table": table_str, + "filter": filter_str, + "results": results })) } diff --git a/src/basic/keywords/first.rs b/src/basic/keywords/first.rs index f93e71408..4f9bab412 100644 --- a/src/basic/keywords/first.rs +++ b/src/basic/keywords/first.rs @@ -1,184 +1,14 @@ use rhai::Dynamic; use rhai::Engine; - pub fn first_keyword(engine: &mut Engine) { - engine - .register_custom_syntax(&["FIRST", "$expr$"], false, { - move |context, inputs| { - let input_string = context.eval_expression_tree(&inputs[0])?; - let input_str = input_string.to_string(); - - let first_word = input_str - .split_whitespace() - .next() - .unwrap_or("") - .to_string(); - - Ok(Dynamic::from(first_word)) - } - }) - .unwrap(); -} - -#[cfg(test)] -mod tests { - use super::*; - use rhai::Engine; - - fn setup_engine() -> Engine { - let mut engine = Engine::new(); - first_keyword(&mut engine); - engine - } - - #[test] - fn test_first_keyword_basic() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST "hello world" - "#, - ) - .unwrap(); - - assert_eq!(result, "hello"); - } - - #[test] - fn test_first_keyword_single_word() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST "single" - "#, - ) - .unwrap(); - - assert_eq!(result, "single"); - } - - #[test] - fn test_first_keyword_multiple_spaces() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST " leading spaces" - "#, - ) - .unwrap(); - - assert_eq!(result, "leading"); - } - - #[test] - fn test_first_keyword_empty_string() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST "" - "#, - ) - .unwrap(); - - assert_eq!(result, ""); - } - - #[test] - fn test_first_keyword_whitespace_only() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST " " - "#, - ) - .unwrap(); - - assert_eq!(result, ""); - } - - #[test] - fn test_first_keyword_with_tabs() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST " tab separated words" - "#, - ) - .unwrap(); - - assert_eq!(result, "tab"); - } - - #[test] - fn test_first_keyword_with_variable() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - let text = "variable test"; - FIRST text - "#, - ) - .unwrap(); - - assert_eq!(result, "variable"); - } - - #[test] - fn test_first_keyword_with_expression() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST "one two " + "three four" - "#, - ) - .unwrap(); - - assert_eq!(result, "one"); - } - - #[test] - fn test_first_keyword_mixed_whitespace() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST " multiple spaces between words " - "#, - ) - .unwrap(); - - assert_eq!(result, "multiple"); - } - - #[test] - fn test_first_keyword_special_characters() { - let engine = setup_engine(); - - let result = engine - .eval::( - r#" - FIRST "hello-world example" - "#, - ) - .unwrap(); - - assert_eq!(result, "hello-world"); - } + engine + .register_custom_syntax(&["FIRST", "$expr$"], false, { + move |context, inputs| { + let input_string = context.eval_expression_tree(&inputs[0])?; + let input_str = input_string.to_string(); + let first_word = input_str.split_whitespace().next().unwrap_or("").to_string(); + Ok(Dynamic::from(first_word)) + } + }) + .unwrap(); } diff --git a/src/basic/keywords/for_next.rs b/src/basic/keywords/for_next.rs index e480a1a93..735976136 100644 --- a/src/basic/keywords/for_next.rs +++ b/src/basic/keywords/for_next.rs @@ -1,73 +1,47 @@ use crate::shared::models::UserSession; use crate::shared::state::AppState; -use log::info; use rhai::Dynamic; use rhai::Engine; - pub fn for_keyword(_state: &AppState, _user: UserSession, engine: &mut Engine) { - engine - .register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| { - Err("EXIT FOR".into()) - }) - .unwrap(); - - engine - .register_custom_syntax( - &[ - "FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$", - ], - true, - |context, inputs| { - let loop_var = inputs[0].get_string_value().unwrap(); - let next_var = inputs[3].get_string_value().unwrap(); - - if loop_var != next_var { - return Err(format!( - "NEXT variable '{}' doesn't match FOR EACH variable '{}'", - next_var, loop_var - ) - .into()); - } - - let collection = context.eval_expression_tree(&inputs[1])?; - - info!("Collection type: {}", collection.type_name()); - let ccc = collection.clone(); - let array = match collection.into_array() { - Ok(arr) => arr, - Err(err) => { - return Err(format!( - "foreach expected array, got {}: {}", - ccc.type_name(), - err - ) - .into()); - } - }; - let block = &inputs[2]; - - let orig_len = context.scope().len(); - - for item in array { - context.scope_mut().push(loop_var, item); - - match context.eval_expression_tree(block) { - Ok(_) => (), - Err(e) if e.to_string() == "EXIT FOR" => { - context.scope_mut().rewind(orig_len); - break; - } - Err(e) => { - context.scope_mut().rewind(orig_len); - return Err(e); - } - } - - context.scope_mut().rewind(orig_len); - } - - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); + engine + .register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| { + Err("EXIT FOR".into()) + }) + .unwrap(); + engine + .register_custom_syntax(&["FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$"], true, |context, inputs| { + let loop_var = inputs[0].get_string_value().unwrap(); + let next_var = inputs[3].get_string_value().unwrap(); + if loop_var != next_var { + return Err(format!("NEXT variable '{}' doesn't match FOR EACH variable '{}'", next_var, loop_var).into()); + } + let collection = context.eval_expression_tree(&inputs[1])?; + let ccc = collection.clone(); + let array = match collection.into_array() { + Ok(arr) => arr, + Err(err) => { + return Err(format!("foreach expected array, got {}: {}", ccc.type_name(), err).into()); + } + }; + let block = &inputs[2]; + let orig_len = context.scope().len(); + for item in array { + context.scope_mut().push(loop_var, item); + match context.eval_expression_tree(block) { + Ok(_) => (), + Err(e) if e.to_string() == "EXIT FOR" => { + context.scope_mut().rewind(orig_len); + break; + } + Err(e) => { + context.scope_mut().rewind(orig_len); + return Err(e); + } + } + context.scope_mut().rewind(orig_len); + } + Ok(Dynamic::UNIT) + }, + ) + .unwrap(); } diff --git a/src/basic/keywords/format.rs b/src/basic/keywords/format.rs index 34c39086f..6bce00b23 100644 --- a/src/basic/keywords/format.rs +++ b/src/basic/keywords/format.rs @@ -2,303 +2,157 @@ use rhai::{Dynamic, Engine}; use chrono::{NaiveDateTime, Timelike, Datelike}; use num_format::{Locale, ToFormattedString}; use std::str::FromStr; - pub fn format_keyword(engine: &mut Engine) { - engine - .register_custom_syntax(&["FORMAT", "$expr$", "$expr$"], false, { - move |context, inputs| { - let value_dyn = context.eval_expression_tree(&inputs[0])?; - let pattern_dyn = context.eval_expression_tree(&inputs[1])?; - - let value_str = value_dyn.to_string(); - let pattern = pattern_dyn.to_string(); - - if let Ok(num) = f64::from_str(&value_str) { - let formatted = if pattern.starts_with("N") || pattern.starts_with("C") { - let (prefix, decimals, locale_tag) = parse_pattern(&pattern); - - let locale = get_locale(&locale_tag); - let symbol = if prefix == "C" { - get_currency_symbol(&locale_tag) - } else { - "" - }; - - let int_part = num.trunc() as i64; - let frac_part = num.fract(); - - if decimals == 0 { - format!("{}{}", symbol, int_part.to_formatted_string(&locale)) - } else { - let frac_scaled = - ((frac_part * 10f64.powi(decimals as i32)).round()) as i64; - - let decimal_sep = match locale_tag.as_str() { - "pt" | "fr" | "es" | "it" | "de" => ",", - _ => "." - }; - - format!( - "{}{}{}{:0width$}", - symbol, - int_part.to_formatted_string(&locale), - decimal_sep, - frac_scaled, - width = decimals - ) - } - } else { - match pattern.as_str() { - "n" => format!("{:.2}", num), - "F" => format!("{:.2}", num), - "f" => format!("{}", num), - "0%" => format!("{:.0}%", num * 100.0), - _ => format!("{}", num), - } - }; - - return Ok(Dynamic::from(formatted)); - } - - if let Ok(dt) = NaiveDateTime::parse_from_str(&value_str, "%Y-%m-%d %H:%M:%S") { - let formatted = apply_date_format(&dt, &pattern); - return Ok(Dynamic::from(formatted)); - } - - let formatted = apply_text_placeholders(&value_str, &pattern); - Ok(Dynamic::from(formatted)) - } - }) - .unwrap(); + engine + .register_custom_syntax(&["FORMAT", "$expr$", "$expr$"], false, { + move |context, inputs| { + let value_dyn = context.eval_expression_tree(&inputs[0])?; + let pattern_dyn = context.eval_expression_tree(&inputs[1])?; + let value_str = value_dyn.to_string(); + let pattern = pattern_dyn.to_string(); + if let Ok(num) = f64::from_str(&value_str) { + let formatted = if pattern.starts_with("N") || pattern.starts_with("C") { + let (prefix, decimals, locale_tag) = parse_pattern(&pattern); + let locale = get_locale(&locale_tag); + let symbol = if prefix == "C" { get_currency_symbol(&locale_tag) } else { "" }; + let int_part = num.trunc() as i64; + let frac_part = num.fract(); + if decimals == 0 { + format!("{}{}", symbol, int_part.to_formatted_string(&locale)) + } else { + let frac_scaled = ((frac_part * 10f64.powi(decimals as i32)).round()) as i64; + let decimal_sep = match locale_tag.as_str() { + "pt" | "fr" | "es" | "it" | "de" => ",", + _ => "." + }; + format!("{}{}{}{:0width$}", symbol, int_part.to_formatted_string(&locale), decimal_sep, frac_scaled, width = decimals) + } + } else { + match pattern.as_str() { + "n" => format!("{:.2}", num), + "F" => format!("{:.2}", num), + "f" => format!("{}", num), + "0%" => format!("{:.0}%", num * 100.0), + _ => format!("{}", num), + } + }; + return Ok(Dynamic::from(formatted)); + } + if let Ok(dt) = NaiveDateTime::parse_from_str(&value_str, "%Y-%m-%d %H:%M:%S") { + let formatted = apply_date_format(&dt, &pattern); + return Ok(Dynamic::from(formatted)); + } + let formatted = apply_text_placeholders(&value_str, &pattern); + Ok(Dynamic::from(formatted)) + } + }) + .unwrap(); } - fn parse_pattern(pattern: &str) -> (String, usize, String) { - let mut prefix = String::new(); - let mut decimals: usize = 2; - let mut locale_tag = "en".to_string(); - - if pattern.starts_with('C') { - prefix = "C".to_string(); - } else if pattern.starts_with('N') { - prefix = "N".to_string(); - } - - let rest = &pattern[1..]; - let mut num_part = String::new(); - for ch in rest.chars() { - if ch.is_ascii_digit() { - num_part.push(ch); - } else { - break; - } - } - if !num_part.is_empty() { - decimals = num_part.parse().unwrap_or(2); - } - - if let Some(start) = pattern.find('[') { - if let Some(end) = pattern.find(']') { - if end > start { - locale_tag = pattern[start + 1..end].to_string(); - } - } - } - - (prefix, decimals, locale_tag) + let mut prefix = String::new(); + let mut decimals: usize = 2; + let mut locale_tag = "en".to_string(); + if pattern.starts_with('C') { + prefix = "C".to_string(); + } else if pattern.starts_with('N') { + prefix = "N".to_string(); + } + let rest = &pattern[1..]; + let mut num_part = String::new(); + for ch in rest.chars() { + if ch.is_ascii_digit() { + num_part.push(ch); + } else { + break; + } + } + if !num_part.is_empty() { + decimals = num_part.parse().unwrap_or(2); + } + if let Some(start) = pattern.find('[') { + if let Some(end) = pattern.find(']') { + if end > start { + locale_tag = pattern[start + 1..end].to_string(); + } + } + } + (prefix, decimals, locale_tag) } - fn get_locale(tag: &str) -> Locale { - match tag { - "en" => Locale::en, - "fr" => Locale::fr, - "de" => Locale::de, - "pt" => Locale::pt, - "it" => Locale::it, - "es" => Locale::es, - _ => Locale::en, - } + match tag { + "en" => Locale::en, + "fr" => Locale::fr, + "de" => Locale::de, + "pt" => Locale::pt, + "it" => Locale::it, + "es" => Locale::es, + _ => Locale::en, + } } - fn get_currency_symbol(tag: &str) -> &'static str { - match tag { - "en" => "$", - "pt" => "R$ ", - "fr" | "de" | "es" | "it" => "€", - _ => "$", - } + match tag { + "en" => "$", + "pt" => "R$ ", + "fr" | "de" | "es" | "it" => "€", + _ => "$", + } } - fn apply_date_format(dt: &NaiveDateTime, pattern: &str) -> String { - let mut output = pattern.to_string(); - - let year = dt.year(); - let month = dt.month(); - let day = dt.day(); - let hour24 = dt.hour(); - let minute = dt.minute(); - let second = dt.second(); - let millis = dt.and_utc().timestamp_subsec_millis(); - - output = output.replace("yyyy", &format!("{:04}", year)); - output = output.replace("yy", &format!("{:02}", year % 100)); - output = output.replace("MM", &format!("{:02}", month)); - output = output.replace("M", &format!("{}", month)); - output = output.replace("dd", &format!("{:02}", day)); - output = output.replace("d", &format!("{}", day)); - - output = output.replace("HH", &format!("{:02}", hour24)); - output = output.replace("H", &format!("{}", hour24)); - - let mut hour12 = hour24 % 12; - if hour12 == 0 { hour12 = 12; } - output = output.replace("hh", &format!("{:02}", hour12)); - output = output.replace("h", &format!("{}", hour12)); - - output = output.replace("mm", &format!("{:02}", minute)); - output = output.replace("m", &format!("{}", minute)); - - output = output.replace("ss", &format!("{:02}", second)); - output = output.replace("s", &format!("{}", second)); - - output = output.replace("fff", &format!("{:03}", millis)); - - output = output.replace("tt", if hour24 < 12 { "AM" } else { "PM" }); - output = output.replace("t", if hour24 < 12 { "A" } else { "P" }); - - output + let mut output = pattern.to_string(); + let year = dt.year(); + let month = dt.month(); + let day = dt.day(); + let hour24 = dt.hour(); + let minute = dt.minute(); + let second = dt.second(); + let millis = dt.and_utc().timestamp_subsec_millis(); + output = output.replace("yyyy", &format!("{:04}", year)); + output = output.replace("yy", &format!("{:02}", year % 100)); + output = output.replace("MM", &format!("{:02}", month)); + output = output.replace("M", &format!("{}", month)); + output = output.replace("dd", &format!("{:02}", day)); + output = output.replace("d", &format!("{}", day)); + output = output.replace("HH", &format!("{:02}", hour24)); + output = output.replace("H", &format!("{}", hour24)); + let mut hour12 = hour24 % 12; + if hour12 == 0 { hour12 = 12; } + output = output.replace("hh", &format!("{:02}", hour12)); + output = output.replace("h", &format!("{}", hour12)); + output = output.replace("mm", &format!("{:02}", minute)); + output = output.replace("m", &format!("{}", minute)); + output = output.replace("ss", &format!("{:02}", second)); + output = output.replace("s", &format!("{}", second)); + output = output.replace("fff", &format!("{:03}", millis)); + output = output.replace("tt", if hour24 < 12 { "AM" } else { "PM" }); + output = output.replace("t", if hour24 < 12 { "A" } else { "P" }); + output } - fn apply_text_placeholders(value: &str, pattern: &str) -> String { - let mut result = String::new(); - let mut i = 0; - let chars: Vec = pattern.chars().collect(); - - while i < chars.len() { - match chars[i] { - '@' => result.push_str(value), - '&' => { - result.push_str(&value.to_lowercase()); - // Handle modifiers - if i + 1 < chars.len() { - match chars[i+1] { - '!' => { - result.push('!'); - i += 1; - } - '>' => { - i += 1; - } - _ => () - } - } - } - '>' | '!' => result.push_str(&value.to_uppercase()), - _ => result.push(chars[i]), - } - i += 1; - } - - result -} - -#[cfg(test)] -mod tests { - use super::*; - use rhai::Engine; - - fn create_engine() -> Engine { - let mut engine = Engine::new(); - format_keyword(&mut engine); - engine - } - - #[test] - fn test_numeric_formatting_basic() { - let engine = create_engine(); - - assert_eq!( - engine.eval::("FORMAT 1234.567 \"n\"").unwrap(), - "1234.57" - ); - assert_eq!( - engine.eval::("FORMAT 1234.5 \"F\"").unwrap(), - "1234.50" - ); - assert_eq!( - engine.eval::("FORMAT 1234.567 \"f\"").unwrap(), - "1234.567" - ); - assert_eq!( - engine.eval::("FORMAT 0.85 \"0%\"").unwrap(), - "85%" - ); - } - - #[test] - fn test_numeric_formatting_with_locale() { - let engine = create_engine(); - - assert_eq!( - engine.eval::("FORMAT 1234.56 \"N[en]\"").unwrap(), - "1,234.56" - ); - assert_eq!( - engine.eval::("FORMAT 1234.56 \"N[pt]\"").unwrap(), - "1.234,56" - ); - assert_eq!( - engine.eval::("FORMAT 1234.56 \"N[fr]\"").unwrap(), - "1 234,56" - ); - } - - #[test] - fn test_currency_formatting() { - let engine = create_engine(); - - assert_eq!( - engine.eval::("FORMAT 1234.56 \"C[en]\"").unwrap(), - "$1,234.56" - ); - assert_eq!( - engine.eval::("FORMAT 1234.56 \"C[pt]\"").unwrap(), - "R$ 1.234,56" - ); - assert_eq!( - engine.eval::("FORMAT 1234.56 \"C[fr]\"").unwrap(), - "€1 234,56" - ); - } - - #[test] - fn test_date_formatting() { - let engine = create_engine(); - - let result = engine.eval::("FORMAT \"2024-03-15 14:30:25\" \"yyyy-MM-dd HH:mm:ss\"").unwrap(); - assert_eq!(result, "2024-03-15 14:30:25"); - - let result = engine.eval::("FORMAT \"2024-03-15 14:30:25\" \"dd/MM/yyyy\"").unwrap(); - assert_eq!(result, "15/03/2024"); - - let result = engine.eval::("FORMAT \"2024-03-15 14:30:25\" \"MM/dd/yy\"").unwrap(); - assert_eq!(result, "03/15/24"); - } - - #[test] - fn test_text_formatting() { - let engine = create_engine(); - - assert_eq!( - engine.eval::("FORMAT \"hello\" \"Prefix: @\"").unwrap(), - "Prefix: hello" - ); - assert_eq!( - engine.eval::("FORMAT \"HELLO\" \"Result: &!\"").unwrap(), - "Result: hello!" - ); - assert_eq!( - engine.eval::("FORMAT \"hello\" \"RESULT: >\"").unwrap(), - "RESULT: HELLO" - ); - } + let mut result = String::new(); + let mut i = 0; + let chars: Vec = pattern.chars().collect(); + while i < chars.len() { + match chars[i] { + '@' => result.push_str(value), + '&' => { + result.push_str(&value.to_lowercase()); + if i + 1 < chars.len() { + match chars[i+1] { + '!' => { + result.push('!'); + i += 1; + } + '>' => { + i += 1; + } + _ => () + } + } + } + '>' | '!' => result.push_str(&value.to_uppercase()), + _ => result.push(chars[i]), + } + i += 1; + } + result } diff --git a/src/basic/keywords/get.rs b/src/basic/keywords/get.rs index 5d89105fc..81f28515f 100644 --- a/src/basic/keywords/get.rs +++ b/src/basic/keywords/get.rs @@ -1,66 +1,57 @@ use crate::shared::models::schema::bots::dsl::*; -use diesel::prelude::*; use crate::shared::models::UserSession; use crate::shared::state::AppState; -use log::{debug, error, info, trace}; +use diesel::prelude::*; +use log::{error, trace}; use reqwest::{self, Client}; use rhai::{Dynamic, Engine}; use std::error::Error; use std::path::Path; use std::sync::Arc; use std::time::Duration; - pub fn get_keyword(state: Arc, user_session: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); - engine .register_custom_syntax(&["GET", "$expr$"], false, move |context, inputs| { let url = context.eval_expression_tree(&inputs[0])?; let url_str = url.to_string(); - - info!("GET command executed: {}", url_str); - if !is_safe_path(&url_str) { return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "URL contains invalid or unsafe path sequences".into(), rhai::Position::NONE, ))); } - let state_for_blocking = Arc::clone(&state_clone); let url_for_blocking = url_str.clone(); - let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); - let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { if url_for_blocking.starts_with("https://") || url_for_blocking.starts_with("http://") { - info!("HTTP(S) GET request: {}", url_for_blocking); execute_get(&url_for_blocking).await } else { - info!("Local file GET request from bucket: {}", url_for_blocking); - get_from_bucket(&state_for_blocking, &url_for_blocking, - user_session.bot_id) - .await + get_from_bucket( + &state_for_blocking, + &url_for_blocking, + user_session.bot_id, + ) + .await } }); tx.send(result).err() } else { tx.send(Err("failed to build tokio runtime".into())).err() }; - if send_err.is_some() { error!("Failed to send result from thread"); } }); - match rx.recv_timeout(std::time::Duration::from_secs(40)) { Ok(Ok(content)) => Ok(Dynamic::from(content)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( @@ -78,7 +69,6 @@ pub fn get_keyword(state: Arc, user_session: UserSession, engine: &mut }) .unwrap(); } - fn is_safe_path(path: &str) -> bool { if path.starts_with("https://") || path.starts_with("http://") { return true; @@ -105,10 +95,7 @@ fn is_safe_path(path: &str) -> bool { } true } - pub async fn execute_get(url: &str) -> Result> { - debug!("Starting execute_get with URL: {}", url); - let client = Client::builder() .timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(10)) @@ -118,12 +105,10 @@ pub async fn execute_get(url: &str) -> Result Result Result> { - debug!("Getting file from bucket: {}", file_path); - if !is_safe_path(file_path) { error!("Unsafe file path detected: {}", file_path); return Err("Invalid file path".into()); } - let client = state.drive.as_ref().ok_or("S3 client not configured")?; let bot_name: String = { - let mut db_conn = state.conn.lock().unwrap(); + let mut db_conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?; bots.filter(id.eq(&bot_id)) .select(name) .first(&mut *db_conn) @@ -174,32 +153,26 @@ pub async fn get_from_bucket( e })? }; - let bucket_name = { let bucket = format!("{}.gbai", bot_name); - trace!("Resolved GET bucket name: {}", bucket); bucket }; - - let bytes = match tokio::time::timeout( - Duration::from_secs(30), - async { - let result: Result, Box> = match client - .get_object() - .bucket(&bucket_name) - .key(file_path) - .send() - .await - { - Ok(response) => { - let data = response.body.collect().await?.into_bytes(); - Ok(data.to_vec()) - } - Err(e) => Err(format!("S3 operation failed: {}", e).into()), - }; - result - }, - ) + let bytes = match tokio::time::timeout(Duration::from_secs(30), async { + let result: Result, Box> = match client + .get_object() + .bucket(&bucket_name) + .key(file_path) + .send() + .await + { + Ok(response) => { + let data = response.body.collect().await?.into_bytes(); + Ok(data.to_vec()) + } + Err(e) => Err(format!("S3 operation failed: {}", e).into()), + }; + result + }) .await { Ok(Ok(data)) => data.to_vec(), @@ -212,7 +185,6 @@ pub async fn get_from_bucket( return Err("drive operation timed out".into()); } }; - let content = if file_path.to_ascii_lowercase().ends_with(".pdf") { match pdf_extract::extract_text_from_mem(&bytes) { Ok(text) => text, @@ -230,8 +202,7 @@ pub async fn get_from_bucket( } } }; - - info!( + trace!( "Successfully retrieved file from bucket: {}, content length: {}", file_path, content.len() diff --git a/src/basic/keywords/hear_talk.rs b/src/basic/keywords/hear_talk.rs index 235e7d5f1..bfbfcabc6 100644 --- a/src/basic/keywords/hear_talk.rs +++ b/src/basic/keywords/hear_talk.rs @@ -1,159 +1,108 @@ use crate::shared::models::{BotResponse, UserSession}; use crate::shared::state::AppState; -use log::{debug, error, info}; +use log::{error, trace}; use rhai::{Dynamic, Engine, EvalAltResult}; use std::sync::Arc; - pub fn hear_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let session_id = user.id; - let state_clone = Arc::clone(&state); - - engine - .register_custom_syntax(&["HEAR", "$ident$"], true, move |_context, inputs| { - let variable_name = inputs[0] - .get_string_value() - .expect("Expected identifier as string") - .to_string(); - - info!( - "HEAR command waiting for user input to store in variable: {}", - variable_name - ); - - let state_for_spawn = Arc::clone(&state_clone); - let session_id_clone = session_id; - let var_name_clone = variable_name.clone(); - - tokio::spawn(async move { - debug!( - "HEAR: Setting session {} to wait for input for variable '{}'", - session_id_clone, var_name_clone - ); - - let mut session_manager = state_for_spawn.session_manager.lock().await; - session_manager.mark_waiting(session_id_clone); - - if let Some(redis_client) = &state_for_spawn.cache { - let mut conn = match redis_client.get_multiplexed_async_connection().await { - Ok(conn) => conn, - Err(e) => { - error!("Failed to connect to cache: {}", e); - return; - } - }; - - let key = format!("hear:{}:{}", session_id_clone, var_name_clone); - let _: Result<(), _> = redis::cmd("SET") - .arg(&key) - .arg("waiting") - .query_async(&mut conn) - .await; - } - }); - - Err(Box::new(EvalAltResult::ErrorRuntime( - "Waiting for user input".into(), - rhai::Position::NONE, - ))) - }) - .unwrap(); + let session_id = user.id; + let state_clone = Arc::clone(&state); + engine + .register_custom_syntax(&["HEAR", "$ident$"], true, move |_context, inputs| { + let variable_name = inputs[0].get_string_value().expect("Expected identifier as string").to_string(); + trace!("HEAR command waiting for user input to store in variable: {}", variable_name); + let state_for_spawn = Arc::clone(&state_clone); + let session_id_clone = session_id; + let var_name_clone = variable_name.clone(); + tokio::spawn(async move { + trace!("HEAR: Setting session {} to wait for input for variable '{}'", session_id_clone, var_name_clone); + let mut session_manager = state_for_spawn.session_manager.lock().await; + session_manager.mark_waiting(session_id_clone); + if let Some(redis_client) = &state_for_spawn.cache { + let mut conn = match redis_client.get_multiplexed_async_connection().await { + Ok(conn) => conn, + Err(e) => { + error!("Failed to connect to cache: {}", e); + return; + } + }; + let key = format!("hear:{}:{}", session_id_clone, var_name_clone); + let _: Result<(), _> = redis::cmd("SET").arg(&key).arg("waiting").query_async(&mut conn).await; + } + }); + Err(Box::new(EvalAltResult::ErrorRuntime("Waiting for user input".into(), rhai::Position::NONE))) + }) + .unwrap(); } - pub async fn execute_talk(state: Arc, user_session: UserSession, message: String) -> Result> { - info!("Executing TALK with message: {}", message); - debug!("TALK: Sending message: {}", message); - - let mut suggestions = Vec::new(); - - if let Some(redis_client) = &state.cache { - let mut conn: redis::aio::MultiplexedConnection = redis_client.get_multiplexed_async_connection().await?; - - let redis_key = format!("suggestions:{}:{}", user_session.user_id, user_session.id); - debug!("Loading suggestions from Redis key: {}", redis_key); - let suggestions_json: Result, _> = redis::cmd("LRANGE") - .arg(redis_key.as_str()) - .arg(0) - .arg(-1) - .query_async(&mut conn) - .await; - - match suggestions_json { - Ok(suggestions_json) => { - debug!("Found suggestions in Redis: {:?}", suggestions_json); - suggestions = suggestions_json.into_iter() - .filter_map(|s| serde_json::from_str(&s).ok()) - .collect(); - debug!("Parsed suggestions: {:?}", suggestions); - } - Err(e) => { - error!("Failed to load suggestions from Redis: {}", e); - } - } - } - - let response = BotResponse { - bot_id: user_session.bot_id.to_string(), - user_id: user_session.user_id.to_string(), - session_id: user_session.id.to_string(), - channel: "web".to_string(), - content: message, - message_type: 1, - stream_token: None, - is_complete: true, - suggestions, - context_name: None, - context_length: 0, - context_max_length: 0, - }; - - let user_id = user_session.id.to_string(); - let response_clone = response.clone(); - - match state.response_channels.try_lock() { - Ok(response_channels) => { - if let Some(tx) = response_channels.get(&user_id) { - if let Err(e) = tx.try_send(response_clone) { - error!("Failed to send TALK message via WebSocket: {}", e); - } else { - debug!("TALK message sent successfully via WebSocket"); - } - } else { - debug!("No WebSocket connection found for session {}, sending via web adapter", user_id); - let web_adapter = Arc::clone(&state.web_adapter); - tokio::spawn(async move { - if let Err(e) = web_adapter.send_message_to_session(&user_id, response_clone).await { - error!("Failed to send TALK message via web adapter: {}", e); - } else { - debug!("TALK message sent successfully via web adapter"); - } - }); - } - } - Err(_) => { - error!("Failed to acquire lock on response_channels for TALK command"); - } - } - - Ok(response) + let mut suggestions = Vec::new(); + if let Some(redis_client) = &state.cache { + let mut conn: redis::aio::MultiplexedConnection = redis_client.get_multiplexed_async_connection().await?; + let redis_key = format!("suggestions:{}:{}", user_session.user_id, user_session.id); + let suggestions_json: Result, _> = redis::cmd("LRANGE").arg(redis_key.as_str()).arg(0).arg(-1).query_async(&mut conn).await; + match suggestions_json { + Ok(suggestions_json) => { + suggestions = suggestions_json.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect(); + } + Err(e) => { + error!("Failed to load suggestions from Redis: {}", e); + } + } + } + let response = BotResponse { + bot_id: user_session.bot_id.to_string(), + user_id: user_session.user_id.to_string(), + session_id: user_session.id.to_string(), + channel: "web".to_string(), + content: message, + message_type: 1, + stream_token: None, + is_complete: true, + suggestions, + context_name: None, + context_length: 0, + context_max_length: 0, + }; + let user_id = user_session.id.to_string(); + let response_clone = response.clone(); + match state.response_channels.try_lock() { + Ok(response_channels) => { + if let Some(tx) = response_channels.get(&user_id) { + if let Err(e) = tx.try_send(response_clone) { + error!("Failed to send TALK message via WebSocket: {}", e); + } else { + trace!("TALK message sent via WebSocket"); + } + } else { + let web_adapter = Arc::clone(&state.web_adapter); + tokio::spawn(async move { + if let Err(e) = web_adapter.send_message_to_session(&user_id, response_clone).await { + error!("Failed to send TALK message via web adapter: {}", e); + } else { + trace!("TALK message sent via web adapter"); + } + }); + } + } + Err(_) => { + error!("Failed to acquire lock on response_channels for TALK command"); + } + } + Ok(response) } - pub fn talk_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - - engine - .register_custom_syntax(&["TALK", "$expr$"], true, move |context, inputs| { - let message = context.eval_expression_tree(&inputs[0])?.to_string(); - let state_for_talk = Arc::clone(&state_clone); - let user_for_talk = user_clone.clone(); - - tokio::spawn(async move { - if let Err(e) = execute_talk(state_for_talk, user_for_talk, message).await { - error!("Error executing TALK command: {}", e); - } - }); - - Ok(Dynamic::UNIT) - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["TALK", "$expr$"], true, move |context, inputs| { + let message = context.eval_expression_tree(&inputs[0])?.to_string(); + let state_for_talk = Arc::clone(&state_clone); + let user_for_talk = user_clone.clone(); + tokio::spawn(async move { + if let Err(e) = execute_talk(state_for_talk, user_for_talk, message).await { + error!("Error executing TALK command: {}", e); + } + }); + Ok(Dynamic::UNIT) + }) + .unwrap(); } diff --git a/src/basic/keywords/last.rs b/src/basic/keywords/last.rs index 868ad80cb..78d4c9088 100644 --- a/src/basic/keywords/last.rs +++ b/src/basic/keywords/last.rs @@ -1,230 +1,18 @@ use rhai::Dynamic; use rhai::Engine; - pub fn last_keyword(engine: &mut Engine) { - engine - .register_custom_syntax(&["LAST", "(", "$expr$", ")"], false, { - move |context, inputs| { - let input_string = context.eval_expression_tree(&inputs[0])?; - let input_str = input_string.to_string(); - - // Handle empty string case first - if input_str.trim().is_empty() { - return Ok(Dynamic::from("")); - } - - // Split on any whitespace and filter out empty strings - let words: Vec<&str> = input_str - .split_whitespace() - .collect(); - - // Get the last non-empty word - let last_word = words.last().map(|s| *s).unwrap_or(""); - - Ok(Dynamic::from(last_word.to_string())) - } - }) - .unwrap(); -} - -#[cfg(test)] -mod tests { - use super::*; - use rhai::{Engine, Scope}; - - #[test] - fn test_last_keyword_empty_string() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: String = engine.eval("LAST(\"\")").unwrap(); - assert_eq!(result, ""); - } - - #[test] - fn test_last_keyword_multiple_spaces() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: String = engine.eval("LAST(\"hello world \")").unwrap(); - assert_eq!(result, "world"); - } - - #[test] - fn test_last_keyword_tabs_and_newlines() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: String = engine.eval(r#"LAST("hello\tworld\n")"#).unwrap(); - assert_eq!(result, "world"); - } - - #[test] - fn test_last_keyword_with_variable() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - let mut scope = Scope::new(); - - scope.push("text", "this is a test"); - let result: String = engine.eval_with_scope(&mut scope, "LAST(text)").unwrap(); - - assert_eq!(result, "test"); - } - - #[test] - fn test_last_keyword_whitespace_only() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: String = engine.eval("LAST(\" \")").unwrap(); - assert_eq!(result, ""); - } - - #[test] - fn test_last_keyword_mixed_whitespace() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: String = engine.eval(r#"LAST("hello\t \n world \t final")"#).unwrap(); - assert_eq!(result, "final"); - } - - #[test] - fn test_last_keyword_expression() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: String = engine.eval("LAST(\"hello\" + \" \" + \"world\")").unwrap(); - assert_eq!(result, "world"); - } - - #[test] - fn test_last_keyword_unicode() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: String = engine.eval("LAST(\"hello 世界 мир world\")").unwrap(); - assert_eq!(result, "world"); - } - - #[test] - fn test_last_keyword_in_expression() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: bool = engine.eval("LAST(\"hello world\") == \"world\"").unwrap(); - assert!(result); - } - - #[test] - fn test_last_keyword_complex_scenario() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - let mut scope = Scope::new(); - - scope.push("sentence", "The quick brown fox jumps over the lazy dog"); - let result: String = engine.eval_with_scope(&mut scope, "LAST(sentence)").unwrap(); - - assert_eq!(result, "dog"); - } - - #[test] - #[should_panic] - fn test_last_keyword_missing_parentheses() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let _: String = engine.eval("LAST \"hello world\"").unwrap(); - } - - #[test] - #[should_panic] - fn test_last_keyword_missing_closing_parenthesis() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let _: String = engine.eval("LAST(\"hello world\"").unwrap(); - } - - #[test] - #[should_panic] - fn test_last_keyword_missing_opening_parenthesis() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let _: String = engine.eval("LAST \"hello world\")").unwrap(); - } - - #[test] - fn test_last_keyword_dynamic_type() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result = engine.eval::("LAST(\"test string\")").unwrap(); - assert!(result.is::()); - assert_eq!(result.to_string(), "string"); - } - - #[test] - fn test_last_keyword_nested_expression() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let result: String = engine.eval("LAST(\"The result is: \" + \"hello world\")").unwrap(); - assert_eq!(result, "world"); - } -} - -#[cfg(test)] -mod integration_tests { - use super::*; - - #[test] - fn test_last_keyword_in_script() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let script = r#" - let sentence1 = "first second third"; - let sentence2 = "alpha beta gamma"; - - let last1 = LAST(sentence1); - let last2 = LAST(sentence2); - - last1 + " and " + last2 - "#; - - let result: String = engine.eval(script).unwrap(); - assert_eq!(result, "third and gamma"); - } - - #[test] - fn test_last_keyword_with_function() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - engine.register_fn("get_name", || -> String { "john doe".to_string() }); - - let result: String = engine.eval("LAST(get_name())").unwrap(); - assert_eq!(result, "doe"); - } - - #[test] - fn test_last_keyword_multiple_calls() { - let mut engine = Engine::new(); - last_keyword(&mut engine); - - let script = r#" - let text1 = "apple banana cherry"; - let text2 = "cat dog elephant"; - - let result1 = LAST(text1); - let result2 = LAST(text2); - - result1 + "-" + result2 - "#; - - let result: String = engine.eval(script).unwrap(); - assert_eq!(result, "cherry-elephant"); - } + engine + .register_custom_syntax(&["LAST", "(", "$expr$", ")"], false, { + move |context, inputs| { + let input_string = context.eval_expression_tree(&inputs[0])?; + let input_str = input_string.to_string(); + if input_str.trim().is_empty() { + return Ok(Dynamic::from("")); + } + let words: Vec<&str> = input_str.split_whitespace().collect(); + let last_word = words.last().map(|s| *s).unwrap_or(""); + Ok(Dynamic::from(last_word.to_string())) + } + }) + .unwrap(); } diff --git a/src/basic/keywords/list_tools.rs b/src/basic/keywords/list_tools.rs index 5d0c912e5..e1e27b6b5 100644 --- a/src/basic/keywords/list_tools.rs +++ b/src/basic/keywords/list_tools.rs @@ -1,107 +1,62 @@ use crate::basic::keywords::add_tool::get_session_tools; use crate::shared::models::UserSession; use crate::shared::state::AppState; -use log::{error, info}; +use log::{error, trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; - pub fn list_tools_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - - engine - .register_custom_syntax(&["LIST_TOOLS"], false, move |_context, _inputs| { - info!("LIST_TOOLS command executed for session: {}", user_clone.id); - - let state_for_task = Arc::clone(&state_clone); - let user_for_task = user_clone.clone(); - - // Spawn async task to list all tool associations from session - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build(); - - let send_err = if let Ok(rt) = rt { - let result = rt.block_on(async move { - list_session_tools(&state_for_task, &user_for_task).await - }); - tx.send(result).err() - } else { - tx.send(Err("Failed to build tokio runtime".to_string())) - .err() - }; - - if send_err.is_some() { - error!("Failed to send result from thread"); - } - }); - - match rx.recv_timeout(std::time::Duration::from_secs(10)) { - Ok(Ok(message)) => { - info!("LIST_TOOLS completed: {}", message); - Ok(Dynamic::from(message)) - } - Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - e.into(), - rhai::Position::NONE, - ))), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "LIST_TOOLS timed out".into(), - rhai::Position::NONE, - ))) - } - Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - format!("LIST_TOOLS failed: {}", e).into(), - rhai::Position::NONE, - ))), - } - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["LIST_TOOLS"], false, move |_context, _inputs| { + let state_for_task = Arc::clone(&state_clone); + let user_for_task = user_clone.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build(); + let send_err = if let Ok(rt) = rt { + let result = rt.block_on(async move { + list_session_tools(&state_for_task, &user_for_task).await + }); + tx.send(result).err() + } else { + tx.send(Err("Failed to build tokio runtime".to_string())).err() + }; + if send_err.is_some() { + error!("Failed to send result from thread"); + } + }); + match rx.recv_timeout(std::time::Duration::from_secs(10)) { + Ok(Ok(message)) => { + Ok(Dynamic::from(message)) + } + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime("LIST_TOOLS timed out".into(), rhai::Position::NONE))) + } + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("LIST_TOOLS failed: {}", e).into(), rhai::Position::NONE))), + } + }) + .unwrap(); } - -/// List all tools associated with the current session async fn list_session_tools(state: &AppState, user: &UserSession) -> Result { - let mut conn = state.conn.lock().map_err(|e| { - error!("Failed to acquire database lock: {}", e); - format!("Database connection error: {}", e) - })?; - - // Get all tool associations for this session - match get_session_tools(&mut *conn, &user.id) { - Ok(tools) => { - if tools.is_empty() { - info!("No tools associated with session '{}'", user.id); - Ok("No tools are currently active in this conversation".to_string()) - } else { - info!( - "Found {} tool(s) for session '{}' (user: {}, bot: {})", - tools.len(), - user.id, - user.user_id, - user.bot_id - ); - - let tool_list = tools - .iter() - .enumerate() - .map(|(idx, tool)| format!("{}. {}", idx + 1, tool)) - .collect::>() - .join("\n"); - - Ok(format!( - "Active tools in this conversation ({}):\n{}", - tools.len(), - tool_list - )) - } - } - Err(e) => { - error!("Failed to list tools for session '{}': {}", user.id, e); - Err(format!("Failed to list tools: {}", e)) - } - } + let mut conn = state.conn.get().map_err(|e| { + error!("Failed to acquire database lock: {}", e); + format!("Database connection error: {}", e) + })?; + match get_session_tools(&mut *conn, &user.id) { + Ok(tools) => { + if tools.is_empty() { + Ok("No tools are currently active in this conversation".to_string()) + } else { + trace!("Found {} tool(s) for session '{}' (user: {}, bot: {})", tools.len(), user.id, user.user_id, user.bot_id); + let tool_list = tools.iter().enumerate().map(|(idx, tool)| format!("{}. {}", idx + 1, tool)).collect::>().join("\n"); + Ok(format!("Active tools in this conversation ({}):\n{}", tools.len(), tool_list)) + } + } + Err(e) => { + error!("Failed to list tools for session '{}': {}", user.id, e); + Err(format!("Failed to list tools: {}", e)) + } + } } diff --git a/src/basic/keywords/llm_keyword.rs b/src/basic/keywords/llm_keyword.rs index cc6670a88..5da89f1c3 100644 --- a/src/basic/keywords/llm_keyword.rs +++ b/src/basic/keywords/llm_keyword.rs @@ -1,91 +1,51 @@ use crate::shared::models::UserSession; use crate::shared::state::AppState; -use log::{error, info}; +use log::{error}; use rhai::{Dynamic, Engine}; use uuid::Uuid; use std::sync::Arc; use std::time::Duration; - pub fn llm_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - - engine - .register_custom_syntax(&["LLM", "$expr$"], false, move |context, inputs| { - let text = context.eval_expression_tree(&inputs[0])?.to_string(); - - info!("LLM keyword processing text: {}", text); - - let state_for_thread = Arc::clone(&state_clone); - let prompt = build_llm_prompt(&text); - - // ---- safe runtime isolation: no deadlocks possible ---- - let (tx, rx) = std::sync::mpsc::channel(); - - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build(); - - let send_err = if let Ok(rt) = rt { - let result = rt.block_on(async move { - execute_llm_generation(state_for_thread, prompt).await - }); - tx.send(result).err() - } else { - tx.send(Err("failed to build tokio runtime".into())).err() - }; - - if send_err.is_some() { - error!("Failed to send LLM thread result"); - } - }); - - match rx.recv_timeout(Duration::from_secs(500)) { - Ok(Ok(result)) => Ok(Dynamic::from(result)), - Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - e.to_string().into(), - rhai::Position::NONE, - ))), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "LLM generation timed out".into(), - rhai::Position::NONE, - ))) - } - Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - format!("LLM thread failed: {e}").into(), - rhai::Position::NONE, - ))), - } - }) - .unwrap(); + let state_clone = Arc::clone(&state); + engine + .register_custom_syntax(&["LLM", "$expr$"], false, move |context, inputs| { + let text = context.eval_expression_tree(&inputs[0])?.to_string(); + let state_for_thread = Arc::clone(&state_clone); + let prompt = build_llm_prompt(&text); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build(); + let send_err = if let Ok(rt) = rt { + let result = rt.block_on(async move { + execute_llm_generation(state_for_thread, prompt).await + }); + tx.send(result).err() + } else { + tx.send(Err("failed to build tokio runtime".into())).err() + }; + if send_err.is_some() { + error!("Failed to send LLM thread result"); + } + }); + match rx.recv_timeout(Duration::from_secs(500)) { + Ok(Ok(result)) => Ok(Dynamic::from(result)), + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime("LLM generation timed out".into(), rhai::Position::NONE))) + } + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("LLM thread failed: {e}").into(), rhai::Position::NONE))), + } + }) + .unwrap(); } - -/// Builds a consistent LLM prompt used by all Rhai scripts. -/// You can change the style/structure here to guide the model's behavior. fn build_llm_prompt(user_text: &str) -> String { - user_text.trim().to_string() + user_text.trim().to_string() } - -/// Runs the async LLM provider call safely. -pub async fn execute_llm_generation( - state: Arc, - prompt: String, -) -> Result> { - let config_manager = crate::config::ConfigManager::new(Arc::clone(&state.conn)); - let model = config_manager - .get_config(&Uuid::nil(), "llm-model", None) - .unwrap_or_default(); - - info!("Using LLM model: {}", model); - let handler = crate::llm_models::get_handler(&model); - let raw_response = state - .llm_provider - .generate(&prompt, &serde_json::Value::Null) - .await?; - - let processed = handler.process_content(&raw_response); - info!("Processed content: {}", processed); - Ok(processed) +pub async fn execute_llm_generation(state: Arc, prompt: String) -> Result> { + let config_manager = crate::config::ConfigManager::new(state.conn.clone()); + let model = config_manager.get_config(&Uuid::nil(), "llm-model", None).unwrap_or_default(); + let handler = crate::llm_models::get_handler(&model); + let raw_response = state.llm_provider.generate(&prompt, &serde_json::Value::Null).await?; + let processed = handler.process_content(&raw_response); + Ok(processed) } diff --git a/src/basic/keywords/on.rs b/src/basic/keywords/on.rs index ddcb6c892..9b3183dc9 100644 --- a/src/basic/keywords/on.rs +++ b/src/basic/keywords/on.rs @@ -1,78 +1,57 @@ use diesel::prelude::*; -use log::{error, info}; +use log::trace; +use log::{error}; use rhai::Dynamic; use rhai::Engine; use serde_json::{json, Value}; - use crate::shared::models::TriggerKind; use crate::shared::models::UserSession; use crate::shared::state::AppState; - pub fn on_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) { - let state_clone = state.clone(); - - engine - .register_custom_syntax( - &["ON", "$ident$", "OF", "$string$"], - true, - move |context, inputs| { - let trigger_type = context.eval_expression_tree(&inputs[0])?.to_string(); - let table = context.eval_expression_tree(&inputs[1])?.to_string(); - let name = format!("{}_{}.rhai", table, trigger_type.to_lowercase()); - - let kind = match trigger_type.to_uppercase().as_str() { - "UPDATE" => TriggerKind::TableUpdate, - "INSERT" => TriggerKind::TableInsert, - "DELETE" => TriggerKind::TableDelete, - _ => return Err(format!("Invalid trigger type: {}", trigger_type).into()), - }; - - let mut conn = state_clone.conn.lock().unwrap(); - let result = execute_on_trigger(&mut *conn, kind, &table, &name) - .map_err(|e| format!("DB error: {}", e))?; - - if let Some(rows_affected) = result.get("rows_affected") { - Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0))) - } else { - Err("No rows affected".into()) - } - }, - ) - .unwrap(); + let state_clone = state.clone(); + engine + .register_custom_syntax(&["ON", "$ident$", "OF", "$string$"], true, move |context, inputs| { + let trigger_type = context.eval_expression_tree(&inputs[0])?.to_string(); + let table = context.eval_expression_tree(&inputs[1])?.to_string(); + let name = format!("{}_{}.rhai", table, trigger_type.to_lowercase()); + let kind = match trigger_type.to_uppercase().as_str() { + "UPDATE" => TriggerKind::TableUpdate, + "INSERT" => TriggerKind::TableInsert, + "DELETE" => TriggerKind::TableDelete, + _ => return Err(format!("Invalid trigger type: {}", trigger_type).into()), + }; + trace!("Starting execute_on_trigger with kind: {:?}, table: {}, param: {}", kind, table, name); + let mut conn = state_clone.conn.get().map_err(|e| format!("DB error: {}", e))?; + let result = execute_on_trigger(&mut *conn, kind, &table, &name) + .map_err(|e| format!("DB error: {}", e))?; + if let Some(rows_affected) = result.get("rows_affected") { + Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0))) + } else { + Err("No rows affected".into()) + } + }, + ) + .unwrap(); } - -pub fn execute_on_trigger( - conn: &mut diesel::PgConnection, - kind: TriggerKind, - table: &str, - param: &str, -) -> Result { - info!( - "Starting execute_on_trigger with kind: {:?}, table: {}, param: {}", - kind, table, param - ); - - use crate::shared::models::system_automations; - - let new_automation = ( - system_automations::kind.eq(kind as i32), - system_automations::target.eq(table), - system_automations::param.eq(param), - ); - - let result = diesel::insert_into(system_automations::table) - .values(&new_automation) - .execute(conn) - .map_err(|e| { - error!("SQL execution error: {}", e); - e.to_string() - })?; - - Ok(json!({ - "command": "on_trigger", - "trigger_type": format!("{:?}", kind), - "table": table, - "param": param, - "rows_affected": result - })) +pub fn execute_on_trigger(conn: &mut diesel::PgConnection, kind: TriggerKind, table: &str, param: &str) -> Result { + use crate::shared::models::system_automations; + let new_automation = ( + system_automations::kind.eq(kind as i32), + system_automations::target.eq(table), + system_automations::param.eq(param), + ); + let result = diesel::insert_into(system_automations::table) + .values(&new_automation) + .execute(conn) + .map_err(|e| { + error!("SQL execution error: {}", e); + e.to_string() + })?; + Ok(json!({ + "command": "on_trigger", + "trigger_type": format!("{:?}", kind), + "table": table, + "param": param, + "rows_affected": result + })) } diff --git a/src/basic/keywords/print.rs b/src/basic/keywords/print.rs index dcf7743ab..eef5ade1f 100644 --- a/src/basic/keywords/print.rs +++ b/src/basic/keywords/print.rs @@ -1,20 +1,15 @@ -use log::info; +use log::trace; use rhai::Dynamic; use rhai::Engine; - use crate::shared::state::AppState; use crate::shared::models::UserSession; - pub fn print_keyword(_state: &AppState, _user: UserSession, engine: &mut Engine) { - engine - .register_custom_syntax( - &["PRINT", "$expr$"], - true, - |context, inputs| { - let value = context.eval_expression_tree(&inputs[0])?; - info!("{}", value); - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); + engine + .register_custom_syntax(&["PRINT", "$expr$"], true, |context, inputs| { + let value = context.eval_expression_tree(&inputs[0])?; + trace!("PRINT: {}", value); + Ok(Dynamic::UNIT) + }, + ) + .unwrap(); } diff --git a/src/basic/keywords/set.rs b/src/basic/keywords/set.rs index 4a698f205..b5e02f773 100644 --- a/src/basic/keywords/set.rs +++ b/src/basic/keywords/set.rs @@ -1,118 +1,79 @@ use diesel::prelude::*; -use log::{error, info}; +use log::trace; +use log::{error}; use rhai::Dynamic; use rhai::Engine; use serde_json::{json, Value}; use std::error::Error; - use crate::shared::models::UserSession; use crate::shared::state::AppState; - pub fn set_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) { - let state_clone = state.clone(); - - engine - .register_custom_syntax(&["SET", "$expr$", ",", "$expr$", ",", "$expr$"], false, { - move |context, inputs| { - let table_name = context.eval_expression_tree(&inputs[0])?; - let filter = context.eval_expression_tree(&inputs[1])?; - let updates = context.eval_expression_tree(&inputs[2])?; - - let table_str = table_name.to_string(); - let filter_str = filter.to_string(); - let updates_str = updates.to_string(); - - let mut conn = state_clone.conn.lock().unwrap(); - let result = execute_set(&mut *conn, &table_str, &filter_str, &updates_str) - .map_err(|e| format!("DB error: {}", e))?; - - if let Some(rows_affected) = result.get("rows_affected") { - Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0))) - } else { - Err("No rows affected".into()) - } - } - }) - .unwrap(); + let state_clone = state.clone(); + engine + .register_custom_syntax(&["SET", "$expr$", ",", "$expr$", ",", "$expr$"], false, { + move |context, inputs| { + let table_name = context.eval_expression_tree(&inputs[0])?; + let filter = context.eval_expression_tree(&inputs[1])?; + let updates = context.eval_expression_tree(&inputs[2])?; + let table_str = table_name.to_string(); + let filter_str = filter.to_string(); + let updates_str = updates.to_string(); + trace!("Starting execute_set with table: {}, filter: {}, updates: {}", table_str, filter_str, updates_str); + let mut conn = state_clone.conn.get().map_err(|e| format!("DB error: {}", e))?; + let result = execute_set(&mut *conn, &table_str, &filter_str, &updates_str) + .map_err(|e| format!("DB error: {}", e))?; + if let Some(rows_affected) = result.get("rows_affected") { + Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0))) + } else { + Err("No rows affected".into()) + } + } + }) + .unwrap(); } - -pub fn execute_set( - conn: &mut diesel::PgConnection, - table_str: &str, - filter_str: &str, - updates_str: &str, -) -> Result { - info!( - "Starting execute_set with table: {}, filter: {}, updates: {}", - table_str, filter_str, updates_str - ); - - let (set_clause, _update_values) = parse_updates(updates_str).map_err(|e| e.to_string())?; - - let where_clause = parse_filter_for_diesel(filter_str).map_err(|e| e.to_string())?; - - let query = format!( - "UPDATE {} SET {} WHERE {}", - table_str, set_clause, where_clause - ); - info!("Executing query: {}", query); - - let result = diesel::sql_query(&query).execute(conn).map_err(|e| { - error!("SQL execution error: {}", e); - e.to_string() - })?; - - Ok(json!({ - "command": "set", - "table": table_str, - "filter": filter_str, - "updates": updates_str, - "rows_affected": result - })) +pub fn execute_set(conn: &mut diesel::PgConnection, table_str: &str, filter_str: &str, updates_str: &str) -> Result { + let (set_clause, _update_values) = parse_updates(updates_str).map_err(|e| e.to_string())?; + let where_clause = parse_filter_for_diesel(filter_str).map_err(|e| e.to_string())?; + let query = format!("UPDATE {} SET {} WHERE {}", table_str, set_clause, where_clause); + let result = diesel::sql_query(&query).execute(conn).map_err(|e| { + error!("SQL execution error: {}", e); + e.to_string() + })?; + Ok(json!({ + "command": "set", + "table": table_str, + "filter": filter_str, + "updates": updates_str, + "rows_affected": result + })) } - fn parse_updates(updates_str: &str) -> Result<(String, Vec), Box> { - let mut set_clauses = Vec::new(); - let mut params = Vec::new(); - - for (i, update) in updates_str.split(',').enumerate() { - let parts: Vec<&str> = update.split('=').collect(); - if parts.len() != 2 { - return Err("Invalid update format".into()); - } - - let column = parts[0].trim(); - let value = parts[1].trim(); - - if !column - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_') - { - return Err("Invalid column name".into()); - } - - set_clauses.push(format!("{} = ${}", column, i + 1)); - params.push(value.to_string()); - } - - Ok((set_clauses.join(", "), params)) + let mut set_clauses = Vec::new(); + let mut params = Vec::new(); + for (i, update) in updates_str.split(',').enumerate() { + let parts: Vec<&str> = update.split('=').collect(); + if parts.len() != 2 { + return Err("Invalid update format".into()); + } + let column = parts[0].trim(); + let value = parts[1].trim(); + if !column.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + return Err("Invalid column name".into()); + } + set_clauses.push(format!("{} = ${}", column, i + 1)); + params.push(value.to_string()); + } + Ok((set_clauses.join(", "), params)) } - fn parse_filter_for_diesel(filter_str: &str) -> Result> { - let parts: Vec<&str> = filter_str.split('=').collect(); - if parts.len() != 2 { - return Err("Invalid filter format. Expected 'KEY=VALUE'".into()); - } - - let column = parts[0].trim(); - let value = parts[1].trim(); - - if !column - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_') - { - return Err("Invalid column name in filter".into()); - } - - Ok(format!("{} = '{}'", column, value)) + let parts: Vec<&str> = filter_str.split('=').collect(); + if parts.len() != 2 { + return Err("Invalid filter format. Expected 'KEY=VALUE'".into()); + } + let column = parts[0].trim(); + let value = parts[1].trim(); + if !column.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + return Err("Invalid column name in filter".into()); + } + Ok(format!("{} = '{}'", column, value)) } diff --git a/src/basic/keywords/set_context.rs b/src/basic/keywords/set_context.rs index fde37f4bc..d9db4be8e 100644 --- a/src/basic/keywords/set_context.rs +++ b/src/basic/keywords/set_context.rs @@ -1,106 +1,51 @@ use std::sync::Arc; -use log::{error, info, trace}; +use log::trace; +use log::{error}; use crate::shared::state::AppState; use crate::shared::models::UserSession; use rhai::Engine; use rhai::Dynamic; - pub fn set_context_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - // Clone the Redis client (if any) for use inside the async task. - let cache = state.cache.clone(); - - engine - .register_custom_syntax( - &["SET_CONTEXT", "$expr$", "AS", "$expr$"], - true, - move |context, inputs| { - // First expression is the context name, second is the value. - let context_name = context.eval_expression_tree(&inputs[0])?.to_string(); - let context_value = context.eval_expression_tree(&inputs[1])?.to_string(); - - info!( - "SET CONTEXT command executed - name: {}, value: {}", - context_name, - context_value - ); - - // Build a Redis key that is unique per user and session. - let redis_key = format!( - "context:{}:{}:{}", - user.user_id, - user.id, - context_name - ); - - trace!( - target: "app::set_context", - "Constructed Redis key: {} for user {}, session {}, context {}", - redis_key, - user.user_id, - user.id, - context_name - ); - - // If a Redis client is configured, perform the SET operation asynchronously. - if let Some(cache_client) = &cache { - trace!("Redis client is available, preparing to set context value"); - - // Clone values needed inside the async block. - let cache_client = cache_client.clone(); - let redis_key = redis_key.clone(); - let context_value = context_value.clone(); - - trace!( - "Cloned cache_client, redis_key ({}) and context_value (len={}) for async task", - redis_key, - context_value.len() - ); - - // Spawn a background task so we don't need an async closure here. - tokio::spawn(async move { - trace!("Async task started for SET_CONTEXT operation"); - - // Acquire an async Redis connection. - let mut conn = match cache_client.get_multiplexed_async_connection().await { - Ok(conn) => { - trace!("Successfully acquired async Redis connection"); - conn - } - Err(e) => { - error!("Failed to connect to cache: {}", e); - trace!("Aborting SET_CONTEXT task due to connection error"); - return; - } - }; - - // Perform the SET command. - trace!( - "Executing Redis SET command with key: {} and value length: {}", - redis_key, - context_value.len() - ); - let result: Result<(), redis::RedisError> = redis::cmd("SET") - .arg(&redis_key) - .arg(&context_value) - .query_async(&mut conn) - .await; - - match result { - Ok(_) => { - trace!("Successfully set context in Redis for key {}", redis_key); - } - Err(e) => { - error!("Failed to set cache value: {}", e); - trace!("SET_CONTEXT Redis SET command failed"); - } - } - }); - } else { - trace!("No Redis client configured; SET_CONTEXT will not persist to cache"); - } - - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); + let cache = state.cache.clone(); + engine + .register_custom_syntax(&["SET_CONTEXT", "$expr$", "AS", "$expr$"], true, move |context, inputs| { + let context_name = context.eval_expression_tree(&inputs[0])?.to_string(); + let context_value = context.eval_expression_tree(&inputs[1])?.to_string(); + trace!("SET CONTEXT command executed - name: {}, value: {}", context_name, context_value); + let redis_key = format!("context:{}:{}:{}", user.user_id, user.id, context_name); + trace!("Constructed Redis key: {} for user {}, session {}, context {}", redis_key, user.user_id, user.id, context_name); + if let Some(cache_client) = &cache { + let cache_client = cache_client.clone(); + let redis_key = redis_key.clone(); + let context_value = context_value.clone(); + trace!("Cloned cache_client, redis_key ({}) and context_value (len={}) for async task", redis_key, context_value.len()); + tokio::spawn(async move { + let mut conn = match cache_client.get_multiplexed_async_connection().await { + Ok(conn) => { + trace!("Cache connection established successfully"); + conn + } + Err(e) => { + error!("Failed to connect to cache: {}", e); + return; + } + }; + trace!("Executing Redis SET command with key: {} and value length: {}", redis_key, context_value.len()); + let result: Result<(), redis::RedisError> = redis::cmd("SET").arg(&redis_key).arg(&context_value).query_async(&mut conn).await; + match result { + Ok(_) => { + trace!("Context value successfully stored in cache"); + } + Err(e) => { + error!("Failed to set cache value: {}", e); + } + } + }); + } else { + trace!("No cache configured, context not persisted"); + } + Ok(Dynamic::UNIT) + }, + ) + .unwrap(); } diff --git a/src/basic/keywords/set_kb.rs b/src/basic/keywords/set_kb.rs index 9cf28573e..5dff64aad 100644 --- a/src/basic/keywords/set_kb.rs +++ b/src/basic/keywords/set_kb.rs @@ -1,206 +1,101 @@ use crate::shared::models::UserSession; use crate::shared::state::AppState; -use log::{error, info}; +use log::{error, trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; - pub fn set_kb_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - - engine - .register_custom_syntax(&["SET_KB", "$expr$"], false, move |context, inputs| { - let kb_name = context.eval_expression_tree(&inputs[0])?; - let kb_name_str = kb_name.to_string().trim_matches('"').to_string(); - - info!( - "SET_KB command executed: {} for user: {}", - kb_name_str, user_clone.user_id - ); - - // Validate KB name (alphanumeric and underscores only) - if !kb_name_str - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-') - { - return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "KB name must contain only alphanumeric characters, underscores, and hyphens" - .into(), - rhai::Position::NONE, - ))); - } - - if kb_name_str.is_empty() { - return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "KB name cannot be empty".into(), - rhai::Position::NONE, - ))); - } - - let state_for_task = Arc::clone(&state_clone); - let user_for_task = user_clone.clone(); - let kb_name_for_task = kb_name_str.clone(); - - // Spawn async task to set up KB collection - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build(); - - let send_err = if let Ok(rt) = rt { - let result = rt.block_on(async move { - add_kb_to_user( - &state_for_task, - &user_for_task, - &kb_name_for_task, - false, - None, - ) - .await - }); - tx.send(result).err() - } else { - tx.send(Err("failed to build tokio runtime".into())).err() - }; - - if send_err.is_some() { - error!("Failed to send result from thread"); - } - }); - - match rx.recv_timeout(std::time::Duration::from_secs(30)) { - Ok(Ok(message)) => { - info!("SET_KB completed: {}", message); - Ok(Dynamic::from(message)) - } - Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - e.into(), - rhai::Position::NONE, - ))), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "SET_KB timed out".into(), - rhai::Position::NONE, - ))) - } - Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - format!("SET_KB failed: {}", e).into(), - rhai::Position::NONE, - ))), - } - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["SET_KB", "$expr$"], false, move |context, inputs| { + let kb_name = context.eval_expression_tree(&inputs[0])?; + let kb_name_str = kb_name.to_string().trim_matches('"').to_string(); + trace!("SET_KB command executed: {} for user: {}", kb_name_str, user_clone.user_id); + if !kb_name_str.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime("KB name must contain only alphanumeric characters, underscores, and hyphens".into(), rhai::Position::NONE))); + } + if kb_name_str.is_empty() { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime("KB name cannot be empty".into(), rhai::Position::NONE))); + } + let state_for_task = Arc::clone(&state_clone); + let user_for_task = user_clone.clone(); + let kb_name_for_task = kb_name_str.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build(); + let send_err = if let Ok(rt) = rt { + let result = rt.block_on(async move { + add_kb_to_user(&state_for_task, &user_for_task, &kb_name_for_task, false, None).await + }); + tx.send(result).err() + } else { + tx.send(Err("failed to build tokio runtime".into())).err() + }; + if send_err.is_some() { + error!("Failed to send result from thread"); + } + }); + match rx.recv_timeout(std::time::Duration::from_secs(30)) { + Ok(Ok(message)) => { + Ok(Dynamic::from(message)) + } + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime("SET_KB timed out".into(), rhai::Position::NONE))) + } + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SET_KB failed: {}", e).into(), rhai::Position::NONE))), + } + }) + .unwrap(); } - pub fn add_kb_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - - engine - .register_custom_syntax(&["ADD_KB", "$expr$"], false, move |context, inputs| { - let kb_name = context.eval_expression_tree(&inputs[0])?; - let kb_name_str = kb_name.to_string().trim_matches('"').to_string(); - - info!( - "ADD_KB command executed: {} for user: {}", - kb_name_str, user_clone.user_id - ); - - // Validate KB name - if !kb_name_str - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-') - { - return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "KB name must contain only alphanumeric characters, underscores, and hyphens" - .into(), - rhai::Position::NONE, - ))); - } - - let state_for_task = Arc::clone(&state_clone); - let user_for_task = user_clone.clone(); - let kb_name_for_task = kb_name_str.clone(); - - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build(); - - let send_err = if let Ok(rt) = rt { - let result = rt.block_on(async move { - add_kb_to_user( - &state_for_task, - &user_for_task, - &kb_name_for_task, - false, - None, - ) - .await - }); - tx.send(result).err() - } else { - tx.send(Err("failed to build tokio runtime".into())).err() - }; - - if send_err.is_some() { - error!("Failed to send result from thread"); - } - }); - - match rx.recv_timeout(std::time::Duration::from_secs(30)) { - Ok(Ok(message)) => { - info!("ADD_KB completed: {}", message); - Ok(Dynamic::from(message)) - } - Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - e.into(), - rhai::Position::NONE, - ))), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - "ADD_KB timed out".into(), - rhai::Position::NONE, - ))) - } - Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - format!("ADD_KB failed: {}", e).into(), - rhai::Position::NONE, - ))), - } - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["ADD_KB", "$expr$"], false, move |context, inputs| { + let kb_name = context.eval_expression_tree(&inputs[0])?; + let kb_name_str = kb_name.to_string().trim_matches('"').to_string(); + trace!("ADD_KB command executed: {} for user: {}", kb_name_str, user_clone.user_id); + if !kb_name_str.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime("KB name must contain only alphanumeric characters, underscores, and hyphens".into(), rhai::Position::NONE))); + } + let state_for_task = Arc::clone(&state_clone); + let user_for_task = user_clone.clone(); + let kb_name_for_task = kb_name_str.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build(); + let send_err = if let Ok(rt) = rt { + let result = rt.block_on(async move { + add_kb_to_user(&state_for_task, &user_for_task, &kb_name_for_task, false, None).await + }); + tx.send(result).err() + } else { + tx.send(Err("failed to build tokio runtime".into())).err() + }; + if send_err.is_some() { + error!("Failed to send result from thread"); + } + }); + match rx.recv_timeout(std::time::Duration::from_secs(30)) { + Ok(Ok(message)) => { + Ok(Dynamic::from(message)) + } + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime("ADD_KB timed out".into(), rhai::Position::NONE))) + } + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("ADD_KB failed: {}", e).into(), rhai::Position::NONE))), + } + }) + .unwrap(); } - -/// Add a KB to user's active KBs (stored in user_kb_associations table) -async fn add_kb_to_user( - _state: &AppState, - user: &UserSession, - kb_name: &str, - is_website: bool, - website_url: Option, -) -> Result { - // TODO: Insert into user_kb_associations table using Diesel - // For now, just log the action - - info!( - "KB '{}' associated with user '{}' (bot: {}, is_website: {})", - kb_name, user.user_id, user.bot_id, is_website - ); - - if is_website { - if let Some(url) = website_url { - info!("Website URL: {}", url); - return Ok(format!( - "Website KB '{}' added successfully for user", - kb_name - )); - } - } - - Ok(format!("KB '{}' added successfully for user", kb_name)) +async fn add_kb_to_user(_state: &AppState, user: &UserSession, kb_name: &str, is_website: bool, website_url: Option) -> Result { + trace!("KB '{}' associated with user '{}' (bot: {}, is_website: {})", kb_name, user.user_id, user.bot_id, is_website); + if is_website { + if let Some(_url) = website_url { + return Ok(format!("Website KB '{}' added successfully for user", kb_name)); + } + } + Ok(format!("KB '{}' added successfully for user", kb_name)) } diff --git a/src/basic/keywords/set_schedule.rs b/src/basic/keywords/set_schedule.rs index fa5f78996..2b7dc04d6 100644 --- a/src/basic/keywords/set_schedule.rs +++ b/src/basic/keywords/set_schedule.rs @@ -1,69 +1,43 @@ use diesel::prelude::*; -use log::info; +use log::{trace}; use serde_json::{json, Value}; use uuid::Uuid; - use crate::shared::models::TriggerKind; - - -pub fn execute_set_schedule( - conn: &mut diesel::PgConnection, - cron: &str, - script_name: &str, - bot_uuid: Uuid, -) -> Result> { - info!( - "Scheduling SET SCHEDULE cron: {}, script: {}, bot_id: {:?}", - cron, script_name, bot_uuid - ); - - // First check if bot exists - use crate::shared::models::bots::dsl::bots; - let bot_exists: bool = diesel::select(diesel::dsl::exists( - bots.filter(crate::shared::models::bots::dsl::id.eq(bot_uuid)) - )) - .get_result(conn)?; - - if !bot_exists { - return Err(format!("Bot with id {} does not exist", bot_uuid).into()); - } - - use crate::shared::models::system_automations::dsl::*; - - let new_automation = ( - bot_id.eq(bot_uuid), - kind.eq(TriggerKind::Scheduled as i32), - schedule.eq(cron), - param.eq(script_name), - is_active.eq(true), - ); - - // First try to update existing record - let update_result = diesel::update(system_automations) - .filter(bot_id.eq(bot_uuid)) - .filter(kind.eq(TriggerKind::Scheduled as i32)) - .filter(param.eq(script_name)) - .set(( - schedule.eq(cron), - is_active.eq(true), - last_triggered.eq(None::>), - )) - .execute(&mut *conn)?; - - // If no rows were updated, insert new record - let result = if update_result == 0 { - diesel::insert_into(system_automations) - .values(&new_automation) - .execute(&mut *conn)? - } else { - update_result - }; - - Ok(json!({ - "command": "set_schedule", - "schedule": cron, - "script": script_name, - "bot_id": bot_uuid.to_string(), - "rows_affected": result - })) +pub fn execute_set_schedule(conn: &mut diesel::PgConnection, cron: &str, script_name: &str, bot_uuid: Uuid) -> Result> { + trace!("Scheduling SET SCHEDULE cron: {}, script: {}, bot_id: {:?}", cron, script_name, bot_uuid); + use crate::shared::models::bots::dsl::bots; + let bot_exists: bool = diesel::select(diesel::dsl::exists(bots.filter(crate::shared::models::bots::dsl::id.eq(bot_uuid)))).get_result(conn)?; + if !bot_exists { + return Err(format!("Bot with id {} does not exist", bot_uuid).into()); + } + use crate::shared::models::system_automations::dsl::*; + let new_automation = ( + bot_id.eq(bot_uuid), + kind.eq(TriggerKind::Scheduled as i32), + schedule.eq(cron), + param.eq(script_name), + is_active.eq(true), + ); + let update_result = diesel::update(system_automations) + .filter(bot_id.eq(bot_uuid)) + .filter(kind.eq(TriggerKind::Scheduled as i32)) + .filter(param.eq(script_name)) + .set(( + schedule.eq(cron), + is_active.eq(true), + last_triggered.eq(None::>), + )) + .execute(&mut *conn)?; + let result = if update_result == 0 { + diesel::insert_into(system_automations).values(&new_automation).execute(&mut *conn)? + } else { + update_result + }; + Ok(json!({ + "command": "set_schedule", + "schedule": cron, + "script": script_name, + "bot_id": bot_uuid.to_string(), + "rows_affected": result + })) } diff --git a/src/basic/keywords/set_user.rs b/src/basic/keywords/set_user.rs index 516bdf5fe..499acf023 100644 --- a/src/basic/keywords/set_user.rs +++ b/src/basic/keywords/set_user.rs @@ -1,44 +1,31 @@ use crate::shared::state::AppState; use crate::shared::models::UserSession; -use log::{debug, error, info}; +use log::{ error,trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; use uuid::Uuid; - pub fn set_user_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - engine - .register_custom_syntax(&["SET_USER", "$expr$"], true, move |context, inputs| { - let user_id_str = context.eval_expression_tree(&inputs[0])?.to_string(); - - info!("SET USER command executed with ID: {}", user_id_str); - - match Uuid::parse_str(&user_id_str) { - Ok(user_id) => { - debug!("Successfully parsed user UUID: {}", user_id); - - let state_for_spawn = Arc::clone(&state_clone); - let user_clone_spawn = user_clone.clone(); - - let mut session_manager = - futures::executor::block_on(state_for_spawn.session_manager.lock()); - - if let Err(e) = session_manager.update_user_id(user_clone_spawn.id, user_id) { - error!("Failed to update user ID in session: {}", e); - } else { - info!( - "Updated session {} to user ID: {}", - user_clone_spawn.id, user_id - ); - } - } - Err(e) => { - debug!("Invalid UUID format for SET USER: {}", e); - } - } - - Ok(Dynamic::UNIT) - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["SET_USER", "$expr$"], true, move |context, inputs| { + let user_id_str = context.eval_expression_tree(&inputs[0])?.to_string(); + match Uuid::parse_str(&user_id_str) { + Ok(user_id) => { + let state_for_spawn = Arc::clone(&state_clone); + let user_clone_spawn = user_clone.clone(); + let mut session_manager = futures::executor::block_on(state_for_spawn.session_manager.lock()); + if let Err(e) = session_manager.update_user_id(user_clone_spawn.id, user_id) { + error!("Failed to update user ID in session: {}", e); + } else { + trace!("Updated session {} to user ID: {}", user_clone_spawn.id, user_id); + } + } + Err(e) => { + trace!("Invalid user ID format: {}", e); + } + } + Ok(Dynamic::UNIT) + }) + .unwrap(); } diff --git a/src/basic/keywords/wait.rs b/src/basic/keywords/wait.rs index e7d1ac10b..4e50f5e35 100644 --- a/src/basic/keywords/wait.rs +++ b/src/basic/keywords/wait.rs @@ -1,44 +1,27 @@ use crate::shared::state::AppState; use crate::shared::models::UserSession; -use log::info; use rhai::{Dynamic, Engine}; use std::thread; use std::time::Duration; - pub fn wait_keyword(_state: &AppState, _user: UserSession, engine: &mut Engine) { - engine - .register_custom_syntax( - &["WAIT", "$expr$"], - false, - move |context, inputs| { - let seconds = context.eval_expression_tree(&inputs[0])?; - - let duration_secs = if seconds.is::() { - seconds.cast::() as f64 - } else if seconds.is::() { - seconds.cast::() - } else { - return Err(format!("WAIT expects a number, got: {}", seconds).into()); - }; - - if duration_secs < 0.0 { - return Err("WAIT duration cannot be negative".into()); - } - - let capped_duration = if duration_secs > 300.0 { - 300.0 - } else { - duration_secs - }; - - info!("WAIT {} seconds (thread sleep)", capped_duration); - - let duration = Duration::from_secs_f64(capped_duration); - thread::sleep(duration); - - info!("WAIT completed after {} seconds", capped_duration); - Ok(Dynamic::from(format!("Waited {} seconds", capped_duration))) - }, - ) - .unwrap(); + engine + .register_custom_syntax(&["WAIT", "$expr$"], false, move |context, inputs| { + let seconds = context.eval_expression_tree(&inputs[0])?; + let duration_secs = if seconds.is::() { + seconds.cast::() as f64 + } else if seconds.is::() { + seconds.cast::() + } else { + return Err(format!("WAIT expects a number, got: {}", seconds).into()); + }; + if duration_secs < 0.0 { + return Err("WAIT duration cannot be negative".into()); + } + let capped_duration = if duration_secs > 300.0 { 300.0 } else { duration_secs }; + let duration = Duration::from_secs_f64(capped_duration); + thread::sleep(duration); + Ok(Dynamic::from(format!("Waited {} seconds", capped_duration))) + }, + ) + .unwrap(); } diff --git a/src/bootstrap/mod.rs b/src/bootstrap/mod.rs index abc3b1feb..3f20a8134 100644 --- a/src/bootstrap/mod.rs +++ b/src/bootstrap/mod.rs @@ -1,430 +1,360 @@ -use crate::config::{AppConfig, write_drive_config_to_env}; +use crate::config::AppConfig; use crate::package_manager::{InstallMode, PackageManager}; use crate::shared::utils::establish_pg_connection; use anyhow::Result; -use diesel::{connection::SimpleConnection}; -use dotenvy::dotenv; -use log::{debug, error, info, trace}; -use aws_sdk_s3::Client; use aws_config::BehaviorVersion; -use rand::distr::Alphanumeric; -use rand::Rng; +use aws_sdk_s3::Client; +use diesel::connection::SimpleConnection; +use log::{error, info, trace}; use std::io::{self, Write}; use std::path::Path; use std::process::Command; -use std::sync::{Arc, Mutex}; - pub struct ComponentInfo { -pub name: &'static str, + pub name: &'static str, } - pub struct BootstrapManager { -pub install_mode: InstallMode, -pub tenant: Option, -pub s3_client: Client, + pub install_mode: InstallMode, + pub tenant: Option, } - impl BootstrapManager { -fn is_postgres_running() -> bool { -match Command::new("pg_isready").arg("-q").status() { -Ok(status) => status.success(), -Err(_) => { -Command::new("pgrep").arg("postgres").output().map(|o| !o.stdout.is_empty()).unwrap_or(false) -} -} -} - -pub async fn new(install_mode: InstallMode, tenant: Option) -> Self { -trace!("Initializing BootstrapManager with mode {:?} and tenant {:?}", install_mode, tenant); -if !Self::is_postgres_running() { -let pm = PackageManager::new(install_mode.clone(), tenant.clone()) -.expect("Failed to initialize PackageManager"); -if let Err(e) = pm.start("tables") { -error!("Failed to start Tables server component automatically: {}", e); -panic!("Database not available and auto-start failed."); -} else { -info!("Started Tables server component automatically"); -} -} -let config = AppConfig::from_env().expect("Failed to load config from env"); -let s3_client = Self::create_s3_operator(&config).await; -Self { -install_mode, -tenant, -s3_client, -} -} - -pub fn start_all(&mut self) -> Result<()> { -let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; -let components = vec![ -ComponentInfo { name: "tables" }, -ComponentInfo { name: "cache" }, -ComponentInfo { name: "drive" }, -ComponentInfo { name: "llm" }, -ComponentInfo { name: "email" }, -ComponentInfo { name: "proxy" }, -ComponentInfo { name: "directory" }, -ComponentInfo { name: "alm" }, -ComponentInfo { name: "alm_ci" }, -ComponentInfo { name: "dns" }, -ComponentInfo { name: "webmail" }, -ComponentInfo { name: "meeting" }, -ComponentInfo { name: "table_editor" }, -ComponentInfo { name: "doc_editor" }, -ComponentInfo { name: "desktop" }, -ComponentInfo { name: "devtools" }, -ComponentInfo { name: "bot" }, -ComponentInfo { name: "system" }, -ComponentInfo { name: "vector_db" }, -ComponentInfo { name: "host" }, -]; -for component in components { -if pm.is_installed(component.name) { -pm.start(component.name)?; -} -} -Ok(()) -} - -pub async fn bootstrap(&mut self) -> Result { -if let Ok(tables_server) = std::env::var("TABLES_SERVER") { -if !tables_server.is_empty() { -info!("Legacy mode detected (TABLES_SERVER present), skipping bootstrap installation"); -let _database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { -let username = std::env::var("TABLES_USERNAME").unwrap_or_else(|_| "gbuser".to_string()); -let password = std::env::var("TABLES_PASSWORD").unwrap_or_else(|_| "postgres".to_string()); -let server = std::env::var("TABLES_SERVER").unwrap_or_else(|_| "localhost".to_string()); -let port = std::env::var("TABLES_PORT").unwrap_or_else(|_| "5432".to_string()); -let database = std::env::var("TABLES_DATABASE").unwrap_or_else(|_| "gbserver".to_string()); -format!("postgres://{}:{}@{}:{}/{}", username, password, server, port, database) -}); -if let Ok(config) = self.load_config_from_csv().await { -return Ok(config); -} -match establish_pg_connection() { -Ok(mut conn) => { -if let Err(e) = self.apply_migrations(&mut conn) { -log::warn!("Failed to apply migrations: {}", e); -} -return Ok(AppConfig::from_database(&mut conn).expect("Failed to load config from DB")); -} -Err(e) => { -log::warn!("Failed to connect to database: {}", e); -return Ok(AppConfig::from_env()?); -} -} -} -} -let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; -let required_components = vec!["tables", "drive", "cache", "llm"]; -let mut config = AppConfig::from_env().expect("Failed to load config from env"); -for component in required_components { -if !pm.is_installed(component) { -let termination_cmd = pm -.components -.get(component) -.and_then(|cfg| cfg.binary_name.clone()) -.unwrap_or_else(|| component.to_string()); -if !termination_cmd.is_empty() { -let check = Command::new("pgrep") -.arg("-f") -.arg(&termination_cmd) -.output(); -if let Ok(output) = check { -if !output.stdout.is_empty() { -println!("Component '{}' appears to be already running from a previous install.", component); -println!("Do you want to terminate it? (y/n)"); -let mut input = String::new(); -io::stdout().flush().unwrap(); -io::stdin().read_line(&mut input).unwrap(); -if input.trim().eq_ignore_ascii_case("y") { -let _ = Command::new("pkill") -.arg("-f") -.arg(&termination_cmd) -.status(); -println!("Terminated existing '{}' process.", component); -} else { -println!("Skipping start of '{}' as it is already running.", component); -continue; -} -} -} -} -if component == "tables" { -let db_password = self.generate_secure_password(16); -let farm_password = self.generate_secure_password(32); -let env_contents = format!( -"FARM_PASSWORD={}\nDATABASE_URL=postgres://gbuser:{}@localhost:5432/botserver", -farm_password, db_password -); -std::fs::write(".env", &env_contents) -.map_err(|e| anyhow::anyhow!("Failed to write .env file: {}", e))?; -dotenv().ok(); -} -pm.install(component).await?; -if component == "tables" { -let mut conn = establish_pg_connection() -.map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?; -let migration_dir = include_dir::include_dir!("./migrations"); -let mut migration_files: Vec<_> = migration_dir -.files() -.filter_map(|file| { -let path = file.path(); -if path.extension()? == "sql" { -Some(file) -} else { -None -} -}) -.collect(); -migration_files.sort_by_key(|f| f.path()); -for migration_file in migration_files { -let migration = migration_file -.contents_utf8() -.ok_or_else(|| anyhow::anyhow!("Migration file is not valid UTF-8"))?; -if let Err(e) = conn.batch_execute(migration) { -log::error!("Failed to execute migration {}: {}", migration_file.path().display(), e); -return Err(e.into()); -} -trace!("Successfully executed migration: {}", migration_file.path().display()); -} -config = AppConfig::from_database(&mut conn).expect("Failed to load config from DB"); -} -} -} -self.s3_client = Self::create_s3_operator(&config).await; -let final_config = if let Ok(csv_config) = self.load_config_from_csv().await { -csv_config -} else { -config -}; -if std::env::var("DRIVE_SERVER").is_err() { -write_drive_config_to_env(&final_config.drive) -.map_err(|e| anyhow::anyhow!("Failed to write drive config to .env: {}", e))?; -} -Ok(final_config) -} - -async fn create_s3_operator(config: &AppConfig) -> Client { -let endpoint = if !config.drive.server.ends_with('/') { -format!("{}/", config.drive.server) -} else { -config.drive.server.clone() -}; -let base_config = aws_config::defaults(BehaviorVersion::latest()) -.endpoint_url(endpoint) -.region("auto") -.credentials_provider( -aws_sdk_s3::config::Credentials::new( -config.drive.access_key.clone(), -config.drive.secret_key.clone(), -None, -None, -"static", -) -) -.load() -.await; -let s3_config = aws_sdk_s3::config::Builder::from(&base_config) -.force_path_style(true) -.build(); -aws_sdk_s3::Client::from_conf(s3_config) -} - -fn generate_secure_password(&self, length: usize) -> String { -let mut rng = rand::rng(); -std::iter::repeat_with(|| rng.sample(Alphanumeric) as char) -.take(length) -.collect() -} - -pub async fn upload_templates_to_drive(&self, _config: &AppConfig) -> Result<()> { -let mut conn = establish_pg_connection()?; -self.create_bots_from_templates(&mut conn)?; -let templates_dir = Path::new("templates"); -if !templates_dir.exists() { -return Ok(()); -} -let client = &self.s3_client; -let mut read_dir = tokio::fs::read_dir(templates_dir).await?; -while let Some(entry) = read_dir.next_entry().await? { -let path = entry.path(); -if path.is_dir() -&& path -.file_name() -.unwrap() -.to_string_lossy() -.ends_with(".gbai") -{ -let bot_name = path.file_name().unwrap().to_string_lossy().to_string(); -let bucket = bot_name.trim_start_matches('/').to_string(); -if client.head_bucket().bucket(&bucket).send().await.is_err() { -match client.create_bucket() -.bucket(&bucket) -.send() -.await { -Ok(_) => { -trace!("Created bucket: {}", bucket); -self.upload_directory_recursive(client, &path, &bucket, "/") -.await?; -} -Err(e) => { -error!("Failed to create bucket {}: {:?}", bucket, e); -return Err(anyhow::anyhow!( -"Failed to create bucket {}: {}. Check S3 credentials and endpoint configuration", -bucket, e -)); -} -} -} else { -debug!("Bucket {} already exists", bucket); -} -} -} -Ok(()) -} - -fn create_bots_from_templates(&self, conn: &mut diesel::PgConnection) -> Result<()> { -use crate::shared::models::schema::bots; -use diesel::prelude::*; -let templates_dir = Path::new("templates"); -if !templates_dir.exists() { -return Ok(()); -} -for entry in std::fs::read_dir(templates_dir)? { -let entry = entry?; -let path = entry.path(); -if path.is_dir() && path.extension().map(|e| e == "gbai").unwrap_or(false) { -let bot_folder = path.file_name().unwrap().to_string_lossy().to_string(); -let bot_name = bot_folder.trim_end_matches(".gbai"); -let existing: Option = bots::table -.filter(bots::name.eq(&bot_name)) -.select(bots::name) -.first(conn) -.optional()?; -if existing.is_none() { -diesel::sql_query( -"INSERT INTO bots (id, name, description, llm_provider, llm_config, context_provider, context_config, is_active) \ -VALUES (gen_random_uuid(), $1, $2, 'openai', '{\"model\": \"gpt-4\", \"temperature\": 0.7}', 'database', '{}', true)" -) -.bind::(&bot_name) -.bind::(format!("Bot for {} template", bot_name)) -.execute(conn)?; -info!("Created bot: {}", bot_name); -} else { -debug!("Bot {} already exists", bot_name); -} -} -} -Ok(()) -} - -fn upload_directory_recursive<'a>( -&'a self, -client: &'a Client, -local_path: &'a Path, -bucket: &'a str, -prefix: &'a str, -) -> std::pin::Pin> + 'a>> { -Box::pin(async move { -let _normalized_path = if !local_path.to_string_lossy().ends_with('/') { -format!("{}/", local_path.to_string_lossy()) -} else { -local_path.to_string_lossy().to_string() -}; -let mut read_dir = tokio::fs::read_dir(local_path).await?; -while let Some(entry) = read_dir.next_entry().await? { -let path = entry.path(); -let file_name = path.file_name().unwrap().to_string_lossy().to_string(); -let mut key = prefix.trim_matches('/').to_string(); -if !key.is_empty() { -key.push('/'); -} -key.push_str(&file_name); -if path.is_file() { -trace!("Uploading file {} to bucket {} with key {}", path.display(), bucket, key); -let content = tokio::fs::read(&path).await?; -client.put_object() -.bucket(bucket) -.key(&key) -.body(content.into()) -.send() -.await?; -} else if path.is_dir() { -self.upload_directory_recursive(client, &path, bucket, &key).await?; -} -} -Ok(()) -}) -} - -async fn load_config_from_csv(&self) -> Result { -use crate::config::ConfigManager; -use uuid::Uuid; -let client = &self.s3_client; -let bucket = "default.gbai"; -let config_key = "default.gbot/config.csv"; -match client.get_object() -.bucket(bucket) -.key(config_key) -.send() -.await -{ -Ok(response) => { -trace!("Found config.csv in default.gbai"); -let bytes = response.body.collect().await?.into_bytes(); -let csv_content = String::from_utf8(bytes.to_vec())?; -let config_conn = establish_pg_connection()?; -let config_manager = ConfigManager::new(Arc::new(Mutex::new(config_conn))); -let default_bot_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000")?; -let temp_path = std::env::temp_dir().join("config.csv"); -tokio::fs::write(&temp_path, csv_content).await?; -config_manager.sync_gbot_config(&default_bot_id, temp_path.to_str().unwrap()) -.map_err(|e| anyhow::anyhow!("Failed to sync gbot config: {}", e))?; -let mut final_conn = establish_pg_connection()?; -let config = AppConfig::from_database(&mut final_conn)?; -Ok(config) -} -Err(e) => { -debug!("No config.csv found in default.gbai: {:?}", e); -Err(e.into()) -} -} -} - -fn apply_migrations(&self, conn: &mut diesel::PgConnection) -> Result<()> { -let migrations_dir = std::path::Path::new("migrations"); -if !migrations_dir.exists() { -return Ok(()); -} -let mut sql_files: Vec<_> = std::fs::read_dir(migrations_dir)? -.filter_map(|entry| entry.ok()) -.filter(|entry| { -entry -.path() -.extension() -.and_then(|s| s.to_str()) -.map(|s| s == "sql") -.unwrap_or(false) -}) -.collect(); -sql_files.sort_by_key(|entry| entry.path()); -for entry in sql_files { -let path = entry.path(); -let filename = path.file_name().unwrap().to_string_lossy(); -match std::fs::read_to_string(&path) { -Ok(sql) => match conn.batch_execute(&sql) { -Err(e) => { -log::warn!("Migration {} failed: {}", filename, e); -} -_ => {} -}, -Err(e) => { -log::warn!("Failed to read migration {}: {}", filename, e); -} -} -} -Ok(()) -} + fn is_postgres_running() -> bool { + match Command::new("pg_isready").arg("-q").status() { + Ok(status) => status.success(), + Err(_) => Command::new("pgrep") + .arg("postgres") + .output() + .map(|o| !o.stdout.is_empty()) + .unwrap_or(false), + } + } + pub async fn new(install_mode: InstallMode, tenant: Option) -> Self { + trace!( + "Initializing BootstrapManager with mode {:?} and tenant {:?}", + install_mode, + tenant + ); + if !Self::is_postgres_running() { + let pm = PackageManager::new(install_mode.clone(), tenant.clone()) + .expect("Failed to initialize PackageManager"); + if let Err(e) = pm.start("tables") { + error!( + "Failed to start Tables server component automatically: {}", + e + ); + panic!("Database not available and auto-start failed."); + } else { + trace!("Tables server started successfully"); + } + } + Self { + install_mode, + tenant, + } + } + pub fn start_all(&mut self) -> Result<()> { + let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; + let components = vec![ + ComponentInfo { name: "tables" }, + ComponentInfo { name: "cache" }, + ComponentInfo { name: "drive" }, + ComponentInfo { name: "llm" }, + ComponentInfo { name: "email" }, + ComponentInfo { name: "proxy" }, + ComponentInfo { name: "directory" }, + ComponentInfo { name: "alm" }, + ComponentInfo { name: "alm_ci" }, + ComponentInfo { name: "dns" }, + ComponentInfo { name: "webmail" }, + ComponentInfo { name: "meeting" }, + ComponentInfo { + name: "table_editor", + }, + ComponentInfo { name: "doc_editor" }, + ComponentInfo { name: "desktop" }, + ComponentInfo { name: "devtools" }, + ComponentInfo { name: "bot" }, + ComponentInfo { name: "system" }, + ComponentInfo { name: "vector_db" }, + ComponentInfo { name: "host" }, + ]; + for component in components { + if pm.is_installed(component.name) { + pm.start(component.name)?; + } + } + Ok(()) + } + pub async fn bootstrap(&mut self) { + if let Ok(tables_server) = std::env::var("TABLES_SERVER") { + if !tables_server.is_empty() { + info!( + "Legacy mode detected (TABLES_SERVER present), skipping bootstrap installation" + ); + let _database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { + let username = + std::env::var("TABLES_USERNAME").unwrap_or_else(|_| "gbuser".to_string()); + let password = + std::env::var("TABLES_PASSWORD").unwrap_or_else(|_| "postgres".to_string()); + let server = + std::env::var("TABLES_SERVER").unwrap_or_else(|_| "localhost".to_string()); + let port = std::env::var("TABLES_PORT").unwrap_or_else(|_| "5432".to_string()); + let database = + std::env::var("TABLES_DATABASE").unwrap_or_else(|_| "gbserver".to_string()); + format!( + "postgres://{}:{}@{}:{}/{}", + username, password, server, port, database + ) + }); + match establish_pg_connection() { + Ok(mut conn) => { + if let Err(e) = self.apply_migrations(&mut conn) { + log::warn!("Failed to apply migrations: {}", e); + } + } + Err(e) => { + log::warn!("Failed to connect to database: {}", e); + } + } + } + } + let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone()).unwrap(); + let required_components = vec!["tables", "drive", "cache", "llm"]; + for component in required_components { + if !pm.is_installed(component) { + let termination_cmd = pm + .components + .get(component) + .and_then(|cfg| cfg.binary_name.clone()) + .unwrap_or_else(|| component.to_string()); + if !termination_cmd.is_empty() { + let check = Command::new("pgrep") + .arg("-f") + .arg(&termination_cmd) + .output(); + if let Ok(output) = check { + if !output.stdout.is_empty() { + println!("Component '{}' appears to be already running from a previous install.", component); + println!("Do you want to terminate it? (y/n)"); + let mut input = String::new(); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut input).unwrap(); + if input.trim().eq_ignore_ascii_case("y") { + let _ = Command::new("pkill") + .arg("-f") + .arg(&termination_cmd) + .status(); + println!("Terminated existing '{}' process.", component); + } else { + println!( + "Skipping start of '{}' as it is already running.", + component + ); + continue; + } + } + } + } + _ = pm.install(component).await; + if component == "tables" { + let mut conn = establish_pg_connection().unwrap(); + let migration_dir = include_dir::include_dir!("./migrations"); + let mut migration_files: Vec<_> = migration_dir + .files() + .filter_map(|file| { + let path = file.path(); + if path.extension()? == "sql" { + Some(file) + } else { + None + } + }) + .collect(); + migration_files.sort_by_key(|f| f.path()); + for migration_file in migration_files { + let migration = migration_file + .contents_utf8() + .ok_or_else(|| anyhow::anyhow!("Migration file is not valid UTF-8")); + if let Err(e) = conn.batch_execute(migration.unwrap()) { + log::error!( + "Failed to execute migration {}: {}", + migration_file.path().display(), + e + ); + } + trace!( + "Successfully executed migration: {}", + migration_file.path().display() + ); + } + } + } + } + } + async fn create_s3_operator(config: &AppConfig) -> Client { + let endpoint = if !config.drive.server.ends_with('/') { + format!("{}/", config.drive.server) + } else { + config.drive.server.clone() + }; + let base_config = aws_config::defaults(BehaviorVersion::latest()) + .endpoint_url(endpoint) + .region("auto") + .credentials_provider(aws_sdk_s3::config::Credentials::new( + config.drive.access_key.clone(), + config.drive.secret_key.clone(), + None, + None, + "static", + )) + .load() + .await; + let s3_config = aws_sdk_s3::config::Builder::from(&base_config) + .force_path_style(true) + .build(); + aws_sdk_s3::Client::from_conf(s3_config) + } + pub async fn upload_templates_to_drive(&self, _config: &AppConfig) -> Result<()> { + let mut conn = establish_pg_connection()?; + self.create_bots_from_templates(&mut conn)?; + let templates_dir = Path::new("templates"); + if !templates_dir.exists() { + return Ok(()); + } + let client = Self::create_s3_operator(_config).await; + let mut read_dir = tokio::fs::read_dir(templates_dir).await?; + while let Some(entry) = read_dir.next_entry().await? { + let path = entry.path(); + if path.is_dir() + && path + .file_name() + .unwrap() + .to_string_lossy() + .ends_with(".gbai") + { + let bot_name = path.file_name().unwrap().to_string_lossy().to_string(); + let bucket = bot_name.trim_start_matches('/').to_string(); + if client.head_bucket().bucket(&bucket).send().await.is_err() { + match client.create_bucket().bucket(&bucket).send().await { + Ok(_) => { + self.upload_directory_recursive(&client, &path, &bucket, "/") + .await?; + } + Err(e) => { + error!("Failed to create bucket {}: {:?}", bucket, e); + return Err(anyhow::anyhow!("Failed to create bucket {}: {}. Check S3 credentials and endpoint configuration", bucket, e)); + } + } + } else { + trace!("Bucket {} already exists", bucket); + } + } + } + Ok(()) + } + fn create_bots_from_templates(&self, conn: &mut diesel::PgConnection) -> Result<()> { + use crate::shared::models::schema::bots; + use diesel::prelude::*; + let templates_dir = Path::new("templates"); + if !templates_dir.exists() { + return Ok(()); + } + for entry in std::fs::read_dir(templates_dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() && path.extension().map(|e| e == "gbai").unwrap_or(false) { + let bot_folder = path.file_name().unwrap().to_string_lossy().to_string(); + let bot_name = bot_folder.trim_end_matches(".gbai"); + let existing: Option = bots::table + .filter(bots::name.eq(&bot_name)) + .select(bots::name) + .first(conn) + .optional()?; + if existing.is_none() { + diesel::sql_query("INSERT INTO bots (id, name, description, llm_provider, llm_config, context_provider, context_config, is_active) VALUES (gen_random_uuid(), $1, $2, 'openai', '{\"model\": \"gpt-4\", \"temperature\": 0.7}', 'database', '{}', true)").bind::(&bot_name).bind::(format!("Bot for {} template", bot_name)).execute(conn)?; + } else { + trace!("Bot {} already exists", bot_name); + } + } + } + Ok(()) + } + fn upload_directory_recursive<'a>( + &'a self, + client: &'a Client, + local_path: &'a Path, + bucket: &'a str, + prefix: &'a str, + ) -> std::pin::Pin> + 'a>> { + Box::pin(async move { + let _normalized_path = if !local_path.to_string_lossy().ends_with('/') { + format!("{}/", local_path.to_string_lossy()) + } else { + local_path.to_string_lossy().to_string() + }; + let mut read_dir = tokio::fs::read_dir(local_path).await?; + while let Some(entry) = read_dir.next_entry().await? { + let path = entry.path(); + let file_name = path.file_name().unwrap().to_string_lossy().to_string(); + let mut key = prefix.trim_matches('/').to_string(); + if !key.is_empty() { + key.push('/'); + } + key.push_str(&file_name); + if path.is_file() { + trace!( + "Uploading file {} to bucket {} with key {}", + path.display(), + bucket, + key + ); + let content = tokio::fs::read(&path).await?; + client + .put_object() + .bucket(bucket) + .key(&key) + .body(content.into()) + .send() + .await?; + } else if path.is_dir() { + self.upload_directory_recursive(client, &path, bucket, &key) + .await?; + } + } + Ok(()) + }) + } + fn apply_migrations(&self, conn: &mut diesel::PgConnection) -> Result<()> { + let migrations_dir = std::path::Path::new("migrations"); + if !migrations_dir.exists() { + return Ok(()); + } + let mut sql_files: Vec<_> = std::fs::read_dir(migrations_dir)? + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry + .path() + .extension() + .and_then(|s| s.to_str()) + .map(|s| s == "sql") + .unwrap_or(false) + }) + .collect(); + sql_files.sort_by_key(|entry| entry.path()); + for entry in sql_files { + let path = entry.path(); + let filename = path.file_name().unwrap().to_string_lossy(); + match std::fs::read_to_string(&path) { + Ok(sql) => match conn.batch_execute(&sql) { + Err(e) => { + log::warn!("Migration {} failed: {}", filename, e); + } + _ => {} + }, + Err(e) => { + log::warn!("Failed to read migration {}: {}", filename, e); + } + } + } + Ok(()) + } } diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 56b8070e2..5595ab068 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -65,7 +65,7 @@ impl BotOrchestrator { use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut db_conn = self.state.conn.lock().unwrap(); + let mut db_conn = self.state.conn.get().unwrap(); let active_bots = bots .filter(is_active.eq(true)) .select(id) @@ -102,7 +102,7 @@ impl BotOrchestrator { use diesel::prelude::*; let bot_name: String = { - let mut db_conn = state.conn.lock().unwrap(); + let mut db_conn = state.conn.get().unwrap(); bots.filter(id.eq(Uuid::parse_str(&bot_guid)?)) .select(name) .first(&mut *db_conn) @@ -154,7 +154,7 @@ impl BotOrchestrator { use diesel::prelude::*; let bot_name: String = { - let mut db_conn = self.state.conn.lock().unwrap(); + let mut db_conn = self.state.conn.get().unwrap(); bots.filter(id.eq(Uuid::parse_str(&bot_guid)?)) .select(name) .first(&mut *db_conn) @@ -251,7 +251,7 @@ impl BotOrchestrator { ..event_response }; - if let Some(adapter) = self.state.channels.lock().unwrap().get(channel) { + if let Some(adapter) = self.state.channels.lock().await.get(channel) { adapter.send_message(event_response).await?; } else { warn!("No channel adapter found for channel: {}", channel); @@ -310,7 +310,7 @@ impl BotOrchestrator { context_max_length: 0, }; - if let Some(adapter) = self.state.channels.lock().unwrap().get(channel) { + if let Some(adapter) = self.state.channels.lock().await.get(channel) { adapter.send_message(confirmation).await?; } @@ -396,7 +396,7 @@ impl BotOrchestrator { // Get history limit from bot config (default -1 for unlimited) let history_limit = { - let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); + let config_manager = ConfigManager::new(self.state.conn.clone()); config_manager .get_config( &Uuid::parse_str(&message.bot_id).unwrap_or_default(), @@ -496,7 +496,7 @@ impl BotOrchestrator { // Calculate initial token count let initial_tokens = crate::shared::utils::estimate_token_count(&prompt); - let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); + let config_manager = ConfigManager::new(self.state.conn.clone()); let max_context_size = config_manager .get_config( &Uuid::parse_str(&message.bot_id).unwrap_or_default(), @@ -593,11 +593,11 @@ impl BotOrchestrator { } } - trace!( + info!( "Stream processing completed, {} chunks processed", chunk_count ); - + // Sum tokens from all p.push context builds before submission let total_tokens = crate::shared::utils::estimate_token_count(&prompt) + crate::shared::utils::estimate_token_count(&context_data) @@ -608,7 +608,7 @@ impl BotOrchestrator { ); // Trigger compact prompt if enabled - let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); + let config_manager = ConfigManager::new( self.state.conn.clone()); let compact_enabled = config_manager .get_config( &Uuid::parse_str(&message.bot_id).unwrap_or_default(), @@ -636,7 +636,7 @@ impl BotOrchestrator { sm.save_message(session.id, user_id, 2, &full_response, 1)?; } - let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); + let config_manager = ConfigManager::new(self.state.conn.clone()); let max_context_size = config_manager .get_config( &Uuid::parse_str(&message.bot_id).unwrap_or_default(), @@ -710,7 +710,7 @@ impl BotOrchestrator { let bot_id = session.bot_id; let bot_name: String = { - let mut db_conn = state.conn.lock().unwrap(); + let mut db_conn = state.conn.get().unwrap(); bots.filter(id.eq(Uuid::parse_str(&bot_id.to_string())?)) .select(name) .first(&mut *db_conn) @@ -896,7 +896,7 @@ async fn websocket_handler( use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut db_conn = data.conn.lock().unwrap(); + let mut db_conn = data.conn.get().unwrap(); match bots .filter(is_active.eq(true)) .select(id) @@ -1010,7 +1010,7 @@ async fn websocket_handler( use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut db_conn = data.conn.lock().unwrap(); + let mut db_conn = data.conn.get().unwrap(); match bots .filter(is_active.eq(true)) .select(id) @@ -1069,7 +1069,7 @@ async fn websocket_handler( use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut db_conn = data.conn.lock().unwrap(); + let mut db_conn = data.conn.get().unwrap(); match bots .filter(is_active.eq(true)) .select(id) diff --git a/src/config/mod.rs b/src/config/mod.rs index 3871947b6..7850d01da 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,13 +1,10 @@ +use crate::shared::utils::{ DbPool}; use diesel::prelude::*; -use diesel::pg::PgConnection; -use uuid::Uuid; -use log::{trace}; +use diesel::r2d2::{ConnectionManager, PooledConnection}; use std::collections::HashMap; use std::fs::OpenOptions; use std::io::Write; -use std::sync::{Arc, Mutex}; -use crate::shared::utils::establish_pg_connection; - +use uuid::Uuid; #[derive(Clone)] pub struct AppConfig { pub drive: DriveConfig, @@ -15,7 +12,6 @@ pub struct AppConfig { pub database: DatabaseConfig, pub site_path: String, } - #[derive(Clone)] pub struct DatabaseConfig { pub username: String, @@ -24,7 +20,6 @@ pub struct DatabaseConfig { pub port: u32, pub database: String, } - #[derive(Clone)] pub struct DriveConfig { pub server: String, @@ -32,13 +27,11 @@ pub struct DriveConfig { pub secret_key: String, pub use_ssl: bool, } - #[derive(Clone)] pub struct ServerConfig { pub host: String, pub port: u16, } - impl AppConfig { pub fn database_url(&self) -> String { format!( @@ -51,72 +44,82 @@ impl AppConfig { ) } } - impl AppConfig { - pub fn from_database(conn: &mut PgConnection) -> Result { + pub fn from_database(pool: &DbPool) -> Result { use crate::shared::models::schema::bot_configuration::dsl::*; - use diesel::prelude::*; - - let config_map: HashMap = bot_configuration - .select((id, bot_id, config_key, config_value, config_type, is_encrypted)) - .load::<(Uuid, Uuid, String, String, String, bool)>(conn) - .unwrap_or_default() - .into_iter() - .map(|(_, _, key, value, _, _)| (key.clone(), (Uuid::nil(), Uuid::nil(), key, value, String::new(), false))) - .collect(); - + let mut conn = pool.get().map_err(|e| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UnableToSendCommand, + Box::new(e.to_string()), + ) + })?; + let config_map: HashMap = + bot_configuration + .select(( + id, + bot_id, + config_key, + config_value, + config_type, + is_encrypted, + )) + .load::<(Uuid, Uuid, String, String, String, bool)>(&mut conn) + .unwrap_or_default() + .into_iter() + .map(|(_, _, key, value, _, _)| { + ( + key.clone(), + (Uuid::nil(), Uuid::nil(), key, value, String::new(), false), + ) + }) + .collect(); let mut get_str = |key: &str, default: &str| -> String { bot_configuration .filter(config_key.eq(key)) .select(config_value) - .first::(conn) + .first::(&mut conn) .unwrap_or_else(|_| default.to_string()) }; - let get_u32 = |key: &str, default: u32| -> u32 { config_map .get(key) .and_then(|v| v.3.parse().ok()) .unwrap_or(default) }; - let get_u16 = |key: &str, default: u16| -> u16 { config_map .get(key) .and_then(|v| v.3.parse().ok()) .unwrap_or(default) }; - let get_bool = |key: &str, default: bool| -> bool { config_map .get(key) .map(|v| v.3.to_lowercase() == "true") .unwrap_or(default) }; - let database = DatabaseConfig { -username: match std::env::var("TABLES_USERNAME") { - Ok(v) => v, - Err(_) => get_str("TABLES_USERNAME", "gbuser"), -}, -password: match std::env::var("TABLES_PASSWORD") { - Ok(v) => v, - Err(_) => get_str("TABLES_PASSWORD", ""), -}, -server: match std::env::var("TABLES_SERVER") { - Ok(v) => v, - Err(_) => get_str("TABLES_SERVER", "localhost"), -}, -port: std::env::var("TABLES_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or_else(|| get_u32("TABLES_PORT", 5432)), -database: match std::env::var("TABLES_DATABASE") { - Ok(v) => v, - Err(_) => get_str("TABLES_DATABASE", "botserver"), -}, + username: match std::env::var("TABLES_USERNAME") { + Ok(v) => v, + Err(_) => get_str("TABLES_USERNAME", "gbuser"), + }, + password: match std::env::var("TABLES_PASSWORD") { + Ok(v) => v, + Err(_) => get_str("TABLES_PASSWORD", ""), + }, + server: match std::env::var("TABLES_SERVER") { + Ok(v) => v, + Err(_) => get_str("TABLES_SERVER", "localhost"), + }, + port: std::env::var("TABLES_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or_else(|| get_u32("TABLES_PORT", 5432)), + database: match std::env::var("TABLES_DATABASE") { + Ok(v) => v, + Err(_) => get_str("TABLES_DATABASE", "botserver"), + }, }; - let drive = DriveConfig { server: { let server = get_str("DRIVE_SERVER", "http://localhost:9000"); @@ -130,7 +133,6 @@ database: match std::env::var("TABLES_DATABASE") { secret_key: get_str("DRIVE_SECRET", "minioadmin"), use_ssl: get_bool("DRIVE_USE_SSL", false), }; - Ok(AppConfig { drive, server: ServerConfig { @@ -139,20 +141,16 @@ database: match std::env::var("TABLES_DATABASE") { }, database, site_path: { - let fresh_conn = establish_pg_connection().map_err(|e| diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::UnableToSendCommand, Box::new(e.to_string())))?; - ConfigManager::new(Arc::new(Mutex::new(fresh_conn))) - .get_config(&Uuid::nil(), "SITES_ROOT", Some("./botserver-stack/sites"))?.to_string() + ConfigManager::new(pool.clone()) + .get_config(&Uuid::nil(), "SITES_ROOT", Some("./botserver-stack/sites"))? + .to_string() }, }) } - pub fn from_env() -> Result { - let database_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); - + let database_url = std::env::var("DATABASE_URL").unwrap(); let (db_username, db_password, db_server, db_port, db_name) = parse_database_url(&database_url); - let database = DatabaseConfig { username: db_username, password: db_password, @@ -160,19 +158,13 @@ database: match std::env::var("TABLES_DATABASE") { port: db_port, database: db_name, }; - let minio = DriveConfig { server: std::env::var("DRIVE_SERVER") - .unwrap_or_else(|_| "http://localhost:9000".to_string()), + .unwrap(); access_key: std::env::var("DRIVE_ACCESSKEY") - .unwrap_or_else(|_| "minioadmin".to_string()), + .unwrap(); secret_key: std::env::var("DRIVE_SECRET").unwrap_or_else(|_| "minioadmin".to_string()), - use_ssl: std::env::var("DRIVE_USE_SSL") - .unwrap_or_else(|_| "false".to_string()) - .parse() - .unwrap_or(false) }; - Ok(AppConfig { drive: minio, server: ServerConfig { @@ -184,54 +176,36 @@ database: match std::env::var("TABLES_DATABASE") { }, database, site_path: { - let conn = PgConnection::establish(&database_url)?; - ConfigManager::new(Arc::new(Mutex::new(conn))) - .get_config(&Uuid::nil(), "SITES_ROOT", Some("./botserver-stack/sites"))? + let pool = create_conn()?; + ConfigManager::new(pool).get_config( + &Uuid::nil(), + "SITES_ROOT", + Some("./botserver-stack/sites"), + )? }, }) } } - -pub fn write_drive_config_to_env(drive: &DriveConfig) -> std::io::Result<()> { - let mut file = OpenOptions::new() - .append(true) - .create(true) - .open(".env")?; - - writeln!(file, "")?; - writeln!(file, "DRIVE_SERVER={}", drive.server)?; - writeln!(file, "DRIVE_ACCESSKEY={}", drive.access_key)?; - writeln!(file, "DRIVE_SECRET={}", drive.secret_key)?; - writeln!(file, "DRIVE_USE_SSL={}", drive.use_ssl)?; - - Ok(()) -} - fn parse_database_url(url: &str) -> (String, String, String, u32, String) { if let Some(stripped) = url.strip_prefix("postgres://") { let parts: Vec<&str> = stripped.split('@').collect(); if parts.len() == 2 { let user_pass: Vec<&str> = parts[0].split(':').collect(); let host_db: Vec<&str> = parts[1].split('/').collect(); - if user_pass.len() >= 2 && host_db.len() >= 2 { let username = user_pass[0].to_string(); let password = user_pass[1].to_string(); - let host_port: Vec<&str> = host_db[0].split(':').collect(); let server = host_port[0].to_string(); let port = host_port .get(1) .and_then(|p| p.parse().ok()) .unwrap_or(5432); - let database = host_db[1].to_string(); - return (username, password, server, port, database); } } } - ( "gbuser".to_string(), "".to_string(), @@ -240,16 +214,23 @@ fn parse_database_url(url: &str) -> (String, String, String, u32, String) { "botserver".to_string(), ) } - pub struct ConfigManager { - conn: Arc>, + conn: DbPool, } - impl ConfigManager { - pub fn new(conn: Arc>) -> Self { + pub fn new(conn: DbPool) -> Self { Self { conn } } - + fn get_conn( + &self, + ) -> Result>, diesel::result::Error> { + self.conn.get().map_err(|e| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UnableToSendCommand, + Box::new(e.to_string()), + ) + }) + } pub fn get_config( &self, code_bot_id: &uuid::Uuid, @@ -257,72 +238,55 @@ impl ConfigManager { fallback: Option<&str>, ) -> Result { use crate::shared::models::schema::bot_configuration::dsl::*; - - let mut conn = self.conn.lock().unwrap(); + let mut conn = self.get_conn()?; let fallback_str = fallback.unwrap_or(""); - let result = bot_configuration .filter(bot_id.eq(code_bot_id)) .filter(config_key.eq(key)) .select(config_value) - .first::(&mut *conn); - + .first::(&mut conn); let value = match result { Ok(v) => v, Err(_) => { - let (default_bot_id, _default_bot_name) = crate::bot::get_default_bot(&mut *conn); - + let (default_bot_id, _default_bot_name) = crate::bot::get_default_bot(&mut conn); bot_configuration .filter(bot_id.eq(default_bot_id)) .filter(config_key.eq(key)) .select(config_value) - .first::(&mut *conn) + .first::(&mut conn) .unwrap_or(fallback_str.to_string()) } }; - Ok(value) } - - pub fn sync_gbot_config( - &self, - bot_id: &uuid::Uuid, - content: &str, - ) -> Result { + pub fn sync_gbot_config(&self, bot_id: &uuid::Uuid, content: &str) -> Result { use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); - let mut conn = self - .conn - .lock() - .map_err(|e| format!("Failed to acquire lock: {}", e))?; - + .get_conn() + .map_err(|e| format!("Failed to acquire connection: {}", e))?; let mut updated = 0; - for line in content.lines().skip(1) { let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 2 { let key = parts[0].trim(); let value = parts[1].trim(); - let new_id: uuid::Uuid = uuid::Uuid::new_v4(); - diesel::sql_query("INSERT INTO bot_configuration (id, bot_id, config_key, config_value, config_type) VALUES ($1, $2, $3, $4, 'string') ON CONFLICT (bot_id, config_key) DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()") - .bind::(new_id) - .bind::(bot_id) - .bind::(key) - .bind::(value) - .execute(&mut *conn) - .map_err(|e| format!("Failed to update config: {}", e))?; - + .bind::(new_id) + .bind::(bot_id) + .bind::(key) + .bind::(value) + .execute(&mut conn) + .map_err(|e| format!("Failed to update config: {}", e))?; updated += 1; } } - - trace!("Synced {} config values for bot {}", updated, bot_id); - Ok(updated) } } +fn create_conn() -> Result { + crate::shared::utils::create_conn() + .map_err(|e| anyhow::anyhow!("Failed to create database pool: {}", e)) +} diff --git a/src/drive_monitor/mod.rs b/src/drive_monitor/mod.rs index 90e04c91d..1f3a7237e 100644 --- a/src/drive_monitor/mod.rs +++ b/src/drive_monitor/mod.rs @@ -132,7 +132,7 @@ impl DriveMonitor { } async fn check_gbot(&self, client: &Client) -> Result<(), Box> { - let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); + let config_manager = ConfigManager::new(self.state.conn.clone()); let mut continuation_token = None; loop { @@ -194,7 +194,7 @@ impl DriveMonitor { let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content); if restart_needed { - if let Err(e) = ensure_llama_servers_running(&self.state).await { + if let Err(e) = ensure_llama_servers_running(Arc::clone(&self.state)).await { log::error!("Failed to restart LLaMA servers after llm- config change: {}", e); } } diff --git a/src/llm/local.rs b/src/llm/local.rs index 0cfacd3b0..de1b269b1 100644 --- a/src/llm/local.rs +++ b/src/llm/local.rs @@ -28,29 +28,27 @@ pub async fn embeddings_local( } pub async fn ensure_llama_servers_running( - app_state: &Arc + app_state: Arc ) -> Result<(), Box> { // Get all config values before starting async operations let config_values = { let conn_arc = app_state.conn.clone(); - tokio::task::spawn_blocking(move || { - let mut conn = conn_arc.lock().unwrap(); - let config_manager = ConfigManager::new(Arc::clone(&conn_arc)); - - let default_bot_id = bots.filter(name.eq("default")) + let default_bot_id = tokio::task::spawn_blocking(move || { + let mut conn = conn_arc.get().unwrap(); + bots.filter(name.eq("default")) .select(id) .first::(&mut *conn) - .unwrap_or_else(|_| uuid::Uuid::nil()); - - ( - default_bot_id, - config_manager.get_config(&default_bot_id, "llm-url", None).unwrap_or_default(), - config_manager.get_config(&default_bot_id, "llm-model", None).unwrap_or_default(), - config_manager.get_config(&default_bot_id, "embedding-url", None).unwrap_or_default(), - config_manager.get_config(&default_bot_id, "embedding-model", None).unwrap_or_default(), - config_manager.get_config(&default_bot_id, "llm-server-path", None).unwrap_or_default(), - ) - }).await? + .unwrap_or_else(|_| uuid::Uuid::nil()) + }).await?; + let config_manager = ConfigManager::new(app_state.conn.clone()); + ( + default_bot_id, + config_manager.get_config(&default_bot_id, "llm-url", None).unwrap_or_default(), + config_manager.get_config(&default_bot_id, "llm-model", None).unwrap_or_default(), + config_manager.get_config(&default_bot_id, "embedding-url", None).unwrap_or_default(), + config_manager.get_config(&default_bot_id, "embedding-model", None).unwrap_or_default(), + config_manager.get_config(&default_bot_id, "llm-server-path", None).unwrap_or_default(), + ) }; let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_server_path) = config_values; @@ -90,7 +88,7 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se if !llm_running && !llm_model.is_empty() { info!("Starting LLM server..."); tasks.push(tokio::spawn(start_llm_server( - Arc::clone(app_state), + Arc::clone(&app_state), llm_server_path.clone(), llm_model.clone(), llm_url.clone(), @@ -192,14 +190,12 @@ pub async fn start_llm_server( let conn = app_state.conn.clone(); let config_manager = ConfigManager::new(conn.clone()); - let default_bot_id = { - let mut conn = conn.lock().unwrap(); - bots.filter(name.eq("default")) + let mut conn = conn.get().unwrap(); + let default_bot_id = bots.filter(name.eq("default")) .select(id) .first::(&mut *conn) - .unwrap_or_else(|_| uuid::Uuid::nil()) - }; - + .unwrap_or_else(|_| uuid::Uuid::nil()); + let n_moe = config_manager.get_config(&default_bot_id, "llm-server-n-moe", None).unwrap_or("4".to_string()); let parallel = config_manager.get_config(&default_bot_id, "llm-server-parallel", None).unwrap_or("1".to_string()); let cont_batching = config_manager.get_config(&default_bot_id, "llm-server-cont-batching", None).unwrap_or("true".to_string()); diff --git a/src/main.rs b/src/main.rs index 26bab7da7..0481ae0f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use actix_web::{web, App, HttpServer}; use dotenvy::dotenv; use log::{error, info}; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; mod auth; mod automation; mod basic; @@ -37,299 +37,320 @@ use crate::channels::{VoiceAdapter, WebChannelAdapter}; use crate::config::AppConfig; #[cfg(feature = "email")] use crate::email::{ -get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email, + get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email, }; use crate::file::{init_drive, upload_file}; use crate::meet::{voice_start, voice_stop}; use crate::package_manager::InstallMode; use crate::session::{create_session, get_session_history, get_sessions, start_session}; use crate::shared::state::AppState; +use crate::shared::utils::create_conn; use crate::web_server::{bot_index, index, static_files}; - #[derive(Debug, Clone)] pub enum BootstrapProgress { -StartingBootstrap, -InstallingComponent(String), -StartingComponent(String), -UploadingTemplates, -ConnectingDatabase, -StartingLLM, -BootstrapComplete, -BootstrapError(String), + StartingBootstrap, + InstallingComponent(String), + StartingComponent(String), + UploadingTemplates, + ConnectingDatabase, + StartingLLM, + BootstrapComplete, + BootstrapError(String), } - #[tokio::main] async fn main() -> std::io::Result<()> { -use crate::llm::local::ensure_llama_servers_running; -use botserver::config::ConfigManager; -let args: Vec = std::env::args().collect(); -let no_ui = args.contains(&"--noui".to_string()); -if args.len() > 1 { -let command = &args[1]; -match command.as_str() { -"install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help" -| "-h" => match package_manager::cli::run().await { -Ok(_) => return Ok(()), -Err(e) => { -eprintln!("CLI error: {}", e); -return Err(std::io::Error::new( -std::io::ErrorKind::Other, -format!("CLI command failed: {}", e), -)); -} -}, -"--noui" => {} -_ => { -eprintln!("Unknown command: {}", command); -eprintln!("Run 'botserver --help' for usage information"); -return Err(std::io::Error::new( -std::io::ErrorKind::InvalidInput, -format!("Unknown command: {}", command), -)); -} -} -} -dotenv().ok(); -let (progress_tx, progress_rx) = tokio::sync::mpsc::unbounded_channel::(); -let (state_tx, state_rx) = tokio::sync::mpsc::channel::>(1); -let ui_handle = if !no_ui { -let progress_rx = Arc::new(tokio::sync::Mutex::new(progress_rx)); -let state_rx = Arc::new(tokio::sync::Mutex::new(state_rx)); -let handle = std::thread::Builder::new() -.name("ui-thread".to_string()) -.spawn(move || { -let mut ui = crate::ui_tree::XtreeUI::new(); -ui.set_progress_channel(progress_rx.clone()); -let rt = tokio::runtime::Builder::new_current_thread() -.enable_all() -.build() -.expect("Failed to create UI runtime"); -rt.block_on(async { -tokio::select! { -result = async { -let mut rx = state_rx.lock().await; -rx.recv().await -} => { -if let Some(app_state) = result { -ui.set_app_state(app_state); -} -} -_ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => { -eprintln!("UI initialization timeout"); -} -} -}); -if let Err(e) = ui.start_ui() { -eprintln!("UI error: {}", e); -} -}) -.expect("Failed to spawn UI thread"); -Some(handle) -} else { -env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) -.write_style(env_logger::WriteStyle::Always) -.init(); -None -}; -let install_mode = if args.contains(&"--container".to_string()) { -InstallMode::Container -} else { -InstallMode::Local -}; -let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { -args.get(idx + 1).cloned() -} else { -None -}; -let progress_tx_clone = progress_tx.clone(); -let cfg = { - progress_tx_clone.send(BootstrapProgress::StartingBootstrap).ok(); - let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await; - let env_path = match std::env::current_dir() { - Ok(dir) => dir.join("botserver-stack").join(".env"), - Err(_) => { - progress_tx_clone.send(BootstrapProgress::BootstrapError("Failed to get current directory".to_string())).ok(); - return Err(std::io::Error::new(std::io::ErrorKind::Other, "Failed to get current directory")); - } - }; - let cfg = if env_path.exists() { - progress_tx_clone.send(BootstrapProgress::ConnectingDatabase).ok(); - match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string())) { - Ok(mut conn) => { - AppConfig::from_database(&mut conn).unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")) - } - Err(_) => AppConfig::from_env().expect("Failed to load config from env"), - } - } else { - match bootstrap.bootstrap().await { - Ok(config) => config, - Err(e) => { - progress_tx_clone.send(BootstrapProgress::BootstrapError(format!("Bootstrap failed: {}", e))).ok(); - match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string())) { - Ok(mut conn) => { - AppConfig::from_database(&mut conn).unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")) - } - Err(_) => AppConfig::from_env().expect("Failed to load config from env"), + use crate::llm::local::ensure_llama_servers_running; + use botserver::config::ConfigManager; + let args: Vec = std::env::args().collect(); + let no_ui = args.contains(&"--noui".to_string()); + if args.len() > 1 { + let command = &args[1]; + match command.as_str() { + "install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help" + | "-h" => match package_manager::cli::run().await { + Ok(_) => return Ok(()), + Err(e) => { + eprintln!("CLI error: {}", e); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("CLI command failed: {}", e), + )); } + }, + "--noui" => {} + _ => { + eprintln!("Unknown command: {}", command); + eprintln!("Run 'botserver --help' for usage information"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Unknown command: {}", command), + )); } } + } + dotenv().ok(); + let (progress_tx, progress_rx) = tokio::sync::mpsc::unbounded_channel::(); + let (state_tx, state_rx) = tokio::sync::mpsc::channel::>(1); + let ui_handle = if !no_ui { + let progress_rx = Arc::new(tokio::sync::Mutex::new(progress_rx)); + let state_rx = Arc::new(tokio::sync::Mutex::new(state_rx)); + let handle = std::thread::Builder::new() + .name("ui-thread".to_string()) + .spawn(move || { + let mut ui = crate::ui_tree::XtreeUI::new(); + ui.set_progress_channel(progress_rx.clone()); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create UI runtime"); + rt.block_on(async { + tokio::select! { + result = async { + let mut rx = state_rx.lock().await; + rx.recv().await + } => { + if let Some(app_state) = result { + ui.set_app_state(app_state); + } + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => { + eprintln!("UI initialization timeout"); + } + } + }); + if let Err(e) = ui.start_ui() { + eprintln!("UI error: {}", e); + } + }) + .expect("Failed to spawn UI thread"); + Some(handle) + } else { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .write_style(env_logger::WriteStyle::Always) + .init(); + None }; - progress_tx_clone.send(BootstrapProgress::StartingComponent("all services".to_string())).ok(); - if let Err(e) = bootstrap.start_all() { - progress_tx_clone.send(BootstrapProgress::BootstrapError(format!("Failed to start services: {}", e))).ok(); + let install_mode = if args.contains(&"--container".to_string()) { + InstallMode::Container + } else { + InstallMode::Local + }; + let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { + args.get(idx + 1).cloned() + } else { + None + }; + let progress_tx_clone = progress_tx.clone(); + let cfg = { + progress_tx_clone + .send(BootstrapProgress::StartingBootstrap) + .ok(); + let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await; + let env_path = std::env::current_dir().unwrap().join(".env"); + let cfg = if env_path.exists() { + progress_tx_clone + .send(BootstrapProgress::ConnectingDatabase) + .ok(); + match create_conn() { + Ok(pool) => { + let mut conn = pool.get().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::ConnectionRefused, + format!("Database connection failed: {}", e), + ) + })?; + AppConfig::from_database(&pool) + .unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")) + } + Err(_) => AppConfig::from_env().expect("Failed to load config from env"), + } + } else { + bootstrap.bootstrap().await; + match create_conn() { + Ok(pool) => AppConfig::from_database(&pool) + .unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")), + Err(_) => AppConfig::from_env().expect("Failed to load config from env"), + } + }; + progress_tx_clone + .send(BootstrapProgress::StartingComponent( + "all services".to_string(), + )) + .ok(); + if let Err(e) = bootstrap.start_all() { + progress_tx_clone + .send(BootstrapProgress::BootstrapError(format!( + "Failed to start services: {}", + e + ))) + .ok(); + } + progress_tx_clone + .send(BootstrapProgress::UploadingTemplates) + .ok(); + if let Err(e) = bootstrap.upload_templates_to_drive(&cfg).await { + progress_tx_clone + .send(BootstrapProgress::BootstrapError(format!( + "Failed to upload templates: {}", + e + ))) + .ok(); + } + Ok::(cfg) + }; + let cfg = cfg?; + dotenv().ok(); + let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env"); + let config = std::sync::Arc::new(refreshed_cfg.clone()); + progress_tx.send(BootstrapProgress::ConnectingDatabase).ok(); + let pool = match create_conn() { + Ok(pool) => pool, + Err(e) => { + error!("Failed to create database pool: {}", e); + progress_tx + .send(BootstrapProgress::BootstrapError(format!( + "Database pool creation failed: {}", + e + ))) + .ok(); + return Err(std::io::Error::new( + std::io::ErrorKind::ConnectionRefused, + format!("Database pool creation failed: {}", e), + )); + } + }; + let cache_url = std::env::var("CACHE_URL") + .or_else(|_| std::env::var("REDIS_URL")) + .unwrap_or_else(|_| "redis://localhost:6379".to_string()); + let redis_client = match redis::Client::open(cache_url.as_str()) { + Ok(client) => Some(Arc::new(client)), + Err(e) => { + log::warn!("Failed to connect to Redis: {}", e); + None + } + }; + let web_adapter = Arc::new(WebChannelAdapter::new()); + let voice_adapter = Arc::new(VoiceAdapter::new()); + let drive = init_drive(&config.drive) + .await + .expect("Failed to initialize Drive"); + let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new( + pool.get().unwrap(), + redis_client.clone(), + ))); + let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new())); + let config_manager = ConfigManager::new(pool.clone()); + let mut bot_conn = pool.get().expect("Failed to get database connection"); + let (default_bot_id, _default_bot_name) = crate::bot::get_default_bot(&mut bot_conn); + let llm_url = config_manager + .get_config(&default_bot_id, "llm-url", Some("http://localhost:8081")) + .unwrap_or_else(|_| "http://localhost:8081".to_string()); + let llm_provider = Arc::new(crate::llm::OpenAIClient::new( + "empty".to_string(), + Some(llm_url.clone()), + )); + let app_state = Arc::new(AppState { + drive: Some(drive), + config: Some(cfg.clone()), + conn: pool.clone(), + bucket_name: "default.gbai".to_string(), + cache: redis_client.clone(), + session_manager: session_manager.clone(), + llm_provider: llm_provider.clone(), + auth_service: auth_service.clone(), + channels: Arc::new(tokio::sync::Mutex::new({ + let mut map = HashMap::new(); + map.insert( + "web".to_string(), + web_adapter.clone() as Arc, + ); + map + })), + response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + web_adapter: web_adapter.clone(), + voice_adapter: voice_adapter.clone(), + }); + state_tx.send(app_state.clone()).await.ok(); + progress_tx.send(BootstrapProgress::BootstrapComplete).ok(); + info!( + "Starting HTTP server on {}:{}", + config.server.host, config.server.port + ); + let worker_count = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4); + let bot_orchestrator = BotOrchestrator::new(app_state.clone()); + tokio::spawn(async move { + if let Err(e) = bot_orchestrator.mount_all_bots().await { + error!("Failed to mount bots: {}", e); + } + }); + let automation_state = app_state.clone(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create runtime for automation"); + let local = tokio::task::LocalSet::new(); + local.block_on(&rt, async move { + let automation = AutomationService::new(automation_state); + automation.spawn().await.ok(); + }); + }); + let app_state_for_llm = app_state.clone(); + tokio::spawn(async move { + if let Err(e) = ensure_llama_servers_running(app_state_for_llm).await { + error!("Failed to start LLM servers: {}", e); + } + }); + let server_result = HttpServer::new(move || { + let cors = Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .max_age(3600); + let app_state_clone = app_state.clone(); + let mut app = App::new() + .wrap(cors) + .wrap(Logger::default()) + .wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i")) + .app_data(web::Data::from(app_state_clone)) + .service(auth_handler) + .service(create_session) + .service(get_session_history) + .service(get_sessions) + .service(index) + .service(start_session) + .service(upload_file) + .service(voice_start) + .service(voice_stop) + .service(websocket_handler) + .service(crate::bot::create_bot_handler) + .service(crate::bot::mount_bot_handler) + .service(crate::bot::handle_user_input_handler) + .service(crate::bot::get_user_sessions_handler) + .service(crate::bot::get_conversation_history_handler) + .service(crate::bot::send_warning_handler); + #[cfg(feature = "email")] + { + app = app + .service(get_latest_email_from) + .service(get_emails) + .service(list_emails) + .service(send_email) + .service(save_draft) + .service(save_click); + } + app = app.service(static_files); + app = app.service(bot_index); + app + }) + .workers(worker_count) + .bind((config.server.host.clone(), config.server.port))? + .run() + .await; + if let Some(handle) = ui_handle { + handle.join().ok(); } - progress_tx_clone.send(BootstrapProgress::UploadingTemplates).ok(); - if let Err(e) = bootstrap.upload_templates_to_drive(&cfg).await { - progress_tx_clone.send(BootstrapProgress::BootstrapError(format!("Failed to upload templates: {}", e))).ok(); - } - Ok::(cfg) -}; -let cfg = cfg?; -dotenv().ok(); -let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env"); -let config = std::sync::Arc::new(refreshed_cfg.clone()); -progress_tx.send(BootstrapProgress::ConnectingDatabase).ok(); -let db_pool = match diesel::Connection::establish(&refreshed_cfg.database_url()) { -Ok(conn) => Arc::new(Mutex::new(conn)), -Err(e) => { -error!("Failed to connect to main database: {}", e); -progress_tx.send(BootstrapProgress::BootstrapError(format!("Database connection failed: {}", e))).ok(); -return Err(std::io::Error::new( -std::io::ErrorKind::ConnectionRefused, -format!("Database connection failed: {}", e), -)); -} -}; -let cache_url = std::env::var("CACHE_URL") -.or_else(|_| std::env::var("REDIS_URL")) -.unwrap_or_else(|_| "redis://localhost:6379".to_string()); -let redis_client = match redis::Client::open(cache_url.as_str()) { -Ok(client) => Some(Arc::new(client)), -Err(e) => { -log::warn!("Failed to connect to Redis: {}", e); -None -} -}; -let web_adapter = Arc::new(WebChannelAdapter::new()); -let voice_adapter = Arc::new(VoiceAdapter::new()); -let drive = init_drive(&config.drive) -.await -.expect("Failed to initialize Drive"); -let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new( -diesel::Connection::establish(&cfg.database_url()).unwrap(), -redis_client.clone(), -))); -let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new())); -let conn = diesel::Connection::establish(&cfg.database_url()).unwrap(); -let config_manager = ConfigManager::new(Arc::new(Mutex::new(conn))); -let mut bot_conn = diesel::Connection::establish(&cfg.database_url()).unwrap(); -let (default_bot_id, _default_bot_name) = crate::bot::get_default_bot(&mut bot_conn); -let llm_url = config_manager -.get_config(&default_bot_id, "llm-url", Some("http://localhost:8081")) -.unwrap_or_else(|_| "http://localhost:8081".to_string()); -let llm_provider = Arc::new(crate::llm::OpenAIClient::new( -"empty".to_string(), -Some(llm_url.clone()), -)); -let app_state = Arc::new(AppState { -drive: Some(drive), -config: Some(cfg.clone()), -conn: db_pool.clone(), -bucket_name: "default.gbai".to_string(), -cache: redis_client.clone(), -session_manager: session_manager.clone(), -llm_provider: llm_provider.clone(), -auth_service: auth_service.clone(), -channels: Arc::new(Mutex::new({ -let mut map = HashMap::new(); -map.insert( -"web".to_string(), -web_adapter.clone() as Arc, -); -map -})), -response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())), -web_adapter: web_adapter.clone(), -voice_adapter: voice_adapter.clone(), -}); -state_tx.send(app_state.clone()).await.ok(); -progress_tx.send(BootstrapProgress::BootstrapComplete).ok(); -info!("Starting HTTP server on {}:{}", config.server.host, config.server.port); -let worker_count = std::thread::available_parallelism() -.map(|n| n.get()) -.unwrap_or(4); -let bot_orchestrator = BotOrchestrator::new(app_state.clone()); -tokio::spawn(async move { -if let Err(e) = bot_orchestrator.mount_all_bots().await { -error!("Failed to mount bots: {}", e); -} -}); -let automation_state = app_state.clone(); -std::thread::spawn(move || { -let rt = tokio::runtime::Builder::new_current_thread() -.enable_all() -.build() -.expect("Failed to create runtime for automation"); -let local = tokio::task::LocalSet::new(); -local.block_on(&rt, async move { -let automation = AutomationService::new(automation_state); -automation.spawn().await.ok(); -}); -}); -let app_state_for_llm = app_state.clone(); -tokio::spawn(async move { -if let Err(e) = ensure_llama_servers_running(&app_state_for_llm).await { -error!("Failed to start LLM servers: {}", e); -} -}); -let server_result = HttpServer::new(move || { -let cors = Cors::default() -.allow_any_origin() -.allow_any_method() -.allow_any_header() -.max_age(3600); -let app_state_clone = app_state.clone(); -let mut app = App::new() -.wrap(cors) -.wrap(Logger::default()) -.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i")) -.app_data(web::Data::from(app_state_clone)) -.service(auth_handler) -.service(create_session) -.service(get_session_history) -.service(get_sessions) -.service(index) -.service(start_session) -.service(upload_file) -.service(voice_start) -.service(voice_stop) -.service(websocket_handler) -.service(crate::bot::create_bot_handler) -.service(crate::bot::mount_bot_handler) -.service(crate::bot::handle_user_input_handler) -.service(crate::bot::get_user_sessions_handler) -.service(crate::bot::get_conversation_history_handler) -.service(crate::bot::send_warning_handler); -#[cfg(feature = "email")] -{ -app = app -.service(get_latest_email_from) -.service(get_emails) -.service(list_emails) -.service(send_email) -.service(save_draft) -.service(save_click); -} -app = app.service(static_files); -app = app.service(bot_index); -app -}) -.workers(worker_count) -.bind((config.server.host.clone(), config.server.port))? -.run() -.await; -if let Some(handle) = ui_handle { -handle.join().ok(); -} -server_result + server_result } diff --git a/src/package_manager/installer.rs b/src/package_manager/installer.rs index f1ba35a75..57926f1e6 100644 --- a/src/package_manager/installer.rs +++ b/src/package_manager/installer.rs @@ -4,7 +4,6 @@ use crate::package_manager::{InstallMode, OsType}; use anyhow::Result; use log::trace; use rand::distr::Alphanumeric; -use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::path::PathBuf; @@ -61,18 +60,20 @@ impl PackageManager { } fn register_drive(&mut self) { + let drive_password = self.generate_secure_password(16); let drive_user = "gbdriveuser".to_string(); - let farm_password = - std::env::var("FARM_PASSWORD").unwrap_or_else(|_| self.generate_secure_password(32)); - let encrypted_drive_password = self.encrypt_password(&drive_password, &farm_password); - let env_path = self.base_path.join(".env"); + let env_path = std::env::current_dir().unwrap().join(".env"); let env_content = format!( - "DRIVE_USER={}\nDRIVE_PASSWORD={}\nFARM_PASSWORD={}\nDRIVE_ROOT_USER={}\nDRIVE_ROOT_PASSWORD={}\n", - drive_user, drive_password, farm_password, drive_user, drive_password + "\nDRIVE_ACCESSKEY={}\nDRIVE_SECRET={}\nDRIVE_SERVER=http://localhost:9000\n", + drive_user, drive_password ); - let _ = std::fs::write(&env_path, env_content); + let _ = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&env_path) + .and_then(|mut file| std::io::Write::write_all(&mut file, env_content.as_bytes())); self.components.insert( "drive".to_string(), @@ -103,72 +104,20 @@ impl PackageManager { }, ); - // Delay updating drive credentials until database is created - let db_env_path = self.base_path.join(".env"); - let database_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); - let db_line = format!("DATABASE_URL={}\n", database_url); - let _ = std::fs::write(&db_env_path, db_line); - // Append drive credentials after database creation - let env_path = self.base_path.join(".env"); - let drive_lines = format!( - "DRIVE_USER={}\nDRIVE_PASSWORD={}\nFARM_PASSWORD={}\nDRIVE_ROOT_USER={}\nDRIVE_ROOT_PASSWORD={}\n", - drive_user, drive_password, farm_password, drive_user, drive_password - ); - let _ = std::fs::OpenOptions::new() - .append(true) - .open(&env_path) - .and_then(|mut file| std::io::Write::write_all(&mut file, drive_lines.as_bytes())); - - // Update drive credentials in database only after database is ready - if std::process::Command::new("pg_isready") - .arg("-h") - .arg("localhost") - .arg("-p") - .arg("5432") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - { - self.update_drive_credentials_in_database(&encrypted_drive_password) - .ok(); - } } - fn update_drive_credentials_in_database(&self, encrypted_drive_password: &str) -> Result<()> { - use crate::shared::models::schema::bots::dsl::*; - use diesel::pg::PgConnection; - use diesel::prelude::*; - use uuid::Uuid; - - let database_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); - - if let Ok(mut conn) = PgConnection::establish(&database_url) { - let system_bot_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000")?; - diesel::update(bots) - .filter(id.eq(system_bot_id)) - .set(llm_config.eq(serde_json::json!({ - "encrypted_drive_password": encrypted_drive_password, - }))) - .execute(&mut conn)?; - trace!("Updated drive credentials in database for system bot"); - } - Ok(()) - } fn register_tables(&mut self) { - let db_password = std::env::var("DATABASE_URL") - .ok() - .and_then(|url| { - if let Some(stripped) = url.strip_prefix("postgres://gbuser:") { - stripped.split('@').next().map(|s| s.to_string()) - } else { - None - } - }) - .unwrap_or_else(|| self.generate_secure_password(16)); + + let db_env_path = std::env::current_dir().unwrap().join(".env"); + let db_password = self.generate_secure_password(32); + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| format!("postgres://gbuser:{}@localhost:5432/botserver", db_password)); + let db_line = format!("DATABASE_URL={}\n", database_url); + + + let _ = std::fs::write(&db_env_path, db_line); self.components.insert( "tables".to_string(), @@ -848,10 +797,4 @@ impl PackageManager { .collect() } - fn encrypt_password(&self, password: &str, key: &str) -> String { - let mut hasher = Sha256::new(); - hasher.update(key.as_bytes()); - hasher.update(password.as_bytes()); - format!("{:x}", hasher.finalize()) - } } diff --git a/src/session/mod.rs b/src/session/mod.rs index ce888a6f5..00b380f97 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -4,31 +4,30 @@ use crate::shared::state::AppState; use actix_web::{web, HttpResponse, Result}; use chrono::Utc; use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, PooledConnection}; use diesel::PgConnection; -use log::{debug, error, info, warn}; +use log::trace; +use log::{error, warn}; use redis::Client; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::error::Error; use std::sync::Arc; use uuid::Uuid; - #[derive(Clone, Serialize, Deserialize)] pub struct SessionData { pub id: Uuid, pub user_id: Option, pub data: String, } - pub struct SessionManager { - conn: PgConnection, + conn: PooledConnection>, sessions: HashMap, waiting_for_input: HashSet, redis: Option>, } - impl SessionManager { - pub fn new(conn: PgConnection, redis_client: Option>) -> Self { + pub fn new(conn: PooledConnection>, redis_client: Option>) -> Self { SessionManager { conn, sessions: HashMap::new(), @@ -36,13 +35,12 @@ impl SessionManager { redis: redis_client, } } - pub fn provide_input( &mut self, session_id: Uuid, input: String, ) -> Result, Box> { - info!( + trace!( "SessionManager.provide_input called for session {}", session_id ); @@ -61,11 +59,9 @@ impl SessionManager { Ok(Some("user_input".to_string())) } } - pub fn mark_waiting(&mut self, session_id: Uuid) { self.waiting_for_input.insert(session_id); } - pub fn get_session_by_id( &mut self, session_id: Uuid, @@ -77,7 +73,6 @@ impl SessionManager { .optional()?; Ok(result) } - pub fn get_user_session( &mut self, uid: Uuid, @@ -92,7 +87,6 @@ impl SessionManager { .optional()?; Ok(result) } - pub fn get_or_create_user_session( &mut self, uid: Uuid, @@ -104,24 +98,19 @@ impl SessionManager { } self.create_session(uid, bid, session_title).map(Some) } - pub fn get_or_create_anonymous_user( &mut self, uid: Option, ) -> Result> { use crate::shared::models::users::dsl as users_dsl; - let user_id = uid.unwrap_or_else(Uuid::new_v4); - let user_exists: Option = users_dsl::users .filter(users_dsl::id.eq(user_id)) .select(users_dsl::id) .first(&mut self.conn) .optional()?; - if user_exists.is_none() { let now = Utc::now(); - info!("Creating anonymous user with ID {}", user_id); diesel::insert_into(users_dsl::users) .values(( users_dsl::id.eq(user_id), @@ -137,10 +126,8 @@ impl SessionManager { )) .execute(&mut self.conn)?; } - Ok(user_id) } - pub fn create_session( &mut self, uid: Uuid, @@ -148,11 +135,8 @@ impl SessionManager { session_title: &str, ) -> Result> { use crate::shared::models::user_sessions::dsl::*; - - // Ensure user exists (create anonymous if needed) let verified_uid = self.get_or_create_anonymous_user(Some(uid))?; let now = Utc::now(); - let inserted: UserSession = diesel::insert_into(user_sessions) .values(( id.eq(Uuid::new_v4()), @@ -170,18 +154,14 @@ impl SessionManager { error!("Failed to create session in database: {}", e); e })?; - Ok(inserted) } - fn _clear_messages(&mut self, _session_id: Uuid) -> Result<(), Box> { use crate::shared::models::message_history::dsl::*; - diesel::delete(message_history.filter(session_id.eq(session_id))) .execute(&mut self.conn)?; Ok(()) } - pub fn save_message( &mut self, sess_id: Uuid, @@ -191,8 +171,6 @@ impl SessionManager { msg_type: i32, ) -> Result<(), Box> { use crate::shared::models::message_history::dsl::*; - - // Check if this exact message already exists let exists = message_history .filter(session_id.eq(sess_id)) .filter(user_id.eq(uid)) @@ -202,18 +180,14 @@ impl SessionManager { .select(id) .first::(&mut self.conn) .optional()?; - if exists.is_some() { - debug!("Duplicate message detected, skipping save"); return Ok(()); } - let next_index = message_history .filter(session_id.eq(sess_id)) .count() .get_result::(&mut self.conn) .unwrap_or(0); - diesel::insert_into(message_history) .values(( id.eq(Uuid::new_v4()), @@ -226,14 +200,13 @@ impl SessionManager { created_at.eq(chrono::Utc::now()), )) .execute(&mut self.conn)?; - - debug!( + trace!( "Message saved for session {} with index {}", - sess_id, next_index + sess_id, + next_index ); Ok(()) } - pub async fn update_session_context( &mut self, session_id: &Uuid, @@ -241,25 +214,21 @@ impl SessionManager { context_data: String, ) -> Result<(), Box> { use redis::Commands; - let redis_key = format!("context:{}:{}", user_id, session_id); if let Some(redis_client) = &self.redis { let mut conn = redis_client.get_connection()?; conn.set::<_, _, ()>(&redis_key, &context_data)?; - info!("Updated context in Redis for key {}", redis_key); } else { warn!("No Redis client configured, context not persisted"); } Ok(()) } - pub async fn get_session_context_data( &self, session_id: &Uuid, user_id: &Uuid, ) -> Result> { use redis::Commands; - let base_key = format!("context:{}:{}", user_id, session_id); if let Some(redis_client) = &self.redis { let conn_option = redis_client @@ -269,17 +238,14 @@ impl SessionManager { e }) .ok(); - if let Some(mut connection) = conn_option { - // First cache trip: get context name match connection.get::<_, Option>(&base_key) { Ok(Some(context_name)) => { - debug!("Found context name '{}' for key {}", context_name, base_key); - // Second cache trip: get actual context value - let full_key = format!("context:{}:{}:{}", user_id, session_id, context_name); + let full_key = + format!("context:{}:{}:{}", user_id, session_id, context_name); match connection.get::<_, Option>(&full_key) { Ok(Some(context_value)) => { - debug!( + trace!( "Retrieved context value from Cache for key {}: {} chars", full_key, context_value.len() @@ -287,7 +253,7 @@ impl SessionManager { return Ok(context_value); } Ok(None) => { - debug!("No context value found for key {}", full_key); + trace!("No context value found for key: {}", full_key); } Err(e) => { warn!("Failed to retrieve context value from Cache: {}", e); @@ -295,7 +261,7 @@ impl SessionManager { } } Ok(None) => { - debug!("No context name found for key {}", base_key); + trace!("No context name found for key: {}", base_key); } Err(e) => { warn!("Failed to retrieve context name from Cache: {}", e); @@ -303,25 +269,19 @@ impl SessionManager { } } } - Ok(String::new()) } - - - pub fn get_conversation_history( &mut self, sess_id: Uuid, _uid: Uuid, ) -> Result, Box> { use crate::shared::models::message_history::dsl::*; - let messages = message_history .filter(session_id.eq(sess_id)) .order(message_index.asc()) .select((role, content_encrypted)) .load::<(i32, String)>(&mut self.conn)?; - let mut history: Vec<(String, String)> = Vec::new(); for (other_role, content) in messages { let role_str = match other_role { @@ -334,13 +294,11 @@ impl SessionManager { } Ok(history) } - pub fn get_user_sessions( &mut self, uid: Uuid, ) -> Result, Box> { use crate::shared::models::user_sessions::dsl::*; - let sessions = if uid == Uuid::nil() { user_sessions .order(created_at.desc()) @@ -351,42 +309,33 @@ impl SessionManager { .order(created_at.desc()) .load::(&mut self.conn)? }; - Ok(sessions) } - - pub fn update_user_id( &mut self, session_id: Uuid, new_user_id: Uuid, ) -> Result<(), Box> { use crate::shared::models::user_sessions::dsl::*; - let updated_count = diesel::update(user_sessions.filter(id.eq(session_id))) .set((user_id.eq(new_user_id), updated_at.eq(chrono::Utc::now()))) .execute(&mut self.conn)?; - if updated_count == 0 { warn!("No session found with ID: {}", session_id); } else { - debug!("Updated user ID for session {}", session_id); + trace!("Updated user ID for session: {}", session_id); } Ok(()) } } - #[actix_web::post("/api/sessions")] async fn create_session(data: web::Data) -> Result { let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); let bot_id = Uuid::nil(); - - // Acquire lock briefly, then release before performing blocking DB operations let session_result = { let mut sm = data.session_manager.lock().await; sm.get_or_create_user_session(user_id, bot_id, "New Conversation") }; - let session = match session_result { Ok(Some(s)) => s, Ok(None) => { @@ -400,14 +349,12 @@ async fn create_session(data: web::Data) -> Result { .json(serde_json::json!({"error": e.to_string()}))); } }; - Ok(HttpResponse::Ok().json(serde_json::json!({ - "session_id": session.id, - "title": "New Conversation", - "created_at": Utc::now() + "session_id": session.id, + "title": "New Conversation", + "created_at": Utc::now() }))) } - #[actix_web::get("/api/sessions")] async fn get_sessions(data: web::Data) -> Result { let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); @@ -421,14 +368,9 @@ async fn get_sessions(data: web::Data) -> Result { } } } - #[actix_web::post("/api/sessions/{session_id}/start")] -async fn start_session( - data: web::Data, - path: web::Path, -) -> Result { +async fn start_session(data: web::Data, path: web::Path) -> Result { let session_id = path.into_inner(); - match Uuid::parse_str(&session_id) { Ok(session_uuid) => { let mut session_manager = data.session_manager.lock().await; @@ -436,15 +378,13 @@ async fn start_session( Ok(Some(_session)) => { session_manager.mark_waiting(session_uuid); Ok(HttpResponse::Ok().json(serde_json::json!({ - "status": "started", - "session_id": session_id - }))) - } - Ok(None) => { - Ok(HttpResponse::NotFound().json(serde_json::json!({ - "error": "Session not found" + "status": "started", + "session_id": session_id }))) } + Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({ + "error": "Session not found" + }))), Err(e) => { error!("Failed to start session {}: {}", session_id, e); Ok(HttpResponse::InternalServerError() @@ -458,7 +398,6 @@ async fn start_session( } } } - #[actix_web::get("/api/sessions/{session_id}")] async fn get_session_history( data: web::Data, @@ -466,7 +405,6 @@ async fn get_session_history( ) -> Result { let session_id = path.into_inner(); let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - match Uuid::parse_str(&session_id) { Ok(session_uuid) => { let orchestrator = BotOrchestrator::new(Arc::new(data.get_ref().clone())); @@ -475,7 +413,7 @@ async fn get_session_history( .await { Ok(history) => { - info!( + trace!( "Retrieved {} history entries for session {}", history.len(), session_id diff --git a/src/shared/state.rs b/src/shared/state.rs index 4f16187cb..13182dd59 100644 --- a/src/shared/state.rs +++ b/src/shared/state.rs @@ -2,46 +2,43 @@ use crate::channels::{ChannelAdapter, VoiceAdapter, WebChannelAdapter}; use crate::config::AppConfig; use crate::llm::LLMProvider; use crate::session::SessionManager; -use diesel::{ PgConnection}; use aws_sdk_s3::Client as S3Client; use redis::Client as RedisClient; use std::collections::HashMap; use std::sync::Arc; -use std::sync::Mutex; use tokio::sync::mpsc; use crate::shared::models::BotResponse; use crate::auth::AuthService; +use crate::shared::utils::DbPool; pub struct AppState { - pub drive: Option, - pub cache: Option>, - pub bucket_name: String, - pub config: Option, - pub conn: Arc>, - pub session_manager: Arc>, - pub llm_provider: Arc, - pub auth_service: Arc>, - pub channels: Arc>>>, - pub response_channels: Arc>>>, - pub web_adapter: Arc, - pub voice_adapter: Arc, + pub drive: Option, + pub cache: Option>, + pub bucket_name: String, + pub config: Option, + pub conn: DbPool, + pub session_manager: Arc>, + pub llm_provider: Arc, + pub auth_service: Arc>, + pub channels: Arc>>>, + pub response_channels: Arc>>>, + pub web_adapter: Arc, + pub voice_adapter: Arc, } - impl Clone for AppState { - fn clone(&self) -> Self { - Self { - drive: self.drive.clone(), - bucket_name: self.bucket_name.clone(), - config: self.config.clone(), - conn: Arc::clone(&self.conn), - - cache: self.cache.clone(), - session_manager: Arc::clone(&self.session_manager), - llm_provider: Arc::clone(&self.llm_provider), - auth_service: Arc::clone(&self.auth_service), - channels: Arc::clone(&self.channels), - response_channels: Arc::clone(&self.response_channels), - web_adapter: Arc::clone(&self.web_adapter), - voice_adapter: Arc::clone(&self.voice_adapter), - } - } + fn clone(&self) -> Self { + Self { + drive: self.drive.clone(), + bucket_name: self.bucket_name.clone(), + config: self.config.clone(), + conn: self.conn.clone(), + cache: self.cache.clone(), + session_manager: Arc::clone(&self.session_manager), + llm_provider: Arc::clone(&self.llm_provider), + auth_service: Arc::clone(&self.auth_service), + channels: Arc::clone(&self.channels), + response_channels: Arc::clone(&self.response_channels), + web_adapter: Arc::clone(&self.web_adapter), + voice_adapter: Arc::clone(&self.voice_adapter), + } + } } diff --git a/src/shared/utils.rs b/src/shared/utils.rs index d3fba273d..72edf3727 100644 --- a/src/shared/utils.rs +++ b/src/shared/utils.rs @@ -1,8 +1,11 @@ use anyhow::{Context, Result}; -use diesel::{Connection, PgConnection}; +use diesel::Connection; +use diesel::{ + r2d2::{ConnectionManager, Pool}, + PgConnection, +}; use futures_util::StreamExt; use indicatif::{ProgressBar, ProgressStyle}; -use log::trace; use reqwest::Client; use rhai::{Array, Dynamic}; use serde_json::Value; @@ -10,7 +13,6 @@ use smartstring::SmartString; use std::error::Error; use tokio::fs::File as TokioFile; use tokio::io::AsyncWriteExt; - pub fn json_value_to_dynamic(value: &Value) -> Dynamic { match value { Value::Null => Dynamic::UNIT, @@ -37,7 +39,6 @@ pub fn json_value_to_dynamic(value: &Value) -> Dynamic { ), } } - pub fn to_array(value: Dynamic) -> Array { if value.is_array() { value.cast::() @@ -47,80 +48,67 @@ pub fn to_array(value: Dynamic) -> Array { Array::from([value]) } } - -pub async fn download_file( - url: &str, - output_path: &str, -) -> Result<(), anyhow::Error> { +pub async fn download_file(url: &str, output_path: &str) -> Result<(), anyhow::Error> { let url = url.to_string(); let output_path = output_path.to_string(); - let download_handle = tokio::spawn(async move { let client = Client::builder() - .user_agent("Mozilla/5.0 (compatible; BotServer/1.0)") - .build()?; + .user_agent("Mozilla/5.0 (compatible; BotServer/1.0)") + .build()?; let response = client.get(&url).send().await?; - if response.status().is_success() { let total_size = response.content_length().unwrap_or(0); let pb = ProgressBar::new(total_size); pb.set_style(ProgressStyle::default_bar() - .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})") - .unwrap() - .progress_chars("#>-")); + .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .unwrap() + .progress_chars("#>-")); pb.set_message(format!("Downloading {}", url)); - let mut file = TokioFile::create(&output_path).await?; let mut downloaded: u64 = 0; let mut stream = response.bytes_stream(); - while let Some(chunk_result) = stream.next().await { let chunk = chunk_result?; file.write_all(&chunk).await?; downloaded += chunk.len() as u64; pb.set_position(downloaded); } - pb.finish_with_message(format!("Downloaded {}", output_path)); - trace!("Download completed: {} -> {}", url, output_path); Ok(()) } else { Err(anyhow::anyhow!("HTTP {}: {}", response.status(), url)) } }); - download_handle.await? } - pub fn parse_filter(filter_str: &str) -> Result<(String, Vec), Box> { let parts: Vec<&str> = filter_str.split('=').collect(); if parts.len() != 2 { return Err("Invalid filter format. Expected 'KEY=VALUE'".into()); } - let column = parts[0].trim(); let value = parts[1].trim(); - if !column .chars() .all(|c| c.is_ascii_alphanumeric() || c == '_') { return Err("Invalid column name in filter".into()); } - Ok((format!("{} = $1", column), vec![value.to_string()])) } - pub fn estimate_token_count(text: &str) -> usize { let char_count = text.chars().count(); - (char_count / 4).max(1) // Ensure at least 1 token + (char_count / 4).max(1) } - pub fn establish_pg_connection() -> Result { - let database_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); - + let database_url = std::env::var("DATABASE_URL").unwrap(); PgConnection::establish(&database_url) .with_context(|| format!("Failed to connect to database at {}", database_url)) } - \ No newline at end of file +pub type DbPool = Pool>; +pub fn create_conn() -> Result { + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); + let manager = ConnectionManager::::new(database_url); + Pool::builder().build(manager) +} diff --git a/src/ui_tree/chat_panel.rs b/src/ui_tree/chat_panel.rs index c5a7fe3a1..e54e929da 100644 --- a/src/ui_tree/chat_panel.rs +++ b/src/ui_tree/chat_panel.rs @@ -6,8 +6,6 @@ use tokio::sync::mpsc; use uuid::Uuid; pub struct ChatPanel { - pub id: Uuid, - pub app_state: Arc, pub messages: Vec, pub input_buffer: String, pub session_id: Uuid, @@ -16,10 +14,8 @@ pub struct ChatPanel { } impl ChatPanel { - pub fn new(app_state: Arc) -> Self { + pub fn new(_app_state: Arc) -> Self { Self { - id: Uuid::new_v4(), - app_state, messages: vec!["Welcome to General Bots Console Chat!".to_string()], input_buffer: String::new(), session_id: Uuid::new_v4(), @@ -95,7 +91,7 @@ let _ = orchestrator.stream_response(user_message, tx).await; use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut conn = app_state.conn.lock().unwrap(); + let mut conn = app_state.conn.get().unwrap(); let bot_id = bots .filter(name.eq(bot_name)) .select(id) diff --git a/src/ui_tree/status_panel.rs b/src/ui_tree/status_panel.rs index a1e974c2a..a71c6cbcc 100644 --- a/src/ui_tree/status_panel.rs +++ b/src/ui_tree/status_panel.rs @@ -1,177 +1,194 @@ -use std::sync::Arc; -use crate::shared::state::AppState; -use crate::shared::models::schema::bots::dsl::*; -use crate::nvidia; use crate::config::ConfigManager; +use crate::nvidia; +use crate::shared::models::schema::bots::dsl::*; +use crate::shared::state::AppState; use diesel::prelude::*; +use std::sync::Arc; use sysinfo::System; pub struct StatusPanel { - app_state: Arc, - last_update: std::time::Instant, - cached_content: String, - system: System, + app_state: Arc, + last_update: std::time::Instant, + cached_content: String, + system: System, } impl StatusPanel { - pub fn new(app_state: Arc) -> Self { - Self { - app_state, - last_update: std::time::Instant::now(), - cached_content: String::new(), - system: System::new_all(), - } - } + pub fn new(app_state: Arc) -> Self { + Self { + app_state, + last_update: std::time::Instant::now(), + cached_content: String::new(), + system: System::new_all(), + } + } - pub async fn update(&mut self) -> Result<(), std::io::Error> { - if self.last_update.elapsed() < std::time::Duration::from_secs(1) { - return Ok(()); - } + pub async fn update(&mut self) -> Result<(), std::io::Error> { + if self.last_update.elapsed() < std::time::Duration::from_secs(1) { + return Ok(()); + } - self.system.refresh_all(); + self.system.refresh_all(); - self.cached_content = String::new(); - self.last_update = std::time::Instant::now(); - Ok(()) - } + self.cached_content = String::new(); + self.last_update = std::time::Instant::now(); + Ok(()) + } - pub fn render(&mut self, selected_bot: Option) -> String { - let mut lines = Vec::new(); - - self.system.refresh_all(); + pub fn render(&mut self, selected_bot: Option) -> String { + let mut lines = Vec::new(); - lines.push("╔═══════════════════════════════════════╗".to_string()); - lines.push("║ SYSTEM METRICS ║".to_string()); - lines.push("╚═══════════════════════════════════════╝".to_string()); - lines.push("".to_string()); + self.system.refresh_all(); - let system_metrics = match nvidia::get_system_metrics(0, 0) { - Ok(metrics) => metrics, - Err(_) => nvidia::SystemMetrics::default(), - }; + lines.push("╔═══════════════════════════════════════╗".to_string()); + lines.push("║ SYSTEM METRICS ║".to_string()); + lines.push("╚═══════════════════════════════════════╝".to_string()); + lines.push("".to_string()); - let cpu_bar = Self::create_progress_bar(system_metrics.cpu_usage, 20); - lines.push(format!(" CPU: {:5.1}% {}", system_metrics.cpu_usage, cpu_bar)); + let system_metrics = match nvidia::get_system_metrics(0, 0) { + Ok(metrics) => metrics, + Err(_) => nvidia::SystemMetrics::default(), + }; - if let Some(gpu_usage) = system_metrics.gpu_usage { - let gpu_bar = Self::create_progress_bar(gpu_usage, 20); - lines.push(format!(" GPU: {:5.1}% {}", gpu_usage, gpu_bar)); - } else { - lines.push(" GPU: Not available".to_string()); - } + let cpu_bar = Self::create_progress_bar(system_metrics.cpu_usage, 20); + lines.push(format!( + " CPU: {:5.1}% {}", + system_metrics.cpu_usage, cpu_bar + )); - let total_mem = self.system.total_memory() as f32 / 1024.0 / 1024.0 / 1024.0; - let used_mem = self.system.used_memory() as f32 / 1024.0 / 1024.0 / 1024.0; - let mem_percentage = (used_mem / total_mem) * 100.0; - let mem_bar = Self::create_progress_bar(mem_percentage, 20); - lines.push(format!(" MEM: {:5.1}% {} ({:.1}/{:.1} GB)", mem_percentage, mem_bar, used_mem, total_mem)); + if let Some(gpu_usage) = system_metrics.gpu_usage { + let gpu_bar = Self::create_progress_bar(gpu_usage, 20); + lines.push(format!(" GPU: {:5.1}% {}", gpu_usage, gpu_bar)); + } else { + lines.push(" GPU: Not available".to_string()); + } - lines.push("".to_string()); - lines.push("╔═══════════════════════════════════════╗".to_string()); - lines.push("║ COMPONENTS STATUS ║".to_string()); - lines.push("╚═══════════════════════════════════════╝".to_string()); - lines.push("".to_string()); + let total_mem = self.system.total_memory() as f32 / 1024.0 / 1024.0 / 1024.0; + let used_mem = self.system.used_memory() as f32 / 1024.0 / 1024.0 / 1024.0; + let mem_percentage = (used_mem / total_mem) * 100.0; + let mem_bar = Self::create_progress_bar(mem_percentage, 20); + lines.push(format!( + " MEM: {:5.1}% {} ({:.1}/{:.1} GB)", + mem_percentage, mem_bar, used_mem, total_mem + )); - let components = vec![ - ("Tables", "postgres", "5432"), - ("Cache", "valkey-server", "6379"), - ("Drive", "minio", "9000"), - ("LLM", "llama-server", "8081"), - ]; + lines.push("".to_string()); + lines.push("╔═══════════════════════════════════════╗".to_string()); + lines.push("║ COMPONENTS STATUS ║".to_string()); + lines.push("╚═══════════════════════════════════════╝".to_string()); + lines.push("".to_string()); - for (comp_name, process, port) in components { - let status = if Self::check_component_running(process) { - format!("🟢 ONLINE [Port: {}]", port) - } else { - "🔴 OFFLINE".to_string() - }; - lines.push(format!(" {:<10} {}", comp_name, status)); - } + let components = vec![ + ("Tables", "postgres", "5432"), + ("Cache", "valkey-server", "6379"), + ("Drive", "minio", "9000"), + ("LLM", "llama-server", "8081"), + ]; - lines.push("".to_string()); - lines.push("╔═══════════════════════════════════════╗".to_string()); - lines.push("║ ACTIVE BOTS ║".to_string()); - lines.push("╚═══════════════════════════════════════╝".to_string()); - lines.push("".to_string()); + for (comp_name, process, port) in components { + let status = if Self::check_component_running(process) { + format!("🟢 ONLINE [Port: {}]", port) + } else { + "🔴 OFFLINE".to_string() + }; + lines.push(format!(" {:<10} {}", comp_name, status)); + } - if let Ok(mut conn) = self.app_state.conn.try_lock() { - match bots - .filter(is_active.eq(true)) - .select((name, id)) - .load::<(String, uuid::Uuid)>(&mut *conn) - { - Ok(bot_list) => { - if bot_list.is_empty() { - lines.push(" No active bots".to_string()); - } else { - for (bot_name, bot_id) in bot_list { - let marker = if let Some(ref selected) = selected_bot { - if selected == &bot_name { "►" } else { " " } - } else { - " " - }; - lines.push(format!(" {} 🤖 {}", marker, bot_name)); + lines.push("".to_string()); + lines.push("╔═══════════════════════════════════════╗".to_string()); + lines.push("║ ACTIVE BOTS ║".to_string()); + lines.push("╚═══════════════════════════════════════╝".to_string()); + lines.push("".to_string()); - if let Some(ref selected) = selected_bot { - if selected == &bot_name { - lines.push("".to_string()); - lines.push(" ┌─ Bot Configuration ─────────┐".to_string()); - - let config_manager = ConfigManager::new(self.app_state.conn.clone()); - - let llm_model = config_manager.get_config(&bot_id, "llm-model", None) - .unwrap_or_else(|_| "N/A".to_string()); - lines.push(format!(" Model: {}", llm_model)); - - let ctx_size = config_manager.get_config(&bot_id, "llm-server-ctx-size", None) - .unwrap_or_else(|_| "N/A".to_string()); - lines.push(format!(" Context: {}", ctx_size)); - - let temp = config_manager.get_config(&bot_id, "llm-temperature", None) - .unwrap_or_else(|_| "N/A".to_string()); - lines.push(format!(" Temp: {}", temp)); - - lines.push(" └─────────────────────────────┘".to_string()); - } - } - } - } - } - Err(_) => { - lines.push(" Error loading bots".to_string()); - } - } - } else { - lines.push(" Database locked".to_string()); - } + if let Ok(mut conn) = self.app_state.conn.get() { + match bots + .filter(is_active.eq(true)) + .select((name, id)) + .load::<(String, uuid::Uuid)>(&mut *conn) + { + Ok(bot_list) => { + if bot_list.is_empty() { + lines.push(" No active bots".to_string()); + } else { + for (bot_name, bot_id) in bot_list { + let marker = if let Some(ref selected) = selected_bot { + if selected == &bot_name { + "►" + } else { + " " + } + } else { + " " + }; + lines.push(format!(" {} 🤖 {}", marker, bot_name)); - lines.push("".to_string()); - lines.push("╔═══════════════════════════════════════╗".to_string()); - lines.push("║ SESSIONS ║".to_string()); - lines.push("╚═══════════════════════════════════════╝".to_string()); + if let Some(ref selected) = selected_bot { + if selected == &bot_name { + lines.push("".to_string()); + lines.push(" ┌─ Bot Configuration ─────────┐".to_string()); - let session_count = self.app_state.response_channels.try_lock() - .map(|channels| channels.len()) - .unwrap_or(0); - lines.push(format!(" Active Sessions: {}", session_count)); + let config_manager = + ConfigManager::new(self.app_state.conn.clone()); - lines.join("\n") - } + let llm_model = config_manager + .get_config(&bot_id, "llm-model", None) + .unwrap_or_else(|_| "N/A".to_string()); + lines.push(format!(" Model: {}", llm_model)); - fn create_progress_bar(percentage: f32, width: usize) -> String { - let filled = (percentage / 100.0 * width as f32).round() as usize; - let empty = width.saturating_sub(filled); - let filled_chars = "█".repeat(filled); - let empty_chars = "░".repeat(empty); - format!("[{}{}]", filled_chars, empty_chars) - } + let ctx_size = config_manager + .get_config(&bot_id, "llm-server-ctx-size", None) + .unwrap_or_else(|_| "N/A".to_string()); + lines.push(format!(" Context: {}", ctx_size)); - pub fn check_component_running(process_name: &str) -> bool { - std::process::Command::new("pgrep") - .arg("-f") - .arg(process_name) - .output() - .map(|output| !output.stdout.is_empty()) - .unwrap_or(false) - } + let temp = config_manager + .get_config(&bot_id, "llm-temperature", None) + .unwrap_or_else(|_| "N/A".to_string()); + lines.push(format!(" Temp: {}", temp)); + + lines.push(" └─────────────────────────────┘".to_string()); + } + } + } + } + } + Err(_) => { + lines.push(" Error loading bots".to_string()); + } + } + } else { + lines.push(" Database locked".to_string()); + } + + lines.push("".to_string()); + lines.push("╔═══════════════════════════════════════╗".to_string()); + lines.push("║ SESSIONS ║".to_string()); + lines.push("╚═══════════════════════════════════════╝".to_string()); + + let session_count = self + .app_state + .response_channels + .try_lock() + .map(|channels| channels.len()) + .unwrap_or(0); + lines.push(format!(" Active Sessions: {}", session_count)); + + lines.join("\n") + } + + fn create_progress_bar(percentage: f32, width: usize) -> String { + let filled = (percentage / 100.0 * width as f32).round() as usize; + let empty = width.saturating_sub(filled); + let filled_chars = "█".repeat(filled); + let empty_chars = "░".repeat(empty); + format!("[{}{}]", filled_chars, empty_chars) + } + + pub fn check_component_running(process_name: &str) -> bool { + std::process::Command::new("pgrep") + .arg("-f") + .arg(process_name) + .output() + .map(|output| !output.stdout.is_empty()) + .unwrap_or(false) + } } diff --git a/test_llm.rhai b/test_llm.rhai deleted file mode 100644 index eedbcc6b4..000000000 --- a/test_llm.rhai +++ /dev/null @@ -1,3 +0,0 @@ -// Simple test script for LLM keyword -let result = LLM "Hello world"; -result