Compare commits

..

41 Commits

Author SHA1 Message Date
906ddec843 chore: update lockfile 2026-04-26 18:38:08 -04:00
48e92d1736 fix: make /opt-in and /opt-out's responses ephemeral 2026-04-25 20:58:35 -04:00
733e8f73ea feat: create a BotDataManager abstraction by copying and pasting then editing UserDataManager 2026-04-25 20:56:41 -04:00
2c0d5c8479 feat: advertise opt in and opt out commands 2026-04-22 12:11:36 -04:00
9b479d1236 feat: respect consent to be recorded 2026-04-22 11:57:57 -04:00
29f97f82c4 fix: move title to author field cause you can't @mention users in the title 2026-04-22 01:05:34 -04:00
dd17de79de fix: close the writer instead of merely dropping it 2026-04-22 00:45:59 -04:00
bb96724454 fix: check for NotFound error at the correct site in OperatorExt::async_reader_if_exists 2026-04-22 00:27:11 -04:00
4dae5bac7a fix: specify webpki roots for tokio-websockets dependencies 2026-04-21 23:31:11 -04:00
37753fe37c chore: revert opendal to before reqwest 0.13 upgrade to see if it'll run in a SCRATCH container now 2026-04-21 21:45:32 -04:00
7fe6980867 feat: inspect user data 2026-04-21 15:06:56 -04:00
0ce26fc0e5 feat: user consent setting and retrieving (NOTE: does not affect recording yet) 2026-04-21 03:11:27 -04:00
62399c2046 feat: allow setting a corresponding text channel for a voice channel 2026-04-17 01:19:09 -04:00
612a696829 feat: mute 2026-04-16 23:13:45 -04:00
2bf42e47c5 chore: eliminate rocksdb which makes everything better 2026-04-16 19:29:21 -04:00
50230c43b8 chore: update lockfile 2026-04-16 17:59:58 -04:00
e4e274a543 fix: have to use match to detect ReceiveMessageErrorType 2026-04-16 02:03:52 -04:00
6cd7f00028 fix: detect gateway close and failure to reconnect 2026-04-16 02:01:50 -04:00
6a1d8f060f fix: I REALLY did this without rust-analyzer so THIS TIME fix the gateway close error path 2026-04-16 01:57:30 -04:00
766582c9e8 fix: I did this without rust-analyzer so fix the gateway close error path 2026-04-16 01:53:50 -04:00
784ec5e867 fix: exit the program to trigger a restart when the gateway closes 2026-04-16 01:51:34 -04:00
dbcc155c4c chore: run the program in the same image/stage as the builder (eliminating the runner) until I fix static linking 2026-04-15 18:43:51 -04:00
31adbc2027 chore: go back to Alpine dynamic linking since I think it's the best I can do 2026-04-15 17:46:25 -04:00
74bd37a67f chore: remove static requirement from rocksdb 2026-04-15 17:42:23 -04:00
e609428f95 chore: use a patched version of opendal with rocksdb static 2026-04-15 17:28:47 -04:00
d129913235 fix: revert to Rust image 2026-04-15 13:11:59 -04:00
066bff4c07 fix: add the unspecified Linux target 2026-04-15 13:08:37 -04:00
509278c6eb fix: build for unspecified Linux (not specifically Alpine) 2026-04-15 13:04:29 -04:00
f31d06bdf9 fix: force static linking 2026-04-15 13:01:35 -04:00
d93b53267e fix: depend on capnproto-dev 2026-04-15 12:41:16 -04:00
6ad797eaea fix: depend on capnproto 2026-04-15 12:31:51 -04:00
4b71e5ef85 fix: try building from a base Alpine image and install cargo instead 2026-04-15 02:15:37 -04:00
a99840ffb7 fix: depend on versions of llvm dev and static that actually exist 2026-04-15 02:02:55 -04:00
38196e84ec fix: depend on clang and llvm static and dev 2026-04-15 02:00:37 -04:00
0fdb83a9d3 fix: add required clang static dependency 2026-04-15 01:53:19 -04:00
dd6c1723e5 fix: add required libclang dependency 2026-04-15 01:48:59 -04:00
01a55d42ec fix: add required clang libs dependency 2026-04-15 01:40:58 -04:00
33a7b15720 feat: support configuring audio channels and sample rates of recordings; audio recordings now work! 2026-04-15 01:22:38 -04:00
b457375e69 fix: address compiler errors related to recording vc 2026-04-14 21:54:12 -04:00
58212ce240 feat: save VC audio as wav (probably, didn't test yet) 2026-04-14 17:36:37 -04:00
1b88e6a11d feat: support RocksDB 2026-04-14 14:28:47 -04:00
17 changed files with 1673 additions and 625 deletions

368
Cargo.lock generated
View File

@@ -340,6 +340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288"
dependencies = [ dependencies = [
"aws-lc-sys", "aws-lc-sys",
"untrusted 0.7.1",
"zeroize", "zeroize",
] ]
@@ -561,9 +562,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.0" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]] [[package]]
name = "cacache" name = "cacache"
@@ -1594,6 +1595,35 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "ext-trait"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccef4f53516d7589a8ed95216b6ebc9d519df033c1303b42125bfe57aa475d23"
dependencies = [
"ext-trait-proc_macros",
]
[[package]]
name = "ext-trait-proc_macros"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "025e48a9a5db92b84dbd3b6be37853a0e60c1ce9c7c03c08e6ac282766f3e3f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "extension-traits"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7dae1256e3fea2900e1e674bae228edc852f5ce9ccb1c916a496a33cb9bc4cb"
dependencies = [
"ext-trait",
]
[[package]] [[package]]
name = "fastpool" name = "fastpool"
version = "1.1.0" version = "1.1.0"
@@ -1693,21 +1723,29 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"async-trait", "async-trait",
"bytes",
"capnp", "capnp",
"capnpc", "capnpc",
"clap", "clap",
"dashmap 6.1.0", "dashmap 6.1.0",
"extension-traits",
"futures", "futures",
"hound",
"moka",
"opendal", "opendal",
"opus2",
"patricia_tree 0.10.1", "patricia_tree 0.10.1",
"rhai", "rhai",
"rustls 0.23.35", "rustls 0.23.35",
"secrecy 0.10.3", "secrecy 0.10.3",
"snafu", "snafu",
"songbird", "songbird",
"strum 0.28.0",
"time", "time",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tokio-websockets 0.11.4",
"tokio-websockets 0.13.2",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"twilight-gateway", "twilight-gateway",
@@ -1715,6 +1753,7 @@ dependencies = [
"twilight-model", "twilight-model",
"twilight-util", "twilight-util",
"typed-builder 0.23.2", "typed-builder 0.23.2",
"yoke",
] ]
[[package]] [[package]]
@@ -2269,7 +2308,7 @@ dependencies = [
"hex", "hex",
"shorthand", "shorthand",
"stable-vec", "stable-vec",
"strum", "strum 0.17.1",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@@ -2291,6 +2330,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "hound"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]] [[package]]
name = "hpke-rs" name = "hpke-rs"
version = "0.6.1" version = "0.6.1"
@@ -2769,26 +2814,30 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.83" version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [ dependencies = [
"cfg-if",
"futures-util",
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]] [[package]]
name = "jsonwebtoken" name = "jsonwebtoken"
version = "9.3.1" version = "10.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1"
dependencies = [ dependencies = [
"aws-lc-rs",
"base64 0.22.1", "base64 0.22.1",
"getrandom 0.2.16",
"js-sys", "js-sys",
"pem", "pem",
"ring",
"serde", "serde",
"serde_json", "serde_json",
"signature",
"simple_asn1", "simple_asn1",
] ]
@@ -3350,9 +3399,9 @@ dependencies = [
[[package]] [[package]]
name = "moka" name = "moka"
version = "0.12.12" version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"crossbeam-channel", "crossbeam-channel",
@@ -3609,7 +3658,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "opendal" name = "opendal"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"ctor", "ctor",
"opendal-core", "opendal-core",
@@ -3655,7 +3704,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-core" name = "opendal-core"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
@@ -3669,7 +3718,8 @@ dependencies = [
"mea", "mea",
"moka", "moka",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml 0.38.4",
"reqsign-core",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
@@ -3682,7 +3732,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-layer-concurrent-limit" name = "opendal-layer-concurrent-limit"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"futures", "futures",
"http", "http",
@@ -3693,7 +3743,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-layer-logging" name = "opendal-layer-logging"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"log", "log",
"opendal-core", "opendal-core",
@@ -3702,7 +3752,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-layer-retry" name = "opendal-layer-retry"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"backon", "backon",
"log", "log",
@@ -3712,7 +3762,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-layer-timeout" name = "opendal-layer-timeout"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"opendal-core", "opendal-core",
"tokio", "tokio",
@@ -3721,7 +3771,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-aliyun-drive" name = "opendal-service-aliyun-drive"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -3735,7 +3785,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-alluxio" name = "opendal-service-alluxio"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -3748,7 +3798,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-azblob" name = "opendal-service-azblob"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -3756,8 +3806,10 @@ dependencies = [
"log", "log",
"opendal-core", "opendal-core",
"opendal-service-azure-common", "opendal-service-azure-common",
"quick-xml", "quick-xml 0.38.4",
"reqsign", "reqsign-azure-storage",
"reqsign-core",
"reqsign-file-read-tokio",
"serde", "serde",
"sha2", "sha2",
"uuid", "uuid",
@@ -3766,15 +3818,17 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-azdls" name = "opendal-service-azdls"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
"log", "log",
"opendal-core", "opendal-core",
"opendal-service-azure-common", "opendal-service-azure-common",
"quick-xml", "quick-xml 0.38.4",
"reqsign", "reqsign-azure-storage",
"reqsign-core",
"reqsign-file-read-tokio",
"serde", "serde",
"serde_json", "serde_json",
] ]
@@ -3782,32 +3836,33 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-azfile" name = "opendal-service-azfile"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
"log", "log",
"opendal-core", "opendal-core",
"opendal-service-azure-common", "opendal-service-azure-common",
"quick-xml", "quick-xml 0.38.4",
"reqsign", "reqsign-azure-storage",
"reqsign-core",
"reqsign-file-read-tokio",
"serde", "serde",
] ]
[[package]] [[package]]
name = "opendal-service-azure-common" name = "opendal-service-azure-common"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"http", "http",
"opendal-core", "opendal-core",
"reqsign",
] ]
[[package]] [[package]]
name = "opendal-service-b2" name = "opendal-service-b2"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -3821,7 +3876,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-cacache" name = "opendal-service-cacache"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"cacache", "cacache",
@@ -3832,7 +3887,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-d1" name = "opendal-service-d1"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -3844,7 +3899,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-dashmap" name = "opendal-service-dashmap"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"dashmap 6.1.0", "dashmap 6.1.0",
"log", "log",
@@ -3855,7 +3910,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-dbfs" name = "opendal-service-dbfs"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -3869,7 +3924,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-dropbox" name = "opendal-service-dropbox"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -3882,7 +3937,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-etcd" name = "opendal-service-etcd"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"etcd-client", "etcd-client",
"fastpool", "fastpool",
@@ -3894,7 +3949,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-fs" name = "opendal-service-fs"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"log", "log",
@@ -3907,7 +3962,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-ftp" name = "opendal-service-ftp"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"fastpool", "fastpool",
@@ -3925,17 +3980,18 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-gcs" name = "opendal-service-gcs"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"backon", "async-trait",
"bytes", "bytes",
"http", "http",
"log", "log",
"opendal-core", "opendal-core",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml 0.38.4",
"reqsign", "reqsign-core",
"reqwest", "reqsign-file-read-tokio",
"reqsign-google",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -3944,7 +4000,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-gdrive" name = "opendal-service-gdrive"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -3958,7 +4014,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-ghac" name = "opendal-service-ghac"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"ghac", "ghac",
@@ -3967,7 +4023,8 @@ dependencies = [
"opendal-core", "opendal-core",
"opendal-service-azblob", "opendal-service-azblob",
"prost 0.13.5", "prost 0.13.5",
"reqsign", "reqsign-azure-storage",
"reqsign-core",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -3976,7 +4033,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-github" name = "opendal-service-github"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -3990,7 +4047,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-gridfs" name = "opendal-service-gridfs"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"futures", "futures",
"mea", "mea",
@@ -4002,7 +4059,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-hdfs-native" name = "opendal-service-hdfs-native"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures",
@@ -4015,7 +4072,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-ipmfs" name = "opendal-service-ipmfs"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -4028,7 +4085,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-mini-moka" name = "opendal-service-mini-moka"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"log", "log",
"mini-moka", "mini-moka",
@@ -4039,7 +4096,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-moka" name = "opendal-service-moka"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"log", "log",
"moka", "moka",
@@ -4050,7 +4107,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-mongodb" name = "opendal-service-mongodb"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"mea", "mea",
"mongodb", "mongodb",
@@ -4061,7 +4118,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-mysql" name = "opendal-service-mysql"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"mea", "mea",
"opendal-core", "opendal-core",
@@ -4072,7 +4129,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-pcloud" name = "opendal-service-pcloud"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -4085,7 +4142,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-persy" name = "opendal-service-persy"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"opendal-core", "opendal-core",
"persy", "persy",
@@ -4095,7 +4152,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-postgresql" name = "opendal-service-postgresql"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"mea", "mea",
"opendal-core", "opendal-core",
@@ -4106,7 +4163,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-redb" name = "opendal-service-redb"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"opendal-core", "opendal-core",
"redb", "redb",
@@ -4116,7 +4173,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-redis" name = "opendal-service-redis"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"bytes", "bytes",
"fastpool", "fastpool",
@@ -4130,7 +4187,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-s3" name = "opendal-service-s3"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -4139,19 +4196,18 @@ dependencies = [
"log", "log",
"md-5", "md-5",
"opendal-core", "opendal-core",
"quick-xml", "quick-xml 0.38.4",
"reqsign-aws-v4", "reqsign-aws-v4",
"reqsign-core", "reqsign-core",
"reqsign-file-read-tokio", "reqsign-file-read-tokio",
"reqsign-http-send-reqwest",
"reqwest",
"serde", "serde",
"url",
] ]
[[package]] [[package]]
name = "opendal-service-sled" name = "opendal-service-sled"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"opendal-core", "opendal-core",
"serde", "serde",
@@ -4161,7 +4217,7 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-webdav" name = "opendal-service-webdav"
version = "0.55.0" version = "0.55.0"
source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" source = "git+https://github.com/apache/opendal?rev=ecf840b04afd2be109830b9978ba89759adfee79#ecf840b04afd2be109830b9978ba89759adfee79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -4169,7 +4225,7 @@ dependencies = [
"log", "log",
"mea", "mea",
"opendal-core", "opendal-core",
"quick-xml", "quick-xml 0.38.4",
"serde", "serde",
] ]
@@ -4815,6 +4871,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "quick-xml"
version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "quinn" name = "quinn"
version = "0.11.9" version = "0.11.9"
@@ -4872,9 +4938,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -5121,48 +5187,19 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqsign"
version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43451dbf3590a7590684c25fb8d12ecdcc90ed3ac123433e500447c7d77ed701"
dependencies = [
"anyhow",
"async-trait",
"base64 0.22.1",
"chrono",
"form_urlencoded",
"getrandom 0.2.16",
"hex",
"hmac",
"home",
"http",
"jsonwebtoken",
"log",
"percent-encoding",
"rand 0.8.5",
"reqwest",
"rsa",
"serde",
"serde_json",
"sha1",
"sha2",
]
[[package]] [[package]]
name = "reqsign-aws-v4" name = "reqsign-aws-v4"
version = "2.0.2" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab367a07c335a3eaa22395a9d9b0031ac73aee5893573281b2fa27bf97dc94f2" checksum = "44eaca382e94505a49f1a4849658d153aebf79d9c1a58e5dd3b10361511e9f43"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"bytes", "bytes",
"form_urlencoded", "form_urlencoded",
"http", "http",
"log", "log",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml 0.39.2",
"reqsign-core", "reqsign-core",
"rust-ini", "rust-ini",
"serde", "serde",
@@ -5172,16 +5209,38 @@ dependencies = [
] ]
[[package]] [[package]]
name = "reqsign-core" name = "reqsign-azure-storage"
version = "2.0.2" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba66eb941c0f723269a394baf3b19a2fa697a1e593f3e902779df6c35d24e21" checksum = "7a321980405d596bd34aaf95c4722a3de4128a67fd19e74a81a83aa3fdf082e6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"form_urlencoded", "form_urlencoded",
"http",
"jsonwebtoken",
"log",
"pem",
"percent-encoding",
"reqsign-core",
"rsa",
"serde",
"serde_json",
"sha1",
]
[[package]]
name = "reqsign-core"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b10302cf0a7d7e7352ba211fc92c3c5bebf1286153e49cc5aa87348078a8e102"
dependencies = [
"anyhow",
"base64 0.22.1",
"bytes",
"form_urlencoded",
"futures",
"hex", "hex",
"hmac", "hmac",
"http", "http",
@@ -5195,31 +5254,33 @@ dependencies = [
[[package]] [[package]]
name = "reqsign-file-read-tokio" name = "reqsign-file-read-tokio"
version = "2.0.2" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702f12a867bf8e507de907fa0f4d75b96469ace7edd33fcc1fc8a8ef58f3c8d2" checksum = "e2d89295b3d17abea31851cc8de55d843d89c52132c864963c38d41920613dc5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"reqsign-core", "reqsign-core",
"tokio", "tokio",
] ]
[[package]] [[package]]
name = "reqsign-http-send-reqwest" name = "reqsign-google"
version = "2.0.1" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46186bce769674f9200ad01af6f2ca42de3e819ddc002fff1edae135bfb6cd9c" checksum = "35cc609b49c69e76ecaceb775a03f792d1ed3e7755ab3548d4534fd801e3242e"
dependencies = [ dependencies = [
"anyhow", "form_urlencoded",
"async-trait",
"bytes",
"futures-channel",
"http", "http",
"http-body-util", "jsonwebtoken",
"log",
"percent-encoding",
"reqsign-aws-v4",
"reqsign-core", "reqsign-core",
"reqwest", "rsa",
"wasm-bindgen-futures", "serde",
"serde_json",
"sha2",
"tokio",
] ]
[[package]] [[package]]
@@ -5318,7 +5379,7 @@ dependencies = [
"cfg-if", "cfg-if",
"getrandom 0.2.16", "getrandom 0.2.16",
"libc", "libc",
"untrusted", "untrusted 0.9.0",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -5497,7 +5558,7 @@ checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
"untrusted", "untrusted 0.9.0",
] ]
[[package]] [[package]]
@@ -5509,7 +5570,7 @@ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
"untrusted", "untrusted 0.9.0",
] ]
[[package]] [[package]]
@@ -6399,7 +6460,16 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "530efb820d53b712f4e347916c5e7ed20deb76a4f0457943b3182fb889b06d2c" checksum = "530efb820d53b712f4e347916c5e7ed20deb76a4f0457943b3182fb889b06d2c"
dependencies = [ dependencies = [
"strum_macros", "strum_macros 0.17.1",
]
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros 0.28.0",
] ]
[[package]] [[package]]
@@ -6414,6 +6484,18 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -6817,6 +6899,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls 0.26.4", "tokio-rustls 0.26.4",
"tokio-util", "tokio-util",
"webpki-roots 0.26.11",
] ]
[[package]] [[package]]
@@ -7313,6 +7396,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@@ -7436,9 +7525,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.106" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -7449,22 +7538,19 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.56" version = "0.4.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
dependencies = [ dependencies = [
"cfg-if",
"js-sys", "js-sys",
"once_cell",
"wasm-bindgen", "wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.106" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -7472,9 +7558,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.106" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -7485,9 +7571,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.106" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -7541,9 +7627,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.83" version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -8115,9 +8201,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
dependencies = [ dependencies = [
"stable_deref_trait", "stable_deref_trait",
"yoke-derive", "yoke-derive",
@@ -8126,9 +8212,9 @@ dependencies = [
[[package]] [[package]]
name = "yoke-derive" name = "yoke-derive"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -6,11 +6,15 @@ edition = "2024"
[dependencies] [dependencies]
async-compression = { version = "0.4.41", features = ["brotli", "futures-io"] } async-compression = { version = "0.4.41", features = ["brotli", "futures-io"] }
async-trait = "0.1.89" async-trait = "0.1.89"
bytes = "1.11.1"
capnp = "0.25.3" capnp = "0.25.3"
clap = { version = "4.5.40", features = ["derive", "env"] } clap = { version = "4.5.40", features = ["derive", "env"] }
dashmap = "6.1.0" dashmap = "6.1.0"
extension-traits = "2.0.2"
futures = "0.3.32" futures = "0.3.32"
opendal = { git = "https://github.com/apache/opendal", features = [ hound = "3.5.1"
moka = { version = "0.12.15", features = ["future"] }
opendal = { git = "https://github.com/apache/opendal", rev = "ecf840b04afd2be109830b9978ba89759adfee79", features = [
"services-azfile", "services-azfile",
"services-aliyun-drive", "services-aliyun-drive",
"services-alluxio", "services-alluxio",
@@ -47,6 +51,7 @@ opendal = { git = "https://github.com/apache/opendal", features = [
"services-sled", "services-sled",
"services-webdav", "services-webdav",
] } ] }
opus2 = "0.4.0"
patricia_tree = "0.10.1" patricia_tree = "0.10.1"
rhai = "1.23.6" rhai = "1.23.6"
rustls = "0.23" rustls = "0.23"
@@ -60,12 +65,26 @@ songbird = { version = "0.6.0", default-features = false, features = [
"twilight", "twilight",
"tws", "tws",
] } ] }
strum = { version = "0.28.0", features = ["derive"] }
time = "0.3.47" time = "0.3.47"
tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal"] } tokio = { version = "1.46.0", features = [
tokio-util = "0.7.18" "rt-multi-thread",
"macros",
"signal",
] }
tokio-util = { version = "0.7.18", features = ["io"] }
tokio-websockets-0-13 = { package = "tokio-websockets", version = "0.13", features = [
"rustls-webpki-roots",
] }
tokio-websockets-0-11 = { package = "tokio-websockets", version = "0.11", features = [
"rustls-webpki-roots",
] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
twilight-gateway = { version = "0.17", default-features = false, features = ["rustls-webpki-roots", "twilight-http"] } twilight-gateway = { version = "0.17", default-features = false, features = [
"rustls-webpki-roots",
"twilight-http",
] }
twilight-http = { version = "0.17", default-features = false, features = [ twilight-http = { version = "0.17", default-features = false, features = [
"rustls-webpki-roots", "rustls-webpki-roots",
"hickory", "hickory",
@@ -74,6 +93,7 @@ twilight-http = { version = "0.17", default-features = false, features = [
twilight-model = "0.17" twilight-model = "0.17"
twilight-util = { version = "0.17", features = ["builder"] } twilight-util = { version = "0.17", features = ["builder"] }
typed-builder = "0.23.2" typed-builder = "0.23.2"
yoke = "0.8.2"
[build-dependencies] [build-dependencies]
capnpc = "0.25.3" capnpc = "0.25.3"

View File

@@ -5,6 +5,10 @@ ARG BUILD_BASE_VERSION=0.5-r3
RUN --mount=type=cache,sharing=locked,target=/var/cache/apk \ RUN --mount=type=cache,sharing=locked,target=/var/cache/apk \
apk add --update build-base=${BUILD_BASE_VERSION} apk add --update build-base=${BUILD_BASE_VERSION}
ARG CAPNPROTO_DEV_VERSION=1.2.0-r0
RUN --mount=type=cache,sharing=locked,target=/var/cache/apk \
apk add --update capnproto-dev=${CAPNPROTO_DEV_VERSION}
ARG CMAKE_VERSION=4.1.3-r0 ARG CMAKE_VERSION=4.1.3-r0
RUN --mount=type=cache,sharing=locked,target=/var/cache/apk \ RUN --mount=type=cache,sharing=locked,target=/var/cache/apk \
apk add --update cmake=${CMAKE_VERSION} apk add --update cmake=${CMAKE_VERSION}

175
src/bot_data.rs Normal file
View File

@@ -0,0 +1,175 @@
use async_compression::futures::{bufread::BrotliDecoder, write::BrotliEncoder};
use capnp::message::TypedBuilder;
use futures::{AsyncReadExt, AsyncWriteExt};
use opendal::Operator;
use snafu::{ResultExt as _, Snafu};
use crate::{OperatorExt, bot_capnp, option_ext::OptionExt as _};
#[derive(Debug, Clone)]
pub struct BotDataManager {
operator: Operator,
}
impl BotDataManager {
pub fn new(operator: Operator) -> Self {
Self { operator }
}
}
const PATH: &str = "data.bin.brotli";
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum WithError {
/// couldn't read data for this bot from the storage operator
ReadError { source: opendal::Error },
/// couldn't decompress the bot data from storage
DecompressionError { source: std::io::Error },
/// couldn't deserialize the bot data
DeserializeError { source: capnp::Error },
}
impl BotDataManager {
pub async fn with<R>(
&self,
f: impl FnOnce(bot_capnp::bot::Reader<'_>) -> R,
) -> Result<R, WithError> {
let compressed_buffer = self
.operator
.async_reader_if_exists(PATH)
.await
.context(with_error::ReadSnafu)?;
let decompressed_reader = compressed_buffer.map(BrotliDecoder::new);
let decompressed = decompressed_reader
.map_async(|mut reader| async move {
let mut vec = Vec::new();
reader.read_to_end(&mut vec).await?;
Ok(vec)
})
.await
.transpose()
.context(with_error::DecompressionSnafu)?;
let mut message = TypedBuilder::<bot_capnp::bot::Owned>::new_default();
let fallback = message.init_root();
let mut bot_data = fallback.into_reader();
let message_reader;
if let Some(mut bytes) = decompressed.as_deref() {
message_reader = capnp::serialize::read_message_from_flat_slice_no_alloc(
&mut bytes,
Default::default(),
)
.context(with_error::DeserializeSnafu)?;
bot_data = message_reader
.get_root()
.context(with_error::DeserializeSnafu)?;
}
Ok(f(bot_data))
}
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum UpdateError {
/// couldn't read data for this bot from the storage operator
ReadError { source: opendal::Error },
/// couldn't decompress the bot data from storage
DecompressionError { source: std::io::Error },
/// couldn't deserialize the bot data
DeserializeError { source: capnp::Error },
/// couldn't serialize the (modified) bot data
SerializeError { source: capnp::Error },
/// couldn't create a writer for this bot data in the storage operator
WriterError { source: opendal::Error },
/// couldn't write (modified) data for this bot to the storage operator
WriteError { source: std::io::Error },
/// couldn't finalize writing (modified) data for this bot to the storage operator
FinalizeError { source: std::io::Error },
}
impl BotDataManager {
pub async fn update<R>(
&self,
f: impl FnOnce(bot_capnp::bot::Builder<'_>) -> R,
) -> Result<R, UpdateError> {
let compressed_buffer = self
.operator
.async_reader_if_exists(PATH)
.await
.context(update_error::ReadSnafu)?;
let decompressed_reader = compressed_buffer.map(BrotliDecoder::new);
let decompressed = decompressed_reader
.map_async(|mut reader| async move {
let mut vec = Vec::new();
reader.read_to_end(&mut vec).await?;
Ok(vec)
})
.await
.transpose()
.context(update_error::DecompressionSnafu)?;
let mut message = TypedBuilder::<bot_capnp::bot::Owned>::new_default();
let ret = if let Some(mut bytes) = decompressed.as_deref() {
let message_reader = capnp::serialize::read_message_from_flat_slice_no_alloc(
&mut bytes,
Default::default(),
)
.context(update_error::DeserializeSnafu)?;
let bot_data = message_reader
.get_root()
.context(update_error::DeserializeSnafu)?;
message
.set_root(bot_data)
.context(update_error::DeserializeSnafu)?;
f(
message.get_root().unwrap(), // this is logically impossible
)
} else {
f(message.init_root())
};
let mut buffer = Vec::new();
capnp::serialize::write_message(&mut buffer, message.borrow_inner())
.context(update_error::SerializeSnafu)?;
let compressed_writer = self
.operator
.writer(PATH)
.await
.context(update_error::WriterSnafu)?
.into_futures_async_write();
let mut decompressed_writer = BrotliEncoder::new(compressed_writer);
decompressed_writer
.write_all(&buffer)
.await
.context(update_error::WriteSnafu)?;
decompressed_writer
.close()
.await
.context(update_error::FinalizeSnafu)?;
Ok(ret)
}
}

View File

@@ -1,10 +1,6 @@
use std::sync::LazyLock; use futures::TryStreamExt;
use async_compression::futures::bufread::BrotliDecoder;
use capnp::message::ReaderOptions;
use futures::AsyncReadExt;
use opendal::ErrorKind;
use snafu::{OptionExt, Snafu}; use snafu::{OptionExt, Snafu};
use std::sync::LazyLock;
use twilight_model::{ use twilight_model::{
application::{ application::{
command::{Command, CommandType}, command::{Command, CommandType},
@@ -17,14 +13,13 @@ use twilight_model::{
use twilight_util::builder::{ use twilight_util::builder::{
InteractionResponseDataBuilder, InteractionResponseDataBuilder,
command::CommandBuilder, command::CommandBuilder,
embed::{EmbedBuilder, EmbedFieldBuilder}, embed::{EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder},
}; };
use crate::{bot_capnp, command::State}; use crate::command::State;
const NAME: &str = "debug"; const NAME: &str = "info";
const DESCRIPTION: &str = const DESCRIPTION: &str = "Show various information";
"(Only the bot owner can use this) Show various information for debugging purposes";
pub static COMMAND: LazyLock<Command> = LazyLock::new(|| { pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput) CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput)
@@ -114,54 +109,22 @@ pub async fn handle(state: State, interaction: Interaction) {
.await .await
.expect("TODO"); .expect("TODO");
let heat_script_description = { let heat_script_description = state
let compressed_result = state .bot_data_manager
.bot_data .with(|bot_data| {
.reader("data.bin.brotli") let heat_script_option = bot_data.has_heat_script().then(|| {
.await bot_data
.expect("TODO") .get_heat_script()
.into_futures_async_read(..) .expect("TODO")
.await; .to_string()
.expect("TODO")
let mut buf = Vec::default(); });
let mut message = capnp::message::TypedBuilder::<bot_capnp::bot::Owned>::new_default(); heat_script_option.map_or("none set yet".into(), |heat_script| {
let fallback = message.init_root(); format!("```\n{heat_script}\n```")
})
let message_reader;
let mut bot_data = fallback.into_reader();
match compressed_result {
Ok(compressed) => {
let mut decompressed = BrotliDecoder::new(compressed);
decompressed.read_to_end(&mut buf).await.expect("TODO");
message_reader =
capnp::serialize_packed::read_message(&*buf, ReaderOptions::default())
.expect("TODO");
bot_data = message_reader
.get_root::<bot_capnp::bot::Reader>()
.expect("TODO");
}
Err(error) if error.kind() == ErrorKind::NotFound => {
tracing::error!("TODO: proceeding with fallback");
}
Err(error) => {
tracing::error!(?error, "TODO");
return;
}
}
let heat_script_option = bot_data
.has_heat_script()
.then(|| bot_data.get_heat_script().expect("TODO"));
let heat_script_option =
heat_script_option.map(|heat_script| heat_script.to_str().expect("TODO"));
heat_script_option.map_or("none set yet".into(), |heat_script| {
format!("```\n{heat_script}\n```")
}) })
}; .await
.expect("TODO");
state state
.discord_client .discord_client
@@ -175,4 +138,48 @@ pub async fn handle(state: State, interaction: Interaction) {
.flags(MessageFlags::EPHEMERAL) .flags(MessageFlags::EPHEMERAL)
.await .await
.expect("TODO"); .expect("TODO");
let mut user_id_stream = state.user_data_manager.list().await.expect("TODO");
while let Some(user_id) = user_id_stream.try_next().await.expect("TODO") {
let (consent, notification_script) = state
.user_data_manager
.with(user_id, |user_data| {
let consent = user_data.get_voice_recording_consent().unwrap();
let notification_script = user_data.has_notification_script().then_some(
user_data
.get_notification_script()
.expect("TODO")
.to_string()
.expect("TODO"),
);
(consent, notification_script)
})
.await
.expect("TODO");
let user_mention = format!("<@{user_id}>");
state
.discord_client
.interaction(state.discord_application_id)
.create_followup(&interaction.token)
.embeds(&[EmbedBuilder::new()
.author(EmbedAuthorBuilder::new(user_mention))
.field(EmbedFieldBuilder::new("Consent", format!("{consent:?}")).build())
.field(
EmbedFieldBuilder::new(
"Notification Script",
format!("{notification_script:?}"),
)
.build(),
)
.validate()
.unwrap()
.build()])
.flags(MessageFlags::EPHEMERAL)
.await
.expect("TODO");
}
} }

View File

@@ -1,219 +1,362 @@
use crate::{VCs, command::State}; use crate::{
use async_trait::async_trait; OneToManyUniqueBTreeMap, UserDataManager, VCs, command::State, option_ext::OptionExt as _,
use snafu::{OptionExt, Snafu}; user_capnp::user::Consent, user_data::RECORD_IF_CONSENT_UNSPECIFIED,
use songbird::{CoreEvent, Event, EventContext, EventHandler}; };
use std::{sync::LazyLock, time::Instant}; use async_trait::async_trait;
use time::UtcDateTime; use futures::FutureExt;
use twilight_model::{ use hound::{SampleFormat, WavSpec};
application::{ use opendal::Operator;
command::{Command, CommandType}, use snafu::{OptionExt as _, Snafu};
interaction::Interaction, use songbird::{CoreEvent, Event, EventContext, EventHandler};
}, use std::{
channel::message::{Embed, MessageFlags}, io::Cursor,
http::interaction::{InteractionResponse, InteractionResponseType}, sync::{Arc, LazyLock, Mutex},
id::{ time::Instant,
Id, };
marker::{ChannelMarker, GuildMarker}, use time::UtcDateTime;
}, use twilight_model::{
}; application::{
use twilight_util::builder::{ command::{Command, CommandType},
InteractionResponseDataBuilder, interaction::Interaction,
command::CommandBuilder, },
embed::{EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder}, channel::message::{Embed, MessageFlags},
}; http::interaction::{InteractionResponse, InteractionResponseType},
id::{
const NAME: &str = "join"; Id,
const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)"; marker::{ChannelMarker, GuildMarker, UserMarker},
},
pub static COMMAND: LazyLock<Command> = LazyLock::new(|| { };
CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput) use twilight_util::builder::{
.validate() InteractionResponseDataBuilder,
.expect("command wasn't correct") command::CommandBuilder,
.build() embed::{EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder},
}); };
#[derive(Debug, Snafu)] const NAME: &str = "join";
enum GetGuildAndVoiceChannelIdError { const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)";
/// this command was not used inside a guild (Discord server)
NotInGuild, pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput)
/// there is no user who invoked this command .validate()
NoUser, .expect("command wasn't correct")
.build()
/// there are no voice chats in this guild });
NoVCsInGuild,
#[derive(Debug, Snafu)]
/// the user is not in a voice chat in this guild enum GetGuildAndVoiceChannelIdError {
UserNotInVC, /// this command was not used inside a guild (Discord server)
} NotInGuild,
#[tracing::instrument] /// there is no user who invoked this command
fn get_guild_and_voice_channel_id( NoUser,
interaction: &Interaction,
vcs: &VCs, /// there are no voice chats in this guild
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndVoiceChannelIdError> { NoVCsInGuild,
let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
/// the user is not in a voice chat in this guild
let user_id = interaction UserNotInVC,
.member }
.as_ref()
.and_then(|member| member.user.as_ref().map(|user| user.id)) #[tracing::instrument]
.context(NoUserSnafu)?; fn get_guild_and_voice_channel_id(
interaction: &Interaction,
let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?; vcs: &VCs,
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndVoiceChannelIdError> {
let &voice_channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?; let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
Ok((guild_id, voice_channel_id)) let user_id = interaction
} .member
.as_ref()
fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Embed { .and_then(|member| member.user.as_ref().map(|user| user.id))
match error { .context(NoUserSnafu)?;
GetGuildAndVoiceChannelIdError::NotInGuild => {
EmbedBuilder::new().title("Use this in a server").description("This bot can't find a VC to join if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build() let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?;
},
GetGuildAndVoiceChannelIdError::NoUser => { let &voice_channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?;
EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build()
}, Ok((guild_id, voice_channel_id))
GetGuildAndVoiceChannelIdError::NoVCsInGuild => { }
EmbedBuilder::new().title("No VCs in this server").description("This bot can't find a VC to join because there aren't any in this server right now.").validate().unwrap().build()
}, fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Embed {
GetGuildAndVoiceChannelIdError::UserNotInVC => { match error {
EmbedBuilder::new().title("You're not in a VC").description("This bot can't follow you into VC if you aren't in one in this server.").validate().unwrap().build() GetGuildAndVoiceChannelIdError::NotInGuild => {
}, EmbedBuilder::new().title("Use this in a server").description("This bot can't find a VC to join if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build()
} },
} GetGuildAndVoiceChannelIdError::NoUser => {
EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build()
#[derive(Debug, Clone)] },
struct Handler { GetGuildAndVoiceChannelIdError::NoVCsInGuild => {
start_instant: Instant, EmbedBuilder::new().title("No VCs in this server").description("This bot can't find a VC to join because there aren't any in this server right now.").validate().unwrap().build()
start_utc: UtcDateTime, },
} GetGuildAndVoiceChannelIdError::UserNotInVC => {
EmbedBuilder::new().title("You're not in a VC").description("This bot can't follow you into VC if you aren't in one in this server.").validate().unwrap().build()
#[async_trait] },
impl EventHandler for Handler { }
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> { }
tracing::error!(?ctx, "TODO");
#[derive(Debug, Clone)]
let Some(core_event) = ctx.to_core_event() else { struct Handler {
return None; start_instant: Instant,
}; start_utc: UtcDateTime,
tracing::error!(?core_event, "TODO");
recordings: Operator,
let elapsed = self.start_instant.elapsed();
let elapsed = elapsed.try_into().expect("TODO"); guild_id: Id<GuildMarker>,
channel_id: Id<ChannelMarker>,
let now_utc = self.start_utc.checked_add(elapsed).expect("TODO");
tracing::error!(?now_utc, "TODO"); known_ssrcs: Arc<Mutex<OneToManyUniqueBTreeMap<Id<UserMarker>, u32>>>,
match core_event { audio_channels: u16,
CoreEvent::SpeakingStateUpdate => todo!(), audio_sample_rate: u32,
CoreEvent::VoiceTick => todo!(),
CoreEvent::RtpPacket => todo!(), user_data_manager: UserDataManager,
CoreEvent::RtcpPacket => todo!(), }
CoreEvent::ClientDisconnect => todo!(),
CoreEvent::DriverConnect => todo!(), #[async_trait]
CoreEvent::DriverReconnect => todo!(), impl EventHandler for Handler {
CoreEvent::DriverDisconnect => todo!(), async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
_ => todo!(), match ctx {
} EventContext::Track(_items) => {
// Not expected to fire
None }
} EventContext::SpeakingStateUpdate(speaking) => {
} tracing::error!(?speaking);
#[tracing::instrument(skip(state))] if let Some(user_id) = speaking.user_id {
pub async fn handle(state: State, interaction: Interaction) { let user_id = Id::new(user_id.0);
let vcs = state.vcs;
self.known_ssrcs
let (guild_id, voice_channel_id) = match get_guild_and_voice_channel_id(&interaction, &vcs) { .lock()
Ok((guild_id, voice_channel_id)) => (guild_id, voice_channel_id), .unwrap()
Err(error) => { .insert(user_id, speaking.ssrc);
state }
.discord_client }
.interaction(state.discord_application_id) EventContext::VoiceTick(voice_tick) => {
.create_response( tracing::error!(?voice_tick);
interaction.id,
&interaction.token, for (ssrc, voice_data) in &voice_tick.speaking {
&InteractionResponse { let user_id = self.known_ssrcs.lock().unwrap().get_left_for(ssrc).cloned();
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some( tracing::info!(?user_id);
InteractionResponseDataBuilder::new()
.embeds([get_guild_and_vc_error_to_embed(error)]) if let Some(pcm) = &voice_data.decoded_voice {
.flags(MessageFlags::EPHEMERAL) let may_record = user_id
.build(), .map_async(|user_id| {
), self.user_data_manager
}, .with(user_id, |user_data| {
) user_data.get_voice_recording_consent().unwrap()
.await })
.expect("TODO"); .map(|result| result.expect("TODO"))
})
return; .await
} .map_or(RECORD_IF_CONSENT_UNSPECIFIED, |consent| match consent {
}; Consent::Unspecified => RECORD_IF_CONSENT_UNSPECIFIED,
Consent::Granted => true,
state Consent::Withheld => false,
.discord_client });
.interaction(state.discord_application_id)
.create_response( if !may_record {
interaction.id, tracing::warn!(?user_id, "may not be recorded");
&interaction.token, continue;
&InteractionResponse { }
kind: InteractionResponseType::DeferredChannelMessageWithSource,
data: None, let elapsed = self.start_instant.elapsed();
}, let elapsed = elapsed.try_into().expect("TODO");
)
.await let now_utc = self.start_utc.checked_add(elapsed).expect("TODO");
.expect("TODO"); tracing::error!(?now_utc, "TODO");
let call = state let year = now_utc.year();
.songbird let month = now_utc.month();
.join(guild_id, voice_channel_id) let day = now_utc.day();
.await
.expect("TODO"); let hour = now_utc.hour();
let minute = now_utc.minute();
tracing::error!(?call, "successfully joined"); let second = now_utc.second();
let start_instant = Instant::now(); let microseconds = now_utc.microsecond();
let start_utc = UtcDateTime::now();
let guild_id = self.guild_id;
let handler = Handler { let channel_id = self.channel_id;
start_instant,
start_utc, let user = user_id
}; .as_ref()
call.lock() .map_or_else(|| "UNKNOWN".into(), ToString::to_string);
.await
.add_global_event(CoreEvent::RtpPacket.into(), handler); let path = format!(
"{year}/{month}/{day}/{hour}/{minute}/audio-{second}.{microseconds}-{guild_id}-{channel_id}-{user}.wav"
let channel_mention = format!("<#{voice_channel_id}>"); );
let bot_owner_mention = format!("<@{}>", state.discord_bot_owner_user_id); let channels = self.audio_channels;
let sample_rate = self.audio_sample_rate;
state
.discord_client let wav_spec = WavSpec {
.interaction(state.discord_application_id) channels,
.update_response( sample_rate,
&interaction.token, bits_per_sample: 16,
).embeds(Some(&[ sample_format: SampleFormat::Int,
EmbedBuilder::new() };
.title("Joined VC to record")
.description(format!("This bot joined {channel_mention} and intends to record. Here are some pledges backed by faith (because there is no way to verify them yourself) in {bot_owner_mention}:")) let mut buffer = Vec::new();
.field( let writer = Cursor::new(&mut buffer);
EmbedFieldBuilder::new("Recordings are never shared", "Audio recordings are only stored on my home server and desktop computer and will never be uploaded to services or hardware that is owned by another person: not even curated clips, and not even to people who were in the recording. When transcription to text is implemented, this will only be run on my personally owned devices and not on any internet or cloud offering.").build()
) let mut wav_writer = hound::WavWriter::new(writer, wav_spec).expect("TODO");
.field(
EmbedFieldBuilder::new("You won't be \"audited\"", "I will not reference things said in past recordings with the goal of \"making a point\", nor pull them up on the spot (even by the request of the person who said it). Ideally, these are just peace of mind for me that I'm not missing out by not being in a Discord call all the time and can take my life back, so using them in an unhealthy way isn't in my interest.").build() let mut sample_writer = wav_writer.get_i16_writer(pcm.len() as u32);
)
.field( for sample in pcm {
EmbedFieldBuilder::new("Code is publically available", "The latest source code is at https://gitea.katniss.top/jacob/fomo-reducer so that I don't have to write guarantees about the technology here (e.g. what data is acquired, how it's used or stored) and you can just check it yourself").build() sample_writer.write_sample(*sample);
) }
.footer( sample_writer.flush().expect("TODO");
EmbedFooterBuilder::new("Thanks for your patience and understanding as I have bad and unusual mental health and it's crazy that I need this. This - especially if I learn if I can record streams or webcams so I don't miss out on those experiences either - should be the end of abrasion and force about how we spend our time. Again, thank you, I appreciate it.")
) wav_writer.finalize().expect("TODO");
.validate()
.unwrap() tracing::info!("going to write the audio shortly");
.build()
])) let recordings = self.recordings.clone();
.await tokio::spawn(async move {
.expect("TODO"); recordings.write(&path, buffer).await.expect("TODO");
} tracing::info!("successfully wrote the audio!");
});
}
}
}
EventContext::RtpPacket(_rtp_data) => {}
EventContext::RtcpPacket(_rtcp_data) => {}
EventContext::ClientDisconnect(_client_disconnect) => {
// This is already taken care of elsewhere
}
EventContext::DriverConnect(_connect_data) => {}
EventContext::DriverReconnect(_connect_data) => {}
EventContext::DriverDisconnect(_disconnect_data) => {}
other => {
tracing::warn!(?other, "cannot be handled yet");
}
}
None
}
}
#[tracing::instrument(skip(state))]
pub async fn handle(state: State, interaction: Interaction) {
let vcs = state.vcs;
let (guild_id, voice_channel_id) = match get_guild_and_voice_channel_id(&interaction, &vcs) {
Ok((guild_id, voice_channel_id)) => (guild_id, voice_channel_id),
Err(error) => {
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.embeds([get_guild_and_vc_error_to_embed(error)])
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},
)
.await
.expect("TODO");
return;
}
};
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::DeferredChannelMessageWithSource,
data: None,
},
)
.await
.expect("TODO");
let call = state
.songbird
.join(guild_id, voice_channel_id)
.await
.expect("TODO");
tracing::error!(?call, "successfully joined");
let start_instant = Instant::now();
let start_utc = UtcDateTime::now();
let audio_channels = opus2::Channels::from(state.audio_channels) as u16;
let audio_sample_rate = u32::from(state.audio_sample_rate);
let handler = Handler {
start_instant,
start_utc,
recordings: state.recording_data,
guild_id,
channel_id: voice_channel_id,
known_ssrcs: Default::default(),
audio_channels,
audio_sample_rate,
user_data_manager: state.user_data_manager,
};
{
let mut call = call.lock().await;
call.add_global_event(CoreEvent::SpeakingStateUpdate.into(), handler.clone());
call.add_global_event(CoreEvent::VoiceTick.into(), handler);
call.mute(true).await.expect("TODO");
}
let channel_mention = format!("<#{voice_channel_id}>");
let bot_owner_mention = format!("<@{}>", state.discord_bot_owner_user_id);
let opt_in_mention = format!(
"</{}:{}>",
state.discord_opt_in_command_name, state.discord_opt_in_command_id
);
let opt_out_mention = format!(
"</{}:{}>",
state.discord_opt_out_command_name, state.discord_opt_out_command_id
);
state
.discord_client
.interaction(state.discord_application_id)
.update_response(
&interaction.token,
).embeds(Some(&[
EmbedBuilder::new()
.title("Joined VC to record")
.description(format!("This bot joined {channel_mention} and intends to record. You can opt out with {opt_out_mention} or explicitly opt in with {opt_in_mention} (I'd appreciate this one). Here are some pledges backed by faith (because there is no way to verify them yourself) in {bot_owner_mention}:"))
.field(
EmbedFieldBuilder::new("Recordings are never shared", "Audio recordings are only stored on my home server and desktop computer and will never be uploaded to services or hardware that is owned by another person: not even curated clips, and not even to people who were in the recording. When transcription to text is implemented, this will only be run on my personally owned devices and not on any internet or cloud offering.").build()
)
.field(
EmbedFieldBuilder::new("You won't be \"audited\"", "I will not reference things said in past recordings with the goal of \"making a point\", nor pull them up on the spot (even by the request of the person who said it). Ideally, these are just peace of mind for me that I'm not missing out by not being in a Discord call all the time and can take my life back, so using them in an unhealthy way isn't in my interest.").build()
)
.field(
EmbedFieldBuilder::new("Code is publicly available", "The latest source code is at https://gitea.katniss.top/jacob/fomo-reducer so that I don't have to write guarantees about the technology here (e.g. what data is acquired, how it's used or stored) and you can just check it yourself.").build()
)
.footer(
EmbedFooterBuilder::new("Thanks for your patience and understanding as I have bad and unusual mental health and it's crazy that I need this. This - especially if I learn if I can record streams or webcams so I don't miss out on those experiences either - should be the end of abrasion and force about how we spend our time. Again, thank you, I appreciate it.")
)
.validate()
.unwrap()
.build()
]))
.await
.expect("TODO");
}

View File

@@ -3,35 +3,45 @@ use std::{fmt::Debug, sync::Arc};
use futures::future::BoxFuture; use futures::future::BoxFuture;
use opendal::Operator; use opendal::Operator;
use patricia_tree::StringPatriciaMap; use patricia_tree::StringPatriciaMap;
use songbird::Songbird; use songbird::{
Songbird,
driver::{Channels, SampleRate},
};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use twilight_model::{ use twilight_model::{
application::{command::Command, interaction::Interaction}, application::{command::Command, interaction::Interaction},
id::{ id::{
Id, Id,
marker::{ApplicationMarker, UserMarker}, marker::{ApplicationMarker, CommandMarker, UserMarker},
}, },
}; };
use crate::VCs; use crate::{BotDataManager, GuildVoiceChannelToTextChannel, UserDataManager, VCs};
mod debug; pub mod info;
mod join; pub mod join;
mod leave; pub mod leave;
mod opt_in; pub mod opt_in;
mod opt_out; pub mod opt_out;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct State { pub struct State {
pub bot_data: Operator, pub audio_channels: Channels,
pub audio_sample_rate: SampleRate,
pub bot_data_manager: BotDataManager,
pub cancellation_token: CancellationToken, pub cancellation_token: CancellationToken,
pub discord_application_id: Id<ApplicationMarker>, pub discord_application_id: Id<ApplicationMarker>,
pub discord_bot_owner_user_id: Id<UserMarker>, pub discord_bot_owner_user_id: Id<UserMarker>,
pub discord_client: Arc<twilight_http::Client>, pub discord_client: Arc<twilight_http::Client>,
pub discord_opt_in_command_id: Id<CommandMarker>,
pub discord_opt_in_command_name: Arc<str>,
pub discord_opt_out_command_id: Id<CommandMarker>,
pub discord_opt_out_command_name: Arc<str>,
pub discord_user_id: Id<UserMarker>, pub discord_user_id: Id<UserMarker>,
pub discord_voice_channel_corresponding_text_channel: Arc<GuildVoiceChannelToTextChannel>,
pub recording_data: Operator, pub recording_data: Operator,
pub songbird: Arc<Songbird>, pub songbird: Arc<Songbird>,
pub user_data: Operator, pub user_data_manager: UserDataManager,
pub vcs: Arc<VCs>, pub vcs: Arc<VCs>,
} }
@@ -48,7 +58,7 @@ where
pub fn all() -> Vec<(&'static Command, BoxedHandler)> { pub fn all() -> Vec<(&'static Command, BoxedHandler)> {
vec![ vec![
(&debug::COMMAND, box_handler(debug::handle)), (&info::COMMAND, box_handler(info::handle)),
(&join::COMMAND, box_handler(join::handle)), (&join::COMMAND, box_handler(join::handle)),
(&leave::COMMAND, box_handler(leave::handle)), (&leave::COMMAND, box_handler(leave::handle)),
(&opt_in::COMMAND, box_handler(opt_in::handle)), (&opt_in::COMMAND, box_handler(opt_in::handle)),

View File

@@ -1,12 +1,16 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use twilight_model::application::{ use twilight_model::{
command::{Command, CommandType}, application::{
interaction::Interaction, command::{Command, CommandType},
interaction::Interaction,
},
channel::message::MessageFlags,
http::interaction::{InteractionResponse, InteractionResponseType},
}; };
use twilight_util::builder::command::CommandBuilder; use twilight_util::builder::{InteractionResponseDataBuilder, command::CommandBuilder};
use crate::command::State; use crate::{command::State, user_capnp::user::Consent};
const NAME: &str = "opt-in"; const NAME: &str = "opt-in";
const DESCRIPTION: &str = "Opt in to being recorded"; const DESCRIPTION: &str = "Opt in to being recorded";
@@ -20,5 +24,67 @@ pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
#[tracing::instrument] #[tracing::instrument]
pub async fn handle(state: State, interaction: Interaction) { pub async fn handle(state: State, interaction: Interaction) {
todo!(); let user_id = interaction
.member
.as_ref()
.and_then(|member| member.user.as_ref().map(|user| user.id));
let user_id = match user_id {
Some(user_id) => user_id,
None => {
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.content("TODO")
.build(),
),
},
)
.await
.expect("TODO");
return;
}
};
let previous_consent = state
.user_data_manager
.update(user_id, |mut user_data| {
let previous_consent = user_data
.reborrow()
.get_voice_recording_consent()
.expect("TODO");
user_data.set_voice_recording_consent(Consent::Granted);
previous_consent
})
.await
.expect("TODO");
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.content(format!(
"opted you in, your previous consent was {previous_consent:?}"
))
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},
)
.await
.expect("TODO");
} }

View File

@@ -1,24 +1,90 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use twilight_model::application::{ use twilight_model::{
command::{Command, CommandType}, application::{
interaction::Interaction, command::{Command, CommandType},
}; interaction::Interaction,
use twilight_util::builder::command::CommandBuilder; },
channel::message::MessageFlags,
use crate::command::State; http::interaction::{InteractionResponse, InteractionResponseType},
};
const NAME: &str = "opt-out"; use twilight_util::builder::{InteractionResponseDataBuilder, command::CommandBuilder};
const DESCRIPTION: &str = "Opt out of being recorded";
use crate::{command::State, user_capnp::user::Consent};
pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput) const NAME: &str = "opt-out";
.validate() const DESCRIPTION: &str = "Opt out of being recorded";
.expect("command wasn't correct")
.build() pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
}); CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput)
.validate()
#[tracing::instrument] .expect("command wasn't correct")
pub async fn handle(state: State, interaction: Interaction) { .build()
todo!(); });
}
#[tracing::instrument]
pub async fn handle(state: State, interaction: Interaction) {
let user_id = interaction
.member
.as_ref()
.and_then(|member| member.user.as_ref().map(|user| user.id));
let user_id = match user_id {
Some(user_id) => user_id,
None => {
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.content("TODO")
.build(),
),
},
)
.await
.expect("TODO");
return;
}
};
let previous_consent = state
.user_data_manager
.update(user_id, |mut user_data| {
let previous_consent = user_data
.reborrow()
.get_voice_recording_consent()
.expect("TODO");
user_data.set_voice_recording_consent(Consent::Withheld);
previous_consent
})
.await
.expect("TODO");
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.content(format!(
"opted you out, your previous consent was {previous_consent:?}"
))
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},
)
.await
.expect("TODO");
}

