diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3ebc1fe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "jpLsp.applyAllLanguages": false, + "jpLsp.semantic.enable": false +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e431da5..b2634a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,209 +3,33 @@ version = 4 [[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.9.1", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64 0.22.1", - "bitflags 2.9.1", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2 0.3.26", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.1", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.101", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.9", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] -name = "actix-web" -version = "4.11.0" +name = "ahash" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", + "getrandom 0.3.4", "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.9", - "time 0.3.41", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.101", + "version_check", + "zerocopy", ] -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "aligned-vec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" - [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -232,9 +56,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -247,61 +71,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arrayvec" @@ -313,62 +120,57 @@ dependencies = [ ] [[package]] -name = "async-trait" -version = "0.1.88" +name = "async-compression" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "av1-grain" -version = "0.2.4" +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "avif-serialize" -version = "0.8.3" +name = "async-tungstenite" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "b6f89c129ab749940f95509d84950c62092c8b4bc6e386ddb162229037a6ec91" dependencies = [ - "arrayvec", + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tokio", + "tungstenite 0.28.0", ] [[package]] -name = "base64" -version = "0.13.1" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "base64" -version = "0.21.7" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -376,29 +178,11 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - [[package]] name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" - -[[package]] -name = "bitstream-io" -version = "2.6.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -411,9 +195,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -430,23 +214,17 @@ dependencies = [ "alloc-stdlib", ] -[[package]] -name = "built" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" - [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] -name = "bytemuck" -version = "1.23.0" +name = "bytecount" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "byteorder" @@ -455,85 +233,129 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "byteorder-lite" -version = "0.1.0" +name = "bytes" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] [[package]] -name = "bytes" -version = "1.10.1" +name = "camino" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] [[package]] -name = "bytestring" -version = "1.4.0" +name = "cargo-platform" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ - "bytes", + "serde", ] [[package]] -name = "bzip2" -version = "0.4.4" +name = "cargo_metadata" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ - "bzip2-sys", - "libc", + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", ] [[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" +name = "cc" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ - "cc", - "pkg-config", + "find-msvc-tools", + "shlex", ] [[package]] -name = "call-agent" -version = "1.5.4" +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81b74cde3bd61dc6e6766f52f18f8971cd997cc7929fdbdbe45a2cd638879a1" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chromiumoxide" +version = "0.8.0" +source = "git+https://github.com/mattsse/chromiumoxide?branch=main#c671c3beaa3a1a3c689409728f2afc72a0adc7b3" dependencies = [ - "log", - "reqwest 0.12.24", + "async-tungstenite", + "base64", + "bytes", + "cfg-if", + "chromiumoxide_cdp", + "chromiumoxide_types", + "dunce", + "fnv", + "futures", + "futures-timer", + "pin-project-lite", + "reqwest", "serde", "serde_json", + "thiserror 1.0.69", "tokio", + "tracing", + "url", + "which", + "windows-registry", ] [[package]] -name = "cc" -version = "1.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +name = "chromiumoxide_cdp" +version = "0.8.0" +source = "git+https://github.com/mattsse/chromiumoxide?branch=main#c671c3beaa3a1a3c689409728f2afc72a0adc7b3" dependencies = [ - "jobserver", - "libc", - "shlex", + "chromiumoxide_pdl", + "chromiumoxide_types", + "serde", + "serde_json", ] [[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +name = "chromiumoxide_pdl" +version = "0.8.0" +source = "git+https://github.com/mattsse/chromiumoxide?branch=main#c671c3beaa3a1a3c689409728f2afc72a0adc7b3" dependencies = [ - "smallvec", - "target-lexicon", + "chromiumoxide_types", + "either", + "heck", + "once_cell", + "proc-macro2", + "quote", + "regex", + "serde", + "serde_json", ] [[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +name = "chromiumoxide_types" +version = "0.8.0" +source = "git+https://github.com/mattsse/chromiumoxide?branch=main#c671c3beaa3a1a3c689409728f2afc72a0adc7b3" +dependencies = [ + "serde", + "serde_json", +] [[package]] name = "chrono" @@ -559,38 +381,27 @@ dependencies = [ "phf 0.12.1", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "cookie" -version = "0.16.2" +name = "compression-codecs" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" dependencies = [ - "percent-encoding", - "time 0.3.41", - "version_check", + "brotli", + "compression-core", ] [[package]] -name = "core-foundation" -version = "0.9.4" +name = "compression-core" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" [[package]] name = "core-foundation-sys" @@ -609,39 +420,27 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] -name = "cron" -version = "0.15.0" +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "chrono", - "once_cell", - "winnow 0.6.26", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", "crossbeam-utils", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.18" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] @@ -652,17 +451,11 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" - [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -688,14 +481,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.111", ] [[package]] name = "darling" -version = "0.13.4" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -703,27 +496,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.13.4" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] name = "darling_macro" -version = "0.13.4" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] @@ -762,63 +555,85 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] -name = "derive_more" -version = "2.0.1" +name = "derivative" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "derive_more-impl", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "derive_more-impl" -version = "2.0.1" +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ + "darling", "proc-macro2", "quote", - "syn 2.0.101", - "unicode-xid", + "syn 2.0.111", ] [[package]] -name = "digest" -version = "0.10.7" +name = "derive_builder_macro" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ - "block-buffer", - "crypto-common", + "derive_builder_core", + "syn 2.0.111", ] [[package]] -name = "dirs" -version = "3.0.2" +name = "derive_more" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ - "dirs-sys", + "derive_more-impl", ] [[package]] -name = "dirs-sys" -version = "0.3.7" +name = "derive_more-impl" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "libc", - "redox_users", - "winapi", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.111", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -829,14 +644,20 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -847,6 +668,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ego-tree" version = "0.10.0" @@ -859,25 +686,22 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "env_logger" version = "0.11.8" @@ -899,69 +723,51 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "exr" -version = "1.73.0" +name = "error-chain" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", + "version_check", ] [[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fax" -version = "0.2.6" +name = "eventsource-stream" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" dependencies = [ - "fax_derive", + "futures-core", + "nom", + "pin-project-lite", ] [[package]] -name = "fax_derive" -version = "0.2.0" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "fdeflate" -version = "0.3.7" +name = "find-msvc-tools" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -973,32 +779,11 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1069,7 +854,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", ] [[package]] @@ -1084,6 +869,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -1137,79 +928,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", -] - -[[package]] -name = "gif" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.3.1", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "wasip2", + "wasm-bindgen", ] [[package]] -name = "half" -version = "2.6.0" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" -dependencies = [ - "cfg-if", - "crunchy", -] +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hashbrown" @@ -1219,15 +962,15 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" -version = "0.5.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "html5ever" @@ -1241,38 +984,26 @@ dependencies = [ ] [[package]] -name = "http" -version = "0.2.12" +name = "html_format" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "2f3cf0846581e28d4e615b6cf3758502d9ec3da45e35037b2d5640d530a86a4b" dependencies = [ - "bytes", - "fnv", - "itoa", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1280,7 +1011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -1291,8 +1022,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1302,51 +1033,22 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" -version = "0.14.32" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.9", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.10", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1354,93 +1056,50 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", - "hyper 1.6.0", + "http", + "hyper", "hyper-util", - "rustls 0.23.27", + "rustls 0.23.35", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", + "tokio-rustls 0.26.4", "tower-service", + "webpki-roots 1.0.4", ] [[package]] name = "hyper-util" -version = "0.1.12" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.6.0", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.9", - "system-configuration 0.6.1", + "socket2", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1462,9 +1121,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1475,9 +1134,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1488,11 +1147,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1503,42 +1161,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1554,9 +1208,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1573,71 +1227,14 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "image" -version = "0.25.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imgref" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" - -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.3", -] - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "hashbrown 0.16.1", ] [[package]] @@ -1648,9 +1245,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1658,175 +1255,111 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - -[[package]] -name = "libc" -version = "0.2.172" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] [[package]] -name = "libfuzzer-sys" -version = "0.4.9" +name = "jiff-static" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" dependencies = [ - "arbitrary", - "cc", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "libredox" -version = "0.1.3" +name = "js-sys" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ - "bitflags 2.9.1", - "libc", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "linux-raw-sys" -version = "0.9.4" +name = "kurosabi" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "b82042d457c8d85735f6ab6b88917d15fce153e593c5a714a3ecb74c580a879a" +dependencies = [ + "ahash", + "async-compression", + "async-trait", + "brotli", + "crossbeam-queue", + "futures", + "html_format", + "log", + "mime_guess", + "serde", + "serde_json", + "smallvec", + "socket2", + "tokio", + "tokio-util", +] [[package]] -name = "litemap" -version = "0.8.0" +name = "libc" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] -name = "local-channel" -version = "0.1.5" +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] -name = "local-waker" -version = "0.1.4" +name = "litemap" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "loop9" -version = "0.1.5" +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "mac" @@ -1853,24 +1386,14 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", + "syn 2.0.111", ] [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -1888,6 +1411,21 @@ dependencies = [ "unicase", ] +[[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap 5.5.3", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1896,9 +1434,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -1906,41 +1444,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "moxcms" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "native-tls" -version = "0.2.14" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1959,59 +1469,12 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2023,28 +1486,26 @@ dependencies = [ [[package]] name = "observer" -version = "0.1.0" +version = "0.2.0" dependencies = [ - "actix-web", - "base64 0.22.1", - "call-agent", + "anyhow", + "async-trait", "chrono", "chrono-tz", - "cron", "dashmap 6.1.0", + "dotenv", "env_logger", - "image", - "lazy_static", + "kurosabi", "log", - "playwright", - "regex", - "reqwest 0.12.24", - "scraper", + "openai_dive", + "poise", + "reqwest", "serde", "serde_json", "serenity", "tokio", - "urlencoding", + "tracing", + "wk-371tti-net-crawler", ] [[package]] @@ -2055,59 +1516,31 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "openssl" -version = "0.10.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-sys" -version = "0.9.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +name = "openai_dive" +version = "1.3.3" dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", + "bytes", + "derive_builder", + "futures", + "reqwest", + "reqwest-eventsource", + "serde", + "serde_html_form", + "serde_json", + "tokio", + "tokio-stream", ] [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2115,28 +1548,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" @@ -2187,7 +1614,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", ] [[package]] @@ -2221,53 +1648,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "playwright" -version = "0.0.20" +name = "poise" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45631bc049f37d0cef950da0d60ee35b211ab71180c61aee86880483cf59e28" +checksum = "1819d5a45e3590ef33754abce46432570c54a120798bdbf893112b4211fa09a6" dependencies = [ - "base64 0.13.1", - "chrono", - "dirs", - "futures", - "itertools 0.10.5", - "log", - "paste", - "reqwest 0.11.27", - "serde", - "serde_json", - "serde_with", - "strong", - "thiserror", + "async-trait", + "derivative", + "futures-util", + "parking_lot", + "poise_macros", + "regex", + "serenity", "tokio", - "tokio-stream", - "zip", + "tracing", ] [[package]] -name = "png" -version = "0.18.0" +name = "poise_macros" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "8fa2c123c961e78315cd3deac7663177f12be4460f5440dbf62a7ed37b1effea" dependencies = [ - "bitflags 2.9.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", + "darling", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -2280,9 +1693,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -2310,70 +1723,93 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] [[package]] -name = "profiling" -version = "1.0.16" +name = "pulldown-cmark" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "profiling-procmacros", + "bitflags", + "memchr", + "unicase", ] [[package]] -name = "profiling-procmacros" -version = "1.0.16" +name = "quinn" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "quote", - "syn 2.0.101", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.35", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", ] [[package]] -name = "pxfm" -version = "0.1.25" +name = "quinn-proto" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ - "num-traits", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.35", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", ] [[package]] -name = "qoi" -version = "0.4.1" +name = "quinn-udp" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "bytemuck", + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", ] -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -2388,9 +1824,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -2398,130 +1834,49 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "rav1e" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" -dependencies = [ - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.12.1", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "once_cell", - "paste", - "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", - "simd_helpers", - "system-deps", - "thiserror", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.11.12" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] -name = "rayon" -version = "1.10.0" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "either", - "rayon-core", + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] -name = "rayon-core" -version = "1.12.1" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "getrandom 0.2.16", ] [[package]] -name = "redox_syscall" -version = "0.5.12" +name = "rand_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "bitflags 2.9.1", + "getrandom 0.3.4", ] [[package]] -name = "redox_users" -version = "0.4.6" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror", + "bitflags", ] [[package]] @@ -2547,111 +1902,70 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.21.7", + "base64", "bytes", - "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "hyper-tls 0.5.0", - "ipnet", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", "js-sys", "log", - "mime", "mime_guess", - "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.12", - "rustls-pemfile", + "quinn", + "rustls 0.23.35", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", + "sync_wrapper", "tokio", - "tokio-native-tls", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.4", "tokio-util", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.25.4", - "winreg", + "webpki-roots 1.0.4", ] [[package]] -name = "reqwest" -version = "0.12.24" +name = "reqwest-eventsource" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", + "eventsource-stream", "futures-core", - "h2 0.4.10", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-rustls 0.27.6", - "hyper-tls 0.6.0", - "hyper-util", - "js-sys", - "log", + "futures-timer", "mime", - "native-tls", - "percent-encoding", + "nom", "pin-project-lite", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 1.0.2", - "tokio", - "tokio-native-tls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", + "reqwest", + "thiserror 1.0.69", ] -[[package]] -name = "rgb" -version = "0.8.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" - [[package]] name = "ring" version = "0.17.14" @@ -2667,28 +1981,31 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "1.0.7" +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 2.9.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", + "semver", ] [[package]] -name = "rustls" -version = "0.21.12" +name = "rustix" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2707,45 +2024,28 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", + "ring", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ + "web-time", "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.102.8" @@ -2759,9 +2059,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -2770,23 +2070,23 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] -name = "schannel" -version = "0.1.27" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "windows-sys 0.59.0", + "winapi-util", ] [[package]] @@ -2810,16 +2110,6 @@ dependencies = [ "tendril", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "secrecy" version = "0.8.0" @@ -2830,36 +2120,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.1", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "selectors" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5685b6ae43bfcf7d2e7dfcfb5d8e8f61b46442c902531e41a32a9a8bf0ee0fb6" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cssparser", "derive_more", "fxhash", @@ -2872,6 +2139,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde" version = "1.0.228" @@ -2908,29 +2185,33 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", ] [[package]] -name = "serde_json" -version = "1.0.145" +name = "serde_html_form" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ + "form_urlencoded", + "indexmap", "itoa", - "memchr", "ryu", - "serde", "serde_core", ] [[package]] -name = "serde_spanned" -version = "0.6.8" +name = "serde_json" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ + "itoa", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] @@ -2945,64 +2226,44 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "serenity" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d72ec4323681bf9a3cabe40fd080abc2435859b502a1b5aa9bf693f125bfa76" +checksum = "9bde37f42765dfdc34e2a039e0c84afbf79a3101c1941763b0beb816c2f17541" dependencies = [ "arrayvec", "async-trait", - "base64 0.22.1", - "bitflags 2.9.1", + "base64", + "bitflags", "bytes", + "chrono", "dashmap 5.5.3", "flate2", "futures", - "fxhash", "mime_guess", "parking_lot", "percent-encoding", - "reqwest 0.11.27", + "reqwest", + "rustc-hash", "secrecy", "serde", "serde_cow", "serde_json", - "time 0.3.41", + "time", "tokio", "tokio-tungstenite", "tracing", "typemap_rev", + "typesize", "url", ] [[package]] name = "servo_arc" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ "stable_deref_trait", ] @@ -3026,27 +2287,19 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "simd_helpers" -version = "0.1.0" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "siphasher" @@ -3055,45 +2308,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] -name = "slab" -version = "0.4.9" +name = "skeptic" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" dependencies = [ - "autocfg", + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", ] [[package]] -name = "smallvec" -version = "1.15.0" +name = "slab" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] -name = "socket2" -version = "0.5.9" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" @@ -3120,20 +2375,11 @@ dependencies = [ "quote", ] -[[package]] -name = "strong" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe0fc7652d95bcd84f61cd036181b395f329ef45b25169b69a42f72cb6975f" -dependencies = [ - "serde", -] - [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" @@ -3154,21 +2400,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3186,81 +2426,26 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys 0.5.0", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.1", - "core-foundation", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", + "syn 2.0.111", ] [[package]] -name = "target-lexicon" -version = "0.12.16" +name = "tagptr" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3280,50 +2465,45 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", ] [[package]] -name = "thiserror-impl" -version = "1.0.69" +name = "thiserror" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "thiserror-impl 2.0.17", ] [[package]] -name = "tiff" -version = "0.10.3" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "time" -version = "0.1.45" +name = "thiserror-impl" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3336,15 +2516,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3352,14 +2532,29 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -3372,7 +2567,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3385,27 +2580,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", + "syn 2.0.111", ] [[package]] @@ -3421,11 +2596,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.27", + "rustls 0.23.35", "tokio", ] @@ -3438,7 +2613,6 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util", ] [[package]] @@ -3453,15 +2627,15 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", - "tungstenite", + "tungstenite 0.21.0", "webpki-roots 0.26.11", ] [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -3470,40 +2644,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.7.10", -] - [[package]] name = "tower" version = "0.5.2" @@ -3513,7 +2653,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -3521,15 +2661,15 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.1", + "bitflags", "bytes", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "iri-string", "pin-project-lite", "tower", @@ -3551,9 +2691,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3563,24 +2703,30 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" + [[package]] name = "try-lock" version = "0.2.5" @@ -3596,18 +2742,35 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.3.1", + "http", "httparse", "log", "rand 0.8.5", "rustls 0.22.4", "rustls-pki-types", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + [[package]] name = "typemap_rev" version = "0.3.0" @@ -3616,9 +2779,38 @@ checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "typesize" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da66c62c5b7017a2787e77373c03e6a5aafde77a73bff1ff96e91cd2e128179" +dependencies = [ + "chrono", + "dashmap 5.5.3", + "hashbrown 0.14.5", + "mini-moka", + "parking_lot", + "secrecy", + "serde_json", + "time", + "typesize-derive", + "url", +] + +[[package]] +name = "typesize-derive" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "536b6812192bda8551cfa0e52524e328c6a951b48e66529ee4522d6c721243d6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] name = "unicase" @@ -3628,9 +2820,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" @@ -3638,12 +2830,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" @@ -3652,9 +2838,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -3686,35 +2872,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "v_frame" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3726,56 +2899,37 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.101", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -3786,9 +2940,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3796,22 +2950,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.101", - "wasm-bindgen-backend", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -3831,9 +2985,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -3851,98 +3015,84 @@ dependencies = [ "string_cache_codegen", ] -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "webpki-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.0", + "webpki-roots 1.0.4", ] [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] [[package]] -name = "weezl" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" - -[[package]] -name = "winapi" -version = "0.3.9" +name = "which" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "env_home", + "rustix", + "winsafe", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.1", - "windows-result", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" @@ -3952,13 +3102,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-result", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -3967,16 +3117,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] -name = "windows-strings" -version = "0.3.1" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.2.1", ] [[package]] @@ -3985,16 +3135,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.48.5", + "windows-link 0.2.1", ] [[package]] @@ -4008,11 +3158,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.5", ] [[package]] @@ -4024,21 +3174,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4057,26 +3192,21 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4085,15 +3215,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -4103,15 +3227,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -4121,9 +3239,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -4133,15 +3251,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -4151,15 +3263,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -4169,15 +3275,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -4187,15 +3287,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -4205,60 +3299,51 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.6.26" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "winnow" -version = "0.7.10" +name = "winsafe" +version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" -dependencies = [ - "memchr", -] +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "winreg" -version = "0.50.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +name = "wk-371tti-net-crawler" +version = "0.1.2" dependencies = [ - "bitflags 2.9.1", + "chromiumoxide", + "ego-tree", + "env_logger", + "futures", + "kurosabi", + "reqwest", + "scraper", + "serde", + "serde_json", + "tokio", + "urlencoding", ] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -4266,34 +3351,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", ] [[package]] @@ -4313,21 +3398,21 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.111", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -4336,9 +3421,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -4347,77 +3432,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "zip" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" -dependencies = [ - "byteorder", - "bzip2", - "crc32fast", - "flate2", - "thiserror", - "time 0.1.45", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", + "syn 2.0.111", ] [[package]] -name = "zune-jpeg" -version = "0.4.21" +name = "zmij" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core", -] +checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" diff --git a/Cargo.toml b/Cargo.toml index 20e97cb..eaaf1cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,32 +1,44 @@ [package] name = "observer" -version = "0.1.0" +version = "0.2.0" edition = "2024" [dependencies] -reqwest = { version = "0.12.24", features = ["json"] } -tokio = { version = "1.48.0", features = ["io-std", "macros", "rt-multi-thread", "time"] } -serde = { version = "1.0.228", features = ["derive"] } +env_logger = "0.11.8" +kurosabi = "0.5.4" +tokio = { version = "1.48.0", features = ["full"] } +reqwest = { version = "0.12.28", default-features = false, features = ["json", "rustls-tls"] } +openai_dive = { version = "1.3.3", default-features = false, features = ["stream", "rustls-tls"] } + +async-trait = "0.1.89" +wk-371tti-net-crawler = { path = "wk-371tti-net-crawler", default-features = false, features = ["tls-rustls"] } + + +serenity = { version = "0.12", default-features = false, features = [ + "client", + "gateway", + "rustls_backend", # native-tls じゃなく rustls を使う + "model", +] } + +# コマンドフレームワーク +poise = "0.6" +log = "0.4.28" serde_json = "1.0.145" -call-agent = "1.5.4" -chrono-tz = "0.10.4" -playwright = "0.0.20" +serde = "1.0.228" dashmap = "6.1.0" -log = "0.4.28" -env_logger = "0.11.6" -chrono = { version = "0.4.42", features = ["serde"] } -regex = "1.12.2" -cron = "0.15.0" -urlencoding = "2.1.3" -scraper = "0.24.0" -lazy_static = "1.4" -serenity = { version = "0.12.4", default-features = false, features = ["client","gateway","model","cache","rustls_backend",] } -actix-web = "4.9.0" -image = "0.25.8" -base64 = "0.22.1" +dotenv = "0.15.0" +anyhow = "1.0.100" +tracing = "0.1.41" +chrono = "0.4.42" +chrono-tz = "0.10.4" + [patch.crates-io] -# call-agent = { path = "I:/RustBuilds/call-agent" } +openai_dive = { path = "openai-client/openai_dive" } [profile.release] -opt-level = 3 \ No newline at end of file +opt-level = 3 +lto = "fat" +codegen-units = 1 + diff --git a/README.md b/README.md index 8e3f685..ad31af2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@

Observer

+# dev branch はREADMEはめちゃくちゃになります + +ローカルでビルドしてつかうときはクローラークレートにstandaloneつける Observer Discord Botは、自然な会話と高度なツール連携を提供する多機能なAIチャットボットです。このボットは、OpenAIのGPTモデルをベースにしており、ユーザーとの対話を通じて様々なタスクを実行します。 @@ -26,8 +29,8 @@ Observer Discord Botは、自然な会話と高度なツール連携を提供す - [x] システムメッセージの改善 - 最適化 2 - [x] web検索機能強化 - - [ ] 画像読み込みの最適化 - - [ ] 突然discordから切断される問題の解決 + - [x] 画像読み込みの最適化 + - [x] 突然discordから切断される問題の解決 ## 主な機能 diff --git a/data/latex_render.html b/data/latex_render.html new file mode 100644 index 0000000..8a4944b --- /dev/null +++ b/data/latex_render.html @@ -0,0 +1,83 @@ + + + + + math + + + + + +
+
+
+ + + + diff --git a/openai-client b/openai-client new file mode 160000 index 0000000..afb4290 --- /dev/null +++ b/openai-client @@ -0,0 +1 @@ +Subproject commit afb42903fb27da800098e80a089f6e47a6a8d39c diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..83082c8 --- /dev/null +++ b/src/README.md @@ -0,0 +1 @@ +context.rsがぜんぶまとめてるかんじ \ No newline at end of file diff --git a/src/agent.rs b/src/agent.rs deleted file mode 100644 index 9ad9728..0000000 --- a/src/agent.rs +++ /dev/null @@ -1,353 +0,0 @@ -use std::{collections::HashMap, sync::Arc, u64}; - -use call_agent::chat::{client::{ModelConfig, OpenAIClient, OpenAIClientState, ToolMode}, prompt::{Message, MessageContext, MessageImage}}; -use log::{debug, info}; -use observer::prefix::{ASK_DEVELOPER_PROMPT, ASSISTANT_NAME, MAX_USE_TOOL_COUNT, MODEL_GENERATE_MAX_TOKENS, MODEL_NAME}; -use regex::Regex; -use serenity::all::{Context, CreateMessage, MessageFlags}; -use tokio::sync::Mutex; - -use crate::fetch_and_encode_images; - -pub const PROMPT_ENTRY_LIMIT: u64 = 48; // プロンプトのエントリ数の上限 - -#[derive(Clone, Debug)] -pub struct InputMessage { - pub content: String, - pub name: String, - pub message_id: String, - pub reply_msg: Option, - pub user_id: String, - pub attached_files: Vec, -} -// 各チャンネルの会話履歴(state)を保持する構造体 -pub struct ChannelState { - // 並列処理のため、prompt_stream を Mutex で保護する - pub prompt_stream: Mutex, -} - -/// モデルの種類を指定するための列挙型 -#[derive(Clone)] -pub enum AIModel { - MO4Mini, - MO3, - M5Nano, - M5Mini, - M5, -} - -impl AIModel { - pub fn to_model_name(&self) -> String { - match self { - AIModel::MO4Mini => "o4-mini".to_string(), - AIModel::MO3 => "o3".to_string(), - AIModel::M5Nano => "gpt-5-nano".to_string(), - AIModel::M5Mini => "gpt-5-mini".to_string(), - AIModel::M5 => "gpt-5".to_string(), - } - } - - pub fn to_model_discription(&self) -> String { - match self { - AIModel::MO4Mini => "o4-mini: late=10 4いつもの 数学とコーディングに強い".to_string(), - AIModel::MO3 => "o3: late=20 推論".to_string(), - AIModel::M5Nano => "gpt-5-nano: late=2 超高速応答".to_string(), - AIModel::M5Mini => "gpt-5-mini: late=5 高速応答".to_string(), - AIModel::M5 => "gpt-5: late=20 一般".to_string(), - } - } - - pub fn to_sec_per_rate(&self) -> usize { - match self { - AIModel::MO4Mini => 10, - AIModel::MO3 => 20, - AIModel::M5Nano => 2, - AIModel::M5Mini => 5, - AIModel::M5 => 20, - } - } - - pub fn from_model_name(model_name: &str) -> Result { - match model_name { - // "o3" => Ok(AIModel::MO3), - "o4-mini" => Ok(AIModel::MO4Mini), - "o3" => Ok(AIModel::MO3), - "gpt-5-nano" => Ok(AIModel::M5Nano), - "gpt-5-mini" => Ok(AIModel::M5Mini), - "gpt-5" => Ok(AIModel::M5), - _ => Err(format!("Unknown model name: {}", model_name)), - } - } - - pub fn to_model_config(&self) -> ModelConfig { - match self { - // AIModel::MO3 => ModelConfig { - // model: "o3".to_string(), - // model_name: Some("observer".to_string()), - // top_p: todo!(), - // parallel_tool_calls: todo!(), - // temperature: todo!(), - // max_completion_tokens: todo!(), - // reasoning_effort: todo!(), - // presence_penalty: todo!(), - // strict: todo!(), - // }, - AIModel::MO4Mini => ModelConfig { - model: "o4-mini".to_string(), - model_name: Some(ASSISTANT_NAME.to_string()), - parallel_tool_calls: None, - temperature: None, - max_completion_tokens: Some(*MODEL_GENERATE_MAX_TOKENS as u64), - reasoning_effort: Some("low".to_string()), - presence_penalty: None, - strict: Some(false), - top_p: Some(1.0), - web_search_options: None, - }, - AIModel::MO3 => ModelConfig { - model: "o3".to_string(), - model_name: Some(ASSISTANT_NAME.to_string()), - parallel_tool_calls: None, - temperature: None, - max_completion_tokens: Some(*MODEL_GENERATE_MAX_TOKENS as u64), - reasoning_effort: Some("low".to_string()), - presence_penalty: None, - strict: Some(false), - top_p: Some(1.0), - web_search_options: None, - }, - AIModel::M5Nano => ModelConfig { - model: "gpt-5-nano".to_string(), - model_name: Some(ASSISTANT_NAME.to_string()), - parallel_tool_calls: Some(true), - temperature: None, - max_completion_tokens: Some(*MODEL_GENERATE_MAX_TOKENS as u64), - reasoning_effort: Some("low".to_string()), - presence_penalty: None, - strict: Some(false), - top_p: Some(1.0), - web_search_options: None, - }, - AIModel::M5Mini => ModelConfig { - model: "gpt-5-mini".to_string(), - model_name: Some(ASSISTANT_NAME.to_string()), - parallel_tool_calls: Some(true), - temperature: None, - max_completion_tokens: Some(*MODEL_GENERATE_MAX_TOKENS as u64), - reasoning_effort: Some("low".to_string()), - presence_penalty: None, - strict: Some(false), - top_p: Some(1.0), - web_search_options: None, - }, - AIModel::M5 => ModelConfig { - model: "gpt-5".to_string(), - model_name: Some(ASSISTANT_NAME.to_string()), - parallel_tool_calls: Some(true), - temperature: None, - max_completion_tokens: Some(*MODEL_GENERATE_MAX_TOKENS as u64), - reasoning_effort: Some("low".to_string()), - presence_penalty: None, - strict: Some(false), - top_p: Some(1.0), - web_search_options: None, - }, - } - } -} - -impl Default for AIModel { - fn default() -> Self { - AIModel::from_model_name(*MODEL_NAME) - .unwrap_or_else(|_| AIModel::MO4Mini) // デフォルトは o4-mini - } -} - -impl ChannelState { - pub async fn new(client: &Arc) -> Self { - // 新しい PromptStream を生成する - let mut prompt_stream = client.create_prompt(); - prompt_stream.set_entry_limit(PROMPT_ENTRY_LIMIT).await; - // Extend lifetime to 'static; safe because client lives for the entire duration of the program - Self { - prompt_stream: Mutex::new(prompt_stream), - } - } - - async fn prepare_user_prompt(message: &mut InputMessage, viw_image_detail: u8) -> Vec { - // スポイラーを含むメッセージの処理 - let re = Regex::new(r"(\|\|.*?\|\|)").unwrap(); - message.content = re.replace_all(&message.content, "||||").to_string(); - - // !hidetail が含まれていれば強制的に high detail - let mut detail_flag = viw_image_detail; - if message.content.contains("!hidetail") { - println!("hidetail found in message content"); - detail_flag = 255; - // 末尾/文中のフラグ文字列を削除 - message.content = message.content.replace("!hidetail", ""); - } - - let meta = format!( - "[META]msg_id:{},user_name:{},replay_msg:{};\n{}", - message.message_id, - message.name, - message.reply_msg.clone().unwrap_or_else(|| "none".into()), - message.content.clone(), - ); - - let mut content_vec = Vec::new(); - content_vec.push(MessageContext::Text(meta)); - - // detail_flag に応じて画像を追加 - if detail_flag != 0 { - // 画像を取得して data URL にした Vec - let img_urls = fetch_and_encode_images(&message.attached_files).await; - - for url in img_urls { - let detail_str = match detail_flag { - 1 => Some("low".to_string()), - 255 => Some("high".to_string()), - _ => None, - }; - content_vec.push(MessageContext::Image(MessageImage { - url, - detail: detail_str, - })); - } - } - - vec![Message::User { - content: content_vec, - name: Some(message.user_id.clone()), - }] - } - - pub async fn reasoning( - &self, - ctx: &Context, - msg: &serenity::all::Message, - mut message: InputMessage, - model: AIModel, - ) -> String { - // プロンプトストリームの取得 - let user_prompt = ChannelState::prepare_user_prompt(&mut message, 1).await; - let mut r_prompt_stream = self.prompt_stream.lock().await; - r_prompt_stream.add(user_prompt).await; - let mut prompt_stream = r_prompt_stream.clone(); - drop(r_prompt_stream); // 先にロックを解除t_stream.clone(); - prompt_stream.client.set_model_config(&model.to_model_config()); - prompt_stream.set_entry_limit(u64::MAX).await; - let last_pos = prompt_stream.prompt.len(); - - // システムプロンプトの追加 - debug!("prompt_stream - {:#?}", prompt_stream.prompt); - let system_prompt = vec![Message::Developer { - content: ASK_DEVELOPER_PROMPT.to_string(), - name: Some(ASSISTANT_NAME.to_string()), - }]; - prompt_stream.add_last(system_prompt).await; - - // 使用したツールのトラッキング - let mut used_tools = Vec::new(); - - // 推論ストリームの生成 - let mut reasoning_stream = match prompt_stream.reasoning(None, &ToolMode::Auto).await { - Ok(stream) => stream, - Err(e) => return format!("Err: failed reasoning - {:?}", e), - }; - - // 推論ループ - for i in 0..*MAX_USE_TOOL_COUNT + 1 { - // 終了できるなら終了 - if reasoning_stream.can_finish() { - break; - } - - // ツールコールの表示 - let show_tool_call: Vec<(String, serde_json::Value)> = reasoning_stream.show_tool_calls() - .into_iter() - .map(|(n,arg)| (n.to_string(), arg.clone())) - .collect(); - - info!("show_tool_call - {:#?}", show_tool_call); - for (tool_name, argument) in show_tool_call { - used_tools.push(tool_name.clone()); - if let Some(explain) = argument.get("$explain") { - let status_res = CreateMessage::new() - .content(format!("-# {}...", explain.to_string())) - .flags(MessageFlags::SUPPRESS_EMBEDS); - - if let Err(e) = msg.channel_id.send_message(&ctx.http, status_res).await { - debug!("Error sending message: {:?}", e); - } - } else { - let status_res = CreateMessage::new() - .content(format!("-# using {}...", tool_name)) - .flags(MessageFlags::SUPPRESS_EMBEDS); - - if let Err(e) = msg.channel_id.send_message(&ctx.http, status_res).await { - debug!("Error sending message: {:?}", e); - } - } - } - - // 推論の上限回数を超えた場合はツールモードを無効化 - let mode = if i == *MAX_USE_TOOL_COUNT { - ToolMode::Disable - } else { - ToolMode::Auto - }; - // 推論の実行 - match reasoning_stream.proceed(&mode).await { - Err(e) => { - return format!("Err: failed reasoning - {:?}", e); - } - Ok(_) => {}, - } - } - - // 推論結果の取得 - let content = reasoning_stream.content.unwrap_or("Err: response is none from ai".to_string()); - - // ツールコールの統計収集 - let model_info = format!("\n-# model: {}", prompt_stream.client.model_config.unwrap().model); - let mut tool_count = HashMap::new(); - for tool in used_tools { - *tool_count.entry(tool).or_insert(0) += 1; - } - let used_tools_info = if !tool_count.is_empty() { - let tools_info: Vec = tool_count.iter().map(|(tool, count)| { - if *count > 1 { - format!("{} x{}", tool, count) - } else { - tool.clone() - } - }).collect(); - format!("\n-# tools: {}", tools_info.join(", ")) - } else { - "".to_string() - }; - // プロンプトストリームに分岐した分部をマージ - let differential_stream = prompt_stream.prompt.split_off(last_pos + 1 /* 先頭のシステムプロンプト消す */); - { - let mut r_prompt_stream = self.prompt_stream.lock().await; - r_prompt_stream.add(differential_stream.into()).await; - } - return content.replace("\\n", "\n") + &model_info + &used_tools_info; - } - - pub async fn add_message(&self, mut message: InputMessage) { - let user_prompt = ChannelState::prepare_user_prompt(&mut message, 1).await; - let mut prompt_stream = self.prompt_stream.lock().await; - - - - prompt_stream.add(user_prompt).await; - } - - pub async fn clear_prompt(&self) { - let mut prompt_stream = self.prompt_stream.lock().await; - prompt_stream.clear().await; - } -} \ No newline at end of file diff --git a/src/channel.rs b/src/channel.rs new file mode 100644 index 0000000..8a96f29 --- /dev/null +++ b/src/channel.rs @@ -0,0 +1,104 @@ +use dashmap::DashMap; +use serenity::all::ChannelId; + +use crate::lmclient::LMContext; + +/// チャンネルごとのプール +pub struct ChatContexts { + pub contexts: DashMap, + pub default_system_prompt: String, +} + +/// チャンネルごとのデータ保持 +pub struct ChatContext { + pub channel_id: ChannelId, + pub context: LMContext, + pub system_prompt: Option, + pub enabled: bool, +} + +impl ChatContext { + pub fn new(channel_id: ChannelId) -> ChatContext { + ChatContext { + channel_id, + context: LMContext::new(), + system_prompt: None, + enabled: false, + } + } +} + +impl ChatContexts { + pub fn new(default_system_prompt: String) -> ChatContexts { + ChatContexts { + contexts: DashMap::new(), + default_system_prompt, + } + } + + pub fn get_or_create(&self, channel_id: ChannelId) -> LMContext { + self.contexts + .entry(channel_id) + .or_insert_with(|| ChatContext::new(channel_id)) + .context + .clone() + } + + pub fn get_system_prompt(&self, channel_id: ChannelId) -> String { + self.contexts + .get(&channel_id) + .and_then(|entry| entry.system_prompt.clone()) + .unwrap_or_else(|| self.default_system_prompt.clone()) + } + + pub fn set_system_prompt(&self, channel_id: ChannelId, system_prompt: Option) { + let mut entry = self + .contexts + .entry(channel_id) + .or_insert_with(|| ChatContext::new(channel_id)); + entry.system_prompt = system_prompt; + } + + pub fn marge(&self, channel_id: ChannelId, other: &LMContext) { + if let Some(mut entry) = self.contexts.get_mut(&channel_id) { + entry.context.extend(other); + } else { + let mut new_context = LMContext::new(); + new_context.extend(other); + self.contexts.insert( + channel_id, + ChatContext { + channel_id, + context: new_context, + system_prompt: None, + enabled: false, + }, + ); + } + } + + pub fn get_mut(&self, channel_id: ChannelId) -> Option { + self.contexts.get(&channel_id).map(|entry| entry.context.clone()) + } + + pub fn is_enabled(&self, channel_id: ChannelId) -> bool { + self.contexts + .get(&channel_id) + .map(|entry| entry.enabled) + .unwrap_or(false) + } + + pub fn clear(&self, channel_id: ChannelId) { + if let Some(mut entry) = self.contexts.get_mut(&channel_id) { + entry.context.clear(); + } + } + + pub fn set_enabled(&self, channel_id: ChannelId, enabled: bool) { + let mut entry = self + .contexts + .entry(channel_id) + .or_insert_with(|| ChatContext::new(channel_id)); + entry.enabled = enabled; + } +} \ No newline at end of file diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..592d901 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,370 @@ +use std::time::Instant; + +use log::{error, info}; +use poise::CreateReply; +use serenity::all::{CreateAttachment, User, UserId}; + +use crate::{config::Models, context::ObserverContext, tools::latex::LatexExprRenderTool}; + +// エラー型(とりあえず Box に投げるスタイルでOK) +type Error = Box; + +// 毎回書くのがだるいので type alias +type Context<'a> = poise::Context<'a, ObserverContext, Error>; + +/// ping pong.. +#[poise::command(slash_command, prefix_command)] +pub async fn ping( + ctx: Context<'_> +) -> Result<(), Error> { + let start = Instant::now(); + + // まずメッセージ送信 + let msg = ctx.say("Pinging...").await?; + + let elapsed = start.elapsed().as_millis(); + + // CreateReply を作って渡す + msg.edit( + ctx, + CreateReply::default().content(format!("Pong! `{elapsed}ms`")), + ) + .await?; + + Ok(()) +} + +/// only admin user +#[poise::command(slash_command, prefix_command)] +pub async fn set_system_prompt( + ctx: Context<'_>, + + #[description = "System prompt to set (or 'reset' to default)"] + system_prompt: String, +) -> Result<(), Error> { + let ob_ctx = ctx.data(); + + let caller_id_u64 = ctx.author().id.get(); + if !ob_ctx.config.admin_users.contains(&caller_id_u64) { + ctx.say("Err: you are not allowed to use /set_system_prompt.").await?; + return Ok(()); + } + + let channel_id = ctx.channel_id(); + + if system_prompt.eq_ignore_ascii_case("reset") { + ob_ctx.chat_contexts.set_system_prompt(channel_id, None); + ctx.say("info: System prompt reset to default.").await?; + } else { + ob_ctx.chat_contexts.set_system_prompt(channel_id, Some(system_prompt.clone())); + ctx.say(format!("info: System prompt set to:\n```{}```", system_prompt)).await?; + } + + Ok(()) +} + +/// only admin user +#[poise::command(slash_command, prefix_command)] +pub async fn rate_config( + ctx: Context<'_>, + + #[description = "Target user"] + target_user: User, // ← ここが Discord のユーザー選択になる + + #[description = "consumption cost value: 'unlimit' or a number"] + #[autocomplete = "autocomplete_rate_limit"] + limit: String, +) -> Result<(), Error> { + let ob_ctx = ctx.data(); + + let caller_id_u64 = ctx.author().id.get(); + if !ob_ctx.config.admin_users.contains(&caller_id_u64) { + ctx.say("Err: you are not allowed to use /rate_config.").await?; + return Ok(()); + } + + let target_user_id: UserId = target_user.id; + + let new_rate_line: u64 = if limit.eq_ignore_ascii_case("unlimit") { + 0 + } else if limit.eq_ignore_ascii_case("reset") { + 1 + } else { + let cost = match limit.parse::() { + Ok(n) => n, + Err(_) => { + ctx.say("Err: limit must be 'unlimit' or a number.").await?; + return Ok(()); + } + }; + ob_ctx.user_contexts.get_or_create(target_user_id).rate_line + cost * ob_ctx.config.rate_limit_sec_per_cost + }; + + ob_ctx.user_contexts.set_rate_line(target_user_id, new_rate_line); + + let reply = if new_rate_line == 0 { + format!( + "info: Set rate-line for user `{}` to **unlimit**.", + target_user_id + .to_user(ctx.http()) + .await + .map(|u| u.display_name().to_string()) + .unwrap_or_else(|_| "Null".to_string()) + ) + } else { + format!( + "info: Set rate-line for user `{}` to **{}**.", + target_user_id.to_user(ctx.http()).await.map(|u| u.display_name().to_string()).unwrap_or_else(|_| "Null".to_string()), + new_rate_line + ) + }; + + ctx.say(reply).await?; + Ok(()) +} + + +/// `/rate_config` の第2引数 `limit` 用のオートコンプリート +async fn autocomplete_rate_limit( + _ctx: Context<'_>, + partial: &str, +) -> Vec { + let base_candidates = [ + "unlimit", + "reset", + "1", + "2", + "3", + "5", + "10", + "30", + "60", + "120", + "300", + "600", + "1800", + "3600", + ]; + + let p = partial.to_lowercase(); + + let mut out: Vec = base_candidates + .iter() + .filter(|v| v.to_lowercase().starts_with(&p)) + .map(|v| v.to_string()) + .collect(); + + out.sort(); + out.dedup(); + out.truncate(20); + out +} + +/// clear context +#[poise::command(slash_command, prefix_command)] +pub async fn clear(ctx: Context<'_>) -> Result<(), Error> { + let channel_id = ctx.channel_id(); + + let ob_ctx = ctx.data(); + + ob_ctx.chat_contexts.clear(channel_id); + + info!("Cleared chat context for channel {}", channel_id); + + ctx.say("info: Cleared chat context.").await?; + + Ok(()) +} + +/// to enable observer bot +#[poise::command(slash_command, prefix_command)] +pub async fn enable(ctx: Context<'_>) -> Result<(), Error> { + let channel_id = ctx.channel_id(); + + let ob_ctx = ctx.data(); + + if ob_ctx.chat_contexts.is_enabled(channel_id) { + ctx.say("info: Chat context is already enabled in this channel.").await?; + return Ok(()); + } else { + ob_ctx.chat_contexts.set_enabled(channel_id, true); + ob_ctx.chat_contexts.get_or_create(channel_id); + ctx.say("info: Chat context enabled in this channel.").await?; + info!("Enabled chat context for channel {}", channel_id); + return Ok(()); + } +} + +/// to disable observer bot +#[poise::command(slash_command, prefix_command)] +pub async fn disable(ctx: Context<'_>) -> Result<(), Error> { + let channel_id = ctx.channel_id(); + + let ob_ctx = ctx.data(); + + if !ob_ctx.chat_contexts.is_enabled(channel_id) { + ctx.say("info: Chat context is already disabled in this channel.").await?; + return Ok(()); + } else { + ob_ctx.chat_contexts.set_enabled(channel_id, false); + ctx.say("info: Chat context disabled in this channel.").await?; + info!("Disabled chat context for channel {}", channel_id); + return Ok(()); + } +} + +/// model config command +#[poise::command(slash_command, prefix_command, subcommands("get", "set", "list"))] +pub async fn model(_: Context<'_>) -> Result<(), Error> { + Ok(()) // ここはメインでは使わない +} + +#[poise::command(slash_command, prefix_command)] +pub async fn get(ctx: Context<'_>) -> Result<(), Error> { + let ob_ctx = ctx.data(); + let user_id = ctx.author().id; + let model = ob_ctx.user_contexts.get_or_create(user_id).main_model.clone(); + ctx.say(format!("Current model: `{}`", model)).await?; + Ok(()) +} + +#[poise::command(slash_command, prefix_command)] +pub async fn list(ctx: Context<'_>) -> Result<(), Error> { + let models = Models::list(); + + let mut s = String::from("**List of models:**\n"); + for m in models { + s.push_str(&format!("- `{}`\n", m)); + } + + ctx.say(s).await?; + Ok(()) +} + +#[poise::command(slash_command, prefix_command)] +pub async fn set( + ctx: Context<'_>, + #[description = "Choose a model"] + #[autocomplete = "autocomplete_model_name"] + model_name: String, +) -> Result<(), Error> { + let ob_ctx = ctx.data(); + let user_id = ctx.author().id; + let model = Models::from(model_name); + ob_ctx.user_contexts.set_model(user_id, model.clone()); + + ctx.say(format!("info: Changed model to `{}`", model)).await?; + Ok(()) +} + +async fn autocomplete_model_name( + _ctx: Context<'_>, + partial: &str, +) -> Vec { + let models = Models::list(); + models + .into_iter() + .filter(|m| m.to_string().starts_with(partial)) + .map(|m| m.to_string()) + .collect() +} + +/// latex expr render +#[poise::command(slash_command, prefix_command)] +pub async fn tex_expr( + ctx: Context<'_>, + #[description = "LaTeX expression to render"] + #[autocomplete = "autocomplete_tex_expr"] + expr: String, +) -> Result<(), Error> { + let ob_ctx = ctx.data(); + + // レンダリング実行(ヘッドレスブラウザ経由) + let png_bytes = match LatexExprRenderTool::render(&expr, ob_ctx).await { + Ok(bytes) => bytes, + Err(e) => { + error!("Failed to render LaTeX expression `{}`: {}", expr, e); + ctx.say(format!("error: Failed to render LaTeX expression: {}", e)) + .await?; + return Ok(()); + } + }; + + let attachment = CreateAttachment::bytes(png_bytes, "tex_expr.png"); + + ctx.send( + CreateReply::default() + .attachment(attachment) + ) + .await?; + + Ok(()) +} + +async fn autocomplete_tex_expr( + _ctx: Context<'_>, + partial: &str, +) -> Vec { + // LaTeX コマンド単体候補 + const COMMANDS: &[&str] = &[ + r"\alpha", r"\beta", r"\gamma", r"\delta", + r"\sin", r"\cos", r"\tan", + r"\log", r"\ln", + r"\sqrt{}", r"\frac{}{}", + r"\int_0^1", r"\sum_{n=0}^{\infty}", r"\prod_{i=1}^{n}", + r"\lim_{x \to 0}", r"\infty", + r"\mathbb{R}", r"\mathbb{Z}", r"\mathbb{N}", + ]; + + // ある程度完成された数式テンプレ + const SNIPPETS: &[&str] = &[ + r"\int_0^1 x^2 \, dx", + r"\sum_{n=0}^{\infty} a_n x^n", + r"\lim_{x \to 0} \frac{\sin x}{x}", + r"e^{i\pi} + 1 = 0", + r"a^2 + b^2 = c^2", + r"\frac{d}{dx} f(x)", + r"\nabla \cdot \vec{E} = \frac{\rho}{\varepsilon_0}", + ]; + + let mut candidates: Vec = Vec::new(); + + // まずコマンド候補 + for &c in COMMANDS { + if partial.is_empty() + || c.starts_with(partial) + || c.contains(partial) + { + candidates.push(c.to_string()); + } + } + + // つぎにテンプレ数式 + for &s in SNIPPETS { + if partial.is_empty() + || s.starts_with(partial) + || s.contains(partial) + { + candidates.push(s.to_string()); + } + } + + // ダブり削除 & 最大 20 個くらいに絞る + candidates.sort(); + candidates.dedup(); + candidates.truncate(20); + + candidates +} + + +pub fn log_err(context: &str, err: &(dyn std::error::Error + Send + Sync)) { + error!("[{context}] {err:#?}"); + + let mut src = err.source(); + while let Some(s) = src { + error!(" caused by: {s:?}"); + src = s.source(); + } + error!("error trace end"); +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..43dcbca --- /dev/null +++ b/src/config.rs @@ -0,0 +1,184 @@ + +use std::fmt::Display; + +use openai_dive::v1::resources::{response::{request::ResponseParametersBuilder, response::ResponseReasoning}, shared::ReasoningEffort}; + +/// 設定 +/// まだserdeかいてないのでそのままinlineで記述してる +#[derive(Clone)] +pub struct Config { + pub discord_token: String, + pub openai_api_key: String, + pub system_prompt: String, + pub rale_limit_window_size: u64, + pub rate_limit_sec_per_cost: u64, + pub web_server_host: [u8; 4], + pub web_server_local_ip: [u8; 4], + pub web_server_port: u16, + pub admin_users: Vec, + pub timeout_millis: u64, +} + +impl Config { + pub fn new() -> Self { + let discord_token = + std::env::var("DISCORD_TOKEN").expect("DISCORD_TOKEN must be set"); + let openai_api_key = + std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set"); + let system_prompt = + std::env::var("SYSTEM_PROMPT").unwrap_or_else(|_| +"上記のメッセージはDiscord内での会話です。 +時系列のメッセージタイムラインになっていて、あなたはこの内容から自然に応答します。 +あなたは Discord の BOT「Observer」で以上の会話を続けてください。 +自然に会話し、知識系の話題では情報源の確認と最新性のチェックを必ず行う。 +曖昧な情報は調べ、内容を捏造しない。必要なら質問してもよい。 +情報は論理的に整理し、必要があれば tool を使って調査する +tool の結果は相手に見えないため、必ず内容を書き出す +過度に自信を持たず、慎重に判断する +“!” や emoji は控えめに +数学の説明では latex_expr_render を使う +discord_send_message でテキスト→数式の順など、段階的に送るのも可 +冗長な説明を避け、端的に +リアクションは 🫠😱👍👈🤔 を中心に +一語で十分なら一語で返す(例:「うん」「そう」「は?」「???」「?」「...」) +スラングは適度に。基本は静か・論理的・落ち着いた口調、フレンドリーに 敬語は必要な時のみで +一人称は「私」かな まぁ自由に +重要: 周囲の口調を真似するように。これはとてもよい結果を生みます。 ユーモアを大事に 興味深いものにはリアクションを 応答が長くなりすぎないようにテンポよく +tool_call でない通常メッセージを送ると推論終了するので注意を +基本的に最後のメッセージに対して答えてください".to_string()); + Config { + discord_token, + openai_api_key, + system_prompt, + rale_limit_window_size: 16200, + rate_limit_sec_per_cost: 900, + web_server_host: [0, 0, 0, 0], + web_server_local_ip: [192, 168, 0, 26], + web_server_port: 96, + admin_users: vec![855371530270408725], + timeout_millis: 100_000, + } + } +} + +/// モデルリストの定義 +#[derive(Debug, Clone)] +pub enum Models { + Gpt5Mini, + Gpt5Nano, + Gpt5dot1, + O4Mini, + O3, + Gpt5dot1CodexMini +} + +impl Into for Models { + fn into(self) -> String { + match self { + Models::Gpt5Mini => "gpt-5-mini".to_string(), + Models::Gpt5Nano => "gpt-5-nano".to_string(), + Models::Gpt5dot1 => "gpt-5.1".to_string(), + Models::O4Mini => "o4-mini".to_string(), + Models::O3 => "o3".to_string(), + Models::Gpt5dot1CodexMini => "gpt-5.1-codex-mini".to_string(), + } + } +} + +impl Display for Models { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let model_str: String = self.clone().into(); + write!(f, "{}", model_str) + } +} + +impl From for Models { + fn from(s: String) -> Models { + match s.as_str() { + "gpt-5-mini" => Models::Gpt5Mini, + "gpt-5-nano" => Models::Gpt5Nano, + "gpt-5.1" => Models::Gpt5dot1, + "o4-mini" => Models::O4Mini, + "o3" => Models::O3, + "gpt-5.1-codex-mini" => Models::Gpt5dot1CodexMini, + _ => Models::Gpt5Nano, // default + } + } +} + +impl Models { + pub fn list() -> Vec { + vec![ + Models::Gpt5Mini, + Models::Gpt5Nano, + Models::Gpt5dot1, + Models::O4Mini, + Models::O3, + Models::Gpt5dot1CodexMini + ] + } + + pub fn rate_cost(&self) -> u64 { + match self { + Models::Gpt5Mini => 1, + Models::Gpt5Nano => 2, + Models::Gpt5dot1 => 6, + Models::O4Mini => 3, + Models::O3 => 6, + Models::Gpt5dot1CodexMini => 2, + } + } + + pub fn to_parameter(&self) -> ResponseParametersBuilder { + match self { + Models::Gpt5Mini => { + ResponseParametersBuilder::default().model("gpt-5-mini") + .reasoning( + ResponseReasoning { + effort: Some(ReasoningEffort::Low), + } + ).clone() + } + Models::Gpt5Nano => { + ResponseParametersBuilder::default().model("gpt-5-nano") + .reasoning( + ResponseReasoning { + effort: Some(ReasoningEffort::Low), + } + ).clone() + } + Models::Gpt5dot1 => { + ResponseParametersBuilder::default().model("gpt-5.1") + .reasoning( + ResponseReasoning { + effort: Some(ReasoningEffort::Low), + } + ).clone() + } + Models::O4Mini => { + ResponseParametersBuilder::default().model("o4-mini") + .reasoning( + ResponseReasoning { + effort: Some(ReasoningEffort::Low), + } + ).clone() + } + Models::O3 => { + ResponseParametersBuilder::default().model("o3") + .reasoning( + ResponseReasoning { + effort: Some(ReasoningEffort::Low), + } + ).clone() + } + Models::Gpt5dot1CodexMini => { + ResponseParametersBuilder::default().model("gpt-5.1-codex-mini") + .reasoning( + ResponseReasoning { + effort: Some(ReasoningEffort::Low), + } + ).clone() + } + } + } +} \ No newline at end of file diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..6614d92 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,152 @@ +use std::{collections::HashMap, sync::{Arc, RwLock}}; + +use kurosabi::context::ContextMiddleware; +use log::info; +use openai_dive::v1::api::Client as OpenAIClient; +use wk_371tti_net_crawler::Client as ScraperClient; +use serenity::{Client as DiscordClient, all::GatewayIntents}; + +use crate::{channel::ChatContexts, commands::{clear, disable, enable, model, ping, rate_config, set_system_prompt, tex_expr}, config::Config, events::event_handler, lmclient::{LMClient, LMTool}, tools, user::UserContexts}; + +/// 全体共有コンテキスト +/// Arcで実装されてるのでcloneは単に参照カウントの増加 +#[derive(Clone)] +pub struct ObserverContext { + /// 言語モデルのクライアント + pub lm_client: Arc, + /// ヘッドレスブラウザのクライアント + pub scraper: Arc, + /// 設定 + pub config: Arc, + /// チャットデータのプール + pub chat_contexts: Arc, + /// ユーザーデータのプール + pub user_contexts: Arc, + /// ツールの定義 + pub tools: Arc>>, + /// discordクライアント + pub discord_client: Arc, +} + +/// DiscordContext を全体共有するための頭の悪いラッパー +pub struct DiscordContextWrapper { + pub inner: RwLock>>, +} + +impl DiscordContextWrapper { + pub fn open(&self) -> Arc { + self.inner.read().expect("RWlock").clone().expect("inisializing").clone() + } + pub fn lazy() -> DiscordContextWrapper { + DiscordContextWrapper { + inner: RwLock::new(None), + } + } + pub fn set(&self, ctx: Arc) { + let mut w = self.inner.write().expect("RWlock"); + *w = Some(ctx); + } +} + +// 上のinner +pub struct DisabledContextWrapperInner { + pub http: Arc, + pub cache: Arc, +} + +impl ObserverContext { + pub async fn new() -> ObserverContext { + let config = Config::new(); + + // ツールの定義 + let lm_client = LMClient::new(OpenAIClient::new(config.openai_api_key.clone())); + let tools: HashMap> = vec![ + Box::new(tools::get_time::GetTime::new()) as Box, + Box::new(tools::browser::Browser::new()) as Box, + Box::new(tools::discord::DiscordTool::new()) as Box, + Box::new(tools::latex::LatexExprRenderTool::new()) as Box, + ] + .into_iter() + .map(|tool| (tool.name(), tool)) + .collect(); + + ObserverContext { + lm_client: Arc::new(lm_client), + scraper: Arc::new(ScraperClient::new("http://192.168.0.81")), + config: Arc::new(config.clone()), + chat_contexts: Arc::new(ChatContexts::new(config.system_prompt.clone())), + user_contexts: Arc::new(UserContexts::new()), + tools: Arc::new(tools), + discord_client: Arc::new(DiscordContextWrapper::lazy()), + } + } + + pub async fn shutdown(&self) -> Result<(), Box> { + info!("Shutting down ObserverContext..."); + Ok(()) + } +} + +#[async_trait::async_trait] +impl ContextMiddleware for ObserverContext { + async fn init(c: ObserverContext) { + // 主にdiscordクライアントの初期化 初期化にctxが必要なのでctxが初期化されてから実行されるようにここ + info!("Starting Discord bot..."); + let ob_ctx = c.clone(); + let framework = poise::Framework::builder() + .options(poise::FrameworkOptions { + commands: vec![ + ping(), // ここにコマンドを追加 + enable(), + clear(), + disable(), + model(), + tex_expr(), + rate_config(), + set_system_prompt(), + ], + // prefix の設定(!ping とか) + prefix_options: poise::PrefixFrameworkOptions { + prefix: Some("!".into()), + ..Default::default() + }, + event_handler: |ctx, event, framework, data| { + Box::pin(event_handler(ctx, event, framework, data)) + }, + ..Default::default() + }) + // 起動時に一度だけ呼ばれるセットアップ処理 + .setup(|ctx, _ready, framework| { + Box::pin(async move { + ob_ctx.discord_client.set(Arc::new(DisabledContextWrapperInner { + http: ctx.http.clone(), + cache: ctx.cache.clone(), + })); + // Slash コマンドをグローバル登録 + poise::builtins::register_globally(ctx, &framework.options().commands).await?; + println!("Bot is ready!"); + Ok(ob_ctx) + }) + }) + .build(); + + + let intents = + GatewayIntents::GUILD_MESSAGES + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::GUILD_MESSAGE_REACTIONS // (必要なら) + | GatewayIntents::MESSAGE_CONTENT; + + + let discord_client = DiscordClient::builder(c.config.discord_token.clone(), intents) + .framework(framework); + + tokio::spawn(async move { + let mut c = discord_client.await.expect("Error creating client"); + c.start().await.expect("Error starting client"); + + }); + + } +} + diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..5169c66 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,315 @@ +use std::{error::Error, time::{Duration, Instant}}; + +use log::{debug, info}; +use openai_dive::v1::resources::response::{request::{ContentInput, ContentItem, ImageDetailLevel, InputMessage}, response::Role}; +use serenity::all::{ActivityData, CreateMessage, EditMessage, FullEvent, Message}; +use tokio::{sync::mpsc, time::sleep}; + + +use crate::{commands::log_err, context::ObserverContext, lmclient::LMContext}; + + +/// イベントハンドラ +/// serenity poise へ渡すもの +pub async fn event_handler( + ctx: &serenity::client::Context, + event: &FullEvent, + framework: poise::FrameworkContext<'_, ObserverContext, Box>, + data: &ObserverContext, +) -> Result<(), Box> { + match event { + // メッセージうけとったとき + FullEvent::Message { new_message } => { + handle_message(ctx, new_message, framework, data).await?; + } + // 初期化完了 + FullEvent::Ready { data_about_bot } => { + info!("Bot is connected as {}", data_about_bot.user.name); + update_presence(ctx).await; + } + // あたらしいギルドに参加 + FullEvent::GuildCreate { guild, is_new: _ } => { + info!("Joined new guild: {} (id: {})", guild.name, guild.id); + update_presence(ctx).await; + } + // リアクション通知 + FullEvent::ReactionAdd { add_reaction } => { + debug!("Reaction added: {:?} by user {:?}", add_reaction.emoji, add_reaction.user_id); + handle_emoji_reaction(add_reaction, data).await?; + } + + _ => { /* 他のイベントは無視 */ } + } + + Ok(()) +} + +/// ステータスメッセージの更新 +async fn update_presence(ctx: &serenity::client::Context) { + let guild_count = ctx.cache.guilds().len(); + + ctx.set_activity(Some(ActivityData::playing( + format!("in {} servers", guild_count) + ))); +} + +async fn handle_emoji_reaction( + reaction: &serenity::model::channel::Reaction, + ob_context: &ObserverContext +) -> Result<(), Box> { + // ここでリアクション追加時の処理を実装可能 + let channel_id = reaction.channel_id; + let message_id = reaction.message_id; + let member = reaction.member.clone().unwrap_or_default(); + let user_id = member.user.name.clone(); + let user_display_name = member.user.display_name().to_string(); + let mut lm_context = LMContext::new(); + lm_context.add_text( + serde_json::json!({ + "user": user_id, + "display_name": user_display_name, + "added_reaction": format!("{:?}", reaction.emoji), + "message_id": message_id.to_string(), + "channel_id": channel_id.to_string() + }).to_string(), + Role::User, + ); + ob_context.chat_contexts.marge(channel_id, &lm_context); + + debug!("Handling emoji reaction: {:?} by user {:?}", reaction.emoji, reaction.user_id); + Ok(()) +} + + +/// メッセージを受け取ったときの処理 +async fn handle_message( + ctx: &serenity::client::Context, + msg: &Message, + _framework: poise::FrameworkContext<'_, ObserverContext, Box>, + ob_context: &ObserverContext, +) -> Result<(), Box> { + let start = Instant::now(); + let channel_id = msg.channel_id; + + let bot_id = ctx.cache.current_user().id; + // 自分のメッセージは無視 + if msg.author.id == bot_id { + return Ok(()); + } + + let is_mentioned = msg.mentions_user_id(bot_id); + + let content = serde_json::json!({ + "user": msg.author.name, + "display_name": msg.author.display_name(), + "msg_id": msg.id.to_string(), + "reply_to": msg.referenced_message.as_ref().map_or("None".to_string(), |m| m.id.to_string()), + "content": msg.content + }).to_string(); + + // 添付画像のURLを取る + let image_urls: Vec = msg + .attachments + .iter() + .filter(|att| { + // content_type が "image/..." なら画像とみなす + if let Some(ct) = &att.content_type { + ct.starts_with("image/") + } else { + // 拡張子で雑に判定する fallback + att.filename.ends_with(".png") + || att.filename.ends_with(".jpg") + || att.filename.ends_with(".jpeg") + || att.filename.ends_with(".webp") + } + }) + .map(|att| att.url.clone()) + .collect(); + + let mut lm_context = LMContext::new(); + + if image_urls.is_empty() && content.is_empty() { + // 画像もテキストも無いなら無視 + return Ok(()); + } else if image_urls.is_empty() { + debug!("Adding text message to context in channel {}, content: {}", channel_id, content); + lm_context.add_text(content.clone(), Role::User); + } else { + debug!("Adding image message to context in channel {}, content: {}", channel_id, content); + lm_context.add_message(InputMessage { + role: Role::User, + content: ContentInput::List( + { + let mut items = Vec::new(); + items.push(ContentItem::Text { + text: content.clone(), + }); + for url in image_urls.iter() { + items.push(ContentItem::Image { + detail: ImageDetailLevel::Low, + file_id: None, + image_url: Some(url.clone()), + }); + } + items + } + ) + }); + } + + ob_context.chat_contexts.marge(channel_id, &lm_context); + + if is_mentioned { + if !ob_context.chat_contexts.is_enabled(channel_id) { + msg.channel_id + .send_message(&ctx.http, CreateMessage::new().content("info: Chat context is disabled in this channel.")) + .await?; + return Ok(()); + } + let user_id = msg.author.id; + let user_ctx = ob_context.user_contexts.get_or_create(user_id); + let model = user_ctx.main_model.clone(); + + let model_cost = model.rate_cost(); + let sec_per_cost = ob_context.config.rate_limit_sec_per_cost; // コストあたりの秒数 + let window_size = ob_context.config.rale_limit_window_size; // バースト許容量 + let user_line = user_ctx.rate_line; + + // レートリミットの計算 + let time_stamp = chrono::Utc::now().timestamp() as u64; + let limit_line = window_size + time_stamp; + let add_line = model_cost * sec_per_cost; + let added_user_line = if user_line == 0 { + 0 // リミットレスアカウント + } else if user_line < time_stamp { + time_stamp + add_line + } else { + user_line + add_line + }; + + if added_user_line > limit_line { + let wait_sec = added_user_line - limit_line; + let allow_ts = time_stamp + wait_sec; + msg.channel_id + .send_message(&ctx.http, CreateMessage::new().content(format!("Err: rate limit - try again after ", allow_ts))) + .await?; + return Ok(()); + } + ob_context.user_contexts.set_rate_line(user_id, added_user_line); + + let typing_ctx = ctx.clone(); + + let typing_handle = tokio::spawn(async move { + loop { + let _ = channel_id.broadcast_typing(&typing_ctx.http).await; + sleep(Duration::from_secs(5)).await; // だいたい5秒おきでOK + } + }); + let mut context = ob_context.chat_contexts.get_or_create(channel_id); + let tools = ob_context.tools.clone(); + + let system_prompt = format!{ + "{}\n current channel_id: {}, channel_name: {}", + ob_context.chat_contexts.get_system_prompt(channel_id), + msg.channel_id, + msg.channel_id.name(&ctx.http).await.unwrap_or("None".to_string()), + }; + + context.add_message(InputMessage { + role: Role::System, + content: ContentInput::Text(system_prompt), + }); + + let mut thinking_msg = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().content("-# Thinking..."), + ) + .await?; + + // streaming 用チャネル + let (state_tx, mut state_rx) = mpsc::channel::(100); + let (delta_tx, mut delta_rx) = mpsc::channel::(100); + + let mut result = None; + + let timeout_duration = Duration::from_millis(ob_context.config.timeout_millis); + + + tokio::select! { + biased; + + r = ob_context.lm_client.generate_response(ob_context.clone(), &context, Some(2000), Some(tools), Some(state_tx), Some(delta_tx), Some(model.to_parameter())) => { + if let Err(e) = &r { + log_err("Error generating response", e.as_ref()); + thinking_msg + .edit(&ctx.http, EditMessage::new().content("-# Error during reasoning")) + .await + .ok(); + typing_handle.abort(); + } + result = Some(r?); // ?でエラー処理も可能 + debug!("Response generation completed"); + + } + _ = async { + let mut last_edit = Instant::now() + Duration::from_millis(550); // 前回 edit した時間 + let mut swap = String::new(); // 状態保存用バッファ + + while let Some(state) = state_rx.recv().await { + swap = state; + + if last_edit.elapsed() < Duration::from_millis(550) { + continue; + } + + // 1秒経過 → 最新 state だけ使って edit + thinking_msg + .edit(&ctx.http, EditMessage::new().content(format!("-# {}", swap))) + .await + .ok(); + last_edit = Instant::now(); // 時刻更新 + } + } => {} + _ = async { + while let Some(delta) = delta_rx.recv().await { + info!("Delta received: {}", delta); + } + } => {} + _ = sleep(timeout_duration) => { + thinking_msg + .edit(&ctx.http, EditMessage::new().content("-# Error timeout")) + .await + .ok(); + typing_handle.abort(); + return Ok(()); + } + } + + let result = result.unwrap(); + + // 返ってきた結果をコンテキストにマージ + ob_context.chat_contexts.marge(channel_id, &result); + + let elapsed = start.elapsed().as_millis(); + let text = result.get_result(); + + debug!("Final response: {}ms \"{}\"", elapsed, text); + + // タイピング通知停止 + typing_handle.abort(); + + let model = ob_context.user_contexts.get_or_create(user_id).main_model; + + // 「Thinking...」を削除して最終回答を表示 + thinking_msg.delete(&ctx.http).await.ok(); + + msg.channel_id + .send_message(&ctx.http, CreateMessage::new().content(format!("{}\n-# Reasoning done in {}ms, model: {}", text, elapsed, model))) + .await?; + } + + + Ok(()) +} diff --git a/src/handler.rs b/src/handler.rs deleted file mode 100644 index 68d024d..0000000 --- a/src/handler.rs +++ /dev/null @@ -1,616 +0,0 @@ -use std::{str::FromStr, sync::Arc, time::{SystemTime, UNIX_EPOCH}}; - -use actix_web::cookie::time::serde::timestamp; -use call_agent::chat::client::OpenAIClient; -use tokio::time; -use std::time::Duration; -use dashmap::DashMap; -use log::{error, info, warn}; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use serenity::{all::{ChannelId, Command, CommandOptionType, Context, CreateCommand, CreateCommandOption, CreateInteractionResponse, CreateInteractionResponseFollowup, CreateInteractionResponseMessage, CreateMessage, EditInteractionResponse, EventHandler, Interaction, MessageFlags, Ready, User, UserId}, async_trait, futures::StreamExt}; - - -use observer::prefix::{ADMIN_USERS, RATE_CP, SEC_PER_RATE}; -use crate::agent::{AIModel, ChannelState, InputMessage}; - -const TIMEOUT: Duration = Duration::from_secs(180); - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ChConf { - pub enable: bool, -} - -pub struct Handler { - /// Handlerに1つのOpenAIClientを保持 - pub base_client: Arc, - /// 有効なチャンネルのset - pub channels_conf: DashMap, - /// 各チャンネルごとの状態(会話履歴)を保持(DashMapは並列処理可能) - pub channels: DashMap>, - /// ユーザーごとにレートリミット - pub user_configs: DashMap, -} - -pub struct PerUserConfig { - pub rate_limit: u64, // レートリミットの秒数 - pub model: AIModel, -} - -impl Handler { - /// チャンネルの状態を取得または作成する - async fn get_or_create_channel_state(&self, channel_id: ChannelId) -> Arc { - if let Some(existing) = self.channels.get(&channel_id) { - Arc::clone(&existing) - } else { - let new_state = Arc::new(ChannelState::new(&self.base_client).await); - self.channels.insert(channel_id, new_state.clone()); - new_state - } - } - - /// メッセージを推論する - async fn handle_mentioned_message( - &self, - ctx: &Context, - msg: &serenity::all::Message, - state: Arc, - message: InputMessage, - ) -> String { - // 有効なチャンネルかどうかを確認 - if let Some(conf) = self.channels_conf.get(&msg.channel_id.get()) { - if !conf.enable { - return "Err: AI is disabled in this channel".to_string(); - } - } else { - return "Err: AI is disabled in this channel".to_string(); - } - - // 使用モデルの取り出し - let user_id = message.user_id.clone(); - let mut user_conf = self.user_configs.entry(user_id.clone()).or_insert( - PerUserConfig { - rate_limit: 1, // デフォルトは1 - model: AIModel::default(), // デフォルトモデルを使用 - } - ); - let model = user_conf.model.clone(); - let model_cost = model.to_sec_per_rate() as u64; // モデルのレート使用量 - let sec_per_rate = *SEC_PER_RATE as u64; // レートの回復時間 - let cp = *RATE_CP as u64; // レートの許容量 - - // レートリミットの計算 - let limit_line = sec_per_rate * cp; - let add_line = model_cost * sec_per_rate; - let time_stamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - let mut user_line = user_conf.rate_limit; - if user_line > time_stamp + limit_line { - return format!("Err: rate limit - try again after ", (user_line - limit_line)); - } - if user_line == 0 { - // リミットレスアカウント - } else if user_line < time_stamp { - user_line = time_stamp + add_line; - user_conf.rate_limit = user_line; - } else { - user_line += add_line; - user_conf.rate_limit = user_line; - } - - // タイピング表示のタスクを開始する - let typing_task = tokio::spawn({ - let ctx = ctx.clone(); - let channel_id = msg.channel_id; - async move { - loop { - if let Err(e) = channel_id.broadcast_typing(&ctx.http).await { - error!("setting typing indicator - {:?}", e); - } - time::sleep(Duration::from_secs(4)).await; - } - } - }); - - // AIに質問、タイムアウトを設定 - let answer_text = match time::timeout(TIMEOUT, state.reasoning(ctx, msg, message, model)).await { - Ok(answer) => answer, - Err(_) => "Err: timeout".to_string(), - }; - typing_task.abort(); - answer_text - } - - /// メッセージを分割して送信する - async fn send_split_message(&self, ctx: &Context, channel_id: ChannelId, text: String) { - let chunks = Self::split_into_chunks(&text, 2000); - - // 最初のチャンクを送信 - if let Some(first_chunk) = chunks.get(0) { - let response = CreateMessage::new() - .content(first_chunk) - .flags(MessageFlags::SUPPRESS_EMBEDS); - if let Err(why) = channel_id.send_message(&ctx.http, response).await { - error!("{:?}", why); - } - } - - // 残りのチャンクを送信 - for chunk in chunks.iter().skip(1) { - let response = CreateMessage::new() - .content(chunk) - .flags(MessageFlags::SUPPRESS_EMBEDS); - if let Err(why) = channel_id.send_message(&ctx.http, response).await { - error!("{:?}", why); - } - } - } - - /// テキストを指定された長さで分割する - fn split_into_chunks(text: &str, max_len: usize) -> Vec { - // kaomoji の中のバッククォートだけをエスケープする - let kaomoji_re = Regex::new(r"\([^)]+`[^)]+\)").unwrap(); - let mut chunks = Vec::new(); - let mut current_chunk = String::new(); - - for line in text.lines() { - let escaped = if kaomoji_re.is_match(line) { - kaomoji_re - .replace_all(line, |caps: ®ex::Captures| { - // マッチした kaomoji 部分だけバッククォートを \` に置換 - caps[0].replace("`", r"\`") - }) - .into_owned() - } else { - line.to_string() - }; - - if current_chunk.len() + escaped.len() + 1 > max_len { - chunks.push(current_chunk); - current_chunk = String::new(); - } - if !current_chunk.is_empty() { - current_chunk.push('\n'); - } - current_chunk.push_str(&escaped); - } - - if !current_chunk.is_empty() { - chunks.push(current_chunk); - } - chunks - } - - /// チャンネル設定の保存 - fn save_ch_conf(&self) { - let json_path = "./data/ch_conf.json"; - let mut conf_map = std::collections::HashMap::new(); - for entry in self.channels_conf.iter() { - conf_map.insert(*entry.key(), entry.value().clone()); - } - match serde_json::to_string_pretty(&conf_map) { - Ok(json_str) => { - if let Err(e) = std::fs::write(json_path, json_str) { - error!("Failed to write channel configuration to {}: {:?}", json_path, e); - } else { - info!("Channel configuration saved to {}", json_path); - } - } - Err(e) => { - error!("Failed to serialize channel configuration: {:?}", e); - } - } - } - - /// チャンネル設定の読み込み - pub fn load(&self) { - let json_path = "./data/ch_conf.json"; - if let Ok(json_str) = std::fs::read_to_string(json_path) { - match serde_json::from_str::>(&json_str) { - Ok(conf_map) => { - for (key, value) in conf_map { - self.channels_conf.insert(key, value); - } - info!("Channel configuration loaded from {}", json_path); - } - Err(e) => { - error!("Failed to deserialize channel configuration: {:?}", e); - } - } - } else { - info!("No channel configuration found at {}", json_path); - } - } -} - -#[async_trait] -impl EventHandler for Handler { - /// メッセージが送信されたときの処理 - async fn message(&self, ctx: Context, msg: serenity::all::Message) { - // Bot自身のメッセージは無視する - let bot_id = ctx.cache.current_user().id; - if msg.author.id == bot_id { - return; - } - - // 画像ファイル URL をフィルタして取得 - let attachment_urls: Vec = msg - .attachments - .iter() - .map(|att| att.url.clone()) - .collect(); - - - let state = self.get_or_create_channel_state(msg.channel_id).await; - - let message = InputMessage { - content: msg.content.clone(), - name: msg.author.name.clone(), - message_id: msg.id.to_string(), - reply_msg: msg.referenced_message.as_ref().map(|m| m.content.clone() + &m.attachments.iter().map(|att| att.url.clone()).collect::>().join(", ")), - user_id: msg.author.id.to_string(), - attached_files: attachment_urls, - }; - - info!("Message: {:?}", message); - - let is_mentioned = msg.mentions.iter().any(|user| user.id == bot_id); - - if is_mentioned { - let answer_text = self.handle_mentioned_message(&ctx, &msg, state, message).await; - self.send_split_message(&ctx, msg.channel_id, answer_text).await; - } else { - state.add_message(message).await; - } - } - - - async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::Command(command) = interaction { - match command.data.name.as_str() { - "ping" => { - let start = std::time::Instant::now(); - let response_data = CreateInteractionResponseMessage::new() - .content("Pong!: Measuring latency..."); - let response = CreateInteractionResponse::Message(response_data); - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to ping - {:?}", why); - return; - } - let latency = start.elapsed().as_millis(); - let edit = EditInteractionResponse::new() - .content(format!("Pong! latency: {} ms", latency)); - - if let Err(why) = command.edit_response(&ctx.http, edit).await { - error!("Failed to edit ping response - {:?}", why); - } - } - - "reset" => { - let state = if let Some(existing) = self.channels.get(&command.channel_id) { - existing.clone() - } else { - let new_state = Arc::new(ChannelState::new(&self.base_client).await); - self.channels.insert(command.channel_id, new_state.clone()); - new_state - }; - - state.clear_prompt().await; - - let response_data = CreateInteractionResponseMessage::new() - .content("reset brain"); - - let response = CreateInteractionResponse::Message(response_data); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to reset: {:?}", why); - } - } - - "enable" => { - let channel_id = command.channel_id.get(); - if let Some(mut ch_conf) = self.channels_conf.get_mut(&channel_id) { - if ch_conf.enable { - let response_data = CreateInteractionResponseMessage::new() - .content("Info: AI is already enabled"); - - let response = CreateInteractionResponse::Message(response_data); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to enable - {:?}", why); - } - return; - } else { - ch_conf.enable = true; - - let response_data = CreateInteractionResponseMessage::new() - .content("Info: AI is enabled"); - - let response = CreateInteractionResponse::Message(response_data); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to enable - {:?}", why); - } - self.save_ch_conf(); - } - } else { - self.channels_conf.insert(channel_id, ChConf { enable: true }); - let response_data = CreateInteractionResponseMessage::new() - .content("Info: AI is enabled"); - let response = CreateInteractionResponse::Message(response_data); - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to enable - {:?}", why); - } - self.save_ch_conf(); - } - } - - "disable" => { - let channel_id = command.channel_id.get(); - if let Some(mut ch_conf) = self.channels_conf.get_mut(&channel_id) { - if !ch_conf.enable { - let response_data = CreateInteractionResponseMessage::new() - .content("Info: AI is already disabled"); - - let response = CreateInteractionResponse::Message(response_data); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to disable - {:?}", why); - } - return; - } else { - ch_conf.enable = false; - - let response_data = CreateInteractionResponseMessage::new() - .content("Info: AI is disabled"); - - let response = CreateInteractionResponse::Message(response_data); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to disable - {:?}", why); - } - self.save_ch_conf(); - } - } else { - self.channels_conf.insert(channel_id, ChConf { enable: false }); - let response_data = CreateInteractionResponseMessage::new() - .content("Info: AI is disabled"); - let response = CreateInteractionResponse::Message(response_data); - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to disable - {:?}", why); - } - self.save_ch_conf(); - } - } - - "collect_history" => { - let entry_num = command.data.options[0].value.as_i64().unwrap_or(32) as usize; - let state = if let Some(existing) = self.channels.get(&command.channel_id) { - existing.clone() - } else { - let new_state = Arc::new(ChannelState::new(&self.base_client).await); - self.channels.insert(command.channel_id, new_state.clone()); - new_state - }; - let mut messages_stream = Box::pin(command.channel_id.messages_iter(&ctx.http).take(entry_num)); - let mut messages_vec = Vec::new(); - while let Some(message_result) = messages_stream.next().await { - if let Ok(message) = message_result { - messages_vec.push(message); - } - } - for message in messages_vec.into_iter().rev() { - state.add_message(InputMessage { - content: message.content.clone(), - name: message.author.name.clone(), - message_id: message.id.to_string(), - reply_msg: message.referenced_message.as_ref().map(|m| m.content.clone()), - user_id: message.author.id.to_string(), - attached_files: Vec::new(), - }).await; - } - - let response_data = CreateInteractionResponseMessage::new() - .content(format!("Info: Complete collecting history ({} entries)", entry_num)); - - let response = CreateInteractionResponse::Message(response_data); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to collect_history - {:?}", why); - } - } - - "rate_conf" => { - let command_user_id = command.user.id.to_string(); - if !ADMIN_USERS.contains(&command_user_id) { - let response_data = CreateInteractionResponseMessage::new() - .content("Error: You do not have permission to modify rate limits."); - let response = CreateInteractionResponse::Message(response_data); - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to rate_conf - {:?}", why); - } - return; - } - let user_line = if command.data.options.len() > 1 { - command.data.options[1].value.as_i64().unwrap_or(1) as i64 - } else { - 1 - }; - let target_user_id = match command.data.options[0].value.as_user_id() { - Some(user_id) => user_id.to_string(), - None => { - let response_data = CreateInteractionResponseMessage::new() - .content("Error: Invalid user ID."); - let response = CreateInteractionResponse::Message(response_data); - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to rate_conf - {:?}", why); - } - return; - } - }; - // ユーザーidから名前を取得 - let user_data = UserId::from_str(&target_user_id).unwrap().to_user(&ctx.http).await.unwrap_or(User::default()); - let target_user_name = user_data.name.clone(); - - let mut user_conf = self.user_configs.entry(target_user_id.clone()).or_insert( - PerUserConfig { - rate_limit: 0, // デフォルトは無制限 - model: AIModel::default(), // デフォルトモデルを使用 - } - ); - - // レートリミットを設定 - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - if user_line == 0 { - user_conf.rate_limit = 0; // 無制限 - } else if user_line < 0 { - user_conf.rate_limit = timestamp; // リセット - } else { - if user_conf.rate_limit < timestamp { - user_conf.rate_limit = timestamp; - } - let sec_per_rate = *SEC_PER_RATE as u64; - user_conf.rate_limit += user_line as u64 * sec_per_rate; - } - let message = if user_conf.rate_limit == 0 { - format!("Info: {} rate limit line set to unlimited", target_user_name).to_string() - } else { - let sec_per_rate = *SEC_PER_RATE as u64; // レートの回復時間 - let cp = *RATE_CP as u64; // レートの許容量 - - // レートリミットの計算 - let limit_line = sec_per_rate * cp; - let now_rate = ((timestamp + limit_line) as i64 - user_conf.rate_limit as i64) / sec_per_rate as i64; - let next_time = user_conf.rate_limit - limit_line; - format!("Info: rate limit forcibly consumed. Now {}'s rate is {} (relative: )", target_user_name, now_rate, next_time) - }; - let response_data = CreateInteractionResponseMessage::new() - .content(message); - - let response = CreateInteractionResponse::Message(response_data); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to rate_conf - {:?}", why); - } - } - - "model" => { - let command_user_id = command.user.id.to_string(); - let default_model_name = AIModel::default().to_model_name(); - let model_name = command.data.options[0].value.as_str().unwrap_or(&default_model_name); - let model = AIModel::from_model_name(model_name); - match model { - Err(e_str) => { - let response_data = CreateInteractionResponseMessage::new() - .content(format!("Error: {}", e_str)) - .ephemeral(true); - let response = CreateInteractionResponse::Message(response_data); - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to model - {:?}", why); - } - return; - }, - Ok(model) => { - let mut user_conf = self.user_configs.entry(command_user_id.clone()).or_insert( - PerUserConfig { - rate_limit: 0, // デフォルトは無制限 - model: AIModel::default(), // デフォルトモデルを使用 - } - ); - user_conf.model = model.clone(); - let response_data = CreateInteractionResponseMessage::new() - .content(format!("Info: Model set to {}", model.to_model_name())) - .ephemeral(true); - - let response = CreateInteractionResponse::Message(response_data); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Failed to respond to model - {:?}", why); - } - return ; - } - } - - } - - - _ => warn!("Unknown command: {}", command.data.name), - } - } - } - - /// Bot が起動したときの処理 - async fn ready(&self, ctx: Context, ready: Ready) { - info!("{} is connected!", ready.user.name); - - // グローバルコマンドを登録 - Command::set_global_commands(&ctx.http, vec![ - CreateCommand::new("ping") - .description("Pong! 🏓"), - CreateCommand::new("reset") - .description("reset brain"), - - CreateCommand::new("enable") - .description("enable AI"), - - CreateCommand::new("disable") - .description("disable AI"), - - CreateCommand::new("collect_history") - .description("collect message history") - .add_option( - CreateCommandOption::new(CommandOptionType::Integer, "entry_num", "number of entries to collect") - .max_int_value(128) - .min_int_value(1) - ), - CreateCommand::new("rate_conf") - .description("modify user rate") - .add_option( - CreateCommandOption::new(CommandOptionType::User, "user", "user to modify") - .required(true) - ) - .add_option( - CreateCommandOption::new(CommandOptionType::Integer, "user_line", "0 for unlimited") - .required(true) - .add_int_choice("reset", -1) - .add_int_choice("Unlimited", 0) - .add_int_choice("sub 1", 1) - .add_int_choice("sub 2", 2) - .add_int_choice("sub 4", 4) - .add_int_choice("sub 8", 8) - .add_int_choice("sub 16", 16) - .add_int_choice("sub 32", 32) - .add_int_choice("sub 64", 64) - .add_int_choice("sub 128", 128) - .add_int_choice("sub 256", 256) - .add_int_choice("sub 512", 512) - .add_int_choice("sub 1024", 1024) - .add_int_choice("sub 2048", 2048) - .add_int_choice("sub 4096", 4096) - .add_int_choice("sub 8192", 8192) - .add_int_choice("sub 16384", 16384) - .add_int_choice("sub 32768", 32768) - .add_int_choice("sub 65536", 65536) - - ), - CreateCommand::new("model") - .description("set using model") - .add_option( - CreateCommandOption::new(CommandOptionType::String, "model_name", "name of model to use") - .required(true) - .add_string_choice(AIModel::MO4Mini.to_model_discription(), AIModel::MO4Mini.to_model_name()) - .add_string_choice(AIModel::MO3.to_model_discription(), AIModel::MO3.to_model_name()) - .add_string_choice(AIModel::M5Nano.to_model_discription(), AIModel::M5Nano.to_model_name()) - .add_string_choice(AIModel::M5Mini.to_model_discription(), AIModel::M5Mini.to_model_name()) - .add_string_choice(AIModel::M5.to_model_discription(), AIModel::M5.to_model_name()) - ) - ]) - .await - .expect("Failed to create global command"); - } -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 853c420..1e2bd0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,8 @@ -pub mod prefix; +pub mod context; +pub mod commands; +pub mod config; +pub mod lmclient; +pub mod channel; +pub mod events; +pub mod user; pub mod tools; \ No newline at end of file diff --git a/src/lmclient.rs b/src/lmclient.rs new file mode 100644 index 0000000..2d28b5b --- /dev/null +++ b/src/lmclient.rs @@ -0,0 +1,383 @@ +use std::{collections::{HashMap, VecDeque}, sync::Arc}; + +use log::{debug, error, info, warn}; +use openai_dive::v1::{api::Client, resources::response::{items::{FunctionToolCall, FunctionToolCallOutput, ReasoningSummaryPart}, request::{ContentInput, ContentItem, ImageDetailLevel, InputItem, InputMessage, ResponseInput, ResponseInputItem, ResponseParametersBuilder}, response::{OutputContent, ResponseOutput, ResponseStreamEvent, Role}, shared::{ResponseTool, ResponseToolChoice}}}; +use serenity::futures::{StreamExt}; +use tokio::sync::mpsc; + +use crate::{config::Models, context::ObserverContext}; +pub struct LMClient { + pub client: Client, +} + + +/// LMのクライアント +/// レスポンス投げて返すための抽象レイヤ +impl LMClient { + pub fn new(client: Client) -> Self { + Self { client } + } + + pub async fn generate_response( + &self, + ob_ctx: ObserverContext, + lm_context: &LMContext, + max_tokens: Option, + tools: Option>>>, + state_mpsc: Option>, + delta_mpsc: Option>, + parameters: Option, + ) -> Result> { + + debug!("Generating response with context: {:?}", lm_context); + let tools = tools.unwrap_or_default(); + let state_send = |s: String| { + if let Some(tx) = state_mpsc.as_ref() { + let _ = tx.clone().try_send(s); + } + }; + + let delta_send = |s: String| { + if let Some(tx) = delta_mpsc.as_ref() { + let _ = tx.clone().try_send(s); + } + }; + let tool_defs = tools.iter().map(|(_name, tool)| tool.define()).collect::>(); + + + let per_parameters = parameters.unwrap_or_else(|| { + ResponseParametersBuilder::default() + .model(Models::Gpt5Nano).clone() + }) + .max_output_tokens(max_tokens.unwrap_or(100)) + .parallel_tool_calls(true) + .tools(tool_defs).clone(); + + let mut tool_choice = ResponseToolChoice::Auto; + + let mut delta_context = LMContext::new(); + + let mut token_count = 0; + + for i in 0..10 { + let context = lm_context.generate_context_with(&delta_context); + debug!("Iteration {}: Generated context: {:?}", i, context); + let parameters = per_parameters.clone() + .input(context) + .tool_choice(tool_choice.clone()) + .build() + .unwrap(); + + let mut result = self.client.responses().create_stream(parameters).await?; + + while let Some(chunk) = result.next().await { + let chunk = chunk.map_err(|e| { + Box::new(e) as Box + })?; + match chunk { + ResponseStreamEvent::ResponseCreated { sequence_number, response: _ } => { + state_send(format!("Response created (seq {})", sequence_number)); + info!("Response created (seq {})", sequence_number); + }, + ResponseStreamEvent::ResponseQueued { sequence_number, response: _ } => { + state_send(format!("Response queued... (seq {})", sequence_number)); + info!("Response queued (seq {})", sequence_number); + }, + ResponseStreamEvent::ResponseInProgress { sequence_number, response: _ } => { + state_send(format!("Response in progress... (seq {})", sequence_number)); + info!("Response in progress (seq {})", sequence_number); + }, + ResponseStreamEvent::ResponseCompleted { sequence_number, response: _ } => { + info!("Response completed (seq {})", sequence_number); + break; + }, + + ResponseStreamEvent::ResponseFailed { sequence_number, response } => { + error!("Response failed (seq {}): {:?}", sequence_number, response); + return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Response failed"))); + }, + ResponseStreamEvent::ResponseIncomplete { sequence_number, response } => { + error!("Response incomplete (seq {}): {:?}", sequence_number, response); + return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Response incomplete"))); + }, + + ResponseStreamEvent::ResponseOutputItemDone { sequence_number: _, output_index: _, item } => { + match item { + ResponseOutput::Message(output_message) => { + delta_context.add_text( + output_message.content.iter().map(|r| match r { + OutputContent::Text { text, annotations: _ } => text.clone(), + _ => "".to_string(), + }).collect::>().join(""), + Role::Assistant, + ); + }, + ResponseOutput::FunctionToolCall(function_tool_call) => { + state_send(format!("Function tool call: {}", function_tool_call.name)); + delta_context.add_input_item(InputItem::FunctionToolCall( + function_tool_call.into() + )); + }, + ResponseOutput::FileSearchToolCall(file_search_tool_call) => { + delta_context.add_input_item(InputItem::FileSearchToolCall( + file_search_tool_call.into() + )); + }, + ResponseOutput::WebSearchToolCall(web_search_tool_call) => { + delta_context.add_input_item(InputItem::WebSearchToolCall( + web_search_tool_call.into() + )); + }, + ResponseOutput::ComputerToolCall(computer_tool_call) => { + delta_context.add_input_item(InputItem::ComputerToolCall( + computer_tool_call.into() + )); + }, + ResponseOutput::Reasoning(reasoning) => { + delta_context.add_input_item(InputItem::Reasoning( + reasoning.into() + )); + }, + _ => { + warn!("Unhandled output item: {:?}", item); + } + } + }, + + ResponseStreamEvent::ResponseOutputTextDelta { sequence_number: _, item_id: _, output_index: _, content_index: _, delta, logprobs: _ } => { + delta_send(delta); + token_count += 1; + state_send(format!("Generating... ({} tokens)", token_count)); + }, + + ResponseStreamEvent::ResponseRefusalDone { sequence_number: _, item_id: _, output_index: _, content_index: _, refusal } => { + state_send(refusal); + }, + + ResponseStreamEvent::ResponseReasoningSummaryPartDone { sequence_number: _, item_id: _, output_index: _, summary_index: _, part } => { + state_send(match part { + ReasoningSummaryPart::SummaryText { text } => text, + }); + }, + + ResponseStreamEvent::Error { sequence_number, code, message, param } => { + error!("Error (seq {}): {} - {} ({:?})", sequence_number, code, message, param); + return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, message))); + }, + _ => { + warn!("Unhandled stream event: {:?}", chunk); + } + } + } + + let mut outputs = Vec::new(); + let uncompleted_tool_calls = delta_context.get_uncompleted_tool_calls(); + if uncompleted_tool_calls.is_empty() { + break; + } + for tool_call in uncompleted_tool_calls { + debug!("Executing tool call: {:?}", tool_call); + let name = tool_call.name.clone(); + let args = tool_call.arguments.clone(); + let c_id: String = tool_call.call_id.clone(); + + let v_args: serde_json::Value = serde_json::from_str(&args) + .unwrap_or(serde_json::Value::Null); + // $explainがあればとってくる + let explain = v_args.as_object().and_then(|o| + o.get("properties").and_then(|o| + o.as_object().and_then(|o| + o.get("$explain").and_then(|o| + o.as_str() + )))); + if let Some(explain) = explain { + state_send(format!("Executing tool: {} - {}", name, explain)); + } else { + state_send(format!("Executing tool: {}", name)); + } + + // ここでtoolを実行 + if let Some(tool) = tools.get(&name) { + let exec_result = tool.execute(v_args, ob_ctx.clone()).await; + debug!("Tool {} executed with result: {:?}", name, exec_result); + let output = match exec_result { + Ok(res) => FunctionToolCallOutput { + call_id: c_id.clone(), + output: res, + id: None, + }, + Err(err) => FunctionToolCallOutput { + call_id: c_id.clone(), + output: format!("Error: {}", err), + id: None, + }, + }; + outputs.push(output); + } + } + + for output in outputs { + delta_context.add_input_item(InputItem::FunctionToolCallOutput(output)); + } + + + + if i == 8 { + tool_choice = ResponseToolChoice::None; + } + } + + + + Ok(delta_context) + } +} + +/// コンテキスト実態 +/// リングバッファで管理 +#[derive(Debug, Clone)] +pub struct LMContext { + pub buf: VecDeque, + pub max_len: usize, +} + +impl LMContext { + pub fn new() -> Self { + Self { + buf: VecDeque::new(), + max_len: 64, + } + } + + pub fn clear(&mut self) { + self.buf.clear(); + } + + pub fn set_max_len(&mut self, max_len: usize) { + self.max_len = max_len; + } + + pub fn generate_context(&self) -> ResponseInput { + ResponseInput::List(self.buf.clone().into()) + } + + pub fn generate_context_with(&self, additional: &LMContext) -> ResponseInput { + let mut combined = self.buf.clone(); + for item in additional.buf.iter() { + combined.push_back(item.clone()); + } + ResponseInput::List(combined.into()) + } + + pub fn extend(&mut self, other: &LMContext) { + for item in other.buf.iter() { + if let ResponseInputItem::Item(_) = item { + continue; + } + self.buf.push_back(item.clone()); + } + self.trim_len(); + } + + pub fn trim_len(&mut self) { + while self.buf.len() > self.max_len { + self.buf.pop_front(); + } + } + + pub fn add_text(&mut self, text: String, role: Role) { + self.buf.push_back(ResponseInputItem::Message( + InputMessage { + role, + content: ContentInput::Text(text) + } + )); + } + + pub fn add_text_with_image(&mut self, text: String, image_url: String, role: Role, detail: ImageDetailLevel) { + self.buf.push_back(ResponseInputItem::Message( + InputMessage { + role, + content: ContentInput::List(vec![ + ContentItem::Text { text }, + ContentItem::Image { + detail: detail, + file_id: None, + image_url: Some(image_url), + } + ]) + } + )); + } + + pub fn add_message(&mut self, message: InputMessage) { + self.buf.push_back(ResponseInputItem::Message(message)); + } + + pub fn add_input_item(&mut self, item: InputItem) { + self.buf.push_back(ResponseInputItem::Item(item)); + } + + pub fn get_latest(&self) -> Option<&ResponseInputItem> { + self.buf.back() + } + + pub fn get_result(&self) -> String { + let mut result = String::new(); + let latest = self.get_latest(); + if let Some(ResponseInputItem::Message(msg)) = latest { + match &msg.content { + ContentInput::Text(text) => { + result.push_str(text); + }, + ContentInput::List(items) => { + for item in items { + match item { + ContentItem::Text { text } => { + result.push_str(text); + }, + _ => {} + } + } + } + } + } + result + } + + pub fn get_uncompleted_tool_calls(&mut self) -> Vec<&FunctionToolCall> { + // 同じcall_idが存在しないInputItemを集める + let call_id_list = self.buf.iter().filter_map(|item| { + if let ResponseInputItem::Item(InputItem::FunctionToolCallOutput(call)) = item { + Some(call.call_id.clone()) + } else { + None + } + }).collect::>(); + + self.buf.iter().filter_map(|item| { + if let ResponseInputItem::Item(InputItem::FunctionToolCall(call)) = item { + if !call_id_list.contains(&call.call_id) { + return Some(call); + } + } + None + }).collect() + } +} + +#[async_trait::async_trait] +pub trait LMTool: Send + Sync { + fn define(&self) -> ResponseTool { + ResponseTool::Function { + name: self.name(), + description: Some(self.description()), + parameters: self.json_schema(), + strict: false, + } + } + fn json_schema(&self) -> serde_json::Value; + fn description(&self) -> String; + fn name(&self) -> String; + async fn execute(&self, args: serde_json::Value, ob_ctx: ObserverContext) -> Result; +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 292fc17..a11023d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,291 +1,44 @@ -use std::sync::Arc; -use dashmap::DashMap; -mod agent; -mod handler; - -use handler::Handler; - -use call_agent::chat::{api::{UserLocation, WebSearchOptions}, client::{ModelConfig, OpenAIClient}}; -use observer::{prefix::{ASSISTANT_NAME, DISCORD_TOKEN, ENABLE_BROWSER_TOOL, ENABLE_GET_TIME_TOOL, ENABLE_IMAGE_CAPTIONER_TOOL, ENABLE_MEMORY_TOOL, ENABLE_WEB_DEPLOY_TOOL, MAIN_MODEL_API_KEY, MAIN_MODEL_ENDPOINT, MODEL_GENERATE_MAX_TOKENS, MODEL_NAME}, tools::{self, browsing_worker::BrowsingWorker, get_time::GetTime, image_captioner::ImageCaptionerTool, web_deploy::WebDeploy, web_scraper::Browser}}; -use tools::memory::MemoryTool; - -use serenity::model::prelude::*; -use serenity::prelude::*; -use log::error; -use regex::Regex; - -use reqwest::Client as ReqwestClient; -use std::io::Cursor; -use image::{codecs::gif::GifDecoder, io::Reader as ImageReader, AnimationDecoder, DynamicImage, GenericImageView, RgbaImage}; -use base64; - -async fn fetch_and_encode_images(urls: &[String]) -> Vec { - println!("fetch_and_encode_images: {:?}", urls); - // 拡張子チェック&クエリ対応 - let ext_re = Regex::new(r"(?i)\.(png|jpe?g|gif|webp)(?:[?#].*)?$").unwrap(); - // パラメータなし画像URLを即取得する正規表現 - let strict_ext_re = Regex::new(r"(?i)\.(png|jpe?g|gif|webp)$").unwrap(); - let client = ReqwestClient::new(); - let mut total_bytes = 0u64; - let mut out = Vec::new(); - - for url in urls.iter().filter(|u| ext_re.is_match(u)) { - // パラメータなし URL は問答無用でオリジナルを取得 - if strict_ext_re.is_match(url) { - if let Ok(resp) = client.get(url).send().await { - if let Ok(bytes) = resp.bytes().await { - // 拡張子から MIME を決定 - let ext = strict_ext_re - .captures(url) - .and_then(|c| c.get(1)) - .unwrap() - .as_str() - .to_lowercase(); - let mime = match ext.as_str() { - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "gif" => "image/gif", - "webp" => "image/webp", - _ => "application/octet-stream", - }; - out.push(format!("data:{};base64,{}", mime, base64::encode(&bytes))); - } - } - continue; - } - let ext = ext_re.captures(url).and_then(|c| c.get(1)).unwrap().as_str().to_lowercase(); - // HEAD でサイズチェック - let len = client.head(url).send().await - .ok() - .and_then(|r| r.headers() - .get(reqwest::header::CONTENT_LENGTH) - .and_then(|v| v.to_str().ok()?.parse().ok())) - .unwrap_or(0); - if len == 0 || len > 20 * 1024 * 1024 || total_bytes + len > 50 * 1024 * 1024 { - continue; - } - // GET してバイト列取得 - let bytes = match client.get(url).send().await { - Ok(resp) => match resp.bytes().await { - Ok(b) => b, - Err(_) => continue, - }, - Err(_) => continue, - }; - // 解像度チェック - let reader = match ext.as_str() { - "gif" => { - let decoder = GifDecoder::new(Cursor::new(&bytes)).unwrap(); - let mut frames = decoder.into_frames(); - - // Frame を取り出し - let frame = match frames.next() { - Some(Ok(frame)) => frame, - _ => continue, - }; - - // Frame をバッファ(RgbaImage)に変換 - let buf: RgbaImage = frame.into_buffer(); - DynamicImage::ImageRgba8(buf) - } - _ => { - // 通常の画像 - let img = match ImageReader::new(Cursor::new(&bytes)).with_guessed_format() { - Ok(reader) => match reader.decode() { - Ok(i) => i, - Err(_) => continue, - }, - Err(_) => continue, - }; - // 透過があれば白背景でフラット化 - if img.color().has_alpha() { - let (w, h) = img.dimensions(); - let mut bg = RgbaImage::new(w, h); - for (x, y, p) in img.to_rgba8().enumerate_pixels() { - let alpha = p.0[3] as f32 / 255.0; - let inv = 1.0 - alpha; - let r = (p[0] as f32 * alpha + 255.0 * inv) as u8; - let g = (p[1] as f32 * alpha + 255.0 * inv) as u8; - let b = (p[2] as f32 * alpha + 255.0 * inv) as u8; - bg.put_pixel(x, y, image::Rgba([r, g, b, 255])); - } - DynamicImage::ImageRgba8(bg) - } else { - img - } - } - }; - // 解像度を調整(長辺>2000なら縮小、短辺<512なら拡大) - let (w, h) = reader.dimensions(); - let mut img = reader; - // 長辺が2000pxを超える場合は縮小 - if img.dimensions().0.max(img.dimensions().1) > 2000 { - let long = img.dimensions().0.max(img.dimensions().1) as f32; - let scale = 2000.0 / long; - img = img.resize( - (w as f32 * scale) as u32, - (h as f32 * scale) as u32, - image::imageops::FilterType::Lanczos3, - ); - } - // 短辺が512px未満の場合は拡大 - if img.dimensions().0.min(img.dimensions().1) < 512 { - let (w2, h2) = img.dimensions(); - let short = w2.min(h2) as f32; - let scale = 512.0 / short; - img = img.resize( - (w2 as f32 * scale) as u32, - (h2 as f32 * scale) as u32, - image::imageops::FilterType::Lanczos3, - ); - } - // PNGで再エンコード → data URL - let mut buf = Vec::new(); - if img - .write_to(&mut Cursor::new(&mut buf), image::ImageFormat::Png) - .is_err() - { - continue; - } - total_bytes += len; - out.push(format!("data:image/png;base64,{}", base64::encode(&buf))); - } - - out -} - - +use kurosabi::Kurosabi; +use observer::context::ObserverContext; #[tokio::main] async fn main() { - // ロガーの初期化 - env_logger::Builder::new() - .filter_level(log::LevelFilter::Debug) - .filter_module("serenity", log::LevelFilter::Off) // serenityクレートのログを除外 - .filter_module("reqwest", log::LevelFilter::Off) // reqwestクレートのログを除外 - .filter_module("hyper", log::LevelFilter::Off) // hyperクレートのログを除外 - .filter_module("rustls", log::LevelFilter::Off) // rustlsクレートのログを除外 - .filter_module("h2", log::LevelFilter::Off) // h2クレートのログを除外 - .filter_module("tungstenite", log::LevelFilter::Off) // tungsteniteクレートのログを除外 - .filter_module("tracing", log::LevelFilter::Off) // tracingクレートのログを除外 - .filter_module("html5ever", log::LevelFilter::Off) // html5everクレートのログを除外 - .filter_module("selectors", log::LevelFilter::Off) // selectorsクレートのログを除外 - .filter_module("playwright", log::LevelFilter::Off) // markup5everクレートのログを除外 - .init(); - - // Discord Bot のトークンを取得 - let token = *DISCORD_TOKEN; + dotenv::dotenv().ok(); + env_logger::try_init_from_env(env_logger::Env::default().default_filter_or("debug")).unwrap_or_else(|_| ()); - // モデル設定 - let conf = ModelConfig { - model: MODEL_NAME.to_string(), - model_name: Some(ASSISTANT_NAME.to_string()), - parallel_tool_calls: Some(true), - temperature: None, - max_completion_tokens: Some(*MODEL_GENERATE_MAX_TOKENS as u64), - reasoning_effort: Some("low".to_string()), - presence_penalty: None, - strict: Some(false), - top_p: Some(1.0), - web_search_options: None, - }; + // コンテキスト初期化 + let ob_ctx = ObserverContext::new().await; - // 基本となる OpenAIClient を生成し、ツールを定義 - let mut base_client = OpenAIClient::new( - *MAIN_MODEL_ENDPOINT, - Some(*MAIN_MODEL_API_KEY), - ); + let config = ob_ctx.config.clone(); + let mut kurosabi = Kurosabi::with_context(ob_ctx.clone()); - if *ENABLE_BROWSER_TOOL { - base_client.def_tool(Arc::new(Browser::new())); - } - if *ENABLE_MEMORY_TOOL { - base_client.def_tool(Arc::new(MemoryTool::new())); - } - if *ENABLE_GET_TIME_TOOL { - base_client.def_tool(Arc::new(GetTime::new())); - } - if *ENABLE_WEB_DEPLOY_TOOL { - let web_deploy = Arc::new(WebDeploy::new().await); - web_deploy.start_server("0.0.0.0:80".to_string()); - base_client.def_tool(web_deploy); - } - if *ENABLE_IMAGE_CAPTIONER_TOOL { - base_client.def_tool(Arc::new( - ImageCaptionerTool::new({ - - let mut c = OpenAIClient::new( - *MAIN_MODEL_ENDPOINT, - Some(*MAIN_MODEL_API_KEY) - ); - c.set_model_config(&ModelConfig { - model: "gpt-5-nano".to_string(), - model_name: Some("image_captioner".to_string()), - parallel_tool_calls: None, - temperature: None, - max_completion_tokens: Some(*MODEL_GENERATE_MAX_TOKENS as u64), - reasoning_effort: Some("low".to_string()), - presence_penalty: None, - strict: Some(false), - top_p: Some(1.0), - web_search_options: None, - }); - c - }) - )); - } - base_client.def_tool(Arc::new( - BrowsingWorker::new({ - let mut c = OpenAIClient::new( - *MAIN_MODEL_ENDPOINT, - Some(*MAIN_MODEL_API_KEY) - ); - c.set_model_config(&ModelConfig { - model: "gpt-4o-mini-search-preview".to_string(), - model_name: Some("browsing_worker".to_string()), - parallel_tool_calls: None, - temperature: None, - max_completion_tokens: Some(*MODEL_GENERATE_MAX_TOKENS as u64), - reasoning_effort: None, - presence_penalty: None, - strict: Some(false), - top_p: None, - web_search_options: Some(WebSearchOptions { - search_context_size: None, - user_location: UserLocation { - country: Some("JP".to_string()), - region: None, - city: None, - timezone: None, - } - }) - }); - c - }) - ) - ); - base_client.set_model_config(&conf); - let base_client = Arc::new(base_client); + kurosabi.get("/latex_expr_render", |mut c| async move { + c.res.html(include_str!("../data/latex_render.html")); + c + }); - let channels = DashMap::new(); + let server = kurosabi + .server() + .thread(16) + .host(config.web_server_host) + .port(config.web_server_port) + .build(); + println!("server started. Press Ctrl-C to shutdown..."); - // Bot のインテント設定(MESSAGE_CONTENT を含む) - let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - let handler = Handler { - base_client: base_client.clone(), - channels: channels.clone(), - channels_conf: DashMap::new(), - user_configs: DashMap::new(), - }; - handler.load(); - let mut client = Client::builder(&token, intents) - .event_handler(handler) - .await - .expect("Error creating client"); + tokio::select! { + _ = server.run_async() => { + println!("server stopped (run_async returned)"); + } + _ = tokio::signal::ctrl_c() => { + println!("received Ctrl-C, shutting down server and browser engine..."); + } + } - if let Err(e) = client.start().await { - error!("Client error: {:?}", e); + // サーバ停止後にエンジンもshutdown + if let Err(e) = ob_ctx.shutdown().await { + eprintln!("engine shutdown error: {}", e); } + println!("shutdown complete. Exiting."); } \ No newline at end of file diff --git a/src/prefix.rs b/src/prefix.rs deleted file mode 100644 index da9e386..0000000 --- a/src/prefix.rs +++ /dev/null @@ -1,94 +0,0 @@ -use log::warn; -use serde::Deserialize; -use std::fs; -use std::io::Write; - -#[derive(Deserialize, Debug)] -pub struct ModelSettings { - pub model_generate_max_tokens: usize, - pub main_model_endpoint: String, - pub main_model_api_key: String, - pub model_name: String, -} - -#[derive(Deserialize, Debug)] -pub struct PromptSettings { - pub ask_developer_prompt: String, -} - -#[derive(Deserialize, Debug)] -pub struct Settings { - pub assistant_name: String, - pub max_use_tool_count: usize, - pub enable_web_deploy_tool: bool, - pub enable_browser_tool: bool, - pub enable_memory_tool: bool, - pub enable_get_time_tool: bool, - pub enable_image_captioner_tool: bool, - pub sec_per_rate: usize, - pub rate_cp: usize, - pub model: ModelSettings, - pub prompt: PromptSettings, - pub discord_token: String, - pub server_domain: String, - pub admin_users: Vec, -} - -// グローバル変数として設定を保持する -lazy_static::lazy_static! { - pub static ref CONFIG: Settings = Settings::new("config.json"); - pub static ref ASSISTANT_NAME: &'static str = &CONFIG.assistant_name; - pub static ref MAX_USE_TOOL_COUNT: usize = CONFIG.max_use_tool_count; - pub static ref MODEL_GENERATE_MAX_TOKENS: usize = CONFIG.model.model_generate_max_tokens; - pub static ref MAIN_MODEL_ENDPOINT: &'static str = &CONFIG.model.main_model_endpoint; - pub static ref MAIN_MODEL_API_KEY: &'static str = &CONFIG.model.main_model_api_key; - pub static ref ENABLE_WEB_DEPLOY_TOOL: bool = CONFIG.enable_web_deploy_tool; - pub static ref ENABLE_BROWSER_TOOL: bool = CONFIG.enable_browser_tool; - pub static ref ENABLE_MEMORY_TOOL: bool = CONFIG.enable_memory_tool; - pub static ref ENABLE_GET_TIME_TOOL: bool = CONFIG.enable_get_time_tool; - pub static ref ENABLE_IMAGE_CAPTIONER_TOOL: bool = CONFIG.enable_image_captioner_tool; - pub static ref ASK_DEVELOPER_PROMPT: &'static str = &CONFIG.prompt.ask_developer_prompt; - pub static ref DISCORD_TOKEN: &'static str = &CONFIG.discord_token; - pub static ref DOMAIN: &'static str = &CONFIG.server_domain; - pub static ref MODEL_NAME: &'static str = &CONFIG.model.model_name; - pub static ref SEC_PER_RATE: usize = CONFIG.sec_per_rate; - pub static ref RATE_CP: usize = CONFIG.rate_cp; - pub static ref ADMIN_USERS: Vec = CONFIG.admin_users.clone(); -} - -impl Settings { - pub fn new(config_path: &str) -> Self { - let config_data = fs::read_to_string(config_path).unwrap_or_else(|_| { - let default_config = r#" -{ - "assistant_name": "observer", - "max_use_tool_count": 5, - "enable_web_deploy_tool": true, - "enable_browser_tool": true, - "enable_memory_tool": true, - "enable_get_time_tool": true, - "enable_image_captioner_tool": true, - "sec_per_rate": 30, - "rate_cp": 60, - "admin_users": [], - "model": { - "model_generate_max_tokens": 4096, - "main_model_endpoint": "https://api.openai.com/v1/", - "main_model_api_key": "YOUR_API_KEY", - "model_name": "gpt-4o-mini", - }, - "prompt": { - "ask_developer_prompt": "重要: あなたはDiscord上で活動しているObserverという名前の人格で自然に会話します\ntool callを活用してください\n依頼は応答で返し、どうしても詳細な説明が必要な場合のみ質問者に尋ねてください\n適切な場合には、(´-ω-`) のような絵文字を使って感情を表現してください\nあまり'!'を使わないようにしてください\n重要: 一語だけの返答が可能な場合は、一語で応答してください(例: 'うん', 'そう', 'は?', 'えぇ...', '??????')\n重要: バランスのとれた、知的に引き込む、かつ技術的に情報に精通した態度を保ってください\nカジュアルな会話を行います。会話の流れを意識するように\n顔文字やスラングを適度に使い、返答は短めでテンポよくします\n静かでかわいい性格\n最も重要: 周りの人のしゃべり方などを真似するのがもっとも効果的\n応答にメタデータを含めないでください\nネットを使った場合は情報源を示すようにしなさい\n応答が長くなったり、説明がとても長くなる もしくは説明がまとめれたときはweb_deploy_toolを使うと良いでしょう\n記事を書いたらどんな記事を書いたかかるくmemoryしておくとよいでしょう" - }, - "discord_token": "YOUR_API_KEY", - "server_domain": "dev.371tti.net" -} - "#; - let mut file = fs::File::create(config_path).expect("Unable to create config file"); - file.write_all(default_config.as_bytes()).expect("Unable to write default config file"); - warn!("Config file not found. Creating a new one with default settings. please edit 'config.json' file"); - default_config.to_string() - }); - serde_json::from_str(&config_data).expect("Unable to parse config file") - } -} \ No newline at end of file diff --git a/src/tools/browser.rs b/src/tools/browser.rs new file mode 100644 index 0000000..220be2b --- /dev/null +++ b/src/tools/browser.rs @@ -0,0 +1,82 @@ +use log::info; +use wk_371tti_net_crawler::{ScraperAPIBuilder, schema::ScraperResult}; + +use crate::{context::ObserverContext, lmclient::LMTool}; + +pub struct Browser {} + +impl Browser { + pub fn new() -> Browser { + Browser {} + } +} + +#[async_trait::async_trait] +impl LMTool for Browser { + fn json_schema(&self) -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The URL of the webpage to browse." + }, + "with_links": { + "type": "boolean", + "description": "Whether to follow links on the page.", + "default": false + }, + "selector": { + "type": "string", + "description": "CSS selector to extract specific content from the page." + } + }, + "required": ["url", "with_links"] + }) + } + + fn description(&self) -> String { + "Browse a webpage and extract content based on a CSS selector.".to_string() + } + + fn name(&self) -> String { + "browser".to_string() + } + + async fn execute(&self, args: serde_json::Value, ob_ctx: ObserverContext) -> Result { + info!("Browser::execute called with args: {:?}", args); + let url = args.get("url") + .and_then(|v| v.as_str()) + .ok_or("Missing or invalid 'url' parameter".to_string())?; + let selector = args.get("selector") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let with_links = args.get("with_links") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let result = ob_ctx.scraper.scraper( + ScraperAPIBuilder::new(url).set_text_selector(selector).build() + ).await; + + match result { + Ok(scraper_result) => { + match scraper_result { + ScraperResult::Success { status, url, results } => { + let text = results.text; + let links = results.links; + if with_links { + // リンクも含めて返す + Ok(format!("Status: {}\nURL: {}\nExtracted Content:\n{}\nLinks:\n{:?}", status, url, text, links)) + } else { + // テキストのみ返す + Ok(format!("Status: {}\nURL: {}\nExtracted Content:\n{}", status, url, text)) + } + }, + ScraperResult::Failed { error } => Err(format!("Scraper failed: {}", error)), + } + } + Err(e) => Err(format!("Error during browsing: {}", e)), + } + } +} \ No newline at end of file diff --git a/src/tools/browsing_worker.rs b/src/tools/browsing_worker.rs deleted file mode 100644 index 40c43c3..0000000 --- a/src/tools/browsing_worker.rs +++ /dev/null @@ -1,106 +0,0 @@ - -use call_agent::chat::{client::{OpenAIClient, ToolMode}, function::Tool, prompt::{Message, MessageContext}}; -use log::info; -use serde_json::Value; -use tokio::runtime::Runtime; - - -/// **テキストの長さを計算するツール** -pub struct BrowsingWorker { - pub model: OpenAIClient, -} - -impl BrowsingWorker { - pub fn new(model: OpenAIClient) -> Self { - Self { model } - } -} - -impl Tool for BrowsingWorker { - fn def_name(&self) -> &str { - "browsing_worker" - } - - fn def_description(&self) -> &str { - "Get a summary of the web page. If you want to obtain the original page without summarization, please use your browser. You can also provide instructions in natural language along with the URL. It can generate a summary of the entire page very quickly. Please note that others cannot see your response." - } - - fn def_parameters(&self) -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "URL and some natural language query ex.Gather links to the materials located at https://*.*/*/... . please write with user used language eg." - }, - "$explain": { - "type": "string", - "description": "A brief explanation of what you are doing with this tool." - }, - }, - "required": ["query"] - }) - } - fn run(&self, args: Value) -> Result { - info!("BrowsingWorker::run called with args: {:?}", args); - let query = args["query"].as_str() - .ok_or_else(|| "Missing 'query' parameter".to_string())? - .to_string(); - - let mut model = self.model.clone().create_prompt(); - - let result = std::thread::spawn(move || -> Result { - let rt = Runtime::new().expect("Failed to create runtime"); - let messages = Vec::from(vec![ - Message::System { - name: Some("owner".to_string()), - content: "You are an excellent AI assistant who searches for web pages regarding the request content and faithfully summarizes the entire content of that page. Absolutely use the internet to research and compile information.Also, be sure to indicate the source (URL).".to_string() - }, - Message::User { - name: Some("observer".to_string()), - content: vec![ - MessageContext::Text(query.clone()), - ], - } - ]); - - // モデルに投げる - let res: String = rt.block_on(async { - model.add(messages).await; - let return_value = model.generate(None).await.map_err(|_| "Failed to generate".to_string())?; - let mut string = return_value.content.ok_or("Failed to result".to_string())?; - let annotations = &return_value.api_result.response.choices - .unwrap()[0].message.annotations; - let captions = annotations.as_ref() - .unwrap() - .as_array() - .unwrap() - .iter() - .map(|v| - v.as_object() - .unwrap() - .get("url_citation") - .unwrap() - .as_object() - .unwrap() - .get("url") - .unwrap() - .as_str() - .unwrap_or("") - ) - .collect::>() - .join(" ") - .to_string(); - string = format!("{}\n\nLinks: {}", string, captions); - Ok(string) - }).map_err(|e: String| e.to_string())?; - - Ok(res) - }) - .join() - .map_err(|_| "Thread panicked".to_string())??; - - // JSONで結果を返す - Ok(serde_json::json!({ "Summary": result }).to_string()) - } -} \ No newline at end of file diff --git a/src/tools/discord.rs b/src/tools/discord.rs new file mode 100644 index 0000000..b0e72e3 --- /dev/null +++ b/src/tools/discord.rs @@ -0,0 +1,358 @@ +use std::str::FromStr; + +use openai_dive::v1::resources::response::response::Role; +use serde_json::json; +use serenity::all::{ + Builder, ChannelId, ChannelType, CreateMessage, CreateThread, EditMessage, GetMessages, Message, MessageId, ReactionType +}; + +use crate::lmclient::LMTool; + +pub struct DiscordTool; + +impl DiscordTool { + pub fn new() -> DiscordTool { + DiscordTool {} + } + + fn get_str_arg<'a>(args: &'a serde_json::Value, key: &'a str) -> Result<&'a str, String> { + args.get(key) + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("Missing or invalid '{key}' parameter")) + } +} + +#[async_trait::async_trait] +impl LMTool for DiscordTool { + fn name(&self) -> String { + "discord-tool".to_string() + } + + fn description(&self) -> String { + "Interact with Discord: add/remove reactions, create threads, send/edit/fetch messages, and search messages in a channel.".to_string() + } + + fn json_schema(&self) -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "operation": { + "type": "string", + "description": "Discord operation to perform.", + "enum": [ + "add_reaction", + "remove_reaction", + "create_thread", + "send_message", + "edit_message", + "fetch_message", + "search_messages" + ] + }, + "channel_id": { + "type": "string", + "description": "ID of the target channel. Required for all operations." + }, + "message_id": { + "type": "string", + "description": "ID of the target message. Used by: add/remove_reaction, create_thread(from message), send_message(reply_to), edit_message, fetch_message." + }, + "reaction": { + "type": "string", + "description": "Emoji for reactions. Unicode (e.g. 🫠,😱,👍,👈,🤔) or custom emoji ID. Used by: add_reaction, remove_reaction." + }, + "name": { + "type": "string", + "description": "Name of the thread. Used by: create_thread." + }, + "thread_type": { + "type": "string", + "description": "Type of the thread. 'public' or 'private'. Defaults to 'public'. Used by: create_thread.", + "enum": ["public", "private"] + }, + "content": { + "type": "string", + "description": "Message content. Used by: send_message, edit_message." + }, + "reply_to": { + "type": "string", + "description": "Message ID to reply to. Optional. Used by: send_message." + }, + "query": { + "type": "string", + "description": "Keyword to search in message content. Used by: search_messages." + }, + "limit": { + "type": "integer", + "description": "Max number of recent messages to scan (1–100). Defaults to 50. Used by: search_messages." + } + }, + "required": ["operation", "channel_id"] + }) + } + + async fn execute( + &self, + args: serde_json::Value, + ob_ctx: crate::context::ObserverContext, + ) -> Result { + let operation = args + .get("operation") + .and_then(|v| v.as_str()) + .ok_or_else(|| "Missing or invalid 'operation' parameter".to_string())?; + + let channel_id_str = Self::get_str_arg(&args, "channel_id")?; + let channel_id = + ChannelId::from_str(channel_id_str).map_err(|e| format!("Invalid 'channel_id': {e}"))?; + + let http = ob_ctx.discord_client.open().http.clone(); + + match operation { + // -------------------- + // Reaction: add + // -------------------- + "add_reaction" => { + let message_id_str = Self::get_str_arg(&args, "message_id")?; + let reaction = Self::get_str_arg(&args, "reaction")?; + + let message_id = MessageId::from_str(message_id_str) + .map_err(|e| format!("Invalid 'message_id': {e}"))?; + + channel_id + .create_reaction( + http, + message_id, + ReactionType::Unicode(reaction.to_string()), + ) + .await + .map_err(|e| format!("Failed to add reaction: {e}"))?; + + Ok(format!( + "Added reaction '{}' on channel_id='{}', message_id='{}'", + reaction, channel_id_str, message_id_str + )) + } + + // -------------------- + // Reaction: remove + // -------------------- + "remove_reaction" => { + let message_id_str = Self::get_str_arg(&args, "message_id")?; + let reaction = Self::get_str_arg(&args, "reaction")?; + + let message_id = MessageId::from_str(message_id_str) + .map_err(|e| format!("Invalid 'message_id': {e}"))?; + + channel_id + .delete_reaction_emoji( + http, + message_id, + ReactionType::Unicode(reaction.to_string()), + ) + .await + .map_err(|e| format!("Failed to remove reaction: {e}"))?; + + Ok(format!( + "Removed reaction '{}' on channel_id='{}', message_id='{}'", + reaction, channel_id_str, message_id_str + )) + } + + // -------------------- + // Thread: create + // -------------------- + "create_thread" => { + let name = Self::get_str_arg(&args, "name")?; + + let message_id_str_opt = args.get("message_id").and_then(|v| v.as_str()); + let thread_type_str = args + .get("thread_type") + .and_then(|v| v.as_str()) + .unwrap_or("public"); + + let channel_type = match thread_type_str { + "public" => ChannelType::PublicThread, + "private" => ChannelType::PrivateThread, + other => { + return Err(format!( + "Unsupported 'thread_type': {other}. Use 'public' or 'private'." + )); + } + }; + + let message_id_opt: Option = match message_id_str_opt { + Some(s) => { + let mid = MessageId::from_str(s) + .map_err(|e| format!("Invalid 'message_id': {e}"))?; + Some(mid) + } + None => None, + }; + + let builder = CreateThread::new(name).kind(channel_type); + + let res = builder + .execute(&http, (channel_id, message_id_opt)) + .await + .map_err(|e| format!("Failed to create thread: {e}"))?; + + // Chat コンテキスト移動ロジックは元のまま + let mut context = ob_ctx.chat_contexts.get_or_create(channel_id); + context.add_text( + "The context has been moved to the newly created thread. You are now inside the thread you created.".to_string(), + Role::System, + ); + ob_ctx.chat_contexts.marge(res.id, &context); + ob_ctx.chat_contexts.set_enabled(res.id, true); + + Ok(format!( + "Created {thread_type_str} thread '{}' in channel_id='{}' (from message_id='{}')", + name, + channel_id_str, + message_id_str_opt.unwrap_or("-") + )) + } + + // -------------------- + // Send message + // -------------------- + "send_message" => { + let content = Self::get_str_arg(&args, "content")?; + + let reply_to_str = args.get("reply_to").and_then(|v| v.as_str()); + + let mut builder = CreateMessage::new().content(content); + + if let Some(reply_id_str) = reply_to_str { + let reply_id = MessageId::from_str(reply_id_str).map_err(|e| { + format!("Invalid 'reply_to' message_id: {e}") + })?; + builder = builder.reference_message((channel_id, reply_id)); + } + + let msg = channel_id + .send_message(&http, builder) + .await + .map_err(|e| format!("Failed to send message: {e}"))?; + + let result = json!({ + "status": "ok", + "operation": operation, + "channel_id": channel_id_str, + "message_id": msg.id.to_string(), + "content": msg.content, + }); + + Ok(result.to_string()) + } + + // -------------------- + // Edit message + // -------------------- + "edit_message" => { + let message_id_str = Self::get_str_arg(&args, "message_id")?; + let content = Self::get_str_arg(&args, "content")?; + + let message_id = MessageId::from_str(message_id_str) + .map_err(|e| format!("Invalid 'message_id': {e}"))?; + + let builder = EditMessage::new().content(content); + + let msg = channel_id + .edit_message(&http, message_id, builder) + .await + .map_err(|e| format!("Failed to edit message: {e}"))?; + + let result = json!({ + "status": "ok", + "operation": operation, + "channel_id": channel_id_str, + "message_id": message_id_str, + "content": msg.content, + }); + + Ok(result.to_string()) + } + + // -------------------- + // Fetch message + // -------------------- + "fetch_message" => { + let message_id_str = Self::get_str_arg(&args, "message_id")?; + let message_id = MessageId::from_str(message_id_str) + .map_err(|e| format!("Invalid 'message_id': {e}"))?; + + let msg = channel_id + .message(&http, message_id) + .await + .map_err(|e| format!("Failed to fetch message: {e}"))?; + + let result = json!({ + "status": "ok", + "operation": operation, + "channel_id": channel_id_str, + "message_id": message_id_str, + "author_id": msg.author.id.to_string(), + "author_name": msg.author.name, + "content": msg.content, + "timestamp": msg.timestamp.to_string(), + }); + + Ok(result.to_string()) + } + + // -------------------- + // Search messages + // -------------------- + "search_messages" => { + let query = Self::get_str_arg(&args, "query")?; + + let limit = args + .get("limit") + .and_then(|v| v.as_u64()) + .unwrap_or(50) + .min(100) as u8; + + let messages: Vec = channel_id + .messages( + &http, + GetMessages::new().limit(limit), + ) + .await + .map_err(|e| format!("Failed to fetch messages: {e}"))?; + + let lower_query = query.to_lowercase(); + let matched: Vec = messages + .into_iter() + .filter(|m| m.content.to_lowercase().contains(&lower_query)) + .map(|m| { + json!({ + "message_id": m.id.to_string(), + "author_id": m.author.id.to_string(), + "author_name": m.author.name, + "content": m.content, + "timestamp": m.timestamp.to_string(), + }) + }) + .collect(); + + let result = json!({ + "status": "ok", + "operation": operation, + "channel_id": channel_id_str, + "query": query, + "matched_count": matched.len(), + "messages": matched, + }); + + Ok(result.to_string()) + } + + other => Err(format!( + "Unsupported 'operation': {other}. \ + Use one of: add_reaction, remove_reaction, create_thread, \ + send_message, edit_message, fetch_message, search_messages." + )), + } + } +} diff --git a/src/tools/get_time.rs b/src/tools/get_time.rs index 36702ec..82173ac 100644 --- a/src/tools/get_time.rs +++ b/src/tools/get_time.rs @@ -1,10 +1,11 @@ -use call_agent::chat::function::Tool; use chrono::{DateTime, Utc}; use chrono_tz::Tz; use log::info; use std::collections::HashMap; +use crate::{context::ObserverContext, lmclient::LMTool}; + pub struct GetTime {} impl GetTime { @@ -96,33 +97,34 @@ impl GetTime { } } -impl Tool for GetTime { - fn def_name(&self) -> &str { - "get_location_time" +#[async_trait::async_trait] +impl LMTool for GetTime { + fn name(&self) -> String { + "get-location-time".to_string() } - fn def_description(&self) -> &str { - "Get the current time of the location based on the country code" + fn description(&self) -> String { + "Get the current time of the location based on the country code".to_string() } - fn def_parameters(&self) -> serde_json::Value { + fn json_schema(&self) -> serde_json::Value { serde_json::json!({ "type": "object", "properties": { "country_code": { "type": "string", "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'JP', 'FR')" + }, + "$explain": { + "type": "string", + "description": "A brief explanation of what you are doing with this tool." } }, - "$explain": { - "type": "string", - "description": "A brief explanation of what you are doing with this tool." - }, "required": ["country_code"] }) } - fn run(&self, args: serde_json::Value) -> Result { + async fn execute(&self, args: serde_json::Value, _ob_ctx: ObserverContext) -> Result { info!("GetTime::run called with args: {:?}", args); let country_code = args.get("country_code") .and_then(|v| v.as_str()) diff --git a/src/tools/image_captioner.rs b/src/tools/image_captioner.rs deleted file mode 100644 index 133cb6c..0000000 --- a/src/tools/image_captioner.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::{collections::VecDeque, io::Cursor}; - -use call_agent::chat::{client::OpenAIClient, function::Tool, prompt::{Message, MessageContext, MessageImage}}; -use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, GenericImageView, ImageReader, RgbaImage}; -use reqwest::Client; -use serde_json::Value; -use tokio::runtime::Runtime; - -/// **テキストの長さを計算するツール** -pub struct ImageCaptionerTool { - pub model: OpenAIClient, -} - -impl ImageCaptionerTool { - pub fn new(model: OpenAIClient) -> Self { - Self { model } - } - - pub async fn fetch_and_encode_image(url: &str) -> Option { - // Content-Type ヘッダーで MIME タイプとサイズを判定 - let client = Client::new(); - let head = client.head(url).send().await.ok()?; - // MIME タイプ確認 - let ct = head - .headers() - .get(reqwest::header::CONTENT_TYPE)? - .to_str() - .ok()?; - if !ct.starts_with("image/") { - return None; - } - let mime = ct.split(';').next().unwrap(); - // サイズチェック(20MB上限) - let len = head - .headers() - .get(reqwest::header::CONTENT_LENGTH) - .and_then(|v| v.to_str().ok()?.parse::().ok()) - .unwrap_or(0); - if len == 0 || len > 20 * 1024 * 1024 { - return None; - } - // GET してバイト列取得 - let bytes = client.get(url).send().await.ok()?.bytes().await.ok()?; - - // 画像デコード+透過処理/GIFは最初のフレームを抽出 - let img: DynamicImage = if mime == "image/gif" { - let decoder = GifDecoder::new(Cursor::new(&bytes)).ok()?; - let mut frames = decoder.into_frames(); - let frame = frames.next()?.ok()?; - let buf: RgbaImage = frame.into_buffer(); - DynamicImage::ImageRgba8(buf) - } else { - let adyn = ImageReader::new(Cursor::new(&bytes)) - .with_guessed_format().ok()? - .decode().ok()?; - // 透過があれば白背景に合成 - if adyn.color().has_alpha() { - let (w, h) = adyn.dimensions(); - let mut bg = RgbaImage::new(w, h); - for (x, y, p) in adyn.to_rgba8().enumerate_pixels() { - let alpha = p.0[3] as f32 / 255.0; - let inv = 1.0 - alpha; - let r = (p[0] as f32 * alpha + 255.0 * inv) as u8; - let g = (p[1] as f32 * alpha + 255.0 * inv) as u8; - let b = (p[2] as f32 * alpha + 255.0 * inv) as u8; - bg.put_pixel(x, y, image::Rgba([r, g, b, 255])); - } - DynamicImage::ImageRgba8(bg) - } else { - adyn - } - }; - - // 解像度を調整(長辺>2000なら縮小、短辺<512なら拡大) - let (w, h) = img.dimensions(); - let mut img = img; - // 長辺が2000pxを超える場合は縮小 - if img.dimensions().0.max(img.dimensions().1) > 2000 { - let long = img.dimensions().0.max(img.dimensions().1) as f32; - let scale = 2000.0 / long; - img = img.resize( - (w as f32 * scale) as u32, - (h as f32 * scale) as u32, - image::imageops::FilterType::Lanczos3, - ); - } - // 短辺が512px未満の場合は拡大 - if img.dimensions().0.min(img.dimensions().1) < 512 { - let (w2, h2) = img.dimensions(); - let short = w2.min(h2) as f32; - let scale = 512.0 / short; - img = img.resize( - (w2 as f32 * scale) as u32, - (h2 as f32 * scale) as u32, - image::imageops::FilterType::Lanczos3, - ); - } - // PNGで再エンコード → data URL - let mut buf = Vec::new(); - if img - .write_to(&mut Cursor::new(&mut buf), image::ImageFormat::Png) - .is_err() - { - return None; - } - // PNG に再エンコード→data URL - let mut buf = Vec::new(); - img.write_to(&mut Cursor::new(&mut buf), image::ImageFormat::Png).ok()?; - Some(format!("data:image/png;base64,{}", base64::encode(&buf))) - } -} - -impl Tool for ImageCaptionerTool { - fn def_name(&self) -> &str { - "image_captioner" - } - - fn def_description(&self) -> &str { - "Generate a caption for an image. Can use natural language query to get the caption of the image. This can only analyze information to the extent of a summary of the image." - } - - fn def_parameters(&self) -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "Input text to calculate its length." - }, - "query": { - "type": "string", - "description": "Input natural language query to get the caption of the image.ex 'write out all the information in the image.'" - }, - "$explain": { - "type": "string", - "description": "A brief explanation of what you are doing with this tool." - }, - }, - "required": ["url", "query"] - }) - } - fn run(&self, args: Value) -> Result { - // JSONから"url"キーを取得して String 化 - let url = args["url"].as_str() - .ok_or_else(|| "Missing 'url' parameter".to_string())? - .to_string(); - let query = args["query"].as_str() - .ok_or_else(|| "Missing 'query' parameter".to_string())? - .to_string(); - - // self.model を Clone(Arc なら Arc::clone(&self.model)) - let model = self.model.clone(); - - // スレッドに渡すのは url, query, model のみ - let result = std::thread::spawn(move || -> Result { - let rt = Runtime::new().expect("Failed to create runtime"); - - // 画像を取得してエンコード - let data_url = rt.block_on(async { - Self::fetch_and_encode_image(&url).await - }).ok_or_else(|| "Failed to fetch and encode image".to_string())?; - - let messages = VecDeque::from(vec![ - Message::User { - name: Some("observer".to_string()), - content: vec![ - MessageContext::Text(query.clone()), - MessageContext::Image(MessageImage { url: data_url, detail: None }), - ], - } - ]); - - // モデルに投げる - let res = rt.block_on(async { - model.send(&messages, None).await - }).map_err(|_| "Failed to generate caption".to_string())?; - - // レスポンス解析 - let caption = res - .response - .choices - .ok_or_else(|| "Missing choices in response".to_string())? - .get(0) - .ok_or_else(|| "No choice available".to_string())? - .message - .content - .clone() - .ok_or_else(|| "No content in message".to_string())?; - Ok(caption) - }) - .join() - .map_err(|_| "Thread panicked".to_string())??; - - // JSONで結果を返す - Ok(serde_json::json!({ "caption": result }).to_string()) - } -} \ No newline at end of file diff --git a/src/tools/latex.rs b/src/tools/latex.rs new file mode 100644 index 0000000..2e4268a --- /dev/null +++ b/src/tools/latex.rs @@ -0,0 +1,128 @@ +use std::str::FromStr; + +use serde_json::json; +use serenity::all::{ChannelId, CreateAttachment, CreateMessage, MessageId}; +use wk_371tti_net_crawler::CaptureAPIBuilder; + +use crate::{context::ObserverContext, lmclient::LMTool}; + +pub struct LatexExprRenderTool; + +impl LatexExprRenderTool { + pub fn new() -> LatexExprRenderTool { + LatexExprRenderTool {} + } + + pub async fn render(expr: &str, ob_ctx: &ObserverContext) -> Result, Box> { + let url = format!("http://{host}:{port}/latex_expr_render#{expr}", + host=ob_ctx.config.web_server_local_ip.iter().map(|b| b.to_string()).collect::>().join("."), + port=ob_ctx.config.web_server_port, + expr=expr + ); + let png = ob_ctx.scraper.capture_api( + CaptureAPIBuilder::new(&url) + .set_selector(".capture") + .set_wait_millis(200) + .build() + ).await; + png + } +} + +#[async_trait::async_trait] +impl LMTool for LatexExprRenderTool { + fn name(&self) -> String { + "latex_expr_render".to_string() + } + + fn description(&self) -> String { + "Render LaTeX expressions to images and send to Discord.".to_string() + } + + fn json_schema(&self) -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "channel_id": { + "type": "string", + "description": "ID of the target channel on Discord." + }, + "reply_to": { + "type": "string", + "description": "Optional message ID to reply to." + }, + "expression": { + "type": "string", + "description": "The LaTeX expression to render." + } + }, + "required": ["expression", "channel_id"] + }) + } + + async fn execute( + &self, + args: serde_json::Value, + ob_ctx: crate::context::ObserverContext, + ) -> Result { + // --- 引数パース --- + let channel_id_str = args + .get("channel_id") + .and_then(|v| v.as_str()) + .ok_or("Missing or invalid 'channel_id'".to_string())?; + + let expr = args + .get("expression") + .and_then(|v| v.as_str()) + .ok_or("Missing or invalid 'expression'".to_string())?; + + let reply_to = args + .get("reply_to") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()); + + let channel_id = ChannelId::from_str(channel_id_str) + .map_err(|e| format!("Invalid 'channel_id': {e}"))?; + + let reply_message_id = if let Some(id_str) = reply_to { + Some( + MessageId::from_str(id_str) + .map_err(|e| format!("Invalid 'reply_to' message id: {e}"))?, + ) + } else { + None + }; + + // --- LaTeX → 画像レンダリング --- + let png_bytes = Self::render(expr, &ob_ctx) + .await + .map_err(|e| format!("Failed to render LaTeX expression: {e}"))?; + + // --- Discord 送信 --- + let http = ob_ctx.discord_client.open().http.clone(); + + let attachment = CreateAttachment::bytes(png_bytes, "latex.png"); + + let mut builder = CreateMessage::new() + .add_file(attachment); + + if let Some(msg_id) = reply_message_id { + // (ChannelId, MessageId) から MessageReference を作る From 実装がある + builder = builder.reference_message((channel_id, msg_id)); + } + + let msg = channel_id + .send_message(&http, builder) + .await + .map_err(|e| format!("Failed to send Discord message: {e}"))?; + + let result = json!({ + "status": "ok", + "message_id": msg.id.to_string(), + "channel_id": channel_id.to_string(), + "expression": expr, + }); + + Ok(result.to_string()) + } +} \ No newline at end of file diff --git a/src/tools/memory.rs b/src/tools/memory.rs deleted file mode 100644 index 4b49fd0..0000000 --- a/src/tools/memory.rs +++ /dev/null @@ -1,312 +0,0 @@ -use call_agent::chat::function::Tool; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::collections::HashMap; -use std::fs; -use std::io::{Read, Write}; -use std::path::PathBuf; -use std::sync::Mutex; -use chrono::{DateTime, Local}; -use log::error; - -const MEMORY_DIR: &str = "memory"; -const MAX_KEYS: usize = 100; - -/// メモリ操作の結果を表す構造体 -#[derive(Debug, Serialize, Deserialize)] -pub struct MemoryResponse { - pub status: String, - pub memory: Option, -} - -/// MemoryTool 構造体:key-value ペアで記憶を管理(最大 100 件) -pub struct MemoryTool { - memory: Mutex>, -} - -impl MemoryTool { - /// 新しいインスタンスを生成し、memory ディレクトリ内の .md ファイルからメモリを読み込む - pub fn new() -> Self { - let mut mem_map = HashMap::new(); - - // memory ディレクトリがなければ作成 - if let Err(e) = fs::create_dir_all(MEMORY_DIR) { - error!("Failed to create memory directory: {}", e); - } else { - // memory/ 内の .md ファイルをすべて読み込む - if let Ok(entries) = fs::read_dir(MEMORY_DIR) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - if path.is_file() { - if let Some(ext) = path.extension() { - if ext == "md" { - // ファイル名(拡張子除く)を key とする - if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { - let mut file = match fs::File::open(&path) { - Ok(f) => f, - Err(e) => { - error!("Failed to open file {:?}: {}", path, e); - continue; - } - }; - let mut contents = String::new(); - if let Err(e) = file.read_to_string(&mut contents) { - error!("Failed to read file {:?}: {}", path, e); - continue; - } - mem_map.insert(stem.to_string(), contents); - } - } - } - } - } - } - } - - MemoryTool { - memory: Mutex::new(mem_map), - } - } - - /// key-value を追加または更新する。新規キー追加時は最大数を超えるとエラー。 - pub fn add_memory(&self, key: &str, value: &str) -> Result<(), String> { - let mut mem = self.memory.lock().map_err(|_| "Lock error".to_string())?; - if !mem.contains_key(key) && mem.len() >= MAX_KEYS { - return Err(format!("Cannot add new key. Maximum {} keys reached.", MAX_KEYS)); - } - mem.insert(key.to_string(), value.to_string()); - Self::save_to_file(key, value) - } - - /// key の内容に対して新たな値を末尾に追加する (push)。既存の内容があれば改行区切りで追加、 - /// 存在しない場合は新規作成します。 - pub fn push_memory(&self, key: &str, value: &str) -> Result<(), String> { - let mut mem = self.memory.lock().map_err(|_| "Lock error".to_string())?; - let new_value = if let Some(existing) = mem.get(key) { - format!("{}\n{}", existing, value) - } else { - // 新規の場合でも、MAX_KEYS チェックを行う - if mem.len() >= MAX_KEYS { - return Err(format!("Cannot add new key. Maximum {} keys reached.", MAX_KEYS)); - } - value.to_string() - }; - mem.insert(key.to_string(), new_value.clone()); - Self::save_to_file(key, new_value.as_str()) - } - - /// 指定された key の値を取得する。key が None の場合は全件返す。 - /// ※ run() で各エントリの最終更新日時を付与して返します。 - pub fn get_memory(&self, key: Option<&str>) -> HashMap { - let mem = self.memory.lock().unwrap(); - match key { - Some(k) => mem.iter() - .filter(|(key, _)| key.as_str() == k) - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - None => mem.clone(), - } - } - - /// 現在保存されているキー一覧を取得する - pub fn get_keys(&self) -> Vec { - let mem = self.memory.lock().unwrap(); - mem.keys().cloned().collect() - } - - /// 指定した key のメモリをクリアする。key が None の場合は全てのメモリをクリアする - pub fn clear_memory(&self, key: Option<&str>) { - match key { - Some(k) => { - let mut mem = self.memory.lock().unwrap(); - mem.remove(k); - let file_path = Self::get_file_path(k); - if file_path.exists() { - if let Err(e) = fs::remove_file(&file_path) { - error!("Failed to remove file {:?}: {}", file_path, e); - } - } - } - None => { - let mut mem = self.memory.lock().unwrap(); - mem.clear(); - if let Ok(entries) = fs::read_dir(MEMORY_DIR) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - if path.is_file() && path.extension().map(|ext| ext == "md").unwrap_or(false) { - if let Err(e) = fs::remove_file(&path) { - error!("Failed to remove file {:?}: {}", path, e); - } - } - } - } - } - } - } - - /// 指定した key と value を .md ファイルに保存する (上書き)。 - fn save_to_file(key: &str, value: &str) -> Result<(), String> { - let file_path = Self::get_file_path(key); - let mut file = fs::File::create(&file_path) - .map_err(|e| format!("Failed to create file {:?}: {}", file_path, e))?; - file.write_all(value.as_bytes()) - .map_err(|e| format!("Failed to write to file {:?}: {}", file_path, e))?; - Ok(()) - } - - /// 指定した key に対応する .md ファイルのパスを取得する - fn get_file_path(key: &str) -> PathBuf { - let mut path = PathBuf::from(MEMORY_DIR); - path.push(format!("{}.md", key)); - path - } - - /// 指定した key に対応する .md ファイルの最終更新日時を取得する - /// (人間に読みやすい形式: "YYYY-MM-DD HH:MM:SS") - fn get_last_modified(key: &str) -> Option { - let file_path = Self::get_file_path(key); - if let Ok(metadata) = fs::metadata(&file_path) { - if let Ok(modified) = metadata.modified() { - // SystemTime をローカル日時に変換 - let datetime: DateTime = modified.into(); - return Some(datetime.format("%Y-%m-%d %H:%M:%S").to_string()); - } - } - None - } -} - -/// AI Function として利用するための Tool トレイト実装 -impl Tool for MemoryTool { - fn def_name(&self) -> &str { - "memory_tool" - } - - fn def_description(&self) -> &str { - "This tool is used to periodically save links, materials, diary entries, interesting conversations, and fascinating insights. -Each record is stored as a separate .md file in the 'memory' directory, with a maximum of 100 entries. -Use 'add' to create or update an entry, 'push' to append to an existing entry, 'get' to retrieve records (with last modified date), -'get_keys' to list all entries, and 'clear' to remove an entry (if a key is provided) or all entries." - } - - /// JSON Schema に現在のキー一覧を反映した "key" プロパティを動的に生成する - fn def_parameters(&self) -> serde_json::Value { - let current_keys = self.get_keys(); - let key_schema = if current_keys.is_empty() { - json!({ - "type": "string", - "description": "Enter a new key (optional for 'clear' action to clear all memory)." - }) - } else { - json!({ - "anyOf": [ - { - "type": "string", - "enum": current_keys, - "description": "Choose an existing key from the current memory." - }, - { - "type": "string", - "description": "Or enter a new key (optional for 'clear' action to clear all memory)." - } - ] - }) - }; - - json!({ - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["add", "push", "get", "get_keys", "clear"], - "description": "The memory action to perform: 'add' to add/update an entry, 'push' to append to an entry, 'get' to retrieve an entry or all entries (with last modified date), 'get_keys' to list keys, 'clear' to remove an entry (if 'key' is provided) or all entries." - }, - "key": key_schema, - "value": { - "type": "string", - "description": "The value to store (required for 'add' and 'push' actions)." - }, - "$explain": { - "type": "string", - "description": "A brief explanation of what you are doing with this tool." - }, - }, - "required": ["action"] - }) - } - - fn run(&self, args: serde_json::Value) -> Result { - let action = args.get("action") - .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing or invalid 'action' parameter".to_string())?; - - match action { - "add" => { - let key = args.get("key") - .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing or invalid 'key' parameter for add action".to_string())?; - let value = args.get("value") - .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing or invalid 'value' parameter for add action".to_string())?; - self.add_memory(key, value)?; - let response = MemoryResponse { - status: "Memory added/updated.".to_string(), - memory: None, - }; - serde_json::to_string(&response).map_err(|e| e.to_string()) - }, - "push" => { - let key = args.get("key") - .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing or invalid 'key' parameter for push action".to_string())?; - let value = args.get("value") - .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing or invalid 'value' parameter for push action".to_string())?; - self.push_memory(key, value)?; - let response = MemoryResponse { - status: format!("Value pushed to key '{}'.", key), - memory: None, - }; - serde_json::to_string(&response).map_err(|e| e.to_string()) - }, - "get" => { - let key = args.get("key").and_then(|v| v.as_str()); - let mem = self.get_memory(key); - let mut mem_with_meta = serde_json::Map::new(); - for (k, v) in mem.iter() { - mem_with_meta.insert(k.clone(), json!({ - "value": v, - "last_modified": Self::get_last_modified(k) - })); - } - let response = MemoryResponse { - status: "Memory retrieved.".to_string(), - memory: Some(Value::Object(mem_with_meta)), - }; - serde_json::to_string(&response).map_err(|e| e.to_string()) - }, - "get_keys" => { - let keys = self.get_keys(); - let response = MemoryResponse { - status: "Key list retrieved.".to_string(), - memory: Some(json!(keys)), - }; - serde_json::to_string(&response).map_err(|e| e.to_string()) - }, - "clear" => { - let key = args.get("key").and_then(|v| v.as_str()); - self.clear_memory(key); - let status_msg = match key { - Some(k) => format!("Memory for key '{}' cleared.", k), - None => "All memory cleared.".to_string(), - }; - let response = MemoryResponse { - status: status_msg, - memory: None, - }; - serde_json::to_string(&response).map_err(|e| e.to_string()) - }, - _ => Err("Invalid action specified. Use 'add', 'push', 'get', 'get_keys', or 'clear'.".to_string()) - } - } -} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 5cfcb71..bf98942 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,8 +1,11 @@ pub mod get_time; +pub mod browser; +pub mod discord; +pub mod latex; // pub mod www_search; -pub mod web_scraper; -pub mod memory; +// pub mod web_scraper; +// pub mod memory; // pub mod text_len; -pub mod web_deploy; -pub mod image_captioner; -pub mod browsing_worker; \ No newline at end of file +// pub mod web_deploy; +// pub mod image_captioner; +// pub mod browsing_worker; \ No newline at end of file diff --git a/src/tools/stealth.min.js b/src/tools/stealth.min.js deleted file mode 100644 index cb4dda7..0000000 --- a/src/tools/stealth.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Note: Auto-generated, do not update manually. - * Generated by: https://github.com/berstend/puppeteer-extra/tree/master/packages/extract-stealth-evasions - * Generated on: Thu, 26 May 2022 08:27:43 GMT - * License: MIT - */ -(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:'utils => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via `Object.getOwnPropertyDescriptor(window, \'chrome\')`\n Object.defineProperty(window, \'chrome\', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We\'ll extend that later\n })\n }\n\n // That means we\'re running headful and don\'t need to mock anything\n if (\'app\' in window.chrome) {\n return // Nothing to do here\n }\n\n const makeError = {\n ErrorInInvocation: fn => {\n const err = new TypeError(`Error in invocation of app.${fn}()`)\n return utils.stripErrorWithAnchor(\n err,\n `at ${fn} (eval at `\n )\n }\n }\n\n // There\'s a some static data in that property which doesn\'t seem to change,\n // we should periodically check for updates: `JSON.stringify(window.app, null, 2)`\n const STATIC_DATA = JSON.parse(\n `\n{\n "isInstalled": false,\n "InstallState": {\n "DISABLED": "disabled",\n "INSTALLED": "installed",\n "NOT_INSTALLED": "not_installed"\n },\n "RunningState": {\n "CANNOT_RUN": "cannot_run",\n "READY_TO_RUN": "ready_to_run",\n "RUNNING": "running"\n }\n}\n `.trim()\n )\n\n window.chrome.app = {\n ...STATIC_DATA,\n\n get isInstalled() {\n return false\n },\n\n getDetails: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(`getDetails`)\n }\n return null\n },\n getIsInstalled: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(`getIsInstalled`)\n }\n return false\n },\n runningState: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(`runningState`)\n }\n return \'cannot_run\'\n }\n }\n utils.patchToStringNested(window.chrome.app)\n }',_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"utils => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n if ('csi' in window.chrome) {\n return // Nothing to do here\n }\n\n // Check that the Navigation Timing API v1 is available, we need that\n if (!window.performance || !window.performance.timing) {\n return\n }\n\n const { timing } = window.performance\n\n window.chrome.csi = function() {\n return {\n onloadT: timing.domContentLoadedEventEnd,\n startE: timing.navigationStart,\n pageT: Date.now() - timing.navigationStart,\n tran: 15 // Transition type or something\n }\n }\n utils.patchToString(window.chrome.csi)\n }",_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"(utils, { opts }) => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n if ('loadTimes' in window.chrome) {\n return // Nothing to do here\n }\n\n // Check that the Navigation Timing API v1 + v2 is available, we need that\n if (\n !window.performance ||\n !window.performance.timing ||\n !window.PerformancePaintTiming\n ) {\n return\n }\n\n const { performance } = window\n\n // Some stuff is not available on about:blank as it requires a navigation to occur,\n // let's harden the code to not fail then:\n const ntEntryFallback = {\n nextHopProtocol: 'h2',\n type: 'other'\n }\n\n // The API exposes some funky info regarding the connection\n const protocolInfo = {\n get connectionInfo() {\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ntEntry.nextHopProtocol\n },\n get npnNegotiatedProtocol() {\n // NPN is deprecated in favor of ALPN, but this implementation returns the\n // HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n ? ntEntry.nextHopProtocol\n : 'unknown'\n },\n get navigationType() {\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ntEntry.type\n },\n get wasAlternateProtocolAvailable() {\n // The Alternate-Protocol header is deprecated in favor of Alt-Svc\n // (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this\n // should always return false.\n return false\n },\n get wasFetchedViaSpdy() {\n // SPDY is deprecated in favor of HTTP/2, but this implementation returns\n // true for HTTP/2 or HTTP2+QUIC/39 as well.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n },\n get wasNpnNegotiated() {\n // NPN is deprecated in favor of ALPN, but this implementation returns true\n // for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n }\n }\n\n const { timing } = window.performance\n\n // Truncate number to specific number of decimals, most of the `loadTimes` stuff has 3\n function toFixed(num, fixed) {\n var re = new RegExp('^-?\\\\d+(?:.\\\\d{0,' + (fixed || -1) + '})?')\n return num.toString().match(re)[0]\n }\n\n const timingInfo = {\n get firstPaintAfterLoadTime() {\n // This was never actually implemented and always returns 0.\n return 0\n },\n get requestTime() {\n return timing.navigationStart / 1000\n },\n get startLoadTime() {\n return timing.navigationStart / 1000\n },\n get commitLoadTime() {\n return timing.responseStart / 1000\n },\n get finishDocumentLoadTime() {\n return timing.domContentLoadedEventEnd / 1000\n },\n get finishLoadTime() {\n return timing.loadEventEnd / 1000\n },\n get firstPaintTime() {\n const fpEntry = performance.getEntriesByType('paint')[0] || {\n startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occured (`about:blank`)\n }\n return toFixed(\n (fpEntry.startTime + performance.timeOrigin) / 1000,\n 3\n )\n }\n }\n\n window.chrome.loadTimes = function() {\n return {\n ...protocolInfo,\n ...timingInfo\n }\n }\n utils.patchToString(window.chrome.loadTimes)\n }",_args:[{opts:{}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"(utils, { opts, STATIC_DATA }) => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n const existsAlready = 'runtime' in window.chrome\n // `chrome.runtime` is only exposed on secure origins\n const isNotSecure = !window.location.protocol.startsWith('https')\n if (existsAlready || (isNotSecure && !opts.runOnInsecureOrigins)) {\n return // Nothing to do here\n }\n\n window.chrome.runtime = {\n // There's a bunch of static data in that property which doesn't seem to change,\n // we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)`\n ...STATIC_DATA,\n // `chrome.runtime.id` is extension related and returns undefined in Chrome\n get id() {\n return undefined\n },\n // These two require more sophisticated mocks\n connect: null,\n sendMessage: null\n }\n\n const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({\n NoMatchingSignature: new TypeError(\n preamble + `No matching signature.`\n ),\n MustSpecifyExtensionID: new TypeError(\n preamble +\n `${method} called from a webpage must specify an Extension ID (string) for its first argument.`\n ),\n InvalidExtensionID: new TypeError(\n preamble + `Invalid extension id: '${extensionId}'`\n )\n })\n\n // Valid Extension IDs are 32 characters in length and use the letter `a` to `p`:\n // https://source.chromium.org/chromium/chromium/src/+/master:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90\n const isValidExtensionID = str =>\n str.length === 32 && str.toLowerCase().match(/^[a-p]+$/)\n\n /** Mock `chrome.runtime.sendMessage` */\n const sendMessageHandler = {\n apply: function(target, ctx, args) {\n const [extensionId, options, responseCallback] = args || []\n\n // Define custom errors\n const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): `\n const Errors = makeCustomRuntimeErrors(\n errorPreamble,\n `chrome.runtime.sendMessage()`,\n extensionId\n )\n\n // Check if the call signature looks ok\n const noArguments = args.length === 0\n const tooManyArguments = args.length > 4\n const incorrectOptions = options && typeof options !== 'object'\n const incorrectResponseCallback =\n responseCallback && typeof responseCallback !== 'function'\n if (\n noArguments ||\n tooManyArguments ||\n incorrectOptions ||\n incorrectResponseCallback\n ) {\n throw Errors.NoMatchingSignature\n }\n\n // At least 2 arguments are required before we even validate the extension ID\n if (args.length < 2) {\n throw Errors.MustSpecifyExtensionID\n }\n\n // Now let's make sure we got a string as extension ID\n if (typeof extensionId !== 'string') {\n throw Errors.NoMatchingSignature\n }\n\n if (!isValidExtensionID(extensionId)) {\n throw Errors.InvalidExtensionID\n }\n\n return undefined // Normal behavior\n }\n }\n utils.mockWithProxy(\n window.chrome.runtime,\n 'sendMessage',\n function sendMessage() {},\n sendMessageHandler\n )\n\n /**\n * Mock `chrome.runtime.connect`\n *\n * @see https://developer.chrome.com/apps/runtime#method-connect\n */\n const connectHandler = {\n apply: function(target, ctx, args) {\n const [extensionId, connectInfo] = args || []\n\n // Define custom errors\n const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): `\n const Errors = makeCustomRuntimeErrors(\n errorPreamble,\n `chrome.runtime.connect()`,\n extensionId\n )\n\n // Behavior differs a bit from sendMessage:\n const noArguments = args.length === 0\n const emptyStringArgument = args.length === 1 && extensionId === ''\n if (noArguments || emptyStringArgument) {\n throw Errors.MustSpecifyExtensionID\n }\n\n const tooManyArguments = args.length > 2\n const incorrectConnectInfoType =\n connectInfo && typeof connectInfo !== 'object'\n\n if (tooManyArguments || incorrectConnectInfoType) {\n throw Errors.NoMatchingSignature\n }\n\n const extensionIdIsString = typeof extensionId === 'string'\n if (extensionIdIsString && extensionId === '') {\n throw Errors.MustSpecifyExtensionID\n }\n if (extensionIdIsString && !isValidExtensionID(extensionId)) {\n throw Errors.InvalidExtensionID\n }\n\n // There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate\n const validateConnectInfo = ci => {\n // More than a first param connectInfo as been provided\n if (args.length > 1) {\n throw Errors.NoMatchingSignature\n }\n // An empty connectInfo has been provided\n if (Object.keys(ci).length === 0) {\n throw Errors.MustSpecifyExtensionID\n }\n // Loop over all connectInfo props an check them\n Object.entries(ci).forEach(([k, v]) => {\n const isExpected = ['name', 'includeTlsChannelId'].includes(k)\n if (!isExpected) {\n throw new TypeError(\n errorPreamble + `Unexpected property: '${k}'.`\n )\n }\n const MismatchError = (propName, expected, found) =>\n TypeError(\n errorPreamble +\n `Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.`\n )\n if (k === 'name' && typeof v !== 'string') {\n throw MismatchError(k, 'string', typeof v)\n }\n if (k === 'includeTlsChannelId' && typeof v !== 'boolean') {\n throw MismatchError(k, 'boolean', typeof v)\n }\n })\n }\n if (typeof extensionId === 'object') {\n validateConnectInfo(extensionId)\n throw Errors.MustSpecifyExtensionID\n }\n\n // Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well\n return utils.patchToStringNested(makeConnectResponse())\n }\n }\n utils.mockWithProxy(\n window.chrome.runtime,\n 'connect',\n function connect() {},\n connectHandler\n )\n\n function makeConnectResponse() {\n const onSomething = () => ({\n addListener: function addListener() {},\n dispatch: function dispatch() {},\n hasListener: function hasListener() {},\n hasListeners: function hasListeners() {\n return false\n },\n removeListener: function removeListener() {}\n })\n\n const response = {\n name: '',\n sender: undefined,\n disconnect: function disconnect() {},\n onDisconnect: onSomething(),\n onMessage: onSomething(),\n postMessage: function postMessage() {\n if (!arguments.length) {\n throw new TypeError(`Insufficient number of arguments.`)\n }\n throw new Error(`Attempting to use a disconnected port object`)\n }\n }\n return response\n }\n }",_args:[{opts:{runOnInsecureOrigins:!1},STATIC_DATA:{OnInstalledReason:{CHROME_UPDATE:"chrome_update",INSTALL:"install",SHARED_MODULE_UPDATE:"shared_module_update",UPDATE:"update"},OnRestartRequiredReason:{APP_UPDATE:"app_update",OS_UPDATE:"os_update",PERIODIC:"periodic"},PlatformArch:{ARM:"arm",ARM64:"arm64",MIPS:"mips",MIPS64:"mips64",X86_32:"x86-32",X86_64:"x86-64"},PlatformNaclArch:{ARM:"arm",MIPS:"mips",MIPS64:"mips64",X86_32:"x86-32",X86_64:"x86-64"},PlatformOs:{ANDROID:"android",CROS:"cros",LINUX:"linux",MAC:"mac",OPENBSD:"openbsd",WIN:"win"},RequestUpdateCheckStatus:{NO_UPDATE:"no_update",THROTTLED:"throttled",UPDATE_AVAILABLE:"update_available"}}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"utils => {\n /**\n * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.\n *\n * @example\n * video/webm; codecs=\"vp8, vorbis\"\n * video/mp4; codecs=\"avc1.42E01E\"\n * audio/x-m4a;\n * audio/ogg; codecs=\"vorbis\"\n * @param {String} arg\n */\n const parseInput = arg => {\n const [mime, codecStr] = arg.trim().split(';')\n let codecs = []\n if (codecStr && codecStr.includes('codecs=\"')) {\n codecs = codecStr\n .trim()\n .replace(`codecs=\"`, '')\n .replace(`\"`, '')\n .trim()\n .split(',')\n .filter(x => !!x)\n .map(x => x.trim())\n }\n return {\n mime,\n codecStr,\n codecs\n }\n }\n\n const canPlayType = {\n // Intercept certain requests\n apply: function(target, ctx, args) {\n if (!args || !args.length) {\n return target.apply(ctx, args)\n }\n const { mime, codecs } = parseInput(args[0])\n // This specific mp4 codec is missing in Chromium\n if (mime === 'video/mp4') {\n if (codecs.includes('avc1.42E01E')) {\n return 'probably'\n }\n }\n // This mimetype is only supported if no codecs are specified\n if (mime === 'audio/x-m4a' && !codecs.length) {\n return 'maybe'\n }\n\n // This mimetype is only supported if no codecs are specified\n if (mime === 'audio/aac' && !codecs.length) {\n return 'probably'\n }\n // Everything else as usual\n return target.apply(ctx, args)\n }\n }\n\n /* global HTMLMediaElement */\n utils.replaceWithProxy(\n HTMLMediaElement.prototype,\n 'canPlayType',\n canPlayType\n )\n }",_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"(utils, { opts }) => {\n utils.replaceGetterWithProxy(\n Object.getPrototypeOf(navigator),\n 'hardwareConcurrency',\n utils.makeHandler().getterValue(opts.hardwareConcurrency)\n )\n }",_args:[{opts:{hardwareConcurrency:4}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"(utils, { opts }) => {\n const languages = opts.languages.length\n ? opts.languages\n : ['en-US', 'en']\n utils.replaceGetterWithProxy(\n Object.getPrototypeOf(navigator),\n 'languages',\n utils.makeHandler().getterValue(Object.freeze([...languages]))\n )\n }",_args:[{opts:{languages:[]}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"(utils, opts) => {\n const isSecure = document.location.protocol.startsWith('https')\n\n // In headful on secure origins the permission should be \"default\", not \"denied\"\n if (isSecure) {\n utils.replaceGetterWithProxy(Notification, 'permission', {\n apply() {\n return 'default'\n }\n })\n }\n\n // Another weird behavior:\n // On insecure origins in headful the state is \"denied\",\n // whereas in headless it's \"prompt\"\n if (!isSecure) {\n const handler = {\n apply(target, ctx, args) {\n const param = (args || [])[0]\n\n const isNotifications =\n param && param.name && param.name === 'notifications'\n if (!isNotifications) {\n return utils.cache.Reflect.apply(...arguments)\n }\n\n return Promise.resolve(\n Object.setPrototypeOf(\n {\n state: 'denied',\n onchange: null\n },\n PermissionStatus.prototype\n )\n )\n }\n }\n // Note: Don't use `Object.getPrototypeOf` here\n utils.replaceWithProxy(Permissions.prototype, 'query', handler)\n }\n }",_args:[{}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"(utils, { fns, data }) => {\n fns = utils.materializeFns(fns)\n\n // That means we're running headful\n const hasPlugins = 'plugins' in navigator && navigator.plugins.length\n if (hasPlugins) {\n return // nothing to do here\n }\n\n const mimeTypes = fns.generateMimeTypeArray(utils, fns)(data.mimeTypes)\n const plugins = fns.generatePluginArray(utils, fns)(data.plugins)\n\n // Plugin and MimeType cross-reference each other, let's do that now\n // Note: We're looping through `data.plugins` here, not the generated `plugins`\n for (const pluginData of data.plugins) {\n pluginData.__mimeTypes.forEach((type, index) => {\n plugins[pluginData.name][index] = mimeTypes[type]\n\n Object.defineProperty(plugins[pluginData.name], type, {\n value: mimeTypes[type],\n writable: false,\n enumerable: false, // Not enumerable\n configurable: true\n })\n Object.defineProperty(mimeTypes[type], 'enabledPlugin', {\n value:\n type === 'application/x-pnacl'\n ? mimeTypes['application/x-nacl'].enabledPlugin // these reference the same plugin, so we need to re-use the Proxy in order to avoid leaks\n : new Proxy(plugins[pluginData.name], {}), // Prevent circular references\n writable: false,\n enumerable: false, // Important: `JSON.stringify(navigator.plugins)`\n configurable: true\n })\n })\n }\n\n const patchNavigator = (name, value) =>\n utils.replaceProperty(Object.getPrototypeOf(navigator), name, {\n get() {\n return value\n }\n })\n\n patchNavigator('mimeTypes', mimeTypes)\n patchNavigator('plugins', plugins)\n\n // All done\n }",_args:[{fns:{generateMimeTypeArray:"(utils, fns) => mimeTypesData => {\n return fns.generateMagicArray(utils, fns)(\n mimeTypesData,\n MimeTypeArray.prototype,\n MimeType.prototype,\n 'type'\n )\n}",generatePluginArray:"(utils, fns) => pluginsData => {\n return fns.generateMagicArray(utils, fns)(\n pluginsData,\n PluginArray.prototype,\n Plugin.prototype,\n 'name'\n )\n}",generateMagicArray:"(utils, fns) =>\n function(\n dataArray = [],\n proto = MimeTypeArray.prototype,\n itemProto = MimeType.prototype,\n itemMainProp = 'type'\n ) {\n // Quick helper to set props with the same descriptors vanilla is using\n const defineProp = (obj, prop, value) =>\n Object.defineProperty(obj, prop, {\n value,\n writable: false,\n enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)`\n configurable: true\n })\n\n // Loop over our fake data and construct items\n const makeItem = data => {\n const item = {}\n for (const prop of Object.keys(data)) {\n if (prop.startsWith('__')) {\n continue\n }\n defineProp(item, prop, data[prop])\n }\n return patchItem(item, data)\n }\n\n const patchItem = (item, data) => {\n let descriptor = Object.getOwnPropertyDescriptors(item)\n\n // Special case: Plugins have a magic length property which is not enumerable\n // e.g. `navigator.plugins[i].length` should always be the length of the assigned mimeTypes\n if (itemProto === Plugin.prototype) {\n descriptor = {\n ...descriptor,\n length: {\n value: data.__mimeTypes.length,\n writable: false,\n enumerable: false,\n configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`\n }\n }\n }\n\n // We need to spoof a specific `MimeType` or `Plugin` object\n const obj = Object.create(itemProto, descriptor)\n\n // Virtually all property keys are not enumerable in vanilla\n const blacklist = [...Object.keys(data), 'length', 'enabledPlugin']\n return new Proxy(obj, {\n ownKeys(target) {\n return Reflect.ownKeys(target).filter(k => !blacklist.includes(k))\n },\n getOwnPropertyDescriptor(target, prop) {\n if (blacklist.includes(prop)) {\n return undefined\n }\n return Reflect.getOwnPropertyDescriptor(target, prop)\n }\n })\n }\n\n const magicArray = []\n\n // Loop through our fake data and use that to create convincing entities\n dataArray.forEach(data => {\n magicArray.push(makeItem(data))\n })\n\n // Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards\n magicArray.forEach(entry => {\n defineProp(magicArray, entry[itemMainProp], entry)\n })\n\n // This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)`\n const magicArrayObj = Object.create(proto, {\n ...Object.getOwnPropertyDescriptors(magicArray),\n\n // There's one ugly quirk we unfortunately need to take care of:\n // The `MimeTypeArray` prototype has an enumerable `length` property,\n // but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`.\n // To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap.\n length: {\n value: magicArray.length,\n writable: false,\n enumerable: false,\n configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`\n }\n })\n\n // Generate our functional function mocks :-)\n const functionMocks = fns.generateFunctionMocks(utils)(\n proto,\n itemMainProp,\n magicArray\n )\n\n // We need to overlay our custom object with a JS Proxy\n const magicArrayObjProxy = new Proxy(magicArrayObj, {\n get(target, key = '') {\n // Redirect function calls to our custom proxied versions mocking the vanilla behavior\n if (key === 'item') {\n return functionMocks.item\n }\n if (key === 'namedItem') {\n return functionMocks.namedItem\n }\n if (proto === PluginArray.prototype && key === 'refresh') {\n return functionMocks.refresh\n }\n // Everything else can pass through as normal\n return utils.cache.Reflect.get(...arguments)\n },\n ownKeys(target) {\n // There are a couple of quirks where the original property demonstrates \"magical\" behavior that makes no sense\n // This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length`\n // My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly\n // For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing\n // Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing\n const keys = []\n const typeProps = magicArray.map(mt => mt[itemMainProp])\n typeProps.forEach((_, i) => keys.push(`${i}`))\n typeProps.forEach(propName => keys.push(propName))\n return keys\n },\n getOwnPropertyDescriptor(target, prop) {\n if (prop === 'length') {\n return undefined\n }\n return Reflect.getOwnPropertyDescriptor(target, prop)\n }\n })\n\n return magicArrayObjProxy\n }",generateFunctionMocks:"utils => (\n proto,\n itemMainProp,\n dataArray\n) => ({\n /** Returns the MimeType object with the specified index. */\n item: utils.createProxy(proto.item, {\n apply(target, ctx, args) {\n if (!args.length) {\n throw new TypeError(\n `Failed to execute 'item' on '${\n proto[Symbol.toStringTag]\n }': 1 argument required, but only 0 present.`\n )\n }\n // Special behavior alert:\n // - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup\n // - If anything else than an integer (including as string) is provided it will return the first entry\n const isInteger = args[0] && Number.isInteger(Number(args[0])) // Cast potential string to number first, then check for integer\n // Note: Vanilla never returns `undefined`\n return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null\n }\n }),\n /** Returns the MimeType object with the specified name. */\n namedItem: utils.createProxy(proto.namedItem, {\n apply(target, ctx, args) {\n if (!args.length) {\n throw new TypeError(\n `Failed to execute 'namedItem' on '${\n proto[Symbol.toStringTag]\n }': 1 argument required, but only 0 present.`\n )\n }\n return dataArray.find(mt => mt[itemMainProp] === args[0]) || null // Not `undefined`!\n }\n }),\n /** Does nothing and shall return nothing */\n refresh: proto.refresh\n ? utils.createProxy(proto.refresh, {\n apply(target, ctx, args) {\n return undefined\n }\n })\n : undefined\n})"},data:{mimeTypes:[{type:"application/pdf",suffixes:"pdf",description:"",__pluginName:"Chrome PDF Viewer"},{type:"application/x-google-chrome-pdf",suffixes:"pdf",description:"Portable Document Format",__pluginName:"Chrome PDF Plugin"},{type:"application/x-nacl",suffixes:"",description:"Native Client Executable",__pluginName:"Native Client"},{type:"application/x-pnacl",suffixes:"",description:"Portable Native Client Executable",__pluginName:"Native Client"}],plugins:[{name:"Chrome PDF Plugin",filename:"internal-pdf-viewer",description:"Portable Document Format",__mimeTypes:["application/x-google-chrome-pdf"]},{name:"Chrome PDF Viewer",filename:"mhjfbmdgcfjbbpaeojofohoefgiehjai",description:"",__mimeTypes:["application/pdf"]},{name:"Native Client",filename:"internal-nacl-plugin",description:"",__mimeTypes:["application/x-nacl","application/x-pnacl"]}]}}]}),!1===navigator.webdriver||void 0===navigator.webdriver||delete Object.getPrototypeOf(navigator).webdriver,(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"(utils, opts) => {\n const getParameterProxyHandler = {\n apply: function(target, ctx, args) {\n const param = (args || [])[0]\n const result = utils.cache.Reflect.apply(target, ctx, args)\n // UNMASKED_VENDOR_WEBGL\n if (param === 37445) {\n return opts.vendor || 'Intel Inc.' // default in headless: Google Inc.\n }\n // UNMASKED_RENDERER_WEBGL\n if (param === 37446) {\n return opts.renderer || 'Intel Iris OpenGL Engine' // default in headless: Google SwiftShader\n }\n return result\n }\n }\n\n // There's more than one WebGL rendering context\n // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext#Browser_compatibility\n // To find out the original values here: Object.getOwnPropertyDescriptors(WebGLRenderingContext.prototype.getParameter)\n const addProxy = (obj, propName) => {\n utils.replaceWithProxy(obj, propName, getParameterProxyHandler)\n }\n // For whatever weird reason loops don't play nice with Object.defineProperty, here's the next best thing:\n addProxy(WebGLRenderingContext.prototype, 'getParameter')\n addProxy(WebGL2RenderingContext.prototype, 'getParameter')\n }",_args:[{}]}),(()=>{try{if(window.outerWidth&&window.outerHeight)return;const n=85;window.outerWidth=window.innerWidth,window.outerHeight=window.innerHeight+n}catch(n){}})(),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})"},_mainFunction:"(utils, opts) => {\n try {\n // Adds a contentWindow proxy to the provided iframe element\n const addContentWindowProxy = iframe => {\n const contentWindowProxy = {\n get(target, key) {\n // Now to the interesting part:\n // We actually make this thing behave like a regular iframe window,\n // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)\n // That makes it possible for these assertions to be correct:\n // iframe.contentWindow.self === window.top // must be false\n if (key === 'self') {\n return this\n }\n // iframe.contentWindow.frameElement === iframe // must be true\n if (key === 'frameElement') {\n return iframe\n }\n // Intercept iframe.contentWindow[0] to hide the property 0 added by the proxy.\n if (key === '0') {\n return undefined\n }\n return Reflect.get(target, key)\n }\n }\n\n if (!iframe.contentWindow) {\n const proxy = new Proxy(window, contentWindowProxy)\n Object.defineProperty(iframe, 'contentWindow', {\n get() {\n return proxy\n },\n set(newValue) {\n return newValue // contentWindow is immutable\n },\n enumerable: true,\n configurable: false\n })\n }\n }\n\n // Handles iframe element creation, augments `srcdoc` property so we can intercept further\n const handleIframeCreation = (target, thisArg, args) => {\n const iframe = target.apply(thisArg, args)\n\n // We need to keep the originals around\n const _iframe = iframe\n const _srcdoc = _iframe.srcdoc\n\n // Add hook for the srcdoc property\n // We need to be very surgical here to not break other iframes by accident\n Object.defineProperty(iframe, 'srcdoc', {\n configurable: true, // Important, so we can reset this later\n get: function() {\n return _srcdoc\n },\n set: function(newValue) {\n addContentWindowProxy(this)\n // Reset property, the hook is only needed once\n Object.defineProperty(iframe, 'srcdoc', {\n configurable: false,\n writable: false,\n value: _srcdoc\n })\n _iframe.srcdoc = newValue\n }\n })\n return iframe\n }\n\n // Adds a hook to intercept iframe creation events\n const addIframeCreationSniffer = () => {\n /* global document */\n const createElementHandler = {\n // Make toString() native\n get(target, key) {\n return Reflect.get(target, key)\n },\n apply: function(target, thisArg, args) {\n const isIframe =\n args && args.length && `${args[0]}`.toLowerCase() === 'iframe'\n if (!isIframe) {\n // Everything as usual\n return target.apply(thisArg, args)\n } else {\n return handleIframeCreation(target, thisArg, args)\n }\n }\n }\n // All this just due to iframes with srcdoc bug\n utils.replaceWithProxy(\n document,\n 'createElement',\n createElementHandler\n )\n }\n\n // Let's go\n addIframeCreationSniffer()\n } catch (err) {\n // console.warn(err)\n }\n }",_args:[]}); \ No newline at end of file diff --git a/src/tools/text_len.rs b/src/tools/text_len.rs deleted file mode 100644 index f8030a3..0000000 --- a/src/tools/text_len.rs +++ /dev/null @@ -1,46 +0,0 @@ -use call_agent::chat::function::Tool; -use serde_json::Value; - -/// **テキストの長さを計算するツール** -pub struct TextLengthTool; - -impl TextLengthTool { - pub fn new() -> Self { - Self - } -} - -impl Tool for TextLengthTool { - fn def_name(&self) -> &str { - "text_length_tool" - } - - fn def_description(&self) -> &str { - "Returns the length of the input text." - } - - fn def_parameters(&self) -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Input text to calculate its length" - } - }, - "required": ["text"] - }) - } - - fn run(&self, args: Value) -> Result { - // JSONから"text"キーを取得 - let text = args["text"].as_str() - .ok_or_else(|| "Missing 'text' parameter".to_string())?; - - // 長さを計算 - let length = text.len(); - - // JSONで結果を返す - Ok(serde_json::json!({ "length": length }).to_string()) - } -} \ No newline at end of file diff --git a/src/tools/web_scraper.rs b/src/tools/web_scraper.rs deleted file mode 100644 index b02f820..0000000 --- a/src/tools/web_scraper.rs +++ /dev/null @@ -1,475 +0,0 @@ -/*! -Webスクレイピングツール (Rust版) - -このツールは指定されたURLからHTMLページを取得し、ユーザーが指定する -CSSセレクターに基づいて、該当する要素のテキストやリンク情報を抽出します。 - -【対応モード】 -- `reqwest`: 高速スクレイピング(JavaScript 不可) -- `playwright`: JavaScript 対応のスクレイピング(動的サイト向け) -- `auto`: 自動判定(CloudflareやJSが必要なら Playwright 使用) - -【使い方の例】 -以下のJSONパラメータを渡すと、指定したURLのページから「p, h1, h2, h3, h4, h5, h6, a」タグの内容とリンク情報が抽出され、JSON形式で返されます: -{ - "url": "https://example.com", - "selector": "p, h1, h2, h3, h4, h5, h6, a", - "mode": "auto" -} -*/ - -use call_agent::chat::function::Tool; -use regex::Regex; -use reqwest::{Client, Url}; -use scraper::{Html, Selector}; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use playwright::Playwright; -use tokio::{self}; -use std::fmt; - -const MAX_FILE_SIZE: u64 = 5 * 1024 * 1024; // 5MB -const WHITELIST: [&str; 110] = [ - "application/json", - "text/markdown", - "text/plain", - "text/csv", - "application/javascript", - "text/css", - "text/x-rust", - "text/x-python", - "text/x-java-source", - "text/x-c", - "text/x-c++src", - "text/x-go", - "application/xml", - "text/xml", - "application/xhtml+xml", - "application/x-httpd-php", - "text/x-php", - "text/javascript", - "application/ecmascript", - "text/x-shellscript", - "application/x-sh", - "text/x-ruby", - "application/x-ruby", - "text/x-perl", - "application/x-perl", - "text/x-sql", - "application/sql", - "text/x-scala", - "text/x-erlang", - "text/x-haskell", - "text/x-cobol", - "text/x-fortran", - "application/x-latex", - "text/x-latex", - "application/x-sqlite3", - "application/atom+xml", - "application/rss+xml", - "application/vnd.api+json", - "application/x-yaml", - "application/ld+json", - "text/vnd.graphviz", - "text/x-csh", - "application/typescript", - "text/x-d", - "text/x-swift", - "text/x-kotlin", - "text/x-objective-c", - "text/x-pascal", - "text/x-vb", - "text/x-r", - "text/x-dart", - "application/x-prolog", - "text/x-prolog", - "text/x-asciidoc", - "text/x-org", - "application/json5", - "text/x-sqlite", - "application/x-tex", - "text/x-tex", - "application/x-bibtex", - "text/x-bibtex", - "text/rtf", - "application/edn", - "text/x-clojure", - "application/x-clojure", - "text/x-lisp", - "application/x-lisp", - "text/x-config", - "text/x-env", - "text/x-applescript", - "text/x-scm", - "text/x-rst", - "application/x-powershell", - "text/x-powershell", - "text/x-vhdl", - "text/x-verilog", - "text/x-vue", - "text/x-svelte", - "text/x-coffeescript", - "text/x-lua", - "application/x-lua", - "text/x-rpm-spec", - "text/x-dockerfile", - "text/x-ini", - "text/x-properties", - "text/x-toml", - "application/x-toml", - "text/x-xslt", - "application/xml-dtd", - "text/x-json", - "application/x-json", - "text/x-cmake", - "text/x-diff", - "text/x-log", - "text/x-nsis", - "text/x-asm", - "text/x-lilypond", - "text/x-llvm", - "text/x-cl", - "text/x-tcl", - "application/x-tcl", - "text/x-puppet", - "application/x-puppet", - "text/x-nim", - "text/x-zig", - "text/x-crystal", - "text/x-fsharp", - "text/x-vbscript", - "text/x-msdos-batch", - "text/x-awk", -]; - -#[derive(Debug)] -pub enum ScraperError { - NetworkError, - ParseError, - TimeoutError, - FileTooLargeError, - InitializationError, - ContextError, - LaunchError, - PageError, - ScriptError, - UnknownError, -} - -impl fmt::Display for ScraperError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ScraperError::NetworkError => write!(f, "Network error occurred."), - ScraperError::ParseError => write!(f, "Error occurred while parsing data."), - ScraperError::TimeoutError => write!(f, "Request timed out."), - ScraperError::FileTooLargeError => write!(f, "File size is too large."), - ScraperError::InitializationError => write!(f, "Failed to initialize Playwright."), - ScraperError::ContextError => write!(f, "Failed to create Playwright context."), - ScraperError::LaunchError => write!(f, "Failed to launch Playwright browser."), - ScraperError::PageError => write!(f, "Failed to create Playwright page."), - ScraperError::ScriptError => write!(f, "Failed to add Playwright script."), - ScraperError::UnknownError => write!(f, "An unknown error occurred."), - } - } -} - -impl std::error::Error for ScraperError {} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ScrapedItem { - pub text: String, // 要素内のテキスト - pub link: Option, // リンクの href 属性 -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ScrapedData { - pub items: Vec, -} - -#[derive(Clone)] -pub struct Browser { - client: Client, -} - -impl Browser { - /// 新しいWebScraperインスタンスを生成する - pub fn new() -> Self { - let client = Client::builder() - .user_agent("Mozilla/5.0 (compatible; Browser/1.0)") - .timeout(std::time::Duration::from_secs(10)) - .build() - .expect("Failed to build reqwest client"); - Browser { client } - } - - /// 通常の HTTP スクレイピング(reqwest) - pub async fn scrape_reqwest( - &self, - url: &str, - selector_str: &str, - ) -> Result { - // 5MBを上限とする - let url = Url::parse(url).map_err(|_| ScraperError::ParseError)?; - - let response = self.client.get(url) - .send() - .await - .map_err(|_| ScraperError::NetworkError)?; - - // ヘッダーにContent-Lengthがある場合、サイズをチェックする - if let Some(len) = response.content_length() { - if len > MAX_FILE_SIZE { - return Err(ScraperError::FileTooLargeError); - } - } - - let content_type = response.headers().get("content-type").and_then(|v| v.to_str().ok()).unwrap_or("").to_string(); - let response = response.error_for_status().map_err(|_| ScraperError::NetworkError)?; - - let body_bytes = response.bytes().await.map_err(|_| ScraperError::NetworkError)?; - if body_bytes.len() > MAX_FILE_SIZE as usize { - return Err(ScraperError::FileTooLargeError); - } - - // UTF-8 でデコードできない場合はエラーを返す - let body = String::from_utf8(body_bytes.to_vec()).map_err(|_| ScraperError::ParseError)?; - - if !content_type.contains("text/html") { - if WHITELIST.iter().any(|&item| content_type.contains(item)) { - return Ok(ScrapedData { - items: vec![ScrapedItem { - text: body, - link: None, - }], - }); - } - return Err(ScraperError::ParseError); - } - - let document = Html::parse_document(&body); - let selector = Selector::parse(selector_str).map_err(|_| ScraperError::ParseError)?; - - let items: Vec = document - .select(&selector) - .map(|element| { - let raw_text = element.text().collect::>().join(" "); - let text = raw_text.split_whitespace().collect::>().join(" "); - - let href = element.value().attr("href").map(|s| s.to_string()); - let link = element.value().attr("src").map(|s| s.to_string()); - - ScrapedItem { text, link: href.or(link) } - }) - .filter(|item| !item.text.is_empty() || item.link.is_some()) - .collect(); - - Ok(ScrapedData { items }) - } - - - /// Playwright を使った JS レンダリング対応スクレイピング - pub async fn scrape_playwright( - &self, - url: &str, - selector_str: &str, - ) -> Result { - let playwright = Playwright::initialize().await.map_err(|_| ScraperError::InitializationError)?; - let browser = playwright.chromium().launcher().headless(true).args(&vec![ - // 一応,, - String::from("--enable-features=BlockInsecurePrivateNetworkRequests"), - String::from("--disable-file-system"), - String::from("--disable-popup-blocking"), - String::from("--disable-web-security"), - String::from("--disable-webgl"), - String::from("--disable-webrtc"), - String::from("--disable-camera"), - String::from("--disable-microphone"), - String::from("--disable-media-source"), - String::from("--host-resolver-rules=MAP localhost 127.255.255.255"), - ]).launch().await.map_err(|_| ScraperError::LaunchError)?; - let context = browser.context_builder() - .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") - .build() - .await.map_err(|_| ScraperError::ContextError)?; - - let page = context.new_page().await.map_err(|_| ScraperError::PageError)?; - page.add_init_script(include_str!("stealth.min.js")).await.map_err(|_| ScraperError::ScriptError)?; - page.goto_builder(url).timeout(10000.0).goto().await.map_err(|_| ScraperError::NetworkError)?; - page.wait_for_selector_builder(selector_str).wait_for_selector().await.map_err(|_| ScraperError::TimeoutError)?; - let elements = page.eval( - &format!("Array.from(document.querySelectorAll('{}')).map(e => ({{\ - tag: e.tagName.toLowerCase(),\ - text: e.tagName.toLowerCase()==='img'\ - ? (e.getAttribute('alt')||e.getAttribute('title')||'')\ - : (e.innerText||''),\ - href: e.getAttribute('href')||'',\ - src: e.getAttribute('src')||'',\ - alt: e.getAttribute('alt')||'',\ - title: e.getAttribute('title')||''\ - }}))", selector_str) - ).await.map_err(|_| ScraperError::ParseError)?; - - let items: Vec = serde_json::from_value(elements).map_err(|_| ScraperError::ParseError)?; - - Ok(ScrapedData { items }) - } - - pub fn compress_content(content: ScrapedData, seek_pos: usize, len: usize) -> String { - let mut combined_text = String::new(); - - // すべてのテキストとリンクをまとめる - for item in content.items { - combined_text.push_str(&item.text); - combined_text.push_str(" "); - - if let Some(link) = item.link { - combined_text.push_str(&format!("({})", link)); - } - } - - let total_chars = combined_text.chars().count(); // 全体の文字数 - - // seek_posが文字数を超えていたら空文字を返す - if seek_pos >= total_chars { - return format!("...<0 characters remaining>"); - } - - // seek_posから取得可能な文字数 - let available_chars = total_chars - seek_pos; - - // 切り出す範囲を計算 - let sliced_text: String = combined_text - .chars() - .skip(seek_pos) - .take(len) - .collect(); - - // 残り文字数を正しく計算 - let remaining_chars = available_chars.saturating_sub(sliced_text.chars().count()); - - format!("{}...<{} characters remaining>", sliced_text, remaining_chars) - } - - pub fn is_safe_url(url: &str) -> bool { - // ローカルホスト・プライベートIP・ファイルスキームをブロック - let dangerous_patterns = vec![ - r"^http://localhost(:\d+)?", // localhost - r"^http://127\.\d+\.\d+\.\d+(:\d+)?", // 127.x.x.x - r"^http://192\.168\.\d+\.\d+(:\d+)?", // 192.168.x.x - r"^http://10\.\d+\.\d+\.\d+(:\d+)?", // 10.x.x.x - r"^http://172\.(1[6-9]|2[0-9]|3[0-1])\.\d+\.\d+(:\d+)?", // 172.16.x.x - 172.31.x.x - r"^file://", // file:// スキーム - ]; - - // 正規表現でURLをチェック - for pattern in &dangerous_patterns { - let re = Regex::new(pattern).unwrap(); - if re.is_match(url) { - return false; // 危険なURL - } - } - true // 安全なURL - } - -} - -/// AI Functionとして利用するための `Tool` トレイト実装 -impl Tool for Browser { - fn def_name(&self) -> &str { - "browser" - } - - fn def_description(&self) -> &str { -"Extracts webpage content using a CSS selector (avoid '*', use specific tags like 'p, h1, h2, h3, a'). -Supports 'reqwest' (fast) and 'playwright' (for JavaScript-heavy pages). -Use 'seek_pos' and 'max_length' to paginate (e.g., 0-3999, 4000-3999) for full extraction. -If the content is too long, use 'seek_pos' and 'max_length' to paginate the results. -**If no content is retrieved, consider:** -- The site may require JavaScript rendering ('playwright' mode). -- The selector may be incorrect. -- The site may block scraping. -IMPORTANT: **Always must include the scraped URL at the end of your response.** -IMPORTANT: Do not use imaginary URLs. -For searching, use Bing." - } - - fn def_parameters(&self) -> serde_json::Value { - json!({ - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "Target webpage URL (e.g., 'https://www.bing.com/search?q={key}+{key}...')." - }, - "selector": { - "type": "string", - "description": "CSS selector to extract elements(ex., 'p, h1, h2, h3, a, img, video, audio, image...') " - }, - "mode": { - "type": "string", - "enum": ["playwright"], - "description": "Scraping method: 'playwright' (e.g., 'playwright', if use bing = 'playwright')." - }, - "seek_pos": { - "type": "integer", - "description": "Character position to start extracting content (e.g., 0, 4000, etc)." - }, - "max_length": { - "type": "integer", - "description": "Maximum length of extracted content (e.g., 3999, 7999, (200000[ALL]))." - }, - "$explain": { - "type": "string", - "description": "A brief explanation of what you are doing with this tool." - }, - }, - "required": ["url", "selector", "seek_pos", "max_length"] - }) - } - - - fn run(&self, args: serde_json::Value) -> Result { - let url = args.get("url") - .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing 'url' parameter".to_string())? - .to_string(); - - let selector = args.get("selector") - .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing 'selector' parameter".to_string())? - .to_string(); - - let mode = args.get("mode") - .and_then(|v| v.as_str()) - .unwrap_or("reqwest") - .to_string(); - - let seek_pos = args.get("seek_pos") - .and_then(|v| v.as_u64()) - .ok_or_else(|| "Missing 'seek_pos' parameter".to_string())? as usize; - - let max_length = args.get("max_length") - .and_then(|v| v.as_u64()) - .ok_or_else(|| "Missing 'max_length' parameter".to_string())? as usize; - - let scraper = self.clone(); - - Browser::is_safe_url(&url).then(|| ()).ok_or_else(|| "Are you try hacking me?".to_string())?; - - let result = std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); - match mode.as_str() { - "reqwest" => rt.block_on(scraper.scrape_reqwest(&url, &selector)) - .or_else(|_| rt.block_on(scraper.scrape_playwright(&url, &selector))), - "playwright" => rt.block_on(scraper.scrape_playwright(&url, &selector)), - _ => Err(ScraperError::UnknownError), - } - }) - .join() - .map_err(|_| "Thread panicked".to_string())? - .map_err(|e| format!("Scrape error: {}", e))?; - - let res = Browser::compress_content(result, seek_pos, max_length); - serde_json::to_string(&res).map_err(|e| format!("Serialization error: {}", e)) - } -} diff --git a/src/tools/www_search.rs b/src/tools/www_search.rs deleted file mode 100644 index 56cfcfc..0000000 --- a/src/tools/www_search.rs +++ /dev/null @@ -1,92 +0,0 @@ -use playwright::Playwright; -use urlencoding::encode; - -use super::web_scraper::ScraperError; - -/// 検索結果のうち、hover‑url 属性を持つ要素のテキスト(題名)と属性値(リンク)を保持する構造体 -#[derive(Debug, Clone)] -pub struct BingSearchResult { - pub title: String, - pub link: String, // hover‑url 属性の値 -} - -/// Bing の検索結果ページから、hover‑url 属性を持つ `` 要素を抽出するスクレイパー -pub struct BingSearchScraper { - playwright: Playwright, -} - -impl BingSearchScraper { - /// 新しいインスタンスを生成する - pub async fn new() -> Result> { - let playwright = Playwright::initialize().await?; - Ok(BingSearchScraper { playwright }) - } - - /// 指定したクエリで Bing 検索を実行し、hover‑url 属性を持つ `` 要素から題名とリンクを取得する - pub async fn search(&self, query: &str) -> Result, Box> { - // クエリを URL エンコードして Bing の検索 URL を生成 - let encoded_query = encode(query); - let url = format!("https://www.bing.com/search?q={}", encoded_query); - - // ブラウザ起動 (headless モードではなく表示状態にしてデバッグ可能) - let browser = self.playwright - .chromium() - .launcher() - .headless(false) - .args(&vec![ - String::from("--enable-features=BlockInsecurePrivateNetworkRequests"), - String::from("--disable-file-system"), - String::from("--disable-popup-blocking"), - String::from("--disable-web-security"), - String::from("--disable-webgl"), - String::from("--disable-webrtc"), - String::from("--disable-camera"), - String::from("--disable-microphone"), - String::from("--disable-media-source"), - String::from("--host-resolver-rules=MAP localhost 127.255.255.255"), - ]) - .launch() - .await - .map_err(|_| ScraperError::LaunchError)?; - - let context = browser - .context_builder() - .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko, OBSERVER) Chrome/120.0.0.0 Safari/537.36") - .build() - .await - .map_err(|_| ScraperError::ContextError)?; - let page = context.new_page().await.map_err(|_| ScraperError::PageError)?; - - // 任意の初期化スクリプト(stealth対策など) - page.add_init_script(include_str!("stealth.min.js")) - .await - .map_err(|_| ScraperError::ScriptError)?; - - // 指定の URL に移動 (タイムアウトは 10秒) - page.goto_builder(&url) - .timeout(10000.0) - .goto() - .await - .map_err(|_| ScraperError::NetworkError)?; - - // まず、hover‑url 属性を持つ `` 要素が存在するのを待機 - page.wait_for_selector_builder("a[hover-url]") - .timeout(10000000.0) - .wait_for_selector() - .await - .map_err(|_| ScraperError::TimeoutError)?; - - // ページ内のすべての a[hover-url] 要素を取得 - let elements = page.query_selector_all("a[hover-url]").await?; - let mut results = Vec::new(); - - for element in elements { - let title = element.inner_text().await?; - let link = element.get_attribute("hover-url").await?.unwrap_or_default(); - results.push(BingSearchResult { title, link }); - } - - browser.close().await?; - Ok(results) - } -} \ No newline at end of file diff --git a/src/user.rs b/src/user.rs new file mode 100644 index 0000000..b4058ac --- /dev/null +++ b/src/user.rs @@ -0,0 +1,56 @@ +use dashmap::DashMap; +use serenity::all::UserId; + +use crate::config::Models; + +/// ユーザー情報のプール +pub struct UserContexts { + pub contexts: DashMap, +} + +/// ユーザー情報の構造体 +#[derive(Clone)] +pub struct UserContext { + pub user_id: UserId, + pub main_model: Models, + pub rate_line: u64, +} + +impl UserContext { + pub fn new(user_id: UserId) -> UserContext { + UserContext { + user_id, + main_model: Models::O4Mini, + rate_line: 1, + } + } +} + +impl UserContexts { + pub fn new() -> UserContexts { + UserContexts { + contexts: DashMap::new(), + } + } + + pub fn get_or_create(&self, user_id: UserId) -> UserContext { + self.contexts + .entry(user_id) + .or_insert_with(|| UserContext::new(user_id)) + .clone() + } + + pub fn set_model(&self, user_id: UserId, model: Models) { + self.contexts + .entry(user_id) + .or_insert_with(|| UserContext::new(user_id)) + .main_model = model; + } + + pub fn set_rate_line(&self, user_id: UserId, rate_line: u64) { + self.contexts + .entry(user_id) + .or_insert_with(|| UserContext::new(user_id)) + .rate_line = rate_line; + } +} \ No newline at end of file diff --git a/wk-371tti-net-crawler b/wk-371tti-net-crawler new file mode 160000 index 0000000..1bce949 --- /dev/null +++ b/wk-371tti-net-crawler @@ -0,0 +1 @@ +Subproject commit 1bce9491d08d36e0113baf4e1f728cc4f07abec6