diff --git a/..env.un~ b/..env.un~ new file mode 100644 index 0000000..b8337f7 Binary files /dev/null and b/..env.un~ differ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dc56450 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +HS_URL=http://host.docker.internal:8008 +HS_TOKEN= +AS_TOKEN= +BOT_MXID=@_discord_bot:example.com + +DISCORD_TOKEN= +DISCORD_CHANNEL_ID= +MATRIX_ROOM_ID=!roomid:example.com + +# optional, defaults to bridge.db +DB_PATH=/data/bridge.db diff --git a/.gitignore b/.gitignore index fedaa2b..c8f9bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target .env +*.db +/data diff --git a/Cargo.lock b/Cargo.lock index 6209ec0..c2b3a7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,38 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +dependencies = [ + "serde", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -78,12 +104,27 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.12.1" @@ -119,8 +160,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures", - "rand_core", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", ] [[package]] @@ -132,6 +182,50 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -153,12 +247,40 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -168,6 +290,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.33" @@ -175,6 +311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -183,6 +320,29 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + [[package]] name = "futures-task" version = "0.3.33" @@ -195,12 +355,27 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -224,10 +399,37 @@ dependencies = [ "js-sys", "libc", "r-efi", - "rand_core", + "rand_core 0.10.1", "wasm-bindgen", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "http" version = "1.5.0" @@ -303,11 +505,11 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls", + "rustls 0.23.43", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", - "webpki-roots", + "webpki-roots 1.0.9", ] [[package]] @@ -471,6 +673,17 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libsqlite3-sys" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "litemap" version = "0.8.2" @@ -511,8 +724,10 @@ dependencies = [ "anyhow", "axum", "reqwest", + "rusqlite", "serde", "serde_json", + "serenity", "tokio", "tracing", "tracing-subscriber", @@ -532,6 +747,26 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.2" @@ -552,6 +787,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "once_cell" version = "1.21.4" @@ -593,6 +834,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "potential_utf" version = "0.1.5" @@ -602,6 +849,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -623,9 +885,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls", + "rustls 0.23.43", "socket2", - "thiserror", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -640,14 +902,14 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", - "rustls", + "rustls 0.23.43", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -682,6 +944,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.10.2" @@ -690,7 +963,26 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -705,7 +997,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -726,6 +1018,7 @@ dependencies = [ "base64", "bytes", "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -734,25 +1027,28 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime_guess", "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.43", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 1.0.9", ] [[package]] @@ -769,12 +1065,51 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.19", +] + +[[package]] +name = "rusqlite" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + [[package]] name = "rustc-hash" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.43" @@ -784,7 +1119,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -799,6 +1134,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.13" @@ -828,6 +1174,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "serde" version = "1.0.229" @@ -847,6 +1203,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_cow" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7bbbec7196bfde255ab54b65e34087c0849629280028238e67ee25d6a4b7da" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.229" @@ -894,6 +1259,45 @@ dependencies = [ "serde", ] +[[package]] +name = "serenity" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bde37f42765dfdc34e2a039e0c84afbf79a3101c1941763b0beb816c2f17541" +dependencies = [ + "arrayvec", + "async-trait", + "base64", + "bitflags", + "bytes", + "flate2", + "futures", + "mime_guess", + "percent-encoding", + "reqwest", + "secrecy", + "serde", + "serde_cow", + "serde_json", + "time", + "tokio", + "tokio-tungstenite", + "tracing", + "typemap_rev", + "url", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -919,6 +1323,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "slab" version = "0.4.12" @@ -941,6 +1351,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -995,13 +1417,33 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -1024,6 +1466,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -1077,13 +1549,53 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.43", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", "tokio", ] @@ -1197,6 +1709,45 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.7", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "typemap_rev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1219,6 +1770,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -1227,6 +1779,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -1250,6 +1808,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -1320,6 +1890,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.103" @@ -1340,6 +1923,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + [[package]] name = "webpki-roots" version = "1.0.9" @@ -1466,6 +2058,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 0d9bba7..eb903d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,6 @@ tracing-subscriber = "0.3" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } urlencoding = "2" uuid = { version = "1", features = ["v4"] } -anyhow = "1" \ No newline at end of file +anyhow = "1" +serenity = { version = "0.12.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +rusqlite = { version = "0.40.1", features = ["bundled"] } diff --git a/docker-compose.yaml b/docker-compose.yaml index 20ef4e3..d16dbc2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,4 +6,6 @@ services: - "9000:9000" extra_hosts: - "host.docker.internal:host-gateway" - env_file: .env \ No newline at end of file + env_file: .env + volumes: + - ./data:/data \ No newline at end of file diff --git a/src/bridge.rs b/src/bridge.rs new file mode 100644 index 0000000..3e06070 --- /dev/null +++ b/src/bridge.rs @@ -0,0 +1,58 @@ +use anyhow::Result; +use serenity::model::channel::Message as DiscordMessage; + +use crate::AppState; + +/// Looks up (or creates) the matrix ghost puppet for a discord user, registering +/// it via the appservice and joining it to the bridged room if needed. +pub async fn get_or_create_ghost( + state: &AppState, + discord_user_id: &str, + display_name: &str, +) -> Result { + if let Some((mxid, joined)) = state.db.get_ghost(discord_user_id).await? { + if !joined { + state.matrix.join_room(&state.matrix_room_id, &mxid).await?; + state.db.mark_joined(discord_user_id).await?; + } + return Ok(mxid); + } + + let localpart = format!("_discord_{}", discord_user_id); + let mxid = format!("@{}:{}", localpart, state.matrix_domain); + + state.matrix.register_ghost(&localpart).await?; + if let Err(e) = state.matrix.set_display_name(&mxid, display_name).await { + tracing::warn!("failed to set ghost display name for {}: {}", mxid, e); + } + state.matrix.join_room(&state.matrix_room_id, &mxid).await?; + + state.db.insert_ghost(discord_user_id, &mxid).await?; + state.db.mark_joined(discord_user_id).await?; + + tracing::info!( + "registered ghost {} for discord user {}", + mxid, + discord_user_id + ); + Ok(mxid) +} + +/// Discord -> Matrix: puppet the sending discord user via their ghost. +pub async fn forward_to_matrix(state: &AppState, msg: &DiscordMessage) -> Result<()> { + let mxid = get_or_create_ghost(state, &msg.author.id.to_string(), &msg.author.name).await?; + state + .matrix + .send_text(&state.matrix_room_id, &mxid, &msg.content) + .await?; + Ok(()) +} + +/// Matrix -> Discord: relay as the bridge bot, prefixed with the matrix sender. +pub async fn forward_to_discord(state: &AppState, sender: &str, body: &str) -> Result<()> { + state + .discord_channel_id + .say(&state.discord_http, format!("**{}**: {}", sender, body)) + .await?; + Ok(()) +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..745a3d2 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,53 @@ +use rusqlite::{Connection, OptionalExtension, params}; +use tokio::sync::Mutex; + +pub struct Db { + conn: Mutex, +} + +impl Db { + pub fn open(path: &str) -> rusqlite::Result { + let conn = Connection::open(path)?; + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS ghosts ( + discord_user_id TEXT PRIMARY KEY, + mxid TEXT NOT NULL, + joined_room INTEGER NOT NULL DEFAULT 0 + );", + )?; + Ok(Self { + conn: Mutex::new(conn), + }) + } + + pub async fn get_ghost( + &self, + discord_user_id: &str, + ) -> rusqlite::Result> { + let conn = self.conn.lock().await; + conn.query_row( + "SELECT mxid, joined_room FROM ghosts WHERE discord_user_id = ?1", + params![discord_user_id], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? != 0)), + ) + .optional() + } + + pub async fn insert_ghost(&self, discord_user_id: &str, mxid: &str) -> rusqlite::Result<()> { + let conn = self.conn.lock().await; + conn.execute( + "INSERT OR IGNORE INTO ghosts (discord_user_id, mxid, joined_room) VALUES (?1, ?2, 0)", + params![discord_user_id, mxid], + )?; + Ok(()) + } + + pub async fn mark_joined(&self, discord_user_id: &str) -> rusqlite::Result<()> { + let conn = self.conn.lock().await; + conn.execute( + "UPDATE ghosts SET joined_room = 1 WHERE discord_user_id = ?1", + params![discord_user_id], + )?; + Ok(()) + } +} diff --git a/src/discord.rs b/src/discord.rs new file mode 100644 index 0000000..69a7bf5 --- /dev/null +++ b/src/discord.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use serenity::async_trait; +use serenity::model::channel::Message; +use serenity::model::gateway::Ready; +use serenity::prelude::*; + +use crate::AppState; +use crate::bridge; + +pub struct Handler { + pub state: Arc, +} + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _ctx: Context, ready: Ready) { + tracing::info!("discord bot connected as {}", ready.user.name); + } + + async fn message(&self, _ctx: Context, msg: Message) { + if msg.author.bot { + return; + } + tracing::info!( + "discord [#{}] {}: {}", + msg.channel_id, + msg.author.name, + msg.content + ); + + if msg.channel_id != self.state.discord_channel_id { + return; + } + + let state = self.state.clone(); + tokio::spawn(async move { + if let Err(e) = bridge::forward_to_matrix(&state, &msg).await { + tracing::error!("failed to bridge discord->matrix: {}", e); + } + }); + } +} diff --git a/src/main.rs b/src/main.rs index c4d10f2..9a53116 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,9 @@ use axum::{ }; use serde::Deserialize; use serde_json::{Value, json}; +use serenity::http::Http; +use serenity::model::id::ChannelId; +use serenity::prelude::*; use std::sync::Arc; #[derive(Deserialize)] @@ -18,6 +21,9 @@ struct Transaction { events: Vec, } +mod bridge; +mod db; +mod discord; mod matrix; use matrix::MatrixClient; @@ -31,6 +37,11 @@ struct AppState { hs_token: String, bot_mxid: String, matrix: MatrixClient, + matrix_room_id: String, + matrix_domain: String, + discord_http: Arc, + discord_channel_id: ChannelId, + db: db::Db, } #[tokio::main] @@ -40,11 +51,45 @@ async fn main() { let hs_token = env_var("HS_TOKEN"); let as_token = env_var("AS_TOKEN"); let bot_mxid = env_var("BOT_MXID"); + let discord_token = env_var("DISCORD_TOKEN"); + let discord_channel_id: ChannelId = env_var("DISCORD_CHANNEL_ID") + .parse() + .expect("DISCORD_CHANNEL_ID must be a numeric discord channel id"); + let matrix_room_id = env_var("MATRIX_ROOM_ID"); + let db_path = env::var("DB_PATH").unwrap_or_else(|_| "bridge.db".to_string()); + + let matrix_domain = bot_mxid + .split(':') + .nth(1) + .expect("BOT_MXID must be in the form @user:domain") + .to_string(); + + let db = db::Db::open(&db_path).expect("failed to open sqlite db"); + let discord_http = Arc::new(Http::new(&discord_token)); let state = Arc::new(AppState { hs_token, bot_mxid, matrix: MatrixClient::new(hs_url, as_token), + matrix_room_id, + matrix_domain, + discord_http, + discord_channel_id, + db, + }); + + let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; + let mut discord_client = Client::builder(&discord_token, intents) + .event_handler(discord::Handler { + state: state.clone(), + }) + .await + .expect("failed to build discord client"); + + tokio::spawn(async move { + if let Err(e) = discord_client.start().await { + tracing::error!("discord client error: {}", e); + } }); let app = Router::new() @@ -74,7 +119,7 @@ async fn transactions( check_auth(&auth, &state.hs_token)?; tracing::info!("txn {} with {} events", txn_id, body.events.len()); for ev in &body.events { - if let Err(e) = handle_event(&state.matrix, &state.bot_mxid, ev).await { + if let Err(e) = handle_event(&state, ev).await { tracing::error!("error handling event: {}", e); } } @@ -101,17 +146,36 @@ async fn query_alias( Err(StatusCode::NOT_FOUND) } -async fn handle_event(client: &MatrixClient, bot_mxid: &str, ev: &Value) -> anyhow::Result<()> { +async fn handle_event(state: &AppState, ev: &Value) -> anyhow::Result<()> { let ev_type = ev["type"].as_str().unwrap_or(""); let room_id = ev["room_id"].as_str().unwrap_or(""); + let sender = ev["sender"].as_str().unwrap_or(""); + let bot_mxid = state.bot_mxid.as_str(); if ev_type == "m.room.member" && ev["state_key"].as_str() == Some(bot_mxid) && ev["content"]["membership"].as_str() == Some("invite") { tracing::info!("joining {} as {}", room_id, bot_mxid); - client.join_room(room_id, bot_mxid).await?; - client.send_text(room_id, bot_mxid, "hi, i'm alive").await?; + state.matrix.join_room(room_id, bot_mxid).await?; + state + .matrix + .send_text(room_id, bot_mxid, "hi, i'm alive") + .await?; } + + // Matrix -> Discord: relay messages from the bridged room, skipping our own + // ghosts and the bridge bot itself to avoid an echo loop. + if ev_type == "m.room.message" + && room_id == state.matrix_room_id + && sender != bot_mxid + && !sender.starts_with("@_discord_") + { + let body = ev["content"]["body"].as_str().unwrap_or(""); + if !body.is_empty() { + bridge::forward_to_discord(state, sender, body).await?; + } + } + Ok(()) } diff --git a/src/matrix.rs b/src/matrix.rs index f0d3374..1c8cf25 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -71,6 +71,21 @@ impl MatrixClient { Ok(res["event_id"].as_str().unwrap_or("").to_string()) } + pub async fn set_display_name(&self, user_id: &str, name: &str) -> Result<()> { + let path = format!( + "/_matrix/client/v3/profile/{}/displayname", + urlencoding::encode(user_id) + ); + self.req( + reqwest::Method::PUT, + &path, + Some(user_id), + Some(json!({ "displayname": name })), + ) + .await?; + Ok(()) + } + pub async fn register_ghost(&self, localpart: &str) -> Result<()> { let body = json!({ "type": "m.login.application_service",