View File

@@ -1,18 +1,24 @@
mod command; mod bot_data;
mod one_to_many; pub mod command;
mod one_to_many_with_data; mod one_to_many;
mod one_to_one; mod one_to_many_with_data;
mod storage; mod one_to_one;
mod track_vcs; mod operator_ext;
mod vc_user; mod option_ext;
mod storage;
pub use command::{Router as CommandRouter, State, all as all_commands}; mod track_vcs;
pub use one_to_many::OneToManyUniqueBTreeMap; mod user_data;
pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData; mod vc_user;
pub use one_to_one::OneToOneBTreeMap; capnp::generated_code!(mod bot_capnp);
pub use storage::Storage; capnp::generated_code!(mod user_capnp);
pub use track_vcs::{VCs, initialize_vcs, update_vcs};
pub use vc_user::{UserInVCData, VoiceStatus}; pub use bot_data::BotDataManager;
pub use command::{Router as CommandRouter, State, all as all_commands};
capnp::generated_code!(pub mod user_capnp); pub use one_to_many::OneToManyUniqueBTreeMap;
capnp::generated_code!(pub mod bot_capnp); pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData;
pub use one_to_one::OneToOneBTreeMap;
pub use operator_ext::OperatorExt;
pub use storage::Storage;
pub use track_vcs::{GuildVoiceChannelToTextChannel, VCs, initialize_vcs, update_vcs};
pub use user_data::UserDataManager;
pub use vc_user::{UserInVCData, VoiceStatus};

