From 27e9f02cbc6790edd58c853e0c17090ace52eecd Mon Sep 17 00:00:00 2001 From: Eduard Smet Date: Thu, 12 Mar 2026 00:59:04 +0100 Subject: [PATCH 1/5] WIP: feat: Plugin API rework --- Cargo.lock | 427 +++++++++++++++--- Cargo.toml | 40 +- src/cli.rs | 3 + src/database.rs | 92 ++++ src/discord.rs | 167 ++++--- src/discord/events.rs | 138 +++--- src/discord/interactions.rs | 69 ++- src/discord/requests.rs | 181 ++++---- src/http/registry.rs | 6 +- src/job_scheduler.rs | 188 +++----- src/main.rs | 192 ++++---- src/plugins.rs | 73 +-- src/plugins/permissions.rs | 55 +++ src/plugins/registry.rs | 49 +- src/plugins/runtime.rs | 380 ++++------------ src/plugins/runtime/internal.rs | 136 +----- src/plugins/runtime/internal/core.rs | 134 ++++++ src/plugins/runtime/internal/discord.rs | 38 ++ src/plugins/runtime/internal/job_scheduler.rs | 28 ++ src/utils/channels.rs | 120 +++-- src/utils/env.rs | 34 +- wit/core.wit | 62 +++ wit/discord.wit | 133 ++++-- wit/host.wit | 42 -- wit/job-scheduler.wit | 36 ++ wit/plugin.wit | 64 --- wit/world.wit | 15 +- 27 files changed, 1607 insertions(+), 1295 deletions(-) create mode 100644 src/database.rs create mode 100644 src/plugins/permissions.rs create mode 100644 src/plugins/runtime/internal/core.rs create mode 100644 src/plugins/runtime/internal/discord.rs create mode 100644 src/plugins/runtime/internal/job_scheduler.rs create mode 100644 wit/core.wit delete mode 100644 wit/host.wit create mode 100644 wit/job-scheduler.wit delete mode 100644 wit/plugin.wit diff --git a/Cargo.lock b/Cargo.lock index a941dac..aa999f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -83,9 +83,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -216,11 +216,23 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "byteview" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1c53ba0f290bfc610084c05582d9c5d421662128fc69f4bf236707af6fd321b9" [[package]] name = "cap-fs-ext" @@ -332,9 +344,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -355,9 +367,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -365,9 +377,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -377,9 +389,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -389,9 +401,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" @@ -427,6 +439,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "compare" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0095f6103c2a8b44acd6fd15960c801dafebf02e21940360833e0673f48ba7" + [[package]] name = "core-foundation" version = "0.9.4" @@ -663,6 +681,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-skiplist" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -823,9 +851,10 @@ dependencies = [ "chrono", "clap", "dotenvy", + "fjall", "indexmap", "reqwest", - "rustls 0.23.36", + "rustls 0.23.37", "semver", "serde", "serde_yaml_ng", @@ -841,6 +870,7 @@ dependencies = [ "twilight-http", "twilight-model", "url", + "uuid", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", @@ -917,6 +947,18 @@ dependencies = [ "syn", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -980,6 +1022,23 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fjall" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9530ff159bc3ad3a15da746da0f6e95375c2ac64708cbb85ec1ebd26761a84" +dependencies = [ + "byteorder-lite", + "byteview", + "dashmap", + "flume", + "log", + "lsm-tree", + "lz4_flex", + "tempfile", + "xxhash-rust", +] + [[package]] name = "float-cmp" version = "0.10.0" @@ -989,6 +1048,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1144,11 +1212,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.32.3" @@ -1356,7 +1437,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "rustls-platform-verifier", "tokio", @@ -1382,7 +1463,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -1544,9 +1625,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -1554,6 +1635,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "interval-heap" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11274e5e8e89b8607cfedc2910b6626e998779b48a019151c7604d0adcb86ac6" +dependencies = [ + "compare", +] + [[package]] name = "io-extras" version = "0.18.4" @@ -1701,9 +1791,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libm" @@ -1760,6 +1850,37 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lsm-tree" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d67f95fd716870329c30aaeedf87f23d426564e6ce46efa045a91444faf2a19" +dependencies = [ + "byteorder-lite", + "byteview", + "crossbeam-skiplist", + "enum_dispatch", + "interval-heap", + "log", + "lz4_flex", + "quick_cache", + "rustc-hash", + "self_cell", + "sfa", + "tempfile", + "varint-rs", + "xxhash-rust", +] + +[[package]] +name = "lz4_flex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" +dependencies = [ + "twox-hash", +] + [[package]] name = "mach2" version = "0.4.3" @@ -1798,9 +1919,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -2039,6 +2160,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2091,6 +2222,16 @@ dependencies = [ "syn", ] +[[package]] +name = "quick_cache" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a70b1b8b47e31d0498ecbc3c5470bb931399a8bfed1fd79d1717a61ce7f96e3" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2103,8 +2244,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls 0.23.37", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -2124,7 +2265,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -2142,16 +2283,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2162,6 +2303,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rancor" version = "0.1.1" @@ -2346,9 +2493,9 @@ checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64", "bytes", @@ -2369,7 +2516,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "rustls-platform-verifier", "sync_wrapper", @@ -2406,9 +2553,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360b333c61ae24e5af3ae7c8660bd6b21ccd8200dbbc5d33c2454421e85b9c69" +checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" dependencies = [ "bytes", "hashbrown 0.16.1", @@ -2424,9 +2571,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02f8cdd12b307ab69fe0acf4cd2249c7460eb89dce64a0febadf934ebb6a9e" +checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" dependencies = [ "proc-macro2", "quote", @@ -2497,9 +2644,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -2543,7 +2690,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.9", @@ -2641,6 +2788,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.27" @@ -2750,6 +2903,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sfa" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1296838937cab56cd6c4eeeb8718ec777383700c33f060e2869867bd01d1175" +dependencies = [ + "byteorder-lite", + "log", + "xxhash-rust", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -2855,12 +3019,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2874,9 +3038,9 @@ dependencies = [ [[package]] name = "sonic-rs" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4425ea8d66ec950e0a8f2ef52c766cc3d68d661d9a0845c353c40833179fd866" +checksum = "d971cc77a245ccf1756dbd1a87c3e7f709c0191464096510d43eec056d0f2c4f" dependencies = [ "ahash", "bumpalo", @@ -2885,12 +3049,12 @@ dependencies = [ "faststr", "itoa", "ref-cast", - "ryu", "serde", "simdutf8", "sonic-number", "sonic-simd", "thiserror 2.0.18", + "zmij", ] [[package]] @@ -2902,6 +3066,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2943,9 +3116,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3150,9 +3323,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" dependencies = [ "bytes", "libc", @@ -3160,7 +3333,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -3184,9 +3357,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -3210,7 +3383,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls 0.23.37", "tokio", ] @@ -3391,9 +3564,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -3464,7 +3637,7 @@ dependencies = [ "hyper-rustls", "hyper-util", "percent-encoding", - "rustls 0.23.36", + "rustls 0.23.37", "serde", "serde_json", "simd-json", @@ -3506,6 +3679,12 @@ dependencies = [ "twilight-model", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "typenum" version = "1.19.0" @@ -3568,11 +3747,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -3595,6 +3774,12 @@ dependencies = [ "ryu", ] +[[package]] +name = "varint-rs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" + [[package]] name = "version_check" version = "0.9.5" @@ -3635,6 +3820,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -3735,6 +3929,18 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + [[package]] name = "wasmparser" version = "0.243.0" @@ -3755,6 +3961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", + "hashbrown 0.15.5", "indexmap", "semver", ] @@ -3886,7 +4093,7 @@ dependencies = [ "syn", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser", + "wit-parser 0.243.0", ] [[package]] @@ -4027,7 +4234,7 @@ dependencies = [ "bitflags", "heck", "indexmap", - "wit-parser", + "wit-parser 0.243.0", ] [[package]] @@ -4671,6 +4878,70 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] [[package]] name = "wit-parser" @@ -4690,6 +4961,24 @@ dependencies = [ "wasmparser 0.243.0", ] +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + [[package]] name = "witx" version = "0.9.1" @@ -4708,6 +4997,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 804d60f..6d3f5ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,24 +9,25 @@ edition = "2024" [features] [dependencies] -anyhow = "1" -bytes = "1" -chrono = "0.4" -clap = { version = "4", features = ["derive"] } -dotenvy = "0.15" -indexmap = "2" -reqwest = { version = "0.13", features = ["hickory-dns"] } -rustls = "0.23" -semver = "1" -serde = "1" -serde_yaml_ng = "0.10" # Should replace this with a better maintained YAML 1.2 supporting alternative -sonic-rs = "0.5" -tokio = { version = "1", features = ["full"] } -tokio-cron-scheduler = { version = "0.15", features = ["english"] } -tokio-util = "0.7" -tracing = "0.1" -tracing-subscriber = "0.3" -tracing-appender = "0.2" +anyhow = "1.0.102" +bytes = "1.11.1" +chrono = "0.4.44" +clap = { version = "4.6.0", features = ["derive"] } +dotenvy = "0.15.7" +fjall = "3.1.2" +indexmap = "2.13.1" +reqwest = { version = "0.13.2", features = ["hickory-dns"] } +rustls = "0.23.37" +semver = "1.0.27" +serde = "1.0.102" +serde_yaml_ng = "0.10.0" # Should replace this with a better maintained YAML 1.2 supporting alternative +sonic-rs = "0.5.8" +tokio = { version = "1.51.0", features = ["full"] } +tokio-cron-scheduler = { version = "0.15.1", features = ["english"] } +tokio-util = "0.7.18" +tracing = "0.1.44" +tracing-subscriber = "0.3.23" +tracing-appender = "0.2.4" twilight-cache-inmemory = { git = "https://github.com/celarye/twilight/", branch = "feat/form-buffer" } twilight-gateway = { git = "https://github.com/celarye/twilight/", branch = "feat/form-buffer", features = [ "simd-json", @@ -36,7 +37,8 @@ twilight-http = { git = "https://github.com/celarye/twilight/", branch = "feat/f "hickory", ] } twilight-model = { git = "https://github.com/celarye/twilight/", branch = "feat/form-buffer" } -url = "2" +url = "2.5.8" +uuid = "1.11.1" wasmtime = "41" wasmtime-wasi = "41" wasmtime-wasi-http = "41" diff --git a/src/cli.rs b/src/cli.rs index 6d34823..f90b59f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -39,6 +39,9 @@ pub struct Cli { #[arg(action=ArgAction::Set, default_value_t = true, short = 'C', long, value_name = "BOOL", help = "Enable the usage of cached plugins", long_help = None, hide_possible_values = true)] pub cache: bool, + #[arg(default_value = "./database", short, long, value_name = "DIRECTORY PATH", help = "The path to the program its database", long_help = None)] + pub database_directory: PathBuf, + #[arg(default_value_t = 15, short = 't', long, value_name = "SECONDS", help = "The amount of seconds after which the HTTP client should timeout", long_help = None)] pub http_client_timeout_seconds: u64, } diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..1a1affd --- /dev/null +++ b/src/database.rs @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2026 Eduard Smet */ + +use std::{ + fs::{self}, + io::ErrorKind, + path::Path, +}; + +use anyhow::{Result, bail}; +use fjall::{Database, KeyspaceCreateOptions, PersistMode, Slice}; + +use crate::utils::channels::DatabaseMessages; + +pub enum Keyspaces { + Plugins, + PluginStore, + DependencyFunctions, + ScheduledJobs, + DiscordEvents, + DiscordApplicationCommands, + DiscordMessageComponents, + DiscordModals, +} + +pub fn new(database_directory_path: &Path) -> Result { + if let Err(err) = fs::create_dir_all(database_directory_path) + && err.kind() != ErrorKind::AlreadyExists + { + bail!(err); + } + + Ok(Database::builder(database_directory_path).open()?) +} + +pub fn handle_action(database: Database, message: DatabaseMessages) { + match message { + DatabaseMessages::GetState(keyspace, key, response_sender) => { + response_sender.send(get(database, keyspace, key)); + } + DatabaseMessages::InsertState(keyspace, key, value, response_sender) => { + response_sender.send(insert(database, keyspace, key, value)); + } + DatabaseMessages::DeleteState(keyspace, key, response_sender) => { + response_sender.send(remove(database, keyspace, key)); + } + DatabaseMessages::ContainsKey(keyspace, key, response_sender) => { + response_sender.send(contains_key(database, keyspace, key)); + } + } +} + +pub fn get(database: Database, keyspace: Keyspaces, key: Vec) -> Result> { + let keyspace = database.keyspace(get_keyspace(keyspace), KeyspaceCreateOptions::default)?; + + Ok(keyspace.get(key)?) +} + +pub fn insert(database: Database, keyspace: Keyspaces, key: Vec, value: Vec) -> Result<()> { + let keyspace = database.keyspace(get_keyspace(keyspace), KeyspaceCreateOptions::default)?; + + Ok(keyspace.insert(key, value)?) +} + +pub fn remove(database: Database, keyspace: Keyspaces, key: Vec) -> Result<()> { + let keyspace = database.keyspace(get_keyspace(keyspace), KeyspaceCreateOptions::default)?; + + Ok(keyspace.remove(key)?) +} + +pub fn contains_key(database: Database, keyspace: Keyspaces, key: Vec) -> Result { + let keyspace = database.keyspace(get_keyspace(keyspace), KeyspaceCreateOptions::default)?; + + Ok(keyspace.contains_key(key)?) +} + +pub fn persist(database: Database, persist_mode: PersistMode) -> Result<()> { + Ok(database.persist(persist_mode)?) +} + +fn get_keyspace(keyspace: Keyspaces) -> &'static str { + match keyspace { + Keyspaces::Plugins => "plugins", + Keyspaces::PluginStore => "plugin_store", + Keyspaces::DependencyFunctions => "dependency_functions", + Keyspaces::ScheduledJobs => "scheduled_jobs", + Keyspaces::DiscordEvents => "discord_events", + Keyspaces::DiscordApplicationCommands => "discord_application_commands", + Keyspaces::DiscordMessageComponents => "discord_message_componets", + Keyspaces::DiscordModals => "discord_modals", + } +} diff --git a/src/discord.rs b/src/discord.rs index 59bfb07..991ce6e 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,27 +1,22 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; use tokio::{ - sync::{ - Mutex, RwLock, - mpsc::{Receiver, Sender}, - }, + sync::mpsc::{UnboundedReceiver, UnboundedSender}, task::JoinHandle, }; use tracing::{error, info}; -use twilight_cache_inmemory::{DefaultInMemoryCache, InMemoryCache, ResourceType}; +use twilight_cache_inmemory::{DefaultInMemoryCache, InMemoryCache}; use twilight_gateway::{ - CloseFrame, Config, Event, EventType, EventTypeFlags, Intents, MessageSender, Shard, StreamExt, + CloseFrame, Config, EventType, EventTypeFlags, Intents, MessageSender, Shard, StreamExt, }; use twilight_http::Client; -use twilight_model::id::{Id, marker::GuildMarker}; use crate::{ SHUTDOWN, - plugins::PluginRegistrations, - utils::channels::{DiscordBotClientMessages, RuntimeMessages}, + utils::channels::{CoreMessages, DiscordBotClientMessages}, }; mod events; @@ -30,23 +25,22 @@ mod requests; pub struct DiscordBotClient { http_client: Arc, - shard_message_senders: Arc, Arc>>>, + shards: Vec, + shard_message_senders: Arc>, cache: Arc, - plugin_registrations: Arc>, - runtime_tx: Arc>, - runtime_rx: Arc>>, + core_tx: Arc>, + rx: UnboundedReceiver, } impl DiscordBotClient { pub async fn new( token: String, - plugin_registrations: Arc>, - runtime_tx: Sender, - runtime_rx: Receiver, - ) -> Result<(Self, Box + Send>), ()> { + core_tx: UnboundedSender, + rx: UnboundedReceiver, + ) -> Result { info!("Creating the Discord bot client"); - let intents = Intents::all(); + let intents = Intents::all(); // TODO: Make this configurable rustls::crypto::aws_lc_rs::default_provider() .install_default() @@ -56,14 +50,14 @@ impl DiscordBotClient { let config = Config::new(token, intents); - let shards = match twilight_gateway::create_recommended( + let (shards, shard_message_senders) = match twilight_gateway::create_recommended( &http_client, config, |_, builder| builder.build(), ) .await { - Ok(shards) => Box::new(shards), + Ok(shard_iterator) => Self::shard_message_senders(Box::new(shard_iterator)), Err(err) => { error!( "Something went wrong while getting the recommended amount of shards from Discord, error: {}", @@ -73,74 +67,66 @@ impl DiscordBotClient { } }; - let shard_message_senders = Arc::new(RwLock::new(HashMap::new())); - - let cache = Arc::new( - DefaultInMemoryCache::builder() - .resource_types(ResourceType::all()) - .build(), - ); - - Ok(( - DiscordBotClient { - http_client: Arc::new(http_client), - shard_message_senders, - cache, - plugin_registrations, - runtime_tx: Arc::new(runtime_tx), - runtime_rx: Arc::new(Mutex::new(runtime_rx)), - }, + let cache = Arc::new(DefaultInMemoryCache::default()); // TODO: Make this configurable + + Ok(DiscordBotClient { + http_client: Arc::new(http_client), shards, - )) + shard_message_senders: Arc::new(shard_message_senders), + cache, + core_tx: Arc::new(core_tx), + rx, + }) } - pub fn start(self, shards: Box + Send>) -> JoinHandle<()> { - let mut tasks = Vec::with_capacity(shards.len()); - - let discord_bot_client = Arc::new(self); + pub fn start(mut self) -> JoinHandle<()> { + let mut tasks = Vec::with_capacity(self.shards.len()); - for shard in shards { + for shard in self.shards.drain(..) { tasks.push(tokio::spawn(Self::shard_runner( - discord_bot_client.clone(), + self.cache.clone(), + self.core_tx.clone(), shard, ))); } tokio::spawn(async move { - while let Some(message) = discord_bot_client.runtime_rx.lock().await.recv().await { + while let Some(message) = self.rx.recv().await { match message { - DiscordBotClientMessages::RegisterApplicationCommands(commands) => { - let _ = discord_bot_client - .application_command_registrations(commands) - .await; + DiscordBotClientMessages::RegisterApplicationCommands( + commands, + response_sender, + ) => { + let http_client = self.http_client.clone(); + tokio::spawn(async { + response_sender.send( + Self::application_command_registrations(http_client, commands) + .await, + ); + }); } DiscordBotClientMessages::Request(request, response_sender) => { - let _ = response_sender.send(discord_bot_client.request(request).await); - } - DiscordBotClientMessages::Shutdown(is_done) => { - for sender in discord_bot_client - .shard_message_senders - .read() - .await - .values() - { - _ = sender.close(CloseFrame::NORMAL); - } - - for task in tasks.drain(..) { - let _ = task.await; - } - - let _ = is_done.send(()); + let http_client = self.http_client.clone(); + let shard_message_senders = self.shard_message_senders.clone(); + + tokio::spawn(async { + response_sender.send( + Self::request(http_client, shard_message_senders, request).await, + ); + }); } } } + + self.shutdown(tasks); }) } - pub async fn shard_runner(discord_bot_client: Arc, mut shard: Shard) { - let shard_message_sender = Arc::new(shard.sender()); - + async fn shard_runner( + cache: Arc, + core_tx: Arc>, + mut shard: Shard, + ) { while let Some(item) = shard.next_event(EventTypeFlags::all()).await { let Ok(event) = item else { error!( @@ -155,24 +141,33 @@ impl DiscordBotClient { break; } - discord_bot_client.cache.update(&event); + cache.update(&event); - match event { - Event::Ready(ready) => { - info!("Shard is ready, logged in as {}", &ready.user.name); + tokio::spawn(Self::handle_event(core_tx.clone(), event)); + } + } - for guild in ready.guilds { - discord_bot_client - .shard_message_senders - .write() - .await - .insert(guild.id, shard_message_sender.clone()); - } - } - _ => { - tokio::spawn(Self::handle_event(discord_bot_client.clone(), event)); - } - } + fn shard_message_senders( + shard_iterator: Box>, + ) -> (Vec, Vec) { + let mut shards = vec![]; + let mut shard_message_senders = vec![]; + + for shard in shard_iterator { + shard_message_senders.push(shard.sender()); + shards.push(shard); + } + + (shards, shard_message_senders) + } + + async fn shutdown(&self, mut tasks: Vec>) { + for shard_message_sender in self.shard_message_senders.iter() { + _ = shard_message_sender.close(CloseFrame::NORMAL); + } + + for task in tasks.drain(..) { + let _ = task.await; } } } diff --git a/src/discord/events.rs b/src/discord/events.rs index 42cb2e7..d6c125b 100644 --- a/src/discord/events.rs +++ b/src/discord/events.rs @@ -1,66 +1,48 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use std::{any::Any, sync::Arc}; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, error}; use twilight_gateway::Event; use twilight_model::application::interaction::InteractionData; use crate::{ discord::DiscordBotClient, - plugins::discord_bot::plugin::discord_types::Events as DiscordEvents, - utils::channels::RuntimeMessages, + plugins::discord_bot::plugin::discord_export_types::DiscordEvents, + utils::channels::{CoreMessages, RuntimeMessages, RuntimeMessagesDiscord}, }; impl DiscordBotClient { #[allow(clippy::too_many_lines)] - pub async fn handle_event(discord_bot_client: Arc, event: Event) { + pub async fn handle_event(core_tx: Arc>, event: Event) { match event { Event::InteractionCreate(interaction_create) => { match interaction_create.data.as_ref() { Some(InteractionData::ApplicationCommand(command_data)) => { - let initialized_plugins = - discord_bot_client.plugin_registrations.read().await; + // TODO: get value - let Some(plugin) = initialized_plugins - .discord_events - .interaction_create - .application_commands - .get(&command_data.id) - else { - return; - }; - - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::InteractionCreate( - sonic_rs::to_vec(&interaction_create).unwrap(), + let _ = core_tx + .send(CoreMessages::Runtime(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin_uuid, + DiscordEvents::InteractionCreate( + sonic_rs::to_vec(&interaction_create).unwrap(), + ), ), - )) + ))) .await; } Some(InteractionData::MessageComponent(message_component_interaction_data)) => { - let initialized_plugins = - discord_bot_client.plugin_registrations.read().await; - - let Some(plugin) = initialized_plugins - .discord_events - .interaction_create - .message_components - .get(&message_component_interaction_data.custom_id) - else { - return; - }; - let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::InteractionCreate( - sonic_rs::to_vec(&interaction_create).unwrap(), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::InteractionCreate( + sonic_rs::to_vec(&interaction_create).unwrap(), + ), ), )) .await; @@ -80,10 +62,12 @@ impl DiscordBotClient { let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::InteractionCreate( - sonic_rs::to_vec(&interaction_create).unwrap(), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::InteractionCreate( + sonic_rs::to_vec(&interaction_create).unwrap(), + ), ), )) .await; @@ -105,10 +89,12 @@ impl DiscordBotClient { { let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::MessageCreate( - sonic_rs::to_vec(&message_create).unwrap(), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::MessageCreate( + sonic_rs::to_vec(&message_create).unwrap(), + ), ), )) .await; @@ -124,9 +110,13 @@ impl DiscordBotClient { { let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadCreate(sonic_rs::to_vec(&thread_create).unwrap()), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::ThreadCreate( + sonic_rs::to_vec(&thread_create).unwrap(), + ), + ), )) .await; } @@ -141,9 +131,13 @@ impl DiscordBotClient { { let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadDelete(sonic_rs::to_vec(&thread_delete).unwrap()), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::ThreadDelete( + sonic_rs::to_vec(&thread_delete).unwrap(), + ), + ), )) .await; } @@ -158,10 +152,12 @@ impl DiscordBotClient { { let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadListSync( - sonic_rs::to_vec(&thread_list_sync).unwrap(), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::ThreadListSync( + sonic_rs::to_vec(&thread_list_sync).unwrap(), + ), ), )) .await; @@ -177,10 +173,12 @@ impl DiscordBotClient { { let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadMemberUpdate( - sonic_rs::to_vec(&thread_member_update).unwrap(), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::ThreadMemberUpdate( + sonic_rs::to_vec(&thread_member_update).unwrap(), + ), ), )) .await; @@ -196,10 +194,12 @@ impl DiscordBotClient { { let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadMembersUpdate( - sonic_rs::to_vec(&thread_members_update).unwrap(), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::ThreadMembersUpdate( + sonic_rs::to_vec(&thread_members_update).unwrap(), + ), ), )) .await; @@ -215,9 +215,13 @@ impl DiscordBotClient { { let _ = discord_bot_client .runtime_tx - .send(RuntimeMessages::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadUpdate(sonic_rs::to_vec(&thread_update).unwrap()), + .send(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin.clone(), + DiscordEvents::ThreadUpdate( + sonic_rs::to_vec(&thread_update).unwrap(), + ), + ), )) .await; } diff --git a/src/discord/interactions.rs b/src/discord/interactions.rs index 06f559e..1d3dc68 100644 --- a/src/discord/interactions.rs +++ b/src/discord/interactions.rs @@ -1,10 +1,11 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; +use anyhow::Result; use tracing::{error, info}; -use twilight_http::{request::Request, routing::Route}; +use twilight_http::{Client, request::Request, routing::Route}; use twilight_model::{ application::command::Command, id::{ @@ -17,11 +18,11 @@ use crate::{discord::DiscordBotClient, plugins::PluginRegistrationRequestsApplic impl DiscordBotClient { pub async fn application_command_registrations( - &self, + http_client: Arc, discord_application_command_registration_request: Vec< PluginRegistrationRequestsApplicationCommand, >, - ) -> Result<(), ()> { + ) -> Result<(Vec, Vec)> { let mut discord_commands = HashMap::new(); let mut commands = HashMap::new(); @@ -44,7 +45,7 @@ impl DiscordBotClient { .push((command.plugin_id, command_data.clone())); } - let application_id = match self.http_client.current_user_application().await { + let application_id = match http_client.current_user_application().await { Ok(response) => match response.model().await { Ok(application) => application.id, Err(err) => { @@ -80,8 +81,7 @@ impl DiscordBotClient { } }; - match self - .http_client + match http_client .request::>(global_discord_commands_request) .await { @@ -110,7 +110,7 @@ impl DiscordBotClient { } // TODO: Endpoint is limited to 200 guilds per request, pagination needs to be implemented. - let current_user_guilds = match self.http_client.current_user_guilds().await { + let current_user_guilds = match http_client.current_user_guilds().await { Ok(response) => match response.model().await { Ok(current_user_guilds) => current_user_guilds, Err(err) => { @@ -148,8 +148,7 @@ impl DiscordBotClient { } }; - match self - .http_client + match http_client .request::>(guild_commands_request) .await { @@ -182,18 +181,16 @@ impl DiscordBotClient { if commands_by_name.1.len() == 1 { let command = commands_by_name.1.remove(0); - match self - .register_application_command(application_id, &mut discord_commands, &command.1) - .await + match Self::register_application_command( + http_client.clone(), + application_id, + &mut discord_commands, + &command.1, + ) + .await { Ok(command_id) => { - self.plugin_registrations - .write() - .await - .discord_events - .interaction_create - .application_commands - .insert(command_id, command.0); + // TODO: return value } Err(()) => { error!( @@ -208,22 +205,16 @@ impl DiscordBotClient { for mut command in commands_by_name.1 { command.1.name += format!("~{command_name_occurence_count}").as_str(); - match self - .register_application_command( - application_id, - &mut discord_commands, - &command.1, - ) - .await + match Self::register_application_command( + http_client.clone(), + application_id, + &mut discord_commands, + &command.1, + ) + .await { Ok(command_id) => { - self.plugin_registrations - .write() - .await - .discord_events - .interaction_create - .application_commands - .insert(command_id, command.0); + // TODO: return value } Err(()) => { error!( @@ -238,14 +229,14 @@ impl DiscordBotClient { } } - self.delete_old_application_commands(application_id, &discord_commands) + Self::delete_old_application_commands(http_client, application_id, &discord_commands) .await?; Ok(()) } async fn register_application_command( - &self, + http_client: Arc, application_id: Id, discord_commands: &mut HashMap, command: &Command, @@ -304,7 +295,7 @@ impl DiscordBotClient { } }; - match self.http_client.request::(request).await { + match http_client.request::(request).await { Ok(response) => match response.model().await { Ok(command) => Ok(command.id.unwrap()), Err(err) => { @@ -326,7 +317,7 @@ impl DiscordBotClient { } async fn delete_old_application_commands( - &self, + http_client: Arc, application_id: Id, discord_commands: &HashMap, ) -> Result<(), ()> { @@ -358,7 +349,7 @@ impl DiscordBotClient { "Deleting the {} command, guild id: {:?}", &discord_command.name, &discord_command.guild_id ); - match self.http_client.request::<()>(request).await { + match http_client.request::<()>(request).await { Ok(_) => (), Err(err) => { error!( diff --git a/src/discord/requests.rs b/src/discord/requests.rs index 7c229e3..f8b398a 100644 --- a/src/discord/requests.rs +++ b/src/discord/requests.rs @@ -1,52 +1,47 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use twilight_http::{request::Request, routing::Route}; -use twilight_model::{ - gateway::{ - OpCode, - payload::outgoing::{ - RequestGuildMembers, UpdatePresence, UpdateVoiceState, - request_guild_members::RequestGuildMembersInfo, update_presence::UpdatePresencePayload, - update_voice_state::UpdateVoiceStateInfo, - }, +use std::sync::Arc; + +use anyhow::{Result, anyhow, bail}; +use twilight_gateway::MessageSender; +use twilight_http::{Client, request::Request, routing::Route}; +use twilight_model::gateway::{ + OpCode, + payload::outgoing::{ + RequestGuildMembers, UpdatePresence, UpdateVoiceState, + request_guild_members::RequestGuildMembersInfo, update_presence::UpdatePresencePayload, + update_voice_state::UpdateVoiceStateInfo, }, - id::Id, }; use crate::{ discord::DiscordBotClient, plugins::discord_bot::plugin::{ - discord_types::Contents, - host_functions::{DiscordRequests, DiscordResponses}, + discord_import_functions::{DiscordRequests, DiscordResponses}, + discord_import_types::Body, }, }; impl DiscordBotClient { #[allow(clippy::too_many_lines)] pub async fn request( - &self, + http_client: Arc, + shard_message_senders: Arc>, request: DiscordRequests, - ) -> Result, String> { + ) -> Result> { let request = match request { // Shard message sender commands DiscordRequests::RequestGuildMembers((guild_id, body)) => { - let guild_id = Id::new(guild_id); - - let guild_shard_message_sender = if let Some(guild_shard_message_sender) = - self.shard_message_senders.read().await.get(&guild_id) - { - guild_shard_message_sender.clone() - } else { - return Err(String::from("No guild found")); - }; + let guild_shard_message_sender = + Self::get_guild_shard_id(&shard_message_senders, guild_id); let d = match sonic_rs::from_slice::(&body) { Ok(d) => d, Err(err) => { - return Err(format!( + bail!( "Something went wrong while deserializing RequestGuildMembersInfo, error: {err}", - )); + ); } }; @@ -60,27 +55,18 @@ impl DiscordBotClient { None } DiscordRequests::RequestSoundboardSounds(_guild_ids) => { - return Err(String::from( - "RequestSoundboardSounds has not yet been implemented in Twilight.", - )); + bail!("RequestSoundboardSounds has not yet been implemented in Twilight.",); } DiscordRequests::UpdateVoiceState((guild_id, body)) => { - let guild_id = Id::new(guild_id); - - let guild_shard_message_sender = if let Some(guild_shard_message_sender) = - self.shard_message_senders.read().await.get(&guild_id) - { - guild_shard_message_sender.clone() - } else { - return Err(String::from("No guild found")); - }; + let guild_shard_message_sender = + Self::get_guild_shard_id(&shard_message_senders, guild_id); let d = match sonic_rs::from_slice::(&body) { Ok(d) => d, Err(err) => { - return Err(format!( + bail!( "Something went wrong while deserializing RequestGuildMembersInfo, error: {err}", - )); + ); } }; @@ -94,20 +80,14 @@ impl DiscordBotClient { None } DiscordRequests::UpdatePresence(body) => { - let guild_shard_message_sender = if let Some(guild_shard_message_sender) = - self.shard_message_senders.read().await.values().next() - { - guild_shard_message_sender.clone() - } else { - return Err(String::from("No guild found")); - }; + let guild_shard_message_sender = shard_message_senders.get(0).unwrap(); let d = match sonic_rs::from_slice::(&body) { Ok(d) => d, Err(err) => { - return Err(format!( + bail!( "Something went wrong while deserializing RequestGuildMembersInfo, error: {err}", - )); + ); } }; @@ -131,9 +111,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -144,9 +124,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -154,11 +134,11 @@ impl DiscordBotClient { let request_builder = Request::builder(&Route::CreateForumThread { channel_id }); let request_builder = match content { - Contents::Json(bytes) => request_builder.body(bytes), - Contents::Form(buffer) => match request_builder.multipart(buffer) { + Body::Json(bytes) => request_builder.body(bytes), + Body::Form(buffer) => match request_builder.multipart(buffer) { Ok(request) => request, Err(err) => { - return Err(err.to_string()); + bail!(err); } }, }; @@ -166,9 +146,9 @@ impl DiscordBotClient { match request_builder.build() { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -176,11 +156,11 @@ impl DiscordBotClient { let request_builder = Request::builder(&Route::CreateMessage { channel_id }); let request_builder = match content { - Contents::Json(bytes) => request_builder.body(bytes), - Contents::Form(buffer) => match request_builder.multipart(buffer) { + Body::Json(bytes) => request_builder.body(bytes), + Body::Form(buffer) => match request_builder.multipart(buffer) { Ok(request) => request, Err(err) => { - return Err(err.to_string()); + bail!(err); } }, }; @@ -188,9 +168,9 @@ impl DiscordBotClient { match request_builder.build() { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -201,9 +181,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -217,9 +197,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -232,9 +212,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -242,9 +222,9 @@ impl DiscordBotClient { match Request::builder(&Route::GetActiveThreads { guild_id }).build() { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -252,9 +232,9 @@ impl DiscordBotClient { match Request::builder(&Route::GetChannel { channel_id }).build() { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -268,9 +248,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -284,9 +264,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -300,9 +280,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -315,9 +295,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -332,9 +312,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -354,9 +334,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -364,9 +344,9 @@ impl DiscordBotClient { match Request::builder(&Route::JoinThread { channel_id }).build() { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -374,9 +354,9 @@ impl DiscordBotClient { match Request::builder(&Route::LeaveThread { channel_id }).build() { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -389,9 +369,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -402,9 +382,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } @@ -422,18 +402,18 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - return Err(format!( + bail!( "Something went wrong while building a Discord request, error: {err}" - )); + ); } } } }; if let Some(request) = request { - match self.http_client.request::>(request).await { + match http_client.request::>(request).await { Ok(response) => Ok(Some(response.bytes().await.unwrap().clone())), - Err(err) => Err(format!( + Err(err) => Err(anyhow!( "Something went wrong while making a Discord request, error: {err}" )), } @@ -441,4 +421,13 @@ impl DiscordBotClient { Ok(None) } } + + fn get_guild_shard_id( + shard_message_senders: &Arc>, + guild_id: u64, + ) -> &MessageSender { + shard_message_senders + .get((guild_id >> 22) as usize % shard_message_senders.len()) + .unwrap() + } } diff --git a/src/http/registry.rs b/src/http/registry.rs index 41583a4..1bf826d 100644 --- a/src/http/registry.rs +++ b/src/http/registry.rs @@ -3,7 +3,7 @@ use std::str::FromStr; -use anyhow::{Context, Error, Result}; +use anyhow::{Context, Result, bail}; use reqwest::StatusCode; use tracing::debug; use url::{ParseError, Url}; @@ -19,10 +19,10 @@ impl HttpClient { let response = self.client.get(url).send().await?; if response.status() != StatusCode::OK { - return Err(Error::msg(format!( + bail!( "The response was undesired, status code: {}", response.status() - ))); + ); } Ok(response diff --git a/src/job_scheduler.rs b/src/job_scheduler.rs index 25e1581..efa3920 100644 --- a/src/job_scheduler.rs +++ b/src/job_scheduler.rs @@ -1,147 +1,95 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use std::sync::Arc; - +use anyhow::Result; use tokio::{ - sync::{ - Mutex, RwLock, - mpsc::{Receiver, Sender}, - }, + sync::mpsc::{UnboundedReceiver, UnboundedSender}, task::JoinHandle, }; -use tokio_cron_scheduler::{Job, JobScheduler as TCScheduler}; -use tracing::{error, info}; +use tokio_cron_scheduler::{Job, JobScheduler as TokioCronScheduler}; +use tracing::info; +use uuid::Uuid; -use crate::{ - plugins::{PluginRegistrationRequestsScheduledJob, PluginRegistrations}, - utils::channels::{JobSchedulerMessages, RuntimeMessages}, +use crate::utils::channels::{ + CoreMessages, JobSchedulerMessages, RuntimeMessages, RuntimeMessagesJobScheduler, }; pub struct JobScheduler { - tokio_cron_scheduler: Arc>, - plugin_registrations: Arc>, - runtime_tx: Arc>, - runtime_rx: Arc>>, + tokio_cron_scheduler: TokioCronScheduler, + core_tx: UnboundedSender, + rx: UnboundedReceiver, } impl JobScheduler { pub async fn new( - plugin_registrations: Arc>, - runtime_tx: Sender, - runtime_rx: Receiver, - ) -> Result { - match TCScheduler::new().await { - Ok(job_scheduler) => Ok(JobScheduler { - tokio_cron_scheduler: Arc::new(RwLock::new(job_scheduler)), - plugin_registrations, - runtime_tx: Arc::new(runtime_tx), - runtime_rx: Arc::new(Mutex::new(runtime_rx)), - }), - Err(err) => { - error!( - "Something went wrong while creating a new instance of the job scheduler, error {}", - &err - ); - Err(()) - } - } - } + core_tx: UnboundedSender, + rx: UnboundedReceiver, + ) -> Result { + info!("Creating the job scheduler"); - pub async fn scheduled_job_registrations( - &self, - initialized_plugin_registrations_scheduled_jobs: Vec< - PluginRegistrationRequestsScheduledJob, - >, - ) { - for scheduled_job in &initialized_plugin_registrations_scheduled_jobs { - for cron in &scheduled_job.crons { - info!( - "Scheduled Job {} from the {} plugin requested to be registered.", - &scheduled_job.id, &scheduled_job.plugin_id - ); - - let runtime_tx = self.runtime_tx.clone(); - let plugin_id = scheduled_job.plugin_id.clone(); - let internal_id = scheduled_job.id.clone(); - - let job = match Job::new_async_tz( - cron.clone(), - chrono::Local, - move |_uuid, _lock| { - let runtime_tx = runtime_tx.clone(); - let plugin_id = plugin_id.clone(); - let internal_id = internal_id.clone(); - - Box::pin(async move { - let _ = runtime_tx - .send(RuntimeMessages::CallScheduledJob(plugin_id, internal_id)) - .await; - }) - }, - ) { - Ok(job) => job, - Err(err) => { - error!( - "Something went wrong while adding {} job from the {} plugin to the job scheduler, error: {}", - &scheduled_job.id, &scheduled_job.plugin_id, &err - ); - continue; - } - }; - - match self.tokio_cron_scheduler.read().await.add(job).await { - Ok(uuid) => { - self.plugin_registrations - .write() - .await - .scheduled_jobs - .insert( - uuid.as_u128(), - (scheduled_job.plugin_id.clone(), scheduled_job.id.clone()), - ); - } - Err(err) => { - error!( - "Something went wrong while adding {} job from the {} plugin to the job scheduler, error: {}", - &scheduled_job.id, &scheduled_job.plugin_id, &err - ); - } - } - } - } + Ok(JobScheduler { + tokio_cron_scheduler: TokioCronScheduler::new().await?, + core_tx, + rx, + }) } - pub async fn start(self) -> Result, ()> { - if let Err(err) = self.tokio_cron_scheduler.read().await.start().await { - error!( - "Something went wrong while starting the job scheduler, error: {}", - &err - ); - return Err(()); - } - - let job_scheduler = Arc::new(self); + pub async fn start(mut self) -> Result> { + self.tokio_cron_scheduler.start().await?; Ok(tokio::spawn(async move { - while let Some(message) = job_scheduler.runtime_rx.lock().await.recv().await { + while let Some(message) = self.rx.recv().await { match message { - JobSchedulerMessages::RegisterScheduledJobs(scheduled_jobs) => { - job_scheduler - .scheduled_job_registrations(scheduled_jobs) - .await; + JobSchedulerMessages::AddJob(plugin_id, cron, result) => { + let tokio_cron_scheduler = self.tokio_cron_scheduler.clone(); + let core_tx = self.core_tx.clone(); + + tokio::spawn(async move { + result.send( + Self::add_job(tokio_cron_scheduler, core_tx, plugin_id, cron).await, + ); + }); } - JobSchedulerMessages::Shutdown(is_done) => { - let _ = job_scheduler - .tokio_cron_scheduler - .write() - .await - .shutdown() - .await; - let _ = is_done.send(()); + JobSchedulerMessages::RemoveJob(uuid, result) => { + let tokio_cron_scheduler = self.tokio_cron_scheduler.clone(); + + tokio::spawn(async move { + result.send(Self::remove_job(tokio_cron_scheduler, uuid).await); + }); } } } + + let _ = self.tokio_cron_scheduler.shutdown().await; })) } + + async fn add_job( + tokio_cron_scheduler: TokioCronScheduler, + core_tx: UnboundedSender, + plugin_id: Uuid, + cron: String, + ) -> Result { + info!( + "Scheduled Job at {cron} cron from the {plugin_id} plugin requested to be registered" + ); + + let job = Job::new_async_tz(cron.clone(), chrono::Local, move |job_id, _lock| { + let core_tx = core_tx.clone(); + + Box::pin(async move { + let _ = core_tx.send(CoreMessages::RuntimeModule(RuntimeMessages::JobScheduler( + RuntimeMessagesJobScheduler::CallScheduledJob(plugin_id, job_id), + ))); + }) + })?; + + Ok(tokio_cron_scheduler.add(job).await?) + } + + async fn remove_job(tokio_cron_scheduler: TokioCronScheduler, uuid: Uuid) -> Result<()> { + info!("Removing scheduled Job {uuid}"); + + Ok(tokio_cron_scheduler.remove(&uuid).await?) + } } diff --git a/src/main.rs b/src/main.rs index 0f31578..9aad980 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,21 +5,23 @@ use std::os::unix::process::CommandExt; use std::{ - collections::{HashMap, VecDeque}, + collections::VecDeque, env, ffi::OsString, - path::{Path, PathBuf}, + path::Path, process::{Command, ExitCode, exit}, - sync::{Arc, LazyLock}, + sync::LazyLock, }; use clap::Parser; -use tokio::{signal, sync::RwLock}; +use fjall::{Database, PersistMode}; +use tokio::{signal, sync::RwLock, task::JoinHandle}; use tracing::{error, info, warn}; use tracing_appender::non_blocking::WorkerGuard; mod cli; mod config; +mod database; mod discord; mod http; mod job_scheduler; @@ -29,13 +31,11 @@ mod utils; use cli::{Cli, CliLogParameters}; use config::Config; use discord::DiscordBotClient; -use http::HttpClient; use job_scheduler::JobScheduler; -use plugins::{ - AvailablePlugin, PluginRegistrations, builder::PluginBuilder, registry, runtime::Runtime, -}; +use plugins::{registry, runtime::Runtime}; +use utils::{channels::Channels, env::Secrets}; -use crate::utils::channels::Channels; +use crate::utils::channels::{ChannelsCore, CoreMessages}; #[derive(PartialEq)] enum Shutdown { @@ -65,14 +65,18 @@ async fn main() -> ExitCode { async fn run() -> Result<(), ()> { let cli = Cli::parse(); - //let mut tasks: Arc>>> = Arc::new(Mutex::new(vec![])); // TODO: Rework shutdown - let (_guard, discord_bot_client_token, channels) = - initialization(cli.log_parameters, &cli.env_file)?; + let mut tasks: Vec> = vec![]; + + let (_guard, secrets, channels) = initialization(cli.log_parameters, &cli.env_file)?; let config = Config::new(&cli.config_file)?; - let available_plugins = registry_get_plugins( + let database = database::new(&cli.database_directory).map_err(|_| ())?; + + tasks.push(start(database, channels.core)); + + let available_plugins = registry::registry_get_plugins( cli.http_client_timeout_seconds, config, cli.plugin_directory.clone(), @@ -80,125 +84,111 @@ async fn run() -> Result<(), ()> { ) .await?; - let plugin_registrations = Arc::new(RwLock::new(PluginRegistrations::new())); - - let (discord_bot_client, shards) = DiscordBotClient::new( - discord_bot_client_token, - plugin_registrations.clone(), - channels.runtime.discord_bot_client_sender, - channels.discord_bot_client.receiver, + let discord_bot_client = DiscordBotClient::new( + secrets.discord_bot_client, + channels.discord_bot_client.core_tx, + channels.discord_bot_client.rx, ) .await?; - info!("Creating the job scheduler"); - let job_scheduler = JobScheduler::new( - plugin_registrations.clone(), - channels.runtime.job_scheduler_sender, - channels.job_scheduler.receiver, - ) - .await?; + let job_scheduler = + JobScheduler::new(channels.job_scheduler.core_tx, channels.job_scheduler.rx) + .await + .map_err(|_| ())?; - info!("Creating the WASI runtime"); - let runtime = Arc::new(Runtime::new( - channels.discord_bot_client.sender, - channels.job_scheduler.sender, - channels.runtime.receiver, - )); + let runtime = Runtime::new(channels.runtime.rx); - discord_bot_client.start(shards); + tasks.push(job_scheduler.start().await.map_err(|_| ())?); - job_scheduler.start().await?; + tasks.push(discord_bot_client.start()); - plugin_initializations( - runtime.clone(), - available_plugins, - plugin_registrations, - &cli.plugin_directory, - ) - .await?; + runtime + .initialize_plugins( + available_plugins, + channels.runtime.core_tx, + &cli.plugin_directory, + ) + .await?; - Runtime::start(runtime.clone()); + tasks.push(runtime.start()); - shutdown(runtime).await + shutdown(tasks).await } fn initialization( cli_log_parameters: CliLogParameters, env_file: &Path, -) -> Result<(Option, String, Channels), ()> { +) -> Result<(Option, Secrets, Channels), ()> { let guard = utils::logger::new(cli_log_parameters)?; - utils::env::load_env_file(env_file)?; + utils::env::load_env_file(env_file).map_err(|_| ())?; - let discord_bot_client_token = utils::env::validate()?; + let secrets = utils::env::get_secrets().map_err(|_| ())?; let channels = utils::channels::new(); - Ok((guard, discord_bot_client_token, channels)) + Ok((guard, secrets, channels)) } -async fn registry_get_plugins( - http_client_timeout_seconds: u64, - config: Config, - plugin_directory: PathBuf, - cache: bool, -) -> Result, ()> { - let http_client = Arc::new(HttpClient::new(http_client_timeout_seconds)?); - - registry::get_plugins(http_client, config, plugin_directory, cache).await -} +fn start(database: Database, mut channels_core: ChannelsCore) -> JoinHandle<()> { + tokio::spawn(async move { + while let Some(core_message) = channels_core.rx.recv().await { + match core_message { + CoreMessages::DatabaseModule(database_message) => { + let database = database.clone(); + + tokio::spawn(async { + database::handle_action(database, database_message); + }); + } + CoreMessages::JobSchedulerModule(job_scheduler_message) => { + channels_core.job_scheduler_tx.send(job_scheduler_message); + } + CoreMessages::DiscordBotClientModule(discord_bot_client_message) => { + channels_core + .discord_bot_client_tx + .send(discord_bot_client_message); + } + CoreMessages::RuntimeModule(runtime_message) => { + channels_core.runtime_tx.send(runtime_message); + } + CoreMessages::Shutdown(shutdown) => todo!(), // TODO: Figure shutdown out + } + } -async fn plugin_initializations( - runtime: Arc, - available_plugins: HashMap, - plugin_registrations: Arc>, - config_directory: &Path, -) -> Result<(), ()> { - info!("Creating the WASI plugin builder"); - let plugin_builder = PluginBuilder::new(); - - info!("Initializing the plugins"); - Runtime::initialize_plugins( - runtime, - plugin_builder, - available_plugins, - plugin_registrations, - config_directory, - ) - .await + database::persist(database, PersistMode::SyncAll); + }) } -async fn shutdown(runtime: Arc) -> Result<(), ()> { - let cancellation_token = runtime.cancellation_token.clone(); - - tokio::select! { - result = async move { - if let Err(err) = signal::ctrl_c().await { - error!( - "Failed to listen for the terminal interrupt signal, error: {}", - &err - ); - return Err(()); - } - - info!("Terminal interrupt signal received, send another to force immediate shutdown"); +async fn shutdown(mut tasks: Vec>) -> Result<(), ()> { + tokio::spawn(async { + if let Err(err) = signal::ctrl_c().await { + error!( + "Failed to listen for the terminal interrupt signal, error: {}", + &err + ); + return Err(()); + } - tokio::spawn(async { - signal::ctrl_c() - .await - .expect("failed to listen for the terminal interrupt signal"); + info!("Terminal interrupt signal received, send another to force immediate shutdown"); - warn!("Second terminal interrupt signal received, forcing immediate shutdown"); - exit(130); - }); + tokio::spawn(async { + signal::ctrl_c() + .await + .expect("failed to listen for the terminal interrupt signal"); - runtime.shutdown(Shutdown::SigInt).await; + warn!("Second terminal interrupt signal received, forcing immediate shutdown"); + exit(130); + }); - Ok(()) + Ok(()) + }); - } => {result} - () = cancellation_token.cancelled() => {Ok(())} + for task in tasks.drain(..) { + task.await; } + + Ok(()) } fn restart() { diff --git a/src/plugins.rs b/src/plugins.rs index dc7becd..79ca671 100644 --- a/src/plugins.rs +++ b/src/plugins.rs @@ -2,95 +2,36 @@ /* Copyright © 2026 Eduard Smet */ pub mod builder; +pub mod permissions; pub mod registry; pub mod runtime; use std::collections::{HashMap, HashSet}; use semver::Version; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; use serde_yaml_ng::Value; use twilight_model::id::{Id, marker::CommandMarker}; -use crate::plugins::discord_bot::plugin::plugin_types::SupportedRegistrations; +use crate::plugins::permissions::ConfigPluginPermissions; wasmtime::component::bindgen!({ imports: { default: async }, exports: { default: async } }); -#[derive(Clone, Deserialize)] +#[derive(Deserialize)] pub struct ConfigPlugin { pub plugin: String, pub cache: Option, - #[serde(default = "ConfigPlugin::permissions_default")] - pub permissions: SupportedRegistrations, + pub permissions: ConfigPluginPermissions, pub environment: Option>, pub settings: Option, } -impl ConfigPlugin { - fn permissions_default() -> SupportedRegistrations { - let mut supported_registrations = SupportedRegistrations::all(); - - supported_registrations &= !SupportedRegistrations::SHUTDOWN; - - supported_registrations - } -} - -impl<'de> Deserialize<'de> for SupportedRegistrations { - fn deserialize>(deserializer: D) -> Result { - let mut supported_registrations = SupportedRegistrations::empty(); - - let supported_registration_strings = Vec::::deserialize(deserializer)?; - - for supported_registration_string in supported_registration_strings { - match supported_registration_string.to_uppercase().as_str() { - "DEPENDENCY_FUNCTIONS" => { - supported_registrations |= SupportedRegistrations::DEPENDENCY_FUNCTIONS; - } - "DISCORD_EVENT_MESSAGE_CREATE" => { - supported_registrations |= SupportedRegistrations::DISCORD_EVENT_MESSAGE_CREATE; - } - "DISCORD_EVENT_INTERACTION_CREATE" => { - supported_registrations |= - SupportedRegistrations::DISCORD_EVENT_INTERACTION_CREATE; - } - "DISCORD_EVENT_THREAD_CREATE" => { - supported_registrations |= SupportedRegistrations::DISCORD_EVENT_THREAD_CREATE; - } - "DISCORD_EVENT_THREAD_DELETE" => { - supported_registrations |= SupportedRegistrations::DISCORD_EVENT_THREAD_DELETE; - } - "DISCORD_EVENT_THREAD_LIST_SYNC" => { - supported_registrations |= - SupportedRegistrations::DISCORD_EVENT_THREAD_LIST_SYNC; - } - "DISCORD_EVENT_THREAD_MEMBER_UPDATE" => { - supported_registrations |= - SupportedRegistrations::DISCORD_EVENT_THREAD_MEMBER_UPDATE; - } - "DISCORD_EVENT_THREAD_MEMBERS_UPDATE" => { - supported_registrations |= - SupportedRegistrations::DISCORD_EVENT_THREAD_MEMBERS_UPDATE; - } - "DISCORD_EVENT_THREAD_UPDATE" => { - supported_registrations |= SupportedRegistrations::DISCORD_EVENT_THREAD_UPDATE; - } - "SHUTDOWN" => { - supported_registrations |= SupportedRegistrations::SHUTDOWN; - } - &_ => unimplemented!(), - } - } - - Ok(supported_registrations) - } -} - pub struct AvailablePlugin { pub registry_id: String, pub id: String, + pub user_id: String, pub version: Version, - pub permissions: SupportedRegistrations, + pub permissions: ConfigPluginPermissions, pub environment: Option>, pub settings: Option, } diff --git a/src/plugins/permissions.rs b/src/plugins/permissions.rs new file mode 100644 index 0000000..5b5a195 --- /dev/null +++ b/src/plugins/permissions.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; + +use crate::plugins::discord_bot::plugin::core_import_types::SupportedCoreRegistrations; + +#[derive(Deserialize, Serialize)] +pub struct ConfigPluginPermissions { + pub core: Vec, + pub job_scheduler: Vec, + pub discord: Vec, +} + +#[derive(Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ConfigSupportedCoreRegistrations { + DependencyFunctions, + Shutdown, +} + +#[derive(Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ConfigSupportedJobSchedulerRegistrations { + ScheduledJobs, +} + +#[derive(Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ConfigSupportedDiscordRegistrations { + MessageCreate, + InteractionCreate, + ThreadCreate, + ThreadDelete, + ThreadListSync, + ThreadMemberUpdate, + ThreadMembersUpdate, + ThreadUpdate, +} + +impl From> for SupportedCoreRegistrations { + fn from(config_supported_core_registrations: Vec) -> Self { + let mut supported_core_registrations = Self::empty(); + + for registration in &config_supported_core_registrations { + match registration { + ConfigSupportedCoreRegistrations::DependencyFunctions => { + supported_core_registrations &= SupportedCoreRegistrations::DEPENDENCY_FUNCTIONS + } + ConfigSupportedCoreRegistrations::Shutdown => { + supported_core_registrations &= SupportedCoreRegistrations::SHUTDOWN + } + } + } + + supported_core_registrations + } +} diff --git a/src/plugins/registry.rs b/src/plugins/registry.rs index b8fdf8d..b6e0860 100644 --- a/src/plugins/registry.rs +++ b/src/plugins/registry.rs @@ -8,11 +8,12 @@ use std::{ sync::{Arc, LazyLock}, }; -use anyhow::{Error, Result}; +use anyhow::{Result, bail}; use semver::{Version, VersionReq}; use serde::Deserialize; use tokio::fs; use tracing::{error, info, warn}; +use uuid::Uuid; use crate::{ config::Config, @@ -46,7 +47,7 @@ pub struct RegistryPluginVersion { pub deprecation_reason: Option, } -type RegistryTask = Vec>>>; +type RegistryTask = Vec>>>; static DEFAULT_REGISTRY_ID: &str = "raw.githubusercontent.com/celarye/discord-bot-plugins/refs/heads/master"; @@ -54,15 +55,26 @@ static DEFAULT_REGISTRY_ID: &str = static PROGRAM_VERSION: LazyLock = LazyLock::new(|| Version::parse(env!("CARGO_PKG_VERSION")).unwrap()); +pub async fn registry_get_plugins( + http_client_timeout_seconds: u64, + config: Config, + plugin_directory: PathBuf, + cache: bool, +) -> Result, ()> { + let http_client = Arc::new(HttpClient::new(http_client_timeout_seconds)?); + + get_plugins(http_client, config, plugin_directory, cache).await +} + pub async fn get_plugins( http_client: Arc, config: Config, base_plugin_directory_path: PathBuf, cache: bool, -) -> Result, ()> { +) -> Result, ()> { info!("Fetching and storing the plugins"); - let mut available_plugins = HashMap::new(); + let mut available_plugins = vec![]; let registries = get_cached_plugins( &base_plugin_directory_path, @@ -92,7 +104,7 @@ async fn get_cached_plugins( base_plugin_directory_path: &Path, config: Config, cache: bool, - available_plugins: &mut HashMap, + available_plugins: &mut Vec<(Uuid, AvailablePlugin)>, ) -> HashMap> { let mut registries = HashMap::new(); @@ -112,17 +124,18 @@ async fn get_cached_plugins( { Ok(cache_check) => { if let Some(plugin_version) = cache_check { - available_plugins.insert( - plugin_uid, + available_plugins.push(( + Uuid::new_v4(), AvailablePlugin { registry_id: registry_id.to_string(), id: plugin_id.to_string(), + user_id: plugin_uid, version: plugin_version, permissions: plugin_options.permissions, environment: plugin_options.environment, settings: plugin_options.settings, }, - ); + )); continue; } @@ -146,7 +159,7 @@ async fn fetch_non_cached_plugins( http_client: Arc, base_plugin_directory_path: &Path, registries: HashMap>, - available_plugins: &mut HashMap, + available_plugins: &mut Vec<(Uuid, AvailablePlugin)>, ) { let mut registry_tasks: RegistryTask = vec![]; @@ -175,8 +188,8 @@ async fn fetch_non_cached_plugins( let mut plugin_directory_path = registry_directory_path.join(plugin_id); let Some(registry_plugin) = registry.plugins.get(plugin_id) else { - return Err(Error::msg(format!("The {registry_id} registry has no {plugin_id} plugin entry", - ))); + bail!("The {registry_id} registry has no {plugin_id} plugin entry", + ); }; let Some(plugin_version) = get_plugin_matching_version( @@ -184,8 +197,8 @@ async fn fetch_non_cached_plugins( ®istry_plugin.versions, )? else { - return Err(Error::msg(format!( - "The {plugin_uid} plugin has no version which isn't marked as deprecated and is compatible with this version of the program"))); + bail!( + "The {plugin_uid} plugin has no version which isn't marked as deprecated and is compatible with this version of the program"); }; plugin_directory_path.push(plugin_version.to_string()); @@ -202,10 +215,11 @@ async fn fetch_non_cached_plugins( .await?; Ok(( - plugin_uid, + Uuid::new_v4(), AvailablePlugin { registry_id: registry_id.to_string(), id: plugin_id.to_string(), + user_id: plugin_uid, version: plugin_version, permissions: plugin_options.permissions, environment: plugin_options.environment, @@ -230,8 +244,7 @@ async fn fetch_non_cached_plugins( match registry_task.await.unwrap() { Ok(available_registry_plugins) => { for available_registry_plugin in available_registry_plugins { - available_plugins - .insert(available_registry_plugin.0, available_registry_plugin.1); + available_plugins.push(available_registry_plugin); } } Err(err) => { @@ -301,7 +314,7 @@ async fn get_plugin_latest_cached_version(plugin_path: &Path) -> Result(®istry_metadata_bytes).map_err(Error::new) + Ok(sonic_rs::from_slice::(®istry_metadata_bytes)?) } async fn fetch_plugin( diff --git a/src/plugins/runtime.rs b/src/plugins/runtime.rs index 45a6459..912041c 100644 --- a/src/plugins/runtime.rs +++ b/src/plugins/runtime.rs @@ -3,106 +3,90 @@ pub mod internal; -use std::{ - collections::{HashMap, HashSet}, - fs, - path::Path, - sync::Arc, -}; +use std::{collections::HashMap, fs, path::Path}; +use anyhow::Result; use serde_yaml_ng::Value; -use tokio::sync::{ - Mutex, RwLock, - mpsc::{Receiver, Sender}, - oneshot, +use tokio::{ + sync::{ + Mutex, RwLock, + mpsc::{UnboundedReceiver, UnboundedSender}, + }, + task::JoinHandle, }; -use tokio_util::sync::CancellationToken; use tracing::{error, info}; +use uuid::Uuid; use wasmtime::{Store, component::Component}; use wasmtime_wasi::{DirPerms, FilePerms, ResourceTable, WasiCtxBuilder}; use wasmtime_wasi_http::WasiHttpCtx; use crate::{ - SHUTDOWN, Shutdown, plugins::{ - AvailablePlugin, Plugin, PluginRegistrationRequests, - PluginRegistrationRequestsApplicationCommand, PluginRegistrationRequestsScheduledJob, - PluginRegistrations, builder::PluginBuilder, - discord_bot::plugin::discord_types::Events as DiscordEvents, + AvailablePlugin, Plugin, builder::PluginBuilder, + discord_bot::plugin::discord_export_types::DiscordEvents, runtime::internal::InternalRuntime, }, - utils::channels::{DiscordBotClientMessages, JobSchedulerMessages, RuntimeMessages}, + utils::channels::{ + CoreMessages, RuntimeMessages, RuntimeMessagesDiscord, RuntimeMessagesJobScheduler, + }, }; pub struct Runtime { - plugins: RwLock>, - discord_bot_client_tx: Arc>, - job_scheduler_tx: Arc>, - dbc_js_rx: RwLock>, - pub cancellation_token: CancellationToken, + plugins: RwLock>, + rx: UnboundedReceiver, } pub struct RuntimePlugin { instance: Plugin, - store: Mutex>, // TODO: Add async support, waiting for better WASIp3 component creation support + store: Mutex>, // TODO: Add async support } impl Runtime { - pub fn new( - discord_bot_client_tx: Sender, - job_scheduler_tx: Sender, - dbc_js_rx: Receiver, - ) -> Self { + pub fn new(rx: UnboundedReceiver) -> Self { + info!("Creating the WASI runtime"); + Runtime { plugins: RwLock::new(HashMap::new()), - discord_bot_client_tx: Arc::new(discord_bot_client_tx), - job_scheduler_tx: Arc::new(job_scheduler_tx), - dbc_js_rx: RwLock::new(dbc_js_rx), - cancellation_token: CancellationToken::new(), + rx, } } - pub fn start(runtime: Arc) { + pub fn start(mut self) -> JoinHandle<()> { tokio::spawn(async move { - let mut dbc_js_rx = runtime.dbc_js_rx.write().await; - - tokio::select! { - () = async { - while let Some(message) = dbc_js_rx.recv().await { - match message { - RuntimeMessages::CallDiscordEvent(plugin_name, event) => { - runtime.call_discord_event(&plugin_name, &event).await; + while let Some(message) = self.rx.recv().await { + match message { + RuntimeMessages::JobScheduler(job_scheduler_message) => { + match job_scheduler_message { + RuntimeMessagesJobScheduler::CallScheduledJob(plugin_id, job_id) => { + self.call_scheduled_job(plugin_id, job_id).await; } - RuntimeMessages::CallScheduledJob(plugin_name, scheduled_job_name) => { - runtime.call_scheduled_job(&plugin_name, &scheduled_job_name).await;} } } - } => {} - () = runtime.cancellation_token.cancelled() => { - dbc_js_rx.close(); + RuntimeMessages::Discord(discord_message) => match discord_message { + RuntimeMessagesDiscord::CallDiscordEvent(plugin_id, event) => { + self.call_discord_event(plugin_id, &event).await; + } + }, } } - }); + + self.shutdown().await; + }) } pub async fn initialize_plugins( - runtime: Arc, - plugin_builder: PluginBuilder, - plugins: HashMap, - plugin_registrations: Arc>, - directory: &Path, + &self, + available_plugins: Vec<(Uuid, AvailablePlugin)>, + core_tx: UnboundedSender, + plugin_directory: &Path, ) -> Result<(), ()> { - let mut registration_requests = PluginRegistrationRequests { - discord_event_interaction_create: super::PluginRegistrationRequestsInteractionCreate { - application_commands: vec![], - message_component: vec![], - modals: vec![], - }, - scheduled_jobs: vec![], - }; - - for (plugin_uid, plugin) in plugins { - let plugin_directory = directory + info!("Creating the WASI plugin builder"); + let plugin_builder = PluginBuilder::new(); + + info!("Initializing the plugins"); + + for (plugin_id, plugin) in available_plugins { + let plugin_directory = plugin_directory .join(&plugin.registry_id) .join(&plugin.id) .join(plugin.version.to_string()); @@ -112,7 +96,7 @@ impl Runtime { Err(err) => { error!( "An error occured while reading the {} plugin file: {err}", - plugin_uid + plugin.user_id ); continue; } @@ -123,7 +107,7 @@ impl Runtime { Err(err) => { error!( "An error occured while creating a WASI component from the {} plugin: {err}", - plugin_uid + plugin.user_id ); continue; } @@ -142,15 +126,15 @@ impl Runtime { Ok(exists) => { if !exists && let Err(err) = fs::create_dir(&workspace_plugin_dir) { error!( - "Something went wrong while creating the workspace directory for the {} plugin, error: {}", - &plugin_uid, &err + "Something went wrong while creating the workspace directory for the {} plugin, error: {err}", + plugin.user_id ); } } Err(err) => { error!( - "Something went wrong while checking if the workspace directory of the {} plugin exists, error: {}", - &plugin_uid, &err + "Something went wrong while checking if the workspace directory of the {} plugin exists, error: {err}", + plugin.user_id ); return Err(()); } @@ -165,11 +149,11 @@ impl Runtime { let mut store = Store::::new( &plugin_builder.engine, InternalRuntime::new( - plugin_uid.clone(), + plugin_id, wasi, WasiHttpCtx::new(), ResourceTable::new(), - Arc::downgrade(&runtime), + core_tx.clone(), ), ); @@ -180,36 +164,34 @@ impl Runtime { Ok(instance) => instance, Err(err) => { error!( - "Failed to instantiate the {} plugin, error: {}", - &plugin_uid, &err + "Failed to instantiate the {} plugin, error: {err}", + plugin.user_id ); continue; } }; - let plugin_registrations_request = match instance - .discord_bot_plugin_plugin_functions() + match instance + .discord_bot_plugin_core_export_functions() .call_initialization( &mut store, &sonic_rs::to_vec(&plugin.settings.unwrap_or(Value::default())).unwrap(), - plugin.permissions, ) .await { - Ok(init_result) => match init_result { - Ok(registrations_request) => registrations_request, - Err(err) => { + Ok(init_result) => { + if let Err(err) = init_result { error!( - "Failed to initialize the {} plugin, error: {}", - &plugin_uid, &err + "the {} plugin returned an error while intiializing: {err}", + plugin.user_id ); continue; } - }, + } Err(err) => { error!( - "The {} plugin exprienced a critical error: {}", - &plugin_uid, &err + "The {} plugin exprienced a critical error: {err}", + plugin.user_id ); continue; } @@ -220,268 +202,80 @@ impl Runtime { store: Mutex::new(store), }; - if let Some(discord_events) = plugin_registrations_request.discord_events { - if discord_events.message_create { - plugin_registrations - .write() - .await - .discord_events - .message_create - .push(plugin_uid.clone()); - } - - if discord_events.thread_create { - plugin_registrations - .write() - .await - .discord_events - .thread_create - .push(plugin_uid.clone()); - } - - if discord_events.thread_delete { - plugin_registrations - .write() - .await - .discord_events - .thread_delete - .push(plugin_uid.clone()); - } - - if discord_events.thread_list_sync { - plugin_registrations - .write() - .await - .discord_events - .thread_list_sync - .push(plugin_uid.clone()); - } - - if discord_events.thread_member_update { - plugin_registrations - .write() - .await - .discord_events - .thread_member_update - .push(plugin_uid.clone()); - } - - if discord_events.thread_members_update { - plugin_registrations - .write() - .await - .discord_events - .thread_members_update - .push(plugin_uid.clone()); - } - - if discord_events.thread_update { - plugin_registrations - .write() - .await - .discord_events - .thread_update - .push(plugin_uid.clone()); - } - - if let Some(interaction_create) = discord_events.interaction_create { - if let Some(application_commands) = interaction_create.application_commands { - for application_command in application_commands { - registration_requests - .discord_event_interaction_create - .application_commands - .push(PluginRegistrationRequestsApplicationCommand { - plugin_id: plugin_uid.clone(), - data: application_command, - }); - } - } - - if let Some(message_components) = interaction_create.message_components { - // TODO: Prevent duplicate entries - - for message_component in message_components { - plugin_registrations - .write() - .await - .discord_events - .interaction_create - .message_components - .insert(message_component.clone(), plugin_uid.clone()); - } - } - - if let Some(modals) = interaction_create.modals { - // TODO: Prevent duplicate entries - - for modal in modals { - plugin_registrations - .write() - .await - .discord_events - .interaction_create - .modals - .insert(modal.clone(), plugin_uid.clone()); - } - } - } - } - - if let Some(scheduled_jobs) = plugin_registrations_request.scheduled_jobs { - for scheduled_job in scheduled_jobs { - registration_requests.scheduled_jobs.push( - PluginRegistrationRequestsScheduledJob { - plugin_id: plugin_uid.clone(), - id: scheduled_job.0, - crons: scheduled_job.1, - }, - ); - } - } - - if let Some(dependency_functions) = plugin_registrations_request.dependency_functions { - for dependency_function in dependency_functions { - let mut plugin_registrations = plugin_registrations.write().await; - let functions = plugin_registrations - .dependency_functions - .entry(plugin_uid.clone()) - .or_insert(HashSet::new()); - - functions.insert(dependency_function); - } - } - - runtime - .plugins - .write() - .await - .insert(plugin_uid, plugin_context); + self.plugins.write().await.insert(plugin_id, plugin_context); } - let _ = runtime - .discord_bot_client_tx - .send(DiscordBotClientMessages::RegisterApplicationCommands( - registration_requests - .discord_event_interaction_create - .application_commands, - )) - .await; - - let _ = runtime - .job_scheduler_tx - .send(JobSchedulerMessages::RegisterScheduledJobs( - registration_requests.scheduled_jobs, - )) - .await; - Ok(()) } // TODO: Remove trapped plugins - async fn call_discord_event(&self, plugin_name: &str, event: &DiscordEvents) { + async fn call_discord_event(&self, plugin_id: Uuid, event: &DiscordEvents) { let plugins = self.plugins.read().await; - let plugin = plugins.get(plugin_name).unwrap(); + let plugin = plugins.get(&plugin_id).unwrap(); match plugin .instance - .discord_bot_plugin_plugin_functions() + .discord_bot_plugin_discord_export_functions() .call_discord_event(&mut *plugin.store.lock().await, event) .await { Ok(result) => { if let Err(err) = result { - error!("The {} plugin returned an error: {}", plugin_name, &err); + error!("The {plugin_id} plugin returned an error: {err}"); } } Err(err) => { - error!( - "The {} plugin exprienced a critical error: {}", - plugin_name, &err - ); + error!("The {plugin_id} plugin exprienced a critical error: {err}"); } } } - async fn call_scheduled_job(&self, plugin_name: &str, scheduled_job_name: &str) { + async fn call_scheduled_job(&self, plugin_id: Uuid, uuid: Uuid) { let plugins = self.plugins.read().await; - let plugin = plugins.get(plugin_name).unwrap(); + let plugin = plugins.get(&plugin_id).unwrap(); match plugin .instance - .discord_bot_plugin_plugin_functions() - .call_scheduled_job(&mut *plugin.store.lock().await, scheduled_job_name) + .discord_bot_plugin_job_scheduler_export_functions() + .call_scheduled_job(&mut *plugin.store.lock().await, &uuid.to_string()) .await { Ok(result) => { if let Err(err) = result { - error!("The {} plugin returned an error: {}", plugin_name, &err); + error!("The {plugin_id} plugin returned an error: {err}"); } } Err(err) => { - error!( - "The {} plugin exprienced a critical error: {}", - plugin_name, &err - ); + error!("The {plugin_id} plugin exprienced a critical error: {err}"); } } } - async fn call_shutdown(&self, plugin_name: String) { + async fn call_shutdown(&self, plugin_id: Uuid) { let plugins = self.plugins.read().await; - let plugin = plugins.get(&plugin_name).unwrap(); + let plugin = plugins.get(&plugin_id).unwrap(); match plugin .instance - .discord_bot_plugin_plugin_functions() + .discord_bot_plugin_core_export_functions() .call_shutdown(&mut *plugin.store.lock().await) .await { Ok(result) => { if let Err(err) = result { - error!("The {} plugin returned an error: {}", plugin_name, &err); + error!("The {plugin_id} plugin returned an error: {err}"); } } Err(err) => { - error!( - "The {} plugin exprienced a critical error: {}", - plugin_name, &err - ); + error!("The {plugin_id} plugin exprienced a critical error: {err}"); } } } - pub async fn shutdown(&self, shutdown_type: Shutdown) { - if SHUTDOWN.read().await.is_some() { - // TODO: Do not wait for shutdown to complete, the main function shutdown logic needs to get reworked first - self.cancellation_token.cancelled().await; - return; - } - - *SHUTDOWN.write().await = Some(shutdown_type); - - let job_scheduler_is_done = oneshot::channel(); - let discord_bot_client_is_done = oneshot::channel(); - - info!("Shutting down the job scheduler"); - let _ = self - .job_scheduler_tx - .send(JobSchedulerMessages::Shutdown(job_scheduler_is_done.0)) - .await; - let _ = job_scheduler_is_done.1.await; - - info!("Shutting down the Discord bot client shards"); - let _ = self - .discord_bot_client_tx - .send(DiscordBotClientMessages::Shutdown( - discord_bot_client_is_done.0, - )) - .await; - - let _ = discord_bot_client_is_done.1.await; - - // TODO: Allow all plugin calls to finish, call the shutdown methods on them and only then return - - self.cancellation_token.cancel(); + async fn shutdown(&self) { + // TODO: Allow all plugin calls to finish and then call the shutdown methods + // This will be achieved by closing the plugin call channel tasks which then will call + // shutdown one more time before returning } } diff --git a/src/plugins/runtime/internal.rs b/src/plugins/runtime/internal.rs index 4aef3fb..a0e4dab 100644 --- a/src/plugins/runtime/internal.rs +++ b/src/plugins/runtime/internal.rs @@ -1,35 +1,23 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use std::sync::Weak; - -use tokio::sync::oneshot; -use tracing::{debug, error, info, trace, warn}; +use tokio::sync::mpsc::UnboundedSender; +use uuid::Uuid; use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxView, WasiView}; use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -use crate::{ - Shutdown, - plugins::{ - discord_bot::plugin::{ - discord_types::{ - Host as DiscordTypes, Requests as DiscordRequests, Responses as DiscordResponses, - }, - host_functions::Host as HostFunctions, - host_types::{Host as HostTypes, LogLevels}, - plugin_types::Host as PluginTypes, - }, - runtime::Runtime, - }, - utils::channels::DiscordBotClientMessages, -}; +mod core; +mod discord; +mod job_scheduler; + +use crate::utils::channels::CoreMessages; pub struct InternalRuntime { - uid: String, + plugin_id: Uuid, wasi: WasiCtx, wasi_http: WasiHttpCtx, table: ResourceTable, - runtime: Weak, + core_tx: UnboundedSender, } impl WasiView for InternalRuntime { @@ -51,118 +39,20 @@ impl WasiHttpView for InternalRuntime { } } -impl HostFunctions for InternalRuntime { - async fn log(&mut self, level: LogLevels, message: String) { - match level { - LogLevels::Trace => trace!(message), - LogLevels::Debug => debug!(message), - LogLevels::Info => info!(message), - LogLevels::Warn => warn!(message), - LogLevels::Error => error!(message), - } - } - - async fn discord_request( - &mut self, - request: DiscordRequests, - ) -> Result, String> { - let runtime = self.runtime.upgrade().unwrap(); - - let (tx, rx) = oneshot::channel(); - - if let Err(err) = runtime - .discord_bot_client_tx - .send(DiscordBotClientMessages::Request(request, tx)) - .await - { - let err = format!( - "Something went wrong while sending a message over the Discord channel, error: {err}" - ); - - error!(err); - - return Err(err); - } - - match rx.await { - Ok(result) => result, - Err(err) => { - let err = format!("The OneShot sender was dropped: {err}"); - error!(err); - Err(err) - } - } - } - - async fn dependency_function( - &mut self, - dependency: String, - function: String, - params: Vec, - ) -> Result, String> { - let runtime = self.runtime.upgrade().unwrap(); - - let plugins = runtime.plugins.read().await; - let plugin = plugins.get(&dependency).unwrap(); - - // TODO: Check if it is an actual dependency and prevent deadlocks, the channel rework should fix - // the potential deadlocks. - - match plugin - .instance - .discord_bot_plugin_plugin_functions() - .call_dependency_function(&mut *plugin.store.lock().await, &function, ¶ms) - .await - { - Ok(call_result) => match call_result { - Ok(dependency_result) => Ok(dependency_result), - Err(err) => { - let err = format!("The plugin returned an error: {err}"); - error!(err); - Err(err) - } - }, - Err(err) => { - let err = format!("Something went wrong while calling the plugin: {err}"); - error!(err); - Err(err) - } - } - } - - async fn shutdown(&mut self, restart: bool) { - let shutdown_type = if restart { - Shutdown::Restart - } else { - Shutdown::Normal - }; - - self.runtime - .upgrade() - .unwrap() - .shutdown(shutdown_type) - .await; - } -} - -impl HostTypes for InternalRuntime {} -impl PluginTypes for InternalRuntime {} -impl DiscordTypes for InternalRuntime {} - impl InternalRuntime { pub fn new( - uid: String, + plugin_id: Uuid, wasi: WasiCtx, wasi_http: WasiHttpCtx, table: ResourceTable, - runtime: Weak, + core_tx: UnboundedSender, ) -> Self { InternalRuntime { - uid, + plugin_id, wasi, wasi_http, table, - runtime, + core_tx, } } } diff --git a/src/plugins/runtime/internal/core.rs b/src/plugins/runtime/internal/core.rs new file mode 100644 index 0000000..b5c48c3 --- /dev/null +++ b/src/plugins/runtime/internal/core.rs @@ -0,0 +1,134 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2026 Eduard Smet */ + +use tokio::sync::oneshot::channel; +use tracing::{debug, error, info, trace, warn}; + +use crate::{ + Shutdown, + database::Keyspaces, + plugins::{ + discord_bot::plugin::{ + core_export_types::Host as CoreExportTypesHost, + core_import_functions::Host as CoreImportFunctionsHost, + core_import_types::{ + CoreRegistrations, CoreRegistrationsResult, Error, Host as CoreImportTypesHost, + LogLevels, SupportedCoreRegistrations, + }, + core_types::Host as CoreTypesHost, + }, + permissions::{ConfigPluginPermissions, ConfigSupportedCoreRegistrations}, + runtime::internal::InternalRuntime, + }, + utils::channels::{CoreMessages, DatabaseMessages}, +}; + +impl CoreTypesHost for InternalRuntime {} +impl CoreImportTypesHost for InternalRuntime {} +impl CoreExportTypesHost for InternalRuntime {} + +impl CoreImportFunctionsHost for InternalRuntime { + async fn get_supported_registrations(&mut self) -> SupportedCoreRegistrations { + let (sender, receiver) = channel(); + + self.core_tx + .send(CoreMessages::DatabaseModule(DatabaseMessages::GetState( + Keyspaces::Plugins, + self.plugin_id.as_bytes().to_vec(), + sender, + ))); + + let response_bytes = receiver.await.unwrap().unwrap().unwrap().to_vec(); + + let plugin_permissions = + sonic_rs::from_slice::(&response_bytes).unwrap(); + + plugin_permissions.core.into() + } + + async fn register(&mut self, registrations: CoreRegistrations) -> CoreRegistrationsResult { + let mut result = CoreRegistrationsResult { + dependency_functions: None, + }; + + if let Some(dependency_functions) = registrations.dependency_functions { + result.dependency_functions = Some((Vec::new(), Vec::new())); + + for dependency_function in dependency_functions { + let (sender, receiver) = channel(); + + let key = format!("{}-{dependency_function}", self.plugin_id.to_string()); + + self.core_tx + .send(CoreMessages::DatabaseModule(DatabaseMessages::InsertState( + Keyspaces::DependencyFunctions, + key.as_bytes().to_vec(), + Vec::new(), + sender, + ))); + + let _ = receiver.await; + result + .dependency_functions + .as_mut() + .unwrap() + .0 + .push(dependency_function) + } + } + + result + } + + async fn log(&mut self, level: LogLevels, message: String) { + match level { + LogLevels::Trace => trace!(message), + LogLevels::Debug => debug!(message), + LogLevels::Info => info!(message), + LogLevels::Warn => warn!(message), + LogLevels::Error => error!(message), + } + } + + async fn shutdown(&mut self, restart: bool) -> Result<(), Error> { + let (sender, receiver) = channel(); + + self.core_tx + .send(CoreMessages::DatabaseModule(DatabaseMessages::GetState( + Keyspaces::Plugins, + self.plugin_id.as_bytes().to_vec(), + sender, + ))); + + let response_bytes = receiver.await.unwrap().unwrap().unwrap().to_vec(); + + let plugin_permissions = + sonic_rs::from_slice::(&response_bytes).unwrap(); + + if !plugin_permissions + .core + .contains(&ConfigSupportedCoreRegistrations::Shutdown) + { + return Err(Error::from("Not allowed to call shutdown")); + } + + let shutdown_type = if restart { + Shutdown::Restart + } else { + Shutdown::Normal + }; + + self.core_tx.send(CoreMessages::Shutdown(shutdown_type)); + + Ok(()) + } + + async fn dependency_function( + &mut self, + dependency_id: String, + function_id: String, + params: Vec, + ) -> Result, Error> { + todo!() + } +} diff --git a/src/plugins/runtime/internal/discord.rs b/src/plugins/runtime/internal/discord.rs new file mode 100644 index 0000000..855edab --- /dev/null +++ b/src/plugins/runtime/internal/discord.rs @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2026 Eduard Smet */ + +use crate::plugins::{ + discord_bot::plugin::{ + core_import_types::Error, + discord_export_types::Host as DiscordExportTypesHost, + discord_import_functions::Host as DiscordImportFunctionsHost, + discord_import_types::{ + DiscordRegistrations, DiscordRegistrationsResult, DiscordRequests, DiscordResponses, + Host as DiscordImportTypesHost, SupportedDiscordRegistrations, + }, + }, + runtime::internal::InternalRuntime, +}; + +impl DiscordImportTypesHost for InternalRuntime {} +impl DiscordExportTypesHost for InternalRuntime {} + +impl DiscordImportFunctionsHost for InternalRuntime { + async fn get_supported_discord_registrations(&mut self) -> SupportedDiscordRegistrations { + todo!() + } + + async fn discord_register( + &mut self, + registrations: DiscordRegistrations, + ) -> DiscordRegistrationsResult { + todo!() + } + + async fn discord_request( + &mut self, + request: DiscordRequests, + ) -> Result, Error> { + todo!() + } +} diff --git a/src/plugins/runtime/internal/job_scheduler.rs b/src/plugins/runtime/internal/job_scheduler.rs new file mode 100644 index 0000000..f2d73b4 --- /dev/null +++ b/src/plugins/runtime/internal/job_scheduler.rs @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2026 Eduard Smet */ + +use crate::plugins::{ + discord_bot::plugin::{ + job_scheduler_import_functions::Host as JobSchedulerImportFunctionsHost, + job_scheduler_import_types::{ + Host as JobSchedulerImportTypesHost, JobSchedulerRegistrations, + JobSchedulerRegistrationsResult, SupportedJobSchedulerRegistrations, + }, + }, + runtime::internal::InternalRuntime, +}; + +impl JobSchedulerImportTypesHost for InternalRuntime {} + +impl JobSchedulerImportFunctionsHost for InternalRuntime { + async fn get_supported_registrations(&mut self) -> SupportedJobSchedulerRegistrations { + todo!() + } + + async fn register( + &mut self, + registrations: JobSchedulerRegistrations, + ) -> JobSchedulerRegistrationsResult { + todo!() + } +} diff --git a/src/utils/channels.rs b/src/utils/channels.rs index cfa6531..62c0b2a 100644 --- a/src/utils/channels.rs +++ b/src/utils/channels.rs @@ -1,76 +1,124 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ +use anyhow::Result; +use fjall::Slice; use tokio::sync::{ - mpsc::{Receiver as MPSCReceiver, Sender as MPSCSender, channel}, + mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, oneshot::Sender as OSSender, }; +use uuid::Uuid; -use crate::plugins::{ - PluginRegistrationRequestsApplicationCommand, PluginRegistrationRequestsScheduledJob, - discord_bot::plugin::host_functions::{DiscordRequests, DiscordResponses}, - exports::discord_bot::plugin::plugin_functions::DiscordEvents, +use crate::{ + Shutdown, + database::Keyspaces, + plugins::{ + PluginRegistrationRequestsApplicationCommand, + discord_bot::plugin::{ + discord_export_types::DiscordEvents, + discord_import_types::{DiscordRequests, DiscordResponses}, + }, + }, }; -pub enum DiscordBotClientMessages { - RegisterApplicationCommands(Vec), - Request( - DiscordRequests, - OSSender, String>>, - ), - Shutdown(OSSender<()>), +pub enum CoreMessages { + DatabaseModule(DatabaseMessages), + + JobSchedulerModule(JobSchedulerMessages), + DiscordBotClientModule(DiscordBotClientMessages), + + RuntimeModule(RuntimeMessages), + + Shutdown(Shutdown), +} + +pub enum DatabaseMessages { + GetState(Keyspaces, Vec, OSSender>>), + InsertState(Keyspaces, Vec, Vec, OSSender>), + DeleteState(Keyspaces, Vec, OSSender>), + ContainsKey(Keyspaces, Vec, OSSender>), } pub enum JobSchedulerMessages { - RegisterScheduledJobs(Vec), - Shutdown(OSSender<()>), + AddJob(Uuid, String, OSSender>), + RemoveJob(Uuid, OSSender>), +} + +pub enum DiscordBotClientMessages { + RegisterApplicationCommands( + Vec, + OSSender, Vec)>>, + ), + Request(DiscordRequests, OSSender>>), } pub enum RuntimeMessages { - CallDiscordEvent(String, DiscordEvents), - CallScheduledJob(String, String), + JobScheduler(RuntimeMessagesJobScheduler), + Discord(RuntimeMessagesDiscord), +} + +pub enum RuntimeMessagesJobScheduler { + CallScheduledJob(Uuid, Uuid), +} + +pub enum RuntimeMessagesDiscord { + CallDiscordEvent(Uuid, DiscordEvents), } pub struct Channels { - pub discord_bot_client: ChannelsDiscordBotClient, + pub core: ChannelsCore, pub job_scheduler: ChannelsJobScheduler, + pub discord_bot_client: ChannelsDiscordBotClient, pub runtime: ChannelsRuntime, } -pub struct ChannelsDiscordBotClient { - pub sender: MPSCSender, - pub receiver: MPSCReceiver, +pub struct ChannelsCore { + pub job_scheduler_tx: UnboundedSender, + pub discord_bot_client_tx: UnboundedSender, + pub runtime_tx: UnboundedSender, + pub rx: UnboundedReceiver, } pub struct ChannelsJobScheduler { - pub sender: MPSCSender, - pub receiver: MPSCReceiver, + pub core_tx: UnboundedSender, + pub rx: UnboundedReceiver, +} + +pub struct ChannelsDiscordBotClient { + pub core_tx: UnboundedSender, + pub rx: UnboundedReceiver, } pub struct ChannelsRuntime { - pub discord_bot_client_sender: MPSCSender, - pub job_scheduler_sender: MPSCSender, - pub receiver: MPSCReceiver, + pub core_tx: UnboundedSender, + pub rx: UnboundedReceiver, } pub fn new() -> Channels { - let (discord_bot_client_tx, discord_bot_client_rx) = channel::(200); - let (job_scheduler_tx, job_scheduler_rx) = channel::(200); - let (runtime_tx, runtime_rx) = channel::(400); + let (core_tx, core_rx) = unbounded_channel::(); + let (job_scheduler_tx, job_scheduler_rx) = unbounded_channel::(); + let (discord_bot_client_tx, discord_bot_client_rx) = + unbounded_channel::(); + let (runtime_tx, runtime_rx) = unbounded_channel::(); Channels { - discord_bot_client: ChannelsDiscordBotClient { - sender: discord_bot_client_tx, - receiver: discord_bot_client_rx, + core: ChannelsCore { + job_scheduler_tx, + discord_bot_client_tx, + runtime_tx, + rx: core_rx, }, job_scheduler: ChannelsJobScheduler { - sender: job_scheduler_tx, - receiver: job_scheduler_rx, + core_tx: core_tx.clone(), + rx: job_scheduler_rx, + }, + discord_bot_client: ChannelsDiscordBotClient { + core_tx: core_tx.clone(), + rx: discord_bot_client_rx, }, runtime: ChannelsRuntime { - discord_bot_client_sender: runtime_tx.clone(), - job_scheduler_sender: runtime_tx, - receiver: runtime_rx, + core_tx, + rx: runtime_rx, }, } } diff --git a/src/utils/env.rs b/src/utils/env.rs index 13c58a4..fca17b4 100644 --- a/src/utils/env.rs +++ b/src/utils/env.rs @@ -3,38 +3,34 @@ use std::{env, path::Path}; -use tracing::{debug, error, info}; - +use anyhow::{Context, Result, bail}; use dotenvy; +use tracing::{debug, info}; + +pub struct Secrets { + pub discord_bot_client: String, +} -pub fn load_env_file(env_file: &Path) -> Result<(), ()> { +pub fn load_env_file(env_file_path: &Path) -> Result<()> { info!("Loading the env file"); - if let Err(err) = dotenvy::from_path(env_file) { + if let Err(err) = dotenvy::from_path(env_file_path) { if err.not_found() { - debug!("No env file found for the following path: {env_file:?}"); + debug!("No env file found at: {env_file_path:?}"); return Ok(()); } - error!("An error occurred wile trying to load the env file: {err}"); - - return Err(()); + bail!("An error occurred wile trying to load the env file: {err}"); } Ok(()) } -pub fn validate() -> Result { +pub fn get_secrets() -> Result { info!("Validating the environment variables (DISCORD_BOT_CLIENT_TOKEN)"); - if let Ok(value) = env::var("DISCORD_BOT_CLIENT_TOKEN") { - debug!("DISCORD_BOT_CLIENT_TOKEN environment variable was found: {value:.3}... (redacted)"); - - Ok(value) - } else { - error!( - "The DISCORD_BOT_CLIENT_TOKEN environment variable was not set, contains an illegal character ('=' or '0') or was not valid unicode" - ); - Err(()) - } + Ok(Secrets { + discord_bot_client: env::var("DISCORD_BOT_CLIENT_TOKEN") + .context("Failed to load the DISCORD_BOT_CLIENT_TOKEN environment variable")?, + }) } diff --git a/wit/core.wit b/wit/core.wit new file mode 100644 index 0000000..259ad91 --- /dev/null +++ b/wit/core.wit @@ -0,0 +1,62 @@ +interface core-types { + type json = list; +} + +interface core-import-types { + type error = string; + + enum log-levels { + trace, + debug, + info, + warn, + error, + } + + flags supported-core-registrations { + dependency-functions, + shutdown, + } + + /// All registrations are opt in. + record core-registrations { + dependency-functions: option>, + } + + /// The `dependency-functions` field returns separate lists of successful and failed IDs. + /// + /// The `shutdown` field returns a result to indicate if the shutdown registration was accepted or not. + record core-registrations-result { + dependency-functions: option, list>>, + } +} + +interface core-export-types { + type error = string; +} + +interface core-import-functions { + use core-import-types.{error, log-levels, core-registrations, core-registrations-result, supported-core-registrations}; + + get-supported-registrations: func() -> supported-core-registrations; + register: func(registrations: core-registrations) -> core-registrations-result; + log: func(level: log-levels, message: string); + + shutdown: func(restart: bool) -> result<_, error>; + + /// The `params` parameter and result ok value are bytes. The serialized format has to be decided between plugins. + dependency-function: func(dependency-id: string, function-id: string, params: list) -> result, error>; +} + +interface core-export-functions { + use core-export-types.{error}; + use core-types.{json}; + + initialization: func(settings: json) -> result<_, error>; + + shutdown: func() -> result<_, error>; + + /// The `params` parameter and result ok value are bytes. The serialized format has to be decided between plugins. + dependency-function: func(function-id: string, params: list) -> result, error>; +} + diff --git a/wit/discord.wit b/wit/discord.wit index d5877b0..6ea6873 100644 --- a/wit/discord.wit +++ b/wit/discord.wit @@ -1,36 +1,65 @@ -interface discord-types { - /// variant data is JSON, check the [Discord Gateway Event docs] for the structures. +interface discord-import-types { + use core-types.{json}; + + type form = list; + + flags discord-events { + message-create, + interaction-create, + thread-create, + thread-delete, + thread-list-sync, + thread-member-update, + thread-members-update, + thread-update, + } + + type supported-discord-registrations = discord-events; + + /// All registrations are opt in. + record discord-registrations { + events: option, + interactions: option, + } + + /// The `application-commands` field its structure can be found in the Discord docs: https://docs.discord.com/developers/interactions/application-commands#application-command-object + record discord-registrations-interactions { + application-commands: option>, + message-components: option>, + modals: option>, + } + + record discord-registrations-result { + events: discord-events, + interactions: discord-registrations-result-interactions, + } + + /// The fields return separate lists of successful and failed IDs. /// - /// [Discord Gateway Event docs]: https://discord.com/developers/docs/events/gateway-events - variant events { - interaction-create(list), - message-create(list), - thread-create(list), - thread-delete(list), - thread-list-sync(list), - thread-member-update(list), - thread-members-update(list), - thread-update(list), - } - - /// variant data last tuple entry might be JSON, check the Discord [Gateway Send Event] and HTTP Resource (like the [Message Resource docs]) docs for the structures. + /// The successful IDs gets mapped to host IDs which the plugin should use and then expect on events. + record discord-registrations-result-interactions { + application-commands: tuple>, list>, + message-components: tuple>, list>, + modals: tuple>, list>, + } + + /// Check the Discord Gateway Send Event and HTTP Resource docs for the structures of the variant data: https://discord.com/developers/docs/events/gateway-events#send-events /// - /// [Gateway Send Event]: https://discord.com/developers/docs/events/gateway-events#send-events - /// [Message Resource]: https://discord.com/developers/docs/resources/message - variant requests { + /// Message Resource Example: https://discord.com/developers/docs/resources/message + variant discord-requests { // Shard message sender commands - request-guild-members(tuple>), + request-guild-members(tuple), request-soundboard-sounds(list), - update-voice-state(tuple>), - update-presence(list), + update-voice-state(tuple), + update-presence(json), // HTTP requests add-thread-member(tuple), - create-ban(tuple>), - create-forum-thread(tuple), - create-message(tuple), - create-thread(tuple>), - create-thread-from-message(tuple>), + create-ban(tuple), + create-forum-thread(tuple), + create-message(tuple), + create-thread(tuple), + create-thread-from-message(tuple), delete-message(tuple), get-active-threads(u64), get-channel(u64), @@ -39,20 +68,54 @@ interface discord-types { get-public-archived-threads(tuple, u64, option>), get-thread-member(tuple), get-thread-members(tuple, u64, option, option>), - interaction-callback(tuple>), + interaction-callback(tuple), join-thread(u64), leave-thread(u64), remove-thread-member(tuple), - update-member(tuple>), - update-interaction-original(tuple>), + update-member(tuple), + update-interaction-original(tuple), } - // variant data is either a JSON body or a multipart form buffer. - variant contents { - json(list), - form(list), + variant body { + json(json), + form(form), } - /// responses is JSON. - type responses = list; + type discord-responses = json; +} + +interface discord-export-types { + use core-types.{json}; + + /// Check the Discord Gateway Event docs for the structures of the variant data: https://discord.com/developers/docs/events/gateway-events + variant discord-events { + interaction-create(json), + message-create(json), + thread-create(json), + thread-delete(json), + thread-list-sync(json), + thread-member-update(json), + thread-members-update(json), + thread-update(json), + } +} + +interface discord-import-functions { + use core-import-types.{error}; + use discord-import-types.{discord-registrations, discord-registrations-result, discord-requests, discord-responses, supported-discord-registrations}; + + get-supported-discord-registrations: func() -> supported-discord-registrations; + /// Application Command registrations past the initialization phase are ignored. + /// + /// See the following Discord docs for more information: https://docs.discord.com/developers/interactions/application-commands#registering-a-command + discord-register: func(registrations: discord-registrations) -> discord-registrations-result; + + discord-request: func(request: discord-requests) -> result, error>; +} + +interface discord-export-functions { + use core-export-types.{error}; + use discord-export-types.{discord-events}; + + discord-event: func(event: discord-events) -> result<_, error>; } diff --git a/wit/host.wit b/wit/host.wit deleted file mode 100644 index 503c03e..0000000 --- a/wit/host.wit +++ /dev/null @@ -1,42 +0,0 @@ -/// TODO: Implement the registrations-request host function, blocked by missing component -/// information on host calls, fixed by adding Store context to host functions. -/// -/// Relevant Zulip discussion: https://bytecodealliance.zulipchat.com/#narrow/channel/217126-wasmtime/topic/Support.20for.20identifying.20the.20guest.20making.20a.20host.20call/with/571099564 -interface host-types { - //record registrations-result { - // discord-events: registrations-result-discord-events-interaction-create, - // scheduled-jobs: list>, - // dependency-functions: list>, - //} - - //record registrations-result-discord-events-interaction-create { - // application-commands: list>, - // message-components: list>, - // modals: list>, - //} - - enum log-levels { - trace, - debug, - info, - warn, - error, - } -} - -interface host-functions { - use host-types.{log-levels}; - use discord-types.{requests as discord-requests, responses as discord-responses}; - - /// Can only be called during the initialization plugin call, calls at other times will be ignored. - //registrations-request: func(registrations: registrations) -> result; - - log: func(level: log-levels, message: string); - - discord-request: func(request: discord-requests) -> result, string>; - - /// params, result Ok and Err are JSON. - dependency-function: func(dependency: string, function: string, params: list) -> result, string>; - - shutdown: func(restart: bool); -} diff --git a/wit/job-scheduler.wit b/wit/job-scheduler.wit new file mode 100644 index 0000000..5b3a0ac --- /dev/null +++ b/wit/job-scheduler.wit @@ -0,0 +1,36 @@ +interface job-scheduler-import-types { + flags supported-job-scheduler-registrations { + scheduled-jobs, + } + + /// All registrations are opt in. + /// + /// The `scheduled-jobs` field list entries should exist out of cron values. + /// + /// cron: https://en.wikipedia.org/wiki/Cron + record job-scheduler-registrations { + scheduled-jobs: list, + } + + /// The `scheduled-jobs` field returns separate lists of successful and failed cron values. + record job-scheduler-registrations-result { + scheduled-jobs: tuple, list>, + } +} + +interface job-scheduler-export-types { + +} + +interface job-scheduler-import-functions { + use job-scheduler-import-types.{job-scheduler-registrations, job-scheduler-registrations-result, supported-job-scheduler-registrations}; + + get-supported-registrations: func() -> supported-job-scheduler-registrations; + register: func(registrations: job-scheduler-registrations) -> job-scheduler-registrations-result; +} + +interface job-scheduler-export-functions { + use core-export-types.{error}; + + scheduled-job: func(job-id: string) -> result<_, error>; +} diff --git a/wit/plugin.wit b/wit/plugin.wit deleted file mode 100644 index e967d1a..0000000 --- a/wit/plugin.wit +++ /dev/null @@ -1,64 +0,0 @@ -/// Down the line once map gets introduced to the component model -/// mutliple plugin types will rely on it instead of list> or list -/// to enforce uniqueness on the plugin level. -/// [Relevant PR] in the component model repository. -/// -/// [Relevant PR]: https://github.com/WebAssembly/component-model/pull/554 -interface plugin-types { - /// scheduled-jobs: tuple entry 0 is the id and entry 1 is a list of cron - /// values. - record registrations-request { - discord-events: option, - scheduled-jobs: option>>>, - dependency-functions: option>, - } - - record registrations-request-discord-events { - interaction-create: option, - message-create: bool, - thread-create: bool, - thread-delete: bool, - thread-list-sync: bool, - thread-member-update: bool, - thread-members-update: bool, - thread-update: bool, - } - - /// application-commands: tuple entry 0 is the ID and entry 1 is - /// JSON, check the [Discord Application Command docs] for the structure. - /// - /// [Discord Application Command docs]: https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure - record registrations-request-interaction-create { - application-commands: option>>, - message-components: option>, - modals: option>, - } - - flags supported-registrations { - dependency-functions, - discord-event-message-create, - discord-event-interaction-create, - discord-event-thread-create, - discord-event-thread-delete, - discord-event-thread-list-sync, - discord-event-thread-member-update, - discord-event-thread-members-update, - discord-event-thread-update, - scheduled-jobs, - shutdown, - } -} - -interface plugin-functions { - use plugin-types.{registrations-request, supported-registrations}; - use discord-types.{events as discord-events, requests as discord-requests}; - - /// settings is JSON. - initialization: func(settings: list, supported-registrations: supported-registrations) -> result; - shutdown: func() -> result<_, string>; - - discord-event: func(event: discord-events) -> result<_, string>; - scheduled-job: func(job: string) -> result<_, string>; - /// params and result Ok are JSON. - dependency-function: func(function: string, params: list) -> result, string>; -} diff --git a/wit/world.wit b/wit/world.wit index 14b9017..b454c7b 100644 --- a/wit/world.wit +++ b/wit/world.wit @@ -1,7 +1,18 @@ package discord-bot:plugin@0.1.0; +/// Down the line once map gets introduced to the component model +/// multiple plugin types will rely on it instead of list> or list +/// to enforce uniqueness on the plugin level. +/// +/// Relevant PR in the component model repository: https://github.com/WebAssembly/component-model/pull/554 +/// Relevant PR in the Wasmtime repository: https://github.com/bytecodealliance/wasmtime/pull/12216 world plugin { - import host-functions; + import core-import-functions; + export core-export-functions; - export plugin-functions; + import job-scheduler-import-functions; + export job-scheduler-export-functions; + + import discord-import-functions; + export discord-export-functions; } From d4f84529c1cc87d2bf0bdd2b50f9221fadde205a Mon Sep 17 00:00:00 2001 From: Eduard Smet Date: Sun, 5 Apr 2026 02:47:34 +0200 Subject: [PATCH 2/5] WIP: feat: Project restructure --- Cargo.lock | 68 +++++----- Cargo.toml | 4 +- src/config.rs | 4 +- src/config/plugins.rs | 17 +++ src/config/plugins/permissions.rs | 37 ++++++ src/discord/events.rs | 2 +- src/discord/interactions.rs | 8 +- src/discord/requests.rs | 5 +- src/job_scheduler.rs | 2 +- src/main.rs | 11 +- src/plugins.rs | 120 ------------------ src/plugins/permissions.rs | 55 -------- src/{plugins => }/registry.rs | 23 +--- src/registry/plugins.rs | 37 ++++++ src/{plugins => }/runtime.rs | 23 ++-- src/{plugins => runtime}/builder.rs | 2 +- src/{plugins => }/runtime/internal.rs | 0 src/{plugins => }/runtime/internal/core.rs | 8 +- src/{plugins => }/runtime/internal/discord.rs | 6 +- .../runtime/internal/job_scheduler.rs | 6 +- src/runtime/plugins.rs | 60 +++++++++ src/utils/channels.rs | 16 +-- wit/world.wit | 2 +- 23 files changed, 238 insertions(+), 278 deletions(-) create mode 100644 src/config/plugins.rs create mode 100644 src/config/plugins/permissions.rs delete mode 100644 src/plugins.rs delete mode 100644 src/plugins/permissions.rs rename src/{plugins => }/registry.rs (96%) create mode 100644 src/registry/plugins.rs rename src/{plugins => }/runtime.rs (94%) rename src/{plugins => runtime}/builder.rs (94%) rename src/{plugins => }/runtime/internal.rs (100%) rename src/{plugins => }/runtime/internal/core.rs (95%) rename src/{plugins => }/runtime/internal/discord.rs (92%) rename src/{plugins => }/runtime/internal/job_scheduler.rs (89%) create mode 100644 src/runtime/plugins.rs diff --git a/Cargo.lock b/Cargo.lock index aa999f5..3b4d4ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -842,40 +842,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "discord-bot" -version = "0.1.0" -dependencies = [ - "anyhow", - "bytes", - "chrono", - "clap", - "dotenvy", - "fjall", - "indexmap", - "reqwest", - "rustls 0.23.37", - "semver", - "serde", - "serde_yaml_ng", - "sonic-rs", - "tokio", - "tokio-cron-scheduler", - "tokio-util", - "tracing", - "tracing-appender", - "tracing-subscriber", - "twilight-cache-inmemory", - "twilight-gateway", - "twilight-http", - "twilight-model", - "url", - "uuid", - "wasmtime", - "wasmtime-wasi", - "wasmtime-wasi-http", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -4336,6 +4302,40 @@ dependencies = [ "wast 244.0.0", ] +[[package]] +name = "wbps" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "clap", + "dotenvy", + "fjall", + "indexmap", + "reqwest", + "rustls 0.23.37", + "semver", + "serde", + "serde_yaml_ng", + "sonic-rs", + "tokio", + "tokio-cron-scheduler", + "tokio-util", + "tracing", + "tracing-appender", + "tracing-subscriber", + "twilight-cache-inmemory", + "twilight-gateway", + "twilight-http", + "twilight-model", + "url", + "uuid", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-http", +] + [[package]] name = "web-sys" version = "0.3.85" diff --git a/Cargo.toml b/Cargo.toml index 6d3f5ab..216e5c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "discord-bot" -description = "A WASI plugin based Discord bot, configurable through YAML." +name = "wbps" +description = "WASM based plugin services." version = "0.1.0" authors = ["Eduard Smet "] license = "GPL-3.0-or-later" diff --git a/src/config.rs b/src/config.rs index bdf4854..1020da2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,9 @@ use indexmap::IndexMap; use serde::Deserialize; use tracing::{error, info}; -use crate::plugins::ConfigPlugin; +use crate::config::plugins::ConfigPlugin; + +pub mod plugins; #[derive(Deserialize)] pub struct Config { diff --git a/src/config/plugins.rs b/src/config/plugins.rs new file mode 100644 index 0000000..362313b --- /dev/null +++ b/src/config/plugins.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; + +use serde::Deserialize; +use sonic_rs::Value; + +use crate::config::plugins::permissions::ConfigPluginPermissions; + +pub mod permissions; + +#[derive(Deserialize)] +pub struct ConfigPlugin { + pub plugin: String, + pub cache: Option, + pub permissions: ConfigPluginPermissions, + pub environment: Option>, + pub settings: Option, +} diff --git a/src/config/plugins/permissions.rs b/src/config/plugins/permissions.rs new file mode 100644 index 0000000..ceaa72b --- /dev/null +++ b/src/config/plugins/permissions.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct ConfigPluginPermissions { + #[serde(default)] + pub core: Vec, + #[serde(default)] + pub job_scheduler: Vec, + #[serde(default)] + pub discord: Vec, +} + +#[derive(Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ConfigSupportedCoreRegistrations { + DependencyFunctions, + Shutdown, +} + +#[derive(Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ConfigSupportedJobSchedulerRegistrations { + ScheduledJobs, +} + +#[derive(Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ConfigSupportedDiscordRegistrations { + MessageCreate, + InteractionCreate, + ThreadCreate, + ThreadDelete, + ThreadListSync, + ThreadMemberUpdate, + ThreadMembersUpdate, + ThreadUpdate, +} diff --git a/src/discord/events.rs b/src/discord/events.rs index d6c125b..6a62197 100644 --- a/src/discord/events.rs +++ b/src/discord/events.rs @@ -10,7 +10,7 @@ use twilight_model::application::interaction::InteractionData; use crate::{ discord::DiscordBotClient, - plugins::discord_bot::plugin::discord_export_types::DiscordEvents, + runtime::plugins::exports::wbps::plugin::discord_export_functions::DiscordEvents, utils::channels::{CoreMessages, RuntimeMessages, RuntimeMessagesDiscord}, }; diff --git a/src/discord/interactions.rs b/src/discord/interactions.rs index 1d3dc68..dfd8589 100644 --- a/src/discord/interactions.rs +++ b/src/discord/interactions.rs @@ -14,21 +14,19 @@ use twilight_model::{ }, }; -use crate::{discord::DiscordBotClient, plugins::PluginRegistrationRequestsApplicationCommand}; +use crate::discord::DiscordBotClient; impl DiscordBotClient { pub async fn application_command_registrations( http_client: Arc, - discord_application_command_registration_request: Vec< - PluginRegistrationRequestsApplicationCommand, - >, + discord_application_command_registration_request: Vec>, ) -> Result<(Vec, Vec)> { let mut discord_commands = HashMap::new(); let mut commands = HashMap::new(); for command in discord_application_command_registration_request { - let command_data = match sonic_rs::from_slice::(&command.data) { + let command_data = match sonic_rs::from_slice::(&command) { Ok(command) => command, Err(err) => { error!( diff --git a/src/discord/requests.rs b/src/discord/requests.rs index f8b398a..be77bf5 100644 --- a/src/discord/requests.rs +++ b/src/discord/requests.rs @@ -17,9 +17,8 @@ use twilight_model::gateway::{ use crate::{ discord::DiscordBotClient, - plugins::discord_bot::plugin::{ - discord_import_functions::{DiscordRequests, DiscordResponses}, - discord_import_types::Body, + runtime::plugins::wbps::plugin::discord_import_types::{ + Body, DiscordRequests, DiscordResponses, }, }; diff --git a/src/job_scheduler.rs b/src/job_scheduler.rs index efa3920..2c04ea0 100644 --- a/src/job_scheduler.rs +++ b/src/job_scheduler.rs @@ -78,7 +78,7 @@ impl JobScheduler { let core_tx = core_tx.clone(); Box::pin(async move { - let _ = core_tx.send(CoreMessages::RuntimeModule(RuntimeMessages::JobScheduler( + let _ = core_tx.send(CoreMessages::Runtime(RuntimeMessages::JobScheduler( RuntimeMessagesJobScheduler::CallScheduledJob(plugin_id, job_id), ))); }) diff --git a/src/main.rs b/src/main.rs index 9aad980..be0e24e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,17 +25,20 @@ mod database; mod discord; mod http; mod job_scheduler; -mod plugins; +mod registry; +mod runtime; mod utils; use cli::{Cli, CliLogParameters}; use config::Config; use discord::DiscordBotClient; use job_scheduler::JobScheduler; -use plugins::{registry, runtime::Runtime}; use utils::{channels::Channels, env::Secrets}; -use crate::utils::channels::{ChannelsCore, CoreMessages}; +use crate::{ + runtime::Runtime, + utils::channels::{ChannelsCore, CoreMessages}, +}; #[derive(PartialEq)] enum Shutdown { @@ -149,7 +152,7 @@ fn start(database: Database, mut channels_core: ChannelsCore) -> JoinHandle<()> .discord_bot_client_tx .send(discord_bot_client_message); } - CoreMessages::RuntimeModule(runtime_message) => { + CoreMessages::Runtime(runtime_message) => { channels_core.runtime_tx.send(runtime_message); } CoreMessages::Shutdown(shutdown) => todo!(), // TODO: Figure shutdown out diff --git a/src/plugins.rs b/src/plugins.rs deleted file mode 100644 index 79ca671..0000000 --- a/src/plugins.rs +++ /dev/null @@ -1,120 +0,0 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ -/* Copyright © 2026 Eduard Smet */ - -pub mod builder; -pub mod permissions; -pub mod registry; -pub mod runtime; - -use std::collections::{HashMap, HashSet}; - -use semver::Version; -use serde::Deserialize; -use serde_yaml_ng::Value; -use twilight_model::id::{Id, marker::CommandMarker}; - -use crate::plugins::permissions::ConfigPluginPermissions; - -wasmtime::component::bindgen!({ imports: { default: async }, exports: { default: async } }); - -#[derive(Deserialize)] -pub struct ConfigPlugin { - pub plugin: String, - pub cache: Option, - pub permissions: ConfigPluginPermissions, - pub environment: Option>, - pub settings: Option, -} - -pub struct AvailablePlugin { - pub registry_id: String, - pub id: String, - pub user_id: String, - pub version: Version, - pub permissions: ConfigPluginPermissions, - pub environment: Option>, - pub settings: Option, -} - -// TODO: Plugins which did not register anything should get dropped -pub struct PluginRegistrations { - pub discord_events: PluginRegistrationsDiscordEvents, - pub scheduled_jobs: HashMap, // UUID, plugin ID, internal ID - pub dependency_functions: HashMap>, -} - -pub struct PluginRegistrationsDiscordEvents { - pub interaction_create: PluginRegistrationsInteractionCreate, - pub message_create: Vec, - pub thread_create: Vec, - pub thread_delete: Vec, - pub thread_list_sync: Vec, - pub thread_member_update: Vec, - pub thread_members_update: Vec, - pub thread_update: Vec, -} - -pub struct PluginRegistrationsInteractionCreate { - pub application_commands: HashMap, String>, // Command ID, plugin ID - pub message_components: HashMap, // Message Component ID, plugin ID ISSUE: ID overlap is possible - pub modals: HashMap, // Modal ID, plugin ID ISSUE: ID overlap is possible -} - -pub struct PluginRegistrationRequests { - pub discord_event_interaction_create: PluginRegistrationRequestsInteractionCreate, - pub scheduled_jobs: Vec, -} - -pub struct PluginRegistrationRequestsInteractionCreate { - pub application_commands: Vec, - #[allow(unused)] // Will be used when wasmtime provides component information on host calls - pub message_component: Vec, - #[allow(unused)] // Will be used when wasmtime provides component information on host calls - pub modals: Vec, -} - -pub struct PluginRegistrationRequestsApplicationCommand { - pub plugin_id: String, - pub data: Vec, -} - -#[allow(unused)] // Will be used when wasmtime provides component information on host calls -pub struct PluginRegistrationRequestsMessageComponent { - pub plugin_id: String, - pub id: String, -} - -#[allow(unused)] // Will be used when wasmtime provides component information on host calls -pub struct PluginRegistrationRequestsModal { - pub plugin_id: String, - pub id: String, -} - -pub struct PluginRegistrationRequestsScheduledJob { - pub plugin_id: String, - pub id: String, - pub crons: Vec, -} - -impl PluginRegistrations { - pub fn new() -> Self { - PluginRegistrations { - discord_events: PluginRegistrationsDiscordEvents { - interaction_create: PluginRegistrationsInteractionCreate { - application_commands: HashMap::new(), - message_components: HashMap::new(), - modals: HashMap::new(), - }, - message_create: vec![], - thread_create: vec![], - thread_delete: vec![], - thread_list_sync: vec![], - thread_member_update: vec![], - thread_members_update: vec![], - thread_update: vec![], - }, - scheduled_jobs: HashMap::new(), - dependency_functions: HashMap::new(), - } - } -} diff --git a/src/plugins/permissions.rs b/src/plugins/permissions.rs deleted file mode 100644 index 5b5a195..0000000 --- a/src/plugins/permissions.rs +++ /dev/null @@ -1,55 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::plugins::discord_bot::plugin::core_import_types::SupportedCoreRegistrations; - -#[derive(Deserialize, Serialize)] -pub struct ConfigPluginPermissions { - pub core: Vec, - pub job_scheduler: Vec, - pub discord: Vec, -} - -#[derive(Deserialize, PartialEq, Serialize)] -#[serde(untagged)] -pub enum ConfigSupportedCoreRegistrations { - DependencyFunctions, - Shutdown, -} - -#[derive(Deserialize, PartialEq, Serialize)] -#[serde(untagged)] -pub enum ConfigSupportedJobSchedulerRegistrations { - ScheduledJobs, -} - -#[derive(Deserialize, PartialEq, Serialize)] -#[serde(untagged)] -pub enum ConfigSupportedDiscordRegistrations { - MessageCreate, - InteractionCreate, - ThreadCreate, - ThreadDelete, - ThreadListSync, - ThreadMemberUpdate, - ThreadMembersUpdate, - ThreadUpdate, -} - -impl From> for SupportedCoreRegistrations { - fn from(config_supported_core_registrations: Vec) -> Self { - let mut supported_core_registrations = Self::empty(); - - for registration in &config_supported_core_registrations { - match registration { - ConfigSupportedCoreRegistrations::DependencyFunctions => { - supported_core_registrations &= SupportedCoreRegistrations::DEPENDENCY_FUNCTIONS - } - ConfigSupportedCoreRegistrations::Shutdown => { - supported_core_registrations &= SupportedCoreRegistrations::SHUTDOWN - } - } - } - - supported_core_registrations - } -} diff --git a/src/plugins/registry.rs b/src/registry.rs similarity index 96% rename from src/plugins/registry.rs rename to src/registry.rs index b6e0860..ba1e63e 100644 --- a/src/plugins/registry.rs +++ b/src/registry.rs @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ +pub mod plugins; + use std::{ collections::{BTreeMap, HashMap}, io::ErrorKind, @@ -16,9 +18,9 @@ use tracing::{error, info, warn}; use uuid::Uuid; use crate::{ - config::Config, + config::{Config, plugins::ConfigPlugin}, http::HttpClient, - plugins::{AvailablePlugin, ConfigPlugin}, + registry::plugins::{AvailablePlugin, RegistryPlugin, RegistryPluginVersion}, }; #[derive(Deserialize)] @@ -30,23 +32,6 @@ pub struct Registry { pub plugins: BTreeMap, } -#[derive(Deserialize)] -#[allow(unused)] -pub struct RegistryPlugin { - pub versions: Vec, - pub description: String, -} - -#[derive(Deserialize)] -#[allow(unused)] -pub struct RegistryPluginVersion { - pub version: String, - pub release_time: String, - pub compatible_program_version: String, - pub deprecated: Option, - pub deprecation_reason: Option, -} - type RegistryTask = Vec>>>; static DEFAULT_REGISTRY_ID: &str = diff --git a/src/registry/plugins.rs b/src/registry/plugins.rs new file mode 100644 index 0000000..4b109cb --- /dev/null +++ b/src/registry/plugins.rs @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2026 Eduard Smet */ + +use std::collections::HashMap; + +use semver::Version; +use serde::Deserialize; +use sonic_rs::Value; + +use crate::config::plugins::permissions::ConfigPluginPermissions; + +#[derive(Deserialize)] +#[allow(unused)] +pub struct RegistryPlugin { + pub versions: Vec, + pub description: String, +} + +#[derive(Deserialize)] +#[allow(unused)] +pub struct RegistryPluginVersion { + pub version: String, + pub release_time: String, + pub compatible_program_version: String, + pub deprecated: Option, + pub deprecation_reason: Option, +} + +pub struct AvailablePlugin { + pub registry_id: String, + pub id: String, + pub user_id: String, + pub version: Version, + pub permissions: ConfigPluginPermissions, + pub environment: Option>, + pub settings: Option, +} diff --git a/src/plugins/runtime.rs b/src/runtime.rs similarity index 94% rename from src/plugins/runtime.rs rename to src/runtime.rs index 912041c..16bb70c 100644 --- a/src/plugins/runtime.rs +++ b/src/runtime.rs @@ -1,12 +1,14 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -pub mod internal; +mod builder; +mod internal; +pub mod plugins; use std::{collections::HashMap, fs, path::Path}; use anyhow::Result; -use serde_yaml_ng::Value; +use sonic_rs::Value; use tokio::{ sync::{ Mutex, RwLock, @@ -21,10 +23,11 @@ use wasmtime_wasi::{DirPerms, FilePerms, ResourceTable, WasiCtxBuilder}; use wasmtime_wasi_http::WasiHttpCtx; use crate::{ - plugins::{ - AvailablePlugin, Plugin, builder::PluginBuilder, - discord_bot::plugin::discord_export_types::DiscordEvents, - runtime::internal::InternalRuntime, + registry::plugins::AvailablePlugin, + runtime::{ + builder::PluginBuilder, + internal::InternalRuntime, + plugins::{Plugin, wbps::plugin::discord_export_types::DiscordEvents}, }, utils::channels::{ CoreMessages, RuntimeMessages, RuntimeMessagesDiscord, RuntimeMessagesJobScheduler, @@ -172,7 +175,7 @@ impl Runtime { }; match instance - .discord_bot_plugin_core_export_functions() + .wbps_plugin_core_export_functions() .call_initialization( &mut store, &sonic_rs::to_vec(&plugin.settings.unwrap_or(Value::default())).unwrap(), @@ -216,7 +219,7 @@ impl Runtime { match plugin .instance - .discord_bot_plugin_discord_export_functions() + .wbps_plugin_discord_export_functions() .call_discord_event(&mut *plugin.store.lock().await, event) .await { @@ -237,7 +240,7 @@ impl Runtime { match plugin .instance - .discord_bot_plugin_job_scheduler_export_functions() + .wbps_plugin_job_scheduler_export_functions() .call_scheduled_job(&mut *plugin.store.lock().await, &uuid.to_string()) .await { @@ -258,7 +261,7 @@ impl Runtime { match plugin .instance - .discord_bot_plugin_core_export_functions() + .wbps_plugin_core_export_functions() .call_shutdown(&mut *plugin.store.lock().await) .await { diff --git a/src/plugins/builder.rs b/src/runtime/builder.rs similarity index 94% rename from src/plugins/builder.rs rename to src/runtime/builder.rs index 6a29668..d92da72 100644 --- a/src/plugins/builder.rs +++ b/src/runtime/builder.rs @@ -6,7 +6,7 @@ use wasmtime::{ component::{HasSelf, Linker}, }; -use crate::plugins::{Plugin, runtime::internal::InternalRuntime}; +use crate::runtime::{internal::InternalRuntime, plugins::Plugin}; pub struct PluginBuilder { pub engine: Engine, diff --git a/src/plugins/runtime/internal.rs b/src/runtime/internal.rs similarity index 100% rename from src/plugins/runtime/internal.rs rename to src/runtime/internal.rs diff --git a/src/plugins/runtime/internal/core.rs b/src/runtime/internal/core.rs similarity index 95% rename from src/plugins/runtime/internal/core.rs rename to src/runtime/internal/core.rs index b5c48c3..566e61b 100644 --- a/src/plugins/runtime/internal/core.rs +++ b/src/runtime/internal/core.rs @@ -6,9 +6,11 @@ use tracing::{debug, error, info, trace, warn}; use crate::{ Shutdown, + config::plugins::permissions::{ConfigPluginPermissions, ConfigSupportedCoreRegistrations}, database::Keyspaces, - plugins::{ - discord_bot::plugin::{ + runtime::{ + internal::InternalRuntime, + plugins::wbps::plugin::{ core_export_types::Host as CoreExportTypesHost, core_import_functions::Host as CoreImportFunctionsHost, core_import_types::{ @@ -17,8 +19,6 @@ use crate::{ }, core_types::Host as CoreTypesHost, }, - permissions::{ConfigPluginPermissions, ConfigSupportedCoreRegistrations}, - runtime::internal::InternalRuntime, }, utils::channels::{CoreMessages, DatabaseMessages}, }; diff --git a/src/plugins/runtime/internal/discord.rs b/src/runtime/internal/discord.rs similarity index 92% rename from src/plugins/runtime/internal/discord.rs rename to src/runtime/internal/discord.rs index 855edab..1bfeaa9 100644 --- a/src/plugins/runtime/internal/discord.rs +++ b/src/runtime/internal/discord.rs @@ -1,8 +1,9 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use crate::plugins::{ - discord_bot::plugin::{ +use crate::runtime::{ + internal::InternalRuntime, + plugins::wbps::plugin::{ core_import_types::Error, discord_export_types::Host as DiscordExportTypesHost, discord_import_functions::Host as DiscordImportFunctionsHost, @@ -11,7 +12,6 @@ use crate::plugins::{ Host as DiscordImportTypesHost, SupportedDiscordRegistrations, }, }, - runtime::internal::InternalRuntime, }; impl DiscordImportTypesHost for InternalRuntime {} diff --git a/src/plugins/runtime/internal/job_scheduler.rs b/src/runtime/internal/job_scheduler.rs similarity index 89% rename from src/plugins/runtime/internal/job_scheduler.rs rename to src/runtime/internal/job_scheduler.rs index f2d73b4..1e39e8c 100644 --- a/src/plugins/runtime/internal/job_scheduler.rs +++ b/src/runtime/internal/job_scheduler.rs @@ -1,15 +1,15 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use crate::plugins::{ - discord_bot::plugin::{ +use crate::runtime::{ + internal::InternalRuntime, + plugins::wbps::plugin::{ job_scheduler_import_functions::Host as JobSchedulerImportFunctionsHost, job_scheduler_import_types::{ Host as JobSchedulerImportTypesHost, JobSchedulerRegistrations, JobSchedulerRegistrationsResult, SupportedJobSchedulerRegistrations, }, }, - runtime::internal::InternalRuntime, }; impl JobSchedulerImportTypesHost for InternalRuntime {} diff --git a/src/runtime/plugins.rs b/src/runtime/plugins.rs new file mode 100644 index 0000000..117e199 --- /dev/null +++ b/src/runtime/plugins.rs @@ -0,0 +1,60 @@ +use crate::{ + config::plugins::permissions::{ + ConfigSupportedCoreRegistrations, ConfigSupportedDiscordRegistrations, + ConfigSupportedJobSchedulerRegistrations, + }, + runtime::plugins::wbps::plugin::{ + core_import_types::SupportedCoreRegistrations, + discord_import_types::SupportedDiscordRegistrations, + job_scheduler_import_types::SupportedJobSchedulerRegistrations, + }, +}; + +wasmtime::component::bindgen!({ imports: { default: async }, exports: { default: async } }); + +impl From> for SupportedCoreRegistrations { + fn from(config_supported_core_registrations: Vec) -> Self { + let mut supported_core_registrations = Self::empty(); + + for registration in &config_supported_core_registrations { + match registration { + ConfigSupportedCoreRegistrations::DependencyFunctions => { + supported_core_registrations &= SupportedCoreRegistrations::DEPENDENCY_FUNCTIONS + } + ConfigSupportedCoreRegistrations::Shutdown => { + supported_core_registrations &= SupportedCoreRegistrations::SHUTDOWN + } + } + } + + supported_core_registrations + } +} + +impl From> for SupportedJobSchedulerRegistrations { + fn from( + config_supported_job_scheduler_registrations: Vec, + ) -> Self { + let mut supported_job_scheduler_registrations = Self::empty(); + + for registration in &config_supported_job_scheduler_registrations { + todo!(); + } + + supported_job_scheduler_registrations + } +} + +impl From> for SupportedDiscordRegistrations { + fn from( + config_supported_discord_registrations: Vec, + ) -> Self { + let mut supported_discord_registrations = Self::empty(); + + for registration in &config_supported_discord_registrations { + todo!(); + } + + supported_discord_registrations + } +} diff --git a/src/utils/channels.rs b/src/utils/channels.rs index 62c0b2a..80416d7 100644 --- a/src/utils/channels.rs +++ b/src/utils/channels.rs @@ -12,12 +12,9 @@ use uuid::Uuid; use crate::{ Shutdown, database::Keyspaces, - plugins::{ - PluginRegistrationRequestsApplicationCommand, - discord_bot::plugin::{ - discord_export_types::DiscordEvents, - discord_import_types::{DiscordRequests, DiscordResponses}, - }, + runtime::plugins::wbps::plugin::{ + discord_export_types::DiscordEvents, discord_import_functions::DiscordRequests, + discord_import_types::DiscordResponses, }, }; @@ -27,7 +24,7 @@ pub enum CoreMessages { JobSchedulerModule(JobSchedulerMessages), DiscordBotClientModule(DiscordBotClientMessages), - RuntimeModule(RuntimeMessages), + Runtime(RuntimeMessages), Shutdown(Shutdown), } @@ -45,10 +42,7 @@ pub enum JobSchedulerMessages { } pub enum DiscordBotClientMessages { - RegisterApplicationCommands( - Vec, - OSSender, Vec)>>, - ), + RegisterApplicationCommands(Vec>, OSSender, Vec)>>), Request(DiscordRequests, OSSender>>), } diff --git a/wit/world.wit b/wit/world.wit index b454c7b..89e4874 100644 --- a/wit/world.wit +++ b/wit/world.wit @@ -1,4 +1,4 @@ -package discord-bot:plugin@0.1.0; +package wbps:plugin@0.1.0; /// Down the line once map gets introduced to the component model /// multiple plugin types will rely on it instead of list> or list From 00dc8b59b4a4e0668311b64c80743bd6e86d8a91 Mon Sep 17 00:00:00 2001 From: Eduard Smet Date: Mon, 6 Apr 2026 00:23:30 +0200 Subject: [PATCH 3/5] WIP: feat: Discord service update --- README.md | 22 +-- justfile | 3 + src/config/plugins.rs | 6 +- src/database.rs | 54 +++++-- src/discord.rs | 16 +- src/discord/events.rs | 295 +++++++++++++--------------------- src/discord/interactions.rs | 32 +++- src/registry/plugins.rs | 4 +- src/runtime.rs | 29 ++-- src/runtime/internal.rs | 5 +- src/runtime/internal/core.rs | 6 +- src/services/discord.rs | 0 src/services/job_scheduler.rs | 0 src/utils/channels.rs | 10 +- 14 files changed, 229 insertions(+), 253 deletions(-) create mode 100644 src/services/discord.rs create mode 100644 src/services/job_scheduler.rs diff --git a/README.md b/README.md index 9649621..5be79c7 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,17 @@ -# Discord Bot +# wbps -A WASI plugin based Discord bot, configurable through YAML. +WASM based plugin services. ## Project Goal The goal of this project is to create a **Docker Compose-like experience** for -Discord bots. +running services like Discord bots or cron jobs. -Users are able to self-host their own personal bot, assembled from plugins +Users are able to self-host their own personal instance, assembled from plugins available from the official registry. -This official registry contains plugins covering the most common features -required from Discord bots. +This official registry contains plugins covering the most used features. Programmers are also able to add their own custom plugins. This allows them to focus on what really matters (the functionality of that plugin) while relying on features provided by other plugins. - -## To Do List - -- [X] Codebase restructure -- [X] Complete the job scheduler -- [X] Implement all WASI host functions -- [X] Implement the Discord request handler -- [ ] Microservice based daemon rewrite -- [ ] Implement all TODOs -- [ ] Add support for all Discord events and requests -- [ ] Make plugins diff --git a/justfile b/justfile index cc84f4b..ef74a2a 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,9 @@ clippy: clippy-fix: cargo clippy --fix -- -W clippy::pedantic +fmt: + cargo fmt + build-dev: cargo build diff --git a/src/config/plugins.rs b/src/config/plugins.rs index 362313b..d0952c2 100644 --- a/src/config/plugins.rs +++ b/src/config/plugins.rs @@ -12,6 +12,8 @@ pub struct ConfigPlugin { pub plugin: String, pub cache: Option, pub permissions: ConfigPluginPermissions, - pub environment: Option>, - pub settings: Option, + #[serde(default)] + pub environment: HashMap, + #[serde(default)] + pub settings: Value, } diff --git a/src/database.rs b/src/database.rs index 1a1affd..fdaa10e 100644 --- a/src/database.rs +++ b/src/database.rs @@ -4,23 +4,24 @@ use std::{ fs::{self}, io::ErrorKind, + ops::RangeBounds, path::Path, }; use anyhow::{Result, bail}; -use fjall::{Database, KeyspaceCreateOptions, PersistMode, Slice}; +use fjall::{Database, Iter, KeyspaceCreateOptions, PersistMode, Slice}; use crate::utils::channels::DatabaseMessages; pub enum Keyspaces { - Plugins, - PluginStore, - DependencyFunctions, - ScheduledJobs, - DiscordEvents, - DiscordApplicationCommands, - DiscordMessageComponents, - DiscordModals, + Plugins, // K: Uuid; AvailablePlugin + PluginStore, // K: String (Uuid-String); V: Vec + DependencyFunctions, // K: String (registry_id/plugin_id): V: Uuid + ScheduledJobs, // K: Uuid; V: Uuid; + DiscordEvents, // K: String; V: Vec + DiscordApplicationCommands, // K: u64; V: Uuid + DiscordMessageComponents, // K: Uuid; V: Uuid + DiscordModals, // K: Uuid; V: Uuid } pub fn new(database_directory_path: &Path) -> Result { @@ -35,18 +36,24 @@ pub fn new(database_directory_path: &Path) -> Result { pub fn handle_action(database: Database, message: DatabaseMessages) { match message { - DatabaseMessages::GetState(keyspace, key, response_sender) => { + DatabaseMessages::Get(keyspace, key, response_sender) => { response_sender.send(get(database, keyspace, key)); } - DatabaseMessages::InsertState(keyspace, key, value, response_sender) => { + DatabaseMessages::GetAll(keyspace, response_sender) => { + response_sender.send(get_all(database, keyspace)); + } + DatabaseMessages::Insert(keyspace, key, value, response_sender) => { response_sender.send(insert(database, keyspace, key, value)); } - DatabaseMessages::DeleteState(keyspace, key, response_sender) => { + DatabaseMessages::Remove(keyspace, key, response_sender) => { response_sender.send(remove(database, keyspace, key)); } DatabaseMessages::ContainsKey(keyspace, key, response_sender) => { response_sender.send(contains_key(database, keyspace, key)); } + DatabaseMessages::Clear(keyspace, response_sender) => { + response_sender.send(clear(database, keyspace)); + } } } @@ -56,6 +63,23 @@ pub fn get(database: Database, keyspace: Keyspaces, key: Vec) -> Result(database: Database, keyspace: Keyspaces, range: R) -> Result +where + K: AsRef<[u8]>, + R: RangeBounds, +{ + let keyspace = database.keyspace(get_keyspace(keyspace), KeyspaceCreateOptions::default)?; + + Ok(keyspace.range(range)) +} + +pub fn get_all(database: Database, keyspace: Keyspaces) -> Result> { + Ok(range(database, keyspace, Vec::new()..=Vec::new())? + .map(|g| g.value()) + .collect::, fjall::Error>>()?) +} + pub fn insert(database: Database, keyspace: Keyspaces, key: Vec, value: Vec) -> Result<()> { let keyspace = database.keyspace(get_keyspace(keyspace), KeyspaceCreateOptions::default)?; @@ -74,6 +98,12 @@ pub fn contains_key(database: Database, keyspace: Keyspaces, key: Vec) -> Re Ok(keyspace.contains_key(key)?) } +pub fn clear(database: Database, keyspace: Keyspaces) -> Result<()> { + let keyspace = database.keyspace(get_keyspace(keyspace), KeyspaceCreateOptions::default)?; + + Ok(keyspace.clear()?) +} + pub fn persist(database: Database, persist_mode: PersistMode) -> Result<()> { Ok(database.persist(persist_mode)?) } diff --git a/src/discord.rs b/src/discord.rs index 991ce6e..3daf719 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -93,17 +93,11 @@ impl DiscordBotClient { tokio::spawn(async move { while let Some(message) = self.rx.recv().await { match message { - DiscordBotClientMessages::RegisterApplicationCommands( - commands, - response_sender, - ) => { - let http_client = self.http_client.clone(); - tokio::spawn(async { - response_sender.send( - Self::application_command_registrations(http_client, commands) - .await, - ); - }); + DiscordBotClientMessages::RegisterApplicationCommands => { + tokio::spawn(Self::application_command_registrations( + self.http_client.clone(), + self.core_tx.clone(), + )); } DiscordBotClientMessages::Request(request, response_sender) => { let http_client = self.http_client.clone(); diff --git a/src/discord/events.rs b/src/discord/events.rs index 6a62197..af10850 100644 --- a/src/discord/events.rs +++ b/src/discord/events.rs @@ -1,235 +1,164 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; -use tokio::sync::mpsc::UnboundedSender; +use serde::Serialize; +use tokio::sync::{mpsc::UnboundedSender, oneshot::channel}; use tracing::{debug, error}; use twilight_gateway::Event; use twilight_model::application::interaction::InteractionData; +use uuid::Uuid; use crate::{ + database::Keyspaces, discord::DiscordBotClient, runtime::plugins::exports::wbps::plugin::discord_export_functions::DiscordEvents, - utils::channels::{CoreMessages, RuntimeMessages, RuntimeMessagesDiscord}, + utils::channels::{CoreMessages, DatabaseMessages, RuntimeMessages, RuntimeMessagesDiscord}, }; impl DiscordBotClient { - #[allow(clippy::too_many_lines)] pub async fn handle_event(core_tx: Arc>, event: Event) { match event { Event::InteractionCreate(interaction_create) => { match interaction_create.data.as_ref() { Some(InteractionData::ApplicationCommand(command_data)) => { - // TODO: get value - - let _ = core_tx - .send(CoreMessages::Runtime(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin_uuid, - DiscordEvents::InteractionCreate( - sonic_rs::to_vec(&interaction_create).unwrap(), - ), + let (sender, receiver) = channel(); + + core_tx.send(CoreMessages::DatabaseModule(DatabaseMessages::Get( + Keyspaces::DiscordApplicationCommands, + sonic_rs::to_vec(&command_data.id.get()).unwrap(), + sender, + ))); + + let Some(response_bytes) = receiver.await.unwrap().unwrap() else { + return; + }; + + core_tx.send(CoreMessages::Runtime(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + Uuid::from_slice(&response_bytes).unwrap(), + DiscordEvents::InteractionCreate( + sonic_rs::to_vec(&interaction_create).unwrap(), ), - ))) - .await; + ), + ))); } Some(InteractionData::MessageComponent(message_component_interaction_data)) => { - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::InteractionCreate( - sonic_rs::to_vec(&interaction_create).unwrap(), - ), + let (sender, receiver) = channel(); + + let Ok(message_component_id) = + Uuid::from_str(&message_component_interaction_data.custom_id) + else { + return; + }; + + core_tx.send(CoreMessages::DatabaseModule(DatabaseMessages::Get( + Keyspaces::DiscordMessageComponents, + message_component_id.as_bytes().to_vec(), + sender, + ))); + + let Some(response_bytes) = receiver.await.unwrap().unwrap() else { + return; + }; + + core_tx.send(CoreMessages::Runtime(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + Uuid::from_slice(&response_bytes).unwrap(), + DiscordEvents::InteractionCreate( + sonic_rs::to_vec(&interaction_create).unwrap(), ), - )) - .await; + ), + ))); } Some(InteractionData::ModalSubmit(modal_interaction_data)) => { - let initialized_plugins = - discord_bot_client.plugin_registrations.read().await; - - let Some(plugin) = initialized_plugins - .discord_events - .interaction_create - .modals - .get(&modal_interaction_data.custom_id) - else { + let (sender, receiver) = channel(); + + let Ok(modal_id) = Uuid::from_str(&modal_interaction_data.custom_id) else { + return; + }; + + core_tx.send(CoreMessages::DatabaseModule(DatabaseMessages::Get( + Keyspaces::DiscordMessageComponents, + modal_id.as_bytes().to_vec(), + sender, + ))); + + let Some(response_bytes) = receiver.await.unwrap().unwrap() else { return; }; - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::InteractionCreate( - sonic_rs::to_vec(&interaction_create).unwrap(), - ), + core_tx.send(CoreMessages::Runtime(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + Uuid::from_slice(&response_bytes).unwrap(), + DiscordEvents::InteractionCreate( + sonic_rs::to_vec(&interaction_create).unwrap(), ), - )) - .await; + ), + ))); } - None => error!("Interaction data is required."), _ => error!( - "This interaction create data type does not have support yet, interaction data type: {:#?}", - &interaction_create.data.as_ref().unwrap().type_id() + "Received unsupported interaction event: {}", + interaction_create.kind.kind() ), } } Event::MessageCreate(message_create) => { - for plugin in &discord_bot_client - .plugin_registrations - .read() - .await - .discord_events - .message_create - { - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::MessageCreate( - sonic_rs::to_vec(&message_create).unwrap(), - ), - ), - )) - .await; - } + Self::handle_basic_event(core_tx, "MESSAGE_CREATE", message_create); } Event::ThreadCreate(thread_create) => { - for plugin in &discord_bot_client - .plugin_registrations - .read() - .await - .discord_events - .thread_create - { - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadCreate( - sonic_rs::to_vec(&thread_create).unwrap(), - ), - ), - )) - .await; - } + Self::handle_basic_event(core_tx, "THREAD_CREATE", thread_create); } Event::ThreadDelete(thread_delete) => { - for plugin in &discord_bot_client - .plugin_registrations - .read() - .await - .discord_events - .thread_delete - { - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadDelete( - sonic_rs::to_vec(&thread_delete).unwrap(), - ), - ), - )) - .await; - } - } - Event::ThreadListSync(thread_list_sync) => { - for plugin in &discord_bot_client - .plugin_registrations - .read() - .await - .discord_events - .thread_list_sync - { - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadListSync( - sonic_rs::to_vec(&thread_list_sync).unwrap(), - ), - ), - )) - .await; - } + Self::handle_basic_event(core_tx, "THREAD_DELETE", thread_delete); } Event::ThreadMemberUpdate(thread_member_update) => { - for plugin in &discord_bot_client - .plugin_registrations - .read() - .await - .discord_events - .thread_member_update - { - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadMemberUpdate( - sonic_rs::to_vec(&thread_member_update).unwrap(), - ), - ), - )) - .await; - } + Self::handle_basic_event(core_tx, "THREAD_MEMBER_UPDATE", thread_member_update); } Event::ThreadMembersUpdate(thread_members_update) => { - for plugin in &discord_bot_client - .plugin_registrations - .read() - .await - .discord_events - .thread_members_update - { - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadMembersUpdate( - sonic_rs::to_vec(&thread_members_update).unwrap(), - ), - ), - )) - .await; - } + Self::handle_basic_event(core_tx, "THREAD_MEMBERS_UPDATE", thread_members_update); } Event::ThreadUpdate(thread_update) => { - for plugin in &discord_bot_client - .plugin_registrations - .read() - .await - .discord_events - .thread_update - { - let _ = discord_bot_client - .runtime_tx - .send(RuntimeMessages::Discord( - RuntimeMessagesDiscord::CallDiscordEvent( - plugin.clone(), - DiscordEvents::ThreadUpdate( - sonic_rs::to_vec(&thread_update).unwrap(), - ), - ), - )) - .await; - } + Self::handle_basic_event(core_tx, "THREAD_UPDATE", thread_update); } _ => debug!( - "Received an unhandled event: {}", + "Received unsupported event: {}", &event.kind().name().unwrap_or("[No event kind name]") ), } } + + pub async fn handle_basic_event( + core_tx: Arc>, + key: &str, + data: D, + ) where + D: Serialize, + { + let (sender, receiver) = channel(); + + core_tx.send(CoreMessages::DatabaseModule(DatabaseMessages::Get( + Keyspaces::DiscordEvents, + key.as_bytes().to_vec(), + sender, + ))); + + let Some(response_bytes) = receiver.await.unwrap().unwrap() else { + return; + }; + + let plugin_ids_bytes = sonic_rs::from_slice::>>(&response_bytes).unwrap(); + + for plugin_id_bytes in plugin_ids_bytes { + let plugin_id = Uuid::from_slice(&plugin_id_bytes).unwrap(); + + core_tx.send(CoreMessages::Runtime(RuntimeMessages::Discord( + RuntimeMessagesDiscord::CallDiscordEvent( + plugin_id, + DiscordEvents::MessageCreate(sonic_rs::to_vec(&data).unwrap()), + ), + ))); + } + } } diff --git a/src/discord/interactions.rs b/src/discord/interactions.rs index dfd8589..c97c6d2 100644 --- a/src/discord/interactions.rs +++ b/src/discord/interactions.rs @@ -4,6 +4,8 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::Result; +use fjall::Slice; +use tokio::sync::{mpsc::UnboundedSender, oneshot::channel}; use tracing::{error, info}; use twilight_http::{Client, request::Request, routing::Route}; use twilight_model::{ @@ -14,13 +16,37 @@ use twilight_model::{ }, }; -use crate::discord::DiscordBotClient; +use crate::{ + database::Keyspaces, + discord::DiscordBotClient, + utils::channels::{CoreMessages, DatabaseMessages}, +}; impl DiscordBotClient { pub async fn application_command_registrations( http_client: Arc, - discord_application_command_registration_request: Vec>, - ) -> Result<(Vec, Vec)> { + core_tx: Arc>, + ) -> Result<(), ()> { + let (sender, receiver) = channel(); + + // NOTE: To be replaced by a get all KEYS + core_tx.send(CoreMessages::DatabaseModule(DatabaseMessages::GetAll( + Keyspaces::DiscordApplicationCommands, + sender, + ))); + + let responses: Vec = receiver.await.unwrap().unwrap(); + + for response_bytes in responses { + // NOTE: + // - Should this succeed and be tested beforehand? + // - No, as it can fail later anyways. + // - Should the plugin get called into to let it know its registration result? + // - Only reasonable solution I can think of. + // - Can happen from here. + let command: Command = sonic_rs::from_slice(&response_bytes).unwrap(); + } + let mut discord_commands = HashMap::new(); let mut commands = HashMap::new(); diff --git a/src/registry/plugins.rs b/src/registry/plugins.rs index 4b109cb..dfd29fe 100644 --- a/src/registry/plugins.rs +++ b/src/registry/plugins.rs @@ -32,6 +32,6 @@ pub struct AvailablePlugin { pub user_id: String, pub version: Version, pub permissions: ConfigPluginPermissions, - pub environment: Option>, - pub settings: Option, + pub environment: HashMap, + pub settings: Value, } diff --git a/src/runtime.rs b/src/runtime.rs index 16bb70c..d369ac2 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -8,7 +8,6 @@ pub mod plugins; use std::{collections::HashMap, fs, path::Path}; use anyhow::Result; -use sonic_rs::Value; use tokio::{ sync::{ Mutex, RwLock, @@ -89,6 +88,9 @@ impl Runtime { info!("Initializing the plugins"); for (plugin_id, plugin) in available_plugins { + let plugin_user_id = plugin.user_id.clone(); + let plugin_settings = plugin.settings.clone(); + let plugin_directory = plugin_directory .join(&plugin.registry_id) .join(&plugin.id) @@ -99,7 +101,7 @@ impl Runtime { Err(err) => { error!( "An error occured while reading the {} plugin file: {err}", - plugin.user_id + plugin_user_id ); continue; } @@ -110,15 +112,14 @@ impl Runtime { Err(err) => { error!( "An error occured while creating a WASI component from the {} plugin: {err}", - plugin.user_id + plugin_user_id ); continue; } }; - let env_hm = plugin.environment.unwrap_or(HashMap::new()); - - let env: Box<[(&str, &str)]> = env_hm + let env: Box<[(&str, &str)]> = plugin + .environment .iter() .map(|(k, v)| (k.as_str(), v.as_str())) .collect(); @@ -130,14 +131,14 @@ impl Runtime { if !exists && let Err(err) = fs::create_dir(&workspace_plugin_dir) { error!( "Something went wrong while creating the workspace directory for the {} plugin, error: {err}", - plugin.user_id + plugin_user_id ); } } Err(err) => { error!( "Something went wrong while checking if the workspace directory of the {} plugin exists, error: {err}", - plugin.user_id + plugin_user_id ); return Err(()); } @@ -153,6 +154,7 @@ impl Runtime { &plugin_builder.engine, InternalRuntime::new( plugin_id, + plugin, wasi, WasiHttpCtx::new(), ResourceTable::new(), @@ -168,7 +170,7 @@ impl Runtime { Err(err) => { error!( "Failed to instantiate the {} plugin, error: {err}", - plugin.user_id + plugin_user_id ); continue; } @@ -176,17 +178,14 @@ impl Runtime { match instance .wbps_plugin_core_export_functions() - .call_initialization( - &mut store, - &sonic_rs::to_vec(&plugin.settings.unwrap_or(Value::default())).unwrap(), - ) + .call_initialization(&mut store, &sonic_rs::to_vec(&plugin_settings).unwrap()) .await { Ok(init_result) => { if let Err(err) = init_result { error!( "the {} plugin returned an error while intiializing: {err}", - plugin.user_id + plugin_user_id ); continue; } @@ -194,7 +193,7 @@ impl Runtime { Err(err) => { error!( "The {} plugin exprienced a critical error: {err}", - plugin.user_id + plugin_user_id ); continue; } diff --git a/src/runtime/internal.rs b/src/runtime/internal.rs index a0e4dab..e30677f 100644 --- a/src/runtime/internal.rs +++ b/src/runtime/internal.rs @@ -10,10 +10,11 @@ mod core; mod discord; mod job_scheduler; -use crate::utils::channels::CoreMessages; +use crate::{registry::plugins::AvailablePlugin, utils::channels::CoreMessages}; pub struct InternalRuntime { plugin_id: Uuid, + plugin_metadata: AvailablePlugin, wasi: WasiCtx, wasi_http: WasiHttpCtx, table: ResourceTable, @@ -42,6 +43,7 @@ impl WasiHttpView for InternalRuntime { impl InternalRuntime { pub fn new( plugin_id: Uuid, + plugin_metadata: AvailablePlugin, wasi: WasiCtx, wasi_http: WasiHttpCtx, table: ResourceTable, @@ -49,6 +51,7 @@ impl InternalRuntime { ) -> Self { InternalRuntime { plugin_id, + plugin_metadata, wasi, wasi_http, table, diff --git a/src/runtime/internal/core.rs b/src/runtime/internal/core.rs index 566e61b..8296819 100644 --- a/src/runtime/internal/core.rs +++ b/src/runtime/internal/core.rs @@ -32,7 +32,7 @@ impl CoreImportFunctionsHost for InternalRuntime { let (sender, receiver) = channel(); self.core_tx - .send(CoreMessages::DatabaseModule(DatabaseMessages::GetState( + .send(CoreMessages::DatabaseModule(DatabaseMessages::Get( Keyspaces::Plugins, self.plugin_id.as_bytes().to_vec(), sender, @@ -60,7 +60,7 @@ impl CoreImportFunctionsHost for InternalRuntime { let key = format!("{}-{dependency_function}", self.plugin_id.to_string()); self.core_tx - .send(CoreMessages::DatabaseModule(DatabaseMessages::InsertState( + .send(CoreMessages::DatabaseModule(DatabaseMessages::Insert( Keyspaces::DependencyFunctions, key.as_bytes().to_vec(), Vec::new(), @@ -94,7 +94,7 @@ impl CoreImportFunctionsHost for InternalRuntime { let (sender, receiver) = channel(); self.core_tx - .send(CoreMessages::DatabaseModule(DatabaseMessages::GetState( + .send(CoreMessages::DatabaseModule(DatabaseMessages::Get( Keyspaces::Plugins, self.plugin_id.as_bytes().to_vec(), sender, diff --git a/src/services/discord.rs b/src/services/discord.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/services/job_scheduler.rs b/src/services/job_scheduler.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/channels.rs b/src/utils/channels.rs index 80416d7..ed069e8 100644 --- a/src/utils/channels.rs +++ b/src/utils/channels.rs @@ -30,10 +30,12 @@ pub enum CoreMessages { } pub enum DatabaseMessages { - GetState(Keyspaces, Vec, OSSender>>), - InsertState(Keyspaces, Vec, Vec, OSSender>), - DeleteState(Keyspaces, Vec, OSSender>), + Get(Keyspaces, Vec, OSSender>>), + GetAll(Keyspaces, OSSender>>), + Insert(Keyspaces, Vec, Vec, OSSender>), + Remove(Keyspaces, Vec, OSSender>), ContainsKey(Keyspaces, Vec, OSSender>), + Clear(Keyspaces, OSSender>), } pub enum JobSchedulerMessages { @@ -42,7 +44,7 @@ pub enum JobSchedulerMessages { } pub enum DiscordBotClientMessages { - RegisterApplicationCommands(Vec>, OSSender, Vec)>>), + RegisterApplicationCommands, Request(DiscordRequests, OSSender>>), } From 26da38658c5d174043b18b1460ed14e94c78c8ae Mon Sep 17 00:00:00 2001 From: Eduard Smet Date: Mon, 6 Apr 2026 21:10:57 +0200 Subject: [PATCH 4/5] WIP: feat: Plugin API tweaks --- src/discord.rs | 167 --------------------- src/job_scheduler.rs | 95 ------------ src/main.rs | 6 +- src/runtime/internal/core.rs | 20 ++- src/services.rs | 2 + src/services/discord.rs | 167 +++++++++++++++++++++ src/{ => services}/discord/events.rs | 2 +- src/{ => services}/discord/interactions.rs | 2 +- src/{ => services}/discord/requests.rs | 2 +- src/services/job_scheduler.rs | 95 ++++++++++++ wit/core.wit | 5 +- wit/discord.wit | 89 ++++++++--- 12 files changed, 354 insertions(+), 298 deletions(-) delete mode 100644 src/discord.rs delete mode 100644 src/job_scheduler.rs create mode 100644 src/services.rs rename src/{ => services}/discord/events.rs (99%) rename src/{ => services}/discord/interactions.rs (99%) rename src/{ => services}/discord/requests.rs (99%) diff --git a/src/discord.rs b/src/discord.rs deleted file mode 100644 index 3daf719..0000000 --- a/src/discord.rs +++ /dev/null @@ -1,167 +0,0 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ -/* Copyright © 2026 Eduard Smet */ - -use std::sync::Arc; - -use tokio::{ - sync::mpsc::{UnboundedReceiver, UnboundedSender}, - task::JoinHandle, -}; -use tracing::{error, info}; -use twilight_cache_inmemory::{DefaultInMemoryCache, InMemoryCache}; -use twilight_gateway::{ - CloseFrame, Config, EventType, EventTypeFlags, Intents, MessageSender, Shard, StreamExt, -}; -use twilight_http::Client; - -use crate::{ - SHUTDOWN, - utils::channels::{CoreMessages, DiscordBotClientMessages}, -}; - -mod events; -mod interactions; -mod requests; - -pub struct DiscordBotClient { - http_client: Arc, - shards: Vec, - shard_message_senders: Arc>, - cache: Arc, - core_tx: Arc>, - rx: UnboundedReceiver, -} - -impl DiscordBotClient { - pub async fn new( - token: String, - core_tx: UnboundedSender, - rx: UnboundedReceiver, - ) -> Result { - info!("Creating the Discord bot client"); - - let intents = Intents::all(); // TODO: Make this configurable - - rustls::crypto::aws_lc_rs::default_provider() - .install_default() - .unwrap(); - - let http_client = Client::new(token.clone()); - - let config = Config::new(token, intents); - - let (shards, shard_message_senders) = match twilight_gateway::create_recommended( - &http_client, - config, - |_, builder| builder.build(), - ) - .await - { - Ok(shard_iterator) => Self::shard_message_senders(Box::new(shard_iterator)), - Err(err) => { - error!( - "Something went wrong while getting the recommended amount of shards from Discord, error: {}", - &err - ); - return Err(()); - } - }; - - let cache = Arc::new(DefaultInMemoryCache::default()); // TODO: Make this configurable - - Ok(DiscordBotClient { - http_client: Arc::new(http_client), - shards, - shard_message_senders: Arc::new(shard_message_senders), - cache, - core_tx: Arc::new(core_tx), - rx, - }) - } - - pub fn start(mut self) -> JoinHandle<()> { - let mut tasks = Vec::with_capacity(self.shards.len()); - - for shard in self.shards.drain(..) { - tasks.push(tokio::spawn(Self::shard_runner( - self.cache.clone(), - self.core_tx.clone(), - shard, - ))); - } - - tokio::spawn(async move { - while let Some(message) = self.rx.recv().await { - match message { - DiscordBotClientMessages::RegisterApplicationCommands => { - tokio::spawn(Self::application_command_registrations( - self.http_client.clone(), - self.core_tx.clone(), - )); - } - DiscordBotClientMessages::Request(request, response_sender) => { - let http_client = self.http_client.clone(); - let shard_message_senders = self.shard_message_senders.clone(); - - tokio::spawn(async { - response_sender.send( - Self::request(http_client, shard_message_senders, request).await, - ); - }); - } - } - } - - self.shutdown(tasks); - }) - } - - async fn shard_runner( - cache: Arc, - core_tx: Arc>, - mut shard: Shard, - ) { - while let Some(item) = shard.next_event(EventTypeFlags::all()).await { - let Ok(event) = item else { - error!( - "Something went wrong while receiving the next gateway event: {}", - item.as_ref().unwrap_err() - ); - - continue; - }; - - if event.kind() == EventType::GatewayClose && SHUTDOWN.read().await.is_some() { - break; - } - - cache.update(&event); - - tokio::spawn(Self::handle_event(core_tx.clone(), event)); - } - } - - fn shard_message_senders( - shard_iterator: Box>, - ) -> (Vec, Vec) { - let mut shards = vec![]; - let mut shard_message_senders = vec![]; - - for shard in shard_iterator { - shard_message_senders.push(shard.sender()); - shards.push(shard); - } - - (shards, shard_message_senders) - } - - async fn shutdown(&self, mut tasks: Vec>) { - for shard_message_sender in self.shard_message_senders.iter() { - _ = shard_message_sender.close(CloseFrame::NORMAL); - } - - for task in tasks.drain(..) { - let _ = task.await; - } - } -} diff --git a/src/job_scheduler.rs b/src/job_scheduler.rs deleted file mode 100644 index 2c04ea0..0000000 --- a/src/job_scheduler.rs +++ /dev/null @@ -1,95 +0,0 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ -/* Copyright © 2026 Eduard Smet */ - -use anyhow::Result; -use tokio::{ - sync::mpsc::{UnboundedReceiver, UnboundedSender}, - task::JoinHandle, -}; -use tokio_cron_scheduler::{Job, JobScheduler as TokioCronScheduler}; -use tracing::info; -use uuid::Uuid; - -use crate::utils::channels::{ - CoreMessages, JobSchedulerMessages, RuntimeMessages, RuntimeMessagesJobScheduler, -}; - -pub struct JobScheduler { - tokio_cron_scheduler: TokioCronScheduler, - core_tx: UnboundedSender, - rx: UnboundedReceiver, -} - -impl JobScheduler { - pub async fn new( - core_tx: UnboundedSender, - rx: UnboundedReceiver, - ) -> Result { - info!("Creating the job scheduler"); - - Ok(JobScheduler { - tokio_cron_scheduler: TokioCronScheduler::new().await?, - core_tx, - rx, - }) - } - - pub async fn start(mut self) -> Result> { - self.tokio_cron_scheduler.start().await?; - - Ok(tokio::spawn(async move { - while let Some(message) = self.rx.recv().await { - match message { - JobSchedulerMessages::AddJob(plugin_id, cron, result) => { - let tokio_cron_scheduler = self.tokio_cron_scheduler.clone(); - let core_tx = self.core_tx.clone(); - - tokio::spawn(async move { - result.send( - Self::add_job(tokio_cron_scheduler, core_tx, plugin_id, cron).await, - ); - }); - } - JobSchedulerMessages::RemoveJob(uuid, result) => { - let tokio_cron_scheduler = self.tokio_cron_scheduler.clone(); - - tokio::spawn(async move { - result.send(Self::remove_job(tokio_cron_scheduler, uuid).await); - }); - } - } - } - - let _ = self.tokio_cron_scheduler.shutdown().await; - })) - } - - async fn add_job( - tokio_cron_scheduler: TokioCronScheduler, - core_tx: UnboundedSender, - plugin_id: Uuid, - cron: String, - ) -> Result { - info!( - "Scheduled Job at {cron} cron from the {plugin_id} plugin requested to be registered" - ); - - let job = Job::new_async_tz(cron.clone(), chrono::Local, move |job_id, _lock| { - let core_tx = core_tx.clone(); - - Box::pin(async move { - let _ = core_tx.send(CoreMessages::Runtime(RuntimeMessages::JobScheduler( - RuntimeMessagesJobScheduler::CallScheduledJob(plugin_id, job_id), - ))); - }) - })?; - - Ok(tokio_cron_scheduler.add(job).await?) - } - - async fn remove_job(tokio_cron_scheduler: TokioCronScheduler, uuid: Uuid) -> Result<()> { - info!("Removing scheduled Job {uuid}"); - - Ok(tokio_cron_scheduler.remove(&uuid).await?) - } -} diff --git a/src/main.rs b/src/main.rs index be0e24e..91fdac7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,17 +22,15 @@ use tracing_appender::non_blocking::WorkerGuard; mod cli; mod config; mod database; -mod discord; mod http; -mod job_scheduler; mod registry; mod runtime; +mod services; mod utils; use cli::{Cli, CliLogParameters}; use config::Config; -use discord::DiscordBotClient; -use job_scheduler::JobScheduler; +use services::{discord::DiscordBotClient, job_scheduler::JobScheduler}; use utils::{channels::Channels, env::Secrets}; use crate::{ diff --git a/src/runtime/internal/core.rs b/src/runtime/internal/core.rs index 8296819..0dba71f 100644 --- a/src/runtime/internal/core.rs +++ b/src/runtime/internal/core.rs @@ -28,6 +28,16 @@ impl CoreImportTypesHost for InternalRuntime {} impl CoreExportTypesHost for InternalRuntime {} impl CoreImportFunctionsHost for InternalRuntime { + async fn log(&mut self, level: LogLevels, message: String) { + match level { + LogLevels::Trace => trace!(message), + LogLevels::Debug => debug!(message), + LogLevels::Info => info!(message), + LogLevels::Warn => warn!(message), + LogLevels::Error => error!(message), + } + } + async fn get_supported_registrations(&mut self) -> SupportedCoreRegistrations { let (sender, receiver) = channel(); @@ -80,14 +90,8 @@ impl CoreImportFunctionsHost for InternalRuntime { result } - async fn log(&mut self, level: LogLevels, message: String) { - match level { - LogLevels::Trace => trace!(message), - LogLevels::Debug => debug!(message), - LogLevels::Info => info!(message), - LogLevels::Warn => warn!(message), - LogLevels::Error => error!(message), - } + async fn unload(&mut self, reason: String) { + todo!() } async fn shutdown(&mut self, restart: bool) -> Result<(), Error> { diff --git a/src/services.rs b/src/services.rs new file mode 100644 index 0000000..d3b973d --- /dev/null +++ b/src/services.rs @@ -0,0 +1,2 @@ +pub mod discord; +pub mod job_scheduler; diff --git a/src/services/discord.rs b/src/services/discord.rs index e69de29..3daf719 100644 --- a/src/services/discord.rs +++ b/src/services/discord.rs @@ -0,0 +1,167 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2026 Eduard Smet */ + +use std::sync::Arc; + +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinHandle, +}; +use tracing::{error, info}; +use twilight_cache_inmemory::{DefaultInMemoryCache, InMemoryCache}; +use twilight_gateway::{ + CloseFrame, Config, EventType, EventTypeFlags, Intents, MessageSender, Shard, StreamExt, +}; +use twilight_http::Client; + +use crate::{ + SHUTDOWN, + utils::channels::{CoreMessages, DiscordBotClientMessages}, +}; + +mod events; +mod interactions; +mod requests; + +pub struct DiscordBotClient { + http_client: Arc, + shards: Vec, + shard_message_senders: Arc>, + cache: Arc, + core_tx: Arc>, + rx: UnboundedReceiver, +} + +impl DiscordBotClient { + pub async fn new( + token: String, + core_tx: UnboundedSender, + rx: UnboundedReceiver, + ) -> Result { + info!("Creating the Discord bot client"); + + let intents = Intents::all(); // TODO: Make this configurable + + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .unwrap(); + + let http_client = Client::new(token.clone()); + + let config = Config::new(token, intents); + + let (shards, shard_message_senders) = match twilight_gateway::create_recommended( + &http_client, + config, + |_, builder| builder.build(), + ) + .await + { + Ok(shard_iterator) => Self::shard_message_senders(Box::new(shard_iterator)), + Err(err) => { + error!( + "Something went wrong while getting the recommended amount of shards from Discord, error: {}", + &err + ); + return Err(()); + } + }; + + let cache = Arc::new(DefaultInMemoryCache::default()); // TODO: Make this configurable + + Ok(DiscordBotClient { + http_client: Arc::new(http_client), + shards, + shard_message_senders: Arc::new(shard_message_senders), + cache, + core_tx: Arc::new(core_tx), + rx, + }) + } + + pub fn start(mut self) -> JoinHandle<()> { + let mut tasks = Vec::with_capacity(self.shards.len()); + + for shard in self.shards.drain(..) { + tasks.push(tokio::spawn(Self::shard_runner( + self.cache.clone(), + self.core_tx.clone(), + shard, + ))); + } + + tokio::spawn(async move { + while let Some(message) = self.rx.recv().await { + match message { + DiscordBotClientMessages::RegisterApplicationCommands => { + tokio::spawn(Self::application_command_registrations( + self.http_client.clone(), + self.core_tx.clone(), + )); + } + DiscordBotClientMessages::Request(request, response_sender) => { + let http_client = self.http_client.clone(); + let shard_message_senders = self.shard_message_senders.clone(); + + tokio::spawn(async { + response_sender.send( + Self::request(http_client, shard_message_senders, request).await, + ); + }); + } + } + } + + self.shutdown(tasks); + }) + } + + async fn shard_runner( + cache: Arc, + core_tx: Arc>, + mut shard: Shard, + ) { + while let Some(item) = shard.next_event(EventTypeFlags::all()).await { + let Ok(event) = item else { + error!( + "Something went wrong while receiving the next gateway event: {}", + item.as_ref().unwrap_err() + ); + + continue; + }; + + if event.kind() == EventType::GatewayClose && SHUTDOWN.read().await.is_some() { + break; + } + + cache.update(&event); + + tokio::spawn(Self::handle_event(core_tx.clone(), event)); + } + } + + fn shard_message_senders( + shard_iterator: Box>, + ) -> (Vec, Vec) { + let mut shards = vec![]; + let mut shard_message_senders = vec![]; + + for shard in shard_iterator { + shard_message_senders.push(shard.sender()); + shards.push(shard); + } + + (shards, shard_message_senders) + } + + async fn shutdown(&self, mut tasks: Vec>) { + for shard_message_sender in self.shard_message_senders.iter() { + _ = shard_message_sender.close(CloseFrame::NORMAL); + } + + for task in tasks.drain(..) { + let _ = task.await; + } + } +} diff --git a/src/discord/events.rs b/src/services/discord/events.rs similarity index 99% rename from src/discord/events.rs rename to src/services/discord/events.rs index af10850..37d9f0b 100644 --- a/src/discord/events.rs +++ b/src/services/discord/events.rs @@ -12,8 +12,8 @@ use uuid::Uuid; use crate::{ database::Keyspaces, - discord::DiscordBotClient, runtime::plugins::exports::wbps::plugin::discord_export_functions::DiscordEvents, + services::discord::DiscordBotClient, utils::channels::{CoreMessages, DatabaseMessages, RuntimeMessages, RuntimeMessagesDiscord}, }; diff --git a/src/discord/interactions.rs b/src/services/discord/interactions.rs similarity index 99% rename from src/discord/interactions.rs rename to src/services/discord/interactions.rs index c97c6d2..4a1b294 100644 --- a/src/discord/interactions.rs +++ b/src/services/discord/interactions.rs @@ -18,7 +18,7 @@ use twilight_model::{ use crate::{ database::Keyspaces, - discord::DiscordBotClient, + services::discord::DiscordBotClient, utils::channels::{CoreMessages, DatabaseMessages}, }; diff --git a/src/discord/requests.rs b/src/services/discord/requests.rs similarity index 99% rename from src/discord/requests.rs rename to src/services/discord/requests.rs index be77bf5..bf2f178 100644 --- a/src/discord/requests.rs +++ b/src/services/discord/requests.rs @@ -16,10 +16,10 @@ use twilight_model::gateway::{ }; use crate::{ - discord::DiscordBotClient, runtime::plugins::wbps::plugin::discord_import_types::{ Body, DiscordRequests, DiscordResponses, }, + services::discord::DiscordBotClient, }; impl DiscordBotClient { diff --git a/src/services/job_scheduler.rs b/src/services/job_scheduler.rs index e69de29..2c04ea0 100644 --- a/src/services/job_scheduler.rs +++ b/src/services/job_scheduler.rs @@ -0,0 +1,95 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2026 Eduard Smet */ + +use anyhow::Result; +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinHandle, +}; +use tokio_cron_scheduler::{Job, JobScheduler as TokioCronScheduler}; +use tracing::info; +use uuid::Uuid; + +use crate::utils::channels::{ + CoreMessages, JobSchedulerMessages, RuntimeMessages, RuntimeMessagesJobScheduler, +}; + +pub struct JobScheduler { + tokio_cron_scheduler: TokioCronScheduler, + core_tx: UnboundedSender, + rx: UnboundedReceiver, +} + +impl JobScheduler { + pub async fn new( + core_tx: UnboundedSender, + rx: UnboundedReceiver, + ) -> Result { + info!("Creating the job scheduler"); + + Ok(JobScheduler { + tokio_cron_scheduler: TokioCronScheduler::new().await?, + core_tx, + rx, + }) + } + + pub async fn start(mut self) -> Result> { + self.tokio_cron_scheduler.start().await?; + + Ok(tokio::spawn(async move { + while let Some(message) = self.rx.recv().await { + match message { + JobSchedulerMessages::AddJob(plugin_id, cron, result) => { + let tokio_cron_scheduler = self.tokio_cron_scheduler.clone(); + let core_tx = self.core_tx.clone(); + + tokio::spawn(async move { + result.send( + Self::add_job(tokio_cron_scheduler, core_tx, plugin_id, cron).await, + ); + }); + } + JobSchedulerMessages::RemoveJob(uuid, result) => { + let tokio_cron_scheduler = self.tokio_cron_scheduler.clone(); + + tokio::spawn(async move { + result.send(Self::remove_job(tokio_cron_scheduler, uuid).await); + }); + } + } + } + + let _ = self.tokio_cron_scheduler.shutdown().await; + })) + } + + async fn add_job( + tokio_cron_scheduler: TokioCronScheduler, + core_tx: UnboundedSender, + plugin_id: Uuid, + cron: String, + ) -> Result { + info!( + "Scheduled Job at {cron} cron from the {plugin_id} plugin requested to be registered" + ); + + let job = Job::new_async_tz(cron.clone(), chrono::Local, move |job_id, _lock| { + let core_tx = core_tx.clone(); + + Box::pin(async move { + let _ = core_tx.send(CoreMessages::Runtime(RuntimeMessages::JobScheduler( + RuntimeMessagesJobScheduler::CallScheduledJob(plugin_id, job_id), + ))); + }) + })?; + + Ok(tokio_cron_scheduler.add(job).await?) + } + + async fn remove_job(tokio_cron_scheduler: TokioCronScheduler, uuid: Uuid) -> Result<()> { + info!("Removing scheduled Job {uuid}"); + + Ok(tokio_cron_scheduler.remove(&uuid).await?) + } +} diff --git a/wit/core.wit b/wit/core.wit index 259ad91..41c51f8 100644 --- a/wit/core.wit +++ b/wit/core.wit @@ -38,9 +38,12 @@ interface core-export-types { interface core-import-functions { use core-import-types.{error, log-levels, core-registrations, core-registrations-result, supported-core-registrations}; + log: func(level: log-levels, message: string); + get-supported-registrations: func() -> supported-core-registrations; register: func(registrations: core-registrations) -> core-registrations-result; - log: func(level: log-levels, message: string); + + unload: func(reason: string); shutdown: func(restart: bool) -> result<_, error>; diff --git a/wit/discord.wit b/wit/discord.wit index 6ea6873..893d9c1 100644 --- a/wit/discord.wit +++ b/wit/discord.wit @@ -1,8 +1,12 @@ interface discord-import-types { use core-types.{json}; + use core-import-types.{error}; + /// Raw form data for Discord requests with file attachments (e.g. embeds with + /// images). type form = list; + /// All supported Discord gateway events. flags discord-events { message-create, interaction-create, @@ -14,46 +18,64 @@ interface discord-import-types { thread-update, } + /// Discord gateway events the plugin can register to receive via `discord-event`. type supported-discord-registrations = discord-events; - /// All registrations are opt in. + /// Opt-in registration for Discord gateway events and interactions. + /// + /// Pass `none` for fields the plugin does not need. The plugin will only receive + /// what it registered for. record discord-registrations { events: option, interactions: option, } - /// The `application-commands` field its structure can be found in the Discord docs: https://docs.discord.com/developers/interactions/application-commands#application-command-object + /// Interactions to register. + /// + /// `application-commands`: Slash commands and context menus. Structure per + /// Discord docs: + /// + /// `message-components`: Button/select menus. Value is the UUID count needed + /// by the plugin. + /// + /// `modals`: Modals. Value is the UUID count needed by the plugin. record discord-registrations-interactions { application-commands: option>, - message-components: option>, - modals: option>, + message-components: option, + modals: option, } + /// Result of `discord-register`. + /// + /// `events`: Returns an error if the plugin tried to register for an event + /// it's not allowed to. + /// + /// `interactions`: Returns an error if the plugin does not have permission + /// to register for the `interaction-create` event. record discord-registrations-result { - events: discord-events, - interactions: discord-registrations-result-interactions, + events: option>, + interactions: option>, } - /// The fields return separate lists of successful and failed IDs. - /// - /// The successful IDs gets mapped to host IDs which the plugin should use and then expect on events. + /// UUIDs for registered message components and modals. record discord-registrations-result-interactions { - application-commands: tuple>, list>, - message-components: tuple>, list>, - modals: tuple>, list>, + message-components: option>, + modals: option>, } - /// Check the Discord Gateway Send Event and HTTP Resource docs for the structures of the variant data: https://discord.com/developers/docs/events/gateway-events#send-events + /// API requests to Discord. /// - /// Message Resource Example: https://discord.com/developers/docs/resources/message + /// Variant data follows Discord HTTP and Gateway docs. Examples: + /// - Gateway commands: + /// - HTTP operations: variant discord-requests { - // Shard message sender commands + // Gateway commands request-guild-members(tuple), request-soundboard-sounds(list), update-voice-state(tuple), update-presence(json), - // HTTP requests + // HTTP operations add-thread-member(tuple), create-ban(tuple), create-forum-thread(tuple), @@ -76,18 +98,30 @@ interface discord-import-types { update-interaction-original(tuple), } + /// Request body type: JSON for normal payloads, form-data for file attachments. variant body { json(json), form(form), } + /// API responses from Discord. type discord-responses = json; } interface discord-export-types { use core-types.{json}; + use core-import-types.{error}; + + /// Result of application command registrations. + /// + /// First list: successful registrations as `(command-name, snowflake)`. + /// Second list: failed registrations as `(command-name, error-message)`. + type discord-registrations-result-application-commands = tuple>, list>>; - /// Check the Discord Gateway Event docs for the structures of the variant data: https://discord.com/developers/docs/events/gateway-events + /// Gateway events received via `discord-event`. + /// + /// Payload is raw JSON from Discord. See: + /// variant discord-events { interaction-create(json), message-create(json), @@ -104,18 +138,33 @@ interface discord-import-functions { use core-import-types.{error}; use discord-import-types.{discord-registrations, discord-registrations-result, discord-requests, discord-responses, supported-discord-registrations}; + /// Returns the Discord events and interaction types the plugin can register for. get-supported-discord-registrations: func() -> supported-discord-registrations; - /// Application Command registrations past the initialization phase are ignored. + + /// Registers for Discord events and interactions. /// - /// See the following Discord docs for more information: https://docs.discord.com/developers/interactions/application-commands#registering-a-command + /// Must be called during initialization. Application commands registered after + /// initialization are ignored by Discord. + /// discord-registrations-result; + /// Makes a request to Discord API via the host. + /// + /// Returns `none` on success (Discord returns 204 No Content). + /// Returns `some(json)` on success with a response body. discord-request: func(request: discord-requests) -> result, error>; } interface discord-export-functions { use core-export-types.{error}; - use discord-export-types.{discord-events}; + use discord-export-types.{discord-events, discord-registrations-result-application-commands}; + + /// Called when application command registration completes. + /// + /// The host doesn't know the outcome of its own register call, so it passes + /// the result here. Use this to log or handle failures. + discord-application-commands: func(registrations-result: discord-registrations-result-application-commands) -> result<_, error>; + /// Called when a registered Discord event occurs. discord-event: func(event: discord-events) -> result<_, error>; } From c07958a5f28af2f870f722f761c981df757092f0 Mon Sep 17 00:00:00 2001 From: Eduard Smet Date: Sun, 12 Apr 2026 01:46:21 +0200 Subject: [PATCH 5/5] WIP: feat: Further plugin API tweaks and integration --- Cargo.lock | 68 +++++++-------- Cargo.toml | 4 +- src/config/plugins.rs | 4 +- src/config/plugins/permissions.rs | 34 +++++--- src/database.rs | 15 +++- src/main.rs | 4 +- src/registry/plugins.rs | 4 +- src/runtime.rs | 119 +++++++++++++++++++++---- src/runtime/internal/core.rs | 64 +++++++++++--- src/runtime/internal/discord.rs | 121 +++++++++++++++++++++++--- src/runtime/plugins.rs | 139 +++++++++++++++++++++++++----- src/services/discord/requests.rs | 114 ++++++++++++------------ src/utils/channels.rs | 17 +++- wit/core.wit | 2 +- wit/discord.wit | 105 ++++++++++++---------- 15 files changed, 592 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b4d4ab..998fc48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4302,40 +4302,6 @@ dependencies = [ "wast 244.0.0", ] -[[package]] -name = "wbps" -version = "0.1.0" -dependencies = [ - "anyhow", - "bytes", - "chrono", - "clap", - "dotenvy", - "fjall", - "indexmap", - "reqwest", - "rustls 0.23.37", - "semver", - "serde", - "serde_yaml_ng", - "sonic-rs", - "tokio", - "tokio-cron-scheduler", - "tokio-util", - "tracing", - "tracing-appender", - "tracing-subscriber", - "twilight-cache-inmemory", - "twilight-gateway", - "twilight-http", - "twilight-model", - "url", - "uuid", - "wasmtime", - "wasmtime-wasi", - "wasmtime-wasi-http", -] - [[package]] name = "web-sys" version = "0.3.85" @@ -4991,6 +4957,40 @@ dependencies = [ "wast 35.0.2", ] +[[package]] +name = "wpbs" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "clap", + "dotenvy", + "fjall", + "indexmap", + "reqwest", + "rustls 0.23.37", + "semver", + "serde", + "serde_yaml_ng", + "sonic-rs", + "tokio", + "tokio-cron-scheduler", + "tokio-util", + "tracing", + "tracing-appender", + "tracing-subscriber", + "twilight-cache-inmemory", + "twilight-gateway", + "twilight-http", + "twilight-model", + "url", + "uuid", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-http", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 216e5c6..983e367 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "wbps" -description = "WASM based plugin services." +name = "wpbs" +description = "WASM plugin based services." version = "0.1.0" authors = ["Eduard Smet "] license = "GPL-3.0-or-later" diff --git a/src/config/plugins.rs b/src/config/plugins.rs index d0952c2..fda9f2f 100644 --- a/src/config/plugins.rs +++ b/src/config/plugins.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use serde::Deserialize; use sonic_rs::Value; -use crate::config::plugins::permissions::ConfigPluginPermissions; +use crate::config::plugins::permissions::PluginPermissions; pub mod permissions; @@ -11,7 +11,7 @@ pub mod permissions; pub struct ConfigPlugin { pub plugin: String, pub cache: Option, - pub permissions: ConfigPluginPermissions, + pub permissions: PluginPermissions, #[serde(default)] pub environment: HashMap, #[serde(default)] diff --git a/src/config/plugins/permissions.rs b/src/config/plugins/permissions.rs index ceaa72b..b123f17 100644 --- a/src/config/plugins/permissions.rs +++ b/src/config/plugins/permissions.rs @@ -1,31 +1,37 @@ use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] -pub struct ConfigPluginPermissions { +pub struct PluginPermissions { #[serde(default)] - pub core: Vec, + pub core: Vec, #[serde(default)] - pub job_scheduler: Vec, + pub job_scheduler: Vec, #[serde(default)] - pub discord: Vec, + pub discord: PluginPermissionsDiscord, } -#[derive(Deserialize, PartialEq, Serialize)] +#[derive(Default, Deserialize, Serialize)] +pub struct PluginPermissionsDiscord { + pub events: Vec, + pub interactions: Vec, +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(untagged)] -pub enum ConfigSupportedCoreRegistrations { +pub enum PluginPermissionsCore { DependencyFunctions, Shutdown, } -#[derive(Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(untagged)] -pub enum ConfigSupportedJobSchedulerRegistrations { +pub enum PluginPermissionsJobScheduler { ScheduledJobs, } -#[derive(Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(untagged)] -pub enum ConfigSupportedDiscordRegistrations { +pub enum PluginPermissionsDiscordEvents { MessageCreate, InteractionCreate, ThreadCreate, @@ -35,3 +41,11 @@ pub enum ConfigSupportedDiscordRegistrations { ThreadMembersUpdate, ThreadUpdate, } + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum PluginPermissionsDiscordInteractions { + ApplicationCommands, + MessageComponents, + Modals, +} diff --git a/src/database.rs b/src/database.rs index fdaa10e..5dfdf48 100644 --- a/src/database.rs +++ b/src/database.rs @@ -39,8 +39,11 @@ pub fn handle_action(database: Database, message: DatabaseMessages) { DatabaseMessages::Get(keyspace, key, response_sender) => { response_sender.send(get(database, keyspace, key)); } - DatabaseMessages::GetAll(keyspace, response_sender) => { - response_sender.send(get_all(database, keyspace)); + DatabaseMessages::GetAllKeys(keyspace, response_sender) => { + response_sender.send(get_all_keys(database, keyspace)); + } + DatabaseMessages::GetAllValues(keyspace, response_sender) => { + response_sender.send(get_all_values(database, keyspace)); } DatabaseMessages::Insert(keyspace, key, value, response_sender) => { response_sender.send(insert(database, keyspace, key, value)); @@ -74,7 +77,13 @@ where Ok(keyspace.range(range)) } -pub fn get_all(database: Database, keyspace: Keyspaces) -> Result> { +pub fn get_all_keys(database: Database, keyspace: Keyspaces) -> Result> { + Ok(range(database, keyspace, Vec::new()..=Vec::new())? + .map(|g| g.key()) + .collect::, fjall::Error>>()?) +} + +pub fn get_all_values(database: Database, keyspace: Keyspaces) -> Result> { Ok(range(database, keyspace, Vec::new()..=Vec::new())? .map(|g| g.value()) .collect::, fjall::Error>>()?) diff --git a/src/main.rs b/src/main.rs index 91fdac7..c6f3219 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,7 +61,7 @@ async fn main() -> ExitCode { } } - ExitCode::from(0) + ExitCode::from(1) } async fn run() -> Result<(), ()> { @@ -97,7 +97,7 @@ async fn run() -> Result<(), ()> { .await .map_err(|_| ())?; - let runtime = Runtime::new(channels.runtime.rx); + let runtime = Runtime::new(channels.runtime.core_tx.clone(), channels.runtime.rx); tasks.push(job_scheduler.start().await.map_err(|_| ())?); diff --git a/src/registry/plugins.rs b/src/registry/plugins.rs index dfd29fe..e07949b 100644 --- a/src/registry/plugins.rs +++ b/src/registry/plugins.rs @@ -7,7 +7,7 @@ use semver::Version; use serde::Deserialize; use sonic_rs::Value; -use crate::config::plugins::permissions::ConfigPluginPermissions; +use crate::config::plugins::permissions::PluginPermissions; #[derive(Deserialize)] #[allow(unused)] @@ -31,7 +31,7 @@ pub struct AvailablePlugin { pub id: String, pub user_id: String, pub version: Version, - pub permissions: ConfigPluginPermissions, + pub permissions: PluginPermissions, pub environment: HashMap, pub settings: Value, } diff --git a/src/runtime.rs b/src/runtime.rs index d369ac2..bda213b 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -5,13 +5,14 @@ mod builder; mod internal; pub mod plugins; -use std::{collections::HashMap, fs, path::Path}; +use std::{collections::HashMap, fs, path::Path, sync::Arc}; use anyhow::Result; use tokio::{ sync::{ Mutex, RwLock, mpsc::{UnboundedReceiver, UnboundedSender}, + oneshot::Sender, }, task::JoinHandle, }; @@ -26,15 +27,20 @@ use crate::{ runtime::{ builder::PluginBuilder, internal::InternalRuntime, - plugins::{Plugin, wbps::plugin::discord_export_types::DiscordEvents}, + plugins::{ + Plugin, + wbps::plugin::discord_export_types::{DiscordEvents, Error}, + }, }, utils::channels::{ - CoreMessages, RuntimeMessages, RuntimeMessagesDiscord, RuntimeMessagesJobScheduler, + CoreMessages, RuntimeMessages, RuntimeMessagesCore, RuntimeMessagesDiscord, + RuntimeMessagesJobScheduler, }, }; pub struct Runtime { - plugins: RwLock>, + plugins: Arc>>, + core_tx: UnboundedSender, rx: UnboundedReceiver, } @@ -44,11 +50,15 @@ pub struct RuntimePlugin { } impl Runtime { - pub fn new(rx: UnboundedReceiver) -> Self { + pub fn new( + core_tx: UnboundedSender, + rx: UnboundedReceiver, + ) -> Self { info!("Creating the WASI runtime"); Runtime { - plugins: RwLock::new(HashMap::new()), + plugins: Arc::new(RwLock::new(HashMap::new())), + core_tx, rx, } } @@ -57,16 +67,45 @@ impl Runtime { tokio::spawn(async move { while let Some(message) = self.rx.recv().await { match message { + RuntimeMessages::Core(core_message) => match core_message { + RuntimeMessagesCore::CallDependencyFunction( + plugin_id, + function_id, + params, + response_sender, + ) => { + let plugins = self.plugins.clone(); + + tokio::spawn(Self::call_dependency_function( + plugins, + plugin_id, + function_id, + params, + response_sender, + )); + } + RuntimeMessagesCore::UnloadPlugin(plugin) => { + let plugins = self.plugins.clone(); + + tokio::spawn(async move { + plugins.write().await.remove(&plugin); + }); + } + }, RuntimeMessages::JobScheduler(job_scheduler_message) => { match job_scheduler_message { RuntimeMessagesJobScheduler::CallScheduledJob(plugin_id, job_id) => { - self.call_scheduled_job(plugin_id, job_id).await; + let plugins = self.plugins.clone(); + + tokio::spawn(Self::call_scheduled_job(plugins, plugin_id, job_id)); } } } RuntimeMessages::Discord(discord_message) => match discord_message { RuntimeMessagesDiscord::CallDiscordEvent(plugin_id, event) => { - self.call_discord_event(plugin_id, &event).await; + let plugins = self.plugins.clone(); + + tokio::spawn(Self::call_discord_event(plugins, plugin_id, event)); } }, } @@ -212,14 +251,18 @@ impl Runtime { // TODO: Remove trapped plugins - async fn call_discord_event(&self, plugin_id: Uuid, event: &DiscordEvents) { - let plugins = self.plugins.read().await; + async fn call_discord_event( + plugins: Arc>>, + plugin_id: Uuid, + event: DiscordEvents, + ) { + let plugins = plugins.read().await; let plugin = plugins.get(&plugin_id).unwrap(); match plugin .instance .wbps_plugin_discord_export_functions() - .call_discord_event(&mut *plugin.store.lock().await, event) + .call_discord_event(&mut *plugin.store.lock().await, &event) .await { Ok(result) => { @@ -233,14 +276,18 @@ impl Runtime { } } - async fn call_scheduled_job(&self, plugin_id: Uuid, uuid: Uuid) { - let plugins = self.plugins.read().await; + async fn call_scheduled_job( + plugins: Arc>>, + plugin_id: Uuid, + job_id: Uuid, + ) { + let plugins = plugins.read().await; let plugin = plugins.get(&plugin_id).unwrap(); match plugin .instance .wbps_plugin_job_scheduler_export_functions() - .call_scheduled_job(&mut *plugin.store.lock().await, &uuid.to_string()) + .call_scheduled_job(&mut *plugin.store.lock().await, &job_id.to_string()) .await { Ok(result) => { @@ -254,14 +301,43 @@ impl Runtime { } } - async fn call_shutdown(&self, plugin_id: Uuid) { - let plugins = self.plugins.read().await; + async fn call_dependency_function( + plugins: Arc>>, + plugin_id: Uuid, + function_id: String, + params: Vec, + response_sender: Sender, Error>>, + ) { + let plugins = plugins.read().await; let plugin = plugins.get(&plugin_id).unwrap(); match plugin .instance .wbps_plugin_core_export_functions() - .call_shutdown(&mut *plugin.store.lock().await) + .call_dependency_function(&mut *plugin.store.lock().await, &function_id, ¶ms) + .await + { + Ok(result) => { + response_sender.send(result); + } + Err(err) => { + let err = format!("The {plugin_id} plugin exprienced a critical error: {err}"); + + error!(err); + + response_sender.send(Err(err)); + } + }; + } + + async fn call_shutdown( + plugin_id: &Uuid, + instance: &Plugin, + store: &mut Store, + ) { + match instance + .wbps_plugin_core_export_functions() + .call_shutdown(store) .await { Ok(result) => { @@ -275,9 +351,16 @@ impl Runtime { } } - async fn shutdown(&self) { + async fn shutdown(self) { // TODO: Allow all plugin calls to finish and then call the shutdown methods // This will be achieved by closing the plugin call channel tasks which then will call // shutdown one more time before returning + // Bellow code will get replaced with channel closers + + let plugins = &mut *self.plugins.write().await; + + for (plugin_id, plugin) in plugins.into_iter() { + Self::call_shutdown(plugin_id, &plugin.instance, &mut *plugin.store.lock().await).await; + } } } diff --git a/src/runtime/internal/core.rs b/src/runtime/internal/core.rs index 0dba71f..a269a19 100644 --- a/src/runtime/internal/core.rs +++ b/src/runtime/internal/core.rs @@ -3,24 +3,25 @@ use tokio::sync::oneshot::channel; use tracing::{debug, error, info, trace, warn}; +use uuid::Uuid; use crate::{ Shutdown, - config::plugins::permissions::{ConfigPluginPermissions, ConfigSupportedCoreRegistrations}, + config::plugins::permissions::{PluginPermissions, PluginPermissionsCore}, database::Keyspaces, runtime::{ internal::InternalRuntime, plugins::wbps::plugin::{ - core_export_types::Host as CoreExportTypesHost, + core_export_types::{Error, Host as CoreExportTypesHost}, core_import_functions::Host as CoreImportFunctionsHost, core_import_types::{ - CoreRegistrations, CoreRegistrationsResult, Error, Host as CoreImportTypesHost, - LogLevels, SupportedCoreRegistrations, + CoreRegistrations, CoreRegistrationsResult, Host as CoreImportTypesHost, LogLevels, + SupportedCoreRegistrations, }, core_types::Host as CoreTypesHost, }, }, - utils::channels::{CoreMessages, DatabaseMessages}, + utils::channels::{CoreMessages, DatabaseMessages, RuntimeMessages, RuntimeMessagesCore}, }; impl CoreTypesHost for InternalRuntime {} @@ -51,7 +52,7 @@ impl CoreImportFunctionsHost for InternalRuntime { let response_bytes = receiver.await.unwrap().unwrap().unwrap().to_vec(); let plugin_permissions = - sonic_rs::from_slice::(&response_bytes).unwrap(); + sonic_rs::from_slice::(&response_bytes).unwrap(); plugin_permissions.core.into() } @@ -67,7 +68,10 @@ impl CoreImportFunctionsHost for InternalRuntime { for dependency_function in dependency_functions { let (sender, receiver) = channel(); - let key = format!("{}-{dependency_function}", self.plugin_id.to_string()); + let key = format!( + "{}/{}/{dependency_function}", + self.plugin_metadata.registry_id, self.plugin_metadata.id + ); self.core_tx .send(CoreMessages::DatabaseModule(DatabaseMessages::Insert( @@ -91,7 +95,15 @@ impl CoreImportFunctionsHost for InternalRuntime { } async fn unload(&mut self, reason: String) { - todo!() + self.core_tx + .send(CoreMessages::Runtime(RuntimeMessages::Core( + RuntimeMessagesCore::UnloadPlugin(self.plugin_id), + ))); + + info!( + "The {} plugin has unloaded itself, reason: {reason}", + self.plugin_metadata.user_id + ) } async fn shutdown(&mut self, restart: bool) -> Result<(), Error> { @@ -107,11 +119,11 @@ impl CoreImportFunctionsHost for InternalRuntime { let response_bytes = receiver.await.unwrap().unwrap().unwrap().to_vec(); let plugin_permissions = - sonic_rs::from_slice::(&response_bytes).unwrap(); + sonic_rs::from_slice::(&response_bytes).unwrap(); if !plugin_permissions .core - .contains(&ConfigSupportedCoreRegistrations::Shutdown) + .contains(&PluginPermissionsCore::Shutdown) { return Err(Error::from("Not allowed to call shutdown")); } @@ -129,10 +141,38 @@ impl CoreImportFunctionsHost for InternalRuntime { async fn dependency_function( &mut self, - dependency_id: String, + registry_id: String, + plugin_id: String, function_id: String, params: Vec, ) -> Result, Error> { - todo!() + let (sender, receiver) = channel(); + + let key = format!("{registry_id}/{plugin_id}/{function_id}"); + + self.core_tx + .send(CoreMessages::DatabaseModule(DatabaseMessages::Get( + Keyspaces::Plugins, + key.as_bytes().to_vec(), + sender, + ))); + + let Some(response_bytes) = receiver.await.unwrap().unwrap() else { + return Err(format!("The {key} dependency function was not found")); + }; + + let (sender, receiver) = channel(); + + self.core_tx + .send(CoreMessages::Runtime(RuntimeMessages::Core( + RuntimeMessagesCore::CallDependencyFunction( + Uuid::from_slice(&response_bytes).unwrap(), + function_id, + params, + sender, + ), + ))); + + receiver.await.unwrap() } } diff --git a/src/runtime/internal/discord.rs b/src/runtime/internal/discord.rs index 1bfeaa9..975f4fd 100644 --- a/src/runtime/internal/discord.rs +++ b/src/runtime/internal/discord.rs @@ -1,17 +1,25 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ /* Copyright © 2026 Eduard Smet */ -use crate::runtime::{ - internal::InternalRuntime, - plugins::wbps::plugin::{ - core_import_types::Error, - discord_export_types::Host as DiscordExportTypesHost, - discord_import_functions::Host as DiscordImportFunctionsHost, - discord_import_types::{ - DiscordRegistrations, DiscordRegistrationsResult, DiscordRequests, DiscordResponses, - Host as DiscordImportTypesHost, SupportedDiscordRegistrations, +use tokio::sync::oneshot::channel; + +use crate::{ + config::plugins::permissions::{PluginPermissions, PluginPermissionsDiscordEvents}, + database::Keyspaces, + runtime::{ + internal::InternalRuntime, + plugins::wbps::plugin::{ + core_import_types::Error, + discord_export_types::Host as DiscordExportTypesHost, + discord_import_functions::Host as DiscordImportFunctionsHost, + discord_import_types::{ + DiscordRegistrations, DiscordRegistrationsResult, + DiscordRegistrationsResultInteractions, DiscordRequests, DiscordResponses, + Host as DiscordImportTypesHost, SupportedDiscordRegistrations, + }, }, }, + utils::channels::{CoreMessages, DatabaseMessages, DiscordBotClientMessages}, }; impl DiscordImportTypesHost for InternalRuntime {} @@ -19,20 +27,109 @@ impl DiscordExportTypesHost for InternalRuntime {} impl DiscordImportFunctionsHost for InternalRuntime { async fn get_supported_discord_registrations(&mut self) -> SupportedDiscordRegistrations { - todo!() + let (sender, receiver) = channel(); + + self.core_tx + .send(CoreMessages::DatabaseModule(DatabaseMessages::Get( + Keyspaces::Plugins, + self.plugin_id.as_bytes().to_vec(), + sender, + ))); + + let response_bytes = receiver.await.unwrap().unwrap().unwrap().to_vec(); + + let plugin_permissions = + sonic_rs::from_slice::(&response_bytes).unwrap(); + + SupportedDiscordRegistrations { + events: plugin_permissions.discord.events.into(), + interactions: plugin_permissions.discord.interactions.into(), + } } async fn discord_register( &mut self, registrations: DiscordRegistrations, ) -> DiscordRegistrationsResult { - todo!() + let (sender, receiver) = channel(); + + self.core_tx + .send(CoreMessages::DatabaseModule(DatabaseMessages::Get( + Keyspaces::Plugins, + self.plugin_id.as_bytes().to_vec(), + sender, + ))); + + let response_bytes = receiver.await.unwrap().unwrap().unwrap().to_vec(); + + let plugin_permissions = + sonic_rs::from_slice::(&response_bytes).unwrap(); + + let mut result = DiscordRegistrationsResult { + events: None, + interactions: None, + }; + + if let Some(registered_events_flags) = registrations.events { + let registered_events: Vec = + registered_events_flags.into(); + + for registered_event in registered_events { + if !plugin_permissions + .discord + .events + .contains(®istered_event) + { + result.events = Some(Err(format!( + "Plugin is not allowed to register for the {registered_event:?} event" + ))); + break; + } + } + + result.events = Some(Ok(())); + } + + if let Some(interactions) = registrations.interactions { + // TODO: + // Check permissions + + if let Some(application_commands) = interactions.application_commands { + // TODO: + // Store in db + } + + result.interactions = Some(Ok(DiscordRegistrationsResultInteractions { + message_components: None, + modals: None, + })); + + if let Some(message_components) = interactions.message_components { + // TODO: + // Create UUID entry in db + // Add UUID to result + } + + if let Some(modals) = interactions.modals { + // TODO: + // Create UUID entry in db + // Add UUID to result + } + } + + result } async fn discord_request( &mut self, request: DiscordRequests, ) -> Result, Error> { - todo!() + let (sender, receiver) = channel(); + + self.core_tx.send(CoreMessages::DiscordBotClientModule( + DiscordBotClientMessages::Request(request, sender), + )); + + receiver.await.unwrap() } } diff --git a/src/runtime/plugins.rs b/src/runtime/plugins.rs index 117e199..06a92b2 100644 --- a/src/runtime/plugins.rs +++ b/src/runtime/plugins.rs @@ -1,28 +1,28 @@ use crate::{ config::plugins::permissions::{ - ConfigSupportedCoreRegistrations, ConfigSupportedDiscordRegistrations, - ConfigSupportedJobSchedulerRegistrations, + PluginPermissionsCore, PluginPermissionsDiscordEvents, + PluginPermissionsDiscordInteractions, PluginPermissionsJobScheduler, }, runtime::plugins::wbps::plugin::{ core_import_types::SupportedCoreRegistrations, - discord_import_types::SupportedDiscordRegistrations, + discord_import_types::{DiscordEvents, SupportedDiscordRegistrationsInteractions}, job_scheduler_import_types::SupportedJobSchedulerRegistrations, }, }; wasmtime::component::bindgen!({ imports: { default: async }, exports: { default: async } }); -impl From> for SupportedCoreRegistrations { - fn from(config_supported_core_registrations: Vec) -> Self { +impl From> for SupportedCoreRegistrations { + fn from(plugin_permissions_core: Vec) -> Self { let mut supported_core_registrations = Self::empty(); - for registration in &config_supported_core_registrations { - match registration { - ConfigSupportedCoreRegistrations::DependencyFunctions => { - supported_core_registrations &= SupportedCoreRegistrations::DEPENDENCY_FUNCTIONS + for plugin_permission_core in &plugin_permissions_core { + match plugin_permission_core { + PluginPermissionsCore::DependencyFunctions => { + supported_core_registrations &= Self::DEPENDENCY_FUNCTIONS; } - ConfigSupportedCoreRegistrations::Shutdown => { - supported_core_registrations &= SupportedCoreRegistrations::SHUTDOWN + PluginPermissionsCore::Shutdown => { + supported_core_registrations &= Self::SHUTDOWN; } } } @@ -31,30 +31,123 @@ impl From> for SupportedCoreRegistrations } } -impl From> for SupportedJobSchedulerRegistrations { - fn from( - config_supported_job_scheduler_registrations: Vec, - ) -> Self { +impl From> for SupportedJobSchedulerRegistrations { + fn from(plugin_permissions_job_scheduler: Vec) -> Self { let mut supported_job_scheduler_registrations = Self::empty(); - for registration in &config_supported_job_scheduler_registrations { - todo!(); + for plugin_permission_job_scheduler in &plugin_permissions_job_scheduler { + match plugin_permission_job_scheduler { + PluginPermissionsJobScheduler::ScheduledJobs => { + supported_job_scheduler_registrations &= Self::SCHEDULED_JOBS; + } + } } supported_job_scheduler_registrations } } -impl From> for SupportedDiscordRegistrations { +// The `DiscordEvents` flags is retyped several times in the plugin API. +impl From> for DiscordEvents { + fn from(plugin_permissions_discord_events: Vec) -> Self { + let mut supported_discord_registrations_events = Self::empty(); + + for plugin_permission_discord_events in &plugin_permissions_discord_events { + match plugin_permission_discord_events { + PluginPermissionsDiscordEvents::MessageCreate => { + supported_discord_registrations_events &= Self::MESSAGE_CREATE; + } + PluginPermissionsDiscordEvents::InteractionCreate => { + supported_discord_registrations_events &= Self::INTERACTION_CREATE; + } + PluginPermissionsDiscordEvents::ThreadCreate => { + supported_discord_registrations_events &= Self::THREAD_CREATE; + } + PluginPermissionsDiscordEvents::ThreadDelete => { + supported_discord_registrations_events &= Self::THREAD_DELETE; + } + PluginPermissionsDiscordEvents::ThreadListSync => { + supported_discord_registrations_events &= Self::THREAD_LIST_SYNC; + } + PluginPermissionsDiscordEvents::ThreadMemberUpdate => { + supported_discord_registrations_events &= Self::THREAD_MEMBER_UPDATE; + } + PluginPermissionsDiscordEvents::ThreadMembersUpdate => { + supported_discord_registrations_events &= Self::THREAD_MEMBERS_UPDATE; + } + PluginPermissionsDiscordEvents::ThreadUpdate => { + supported_discord_registrations_events &= Self::THREAD_UPDATE; + } + } + } + + supported_discord_registrations_events + } +} + +impl From for Vec { + fn from(requested_discord_registrations: DiscordEvents) -> Self { + let mut plugin_permissions_discord_events = Vec::new(); + + if requested_discord_registrations.contains(DiscordEvents::MESSAGE_CREATE) { + plugin_permissions_discord_events.push(PluginPermissionsDiscordEvents::MessageCreate); + } + + if requested_discord_registrations.contains(DiscordEvents::INTERACTION_CREATE) { + plugin_permissions_discord_events + .push(PluginPermissionsDiscordEvents::InteractionCreate); + } + + if requested_discord_registrations.contains(DiscordEvents::THREAD_CREATE) { + plugin_permissions_discord_events.push(PluginPermissionsDiscordEvents::ThreadCreate); + } + + if requested_discord_registrations.contains(DiscordEvents::THREAD_DELETE) { + plugin_permissions_discord_events.push(PluginPermissionsDiscordEvents::ThreadDelete); + } + + if requested_discord_registrations.contains(DiscordEvents::THREAD_LIST_SYNC) { + plugin_permissions_discord_events.push(PluginPermissionsDiscordEvents::ThreadListSync); + } + + if requested_discord_registrations.contains(DiscordEvents::THREAD_MEMBER_UPDATE) { + plugin_permissions_discord_events + .push(PluginPermissionsDiscordEvents::ThreadMemberUpdate); + } + + if requested_discord_registrations.contains(DiscordEvents::THREAD_MEMBERS_UPDATE) { + plugin_permissions_discord_events + .push(PluginPermissionsDiscordEvents::ThreadMembersUpdate); + } + + if requested_discord_registrations.contains(DiscordEvents::THREAD_UPDATE) { + plugin_permissions_discord_events.push(PluginPermissionsDiscordEvents::ThreadUpdate); + } + + plugin_permissions_discord_events + } +} + +impl From> for SupportedDiscordRegistrationsInteractions { fn from( - config_supported_discord_registrations: Vec, + plugin_permissions_discord_interactions: Vec, ) -> Self { - let mut supported_discord_registrations = Self::empty(); + let mut supported_discord_registrations_interactions = Self::empty(); - for registration in &config_supported_discord_registrations { - todo!(); + for plugin_permission_discord_interactions in &plugin_permissions_discord_interactions { + match plugin_permission_discord_interactions { + PluginPermissionsDiscordInteractions::ApplicationCommands => { + supported_discord_registrations_interactions &= Self::APPLICATION_COMMANDS; + } + PluginPermissionsDiscordInteractions::MessageComponents => { + supported_discord_registrations_interactions &= Self::MESSAGE_COMPONENTS; + } + PluginPermissionsDiscordInteractions::Modals => { + supported_discord_registrations_interactions &= Self::MODALS; + } + } } - supported_discord_registrations + supported_discord_registrations_interactions } } diff --git a/src/services/discord/requests.rs b/src/services/discord/requests.rs index bf2f178..f8c7e8b 100644 --- a/src/services/discord/requests.rs +++ b/src/services/discord/requests.rs @@ -3,7 +3,6 @@ use std::sync::Arc; -use anyhow::{Result, anyhow, bail}; use twilight_gateway::MessageSender; use twilight_http::{Client, request::Request, routing::Route}; use twilight_model::gateway::{ @@ -16,8 +15,9 @@ use twilight_model::gateway::{ }; use crate::{ - runtime::plugins::wbps::plugin::discord_import_types::{ - Body, DiscordRequests, DiscordResponses, + runtime::plugins::wbps::plugin::{ + core_import_types::Error, + discord_import_types::{Body, DiscordRequests, DiscordResponses}, }, services::discord::DiscordBotClient, }; @@ -28,7 +28,7 @@ impl DiscordBotClient { http_client: Arc, shard_message_senders: Arc>, request: DiscordRequests, - ) -> Result> { + ) -> Result, Error> { let request = match request { // Shard message sender commands DiscordRequests::RequestGuildMembers((guild_id, body)) => { @@ -38,9 +38,9 @@ impl DiscordBotClient { let d = match sonic_rs::from_slice::(&body) { Ok(d) => d, Err(err) => { - bail!( + return Err(format!( "Something went wrong while deserializing RequestGuildMembersInfo, error: {err}", - ); + )); } }; @@ -54,7 +54,9 @@ impl DiscordBotClient { None } DiscordRequests::RequestSoundboardSounds(_guild_ids) => { - bail!("RequestSoundboardSounds has not yet been implemented in Twilight.",); + return Err(format!( + "RequestSoundboardSounds has not yet been implemented in Twilight." + )); } DiscordRequests::UpdateVoiceState((guild_id, body)) => { let guild_shard_message_sender = @@ -63,9 +65,9 @@ impl DiscordBotClient { let d = match sonic_rs::from_slice::(&body) { Ok(d) => d, Err(err) => { - bail!( + return Err(format!( "Something went wrong while deserializing RequestGuildMembersInfo, error: {err}", - ); + )); } }; @@ -84,9 +86,9 @@ impl DiscordBotClient { let d = match sonic_rs::from_slice::(&body) { Ok(d) => d, Err(err) => { - bail!( + return Err(format!( "Something went wrong while deserializing RequestGuildMembersInfo, error: {err}", - ); + )); } }; @@ -110,9 +112,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -123,9 +125,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -137,7 +139,9 @@ impl DiscordBotClient { Body::Form(buffer) => match request_builder.multipart(buffer) { Ok(request) => request, Err(err) => { - bail!(err); + return Err(format!( + "Something went wrong while building a Discord request, error: {err}" + )); } }, }; @@ -145,9 +149,9 @@ impl DiscordBotClient { match request_builder.build() { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -159,7 +163,9 @@ impl DiscordBotClient { Body::Form(buffer) => match request_builder.multipart(buffer) { Ok(request) => request, Err(err) => { - bail!(err); + return Err(format!( + "Something went wrong while building a Discord request, error: {err}" + )); } }, }; @@ -167,9 +173,9 @@ impl DiscordBotClient { match request_builder.build() { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -180,9 +186,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -196,9 +202,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -211,9 +217,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -221,9 +227,9 @@ impl DiscordBotClient { match Request::builder(&Route::GetActiveThreads { guild_id }).build() { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -231,9 +237,9 @@ impl DiscordBotClient { match Request::builder(&Route::GetChannel { channel_id }).build() { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -247,9 +253,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -263,9 +269,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -279,9 +285,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -294,9 +300,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -311,9 +317,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -333,9 +339,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -343,9 +349,9 @@ impl DiscordBotClient { match Request::builder(&Route::JoinThread { channel_id }).build() { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -353,9 +359,9 @@ impl DiscordBotClient { match Request::builder(&Route::LeaveThread { channel_id }).build() { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -368,9 +374,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -381,9 +387,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -401,9 +407,9 @@ impl DiscordBotClient { { Ok(request) => Some(request), Err(err) => { - bail!( + return Err(format!( "Something went wrong while building a Discord request, error: {err}" - ); + )); } } } @@ -412,7 +418,7 @@ impl DiscordBotClient { if let Some(request) = request { match http_client.request::>(request).await { Ok(response) => Ok(Some(response.bytes().await.unwrap().clone())), - Err(err) => Err(anyhow!( + Err(err) => Err(format!( "Something went wrong while making a Discord request, error: {err}" )), } diff --git a/src/utils/channels.rs b/src/utils/channels.rs index ed069e8..8abb1be 100644 --- a/src/utils/channels.rs +++ b/src/utils/channels.rs @@ -13,7 +13,8 @@ use crate::{ Shutdown, database::Keyspaces, runtime::plugins::wbps::plugin::{ - discord_export_types::DiscordEvents, discord_import_functions::DiscordRequests, + discord_export_types::{DiscordEvents, Error}, + discord_import_functions::DiscordRequests, discord_import_types::DiscordResponses, }, }; @@ -31,7 +32,8 @@ pub enum CoreMessages { pub enum DatabaseMessages { Get(Keyspaces, Vec, OSSender>>), - GetAll(Keyspaces, OSSender>>), + GetAllKeys(Keyspaces, OSSender>>), + GetAllValues(Keyspaces, OSSender>>), Insert(Keyspaces, Vec, Vec, OSSender>), Remove(Keyspaces, Vec, OSSender>), ContainsKey(Keyspaces, Vec, OSSender>), @@ -45,14 +47,23 @@ pub enum JobSchedulerMessages { pub enum DiscordBotClientMessages { RegisterApplicationCommands, - Request(DiscordRequests, OSSender>>), + Request( + DiscordRequests, + OSSender, Error>>, + ), } pub enum RuntimeMessages { + Core(RuntimeMessagesCore), JobScheduler(RuntimeMessagesJobScheduler), Discord(RuntimeMessagesDiscord), } +pub enum RuntimeMessagesCore { + CallDependencyFunction(Uuid, String, Vec, OSSender, Error>>), + UnloadPlugin(Uuid), +} + pub enum RuntimeMessagesJobScheduler { CallScheduledJob(Uuid, Uuid), } diff --git a/wit/core.wit b/wit/core.wit index 41c51f8..d4c2977 100644 --- a/wit/core.wit +++ b/wit/core.wit @@ -48,7 +48,7 @@ interface core-import-functions { shutdown: func(restart: bool) -> result<_, error>; /// The `params` parameter and result ok value are bytes. The serialized format has to be decided between plugins. - dependency-function: func(dependency-id: string, function-id: string, params: list) -> result, error>; + dependency-function: func(registry-id: string, plugin-id: string, function-id: string, params: list) -> result, error>; } interface core-export-functions { diff --git a/wit/discord.wit b/wit/discord.wit index 893d9c1..11d4330 100644 --- a/wit/discord.wit +++ b/wit/discord.wit @@ -2,10 +2,6 @@ interface discord-import-types { use core-types.{json}; use core-import-types.{error}; - /// Raw form data for Discord requests with file attachments (e.g. embeds with - /// images). - type form = list; - /// All supported Discord gateway events. flags discord-events { message-create, @@ -18,38 +14,52 @@ interface discord-import-types { thread-update, } - /// Discord gateway events the plugin can register to receive via `discord-event`. - type supported-discord-registrations = discord-events; + /// Discord gateway events and interactions the plugin is allowed to register + /// for. + record supported-discord-registrations { + events: supported-discord-registrations-events, + interactions: supported-discord-registrations-interactions, + } + + /// Discord gateway events the plugin is allowed to register for. + type supported-discord-registrations-events = discord-events; - /// Opt-in registration for Discord gateway events and interactions. - /// - /// Pass `none` for fields the plugin does not need. The plugin will only receive - /// what it registered for. + /// Discord interactions the plugin is allowed to register for. + flags supported-discord-registrations-interactions { + application-commands, + message-components, + modals, + } + + /// Opt-in Discord gateway event and interaction registrations. record discord-registrations { - events: option, + events: option, interactions: option, } - /// Interactions to register. - /// - /// `application-commands`: Slash commands and context menus. Structure per - /// Discord docs: - /// - /// `message-components`: Button/select menus. Value is the UUID count needed - /// by the plugin. + /// Opt-in Discord gateway event registrations. + type discord-registrations-events = discord-events; + + /// Opt-in Discord interaction registrations. /// - /// `modals`: Modals. Value is the UUID count needed by the plugin. + /// `application-commands`: Slash commands and context menus. + /// The structure is described in the Discord docs: https://docs.discord.com/developers/interactions/application-commands#application-command-object + // + /// `message-components`: Button/select menus. Value is the amount of UUIDs + /// needed by the plugin. + // + /// `modals`: Modals. Value is the amount of UUIDs needed by the plugin. record discord-registrations-interactions { application-commands: option>, message-components: option, modals: option, } - /// Result of `discord-register`. + /// Result of Discord gateway event and interaction registrations. /// /// `events`: Returns an error if the plugin tried to register for an event - /// it's not allowed to. - /// + /// it is not allowed to. + // /// `interactions`: Returns an error if the plugin does not have permission /// to register for the `interaction-create` event. record discord-registrations-result { @@ -57,7 +67,10 @@ interface discord-import-types { interactions: option>, } - /// UUIDs for registered message components and modals. + /// UUIDs assigned to registered message components and modals. + /// + /// Each UUID can be used to recognize which component was clicked or modal + /// submitted. record discord-registrations-result-interactions { message-components: option>, modals: option>, @@ -65,9 +78,10 @@ interface discord-import-types { /// API requests to Discord. /// - /// Variant data follows Discord HTTP and Gateway docs. Examples: + /// Variant data follows Discord HTTP and Gateway data structures. + /// The structures are described in the Discord docs: /// - Gateway commands: - /// - HTTP operations: + /// - HTTP operations: variant discord-requests { // Gateway commands request-guild-members(tuple), @@ -98,12 +112,18 @@ interface discord-import-types { update-interaction-original(tuple), } - /// Request body type: JSON for normal payloads, form-data for file attachments. + /// Request body type: + /// - JSON: For normal payloads + /// - Form: For payloads including file attachments. variant body { json(json), form(form), } + /// Raw form data for Discord requests with file attachments (e.g. embeds with + /// images). + type form = list; + /// API responses from Discord. type discord-responses = json; } @@ -114,14 +134,16 @@ interface discord-export-types { /// Result of application command registrations. /// - /// First list: successful registrations as `(command-name, snowflake)`. - /// Second list: failed registrations as `(command-name, error-message)`. + /// First list: Successful registrations, includes the Discord snowflake which + /// can be used to recognize the application command which got called. + // + /// Second list: Failed registrations, includes an error. type discord-registrations-result-application-commands = tuple>, list>>; - /// Gateway events received via `discord-event`. + /// Gateway events the plugin registered for and receives. /// - /// Payload is raw JSON from Discord. See: - /// + /// Payload is raw JSON from Discord. + /// The structure is described in the Discord docs: https://discord.com/developers/docs/events/gateway-events#receive-events variant discord-events { interaction-create(json), message-create(json), @@ -138,20 +160,18 @@ interface discord-import-functions { use core-import-types.{error}; use discord-import-types.{discord-registrations, discord-registrations-result, discord-requests, discord-responses, supported-discord-registrations}; - /// Returns the Discord events and interaction types the plugin can register for. + /// Returns the Discord gateway events the plugin can register for. get-supported-discord-registrations: func() -> supported-discord-registrations; - /// Registers for Discord events and interactions. + /// Registers for Discord gateway events and interactions. /// - /// Must be called during initialization. Application commands registered after - /// initialization are ignored by Discord. - /// discord-registrations-result; - /// Makes a request to Discord API via the host. + /// Makes a Discord request, this can be a gateway command or HTTP request. /// - /// Returns `none` on success (Discord returns 204 No Content). - /// Returns `some(json)` on success with a response body. + /// Gateway commands do not return data. discord-request: func(request: discord-requests) -> result, error>; } @@ -159,12 +179,9 @@ interface discord-export-functions { use core-export-types.{error}; use discord-export-types.{discord-events, discord-registrations-result-application-commands}; - /// Called when application command registration completes. - /// - /// The host doesn't know the outcome of its own register call, so it passes - /// the result here. Use this to log or handle failures. + /// Called when Discord application command registration completes. discord-application-commands: func(registrations-result: discord-registrations-result-application-commands) -> result<_, error>; - /// Called when a registered Discord event occurs. + /// Called when a registered Discord gatway event occurs. discord-event: func(event: discord-events) -> result<_, error>; }