Compare commits
1 Commits
main
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27ad93d749
|
391
Cargo.lock
generated
391
Cargo.lock
generated
@@ -67,22 +67,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -129,6 +129,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -163,15 +169,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@@ -180,9 +177,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cargo-husky"
|
||||
@@ -192,9 +189,9 @@ checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.46"
|
||||
version = "1.2.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
|
||||
checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -226,9 +223,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -236,9 +233,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.53"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -299,9 +296,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.15.19"
|
||||
version = "0.15.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6"
|
||||
checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"convert_case",
|
||||
@@ -391,9 +388,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
@@ -516,16 +513,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@@ -639,9 +626,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.9"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3"
|
||||
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
@@ -687,9 +674,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
@@ -824,9 +811,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "genai"
|
||||
version = "0.4.4"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "814c33e79506556ecba6b5f8e39a2fe423262fd3903856377ad2ae6a857c6032"
|
||||
checksum = "48317c8c4a7011ffb748502f9c45408a351103ad225f26825d84f2ff0ac49b25"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"derive_more 2.0.1",
|
||||
@@ -845,9 +832,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
version = "0.14.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
@@ -998,9 +985,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -1036,11 +1023,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.18"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
|
||||
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1263,9 +1250,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.9"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
|
||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -1460,165 +1447,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-cloud-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-data"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"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",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-location"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-text"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"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",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-ui-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-location",
|
||||
"objc2-core-text",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"objc2-user-notifications",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-user-notifications"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
@@ -1642,9 +1470,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
version = "0.10.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
@@ -1674,9 +1502,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.111"
|
||||
version = "0.9.110"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1702,18 +1530,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.13.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c39b5918402d564846d5aba164c09a66cc88d232179dfd3e3c619a25a268392"
|
||||
checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"log",
|
||||
"nix",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"plist",
|
||||
"serde",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1838,6 +1662,19 @@ 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",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
@@ -1880,6 +1717,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -1937,9 +1783,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -2060,7 +1906,7 @@ version = "0.12.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -2130,7 +1976,6 @@ name = "robotnik"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"better-panic",
|
||||
"bytes",
|
||||
"cargo-husky",
|
||||
"clap",
|
||||
"color-eyre",
|
||||
@@ -2144,8 +1989,6 @@ dependencies = [
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -2153,16 +1996,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.12.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32"
|
||||
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bitflags",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"typeid",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2241,9 +2082,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.35"
|
||||
version = "0.23.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
@@ -2286,15 +2127,6 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
|
||||
dependencies = [
|
||||
"sdd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
@@ -2318,9 +2150,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.1.0"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
|
||||
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
@@ -2334,12 +2166,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sdd"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
@@ -2456,17 +2282,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.16.0"
|
||||
version = "3.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1"
|
||||
checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.12.0",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.1.0",
|
||||
"schemars 1.0.4",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
@@ -2475,9 +2301,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.16.0"
|
||||
version = "3.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b"
|
||||
checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -2485,31 +2311,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"scc",
|
||||
"serial_test_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test_derive"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -2536,15 +2337,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
@@ -2587,9 +2379,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.110"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2753,7 +2545,6 @@ dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -2803,9 +2594,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.17"
|
||||
version = "0.7.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
||||
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -3237,9 +3028,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.4"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
|
||||
checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
50
Cargo.toml
50
Cargo.toml
@@ -5,7 +5,6 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
better-panic = "0.3.0"
|
||||
bytes = "1"
|
||||
color-eyre = "0.6.3"
|
||||
directories = "6.0"
|
||||
futures = "0.3"
|
||||
@@ -16,42 +15,35 @@ serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
[dependencies.nix]
|
||||
version = "0.30.1"
|
||||
features = ["fs", "resource"]
|
||||
[dependencies.nix]
|
||||
version = "0.30.1"
|
||||
features = [ "fs" ]
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.5"
|
||||
features = ["derive"]
|
||||
[dependencies.clap]
|
||||
version = "4.5"
|
||||
features = [ "derive" ]
|
||||
|
||||
[dependencies.config]
|
||||
version = "0.15"
|
||||
features = ["toml"]
|
||||
[dependencies.config]
|
||||
version = "0.15"
|
||||
features = [ "toml" ]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = [ "derive" ]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = [
|
||||
"io-util",
|
||||
"macros",
|
||||
"net",
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
"sync",
|
||||
"time",
|
||||
]
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = [ "io-util", "macros", "net", "rt-multi-thread", "sync" ]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.24"
|
||||
serial_test = "3.2"
|
||||
tempfile = "3.13"
|
||||
|
||||
[dev-dependencies.cargo-husky]
|
||||
version = "1"
|
||||
features = ["run-cargo-check", "run-cargo-clippy"]
|
||||
[dev-dependencies.cargo-husky]
|
||||
version = "1"
|
||||
features = [
|
||||
"run-cargo-check",
|
||||
"run-cargo-clippy",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
@@ -2,8 +2,6 @@ edition = "2024"
|
||||
style_edition = "2024"
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
format_macro_bodies = true
|
||||
format_macro_matchers = true
|
||||
imports_granularity = "Crate"
|
||||
imports_layout = "HorizontalVertical"
|
||||
wrap_comments = true
|
||||
|
||||
162
src/chat.rs
162
src/chat.rs
@@ -1,131 +1,67 @@
|
||||
//! Handles interaction with IRC.
|
||||
//!
|
||||
//! Each instance of [`Chat`] handles a single connection to an IRC
|
||||
//! server.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use color_eyre::{Result, eyre::WrapErr};
|
||||
// Lots of namespace confusion potential
|
||||
use crate::qna::LLMHandle;
|
||||
use config::Config as MainConfig;
|
||||
use futures::StreamExt;
|
||||
use irc::client::prelude::{Client, Command, Config as IRCConfig, Message};
|
||||
use tokio::sync::mpsc;
|
||||
use irc::client::prelude::{Client as IRCClient, Command, Config as IRCConfig};
|
||||
use tracing::{Level, event, instrument};
|
||||
|
||||
use crate::{Event, EventManager, LLMHandle, plugin};
|
||||
|
||||
/// Chat struct that is used to interact with IRC chat.
|
||||
#[derive(Debug)]
|
||||
pub struct Chat {
|
||||
/// The actual IRC [`irc::client`](client).
|
||||
client: Client,
|
||||
/// Event manager for handling plugin interaction.
|
||||
event_manager: Arc<EventManager>,
|
||||
/// Handle for whichever LLM is being used.
|
||||
client: IRCClient,
|
||||
llm_handle: LLMHandle, // FIXME: This needs to be thread safe, and shared, etc.
|
||||
}
|
||||
|
||||
// Need: owners, channels, username, nick, server, password
|
||||
#[instrument]
|
||||
pub async fn new(settings: &MainConfig, handle: &LLMHandle) -> Result<Chat> {
|
||||
// Going to just assign and let the irc library handle errors for now, and
|
||||
// add my own checking if necessary.
|
||||
let port: u16 = settings.get("port")?;
|
||||
let channels: Vec<String> = settings.get("channels").wrap_err("No channels provided.")?;
|
||||
|
||||
event!(Level::INFO, "Channels = {:?}", channels);
|
||||
|
||||
let config = IRCConfig {
|
||||
server: settings.get_string("server").ok(),
|
||||
nickname: settings.get_string("nickname").ok(),
|
||||
port: Some(port),
|
||||
username: settings.get_string("username").ok(),
|
||||
use_tls: settings.get_bool("use_tls").ok(),
|
||||
channels,
|
||||
..IRCConfig::default()
|
||||
};
|
||||
|
||||
event!(Level::INFO, "IRC connection starting...");
|
||||
|
||||
Ok(Chat {
|
||||
client: IRCClient::from_config(config).await?,
|
||||
llm_handle: handle.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
// Need: owners, channels, username, nick, server, password rather than reading
|
||||
// the config values directly.
|
||||
/// Creates a new [`Chat`].
|
||||
#[instrument]
|
||||
pub async fn new(
|
||||
settings: &MainConfig,
|
||||
handle: &LLMHandle,
|
||||
manager: Arc<EventManager>,
|
||||
) -> Result<Chat> {
|
||||
// Going to just assign and let the irc library handle errors for now, and
|
||||
// add my own checking if necessary.
|
||||
let port: u16 = settings.get("port")?;
|
||||
let channels: Vec<String> = settings.get("channels").wrap_err("No channels provided.")?;
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
let client = &mut self.client;
|
||||
|
||||
event!(Level::INFO, "Channels = {:?}", channels);
|
||||
client.identify()?;
|
||||
|
||||
let config = IRCConfig {
|
||||
server: settings.get_string("server").ok(),
|
||||
nickname: settings.get_string("nickname").ok(),
|
||||
port: Some(port),
|
||||
username: settings.get_string("username").ok(),
|
||||
use_tls: settings.get_bool("use_tls").ok(),
|
||||
channels,
|
||||
..IRCConfig::default()
|
||||
};
|
||||
let mut stream = client.stream()?;
|
||||
|
||||
event!(Level::INFO, "IRC connection starting...");
|
||||
while let Some(message) = stream.next().await.transpose()? {
|
||||
if let Command::PRIVMSG(channel, message) = message.command
|
||||
&& message.starts_with("!gem")
|
||||
{
|
||||
let mut msg = self.llm_handle.send_request(&message).await?;
|
||||
event!(Level::INFO, "Asked: {}", message);
|
||||
event!(Level::INFO, "Answered: {}", msg);
|
||||
|
||||
Ok(Chat {
|
||||
client: Client::from_config(config).await?,
|
||||
llm_handle: handle.clone(),
|
||||
event_manager: manager,
|
||||
})
|
||||
}
|
||||
|
||||
/// Drives the event loop for the chat.
|
||||
pub async fn run(&mut self, mut command_in: mpsc::Receiver<plugin::PluginMsg>) -> Result<()> {
|
||||
self.client.identify()?;
|
||||
|
||||
let mut stream = self.client.stream()?;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
message = stream.next() => {
|
||||
match message {
|
||||
Some(Ok(msg)) => {
|
||||
self.handle_chat_message(&msg).await?;
|
||||
}
|
||||
Some(Err(e)) => return Err(e.into()),
|
||||
None => break, // disconnected
|
||||
}
|
||||
}
|
||||
command = command_in.recv() => {
|
||||
event!(Level::INFO, "Received command {:#?}", command);
|
||||
match command {
|
||||
Some(plugin::PluginMsg::SendMessage {channel, message} ) => {
|
||||
// Now to pass on the message.
|
||||
event!(Level::INFO, "Trying to send to channel.");
|
||||
self.client.send_privmsg(&channel, &message).wrap_err("Couldn't send to channel")?;
|
||||
event!(Level::INFO, "Message sent successfully.");
|
||||
|
||||
}
|
||||
None => {
|
||||
event!(Level::ERROR,
|
||||
"Command channel unexpectedly closed - \
|
||||
FIFO reader may have crashed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_chat_message(&mut self, message: &Message) -> Result<()> {
|
||||
// Broadcast anything here. If it should not be broadcasted then
|
||||
// TryFrom should fail.
|
||||
if let Ok(event) = Event::try_from(message) {
|
||||
self.event_manager.broadcast(&event).await?;
|
||||
}
|
||||
|
||||
// Only handle PRIVMSG for now.
|
||||
if let Command::PRIVMSG(channel, msg) = &message.command {
|
||||
// Just preserve the original behavior for now.
|
||||
if msg.starts_with("!gem") {
|
||||
let mut llm_response = self.llm_handle.send_request(msg).await?;
|
||||
|
||||
event!(Level::INFO, "Asked: {message}");
|
||||
event!(Level::INFO, "Response: {llm_response}");
|
||||
|
||||
// Keep responses to one line for now.
|
||||
llm_response.retain(|c| c != '\n' && c != '\r');
|
||||
|
||||
// TODO: Make this configurable.
|
||||
llm_response.truncate(500);
|
||||
|
||||
event!(Level::INFO, "Sending {llm_response} to channel {channel}");
|
||||
self.client.send_privmsg(channel, llm_response)?;
|
||||
// Make it all one line.
|
||||
msg.retain(|c| c != '\n' && c != '\r');
|
||||
msg.truncate(500);
|
||||
client
|
||||
.send_privmsg(&channel, msg)
|
||||
.wrap_err("Could not send to {channel}")?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
193
src/command.rs
193
src/command.rs
@@ -1,193 +0,0 @@
|
||||
//! Commands that are associated with external processes (commands).
|
||||
//!
|
||||
//! Process based plugins are just an assortment of executable files in
|
||||
//! a provided directory. They are given arguments, and the response from
|
||||
//! them is expected on stdout.
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use color_eyre::{Result, eyre::eyre};
|
||||
use tokio::{fs::try_exists, process::Command, time::timeout};
|
||||
use tracing::{Level, event};
|
||||
|
||||
/// Handle containing information about the directory containing commands.
|
||||
#[derive(Debug)]
|
||||
pub struct CommandDir {
|
||||
command_path: PathBuf,
|
||||
}
|
||||
|
||||
impl CommandDir {
|
||||
/// Register a path containing commands.
|
||||
pub fn new(command_path: impl AsRef<Path>) -> Self {
|
||||
event!(
|
||||
Level::INFO,
|
||||
"CommandDir initialized with path: {:?}",
|
||||
command_path.as_ref()
|
||||
);
|
||||
CommandDir {
|
||||
command_path: command_path.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Look for a command. If it exists Ok(path) is returned.
|
||||
async fn find_command(&self, name: impl AsRef<Path>) -> Result<String> {
|
||||
let path = self.command_path.join(name.as_ref());
|
||||
|
||||
event!(
|
||||
Level::INFO,
|
||||
"Looking for {} command.",
|
||||
name.as_ref().display()
|
||||
);
|
||||
|
||||
match try_exists(&path).await {
|
||||
Ok(true) => Ok(path.to_string_lossy().to_string()),
|
||||
Ok(false) => Err(eyre!(format!("{} Not found.", path.to_string_lossy()))),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the given [`command_name`]. It should exist in the directory provided as
|
||||
/// the command_path.
|
||||
pub async fn run_command(
|
||||
&self,
|
||||
command_name: impl AsRef<str>,
|
||||
input: impl AsRef<str>,
|
||||
) -> Result<Bytes> {
|
||||
let path = self.find_command(Path::new(command_name.as_ref())).await?;
|
||||
// Well it exists let's cross our fingers...
|
||||
let output = Command::new(path).arg(input.as_ref()).output().await?;
|
||||
|
||||
if output.status.success() {
|
||||
// So far so good
|
||||
Ok(Bytes::from(output.stdout))
|
||||
} else {
|
||||
// Whoops
|
||||
Err(eyre!(format!(
|
||||
"Error running {}: {}",
|
||||
command_name.as_ref(),
|
||||
output.status
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// [`run_command`] but with a timeout.
|
||||
pub async fn run_command_with_timeout(
|
||||
&self,
|
||||
command_name: impl AsRef<str>,
|
||||
input: impl AsRef<str>,
|
||||
time_out: Duration,
|
||||
) -> Result<Bytes> {
|
||||
timeout(time_out, self.run_command(command_name, input)).await?
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{
|
||||
fs::{self, Permissions},
|
||||
os::unix::fs::PermissionsExt,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn create_test_script(dir: &Path, name: &str, script: &str) -> PathBuf {
|
||||
let path = dir.join(name);
|
||||
fs::write(&path, script).unwrap();
|
||||
fs::set_permissions(&path, Permissions::from_mode(0o755)).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_dir_new() {
|
||||
let dir = CommandDir::new("/some/path");
|
||||
assert_eq!(dir.command_path, PathBuf::from("/some/path"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_find_command_exists() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
create_test_script(temp.path(), "test_cmd", "#!/bin/bash\necho hello");
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let result = cmd_dir.find_command("test_cmd").await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().contains("test_cmd"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_find_command_not_found() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
|
||||
let result = cmd_dir.find_command("nonexistent").await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Not found"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_command_success() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
create_test_script(temp.path(), "echo_cmd", "#!/bin/bash\necho \"$1\"");
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let result = cmd_dir.run_command("echo_cmd", "hello world").await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
assert_eq!(output.as_ref(), b"hello world\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_command_failure() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
create_test_script(temp.path(), "fail_cmd", "#!/bin/bash\nexit 1");
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let result = cmd_dir.run_command("fail_cmd", "input").await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Error running"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_command_not_found() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
|
||||
let result = cmd_dir.run_command("missing", "input").await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_command_with_timeout_success() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
create_test_script(temp.path(), "fast_cmd", "#!/bin/bash\necho \"$1\"");
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let result = cmd_dir
|
||||
.run_command_with_timeout("fast_cmd", "quick", Duration::from_secs(5))
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_command_with_timeout_expires() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
create_test_script(temp.path(), "slow_cmd", "#!/bin/bash\nsleep 10\necho done");
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let result = cmd_dir
|
||||
.run_command_with_timeout("slow_cmd", "input", Duration::from_millis(100))
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
26
src/event.rs
26
src/event.rs
@@ -1,38 +1,14 @@
|
||||
//! Internal representations of incoming events.
|
||||
|
||||
use irc::proto::{Command, Message};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents an event. Probably from IRC.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Event {
|
||||
/// Who is the message from?
|
||||
from: String,
|
||||
/// What is the message?
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Creates a new message.
|
||||
pub fn new(from: impl Into<String>, msg: impl Into<String>) -> Self {
|
||||
pub fn new(msg: impl Into<String>) -> Self {
|
||||
Self {
|
||||
from: from.into(),
|
||||
message: msg.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Message> for Event {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: &Message) -> Result<Self, Self::Error> {
|
||||
let from = value.response_target().unwrap_or("unknown").to_string();
|
||||
match &value.command {
|
||||
Command::PRIVMSG(_channel, message) => Ok(Event {
|
||||
from,
|
||||
message: message.clone(),
|
||||
}),
|
||||
_ => Err("Not a PRIVMSG"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
//! Handler for events to and from IPC, and process plugins.
|
||||
|
||||
use std::{collections::VecDeque, path::Path, sync::Arc};
|
||||
|
||||
use color_eyre::Result;
|
||||
use nix::{NixPath, sys::stat, unistd::mkfifo};
|
||||
//use nix::{NixPath, sys::stat, unistd::mkfifo};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::{UnixListener, UnixStream, unix::pipe},
|
||||
sync::{RwLock, broadcast, mpsc},
|
||||
// fs::File,
|
||||
io::AsyncWriteExt,
|
||||
net::{
|
||||
UnixListener,
|
||||
UnixStream,
|
||||
// unix::pipe::{self, Receiver},
|
||||
},
|
||||
sync::{RwLock, broadcast},
|
||||
};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{event::Event, plugin::PluginMsg};
|
||||
use crate::event::Event;
|
||||
|
||||
// Hard coding for now. Maybe make this a parameter to new.
|
||||
const EVENT_BUF_MAX: usize = 1000;
|
||||
|
||||
/// Manager for communication with plugins.
|
||||
///
|
||||
/// Keeps events in a ring buffer to track a certain amount of history.
|
||||
#[derive(Debug)]
|
||||
// Manager for communication with plugins.
|
||||
pub struct EventManager {
|
||||
announce: broadcast::Sender<String>, // Everything broadcasts here.
|
||||
events: Arc<RwLock<VecDeque<String>>>, // Ring buffer.
|
||||
}
|
||||
|
||||
impl EventManager {
|
||||
/// Create a new [`EventManager``].
|
||||
pub fn new() -> Result<Self> {
|
||||
let (announce, _) = broadcast::channel(100);
|
||||
|
||||
@@ -36,7 +35,6 @@ impl EventManager {
|
||||
})
|
||||
}
|
||||
|
||||
/// Broadcast an event to every subscribed listener.
|
||||
pub async fn broadcast(&self, event: &Event) -> Result<()> {
|
||||
let msg = serde_json::to_string(event)? + "\n";
|
||||
|
||||
@@ -55,35 +53,17 @@ impl EventManager {
|
||||
}
|
||||
|
||||
// NB: This assumes it has exclusive control of the FIFO.
|
||||
/// Opens a fifo at [`path`]. This is where some plugins can send response events
|
||||
/// to. The messages MUST be formatted in JSON and match one of the possible
|
||||
/// [`PluginMsg`](plugin messages).
|
||||
pub async fn start_fifo<P>(path: &P, command_tx: mpsc::Sender<PluginMsg>) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path> + NixPath + ?Sized,
|
||||
{
|
||||
// Overwrite, or create the FIFO.
|
||||
let _ = std::fs::remove_file(path);
|
||||
mkfifo(path, stat::Mode::S_IRWXU)?;
|
||||
// async fn start_fifo<P>(path: &P) -> Result<()>
|
||||
// where
|
||||
// P: AsRef<Path> + NixPath + ?Sized,
|
||||
// {
|
||||
// // Just delete the old FIFO if it exists.
|
||||
// let _ = std::fs::remove_file(path);
|
||||
// mkfifo(path, stat::Mode::S_IRWXU)?;
|
||||
|
||||
loop {
|
||||
let rx = pipe::OpenOptions::new().open_receiver(path)?;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
let mut reader = BufReader::new(rx);
|
||||
let mut line = String::new();
|
||||
|
||||
while reader.read_line(&mut line).await? > 0 {
|
||||
// Now handle the command.
|
||||
let cmd: PluginMsg = serde_json::from_str(&line)?;
|
||||
info!("Command received: {:?}.", cmd);
|
||||
command_tx.send(cmd).await?;
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a UNIX socket that will provide broadcast messages to any client that opens
|
||||
/// the socket for listening.
|
||||
pub async fn start_listening(self: Arc<Self>, broadcast_path: impl AsRef<Path>) {
|
||||
let listener = UnixListener::bind(broadcast_path).unwrap();
|
||||
|
||||
@@ -104,7 +84,6 @@ impl EventManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send any events queued up to the [`stream`].
|
||||
async fn send_events(&self, stream: UnixStream) -> Result<()> {
|
||||
let mut writer = stream;
|
||||
|
||||
@@ -143,7 +122,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_broadcast_adds_event_to_buffer() {
|
||||
let manager = EventManager::new().unwrap();
|
||||
let event = Event::new("test_user", "test message");
|
||||
let event = Event::new("test message");
|
||||
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
|
||||
@@ -156,7 +135,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_broadcast_serializes_event_as_json() {
|
||||
let manager = EventManager::new().unwrap();
|
||||
let event = Event::new("test_user", "hello world");
|
||||
let event = Event::new("hello world");
|
||||
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
|
||||
@@ -178,7 +157,7 @@ mod tests {
|
||||
let manager = EventManager::new().unwrap();
|
||||
|
||||
for i in 0..count {
|
||||
let event = Event::new("test_user", format!("event {}", i));
|
||||
let event = Event::new(format!("event {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -192,7 +171,7 @@ mod tests {
|
||||
|
||||
// Fill to exactly EVENT_BUF_MAX (1000)
|
||||
for i in 0..EVENT_BUF_MAX {
|
||||
let event = Event::new("test_user", format!("event {}", i));
|
||||
let event = Event::new(format!("event {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -214,7 +193,7 @@ mod tests {
|
||||
|
||||
// Broadcast more events than buffer can hold
|
||||
for i in 0..total {
|
||||
let event = Event::new("test_user", format!("event {}", i));
|
||||
let event = Event::new(format!("event {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -242,7 +221,7 @@ mod tests {
|
||||
let messages = vec!["first", "second", "third", "fourth", "fifth"];
|
||||
|
||||
for msg in &messages {
|
||||
let event = Event::new("test_user", *msg);
|
||||
let event = Event::new(*msg);
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -260,13 +239,13 @@ mod tests {
|
||||
|
||||
// Fill buffer completely
|
||||
for i in 0..EVENT_BUF_MAX {
|
||||
let event = Event::new("test_user", format!("old {}", i));
|
||||
let event = Event::new(format!("old {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
// Add 5 more events
|
||||
for i in 0..5 {
|
||||
let event = Event::new("test_user", format!("new {}", i));
|
||||
let event = Event::new(format!("new {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -296,7 +275,7 @@ mod tests {
|
||||
let manager_clone = Arc::clone(&manager);
|
||||
let handle = tokio::spawn(async move {
|
||||
for i in 0..10 {
|
||||
let event = Event::new("test_user", format!("task {} event {}", task_id, i));
|
||||
let event = Event::new(format!("task {} event {}", task_id, i));
|
||||
manager_clone.broadcast(&event).await.unwrap();
|
||||
}
|
||||
});
|
||||
@@ -311,277 +290,4 @@ mod tests {
|
||||
let events = manager.events.read().await;
|
||||
assert_eq!(events.len(), 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fifo_receives_and_forwards_single_command() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let fifo_path = temp_dir.path().join("test.fifo");
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
// Spawn the FIFO reader
|
||||
let path = fifo_path.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = EventManager::start_fifo(&path, tx).await;
|
||||
});
|
||||
|
||||
// Give it time to create the FIFO
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
// Write a command to the FIFO
|
||||
let cmd = PluginMsg::SendMessage {
|
||||
channel: "#test".to_string(),
|
||||
message: "hello".to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&cmd).unwrap() + "\n";
|
||||
|
||||
// Open FIFO for writing and write the command
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let tx = pipe::OpenOptions::new().open_sender(&fifo_path).unwrap();
|
||||
let mut tx = tokio::io::BufWriter::new(tx);
|
||||
tx.write_all(json.as_bytes()).await.unwrap();
|
||||
tx.flush().await.unwrap();
|
||||
});
|
||||
|
||||
// Should receive the command within a reasonable time
|
||||
let received = tokio::time::timeout(tokio::time::Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("timeout waiting for command")
|
||||
.expect("channel closed");
|
||||
|
||||
match received {
|
||||
PluginMsg::SendMessage { channel, message } => {
|
||||
assert_eq!(channel, "#test");
|
||||
assert_eq!(message, "hello");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fifo_handles_multiple_commands() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let fifo_path = temp_dir.path().join("test.fifo");
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
// Spawn the FIFO reader
|
||||
let path = fifo_path.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = EventManager::start_fifo(&path, tx).await;
|
||||
});
|
||||
|
||||
// Give it time to create the FIFO
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
// Write multiple commands
|
||||
let commands = vec![
|
||||
PluginMsg::SendMessage {
|
||||
channel: "#chan1".to_string(),
|
||||
message: "first".to_string(),
|
||||
},
|
||||
PluginMsg::SendMessage {
|
||||
channel: "#chan2".to_string(),
|
||||
message: "second".to_string(),
|
||||
},
|
||||
PluginMsg::SendMessage {
|
||||
channel: "#chan3".to_string(),
|
||||
message: "third".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let tx = pipe::OpenOptions::new().open_sender(&fifo_path).unwrap();
|
||||
let mut tx = tokio::io::BufWriter::new(tx);
|
||||
|
||||
for cmd in commands {
|
||||
let json = serde_json::to_string(&cmd).unwrap() + "\n";
|
||||
tx.write_all(json.as_bytes()).await.unwrap();
|
||||
}
|
||||
tx.flush().await.unwrap();
|
||||
});
|
||||
|
||||
// Receive all three commands in order
|
||||
let first = tokio::time::timeout(tokio::time::Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("timeout on first")
|
||||
.expect("channel closed");
|
||||
|
||||
match first {
|
||||
PluginMsg::SendMessage { channel, message } => {
|
||||
assert_eq!(channel, "#chan1");
|
||||
assert_eq!(message, "first");
|
||||
}
|
||||
}
|
||||
|
||||
let second = tokio::time::timeout(tokio::time::Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("timeout on second")
|
||||
.expect("channel closed");
|
||||
|
||||
match second {
|
||||
PluginMsg::SendMessage { channel, message } => {
|
||||
assert_eq!(channel, "#chan2");
|
||||
assert_eq!(message, "second");
|
||||
}
|
||||
}
|
||||
|
||||
let third = tokio::time::timeout(tokio::time::Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("timeout on third")
|
||||
.expect("channel closed");
|
||||
|
||||
match third {
|
||||
PluginMsg::SendMessage { channel, message } => {
|
||||
assert_eq!(channel, "#chan3");
|
||||
assert_eq!(message, "third");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fifo_reopens_after_writer_closes() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let fifo_path = temp_dir.path().join("test.fifo");
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
// Spawn the FIFO reader
|
||||
let path = fifo_path.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = EventManager::start_fifo(&path, tx).await;
|
||||
});
|
||||
|
||||
// Give it time to create the FIFO
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
// First writer sends a command and closes
|
||||
{
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let path = fifo_path.clone();
|
||||
tokio::spawn(async move {
|
||||
let tx = pipe::OpenOptions::new().open_sender(&path).unwrap();
|
||||
let mut tx = tokio::io::BufWriter::new(tx);
|
||||
|
||||
let cmd = PluginMsg::SendMessage {
|
||||
channel: "#first".to_string(),
|
||||
message: "batch1".to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&cmd).unwrap() + "\n";
|
||||
tx.write_all(json.as_bytes()).await.unwrap();
|
||||
tx.flush().await.unwrap();
|
||||
// Writer drops here, closing the FIFO
|
||||
});
|
||||
|
||||
let first = tokio::time::timeout(tokio::time::Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("timeout on first batch")
|
||||
.expect("channel closed");
|
||||
|
||||
match first {
|
||||
PluginMsg::SendMessage { channel, message } => {
|
||||
assert_eq!(channel, "#first");
|
||||
assert_eq!(message, "batch1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give the FIFO time to reopen
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
// Second writer opens and sends a command
|
||||
{
|
||||
use tokio::io::AsyncWriteExt;
|
||||
tokio::spawn(async move {
|
||||
let tx = pipe::OpenOptions::new().open_sender(&fifo_path).unwrap();
|
||||
let mut tx = tokio::io::BufWriter::new(tx);
|
||||
|
||||
let cmd = PluginMsg::SendMessage {
|
||||
channel: "#second".to_string(),
|
||||
message: "batch2".to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&cmd).unwrap() + "\n";
|
||||
tx.write_all(json.as_bytes()).await.unwrap();
|
||||
tx.flush().await.unwrap();
|
||||
});
|
||||
|
||||
let second = tokio::time::timeout(tokio::time::Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("timeout on second batch - FIFO may not have reopened")
|
||||
.expect("channel closed");
|
||||
|
||||
match second {
|
||||
PluginMsg::SendMessage { channel, message } => {
|
||||
assert_eq!(channel, "#second");
|
||||
assert_eq!(message, "batch2");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fifo_handles_empty_lines() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let fifo_path = temp_dir.path().join("test.fifo");
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
// Spawn the FIFO reader
|
||||
let path = fifo_path.clone();
|
||||
let handle = tokio::spawn(async move { EventManager::start_fifo(&path, tx).await });
|
||||
|
||||
// Give it time to create the FIFO
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
// Write command, empty line, whitespace, another command
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let tx = pipe::OpenOptions::new().open_sender(&fifo_path).unwrap();
|
||||
let mut tx = tokio::io::BufWriter::new(tx);
|
||||
|
||||
let cmd1 = PluginMsg::SendMessage {
|
||||
channel: "#test".to_string(),
|
||||
message: "first".to_string(),
|
||||
};
|
||||
let json1 = serde_json::to_string(&cmd1).unwrap() + "\n";
|
||||
tx.write_all(json1.as_bytes()).await.unwrap();
|
||||
|
||||
// Write empty line
|
||||
tx.write_all(b"\n").await.unwrap();
|
||||
|
||||
// Write whitespace line
|
||||
tx.write_all(b" \n").await.unwrap();
|
||||
|
||||
let cmd2 = PluginMsg::SendMessage {
|
||||
channel: "#test".to_string(),
|
||||
message: "second".to_string(),
|
||||
};
|
||||
let json2 = serde_json::to_string(&cmd2).unwrap() + "\n";
|
||||
tx.write_all(json2.as_bytes()).await.unwrap();
|
||||
tx.flush().await.unwrap();
|
||||
});
|
||||
|
||||
// Should receive first command
|
||||
let first = tokio::time::timeout(tokio::time::Duration::from_millis(500), rx.recv())
|
||||
.await
|
||||
.expect("timeout on first")
|
||||
.expect("channel closed");
|
||||
|
||||
match first {
|
||||
PluginMsg::SendMessage { channel, message } => {
|
||||
assert_eq!(channel, "#test");
|
||||
assert_eq!(message, "first");
|
||||
}
|
||||
}
|
||||
|
||||
// The empty/whitespace lines should cause JSON parse errors
|
||||
// which will cause start_fifo to error and exit
|
||||
// So we expect the handle to complete (with an error)
|
||||
let result = tokio::time::timeout(tokio::time::Duration::from_secs(1), handle)
|
||||
.await
|
||||
.expect("FIFO task should exit due to parse error");
|
||||
|
||||
// The task should have errored
|
||||
assert!(
|
||||
result.unwrap().is_err(),
|
||||
"Expected parse error from empty line"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
26
src/ipc.rs
Normal file
26
src/ipc.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Provides an IPC socket to communicate with other processes.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::Result;
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
pub struct IPC {
|
||||
listener: UnixListener,
|
||||
}
|
||||
|
||||
impl IPC {
|
||||
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let listener = UnixListener::bind(path)?;
|
||||
Ok(Self { listener })
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
loop {
|
||||
match self.listener.accept().await {
|
||||
Ok((_stream, _addr)) => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/lib.rs
33
src/lib.rs
@@ -1,23 +1,19 @@
|
||||
#![warn(missing_docs)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
// Robotnik libraries
|
||||
|
||||
use std::{os::unix::fs, sync::Arc};
|
||||
|
||||
use color_eyre::{Result, eyre::WrapErr};
|
||||
use human_panic::setup_panic;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{Level, info};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
pub mod chat;
|
||||
pub mod command;
|
||||
pub mod event;
|
||||
pub mod event_manager;
|
||||
pub mod plugin;
|
||||
pub mod ipc;
|
||||
pub mod qna;
|
||||
pub mod setup;
|
||||
|
||||
pub use chat::Chat;
|
||||
pub use event::Event;
|
||||
pub use event_manager::EventManager;
|
||||
pub use qna::LLMHandle;
|
||||
@@ -27,9 +23,7 @@ const DEFAULT_INSTRUCT: &str =
|
||||
be sent in a single IRC response according to the specification. Keep answers to
|
||||
500 characters or less.";
|
||||
|
||||
/// Initialize all logging facilities.
|
||||
///
|
||||
/// This should cause a panic if there's a failure.
|
||||
// NB: Everything should fail if logging doesn't start properly.
|
||||
async fn init_logging() {
|
||||
better_panic::install();
|
||||
setup_panic!();
|
||||
@@ -41,10 +35,6 @@ async fn init_logging() {
|
||||
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||
}
|
||||
|
||||
/// Sets up and runs the main event loop.
|
||||
///
|
||||
/// Should return an error if it's recoverable, but could panic if something
|
||||
/// is particularly bad.
|
||||
pub async fn run() -> Result<()> {
|
||||
init_logging().await;
|
||||
info!("Starting up.");
|
||||
@@ -76,23 +66,18 @@ pub async fn run() -> Result<()> {
|
||||
|
||||
let ev_manager = Arc::new(EventManager::new()?);
|
||||
let ev_manager_clone = Arc::clone(&ev_manager);
|
||||
ev_manager_clone
|
||||
.broadcast(&Event::new("Starting..."))
|
||||
.await?;
|
||||
|
||||
let mut c = Chat::new(&config, &handle, Arc::clone(&ev_manager)).await?;
|
||||
|
||||
let (from_plugins, to_chat) = mpsc::channel(100);
|
||||
let mut c = chat::new(&config, &handle).await?;
|
||||
|
||||
tokio::select! {
|
||||
_ = ev_manager_clone.start_listening("/tmp/robo.sock") => {
|
||||
// Event listener ended
|
||||
}
|
||||
result = c.run(to_chat) => {
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Chat run error: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
fifo = EventManager::start_fifo("/tmp/robo_in.sock", from_plugins) => {
|
||||
fifo.wrap_err("FIFO reader failed.")?;
|
||||
result = c.run() => {
|
||||
result.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
//! Plugin command definitions.
|
||||
|
||||
// Dear future me: If you forget the JSON translations in the future you'll
|
||||
// thank me for the comment overkill.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Message types accepted from plugins.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum PluginMsg {
|
||||
/// Plugin message indicating the bot should send a [`message`] to [`channel`].
|
||||
/// {
|
||||
/// "SendMessage": {
|
||||
/// "channel": "channel_name",
|
||||
/// "message": "your message here"
|
||||
/// }
|
||||
///
|
||||
/// }
|
||||
SendMessage {
|
||||
/// The IRC channel to send the [`message`] to.
|
||||
channel: String,
|
||||
/// The [`message`] to send.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for PluginMsg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::SendMessage { channel, message } => {
|
||||
write!(f, "[{channel}]: {message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Handles communication with a genai compatible LLM.
|
||||
|
||||
use color_eyre::Result;
|
||||
use futures::StreamExt;
|
||||
use genai::{
|
||||
@@ -10,11 +8,8 @@ use genai::{
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
// NB: Docs are quick and dirty as this might move into a plugin.
|
||||
|
||||
// Represents an LLM completion source.
|
||||
// FIXME: Clone is probably temporary.
|
||||
/// Struct containing information about the LLM.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LLMHandle {
|
||||
chat_request: ChatRequest,
|
||||
@@ -23,7 +18,6 @@ pub struct LLMHandle {
|
||||
}
|
||||
|
||||
impl LLMHandle {
|
||||
/// Create a new handle.
|
||||
pub fn new(
|
||||
api_key: String,
|
||||
_base_url: impl AsRef<str>,
|
||||
@@ -50,7 +44,6 @@ impl LLMHandle {
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a chat message to the LLM with the response being returned as a [`String`].
|
||||
pub async fn send_request(&mut self, message: impl Into<String>) -> Result<String> {
|
||||
let mut req = self.chat_request.clone();
|
||||
let client = self.client.clone();
|
||||
|
||||
34
src/setup.rs
34
src/setup.rs
@@ -1,7 +1,3 @@
|
||||
//! Handles configuration for the bot.
|
||||
//!
|
||||
//! Both command line, and configuration file options are handled here.
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::{Result, eyre::WrapErr};
|
||||
use config::Config;
|
||||
@@ -10,7 +6,6 @@ use std::path::PathBuf;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
// TODO: use [clap(long, short, help_heading = Some(section))]
|
||||
/// Struct of potential arguments.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[command(about, version)]
|
||||
pub struct Args {
|
||||
@@ -35,7 +30,6 @@ pub struct Args {
|
||||
pub instruct: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
/// Name of the model to use. E.g. 'deepseek-chat'
|
||||
pub model: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
@@ -66,36 +60,21 @@ pub struct Args {
|
||||
/// IRC Username
|
||||
pub username: Option<String>,
|
||||
|
||||
#[arg(long = "no-tls")]
|
||||
#[arg(long)]
|
||||
/// Whether or not to use TLS when connecting to the IRC server.
|
||||
pub use_tls: Option<bool>,
|
||||
}
|
||||
|
||||
/// Handle for interacting with the bot configuration.
|
||||
pub struct Setup {
|
||||
/// Handle for the configuration file options.
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Initialize a new [`Setup`] instance.
|
||||
///
|
||||
/// This reads the settings file which becomes the bot's default configuration.
|
||||
/// These settings shall be overridden by any command line options.
|
||||
pub async fn init() -> Result<Setup> {
|
||||
// Get arguments. These overrule configuration file, and environment
|
||||
// variables if applicable.
|
||||
let args = Args::parse();
|
||||
|
||||
let settings = make_config(args)?;
|
||||
|
||||
Ok(Setup { config: settings })
|
||||
}
|
||||
|
||||
/// Create a configuration object from arguments.
|
||||
///
|
||||
/// This is exposed for testing purposes.
|
||||
pub fn make_config(args: Args) -> Result<Config> {
|
||||
// Use default config location unless specified.
|
||||
let config_location: PathBuf = if let Some(ref path) = args.config_file {
|
||||
path.to_owned()
|
||||
@@ -109,7 +88,7 @@ pub fn make_config(args: Args) -> Result<Config> {
|
||||
|
||||
info!("Starting.");
|
||||
|
||||
Config::builder()
|
||||
let settings = Config::builder()
|
||||
.add_source(config::File::with_name(&config_location.to_string_lossy()).required(false))
|
||||
.add_source(config::Environment::with_prefix("BOT"))
|
||||
// Doing all of these overrides provides a unified access point for options,
|
||||
@@ -119,14 +98,15 @@ pub fn make_config(args: Args) -> Result<Config> {
|
||||
.set_override_option("chroot-dir", args.chroot_dir.clone())?
|
||||
.set_override_option("command-path", args.command_dir.clone())?
|
||||
.set_override_option("model", args.model.clone())?
|
||||
.set_override_option("nick-password", args.nick_password.clone())?
|
||||
.set_override_option("instruct", args.instruct.clone())?
|
||||
.set_override_option("channels", args.channels.clone())?
|
||||
.set_override_option("server", args.server.clone())?
|
||||
.set_override_option("port", args.port.clone())?
|
||||
.set_override_option("port", args.port.clone())? // FIXME: Make this a default here not in clap.
|
||||
.set_override_option("nickname", args.nickname.clone())?
|
||||
.set_override_option("username", args.username.clone())?
|
||||
.set_override_option("use-tls", args.use_tls)?
|
||||
.set_override_option("use_tls", args.use_tls)?
|
||||
.build()
|
||||
.wrap_err("Couldn't read configuration settings.")
|
||||
.wrap_err("Couldn't read configuration settings.")?;
|
||||
|
||||
Ok(Setup { config: settings })
|
||||
}
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
use std::{
|
||||
fs::{self, Permissions},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::Path,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use robotnik::command::CommandDir;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Helper to create executable test scripts
|
||||
fn create_command(dir: &Path, name: &str, script: &str) {
|
||||
let path = dir.join(name);
|
||||
fs::write(&path, script).unwrap();
|
||||
fs::set_permissions(&path, Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
|
||||
/// Parse a bot message like "!weather 07008" into (command_name, argument)
|
||||
fn parse_bot_message(message: &str) -> Option<(&str, &str)> {
|
||||
if !message.starts_with('!') {
|
||||
return None;
|
||||
}
|
||||
let without_prefix = &message[1..];
|
||||
let mut parts = without_prefix.splitn(2, ' ');
|
||||
let command = parts.next()?;
|
||||
let arg = parts.next().unwrap_or("");
|
||||
Some((command, arg))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bot_message_finds_and_runs_command() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
// Create a weather command that echoes the zip code
|
||||
create_command(
|
||||
temp.path(),
|
||||
"weather",
|
||||
r#"#!/bin/bash
|
||||
echo "Weather for $1: Sunny, 72°F"
|
||||
"#,
|
||||
);
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let message = "!weather 10096";
|
||||
|
||||
// Parse the message
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
assert_eq!(command_name, "weather");
|
||||
assert_eq!(arg, "10096");
|
||||
|
||||
// Find and run the command
|
||||
let result = cmd_dir.run_command(command_name, arg).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let bytes = result.unwrap();
|
||||
let output = String::from_utf8_lossy(&bytes);
|
||||
assert!(output.contains("Weather for 10096"));
|
||||
assert!(output.contains("Sunny"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bot_message_command_not_found() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
|
||||
let message = "!nonexistent arg";
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
|
||||
let result = cmd_dir.run_command(command_name, arg).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Not found"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bot_message_with_multiple_arguments() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
// Create a command that handles multiple words as a single argument
|
||||
create_command(
|
||||
temp.path(),
|
||||
"echo",
|
||||
r#"#!/bin/bash
|
||||
echo "You said: $1"
|
||||
"#,
|
||||
);
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let message = "!echo hello world how are you";
|
||||
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
assert_eq!(command_name, "echo");
|
||||
assert_eq!(arg, "hello world how are you");
|
||||
|
||||
let result = cmd_dir.run_command(command_name, arg).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let bytes = result.unwrap();
|
||||
let output = String::from_utf8_lossy(&bytes);
|
||||
assert!(output.contains("hello world how are you"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bot_message_without_argument() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
create_command(
|
||||
temp.path(),
|
||||
"help",
|
||||
r#"#!/bin/bash
|
||||
echo "Available commands: weather, echo, help"
|
||||
"#,
|
||||
);
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let message = "!help";
|
||||
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
assert_eq!(command_name, "help");
|
||||
assert_eq!(arg, "");
|
||||
|
||||
let result = cmd_dir.run_command(command_name, arg).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let bytes = result.unwrap();
|
||||
let output = String::from_utf8_lossy(&bytes);
|
||||
assert!(output.contains("Available commands"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bot_message_command_returns_error_exit_code() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
// Create a command that fails for invalid input
|
||||
create_command(
|
||||
temp.path(),
|
||||
"validate",
|
||||
r#"#!/bin/bash
|
||||
if [ -z "$1" ]; then
|
||||
echo "Error: Input required" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Valid: $1"
|
||||
"#,
|
||||
);
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let message = "!validate";
|
||||
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
let result = cmd_dir.run_command(command_name, arg).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Error running"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bot_message_with_timeout() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
create_command(
|
||||
temp.path(),
|
||||
"quick",
|
||||
r#"#!/bin/bash
|
||||
echo "Result: $1"
|
||||
"#,
|
||||
);
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let message = "!quick test";
|
||||
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
let result = cmd_dir
|
||||
.run_command_with_timeout(command_name, arg, Duration::from_secs(5))
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bot_message_command_times_out() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
create_command(
|
||||
temp.path(),
|
||||
"slow",
|
||||
r#"#!/bin/bash
|
||||
sleep 10
|
||||
echo "Done"
|
||||
"#,
|
||||
);
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let message = "!slow arg";
|
||||
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
let result = cmd_dir
|
||||
.run_command_with_timeout(command_name, arg, Duration::from_millis(100))
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_commands_in_directory() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
create_command(
|
||||
temp.path(),
|
||||
"weather",
|
||||
r#"#!/bin/bash
|
||||
echo "Weather: Sunny"
|
||||
"#,
|
||||
);
|
||||
|
||||
create_command(
|
||||
temp.path(),
|
||||
"time",
|
||||
r#"#!/bin/bash
|
||||
echo "Time: 12:00"
|
||||
"#,
|
||||
);
|
||||
|
||||
create_command(
|
||||
temp.path(),
|
||||
"joke",
|
||||
r#"#!/bin/bash
|
||||
echo "Why did the robot go on vacation? To recharge!"
|
||||
"#,
|
||||
);
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
|
||||
// Test each command
|
||||
let messages = ["!weather", "!time", "!joke"];
|
||||
let expected = ["Sunny", "12:00", "recharge"];
|
||||
|
||||
for (message, expect) in messages.iter().zip(expected.iter()) {
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
let result = cmd_dir.run_command(command_name, arg).await;
|
||||
assert!(result.is_ok());
|
||||
let bytes = result.unwrap();
|
||||
let output = String::from_utf8_lossy(&bytes);
|
||||
assert!(
|
||||
output.contains(expect),
|
||||
"Expected '{}' in '{}'",
|
||||
expect,
|
||||
output
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_non_bot_message_ignored() {
|
||||
// Messages not starting with ! should be ignored
|
||||
let messages = ["hello world", "weather 10096", "?help", "/command", ""];
|
||||
|
||||
for message in messages {
|
||||
assert!(
|
||||
parse_bot_message(message).is_none(),
|
||||
"Should ignore: {}",
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_command_output_is_bytes() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
// Create a command that outputs binary-safe content
|
||||
create_command(
|
||||
temp.path(),
|
||||
"binary",
|
||||
r#"#!/bin/bash
|
||||
printf "Hello\x00World"
|
||||
"#,
|
||||
);
|
||||
|
||||
let cmd_dir = CommandDir::new(temp.path());
|
||||
let message = "!binary test";
|
||||
|
||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||
let result = cmd_dir.run_command(command_name, arg).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
// Should preserve the null byte
|
||||
assert_eq!(&output[..], b"Hello\x00World");
|
||||
}
|
||||
@@ -65,7 +65,7 @@ async fn test_client_connects_and_receives_event() {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Broadcast an event
|
||||
let event = Event::new("test_user", "test message");
|
||||
let event = Event::new("test message");
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
|
||||
// Connect as a client
|
||||
@@ -90,7 +90,7 @@ async fn test_client_receives_event_history() {
|
||||
|
||||
// Broadcast events BEFORE starting the listener
|
||||
for i in 0..5 {
|
||||
let event = Event::new("test_user", format!("historical event {}", i));
|
||||
let event = Event::new(format!("historical event {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ async fn test_multiple_clients_receive_same_events() {
|
||||
let mut reader3 = BufReader::new(stream3);
|
||||
|
||||
// Broadcast a new event
|
||||
let event = Event::new("test_user", "broadcast to all");
|
||||
let event = Event::new("broadcast to all");
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
|
||||
// All clients should receive the event
|
||||
@@ -191,7 +191,7 @@ async fn test_late_joiner_receives_full_history() {
|
||||
|
||||
// Broadcast several events
|
||||
for i in 0..10 {
|
||||
let event = Event::new("test_user", format!("event {}", i));
|
||||
let event = Event::new(format!("event {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ async fn test_client_receives_events_in_order() {
|
||||
// Broadcast events rapidly
|
||||
let count = 50;
|
||||
for i in 0..count {
|
||||
let event = Event::new("test_user", format!("sequence {}", i));
|
||||
let event = Event::new(format!("sequence {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ async fn test_concurrent_broadcasts_during_client_connections() {
|
||||
let broadcast_manager = Arc::clone(&manager);
|
||||
let broadcast_handle = tokio::spawn(async move {
|
||||
for i in 0..100 {
|
||||
let event = Event::new("test_user", format!("concurrent event {}", i));
|
||||
let event = Event::new(format!("concurrent event {}", i));
|
||||
broadcast_manager.broadcast(&event).await.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
@@ -329,7 +329,7 @@ async fn test_buffer_overflow_affects_new_clients() {
|
||||
|
||||
// Broadcast more than buffer max (1000)
|
||||
for i in 0..1100 {
|
||||
let event = Event::new("test_user", format!("overflow event {}", i));
|
||||
let event = Event::new(format!("overflow event {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@ async fn test_client_count_scaling(#[case] num_clients: usize, #[case] events_pe
|
||||
|
||||
// Broadcast events
|
||||
for i in 0..events_per_client {
|
||||
let event = Event::new("test_user", format!("scale event {}", i));
|
||||
let event = Event::new(format!("scale event {}", i));
|
||||
manager.broadcast(&event).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ async fn test_client_disconnect_doesnt_affect_others() {
|
||||
|
||||
// Broadcast initial event
|
||||
manager
|
||||
.broadcast(&Event::new("test_user", "before disconnect"))
|
||||
.broadcast(&Event::new("before disconnect"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -440,7 +440,7 @@ async fn test_client_disconnect_doesnt_affect_others() {
|
||||
|
||||
// Broadcast another event
|
||||
manager
|
||||
.broadcast(&Event::new("test_user", "after disconnect"))
|
||||
.broadcast(&Event::new("after disconnect"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -473,10 +473,7 @@ async fn test_json_deserialization_of_received_events() {
|
||||
|
||||
// Broadcast an event with special characters
|
||||
let test_message = "special chars: @#$% newline\\n tab\\t quotes \"test\"";
|
||||
manager
|
||||
.broadcast(&Event::new("test_user", test_message))
|
||||
.await
|
||||
.unwrap();
|
||||
manager.broadcast(&Event::new(test_message)).await.unwrap();
|
||||
|
||||
// Connect and deserialize
|
||||
let stream = UnixStream::connect(&socket_path).await.unwrap();
|
||||
@@ -486,7 +483,7 @@ async fn test_json_deserialization_of_received_events() {
|
||||
reader.read_line(&mut line).await.unwrap();
|
||||
|
||||
// Should be valid JSON
|
||||
let parsed: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
|
||||
let parsed: serde_json::Value = serde_json::from_str(&line.trim()).unwrap();
|
||||
|
||||
assert_eq!(parsed["message"], test_message);
|
||||
|
||||
|
||||
@@ -1,556 +0,0 @@
|
||||
use robotnik::setup::{Args, make_config};
|
||||
use serial_test::serial;
|
||||
use std::{fs, path::PathBuf};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Helper to create a temporary config file
|
||||
fn create_config_file(dir: &TempDir, content: &str) -> PathBuf {
|
||||
let config_path = dir.path().join("config.toml");
|
||||
fs::write(&config_path, content).unwrap();
|
||||
config_path
|
||||
}
|
||||
|
||||
/// Helper to parse config using environment and config file
|
||||
async fn parse_config_from_file(config_path: &PathBuf) -> config::Config {
|
||||
config::Config::builder()
|
||||
.add_source(config::File::with_name(&config_path.to_string_lossy()).required(true))
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_setup_make_config_overrides() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
api-key = \"file-key\"
|
||||
model = \"file-model\"
|
||||
port = 6667
|
||||
";
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
// Construct Args with overrides
|
||||
let args = Args {
|
||||
api_key: Some("cli-key".to_string()),
|
||||
base_url: None, /* Should fail if required and not in file/env? No, base-url is optional
|
||||
* in args */
|
||||
chroot_dir: None,
|
||||
command_dir: None,
|
||||
instruct: None,
|
||||
model: None, // Should fallback to file
|
||||
channels: None,
|
||||
config_file: Some(config_path),
|
||||
server: None, // Should use default or file? Args has default "irc.libera.chat"
|
||||
port: Some("9999".to_string()),
|
||||
nickname: None,
|
||||
nick_password: None,
|
||||
username: None,
|
||||
use_tls: None,
|
||||
};
|
||||
|
||||
let config = make_config(args).expect("Failed to make config");
|
||||
|
||||
// Check overrides
|
||||
assert_eq!(config.get_string("api-key").unwrap(), "cli-key");
|
||||
assert_eq!(config.get_string("port").unwrap(), "9999");
|
||||
assert_eq!(config.get_int("port").unwrap(), 9999);
|
||||
|
||||
// Check fallback to file
|
||||
assert_eq!(config.get_string("model").unwrap(), "file-model");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_config_file_loads_all_settings() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
api-key = \"test-api-key-123\"
|
||||
base-url = \"https://api.test.com\"
|
||||
chroot-dir = \"/test/chroot\"
|
||||
command-path = \"/test/commands\"
|
||||
model = \"test-model\"
|
||||
instruct = \"Test instructions\"
|
||||
server = \"test.irc.server\"
|
||||
port = 6667
|
||||
channels = [\"#test1\", \"#test2\"]
|
||||
username = \"testuser\"
|
||||
nickname = \"testnick\"
|
||||
use-tls = false
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
// Verify all settings are loaded correctly
|
||||
assert_eq!(config.get_string("api-key").unwrap(), "test-api-key-123");
|
||||
assert_eq!(
|
||||
config.get_string("base-url").unwrap(),
|
||||
"https://api.test.com"
|
||||
);
|
||||
assert_eq!(config.get_string("chroot-dir").unwrap(), "/test/chroot");
|
||||
assert_eq!(config.get_string("command-path").unwrap(), "/test/commands");
|
||||
assert_eq!(config.get_string("model").unwrap(), "test-model");
|
||||
assert_eq!(config.get_string("instruct").unwrap(), "Test instructions");
|
||||
assert_eq!(config.get_string("server").unwrap(), "test.irc.server");
|
||||
assert_eq!(config.get_int("port").unwrap(), 6667);
|
||||
|
||||
let channels: Vec<String> = config.get("channels").unwrap();
|
||||
assert_eq!(channels, vec!["#test1", "#test2"]);
|
||||
|
||||
assert_eq!(config.get_string("username").unwrap(), "testuser");
|
||||
assert_eq!(config.get_string("nickname").unwrap(), "testnick");
|
||||
assert_eq!(config.get_bool("use-tls").unwrap(), false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_config_file_partial_settings() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
// Only provide required settings
|
||||
let config_content = "\
|
||||
api-key = \"minimal-key\"
|
||||
base-url = \"https://minimal.api.com\"
|
||||
model = \"minimal-model\"
|
||||
server = \"minimal.server\"
|
||||
port = 6697
|
||||
channels = [\"#minimal\"]
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
// Verify required settings are loaded
|
||||
assert_eq!(config.get_string("api-key").unwrap(), "minimal-key");
|
||||
assert_eq!(
|
||||
config.get_string("base-url").unwrap(),
|
||||
"https://minimal.api.com"
|
||||
);
|
||||
assert_eq!(config.get_string("model").unwrap(), "minimal-model");
|
||||
|
||||
// Verify optional settings are not present
|
||||
assert!(config.get_string("chroot-dir").is_err());
|
||||
assert!(config.get_string("instruct").is_err());
|
||||
assert!(config.get_string("username").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_config_with_environment_variables() {
|
||||
// NOTE: This test documents a limitation in setup.rs
|
||||
// setup.rs uses Environment::with_prefix("BOT") without a separator
|
||||
// This means BOT_API_KEY maps to "api_key", NOT "api-key"
|
||||
// Since config.toml uses kebab-case, environment variables won't override properly
|
||||
// This is a known issue in the current implementation
|
||||
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
api_key = \"file-api-key\"
|
||||
base_url = \"https://file.api.com\"
|
||||
model = \"file-model\"
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
// Set environment variables (with BOT_ prefix as setup.rs uses)
|
||||
unsafe {
|
||||
std::env::set_var("BOT_API_KEY", "env-api-key");
|
||||
std::env::set_var("BOT_MODEL", "env-model");
|
||||
}
|
||||
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::with_name(&config_path.to_string_lossy()).required(true))
|
||||
.add_source(config::Environment::with_prefix("BOT"))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Environment variables should override file settings (when using underscore keys)
|
||||
assert_eq!(config.get_string("api_key").unwrap(), "env-api-key");
|
||||
assert_eq!(config.get_string("model").unwrap(), "env-model");
|
||||
// File setting should be used when no env var
|
||||
assert_eq!(
|
||||
config.get_string("base_url").unwrap(),
|
||||
"https://file.api.com"
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
unsafe {
|
||||
std::env::remove_var("BOT_API_KEY");
|
||||
std::env::remove_var("BOT_MODEL");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_command_line_overrides_config_file() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
api-key = \"file-api-key\"
|
||||
base-url = \"https://file.api.com\"
|
||||
model = \"file-model\"
|
||||
server = \"file.server\"
|
||||
port = 6667
|
||||
channels = [\"#file\"]
|
||||
nickname = \"filenick\"
|
||||
username = \"fileuser\"
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
// Simulate command-line arguments overriding config file
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::with_name(&config_path.to_string_lossy()).required(true))
|
||||
.set_override_option("api-key", Some("cli-api-key".to_string()))
|
||||
.unwrap()
|
||||
.set_override_option("model", Some("cli-model".to_string()))
|
||||
.unwrap()
|
||||
.set_override_option("server", Some("cli.server".to_string()))
|
||||
.unwrap()
|
||||
.set_override_option("nickname", Some("clinick".to_string()))
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Command-line values should override file settings
|
||||
assert_eq!(config.get_string("api-key").unwrap(), "cli-api-key");
|
||||
assert_eq!(config.get_string("model").unwrap(), "cli-model");
|
||||
assert_eq!(config.get_string("server").unwrap(), "cli.server");
|
||||
assert_eq!(config.get_string("nickname").unwrap(), "clinick");
|
||||
|
||||
// Non-overridden values should come from file
|
||||
assert_eq!(
|
||||
config.get_string("base-url").unwrap(),
|
||||
"https://file.api.com"
|
||||
);
|
||||
assert_eq!(config.get_string("username").unwrap(), "fileuser");
|
||||
assert_eq!(config.get_int("port").unwrap(), 6667);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_command_line_overrides_environment_and_file() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
api_key = \"file-api-key\"
|
||||
model = \"file-model\"
|
||||
base_url = \"https://file.api.com\"
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
// Set environment variable
|
||||
unsafe {
|
||||
std::env::set_var("BOT_API_KEY", "env-api-key");
|
||||
}
|
||||
|
||||
// Build config with all three sources
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::with_name(&config_path.to_string_lossy()).required(true))
|
||||
.add_source(config::Environment::with_prefix("BOT"))
|
||||
.set_override_option("api_key", Some("cli-api-key".to_string()))
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Command-line should win over both environment and file
|
||||
assert_eq!(config.get_string("api_key").unwrap(), "cli-api-key");
|
||||
|
||||
// Cleanup
|
||||
unsafe {
|
||||
std::env::remove_var("BOT_API_KEY");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_precedence_order() {
|
||||
// Test: CLI > Environment > Config File > Defaults
|
||||
// Using underscore keys to match how setup.rs actually works
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
api_key = \"file-key\"
|
||||
base_url = \"https://file-url.com\"
|
||||
model = \"file-model\"
|
||||
server = \"file-server\"
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
// Set environment variables
|
||||
unsafe {
|
||||
std::env::set_var("BOT_BASE_URL", "https://env-url.com");
|
||||
std::env::set_var("BOT_MODEL", "env-model");
|
||||
}
|
||||
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::with_name(&config_path.to_string_lossy()).required(true))
|
||||
.add_source(config::Environment::with_prefix("BOT"))
|
||||
.set_override_option("model", Some("cli-model".to_string()))
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// CLI overrides everything
|
||||
assert_eq!(config.get_string("model").unwrap(), "cli-model");
|
||||
|
||||
// Environment overrides file
|
||||
assert_eq!(
|
||||
config.get_string("base_url").unwrap(),
|
||||
"https://env-url.com"
|
||||
);
|
||||
|
||||
// File is used when no env or CLI
|
||||
assert_eq!(config.get_string("api_key").unwrap(), "file-key");
|
||||
assert_eq!(config.get_string("server").unwrap(), "file-server");
|
||||
|
||||
// Cleanup
|
||||
unsafe {
|
||||
std::env::remove_var("BOT_BASE_URL");
|
||||
std::env::remove_var("BOT_MODEL");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_boolean_use_tls_setting() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
// Test with use-tls = true (kebab-case as in config.toml)
|
||||
let config_content_true = r#"
|
||||
use-tls = true
|
||||
"#;
|
||||
let config_path = create_config_file(&temp, config_content_true);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
assert_eq!(config.get_bool("use-tls").unwrap(), true);
|
||||
|
||||
// Test with use-tls = false
|
||||
let config_content_false = r#"
|
||||
use-tls = false
|
||||
"#;
|
||||
let config_path = create_config_file(&temp, config_content_false);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
assert_eq!(config.get_bool("use-tls").unwrap(), false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_use_tls_naming_inconsistency() {
|
||||
// This test documents a bug: setup.rs uses "use_tls" (underscore)
|
||||
// but config.toml uses "use-tls" (kebab-case)
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = r#"
|
||||
use-tls = true
|
||||
"#;
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
// Build config the way setup.rs does it
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::with_name(&config_path.to_string_lossy()).required(true))
|
||||
// setup.rs line 119 uses "use_tls" (underscore) instead of "use-tls" (kebab)
|
||||
.set_override_option("use_tls", Some(false))
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// This should read from the override (false), not the file (true)
|
||||
// But due to the naming mismatch, it might not work as expected
|
||||
// The config file uses "use-tls" but the override uses "use_tls"
|
||||
|
||||
// With kebab-case (matches config.toml)
|
||||
assert_eq!(config.get_bool("use-tls").unwrap(), true);
|
||||
|
||||
// With underscore (matches setup.rs override)
|
||||
assert_eq!(config.get_bool("use_tls").unwrap(), false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_channels_as_array() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
channels = [\"#chan1\", \"#chan2\", \"#chan3\"]
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
let channels: Vec<String> = config.get("channels").unwrap();
|
||||
assert_eq!(channels.len(), 3);
|
||||
assert_eq!(channels[0], "#chan1");
|
||||
assert_eq!(channels[1], "#chan2");
|
||||
assert_eq!(channels[2], "#chan3");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_channels_override_from_cli() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
channels = [\"#file1\", \"#file2\"]
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
let cli_channels = vec![
|
||||
"#cli1".to_string(),
|
||||
"#cli2".to_string(),
|
||||
"#cli3".to_string(),
|
||||
];
|
||||
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::with_name(&config_path.to_string_lossy()).required(true))
|
||||
.set_override_option("channels", Some(cli_channels.clone()))
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let channels: Vec<String> = config.get("channels").unwrap();
|
||||
assert_eq!(channels, cli_channels);
|
||||
assert_eq!(channels.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_port_as_integer() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = r#"
|
||||
port = 6697
|
||||
"#;
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
// Port should be readable as both integer and string
|
||||
assert_eq!(config.get_int("port").unwrap(), 6697);
|
||||
assert_eq!(config.get_string("port").unwrap(), "6697");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_port_override_from_cli_as_string() {
|
||||
// setup.rs passes port as Option<String> from clap
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = r#"
|
||||
port = 6667
|
||||
"#;
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::with_name(&config_path.to_string_lossy()).required(true))
|
||||
.set_override_option("port", Some("9999".to_string()))
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// CLI override should work
|
||||
assert_eq!(config.get_string("port").unwrap(), "9999");
|
||||
assert_eq!(config.get_int("port").unwrap(), 9999);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_missing_required_fields_fails() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
// Create config without required api-key
|
||||
let config_content = r#"
|
||||
model = "test-model"
|
||||
"#;
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
// Should fail when trying to get required field
|
||||
assert!(config.get_string("api-key").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_optional_instruct_field() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = r#"
|
||||
instruct = "Custom bot instructions"
|
||||
"#;
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
assert_eq!(
|
||||
config.get_string("instruct").unwrap(),
|
||||
"Custom bot instructions"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_command_path_field() {
|
||||
// command-path is in config.toml but not used anywhere in the code
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = r#"
|
||||
command-path = "/custom/commands"
|
||||
"#;
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
assert_eq!(
|
||||
config.get_string("command-path").unwrap(),
|
||||
"/custom/commands"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chroot_dir_field() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = r#"
|
||||
chroot-dir = "/var/lib/bot/root"
|
||||
"#;
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
assert_eq!(
|
||||
config.get_string("chroot-dir").unwrap(),
|
||||
"/var/lib/bot/root"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_config_file() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
|
||||
// Should build successfully but have no values
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
assert!(config.get_string("api-key").is_err());
|
||||
assert!(config.get_string("model").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_all_cli_override_keys_match_config_format() {
|
||||
// This test documents which override keys in setup.rs match the config.toml format
|
||||
let temp = TempDir::new().unwrap();
|
||||
let config_content = "\
|
||||
api-key = \"test\"
|
||||
base-url = \"https://test.com\"
|
||||
chroot-dir = \"/test\"
|
||||
command-path = \"/cmds\"
|
||||
model = \"test-model\"
|
||||
instruct = \"test\"
|
||||
channels = [\"#test\"]
|
||||
server = \"test.server\"
|
||||
port = 6697
|
||||
nickname = \"test\"
|
||||
username = \"test\"
|
||||
use-tls = true
|
||||
";
|
||||
|
||||
let config_path = create_config_file(&temp, config_content);
|
||||
let config = parse_config_from_file(&config_path).await;
|
||||
|
||||
// All these should work with kebab-case (as in config.toml)
|
||||
assert!(config.get_string("api-key").is_ok());
|
||||
assert!(config.get_string("base-url").is_ok());
|
||||
assert!(config.get_string("chroot-dir").is_ok());
|
||||
assert!(config.get_string("command-path").is_ok());
|
||||
assert!(config.get_string("model").is_ok());
|
||||
assert!(config.get_string("instruct").is_ok());
|
||||
let channels_result: Result<Vec<String>, _> = config.get("channels");
|
||||
assert!(channels_result.is_ok());
|
||||
assert!(config.get_string("server").is_ok());
|
||||
assert!(config.get_int("port").is_ok());
|
||||
assert!(config.get_string("nickname").is_ok());
|
||||
assert!(config.get_string("username").is_ok());
|
||||
assert!(config.get_bool("use-tls").is_ok());
|
||||
}
|
||||
Reference in New Issue
Block a user