View File

@@ -1,9 +1,17 @@
use clap::Parser; use clap::Parser;
use fomo_reducer::{CommandRouter, State, Storage, all_commands, initialize_vcs, update_vcs}; use fomo_reducer::{
BotDataManager, CommandRouter, GuildVoiceChannelToTextChannel, State, Storage, UserDataManager,
all_commands, command, initialize_vcs, update_vcs,
};
use secrecy::{ExposeSecret, SecretString}; use secrecy::{ExposeSecret, SecretString};
use snafu::Snafu; use snafu::{OptionExt, ResultExt, Snafu};
use songbird::{Songbird, shards::TwilightMap}; use songbird::{
use std::{fmt::Debug, sync::Arc}; Config, Songbird,
driver::{Channels, DecodeConfig, SampleRate},
shards::TwilightMap,
};
use std::{collections::BTreeMap, fmt::Debug, str::FromStr, sync::Arc};
use strum::EnumString;
use tokio::{select, signal::ctrl_c, task::JoinSet}; use tokio::{select, signal::ctrl_c, task::JoinSet};
use tokio_util::{sync::CancellationToken, time::FutureExt as _}; use tokio_util::{sync::CancellationToken, time::FutureExt as _};
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan}; use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
@@ -14,9 +22,88 @@ use twilight_model::{
payload::{incoming::InteractionCreate, outgoing::UpdatePresence}, payload::{incoming::InteractionCreate, outgoing::UpdatePresence},
presence::{ActivityType, MinimalActivity, Status}, presence::{ActivityType, MinimalActivity, Status},
}, },
id::{Id, marker::UserMarker}, id::{
Id,
marker::{ChannelMarker, GuildMarker, UserMarker},
},
}; };
#[derive(Clone, Copy, Debug, strum::Display, EnumString)]
enum AudioChannels {
Mono,
Stereo,
}
impl From<AudioChannels> for Channels {
fn from(value: AudioChannels) -> Self {
match value {
AudioChannels::Mono => Channels::Mono,
AudioChannels::Stereo => Channels::Stereo,
}
}
}
#[derive(Clone, Copy, Debug, strum::Display, EnumString)]
enum AudioSampleRate {
#[strum(serialize = "8000Hz")]
Hz8000,
#[strum(serialize = "12000Hz")]
Hz12000,
#[strum(serialize = "16000Hz")]
Hz16000,
#[strum(serialize = "24000Hz")]
Hz24000,
#[strum(serialize = "48000Hz")]
Hz48000,
}
impl From<AudioSampleRate> for SampleRate {
fn from(value: AudioSampleRate) -> Self {
match value {
AudioSampleRate::Hz8000 => SampleRate::Hz8000,
AudioSampleRate::Hz12000 => SampleRate::Hz12000,
AudioSampleRate::Hz16000 => SampleRate::Hz16000,
AudioSampleRate::Hz24000 => SampleRate::Hz24000,
AudioSampleRate::Hz48000 => SampleRate::Hz48000,
}
}
}
#[derive(Debug, Snafu)]
enum ParseGuildVCToTextChannelError {
NoScope,
NoRelation,
ParseGuildError {
source: <Id<GuildMarker> as FromStr>::Err,
},
ParseVoiceChannelError {
source: <Id<ChannelMarker> as FromStr>::Err,
},
ParseTextChannelError {
source: <Id<ChannelMarker> as FromStr>::Err,
},
}
fn parse_guild_vc_to_text_channel(
source: &str,
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>, Id<ChannelMarker>), ParseGuildVCToTextChannelError>
{
let (guild, voice_channel_and_text_channel) = source.split_once(':').context(NoScopeSnafu)?;
let (voice_channel, text_channel) = voice_channel_and_text_channel
.split_once("->")
.context(NoRelationSnafu)?;
let guild = guild.parse().context(ParseGuildSnafu)?;
let voice_channel = voice_channel.parse().context(ParseVoiceChannelSnafu)?;
let text_channel = text_channel.parse().context(ParseTextChannelSnafu)?;
Ok((guild, voice_channel, text_channel))
}
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct AppArgs { struct AppArgs {
#[arg(long, env)] #[arg(long, env)]
@@ -31,6 +118,16 @@ struct AppArgs {
#[arg(long, env)] #[arg(long, env)]
discord_status: Option<Arc<str>>, discord_status: Option<Arc<str>>,
#[arg(long, env, value_parser = parse_guild_vc_to_text_channel)]
discord_voice_channel_corresponding_text_channel:
Vec<(Id<GuildMarker>, Id<ChannelMarker>, Id<ChannelMarker>)>,
#[arg(long, env, default_value_t = AudioChannels::Mono)]
audio_channels: AudioChannels,
#[arg(long, env, default_value_t = AudioSampleRate::Hz12000)]
audio_sample_rate: AudioSampleRate,
#[arg(long, env)] #[arg(long, env)]
bot_data: Storage, bot_data: Storage,
@@ -89,6 +186,9 @@ async fn main() -> Result<(), MainError> {
discord_bot_owner_user_id, discord_bot_owner_user_id,
discord_nickname, discord_nickname,
discord_status, discord_status,
discord_voice_channel_corresponding_text_channel,
audio_channels,
audio_sample_rate,
bot_data, bot_data,
user_data, user_data,
recording_data, recording_data,
@@ -159,15 +259,23 @@ async fn main() -> Result<(), MainError> {
.collect(), .collect(),
); );
let senders = Arc::new(senders); let audio_channels = audio_channels.into();
let audio_sample_rate = audio_sample_rate.into();
let senders = Arc::new(senders);
let songbird = Songbird::twilight(senders, discord_user_id); let songbird = Songbird::twilight(senders, discord_user_id);
songbird.set_config(
Config::default().decode_mode(songbird::driver::DecodeMode::Decode(DecodeConfig::new(
audio_channels,
audio_sample_rate,
))),
);
let interaction_client = discord_client.interaction(discord_application_id); let interaction_client = discord_client.interaction(discord_application_id);
let commands = all_commands(); let commands = all_commands();
let _returned_commands = interaction_client let returned_commands = interaction_client
.set_global_commands( .set_global_commands(
Vec::from_iter( Vec::from_iter(
commands commands
@@ -182,11 +290,30 @@ async fn main() -> Result<(), MainError> {
.await .await
.expect("failed to deserialize set commands"); // TODO .expect("failed to deserialize set commands"); // TODO
let command_router = CommandRouter::from_iter(commands); let mut discord_command_name_to_returned_command = BTreeMap::from_iter(
let command_router = Arc::new(command_router); returned_commands
.into_iter()
.map(|command| (command.name.clone(), command)),
);
let discord_opt_in_command = discord_command_name_to_returned_command
.remove(&command::opt_in::COMMAND.name)
.expect("TODO");
let discord_opt_out_command = discord_command_name_to_returned_command
.remove(&command::opt_out::COMMAND.name)
.expect("TODO");
let discord_opt_in_command_id = discord_opt_in_command.id.expect("TODO");
let discord_opt_out_command_id = discord_opt_out_command.id.expect("TODO");
let discord_opt_in_command_name = discord_opt_in_command.name.into();
let discord_opt_out_command_name = discord_opt_out_command.name.into();
let vcs = initialize_vcs(&discord_client).await; let vcs = initialize_vcs(&discord_client).await;
let command_router = CommandRouter::from_iter(commands);
let command_router = Arc::new(command_router);
let discord_client = Arc::new(discord_client); let discord_client = Arc::new(discord_client);
let songbird = Arc::new(songbird); let songbird = Arc::new(songbird);
let vcs = Arc::new(vcs); let vcs = Arc::new(vcs);
@@ -195,16 +322,42 @@ async fn main() -> Result<(), MainError> {
let recording_data = recording_data.into_inner(); let recording_data = recording_data.into_inner();
let user_data = user_data.into_inner(); let user_data = user_data.into_inner();
let bot_data_manager = BotDataManager::new(bot_data);
let user_data_manager = UserDataManager::new(user_data);
let discord_voice_channel_corresponding_text_channel = {
let mut map = GuildVoiceChannelToTextChannel::default();
for (guild_id, voice_channel_id, text_channel_id) in
discord_voice_channel_corresponding_text_channel
{
map.entry(guild_id)
.or_default()
.insert(voice_channel_id, text_channel_id);
}
map
};
let discord_voice_channel_corresponding_text_channel =
Arc::new(discord_voice_channel_corresponding_text_channel);
let state = State { let state = State {
bot_data, audio_channels,
audio_sample_rate,
bot_data_manager,
cancellation_token: cancellation_token.clone(), cancellation_token: cancellation_token.clone(),
discord_application_id, discord_application_id,
discord_bot_owner_user_id, discord_bot_owner_user_id,
discord_client, discord_client,
discord_opt_in_command_id,
discord_opt_in_command_name,
discord_opt_out_command_id,
discord_opt_out_command_name,
discord_user_id, discord_user_id,
discord_voice_channel_corresponding_text_channel,
recording_data, recording_data,
songbird, songbird,
user_data, user_data_manager,
vcs, vcs,
}; };
@@ -273,9 +426,22 @@ async fn handle_events(command_router: Arc<CommandRouter>, state: State, mut sha
.await .await
{ {
match event_res { match event_res {
Ok(twilight_model::gateway::event::Event::GatewayClose(frame_option)) => {
tracing::warn!(?frame_option);
return;
}
Ok(event) => { Ok(event) => {
handle_event(command_router.clone(), state.clone(), event).await; handle_event(command_router.clone(), state.clone(), event).await;
} }
Err(reconnect_error)
if matches!(
reconnect_error.kind(),
&twilight_gateway::error::ReceiveMessageErrorType::Reconnect
) =>
{
tracing::error!(?reconnect_error);
return;
}
Err(error) => { Err(error) => {
tracing::error!(?error); tracing::error!(?error);
} }

25
src/operator_ext.rs Normal file
View File

@@ -0,0 +1,25 @@
use extension_traits::extension;
use opendal::{Buffer, Error, ErrorKind, FuturesAsyncReader, Operator};
#[extension(pub trait OperatorExt)]
impl Operator {
async fn read_if_exists(&self, path: &str) -> Result<Option<Buffer>, Error> {
match self.read(path).await {
Ok(buffer) => Ok(Some(buffer)),
Err(error) if matches!(error.kind(), ErrorKind::NotFound) => Ok(None),
Err(error) => Err(error),
}
}
async fn async_reader_if_exists(
&self,
path: &str,
) -> Result<Option<FuturesAsyncReader>, Error> {
let reader = self.reader(path).await?;
match reader.into_futures_async_read(..).await {
Ok(reader) => Ok(Some(reader)),
Err(error) if matches!(error.kind(), ErrorKind::NotFound) => Ok(None),
Err(error) => Err(error),
}
}
}

11
src/option_ext.rs Normal file
View File

@@ -0,0 +1,11 @@
use extension_traits::extension;
#[extension(pub trait OptionExt)]
impl<S> Option<S> {
async fn map_async<M, Fut: Future<Output = M>, F: FnOnce(S) -> Fut>(self, f: F) -> Option<M> {
match self {
Some(s) => Some(f(s).await),
None => None,
}
}
}

View File

@@ -2,7 +2,6 @@ use std::{fmt::Debug, str::FromStr};
use opendal::{IntoOperatorUri, Operator, OperatorUri}; use opendal::{IntoOperatorUri, Operator, OperatorUri};
#[derive(Clone)] #[derive(Clone)]
pub struct Storage { pub struct Storage {
uri: OperatorUri, uri: OperatorUri,

View File

@@ -1,131 +1,135 @@
use dashmap::DashMap; use std::collections::BTreeMap;
use futures::{StreamExt, stream::FuturesUnordered};
use twilight_model::{ use dashmap::DashMap;
gateway::payload::incoming::VoiceStateUpdate, use futures::{StreamExt, stream::FuturesUnordered};
id::{ use twilight_model::{
Id, gateway::payload::incoming::VoiceStateUpdate,
marker::{ChannelMarker, GuildMarker, UserMarker}, id::{
}, Id,
}; marker::{ChannelMarker, GuildMarker, UserMarker},
},
use crate::{OneToManyUniqueBTreeMapWithData, UserInVCData, VoiceStatus}; };
type VCsInGuild = OneToManyUniqueBTreeMapWithData<Id<ChannelMarker>, Id<UserMarker>, UserInVCData>; use crate::{OneToManyUniqueBTreeMapWithData, OneToOneBTreeMap, UserInVCData, VoiceStatus};
pub type VCs = DashMap<Id<GuildMarker>, VCsInGuild>; pub type GuildVoiceChannelToTextChannel =
BTreeMap<Id<GuildMarker>, OneToOneBTreeMap<Id<ChannelMarker>, Id<ChannelMarker>>>;
#[tracing::instrument(skip(discord_client), ret)]
async fn initialize_user_in_vc( type VCsInGuild = OneToManyUniqueBTreeMapWithData<Id<ChannelMarker>, Id<UserMarker>, UserInVCData>;
discord_client: &twilight_http::Client, pub type VCs = DashMap<Id<GuildMarker>, VCsInGuild>;
guild_id: Id<GuildMarker>,
user_id: Id<UserMarker>, #[tracing::instrument(skip(discord_client), ret)]
) -> Option<(Id<ChannelMarker>, UserInVCData)> { async fn initialize_user_in_vc(
if let Ok(voice_state_res) = discord_client.user_voice_state(guild_id, user_id).await discord_client: &twilight_http::Client,
&& let Ok(voice_state) = voice_state_res.model().await guild_id: Id<GuildMarker>,
{ user_id: Id<UserMarker>,
tracing::info!(?user_id, ?voice_state); ) -> Option<(Id<ChannelMarker>, UserInVCData)> {
if let Ok(voice_state_res) = discord_client.user_voice_state(guild_id, user_id).await
let voice_status = VoiceStatus::builder() && let Ok(voice_state) = voice_state_res.model().await
.self_deafened(voice_state.self_deaf) {
.self_muted(voice_state.self_mute) tracing::info!(?user_id, ?voice_state);
.server_deafened(voice_state.deaf)
.server_muted(voice_state.mute) let voice_status = VoiceStatus::builder()
.camming(voice_state.self_video) .self_deafened(voice_state.self_deaf)
.streaming(voice_state.self_stream) .self_muted(voice_state.self_mute)
.build(); .server_deafened(voice_state.deaf)
let user_in_vc_data = voice_status.into(); .server_muted(voice_state.mute)
.camming(voice_state.self_video)
voice_state .streaming(voice_state.self_stream)
.channel_id .build();
.map(|channel_id| (channel_id, user_in_vc_data)) let user_in_vc_data = voice_status.into();
} else {
None // TODO voice_state
} .channel_id
} .map(|channel_id| (channel_id, user_in_vc_data))
} else {
#[tracing::instrument(skip(discord_client), ret)] None // TODO
async fn initialize_server_vcs( }
discord_client: &twilight_http::Client, }
id: Id<GuildMarker>,
) -> VCsInGuild { #[tracing::instrument(skip(discord_client), ret)]
if let Ok(guild_members_res) = discord_client.guild_members(id).limit(999).await async fn initialize_server_vcs(
&& let Ok(guild_members) = guild_members_res.model().await discord_client: &twilight_http::Client,
{ id: Id<GuildMarker>,
FuturesUnordered::from_iter(guild_members.into_iter().map(|member| async move { ) -> VCsInGuild {
( if let Ok(guild_members_res) = discord_client.guild_members(id).limit(999).await
member.user.id, && let Ok(guild_members) = guild_members_res.model().await
initialize_user_in_vc(discord_client, id, member.user.id).await, {
) FuturesUnordered::from_iter(guild_members.into_iter().map(|member| async move {
})) (
.filter_map( member.user.id,
|(user_id, channel_id_and_user_in_vc_data_option)| async move { initialize_user_in_vc(discord_client, id, member.user.id).await,
channel_id_and_user_in_vc_data_option )
.map(|(channel_id, user_in_vc_data)| (channel_id, user_id, user_in_vc_data)) }))
}, .filter_map(
) |(user_id, channel_id_and_user_in_vc_data_option)| async move {
.collect() channel_id_and_user_in_vc_data_option
.await .map(|(channel_id, user_in_vc_data)| (channel_id, user_id, user_in_vc_data))
} else { },
Default::default() )
} .collect()
} .await
} else {
#[tracing::instrument(skip(discord_client), ret)] Default::default()
pub async fn initialize_vcs(discord_client: &twilight_http::Client) -> VCs { }
if let Ok(guilds_res) = discord_client.current_user_guilds().limit(200).await }
&& let Ok(guilds) = guilds_res.model().await
{ #[tracing::instrument(skip(discord_client), ret)]
FuturesUnordered::from_iter(guilds.into_iter().map(|guild| async move { pub async fn initialize_vcs(discord_client: &twilight_http::Client) -> VCs {
let guild_vcs = initialize_server_vcs(discord_client, guild.id).await; if let Ok(guilds_res) = discord_client.current_user_guilds().limit(200).await
&& let Ok(guilds) = guilds_res.model().await
(guild.id, guild_vcs) {
})) FuturesUnordered::from_iter(guilds.into_iter().map(|guild| async move {
.collect() let guild_vcs = initialize_server_vcs(discord_client, guild.id).await;
.await
} else { (guild.id, guild_vcs)
Default::default() }))
} .collect()
} .await
} else {
#[tracing::instrument(skip(vcs))] Default::default()
pub fn update_vcs(voice_state_update: &VoiceStateUpdate, vcs: &VCs) { }
let user_id = voice_state_update.user_id; }
match voice_state_update.guild_id {
Some(guild_id) => match voice_state_update.channel_id { #[tracing::instrument(skip(vcs))]
Some(channel_id) => { pub fn update_vcs(voice_state_update: &VoiceStateUpdate, vcs: &VCs) {
let voice_status = VoiceStatus::builder() let user_id = voice_state_update.user_id;
.self_deafened(voice_state_update.self_deaf) match voice_state_update.guild_id {
.self_muted(voice_state_update.self_mute) Some(guild_id) => match voice_state_update.channel_id {
.server_deafened(voice_state_update.deaf) Some(channel_id) => {
.server_muted(voice_state_update.mute) let voice_status = VoiceStatus::builder()
.camming(voice_state_update.self_video) .self_deafened(voice_state_update.self_deaf)
.streaming(voice_state_update.self_stream) .self_muted(voice_state_update.self_mute)
.build(); .server_deafened(voice_state_update.deaf)
let user_in_vc_data = voice_status.into(); .server_muted(voice_state_update.mute)
.camming(voice_state_update.self_video)
vcs.entry(guild_id) .streaming(voice_state_update.self_stream)
.or_default() .build();
.insert(channel_id, user_id, user_in_vc_data); let user_in_vc_data = voice_status.into();
tracing::info!( vcs.entry(guild_id)
?guild_id, .or_default()
?channel_id, .insert(channel_id, user_id, user_in_vc_data);
?user_id,
"connected or otherwise changed state while connected" tracing::info!(
); ?guild_id,
} ?channel_id,
?user_id,
None => { "connected or otherwise changed state while connected"
if let Some(mut channel_vcers) = vcs.get_mut(&guild_id) { );
channel_vcers.remove_right(&user_id); }
}
None => {
tracing::info!(?guild_id, ?user_id, "disconnected"); if let Some(mut channel_vcers) = vcs.get_mut(&guild_id) {
} channel_vcers.remove_right(&user_id);
}, }
None => { tracing::info!(?guild_id, ?user_id, "disconnected");
tracing::error!("why doesn't this have a guild id attached?!"); }
} },
}
} None => {
tracing::error!("why doesn't this have a guild id attached?!");
}
}
}

254
src/user_data.rs Normal file
View File

@@ -0,0 +1,254 @@
use std::str::FromStr;
use async_compression::futures::{bufread::BrotliDecoder, write::BrotliEncoder};
use capnp::message::TypedBuilder;
use futures::{AsyncReadExt, AsyncWriteExt, TryStream, TryStreamExt};
use opendal::Operator;
use snafu::{OptionExt as _, ResultExt as _, Snafu, ensure};
use twilight_model::id::{Id, marker::UserMarker};
use crate::{OperatorExt, option_ext::OptionExt as _, user_capnp};
pub const RECORD_IF_CONSENT_UNSPECIFIED: bool = true;
#[derive(Debug, Clone)]
pub struct UserDataManager {
operator: Operator,
}
impl UserDataManager {
pub fn new(operator: Operator) -> Self {
Self { operator }
}
}
fn path(id: Id<UserMarker>) -> String {
format!("{id}/data.bin.brotli")
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum ParsePathError {
/// paths must have a / in them because that's how user data is stored, but this one doesn't have one
MissingSlashError,
/// if this isn't a directory, then the file must be "data.bin.brotli" but it was actually {actual:?}
WrongFileError { actual: String },
/// couldn't parse the directory as a user ID
ParseUserIdError {
source: <Id<UserMarker> as FromStr>::Err,
},
}
fn parse(path: &str) -> Result<Id<UserMarker>, ParsePathError> {
let (directory, file) = path
.rsplit_once("/")
.context(parse_path_error::MissingSlashSnafu)?;
ensure!(
file == "" || file == "data.bin.brotli",
parse_path_error::WrongFileSnafu {
actual: file.to_owned()
}
);
let user_id = directory
.parse()
.context(parse_path_error::ParseUserIdSnafu)?;
Ok(user_id)
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum ListError {
/// error creating a lister through the storage operator
CreateListerError { source: opendal::Error },
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum EntryError {
/// failed to get an entry from the storage operator's lister
ReceiveEntryError { source: opendal::Error },
/// failed to parse the entry as an acceptable path
ParsePathError { source: ParsePathError },
}
impl UserDataManager {
pub async fn list(
&self,
) -> Result<impl TryStream<Ok = Id<UserMarker>, Error = EntryError> + Unpin, ListError> {
let lister = self
.operator
.lister("")
.await
.context(list_error::CreateListerSnafu)?;
Ok(lister
.map_err(|error| EntryError::ReceiveEntryError { source: error })
.and_then(|entry| {
std::future::ready(parse(entry.path()).context(entry_error::ParsePathSnafu))
}))
}
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum WithError {
/// couldn't read data for this user from the storage operator
ReadError { source: opendal::Error },
/// couldn't decompress the user data from storage
DecompressionError { source: std::io::Error },
/// couldn't deserialize the user data
DeserializeError { source: capnp::Error },
}
impl UserDataManager {
pub async fn with<R>(
&self,
id: Id<UserMarker>,
f: impl FnOnce(user_capnp::user::Reader<'_>) -> R,
) -> Result<R, WithError> {
let compressed_buffer = self
.operator
.async_reader_if_exists(&path(id))
.await
.context(with_error::ReadSnafu)?;
let decompressed_reader = compressed_buffer.map(BrotliDecoder::new);
let decompressed = decompressed_reader
.map_async(|mut reader| async move {
let mut vec = Vec::new();
reader.read_to_end(&mut vec).await?;
Ok(vec)
})
.await
.transpose()
.context(with_error::DecompressionSnafu)?;
let mut message = TypedBuilder::<user_capnp::user::Owned>::new_default();
let fallback = message.init_root();
let mut user_data = fallback.into_reader();
let message_reader;
if let Some(mut bytes) = decompressed.as_deref() {
message_reader = capnp::serialize::read_message_from_flat_slice_no_alloc(
&mut bytes,
Default::default(),
)
.context(with_error::DeserializeSnafu)?;
user_data = message_reader
.get_root()
.context(with_error::DeserializeSnafu)?;
}
Ok(f(user_data))
}
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum UpdateError {
/// couldn't read data for this user from the storage operator
ReadError { source: opendal::Error },
/// couldn't decompress the user data from storage
DecompressionError { source: std::io::Error },
/// couldn't deserialize the user data
DeserializeError { source: capnp::Error },
/// couldn't serialize the (modified) user data
SerializeError { source: capnp::Error },
/// couldn't create a writer for this user data in the storage operator
WriterError { source: opendal::Error },
/// couldn't write (modified) data for this user to the storage operator
WriteError { source: std::io::Error },
/// couldn't finalize writing (modified) data for this user to the storage operator
FinalizeError { source: std::io::Error },
}
impl UserDataManager {
pub async fn update<R>(
&self,
id: Id<UserMarker>,
f: impl FnOnce(user_capnp::user::Builder<'_>) -> R,
) -> Result<R, UpdateError> {
let path = path(id);
let compressed_buffer = self
.operator
.async_reader_if_exists(&path)
.await
.context(update_error::ReadSnafu)?;
let decompressed_reader = compressed_buffer.map(BrotliDecoder::new);
let decompressed = decompressed_reader
.map_async(|mut reader| async move {
let mut vec = Vec::new();
reader.read_to_end(&mut vec).await?;
Ok(vec)
})
.await
.transpose()
.context(update_error::DecompressionSnafu)?;
let mut message = TypedBuilder::<user_capnp::user::Owned>::new_default();
let ret = if let Some(mut bytes) = decompressed.as_deref() {
let message_reader = capnp::serialize::read_message_from_flat_slice_no_alloc(
&mut bytes,
Default::default(),
)
.context(update_error::DeserializeSnafu)?;
let user_data = message_reader
.get_root()
.context(update_error::DeserializeSnafu)?;
message
.set_root(user_data)
.context(update_error::DeserializeSnafu)?;
f(
message.get_root().unwrap(), // this is logically impossible
)
} else {
f(message.init_root())
};
let mut buffer = Vec::new();
capnp::serialize::write_message(&mut buffer, message.borrow_inner())
.context(update_error::SerializeSnafu)?;
let compressed_writer = self
.operator
.writer(&path)
.await
.context(update_error::WriterSnafu)?
.into_futures_async_write();
let mut decompressed_writer = BrotliEncoder::new(compressed_writer);
decompressed_writer
.write_all(&buffer)
.await
.context(update_error::WriteSnafu)?;
decompressed_writer
.close()
.await
.context(update_error::FinalizeSnafu)?;
Ok(ret)
}
}

View File

@@ -3,5 +3,11 @@
struct User { struct User {
notificationScript @0 :Text; notificationScript @0 :Text;
voiceRecordingConsent @1 :Bool; voiceRecordingConsent @1 :Consent = unspecified;
enum Consent {
unspecified @0;
granted @1;
withheld @2;
}
} }