├── .cargo └── config.toml ├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .woodpecker └── docker.yaml ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── examples ├── README.md └── discordpy │ ├── .editorconfig │ ├── .gitignore │ ├── classes │ ├── bot.py │ ├── guild.py │ ├── member.py │ ├── message.py │ ├── misc.py │ └── state.py │ ├── cogs │ ├── events.py │ └── general.py │ ├── config.example.py │ └── main.py ├── rustfmt.toml └── src ├── cache.rs ├── cluster.rs ├── config.rs ├── constants.rs ├── handler.rs ├── main.rs ├── metrics.rs ├── models.rs └── utils.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "target-cpu=native"] 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | andesite/ 2 | examples/ 3 | debug/ 4 | target/ 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 100 11 | tab_width = 4 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Logging level 2 | RUST_LOG=info 3 | 4 | # Discord bot token 5 | BOT_TOKEN= 6 | 7 | # Sharding information 8 | SHARDS_START=0 9 | SHARDS_END=1 10 | SHARDS_TOTAL=2 11 | SHARDS_CONCURRENCY=1 12 | SHARDS_WAIT=6 13 | 14 | # Number of clusters 15 | CLUSTERS=2 16 | 17 | # Declare default queue 18 | DEFAULT_QUEUE=true 19 | 20 | # Resume after a restart 21 | RESUME=true 22 | 23 | # Identify payload 24 | INTENTS=32767 25 | LARGE_THRESHOLD=250 26 | STATUS=online 27 | ACTIVITY_TYPE=0 28 | ACTIVITY_NAME=Testing 29 | 30 | # Discord channel logs 31 | LOG_CHANNEL= 32 | LOG_GUILD_CHANNEL= 33 | 34 | # State configuration 35 | STATE_ENABLED=true 36 | STATE_MEMBER=true 37 | STATE_MEMBER_TTL=60000 38 | STATE_MESSAGE=true 39 | STATE_MESSAGE_TTL=60000 40 | STATE_PRESENCE=true 41 | STATE_EMOJI=true 42 | STATE_VOICE=true 43 | STATE_OLD=false 44 | 45 | # RabbitMQ details 46 | RABBIT_HOST=127.0.0.1 47 | RABBIT_PORT=5672 48 | RABBIT_USERNAME=guest 49 | RABBIT_PASSWORD=guest 50 | 51 | # Redis address 52 | REDIS_HOST=127.0.0.1 53 | REDIS_PORT=6379 54 | 55 | # Prometheus address 56 | PROMETHEUS_HOST=127.0.0.1 57 | PROMETHEUS_PORT=8005 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # Environmental variables 10 | .env 11 | -------------------------------------------------------------------------------- /.woodpecker/docker.yaml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: tag 3 | - event: push 4 | branch: main 5 | 6 | steps: 7 | - name: publish 8 | image: woodpeckerci/plugin-docker-buildx 9 | settings: 10 | registry: ghcr.io 11 | repo: ghcr.io/chamburr/twilight-dispatch 12 | auto_tag: true 13 | username: 14 | from_secret: docker_username 15 | password: 16 | from_secret: docker_password 17 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "ahash" 13 | version = "0.8.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107" 16 | dependencies = [ 17 | "cfg-if", 18 | "once_cell", 19 | "version_check", 20 | ] 21 | 22 | [[package]] 23 | name = "amq-protocol" 24 | version = "7.0.1" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "acc7cad07d1b4533fcb46f0819a6126fa201fd0385469aba75e405424f3fe009" 27 | dependencies = [ 28 | "amq-protocol-tcp", 29 | "amq-protocol-types", 30 | "amq-protocol-uri", 31 | "cookie-factory", 32 | "nom", 33 | "serde", 34 | ] 35 | 36 | [[package]] 37 | name = "amq-protocol-tcp" 38 | version = "7.0.1" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "5d8b20aba8c35a0b885e1e978eff456ced925730a4e012e63e4ff89a1deb602b" 41 | dependencies = [ 42 | "amq-protocol-uri", 43 | "tcp-stream", 44 | "tracing", 45 | ] 46 | 47 | [[package]] 48 | name = "amq-protocol-types" 49 | version = "7.0.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "e245e0e9083b6a6db5f8c10013074cb382266eb9e2a37204d19c651b8d3b8114" 52 | dependencies = [ 53 | "cookie-factory", 54 | "nom", 55 | "serde", 56 | "serde_json", 57 | ] 58 | 59 | [[package]] 60 | name = "amq-protocol-uri" 61 | version = "7.0.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "56987108bf48d2eb500cae8896cd9291564eedd8744776ecc5c3338a8b2ca5f8" 64 | dependencies = [ 65 | "amq-protocol-types", 66 | "percent-encoding", 67 | "url", 68 | ] 69 | 70 | [[package]] 71 | name = "async-channel" 72 | version = "1.8.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" 75 | dependencies = [ 76 | "concurrent-queue", 77 | "event-listener", 78 | "futures-core", 79 | ] 80 | 81 | [[package]] 82 | name = "async-executor" 83 | version = "1.5.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" 86 | dependencies = [ 87 | "async-lock", 88 | "async-task", 89 | "concurrent-queue", 90 | "fastrand", 91 | "futures-lite", 92 | "slab", 93 | ] 94 | 95 | [[package]] 96 | name = "async-global-executor" 97 | version = "2.3.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" 100 | dependencies = [ 101 | "async-channel", 102 | "async-executor", 103 | "async-io", 104 | "async-lock", 105 | "blocking", 106 | "futures-lite", 107 | "once_cell", 108 | ] 109 | 110 | [[package]] 111 | name = "async-global-executor-trait" 112 | version = "2.1.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "33dd14c5a15affd2abcff50d84efd4009ada28a860f01c14f9d654f3e81b3f75" 115 | dependencies = [ 116 | "async-global-executor", 117 | "async-trait", 118 | "executor-trait", 119 | ] 120 | 121 | [[package]] 122 | name = "async-io" 123 | version = "1.12.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" 126 | dependencies = [ 127 | "async-lock", 128 | "autocfg", 129 | "concurrent-queue", 130 | "futures-lite", 131 | "libc", 132 | "log", 133 | "parking", 134 | "polling", 135 | "slab", 136 | "socket2", 137 | "waker-fn", 138 | "windows-sys", 139 | ] 140 | 141 | [[package]] 142 | name = "async-lock" 143 | version = "2.6.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" 146 | dependencies = [ 147 | "event-listener", 148 | "futures-lite", 149 | ] 150 | 151 | [[package]] 152 | name = "async-reactor-trait" 153 | version = "1.1.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e" 156 | dependencies = [ 157 | "async-io", 158 | "async-trait", 159 | "futures-core", 160 | "reactor-trait", 161 | ] 162 | 163 | [[package]] 164 | name = "async-task" 165 | version = "4.3.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" 168 | 169 | [[package]] 170 | name = "async-trait" 171 | version = "0.1.59" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" 174 | dependencies = [ 175 | "proc-macro2", 176 | "quote", 177 | "syn", 178 | ] 179 | 180 | [[package]] 181 | name = "atomic-waker" 182 | version = "1.0.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" 185 | 186 | [[package]] 187 | name = "autocfg" 188 | version = "1.1.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 191 | 192 | [[package]] 193 | name = "base64" 194 | version = "0.13.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 197 | 198 | [[package]] 199 | name = "bitflags" 200 | version = "1.3.2" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 203 | 204 | [[package]] 205 | name = "block-buffer" 206 | version = "0.10.3" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" 209 | dependencies = [ 210 | "generic-array", 211 | ] 212 | 213 | [[package]] 214 | name = "blocking" 215 | version = "1.3.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" 218 | dependencies = [ 219 | "async-channel", 220 | "async-lock", 221 | "async-task", 222 | "atomic-waker", 223 | "fastrand", 224 | "futures-lite", 225 | ] 226 | 227 | [[package]] 228 | name = "bumpalo" 229 | version = "3.11.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" 232 | 233 | [[package]] 234 | name = "byteorder" 235 | version = "1.4.3" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 238 | 239 | [[package]] 240 | name = "bytes" 241 | version = "1.3.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" 244 | 245 | [[package]] 246 | name = "cc" 247 | version = "1.0.77" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" 250 | 251 | [[package]] 252 | name = "cfg-if" 253 | version = "1.0.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 256 | 257 | [[package]] 258 | name = "cmake" 259 | version = "0.1.49" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" 262 | dependencies = [ 263 | "cc", 264 | ] 265 | 266 | [[package]] 267 | name = "combine" 268 | version = "4.6.6" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" 271 | dependencies = [ 272 | "bytes", 273 | "futures-core", 274 | "memchr", 275 | "pin-project-lite", 276 | "tokio", 277 | "tokio-util", 278 | ] 279 | 280 | [[package]] 281 | name = "concurrent-queue" 282 | version = "2.0.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" 285 | dependencies = [ 286 | "crossbeam-utils", 287 | ] 288 | 289 | [[package]] 290 | name = "cookie-factory" 291 | version = "0.3.2" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" 294 | 295 | [[package]] 296 | name = "cpufeatures" 297 | version = "0.2.5" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" 300 | dependencies = [ 301 | "libc", 302 | ] 303 | 304 | [[package]] 305 | name = "crc32fast" 306 | version = "1.3.2" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 309 | dependencies = [ 310 | "cfg-if", 311 | ] 312 | 313 | [[package]] 314 | name = "crossbeam-utils" 315 | version = "0.8.14" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" 318 | dependencies = [ 319 | "cfg-if", 320 | ] 321 | 322 | [[package]] 323 | name = "crypto-common" 324 | version = "0.1.6" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 327 | dependencies = [ 328 | "generic-array", 329 | "typenum", 330 | ] 331 | 332 | [[package]] 333 | name = "digest" 334 | version = "0.10.6" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 337 | dependencies = [ 338 | "block-buffer", 339 | "crypto-common", 340 | ] 341 | 342 | [[package]] 343 | name = "doc-comment" 344 | version = "0.3.3" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 347 | 348 | [[package]] 349 | name = "dotenv" 350 | version = "0.15.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 353 | 354 | [[package]] 355 | name = "errno" 356 | version = "0.2.8" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 359 | dependencies = [ 360 | "errno-dragonfly", 361 | "libc", 362 | "winapi", 363 | ] 364 | 365 | [[package]] 366 | name = "errno-dragonfly" 367 | version = "0.1.2" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 370 | dependencies = [ 371 | "cc", 372 | "libc", 373 | ] 374 | 375 | [[package]] 376 | name = "event-listener" 377 | version = "2.5.3" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 380 | 381 | [[package]] 382 | name = "executor-trait" 383 | version = "2.1.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "1a1052dd43212a7777ec6a69b117da52f5e52f07aec47d00c1a2b33b85d06b08" 386 | dependencies = [ 387 | "async-trait", 388 | ] 389 | 390 | [[package]] 391 | name = "fastrand" 392 | version = "1.8.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 395 | dependencies = [ 396 | "instant", 397 | ] 398 | 399 | [[package]] 400 | name = "flate2" 401 | version = "1.0.25" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 404 | dependencies = [ 405 | "crc32fast", 406 | "libz-ng-sys", 407 | "miniz_oxide", 408 | ] 409 | 410 | [[package]] 411 | name = "float-cmp" 412 | version = "0.9.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 415 | dependencies = [ 416 | "num-traits", 417 | ] 418 | 419 | [[package]] 420 | name = "flume" 421 | version = "0.10.14" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" 424 | dependencies = [ 425 | "futures-core", 426 | "futures-sink", 427 | "pin-project", 428 | "spin 0.9.4", 429 | ] 430 | 431 | [[package]] 432 | name = "fnv" 433 | version = "1.0.7" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 436 | 437 | [[package]] 438 | name = "form_urlencoded" 439 | version = "1.1.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 442 | dependencies = [ 443 | "percent-encoding", 444 | ] 445 | 446 | [[package]] 447 | name = "futures-channel" 448 | version = "0.3.25" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" 451 | dependencies = [ 452 | "futures-core", 453 | ] 454 | 455 | [[package]] 456 | name = "futures-core" 457 | version = "0.3.25" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" 460 | 461 | [[package]] 462 | name = "futures-io" 463 | version = "0.3.25" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" 466 | 467 | [[package]] 468 | name = "futures-lite" 469 | version = "1.12.0" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" 472 | dependencies = [ 473 | "fastrand", 474 | "futures-core", 475 | "futures-io", 476 | "memchr", 477 | "parking", 478 | "pin-project-lite", 479 | "waker-fn", 480 | ] 481 | 482 | [[package]] 483 | name = "futures-sink" 484 | version = "0.3.25" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" 487 | 488 | [[package]] 489 | name = "futures-task" 490 | version = "0.3.25" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" 493 | 494 | [[package]] 495 | name = "futures-util" 496 | version = "0.3.25" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" 499 | dependencies = [ 500 | "futures-core", 501 | "futures-sink", 502 | "futures-task", 503 | "pin-project-lite", 504 | "pin-utils", 505 | "slab", 506 | ] 507 | 508 | [[package]] 509 | name = "generic-array" 510 | version = "0.14.6" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 513 | dependencies = [ 514 | "typenum", 515 | "version_check", 516 | ] 517 | 518 | [[package]] 519 | name = "getrandom" 520 | version = "0.2.8" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 523 | dependencies = [ 524 | "cfg-if", 525 | "libc", 526 | "wasi", 527 | ] 528 | 529 | [[package]] 530 | name = "h2" 531 | version = "0.3.15" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" 534 | dependencies = [ 535 | "bytes", 536 | "fnv", 537 | "futures-core", 538 | "futures-sink", 539 | "futures-util", 540 | "http", 541 | "indexmap", 542 | "slab", 543 | "tokio", 544 | "tokio-util", 545 | "tracing", 546 | ] 547 | 548 | [[package]] 549 | name = "halfbrown" 550 | version = "0.1.18" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "9e2a3c70a9c00cc1ee87b54e89f9505f73bb17d63f1b25c9a462ba8ef885444f" 553 | dependencies = [ 554 | "hashbrown 0.13.1", 555 | "serde", 556 | ] 557 | 558 | [[package]] 559 | name = "hashbrown" 560 | version = "0.12.3" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 563 | 564 | [[package]] 565 | name = "hashbrown" 566 | version = "0.13.1" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" 569 | dependencies = [ 570 | "ahash", 571 | ] 572 | 573 | [[package]] 574 | name = "hermit-abi" 575 | version = "0.1.19" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 578 | dependencies = [ 579 | "libc", 580 | ] 581 | 582 | [[package]] 583 | name = "hex" 584 | version = "0.4.3" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 587 | 588 | [[package]] 589 | name = "http" 590 | version = "0.2.8" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 593 | dependencies = [ 594 | "bytes", 595 | "fnv", 596 | "itoa", 597 | ] 598 | 599 | [[package]] 600 | name = "http-body" 601 | version = "0.4.5" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 604 | dependencies = [ 605 | "bytes", 606 | "http", 607 | "pin-project-lite", 608 | ] 609 | 610 | [[package]] 611 | name = "httparse" 612 | version = "1.8.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 615 | 616 | [[package]] 617 | name = "httpdate" 618 | version = "1.0.2" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 621 | 622 | [[package]] 623 | name = "hyper" 624 | version = "0.14.23" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" 627 | dependencies = [ 628 | "bytes", 629 | "futures-channel", 630 | "futures-core", 631 | "futures-util", 632 | "h2", 633 | "http", 634 | "http-body", 635 | "httparse", 636 | "httpdate", 637 | "itoa", 638 | "pin-project-lite", 639 | "socket2", 640 | "tokio", 641 | "tower-service", 642 | "tracing", 643 | "want", 644 | ] 645 | 646 | [[package]] 647 | name = "hyper-rustls" 648 | version = "0.23.1" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "59df7c4e19c950e6e0e868dcc0a300b09a9b88e9ec55bd879ca819087a77355d" 651 | dependencies = [ 652 | "http", 653 | "hyper", 654 | "rustls", 655 | "tokio", 656 | "tokio-rustls", 657 | "webpki-roots", 658 | ] 659 | 660 | [[package]] 661 | name = "idna" 662 | version = "0.3.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 665 | dependencies = [ 666 | "unicode-bidi", 667 | "unicode-normalization", 668 | ] 669 | 670 | [[package]] 671 | name = "indexmap" 672 | version = "1.9.2" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 675 | dependencies = [ 676 | "autocfg", 677 | "hashbrown 0.12.3", 678 | ] 679 | 680 | [[package]] 681 | name = "instant" 682 | version = "0.1.12" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 685 | dependencies = [ 686 | "cfg-if", 687 | ] 688 | 689 | [[package]] 690 | name = "io-lifetimes" 691 | version = "0.7.5" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074" 694 | 695 | [[package]] 696 | name = "itoa" 697 | version = "1.0.4" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" 700 | 701 | [[package]] 702 | name = "js-sys" 703 | version = "0.3.60" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 706 | dependencies = [ 707 | "wasm-bindgen", 708 | ] 709 | 710 | [[package]] 711 | name = "lapin" 712 | version = "2.1.1" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "bd03ea5831b44775e296239a64851e2fd14a80a363d202ba147009ffc994ff0f" 715 | dependencies = [ 716 | "amq-protocol", 717 | "async-global-executor-trait", 718 | "async-reactor-trait", 719 | "async-trait", 720 | "executor-trait", 721 | "flume", 722 | "futures-core", 723 | "futures-io", 724 | "parking_lot", 725 | "pinky-swear", 726 | "reactor-trait", 727 | "serde", 728 | "tracing", 729 | "waker-fn", 730 | ] 731 | 732 | [[package]] 733 | name = "lazy_static" 734 | version = "1.4.0" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 737 | 738 | [[package]] 739 | name = "leaky-bucket-lite" 740 | version = "0.5.2" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "1411c737dd21a748044ab29af14b7f920b2dcc277284df6dd986492c98bf5229" 743 | dependencies = [ 744 | "tokio", 745 | ] 746 | 747 | [[package]] 748 | name = "lexical-core" 749 | version = "0.8.5" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" 752 | dependencies = [ 753 | "lexical-parse-float", 754 | "lexical-parse-integer", 755 | "lexical-util", 756 | "lexical-write-float", 757 | "lexical-write-integer", 758 | ] 759 | 760 | [[package]] 761 | name = "lexical-parse-float" 762 | version = "0.8.5" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" 765 | dependencies = [ 766 | "lexical-parse-integer", 767 | "lexical-util", 768 | "static_assertions", 769 | ] 770 | 771 | [[package]] 772 | name = "lexical-parse-integer" 773 | version = "0.8.6" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" 776 | dependencies = [ 777 | "lexical-util", 778 | "static_assertions", 779 | ] 780 | 781 | [[package]] 782 | name = "lexical-util" 783 | version = "0.8.5" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" 786 | dependencies = [ 787 | "static_assertions", 788 | ] 789 | 790 | [[package]] 791 | name = "lexical-write-float" 792 | version = "0.8.5" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" 795 | dependencies = [ 796 | "lexical-util", 797 | "lexical-write-integer", 798 | "static_assertions", 799 | ] 800 | 801 | [[package]] 802 | name = "lexical-write-integer" 803 | version = "0.8.5" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" 806 | dependencies = [ 807 | "lexical-util", 808 | "static_assertions", 809 | ] 810 | 811 | [[package]] 812 | name = "libc" 813 | version = "0.2.137" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" 816 | 817 | [[package]] 818 | name = "libz-ng-sys" 819 | version = "1.1.8" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "4399ae96a9966bf581e726de86969f803a81b7ce795fcd5480e640589457e0f2" 822 | dependencies = [ 823 | "cmake", 824 | "libc", 825 | ] 826 | 827 | [[package]] 828 | name = "linux-raw-sys" 829 | version = "0.0.46" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" 832 | 833 | [[package]] 834 | name = "lock_api" 835 | version = "0.4.9" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 838 | dependencies = [ 839 | "autocfg", 840 | "scopeguard", 841 | ] 842 | 843 | [[package]] 844 | name = "log" 845 | version = "0.4.17" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 848 | dependencies = [ 849 | "cfg-if", 850 | ] 851 | 852 | [[package]] 853 | name = "memchr" 854 | version = "2.5.0" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 857 | 858 | [[package]] 859 | name = "minimal-lexical" 860 | version = "0.2.1" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 863 | 864 | [[package]] 865 | name = "miniz_oxide" 866 | version = "0.6.2" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 869 | dependencies = [ 870 | "adler", 871 | ] 872 | 873 | [[package]] 874 | name = "mio" 875 | version = "0.8.5" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" 878 | dependencies = [ 879 | "libc", 880 | "log", 881 | "wasi", 882 | "windows-sys", 883 | ] 884 | 885 | [[package]] 886 | name = "nom" 887 | version = "7.1.1" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 890 | dependencies = [ 891 | "memchr", 892 | "minimal-lexical", 893 | ] 894 | 895 | [[package]] 896 | name = "nu-ansi-term" 897 | version = "0.46.0" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 900 | dependencies = [ 901 | "overload", 902 | "winapi", 903 | ] 904 | 905 | [[package]] 906 | name = "num-traits" 907 | version = "0.2.15" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 910 | dependencies = [ 911 | "autocfg", 912 | ] 913 | 914 | [[package]] 915 | name = "num_cpus" 916 | version = "1.14.0" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" 919 | dependencies = [ 920 | "hermit-abi", 921 | "libc", 922 | ] 923 | 924 | [[package]] 925 | name = "once_cell" 926 | version = "1.16.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 929 | 930 | [[package]] 931 | name = "ordered-float" 932 | version = "2.10.0" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" 935 | dependencies = [ 936 | "num-traits", 937 | ] 938 | 939 | [[package]] 940 | name = "overload" 941 | version = "0.1.1" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 944 | 945 | [[package]] 946 | name = "parking" 947 | version = "2.0.0" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" 950 | 951 | [[package]] 952 | name = "parking_lot" 953 | version = "0.12.1" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 956 | dependencies = [ 957 | "lock_api", 958 | "parking_lot_core", 959 | ] 960 | 961 | [[package]] 962 | name = "parking_lot_core" 963 | version = "0.9.5" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" 966 | dependencies = [ 967 | "cfg-if", 968 | "libc", 969 | "redox_syscall", 970 | "smallvec", 971 | "windows-sys", 972 | ] 973 | 974 | [[package]] 975 | name = "percent-encoding" 976 | version = "2.2.0" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 979 | 980 | [[package]] 981 | name = "pin-project" 982 | version = "1.0.12" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" 985 | dependencies = [ 986 | "pin-project-internal", 987 | ] 988 | 989 | [[package]] 990 | name = "pin-project-internal" 991 | version = "1.0.12" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" 994 | dependencies = [ 995 | "proc-macro2", 996 | "quote", 997 | "syn", 998 | ] 999 | 1000 | [[package]] 1001 | name = "pin-project-lite" 1002 | version = "0.2.9" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 1005 | 1006 | [[package]] 1007 | name = "pin-utils" 1008 | version = "0.1.0" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1011 | 1012 | [[package]] 1013 | name = "pinky-swear" 1014 | version = "6.1.0" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "d894b67aa7a4bf295db5e85349078c604edaa6fa5c8721e8eca3c7729a27f2ac" 1017 | dependencies = [ 1018 | "doc-comment", 1019 | "flume", 1020 | "parking_lot", 1021 | "tracing", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "polling" 1026 | version = "2.5.1" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" 1029 | dependencies = [ 1030 | "autocfg", 1031 | "cfg-if", 1032 | "libc", 1033 | "log", 1034 | "wepoll-ffi", 1035 | "windows-sys", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "ppv-lite86" 1040 | version = "0.2.17" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1043 | 1044 | [[package]] 1045 | name = "proc-macro2" 1046 | version = "1.0.47" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 1049 | dependencies = [ 1050 | "unicode-ident", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "procfs" 1055 | version = "0.14.1" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "2dfb6451c91904606a1abe93e83a8ec851f45827fa84273f256ade45dc095818" 1058 | dependencies = [ 1059 | "bitflags", 1060 | "byteorder", 1061 | "hex", 1062 | "lazy_static", 1063 | "rustix", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "prometheus" 1068 | version = "0.13.3" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" 1071 | dependencies = [ 1072 | "cfg-if", 1073 | "fnv", 1074 | "lazy_static", 1075 | "libc", 1076 | "memchr", 1077 | "parking_lot", 1078 | "procfs", 1079 | "thiserror", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "quote" 1084 | version = "1.0.21" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 1087 | dependencies = [ 1088 | "proc-macro2", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "rand" 1093 | version = "0.8.5" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1096 | dependencies = [ 1097 | "libc", 1098 | "rand_chacha", 1099 | "rand_core", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "rand_chacha" 1104 | version = "0.3.1" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1107 | dependencies = [ 1108 | "ppv-lite86", 1109 | "rand_core", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "rand_core" 1114 | version = "0.6.4" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1117 | dependencies = [ 1118 | "getrandom", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "reactor-trait" 1123 | version = "1.1.0" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "438a4293e4d097556730f4711998189416232f009c137389e0f961d2bc0ddc58" 1126 | dependencies = [ 1127 | "async-trait", 1128 | "futures-core", 1129 | "futures-io", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "redis" 1134 | version = "0.22.1" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "513b3649f1a111c17954296e4a3b9eecb108b766c803e2b99f179ebe27005985" 1137 | dependencies = [ 1138 | "async-trait", 1139 | "bytes", 1140 | "combine", 1141 | "futures-util", 1142 | "itoa", 1143 | "percent-encoding", 1144 | "pin-project-lite", 1145 | "ryu", 1146 | "tokio", 1147 | "tokio-util", 1148 | "url", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "redox_syscall" 1153 | version = "0.2.16" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1156 | dependencies = [ 1157 | "bitflags", 1158 | ] 1159 | 1160 | [[package]] 1161 | name = "ring" 1162 | version = "0.16.20" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 1165 | dependencies = [ 1166 | "cc", 1167 | "libc", 1168 | "once_cell", 1169 | "spin 0.5.2", 1170 | "untrusted", 1171 | "web-sys", 1172 | "winapi", 1173 | ] 1174 | 1175 | [[package]] 1176 | name = "rustix" 1177 | version = "0.35.13" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9" 1180 | dependencies = [ 1181 | "bitflags", 1182 | "errno", 1183 | "io-lifetimes", 1184 | "libc", 1185 | "linux-raw-sys", 1186 | "windows-sys", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "rustls" 1191 | version = "0.20.7" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" 1194 | dependencies = [ 1195 | "log", 1196 | "ring", 1197 | "sct", 1198 | "webpki", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "ryu" 1203 | version = "1.0.11" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 1206 | 1207 | [[package]] 1208 | name = "scopeguard" 1209 | version = "1.1.0" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1212 | 1213 | [[package]] 1214 | name = "sct" 1215 | version = "0.7.0" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 1218 | dependencies = [ 1219 | "ring", 1220 | "untrusted", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "serde" 1225 | version = "1.0.148" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" 1228 | dependencies = [ 1229 | "serde_derive", 1230 | ] 1231 | 1232 | [[package]] 1233 | name = "serde-value" 1234 | version = "0.7.0" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" 1237 | dependencies = [ 1238 | "ordered-float", 1239 | "serde", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "serde_derive" 1244 | version = "1.0.148" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" 1247 | dependencies = [ 1248 | "proc-macro2", 1249 | "quote", 1250 | "syn", 1251 | ] 1252 | 1253 | [[package]] 1254 | name = "serde_json" 1255 | version = "1.0.89" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" 1258 | dependencies = [ 1259 | "itoa", 1260 | "ryu", 1261 | "serde", 1262 | ] 1263 | 1264 | [[package]] 1265 | name = "serde_repr" 1266 | version = "0.1.9" 1267 | source = "registry+https://github.com/rust-lang/crates.io-index" 1268 | checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" 1269 | dependencies = [ 1270 | "proc-macro2", 1271 | "quote", 1272 | "syn", 1273 | ] 1274 | 1275 | [[package]] 1276 | name = "sha-1" 1277 | version = "0.10.1" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" 1280 | dependencies = [ 1281 | "cfg-if", 1282 | "cpufeatures", 1283 | "digest", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "sharded-slab" 1288 | version = "0.1.4" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1291 | dependencies = [ 1292 | "lazy_static", 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "signal-hook-registry" 1297 | version = "1.4.0" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1300 | dependencies = [ 1301 | "libc", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "simd-json" 1306 | version = "0.6.0" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "9bd78b840b9de64fa3f7d72909b76343849f68e8c3d32608db8d38e4e5481f84" 1309 | dependencies = [ 1310 | "halfbrown", 1311 | "serde", 1312 | "serde_json", 1313 | "simdutf8", 1314 | "value-trait 0.4.0", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "simd-json" 1319 | version = "0.7.0" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "8e3375b6c3d8c048ba09c8b4b6c3f1d3f35e06b71db07d231c323943a949e1b8" 1322 | dependencies = [ 1323 | "halfbrown", 1324 | "lexical-core", 1325 | "serde", 1326 | "serde_json", 1327 | "simdutf8", 1328 | "value-trait 0.5.1", 1329 | ] 1330 | 1331 | [[package]] 1332 | name = "simdutf8" 1333 | version = "0.1.4" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" 1336 | 1337 | [[package]] 1338 | name = "slab" 1339 | version = "0.4.7" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 1342 | dependencies = [ 1343 | "autocfg", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "smallvec" 1348 | version = "1.10.0" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 1351 | 1352 | [[package]] 1353 | name = "socket2" 1354 | version = "0.4.7" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 1357 | dependencies = [ 1358 | "libc", 1359 | "winapi", 1360 | ] 1361 | 1362 | [[package]] 1363 | name = "spin" 1364 | version = "0.5.2" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1367 | 1368 | [[package]] 1369 | name = "spin" 1370 | version = "0.9.4" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" 1373 | dependencies = [ 1374 | "lock_api", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "static_assertions" 1379 | version = "1.1.0" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1382 | 1383 | [[package]] 1384 | name = "syn" 1385 | version = "1.0.105" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 1388 | dependencies = [ 1389 | "proc-macro2", 1390 | "quote", 1391 | "unicode-ident", 1392 | ] 1393 | 1394 | [[package]] 1395 | name = "tcp-stream" 1396 | version = "0.24.4" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "09a4b0a70bac0a58ca6a7659d1328e34ee462339c70b0fa49f72bad1f278910a" 1399 | dependencies = [ 1400 | "cfg-if", 1401 | ] 1402 | 1403 | [[package]] 1404 | name = "thiserror" 1405 | version = "1.0.37" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 1408 | dependencies = [ 1409 | "thiserror-impl", 1410 | ] 1411 | 1412 | [[package]] 1413 | name = "thiserror-impl" 1414 | version = "1.0.37" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 1417 | dependencies = [ 1418 | "proc-macro2", 1419 | "quote", 1420 | "syn", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "thread_local" 1425 | version = "1.1.4" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 1428 | dependencies = [ 1429 | "once_cell", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "time" 1434 | version = "0.3.17" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" 1437 | dependencies = [ 1438 | "itoa", 1439 | "serde", 1440 | "time-core", 1441 | "time-macros", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "time-core" 1446 | version = "0.1.0" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" 1449 | 1450 | [[package]] 1451 | name = "time-macros" 1452 | version = "0.2.6" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" 1455 | dependencies = [ 1456 | "time-core", 1457 | ] 1458 | 1459 | [[package]] 1460 | name = "tinyvec" 1461 | version = "1.6.0" 1462 | source = "registry+https://github.com/rust-lang/crates.io-index" 1463 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1464 | dependencies = [ 1465 | "tinyvec_macros", 1466 | ] 1467 | 1468 | [[package]] 1469 | name = "tinyvec_macros" 1470 | version = "0.1.0" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1473 | 1474 | [[package]] 1475 | name = "tokio" 1476 | version = "1.22.0" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" 1479 | dependencies = [ 1480 | "autocfg", 1481 | "bytes", 1482 | "libc", 1483 | "memchr", 1484 | "mio", 1485 | "num_cpus", 1486 | "pin-project-lite", 1487 | "signal-hook-registry", 1488 | "socket2", 1489 | "tokio-macros", 1490 | "winapi", 1491 | ] 1492 | 1493 | [[package]] 1494 | name = "tokio-macros" 1495 | version = "1.8.2" 1496 | source = "registry+https://github.com/rust-lang/crates.io-index" 1497 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 1498 | dependencies = [ 1499 | "proc-macro2", 1500 | "quote", 1501 | "syn", 1502 | ] 1503 | 1504 | [[package]] 1505 | name = "tokio-rustls" 1506 | version = "0.23.4" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" 1509 | dependencies = [ 1510 | "rustls", 1511 | "tokio", 1512 | "webpki", 1513 | ] 1514 | 1515 | [[package]] 1516 | name = "tokio-tungstenite" 1517 | version = "0.17.2" 1518 | source = "registry+https://github.com/rust-lang/crates.io-index" 1519 | checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" 1520 | dependencies = [ 1521 | "futures-util", 1522 | "log", 1523 | "rustls", 1524 | "tokio", 1525 | "tokio-rustls", 1526 | "tungstenite", 1527 | "webpki", 1528 | "webpki-roots", 1529 | ] 1530 | 1531 | [[package]] 1532 | name = "tokio-util" 1533 | version = "0.7.4" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" 1536 | dependencies = [ 1537 | "bytes", 1538 | "futures-core", 1539 | "futures-sink", 1540 | "pin-project-lite", 1541 | "tokio", 1542 | "tracing", 1543 | ] 1544 | 1545 | [[package]] 1546 | name = "tower-service" 1547 | version = "0.3.2" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1550 | 1551 | [[package]] 1552 | name = "tracing" 1553 | version = "0.1.37" 1554 | source = "registry+https://github.com/rust-lang/crates.io-index" 1555 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1556 | dependencies = [ 1557 | "cfg-if", 1558 | "pin-project-lite", 1559 | "tracing-attributes", 1560 | "tracing-core", 1561 | ] 1562 | 1563 | [[package]] 1564 | name = "tracing-attributes" 1565 | version = "0.1.23" 1566 | source = "registry+https://github.com/rust-lang/crates.io-index" 1567 | checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" 1568 | dependencies = [ 1569 | "proc-macro2", 1570 | "quote", 1571 | "syn", 1572 | ] 1573 | 1574 | [[package]] 1575 | name = "tracing-core" 1576 | version = "0.1.30" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1579 | dependencies = [ 1580 | "once_cell", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "tracing-subscriber" 1585 | version = "0.3.16" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" 1588 | dependencies = [ 1589 | "nu-ansi-term", 1590 | "sharded-slab", 1591 | "thread_local", 1592 | "tracing-core", 1593 | ] 1594 | 1595 | [[package]] 1596 | name = "try-lock" 1597 | version = "0.2.3" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1600 | 1601 | [[package]] 1602 | name = "tungstenite" 1603 | version = "0.17.3" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" 1606 | dependencies = [ 1607 | "base64", 1608 | "byteorder", 1609 | "bytes", 1610 | "http", 1611 | "httparse", 1612 | "log", 1613 | "rand", 1614 | "rustls", 1615 | "sha-1", 1616 | "thiserror", 1617 | "url", 1618 | "utf-8", 1619 | "webpki", 1620 | ] 1621 | 1622 | [[package]] 1623 | name = "twilight-dispatch" 1624 | version = "0.4.6" 1625 | dependencies = [ 1626 | "dotenv", 1627 | "futures-util", 1628 | "hyper", 1629 | "lapin", 1630 | "lazy_static", 1631 | "prometheus", 1632 | "redis", 1633 | "serde", 1634 | "serde_repr", 1635 | "simd-json 0.7.0", 1636 | "time", 1637 | "tokio", 1638 | "tracing", 1639 | "tracing-subscriber", 1640 | "twilight-gateway", 1641 | "twilight-http", 1642 | "twilight-model", 1643 | ] 1644 | 1645 | [[package]] 1646 | name = "twilight-gateway" 1647 | version = "0.14.0" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "ba43f8e8dc9f92f61c9ac4d339f7a8483c3ebef2b4f56fe62fb19e151dc79dae" 1650 | dependencies = [ 1651 | "bitflags", 1652 | "flate2", 1653 | "futures-util", 1654 | "leaky-bucket-lite", 1655 | "rustls", 1656 | "serde", 1657 | "serde_json", 1658 | "simd-json 0.6.0", 1659 | "tokio", 1660 | "tokio-tungstenite", 1661 | "tracing", 1662 | "twilight-gateway-queue", 1663 | "twilight-http", 1664 | "twilight-model", 1665 | "url", 1666 | "webpki-roots", 1667 | ] 1668 | 1669 | [[package]] 1670 | name = "twilight-gateway-queue" 1671 | version = "0.14.0" 1672 | source = "registry+https://github.com/rust-lang/crates.io-index" 1673 | checksum = "21c3de23d1819c451ea9963770115628e8f9376e23254e108c84ccf8adff1b21" 1674 | dependencies = [ 1675 | "tokio", 1676 | "tracing", 1677 | ] 1678 | 1679 | [[package]] 1680 | name = "twilight-http" 1681 | version = "0.14.0" 1682 | source = "registry+https://github.com/rust-lang/crates.io-index" 1683 | checksum = "6537962cfaaa433e18c4805e4d071ef175026bebaff933ce3109a467795e1512" 1684 | dependencies = [ 1685 | "hyper", 1686 | "hyper-rustls", 1687 | "percent-encoding", 1688 | "rand", 1689 | "serde", 1690 | "serde_json", 1691 | "simd-json 0.6.0", 1692 | "tokio", 1693 | "tracing", 1694 | "twilight-http-ratelimiting", 1695 | "twilight-model", 1696 | "twilight-validate", 1697 | ] 1698 | 1699 | [[package]] 1700 | name = "twilight-http-ratelimiting" 1701 | version = "0.14.0" 1702 | source = "registry+https://github.com/rust-lang/crates.io-index" 1703 | checksum = "65c4b3d51ada9826a7d4070374388769586f1b9fee81cf57ca2deb0f399cff5c" 1704 | dependencies = [ 1705 | "futures-util", 1706 | "http", 1707 | "tokio", 1708 | "tracing", 1709 | ] 1710 | 1711 | [[package]] 1712 | name = "twilight-model" 1713 | version = "0.14.0" 1714 | source = "registry+https://github.com/rust-lang/crates.io-index" 1715 | checksum = "a3ccebf3a29754a19a0af5728bb0e5f1c86a3a379058ea233a09c8c67a62cd6e" 1716 | dependencies = [ 1717 | "bitflags", 1718 | "serde", 1719 | "serde-value", 1720 | "serde_repr", 1721 | "time", 1722 | "tracing", 1723 | ] 1724 | 1725 | [[package]] 1726 | name = "twilight-validate" 1727 | version = "0.14.0" 1728 | source = "registry+https://github.com/rust-lang/crates.io-index" 1729 | checksum = "c897f0cd4f73261b8a68724f9ae94795705f4473e7defeb0515890a435c691ce" 1730 | dependencies = [ 1731 | "twilight-model", 1732 | ] 1733 | 1734 | [[package]] 1735 | name = "typenum" 1736 | version = "1.15.0" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 1739 | 1740 | [[package]] 1741 | name = "unicode-bidi" 1742 | version = "0.3.8" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1745 | 1746 | [[package]] 1747 | name = "unicode-ident" 1748 | version = "1.0.5" 1749 | source = "registry+https://github.com/rust-lang/crates.io-index" 1750 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 1751 | 1752 | [[package]] 1753 | name = "unicode-normalization" 1754 | version = "0.1.22" 1755 | source = "registry+https://github.com/rust-lang/crates.io-index" 1756 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1757 | dependencies = [ 1758 | "tinyvec", 1759 | ] 1760 | 1761 | [[package]] 1762 | name = "untrusted" 1763 | version = "0.7.1" 1764 | source = "registry+https://github.com/rust-lang/crates.io-index" 1765 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1766 | 1767 | [[package]] 1768 | name = "url" 1769 | version = "2.3.1" 1770 | source = "registry+https://github.com/rust-lang/crates.io-index" 1771 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1772 | dependencies = [ 1773 | "form_urlencoded", 1774 | "idna", 1775 | "percent-encoding", 1776 | ] 1777 | 1778 | [[package]] 1779 | name = "utf-8" 1780 | version = "0.7.6" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1783 | 1784 | [[package]] 1785 | name = "value-trait" 1786 | version = "0.4.0" 1787 | source = "registry+https://github.com/rust-lang/crates.io-index" 1788 | checksum = "c0a635407649b66e125e4d2ffd208153210179f8c7c8b71c030aa2ad3eeb4c8f" 1789 | dependencies = [ 1790 | "float-cmp", 1791 | "halfbrown", 1792 | "itoa", 1793 | "ryu", 1794 | ] 1795 | 1796 | [[package]] 1797 | name = "value-trait" 1798 | version = "0.5.1" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "995de1aa349a0dc50f4aa40870dce12961a30229027230bad09acd2843edbe9e" 1801 | dependencies = [ 1802 | "float-cmp", 1803 | "halfbrown", 1804 | "itoa", 1805 | "ryu", 1806 | ] 1807 | 1808 | [[package]] 1809 | name = "version_check" 1810 | version = "0.9.4" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1813 | 1814 | [[package]] 1815 | name = "waker-fn" 1816 | version = "1.1.0" 1817 | source = "registry+https://github.com/rust-lang/crates.io-index" 1818 | checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" 1819 | 1820 | [[package]] 1821 | name = "want" 1822 | version = "0.3.0" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1825 | dependencies = [ 1826 | "log", 1827 | "try-lock", 1828 | ] 1829 | 1830 | [[package]] 1831 | name = "wasi" 1832 | version = "0.11.0+wasi-snapshot-preview1" 1833 | source = "registry+https://github.com/rust-lang/crates.io-index" 1834 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1835 | 1836 | [[package]] 1837 | name = "wasm-bindgen" 1838 | version = "0.2.83" 1839 | source = "registry+https://github.com/rust-lang/crates.io-index" 1840 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 1841 | dependencies = [ 1842 | "cfg-if", 1843 | "wasm-bindgen-macro", 1844 | ] 1845 | 1846 | [[package]] 1847 | name = "wasm-bindgen-backend" 1848 | version = "0.2.83" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 1851 | dependencies = [ 1852 | "bumpalo", 1853 | "log", 1854 | "once_cell", 1855 | "proc-macro2", 1856 | "quote", 1857 | "syn", 1858 | "wasm-bindgen-shared", 1859 | ] 1860 | 1861 | [[package]] 1862 | name = "wasm-bindgen-macro" 1863 | version = "0.2.83" 1864 | source = "registry+https://github.com/rust-lang/crates.io-index" 1865 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 1866 | dependencies = [ 1867 | "quote", 1868 | "wasm-bindgen-macro-support", 1869 | ] 1870 | 1871 | [[package]] 1872 | name = "wasm-bindgen-macro-support" 1873 | version = "0.2.83" 1874 | source = "registry+https://github.com/rust-lang/crates.io-index" 1875 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 1876 | dependencies = [ 1877 | "proc-macro2", 1878 | "quote", 1879 | "syn", 1880 | "wasm-bindgen-backend", 1881 | "wasm-bindgen-shared", 1882 | ] 1883 | 1884 | [[package]] 1885 | name = "wasm-bindgen-shared" 1886 | version = "0.2.83" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 1889 | 1890 | [[package]] 1891 | name = "web-sys" 1892 | version = "0.3.60" 1893 | source = "registry+https://github.com/rust-lang/crates.io-index" 1894 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" 1895 | dependencies = [ 1896 | "js-sys", 1897 | "wasm-bindgen", 1898 | ] 1899 | 1900 | [[package]] 1901 | name = "webpki" 1902 | version = "0.22.0" 1903 | source = "registry+https://github.com/rust-lang/crates.io-index" 1904 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" 1905 | dependencies = [ 1906 | "ring", 1907 | "untrusted", 1908 | ] 1909 | 1910 | [[package]] 1911 | name = "webpki-roots" 1912 | version = "0.22.5" 1913 | source = "registry+https://github.com/rust-lang/crates.io-index" 1914 | checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" 1915 | dependencies = [ 1916 | "webpki", 1917 | ] 1918 | 1919 | [[package]] 1920 | name = "wepoll-ffi" 1921 | version = "0.1.2" 1922 | source = "registry+https://github.com/rust-lang/crates.io-index" 1923 | checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" 1924 | dependencies = [ 1925 | "cc", 1926 | ] 1927 | 1928 | [[package]] 1929 | name = "winapi" 1930 | version = "0.3.9" 1931 | source = "registry+https://github.com/rust-lang/crates.io-index" 1932 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1933 | dependencies = [ 1934 | "winapi-i686-pc-windows-gnu", 1935 | "winapi-x86_64-pc-windows-gnu", 1936 | ] 1937 | 1938 | [[package]] 1939 | name = "winapi-i686-pc-windows-gnu" 1940 | version = "0.4.0" 1941 | source = "registry+https://github.com/rust-lang/crates.io-index" 1942 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1943 | 1944 | [[package]] 1945 | name = "winapi-x86_64-pc-windows-gnu" 1946 | version = "0.4.0" 1947 | source = "registry+https://github.com/rust-lang/crates.io-index" 1948 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1949 | 1950 | [[package]] 1951 | name = "windows-sys" 1952 | version = "0.42.0" 1953 | source = "registry+https://github.com/rust-lang/crates.io-index" 1954 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1955 | dependencies = [ 1956 | "windows_aarch64_gnullvm", 1957 | "windows_aarch64_msvc", 1958 | "windows_i686_gnu", 1959 | "windows_i686_msvc", 1960 | "windows_x86_64_gnu", 1961 | "windows_x86_64_gnullvm", 1962 | "windows_x86_64_msvc", 1963 | ] 1964 | 1965 | [[package]] 1966 | name = "windows_aarch64_gnullvm" 1967 | version = "0.42.0" 1968 | source = "registry+https://github.com/rust-lang/crates.io-index" 1969 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 1970 | 1971 | [[package]] 1972 | name = "windows_aarch64_msvc" 1973 | version = "0.42.0" 1974 | source = "registry+https://github.com/rust-lang/crates.io-index" 1975 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 1976 | 1977 | [[package]] 1978 | name = "windows_i686_gnu" 1979 | version = "0.42.0" 1980 | source = "registry+https://github.com/rust-lang/crates.io-index" 1981 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 1982 | 1983 | [[package]] 1984 | name = "windows_i686_msvc" 1985 | version = "0.42.0" 1986 | source = "registry+https://github.com/rust-lang/crates.io-index" 1987 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 1988 | 1989 | [[package]] 1990 | name = "windows_x86_64_gnu" 1991 | version = "0.42.0" 1992 | source = "registry+https://github.com/rust-lang/crates.io-index" 1993 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 1994 | 1995 | [[package]] 1996 | name = "windows_x86_64_gnullvm" 1997 | version = "0.42.0" 1998 | source = "registry+https://github.com/rust-lang/crates.io-index" 1999 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 2000 | 2001 | [[package]] 2002 | name = "windows_x86_64_msvc" 2003 | version = "0.42.0" 2004 | source = "registry+https://github.com/rust-lang/crates.io-index" 2005 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 2006 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "twilight-dispatch" 3 | version = "0.4.6" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | dotenv = { version = "0.15", default-features = false } 8 | futures-util = { version = "0.3", default-features = false } 9 | hyper = { version = "0.14", default-features = false, features = ["server", "tcp", "http1"] } 10 | lapin = { version = "2.0", default-features = false } 11 | lazy_static = { version = "1.4", default-features = false } 12 | prometheus = { version = "0.13", default-features = false, features = ["process"] } 13 | redis = { version = "0.22", default-features = false, features = ["tokio-comp"] } 14 | serde = { version = "1.0", default-features = false } 15 | serde_repr = { version = "0.1", default-features = false } 16 | simd-json = { version = "0.7", default-features = false, features = ["serde_impl"] } 17 | time = { version = "0.3", default-features = false, features = ["std", "formatting"] } 18 | tokio = { version = "1.2", default-features = false, features = ["rt-multi-thread", "macros", "signal"] } 19 | tracing = { version = "0.1", default-features = false } 20 | tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] } 21 | twilight-gateway = { version = "0.14", default-features = false, features = ["rustls-webpki-roots", "simd-json", "zlib-simd"] } 22 | twilight-http = { version = "0.14", default-features = false, features = ["simd-json"] } 23 | twilight-model = { version = "0.14", default-features = false } 24 | 25 | [profile.release] 26 | codegen-units = 1 27 | debug = false 28 | incremental = false 29 | lto = true 30 | opt-level = 3 31 | panic = "abort" 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.65-alpine AS builder 2 | 3 | ENV RUSTFLAGS="-C target-cpu=haswell" 4 | 5 | RUN apk add --no-cache gcc g++ musl-dev cmake make 6 | 7 | WORKDIR /build 8 | 9 | COPY .cargo ./.cargo 10 | COPY Cargo.toml Cargo.lock ./ 11 | 12 | RUN mkdir src 13 | RUN echo 'fn main() {}' > ./src/main.rs 14 | RUN cargo build --release 15 | RUN rm -f target/release/deps/twilight_dispatch* 16 | 17 | COPY src ./src 18 | 19 | RUN cargo build --release 20 | 21 | FROM alpine:3.17 22 | 23 | RUN apk add --no-cache dumb-init 24 | 25 | WORKDIR /app 26 | 27 | COPY --from=builder /build/target/release/twilight-dispatch ./ 28 | 29 | EXPOSE 8005 30 | 31 | ENTRYPOINT ["/usr/bin/dumb-init", "--"] 32 | CMD ["./twilight-dispatch"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2019, 2020 (c) The Twilight Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twilight-dispatch 2 | 3 | A standalone service for connecting to the Discord gateway, written in Rust with the 4 | [twilight](https://github.com/twilight-rs/twilight) crate. This allows the gateway logic to be 5 | separate from your application layer, bringing many benefits such as the following. 6 | 7 | 1. Minimal downtime. You will almost never need to restart the gateway service, allowing absolute 8 | 100% uptime for your bot. Even when a restart is required, twilight-dispatch will resume the 9 | sessions, so you will not lose a single event. 10 | 11 | 2. Flexibility and scalability. Since the events received are in the exchange, you can route them to 12 | different queues based on event type and consume them from multiple processes. This allows for 13 | load balancing between your service workers. 14 | 15 | If you encounter any issues while running the service, please feel free create an issue here, or you 16 | can contact chamburr on Discord. We will try our best to help you. 17 | 18 | ## Features 19 | 20 | - Low CPU and RAM footprint 21 | - Large bot sharding support 22 | - Resumable sessions after restart 23 | - Discord channel status logging 24 | - Prometheus metrics 25 | - Shard information in Redis 26 | - State cache with Redis 27 | - Docker container support 28 | 29 | ## Implementation 30 | 31 | ### Events 32 | 33 | Gateway events are forwarded to and from RabbitMQ. 34 | 35 | Events are sent to a topic exchange `gateway`, with the event name as the routing key. By default, 36 | there is a `gateway.recv` channel bound to all messages from the exchange. The decoded and 37 | decompressed dispatch events from the gateway will be available in the queue. 38 | 39 | To send events to the gateway, connect to the channel `gateway.send`, then publish a message like 40 | the following. Note that the outermost `op` is not the Discord gateway OP code. The only option for 41 | now is 0, but there may be others in the future to reconnect to a shard, etc. 42 | 43 | ```json 44 | { 45 | "op": 0, 46 | "shard": 0, 47 | "data": { 48 | "op": 4, 49 | "d": { 50 | "guild_id": "41771983423143937", 51 | "channel_id": "127121515262115840", 52 | "self_mute": false, 53 | "self_deaf": false 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | ### State Cache 60 | 61 | State caching with Redis is supported out of the box. 62 | 63 | The objects available are in the table below. All values are stored in the plain text form, and you 64 | will need to properly deserialize them before using. Some objects such as presence and member could 65 | be missing if you disable them in the configurations. 66 | 67 | Furthermore, when the old state option is enabled, there will be an additional field for gateway 68 | events in the message queue, `old`, containing the previous state (only if it exists). This could 69 | be useful for the `MESSAGE_DELETE` event and such. 70 | 71 | | Key | Description | 72 | | ------------------------------- | -------------------------------- | 73 | | `bot_user` | Bot user object. | 74 | | `guild:guild_id` | Guild object. | 75 | | `role:guild_id:role_id` | Guild role object. | 76 | | `emoji:guild_id:emoji_id` | Guild emoji object. | 77 | | `member:guild_id:user_id` | Guild member object. | 78 | | `presence:guild_id:user_id` | Guild member presence object. | 79 | | `voice:guild_id:user_id` | Guild member voice state object. | 80 | | `channel:channel_id` | Channel object. | 81 | | `message:channel_id:message_id` | Channel message object. | 82 | 83 | There are additionally some helper keys for state cache below, stored as sets. 84 | 85 | | Key | Description | 86 | | ------------------------- | -------------------------------------- | 87 | | `guild_keys` | List of guild keys. | 88 | | `guild_keys:guild_id` | List of keys related to a guild. | 89 | | `channel_keys` | List of guild channel keys. | 90 | | `role_keys` | List of guild role keys. | 91 | | `emoji_keys` | List of guild emoji keys. | 92 | | `member_keys` | List of guild member keys. | 93 | | `presence_keys` | List of guild member presence keys. | 94 | | `voice_keys` | List of guild member voice state keys. | 95 | | `channel_keys:channel_id` | List of keys related to a channel. | 96 | | `message_keys` | List of channel message keys. | 97 | 98 | ### Information 99 | 100 | Information related to the gateway are stored in Redis. 101 | 102 | | Key | Description | 103 | | ------------------ | ----------------------------------- | 104 | | `gateway_sessions` | Array of shard session information. | 105 | | `gateway_statuses` | Array of shard status information. | 106 | | `gateway_started` | Timestamp when the service started. | 107 | | `gateway_shards` | Total number of shards being ran. | 108 | 109 | ## Installing 110 | 111 | These are the steps to installing and running the service. 112 | 113 | ### Prerequisites 114 | 115 | - [RabbitMQ](https://www.rabbitmq.com/download.html) 116 | - [Redis](https://redis.io/download) 117 | - [Rust](https://www.rust-lang.org/tools/install) 118 | 119 | ### Configuration 120 | 121 | The gateway can be configured with environmental variables or a `.env` file at the root of the 122 | project. An example can be found [here](.env.example). 123 | 124 | ### Running 125 | 126 | Run the following commands to start the service. 127 | 128 | ``` 129 | cargo build --release 130 | cargo run --release 131 | ``` 132 | 133 | ### Running (Docker) 134 | 135 | If you prefer, the service can also be ran with Docker. Run the following commands to start the 136 | container. 137 | 138 | ``` 139 | docker build -t twilight-dispatch:latest . 140 | docker run -it --network host --env-file .env twilight-dispatch:latest 141 | ``` 142 | 143 | The Docker image is also available here: https://ghcr.io/chamburr/twilight-dispatch. 144 | 145 | Note: You do not need to install Rust if you are using Docker. 146 | 147 | ## Related Resources 148 | 149 | Twilight: https://github.com/twilight-rs/twilight 150 | 151 | ## License 152 | 153 | This project is licensed under [ISC License](LICENSE). 154 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Client examples 2 | 3 | There are examples to use twilight-dispatch with various libraries here. 4 | 5 | If you don't find the library you're using, feel free to implement one yourself and add it here! 6 | -------------------------------------------------------------------------------- /examples/discordpy/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.py] 2 | max_line_length = 120 3 | -------------------------------------------------------------------------------- /examples/discordpy/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Custom 141 | config.py 142 | -------------------------------------------------------------------------------- /examples/discordpy/classes/bot.py: -------------------------------------------------------------------------------- 1 | import aio_pika 2 | import asyncio 3 | import config 4 | import inspect 5 | import logging 6 | import orjson 7 | import sys 8 | import traceback 9 | import zangy 10 | 11 | from classes.misc import Status, Session 12 | from classes.state import State 13 | from discord import utils 14 | from discord.ext import commands 15 | from discord.ext.commands import DefaultHelpCommand, Context 16 | from discord.ext.commands.core import _CaseInsensitiveDict 17 | from discord.ext.commands.view import StringView 18 | from discord.gateway import DiscordWebSocket 19 | from discord.http import HTTPClient 20 | from discord.utils import parse_time, to_json 21 | 22 | log = logging.getLogger(__name__) 23 | 24 | 25 | class Bot(commands.AutoShardedBot): 26 | def __init__(self, command_prefix, help_command=DefaultHelpCommand(), description=None, **kwargs): 27 | self.command_prefix = command_prefix 28 | self.extra_events = {} 29 | self._BotBase__cogs = {} 30 | self._BotBase__extensions = {} 31 | self._checks = [] 32 | self._check_once = [] 33 | self._before_invoke = None 34 | self._after_invoke = None 35 | self._help_command = None 36 | self.description = inspect.cleandoc(description) if description else "" 37 | self.owner_id = kwargs.get("owner_id") 38 | self.owner_ids = kwargs.get("owner_ids", set()) 39 | self._skip_check = lambda x, y: x == y 40 | self.help_command = help_command 41 | self.case_insensitive = kwargs.get("case_insensitive", False) 42 | self.all_commands = _CaseInsensitiveDict() if self.case_insensitive else {} 43 | 44 | self.ws = None 45 | self.loop = asyncio.get_event_loop() 46 | self.http = HTTPClient(None, loop=self.loop) 47 | 48 | self._handlers = {"ready": self._handle_ready} 49 | self._hooks = {} 50 | self._listeners = {} 51 | 52 | self._connection = None 53 | self._closed = False 54 | self._ready = asyncio.Event() 55 | 56 | self._redis = None 57 | self._amqp = None 58 | self._amqp_channel = None 59 | self._amqp_queue = None 60 | 61 | @property 62 | def config(self): 63 | return config 64 | 65 | async def user(self): 66 | return await self._connection.user() 67 | 68 | async def users(self): 69 | return await self._connection._users() 70 | 71 | async def guilds(self): 72 | return await self._connection.guilds() 73 | 74 | async def emojis(self): 75 | return await self._connection.emojis() 76 | 77 | async def cached_messages(self): 78 | return await self._connection._messages() 79 | 80 | async def private_channels(self): 81 | return await self._connection.private_channels() 82 | 83 | async def shard_count(self): 84 | return int(await self._redis.get("gateway_shards")) 85 | 86 | async def started(self): 87 | return parse_time(str(await self._connection._get("gateway_started").split(".")[0])) 88 | 89 | async def statuses(self): 90 | return [Status(x) for x in await self._connection._get("gateway_statuses")] 91 | 92 | async def sessions(self): 93 | return {int(x): Session(y) for x, y in (await self._connection._get("gateway_sessions")).items()} 94 | 95 | async def get_channel(self, channel_id): 96 | return await self._connection.get_channel(channel_id) 97 | 98 | async def get_guild(self, guild_id): 99 | return await self._connection._get_guild(guild_id) 100 | 101 | async def get_user(self, user_id): 102 | return await self._connection.get_user(user_id) 103 | 104 | async def get_emoji(self, emoji_id): 105 | return await self._connection.get_emoji(emoji_id) 106 | 107 | async def get_all_channels(self): 108 | for guild in await self.guilds(): 109 | for channel in await guild.channels(): 110 | yield channel 111 | 112 | async def get_all_members(self): 113 | for guild in await self.guilds(): 114 | for member in await guild.members(): 115 | yield member 116 | 117 | async def _get_state(self, **options): 118 | return State( 119 | dispatch=self.dispatch, 120 | handlers=self._handlers, 121 | hooks=self._hooks, 122 | http=self.http, 123 | loop=self.loop, 124 | redis=self._redis, 125 | shard_count=await self.shard_count(), 126 | **options, 127 | ) 128 | 129 | async def get_context(self, message, *, cls=Context): 130 | view = StringView(message.content) 131 | ctx = cls(prefix=None, view=view, bot=self, message=message) 132 | 133 | if self._skip_check((await message.author()).id, (await self.user()).id): 134 | return ctx 135 | 136 | prefix = await self.get_prefix(message) 137 | invoked_prefix = prefix 138 | 139 | if isinstance(prefix, str): 140 | if not view.skip_string(prefix): 141 | return ctx 142 | else: 143 | try: 144 | if message.content.startswith(tuple(prefix)): 145 | invoked_prefix = utils.find(view.skip_string, prefix) 146 | else: 147 | return ctx 148 | 149 | except TypeError: 150 | if not isinstance(prefix, list): 151 | raise TypeError("get_prefix must return either a string or a list of string, " 152 | "not {}".format(prefix.__class__.__name__)) 153 | 154 | for value in prefix: 155 | if not isinstance(value, str): 156 | raise TypeError("Iterable command_prefix or list returned from get_prefix must " 157 | "contain only strings, not {}".format(value.__class__.__name__)) 158 | 159 | raise 160 | 161 | invoker = view.get_word() 162 | ctx.invoked_with = invoker 163 | ctx.prefix = invoked_prefix 164 | ctx.command = self.all_commands.get(invoker) 165 | return ctx 166 | 167 | async def process_commands(self, message): 168 | if (await message.author()).bot: 169 | return 170 | 171 | ctx = await self.get_context(message) 172 | await self.invoke(ctx) 173 | 174 | async def receive_message(self, msg): 175 | self.ws._dispatch("socket_raw_receive", msg) 176 | 177 | msg = orjson.loads(msg) 178 | 179 | self.ws._dispatch("socket_response", msg) 180 | 181 | op = msg.get("op") 182 | data = msg.get("d") 183 | event = msg.get("t") 184 | old = msg.get("old") 185 | 186 | if op != self.ws.DISPATCH: 187 | return 188 | 189 | try: 190 | func = self.ws._discord_parsers[event] 191 | except KeyError: 192 | log.debug("Unknown event %s.", event) 193 | else: 194 | try: 195 | await func(data, old) 196 | except asyncio.CancelledError: 197 | pass 198 | except Exception: 199 | try: 200 | await self.on_error(event) 201 | except asyncio.CancelledError: 202 | pass 203 | 204 | removed = [] 205 | for index, entry in enumerate(self.ws._dispatch_listeners): 206 | if entry.event != event: 207 | continue 208 | 209 | future = entry.future 210 | if future.cancelled(): 211 | removed.append(index) 212 | continue 213 | 214 | try: 215 | valid = entry.predicate(data) 216 | except Exception as exc: 217 | future.set_exception(exc) 218 | removed.append(index) 219 | else: 220 | if valid: 221 | ret = data if entry.result is None else entry.result(data) 222 | future.set_result(ret) 223 | removed.append(index) 224 | 225 | for index in reversed(removed): 226 | del self.ws._dispatch_listeners[index] 227 | 228 | async def send_message(self, msg): 229 | data = to_json(msg) 230 | self.ws._dispatch("socket_raw_send", data) 231 | await self._amqp_channel.default_exchange.publish(aio_pika.Message(body=data), routing_key="gateway.send") 232 | 233 | async def start(self): 234 | log.info("Starting...") 235 | 236 | self._redis = await zangy.create_pool(self.config.redis_url, 5) 237 | self._amqp = await aio_pika.connect_robust(self.config.amqp_url) 238 | self._amqp_channel = await self._amqp.channel() 239 | self._amqp_queue = await self._amqp_channel.get_queue("gateway.recv") 240 | 241 | self._connection = await self._get_state() 242 | self._connection._get_client = lambda: self 243 | 244 | self.ws = DiscordWebSocket(socket=None, loop=self.loop) 245 | self.ws.token = self.http.token 246 | self.ws._connection = self._connection 247 | self.ws._discord_parsers = self._connection.parsers 248 | self.ws._dispatch = self.dispatch 249 | self.ws.call_hooks = self._connection.call_hooks 250 | 251 | await self.http.static_login(self.config.token, bot=True) 252 | 253 | for extension in self.config.cogs: 254 | try: 255 | self.load_extension("cogs." + extension) 256 | except Exception: 257 | log.error(f"Failed to load extension {extension}.", file=sys.stderr) 258 | log.error(traceback.print_exc()) 259 | 260 | async with self._amqp_queue.iterator() as queue_iter: 261 | async for message in queue_iter: 262 | async with message.process(ignore_processed=True): 263 | await self.receive_message(message.body) 264 | message.ack() 265 | -------------------------------------------------------------------------------- /examples/discordpy/classes/guild.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from discord import guild, utils 4 | from discord.channel import _channel_factory 5 | from discord.enums import * 6 | from discord.enums import try_enum 7 | from discord.member import Member, VoiceState 8 | from discord.role import Role 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class Guild(guild.Guild): 14 | def __init__(self, *, data, state): 15 | self._state = state 16 | self._from_data(data) 17 | 18 | def _add_channel(self, channel): 19 | return 20 | 21 | def _remove_channel(self, channel): 22 | return 23 | 24 | def _add_member(self, member): 25 | return 26 | 27 | def _remove_member(self, member): 28 | return 29 | 30 | def _update_voice_state(self, data, channel_id): 31 | return 32 | 33 | def _add_role(self, role): 34 | return 35 | 36 | def _remove_role(self, role_id): 37 | return 38 | 39 | def _from_data(self, guild): 40 | member_count = guild.get("member_count", None) 41 | if member_count is not None: 42 | self._member_count = member_count 43 | 44 | self.name = guild.get("name") 45 | self.region = try_enum(VoiceRegion, guild.get("region")) 46 | self.verification_level = try_enum(VerificationLevel, guild.get("verification_level")) 47 | self.default_notifications = try_enum(NotificationLevel, guild.get("default_message_notifications")) 48 | self.explicit_content_filter = try_enum(ContentFilter, guild.get("explicit_content_filter", 0)) 49 | self.afk_timeout = guild.get("afk_timeout") 50 | self.icon = guild.get("icon") 51 | self.banner = guild.get("banner") 52 | self.unavailable = guild.get("unavailable", False) 53 | self.id = int(guild["id"]) 54 | self.mfa_level = guild.get("mfa_level") 55 | self.features = guild.get("features", []) 56 | self.splash = guild.get("splash") 57 | self._system_channel_id = utils._get_as_snowflake(guild, "system_channel_id") 58 | self.description = guild.get("description") 59 | self.max_presences = guild.get("max_presences") 60 | self.max_members = guild.get("max_members") 61 | self.max_video_channel_users = guild.get("max_video_channel_users") 62 | self.premium_tier = guild.get("premium_tier", 0) 63 | self.premium_subscription_count = guild.get("premium_subscription_count") or 0 64 | self._system_channel_flags = guild.get("system_channel_flags", 0) 65 | self.preferred_locale = guild.get("preferred_locale") 66 | self.discovery_splash = guild.get("discovery_splash") 67 | self._rules_channel_id = utils._get_as_snowflake(guild, "rules_channel_id") 68 | self._public_updates_channel_id = utils._get_as_snowflake(guild, "public_updates_channel_id") 69 | self._large = None if member_count is None else self._member_count >= 250 70 | self.owner_id = utils._get_as_snowflake(guild, "owner_id") 71 | self._afk_channel_id = utils._get_as_snowflake(guild, "afk_channel_id") 72 | 73 | async def _channels(self): 74 | channels = [] 75 | for channel in await self._state._members_get_all("guild", key_id=self.id, name="channel"): 76 | factory, _ = _channel_factory(channel["type"]) 77 | channels.append(factory(guild=self, state=self._state, data=channel)) 78 | return channels 79 | 80 | async def _members(self): 81 | members = [] 82 | for member in await self._state._members_get_all("guild", key_id=self.id, name="member"): 83 | members.append(Member(guild=self, state=self._state, data=member)) 84 | return members 85 | 86 | async def _roles(self): 87 | roles = [] 88 | for role in await self._state._members_get_all("guild", key_id=self.id, name="role"): 89 | roles.append(Role(guild=self, state=self._state, data=role)) 90 | return roles 91 | 92 | async def _voice_states(self): 93 | voices = [] 94 | for voice in await self._state._members_get_all("guild", key_id=self.id, name="voice"): 95 | if voice["channel_id"]: 96 | channel = await self.get_channel(int(voice["channel_id"])) 97 | if channel: 98 | voices.append(VoiceState(channel=channel, data=voice)) 99 | else: 100 | voices.append(VoiceState(channel=None, data=voice)) 101 | return voices 102 | 103 | async def _voice_state_for(self, user_id): 104 | result = await self._state._get(f"voice:{self.id}:{user_id}") 105 | if result and result["channel_id"]: 106 | channel = await self.get_channel(int(result["channel_id"])) 107 | if channel: 108 | result = VoiceState(channel=channel, data=result) 109 | elif result: 110 | result = VoiceState(channel=None, data=result) 111 | return result 112 | 113 | async def channels(self): 114 | return await self._channels() 115 | 116 | async def get_channel(self, channel_id): 117 | result = await self._state._get(f"channel:{channel_id}") 118 | if not result: 119 | return None 120 | factory, _ = _channel_factory(result["type"]) 121 | return factory(guild=self, state=self._state, data=result) 122 | 123 | async def afk_channel(self): 124 | channel_id = self._afk_channel_id 125 | return channel_id and await self.get_channel(channel_id) 126 | 127 | async def system_channel(self): 128 | channel_id = self._system_channel_id 129 | return channel_id and await self.get_channel(channel_id) 130 | 131 | async def rules_channel(self): 132 | channel_id = self._rules_channel_id 133 | return channel_id and await self.get_channel(channel_id) 134 | 135 | async def public_updates_channel(self): 136 | channel_id = self._public_updates_channel_id 137 | return channel_id and await self.get_channel(channel_id) 138 | 139 | async def members(self): 140 | return await self._members() 141 | 142 | async def get_member(self, user_id): 143 | result = await self._state._get(f"member:{self.id}:{user_id}") 144 | if result: 145 | result = Member(guild=self, state=self._state, data=result) 146 | return result 147 | 148 | async def roles(self): 149 | return await self._roles() 150 | 151 | async def get_role(self, role_id): 152 | result = await self._state._get(f"role:{self.id}:{role_id}") 153 | if result: 154 | Role(guild=self, state=self._state, data=result) 155 | return result 156 | -------------------------------------------------------------------------------- /examples/discordpy/classes/member.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from discord import member 5 | from discord.activity import create_activity 6 | from discord.utils import parse_time 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class Member(member.Member): 12 | def __init__(self, *, data, guild, state): 13 | self._state = state 14 | self._user = state.store_user(data["user"]) 15 | self.guild = guild 16 | self.joined_at = parse_time(data.get("joined_at")) 17 | self.premium_since = parse_time(data.get("premium_since")) 18 | self._update_roles(data) 19 | self.nick = data.get("nick", None) 20 | 21 | async def _presence(self): 22 | return await self._state._get(f"presence:{self.guild.id}:{self._user.id}") or {} 23 | 24 | async def activities(self): 25 | return tuple(map(create_activity, (await self._presence()).get("activities", []))) 26 | 27 | async def _client_status(self): 28 | presence = await self._presence() 29 | status = {sys.intern(x): sys.intern(y) for x, y in presence.get("client_status", {}).items()} 30 | status[None] = sys.intern(presence["status"]) if presence.get("status") else "offline" 31 | return status 32 | -------------------------------------------------------------------------------- /examples/discordpy/classes/message.py: -------------------------------------------------------------------------------- 1 | from discord import utils, message 2 | from discord.embeds import Embed 3 | from discord.enums import MessageType, try_enum 4 | from discord.flags import MessageFlags 5 | from discord.guild import Guild 6 | from discord.member import Member 7 | from discord.message import flatten_handlers, Attachment, MessageReference 8 | from discord.reaction import Reaction 9 | 10 | 11 | @flatten_handlers 12 | class Message(message.Message): 13 | def __init__(self, *, state, channel, data): 14 | self._state = state 15 | self._data = data 16 | self.id = int(data["id"]) 17 | self.webhook_id = utils._get_as_snowflake(data, "webhook_id") 18 | self.reactions = [Reaction(message=self, data=d) for d in data.get("reactions", [])] 19 | self.attachments = [Attachment(data=a, state=self._state) for a in data["attachments"]] 20 | self.embeds = [Embed.from_dict(a) for a in data["embeds"]] 21 | self.application = data.get("application") 22 | self.activity = data.get("activity") 23 | self.channel = channel 24 | self._edited_timestamp = utils.parse_time(data["edited_timestamp"]) 25 | self.type = try_enum(MessageType, data["type"]) 26 | self.pinned = data["pinned"] 27 | self.flags = MessageFlags._from_value(data.get("flags", 0)) 28 | self.mention_everyone = data["mention_everyone"] 29 | self.tts = data["tts"] 30 | self.content = data["content"] 31 | self.nonce = data.get("nonce") 32 | 33 | ref = data.get("message_reference") 34 | self.reference = MessageReference(state, **ref) if ref is not None else None 35 | 36 | for handler in ("call", "flags"): 37 | try: 38 | getattr(self, "_handle_%s" % handler)(data[handler]) 39 | except KeyError: 40 | continue 41 | 42 | async def author(self): 43 | try: 44 | author = self._data["author"] 45 | author = self._state.store_user(author) 46 | if isinstance(self.guild, Guild): 47 | found = await self.guild.get_member(author.id) 48 | if found is not None: 49 | author = found 50 | return author 51 | except KeyError: 52 | return None 53 | 54 | async def member(self): 55 | try: 56 | member = self._data["member"] 57 | author = await self.author() 58 | try: 59 | author._update_from_message(member) 60 | except AttributeError: 61 | author = Member._from_message(message=self, data=member) 62 | return author 63 | except KeyError: 64 | return None 65 | 66 | async def mentions(self): 67 | try: 68 | mentions = self._data["mentions"] 69 | members = [] 70 | guild = self.guild 71 | state = self._state 72 | if not isinstance(guild, Guild): 73 | members = [state.store_user(m) for m in mentions] 74 | else: 75 | for mention in filter(None, mentions): 76 | id_search = int(mention["id"]) 77 | member = await guild.get_member(id_search) 78 | if member is not None: 79 | members.append(member) 80 | else: 81 | members.append(Member._try_upgrade(data=mention, guild=guild, state=state)) 82 | return members 83 | except KeyError: 84 | return [] 85 | 86 | async def role_mentions(self): 87 | try: 88 | mentions = self._data["mention_roles"] 89 | roles = [] 90 | if isinstance(self.guild, Guild): 91 | for role_id in map(int, mentions): 92 | role = await self.guild.get_role(role_id) 93 | if role is not None: 94 | roles.append(role) 95 | return roles 96 | except KeyError: 97 | return [] 98 | -------------------------------------------------------------------------------- /examples/discordpy/classes/misc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from discord.utils import parse_time 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | 8 | class Session: 9 | def __init__(self, data): 10 | self._data = data 11 | 12 | @property 13 | def session_id(self): 14 | return self._data["session_id"] 15 | 16 | @property 17 | def sequence(self): 18 | return self._data["sequence"] 19 | 20 | 21 | class Status: 22 | def __init__(self, data): 23 | self._data = data 24 | 25 | @property 26 | def shard(self): 27 | return self._data["shard"] 28 | 29 | @property 30 | def status(self): 31 | return self._data["status"] 32 | 33 | @property 34 | def latency(self): 35 | return self._data["latency"] 36 | 37 | @property 38 | def last_ack(self): 39 | return parse_time(self._data["last_ack"].split(".")[0]) 40 | -------------------------------------------------------------------------------- /examples/discordpy/classes/state.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import copy 3 | import datetime 4 | import inspect 5 | import logging 6 | import orjson 7 | 8 | from classes.guild import Guild 9 | from classes.message import Message 10 | from discord import utils, Reaction 11 | from discord.channel import DMChannel, TextChannel, _channel_factory 12 | from discord.emoji import Emoji 13 | from discord.enums import ChannelType, try_enum 14 | from discord.invite import Invite 15 | from discord.member import Member, VoiceState 16 | from discord.partial_emoji import PartialEmoji 17 | from discord.raw_models import * 18 | from discord.role import Role 19 | from discord.user import User, ClientUser 20 | 21 | log = logging.getLogger(__name__) 22 | 23 | 24 | class State: 25 | def __init__(self, *, dispatch, handlers, hooks, http, loop, redis=None, shard_count=None, **options): 26 | self.dispatch = dispatch 27 | self.handlers = handlers 28 | self.hooks = hooks 29 | self.http = http 30 | self.loop = loop 31 | self.redis = redis 32 | self.shard_count = shard_count 33 | 34 | self._ready_task = None 35 | self._ready_state = None 36 | self._ready_timeout = options.get("guild_ready_timeout", 2.0) 37 | 38 | self._voice_clients = {} 39 | self._private_channels_by_user = {} 40 | 41 | self.allowed_mentions = options.get("allowed_mentions") 42 | 43 | self.parsers = {} 44 | for attr, func in inspect.getmembers(self): 45 | if attr.startswith("parse_"): 46 | self.parsers[attr[6:].upper()] = func 47 | 48 | async def _get(self, key): 49 | result = await self.redis.get(key) 50 | if result: 51 | result = orjson.loads(result) 52 | if isinstance(result, dict): 53 | result["_key"] = key 54 | if result.get("permission_overwrites"): 55 | result["permission_overwrites"] = [ 56 | { 57 | "id": x["id"], 58 | "type": "role" if x["type"] == 0 else "member", 59 | "allow": int(x["allow"]), 60 | "deny": int(x["deny"]), 61 | } 62 | for x in result["permission_overwrites"] 63 | ] 64 | return result 65 | 66 | async def _members(self, key, key_id=None): 67 | key += "_keys" 68 | if key_id: 69 | key += f":{key_id}" 70 | return [x.decode("utf-8") for x in await self.redis.smembers(key)] 71 | 72 | async def _members_get(self, key, key_id=None, name=None, first=None, second=None, predicate=None): 73 | for match in await self._members(key, key_id): 74 | keys = match.split(":") 75 | if name is None or keys[0] == str(name): 76 | if first is None or (len(keys) >= 2 and keys[1] == str(first)): 77 | if second is None or (len(keys) >= 3 and keys[2] == str(second)): 78 | if predicate is None or predicate(match) is True: 79 | return await self._get(match) 80 | return None 81 | 82 | async def _members_get_all(self, key, key_id=None, name=None, first=None, second=None, predicate=None): 83 | results = [] 84 | for match in await self._members(key, key_id): 85 | keys = match.split(":") 86 | if name is None or keys[0] == str(name): 87 | if first is None or (len(keys) >= 1 and keys[1] == str(first)): 88 | if second is None or (len(keys) >= 2 and keys[2] == str(second)): 89 | if predicate is None or predicate(match) is True: 90 | results.append(await self._get(match)) 91 | return results 92 | 93 | def _key_first(self, obj): 94 | keys = obj["_key"].split(":") 95 | return int(keys[1]) 96 | 97 | async def _users(self): 98 | results = [] 99 | user_ids = [] 100 | for match in await self._members("member"): 101 | user_id = match.split(":")[2] 102 | if user_id not in user_ids: 103 | results.append(await self._get(match)) 104 | user_ids.append(user_id) 105 | return [User(state=self, data=x["user"]) for x in results] 106 | 107 | async def _emojis(self): 108 | results = await self._members_get_all("emoji") 109 | emojis = [] 110 | for result in results: 111 | guild = await self._get_guild(self._key_first(result)) 112 | if guild: 113 | emojis.append(Emoji(guild=guild, state=self, data=result)) 114 | return emojis 115 | 116 | async def _guilds(self): 117 | return [Guild(state=self, data=x) for x in await self._members_get_all("guild")] 118 | 119 | async def _private_channels(self): 120 | return [] 121 | 122 | async def _messages(self): 123 | results = await self._members_get_all("message") 124 | messages = [] 125 | for result in results: 126 | channel = await self.get_channel(int(result["channel_id"])) 127 | if channel: 128 | message = Message(channel=channel, state=self, data=result) 129 | messages.append(message) 130 | return messages 131 | 132 | def process_chunk_requests(self, guild_id, nonce, members, complete): 133 | return 134 | 135 | def call_handlers(self, key, *args, **kwargs): 136 | try: 137 | func = self.handlers[key] 138 | except KeyError: 139 | pass 140 | else: 141 | func(*args, **kwargs) 142 | 143 | async def call_hooks(self, key, *args, **kwargs): 144 | try: 145 | func = self.hooks[key] 146 | except KeyError: 147 | pass 148 | else: 149 | await func(*args, **kwargs) 150 | 151 | async def user(self): 152 | result = await self._get("bot_user") 153 | if result: 154 | result = ClientUser(state=self, data=result) 155 | return result 156 | 157 | async def self_id(self): 158 | return (await self.user()).id 159 | 160 | @property 161 | def intents(self): 162 | return 163 | 164 | @property 165 | def voice_clients(self): 166 | return 167 | 168 | def _get_voice_client(self, guild_id): 169 | return 170 | 171 | def _add_voice_client(self, guild_id, voice): 172 | return 173 | 174 | def _remove_voice_client(self, guild_id): 175 | return 176 | 177 | def _update_references(self, ws): 178 | return 179 | 180 | def store_user(self, data): 181 | return User(state=self, data=data) 182 | 183 | async def get_user(self, user_id): 184 | result = await self._members_get("member", second=user_id) 185 | if result: 186 | result = User(state=self, data=result["user"]) 187 | return result 188 | 189 | def store_emoji(self, guild, data): 190 | return Emoji(guild=guild, state=self, data=data) 191 | 192 | async def guilds(self): 193 | return await self._guilds() 194 | 195 | async def _get_guild(self, guild_id): 196 | result = await self._get(f"guild:{guild_id}") 197 | if result: 198 | result = Guild(state=self, data=result) 199 | return result 200 | 201 | def _add_guild(self, guild): 202 | return 203 | 204 | def _remove_guild(self, guild): 205 | return 206 | 207 | async def emojis(self): 208 | return await self._emojis() 209 | 210 | async def get_emoji(self, emoji_id): 211 | result = await self._members_get("emoji", second=emoji_id) 212 | if result: 213 | guild = await self._get_guild(self._key_first(result)) 214 | if guild: 215 | result = Emoji(guild=guild, state=self, data=result) 216 | else: 217 | result = None 218 | return result 219 | 220 | async def private_channels(self): 221 | return await self._private_channels() 222 | 223 | async def _get_private_channel(self, channel_id): 224 | result = await self._get(f"channel:{channel_id}") 225 | if result: 226 | result = DMChannel(me=self.user, state=self, data=result) 227 | return result 228 | 229 | async def _get_private_channel_by_user(self, user_id): 230 | return utils.find(lambda x: x.recipient.id == user_id, await self.private_channels()) 231 | 232 | def _add_private_channel(self, channel): 233 | return 234 | 235 | def add_dm_channel(self, data): 236 | return DMChannel(me=self.user, state=self, data=data) 237 | 238 | def _remove_private_channel(self, channel): 239 | return 240 | 241 | async def _get_message(self, msg_id): 242 | result = await self._members_get("message", second=msg_id) 243 | if result: 244 | channel = await self.get_channel(self._key_first(result)) 245 | if channel: 246 | result = Message(channel=channel, state=self, data=result) 247 | return result 248 | 249 | def _add_guild_from_data(self, guild): 250 | return Guild(state=self, data=guild) 251 | 252 | def _guild_needs_chunking(self, guild): 253 | return 254 | 255 | async def _get_guild_channel(self, channel_id): 256 | result = await self._get(f"channel:{channel_id}") 257 | if result: 258 | factory, _ = _channel_factory(result["type"]) 259 | guild = await self._get_guild(self._key_first(result)) 260 | if guild: 261 | result = factory(guild=guild, state=self, data=result) 262 | else: 263 | result = None 264 | return result 265 | 266 | async def chunker(self, guild_id, query="", limit=0, *, nonce=None): 267 | return 268 | 269 | async def query_members(self, guild, query, limit, user_ids, cache): 270 | return 271 | 272 | async def _delay_ready(self): 273 | try: 274 | while True: 275 | try: 276 | guild = await asyncio.wait_for(self._ready_state.get(), timeout=self._ready_timeout) 277 | except asyncio.TimeoutError: 278 | break 279 | else: 280 | if guild.unavailable is False: 281 | self.dispatch("guild_available", guild) 282 | else: 283 | self.dispatch("guild_join", guild) 284 | try: 285 | del self._ready_state 286 | except AttributeError: 287 | pass 288 | except asyncio.CancelledError: 289 | pass 290 | else: 291 | self.call_handlers("ready") 292 | self.dispatch("ready") 293 | finally: 294 | self._ready_task = None 295 | 296 | async def parse_ready(self, data, old): 297 | if self._ready_task is not None: 298 | self._ready_task.cancel() 299 | self.dispatch("connect") 300 | self._ready_state = asyncio.Queue() 301 | self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop) 302 | 303 | async def parse_resumed(self, data, old): 304 | self.dispatch("resumed") 305 | 306 | async def parse_message_create(self, data, old): 307 | channel = await self.get_channel(int(data["channel_id"])) 308 | if channel: 309 | message = self.create_message(channel=channel, data=data) 310 | self.dispatch("message", message) 311 | 312 | async def parse_message_delete(self, data, old): 313 | raw = RawMessageDeleteEvent(data) 314 | if old: 315 | channel = await self.get_channel(int(data["channel_id"])) 316 | if channel: 317 | old = self.create_message(channel=channel, data=old) 318 | raw.cached_message = old 319 | self.dispatch("message_delete", old) 320 | self.dispatch("raw_message_delete", raw) 321 | 322 | async def parse_message_delete_bulk(self, data, old): 323 | raw = RawBulkMessageDeleteEvent(data) 324 | if old: 325 | messages = [] 326 | for old_message in old: 327 | channel = await self.get_channel(int(old_message["channel_id"])) 328 | if channel: 329 | messages.append(self.create_message(channel=channel, data=old_message)) 330 | raw.cached_messages = old 331 | self.dispatch("bulk_message_delete", old) 332 | self.dispatch("raw_bulk_message_delete", raw) 333 | 334 | async def parse_message_update(self, data, old): 335 | raw = RawMessageUpdateEvent(data) 336 | if old: 337 | channel = await self.get_channel(int(data["channel_id"])) 338 | if channel: 339 | old = self.create_message(channel=channel, data=old) 340 | raw.cached_message = old 341 | new = copy.copy(old) 342 | new._update(data) 343 | self.dispatch("message_edit", old, new) 344 | self.dispatch("raw_message_edit", raw) 345 | 346 | async def parse_message_reaction_add(self, data, old): 347 | emoji = PartialEmoji.with_state( 348 | self, 349 | id=utils._get_as_snowflake(data["emoji"], "id"), 350 | animated=data["emoji"].get("animated", False), 351 | name=data["emoji"]["name"], 352 | ) 353 | raw = RawReactionActionEvent(data, emoji, "REACTION_ADD") 354 | member = data.get("member") 355 | if member: 356 | guild = await self._get_guild(raw.guild_id) 357 | if guild: 358 | raw.member = Member(guild=guild, state=self, data=member) 359 | self.dispatch("raw_reaction_add", raw) 360 | message = await self._get_message(raw.message_id) 361 | if message: 362 | reaction = Reaction(message=message, data=data, emoji=await self._upgrade_partial_emoji(emoji)) 363 | user = raw.member or await self._get_reaction_user(message.channel, raw.user_id) 364 | if user: 365 | self.dispatch("reaction_add", reaction, user) 366 | 367 | async def parse_message_reaction_remove_all(self, data, old): 368 | raw = RawReactionClearEvent(data) 369 | self.dispatch("raw_reaction_clear", raw) 370 | message = await self._get_message(raw.message_id) 371 | if message: 372 | self.dispatch("reaction_clear", message, None) 373 | 374 | async def parse_message_reaction_remove(self, data, old): 375 | emoji = PartialEmoji.with_state( 376 | self, 377 | id=utils._get_as_snowflake(data["emoji"], "id"), 378 | name=data["emoji"]["name"], 379 | ) 380 | raw = RawReactionActionEvent(data, emoji, "REACTION_REMOVE") 381 | self.dispatch("raw_reaction_remove", raw) 382 | message = await self._get_message(raw.message_id) 383 | if message: 384 | reaction = Reaction(message=message, data=data, emoji=await self._upgrade_partial_emoji(emoji)) 385 | user = await self._get_reaction_user(message.channel, raw.user_id) 386 | if user: 387 | self.dispatch("reaction_remove", reaction, user) 388 | 389 | async def parse_message_reaction_remove_emoji(self, data, old): 390 | emoji = PartialEmoji.with_state( 391 | self, 392 | id=utils._get_as_snowflake(data["emoji"], "id"), 393 | name=data["emoji"]["name"], 394 | ) 395 | raw = RawReactionClearEmojiEvent(data, emoji) 396 | self.dispatch("raw_reaction_clear_emoji", raw) 397 | message = await self._get_message(raw.message_id) 398 | if message: 399 | reaction = Reaction(message=message, data=data, emoji=await self._upgrade_partial_emoji(emoji)) 400 | self.dispatch("reaction_clear_emoji", reaction) 401 | 402 | async def parse_presence_update(self, data, old): 403 | guild = await self._get_guild(utils._get_as_snowflake(data, "guild_id")) 404 | if not guild: 405 | return 406 | old_member = None 407 | member = await guild.get_member(int(data["user"]["id"])) 408 | if member and old: 409 | old_member = Member._copy(member) 410 | user_update = old_member._presence_update(data=old, user=old["user"]) 411 | if user_update: 412 | self.dispatch("user_update", user_update[1], user_update[0]) 413 | self.dispatch("member_update", old_member, member) 414 | 415 | async def parse_user_update(self, data, old): 416 | return 417 | 418 | async def parse_invite_create(self, data, old): 419 | invite = Invite.from_gateway(state=self, data=data) 420 | self.dispatch("invite_create", invite) 421 | 422 | async def parse_invite_delete(self, data, old): 423 | invite = Invite.from_gateway(state=self, data=data) 424 | self.dispatch("invite_delete", invite) 425 | 426 | async def parse_channel_delete(self, data, old): 427 | if old and old["guild_id"]: 428 | guild = await self._get_guild(utils._get_as_snowflake(data, "guild_id")) 429 | if guild: 430 | factory, _ = _channel_factory(old["type"]) 431 | channel = factory(guild=guild, state=self, data=old) 432 | self.dispatch("guild_channel_delete", channel) 433 | elif old: 434 | channel = DMChannel(me=self.user, state=self, data=old) 435 | self.dispatch("private_channel_delete", channel) 436 | 437 | async def parse_channel_update(self, data, old): 438 | channel_type = try_enum(ChannelType, data.get("type")) 439 | if old and channel_type is ChannelType.private: 440 | channel = DMChannel(me=self.user, state=self, data=data) 441 | old_channel = DMChannel(me=self.user, state=self, data=old) 442 | self.dispatch("private_channel_update", old_channel, channel) 443 | elif old: 444 | guild = await self._get_guild(utils._get_as_snowflake(data, "guild_id")) 445 | if guild: 446 | factory, _ = _channel_factory(data["type"]) 447 | channel = factory(guild=guild, state=self, data=data) 448 | old_factory, _ = _channel_factory(old["type"]) 449 | old_channel = old_factory(guild=guild, state=self, data=old) 450 | self.dispatch("guild_channel_update", old_channel, channel) 451 | 452 | async def parse_channel_create(self, data, old): 453 | factory, ch_type = _channel_factory(data["type"]) 454 | if ch_type is ChannelType.private: 455 | channel = DMChannel(me=self.user, data=data, state=self) 456 | self.dispatch("private_channel_create", channel) 457 | else: 458 | guild = await self._get_guild(utils._get_as_snowflake(data, "guild_id")) 459 | if guild: 460 | channel = factory(guild=guild, state=self, data=data) 461 | self.dispatch("guild_channel_create", channel) 462 | 463 | async def parse_channel_pins_update(self, data, old): 464 | channel = await self.get_channel(int(data["channel_id"])) 465 | last_pin = utils.parse_time(data["last_pin_timestamp"]) if data["last_pin_timestamp"] else None 466 | try: 467 | channel.guild 468 | except AttributeError: 469 | self.dispatch("private_channel_pins_update", channel, last_pin) 470 | else: 471 | self.dispatch("guild_channel_pins_update", channel, last_pin) 472 | 473 | async def parse_channel_recipient_add(self, data, old): 474 | return 475 | 476 | async def parse_channel_recipient_remove(self, data, old): 477 | return 478 | 479 | async def parse_guild_member_add(self, data, old): 480 | guild = await self._get_guild(int(data["guild_id"])) 481 | if guild: 482 | member = Member(guild=guild, data=data, state=self) 483 | self.dispatch("member_join", member) 484 | 485 | async def parse_guild_member_remove(self, data, old): 486 | if old: 487 | guild = await self._get_guild(int(data["guild_id"])) 488 | if guild: 489 | member = Member(guild=guild, data=old, state=self) 490 | self.dispatch("member_remove", member) 491 | 492 | async def parse_guild_member_update(self, data, old): 493 | guild = await self._get_guild(int(data["guild_id"])) 494 | if old and guild: 495 | member = await guild.get_member(int(data["user"]["id"])) 496 | if member: 497 | old_member = Member._copy(member) 498 | old_member._update(old) 499 | user_update = old_member._update_inner_user(data["user"]) 500 | if user_update: 501 | self.dispatch("user_update", user_update[1], user_update[0]) 502 | self.dispatch("member_update", old_member, member) 503 | 504 | async def parse_guild_emojis_update(self, data, old): 505 | guild = await self._get_guild(int(data["guild_id"])) 506 | if guild: 507 | before_emojis = None 508 | if old: 509 | before_emojis = [self.store_emoji(guild, x) for x in old] 510 | after_emojis = tuple(map(lambda x: self.store_emoji(guild, x), data["emojis"])) 511 | self.dispatch("guild_emojis_update", guild, before_emojis, after_emojis) 512 | 513 | def _get_create_guild(self, data): 514 | return self._add_guild_from_data(data) 515 | 516 | async def chunk_guild(self, guild, *, wait=True, cache=None): 517 | return 518 | 519 | async def _chunk_and_dispatch(self, guild, unavailable): 520 | return 521 | 522 | async def parse_guild_create(self, data, old): 523 | unavailable = data.get("unavailable") 524 | if unavailable is True: 525 | return 526 | guild = self._get_create_guild(data) 527 | try: 528 | self._ready_state.put_nowait(guild) 529 | except AttributeError: 530 | if unavailable is False: 531 | self.dispatch("guild_available", guild) 532 | else: 533 | self.dispatch("guild_join", guild) 534 | 535 | async def parse_guild_sync(self, data, old): 536 | return 537 | 538 | async def parse_guild_update(self, data, old): 539 | guild = await self._get_guild(int(data["id"])) 540 | if guild: 541 | old_guild = None 542 | if old: 543 | old_guild = copy.copy(guild) 544 | old_guild = old_guild._from_data(old) 545 | self.dispatch("guild_update", old_guild, guild) 546 | 547 | async def parse_guild_delete(self, data, old): 548 | if old: 549 | old = Guild(state=self, data=old) 550 | if data.get("unavailable", False): 551 | new = Guild(state=self, data=data) 552 | self.dispatch("guild_unavailable", new) 553 | else: 554 | self.dispatch("guild_remove", old) 555 | 556 | async def parse_guild_ban_add(self, data, old): 557 | guild = await self._get_guild(int(data["guild_id"])) 558 | if guild: 559 | user = self.store_user(data["user"]) 560 | member = await guild.get_member(user.id) or user 561 | self.dispatch("member_ban", guild, member) 562 | 563 | async def parse_guild_ban_remove(self, data, old): 564 | guild = await self._get_guild(int(data["guild_id"])) 565 | if guild: 566 | self.dispatch("member_unban", guild, self.store_user(data["user"])) 567 | 568 | async def parse_guild_role_create(self, data, old): 569 | guild = await self._get_guild(int(data["guild_id"])) 570 | if guild: 571 | role = Role(guild=guild, state=self, data=data["role"]) 572 | self.dispatch("guild_role_create", role) 573 | 574 | async def parse_guild_role_delete(self, data, old): 575 | if old: 576 | guild = await self._get_guild(int(data["guild_id"])) 577 | if guild: 578 | role = Role(guild=guild, state=self, data=old) 579 | self.dispatch("guild_role_delete", role) 580 | 581 | async def parse_guild_role_update(self, data, old): 582 | if old: 583 | guild = await self._get_guild(int(data["guild_id"])) 584 | if guild: 585 | role = Role(guild=guild, state=self, data=data["role"]) 586 | old_role = Role(guild=guild, state=self, data=old) 587 | self.dispatch("guild_role_update", old_role, role) 588 | 589 | async def parse_guild_members_chunk(self, data, old): 590 | return 591 | 592 | async def parse_guild_integrations_update(self, data, old): 593 | guild = await self._get_guild(int(data["guild_id"])) 594 | if guild: 595 | self.dispatch("guild_integrations_update", guild) 596 | 597 | async def parse_webhooks_update(self, data, old): 598 | channel = await self._get_guild(int(data["channel_id"])) 599 | if channel: 600 | self.dispatch("webhooks_update", channel) 601 | 602 | async def parse_voice_state_update(self, data, old): 603 | guild = await self._get_guild(utils._get_as_snowflake(data, "guild_id")) 604 | if guild: 605 | member = await guild.get_member(int(data["user_id"])) 606 | if member: 607 | channel = await self.get_channel(utils._get_as_snowflake(data, "channel_id")) 608 | if channel: 609 | before = None 610 | after = VoiceState(data=data, channel=channel) 611 | old_channel = await self.get_channel(old["channel_id"]) 612 | if old and old_channel: 613 | before = VoiceState(data=data, channel=old_channel) 614 | self.dispatch("voice_state_update", member, before, after) 615 | 616 | def parse_voice_server_update(self, data, old): 617 | return 618 | 619 | async def parse_typing_start(self, data, old): 620 | channel = await self._get_guild_channel(int(data["channel_id"])) 621 | if channel: 622 | member = None 623 | if isinstance(channel, DMChannel): 624 | member = channel.recipient 625 | elif isinstance(channel, TextChannel): 626 | guild = await self._get_guild(int(data["guild_id"])) 627 | if guild: 628 | member = await guild.get_member(utils._get_as_snowflake(data, "user_id")) 629 | if member: 630 | self.dispatch("typing", channel, member, datetime.datetime.utcfromtimestamp(data.get("timestamp"))) 631 | 632 | async def parse_relationship_add(self, data, old): 633 | return 634 | 635 | async def parse_relationship_remove(self, data, old): 636 | return 637 | 638 | async def _get_reaction_user(self, channel, user_id): 639 | if isinstance(channel, TextChannel): 640 | return await channel.guild.get_member(user_id) 641 | return await self.get_user(user_id) 642 | 643 | async def get_reaction_emoji(self, data): 644 | emoji_id = utils._get_as_snowflake(data, "id") 645 | if not emoji_id: 646 | return data["name"] 647 | return await self.get_emoji(emoji_id) 648 | 649 | async def _upgrade_partial_emoji(self, emoji): 650 | if not emoji.id: 651 | return emoji.name 652 | return await self.get_emoji(emoji.id) 653 | 654 | async def get_channel(self, channel_id): 655 | if not channel_id: 656 | return None 657 | return await self._get_private_channel(channel_id) or await self._get_guild_channel(channel_id) 658 | 659 | def create_message(self, *, channel, data): 660 | message = Message(state=self, channel=channel, data=data) 661 | return message 662 | -------------------------------------------------------------------------------- /examples/discordpy/cogs/events.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import orjson 3 | 4 | from discord.ext import commands 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | class Events(commands.Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | @commands.Cog.listener() 14 | async def on_ready(self): 15 | user = await self.bot.user() 16 | log.info(f"{user.name}#{user.discriminator} is ready") 17 | log.info("--------") 18 | 19 | @commands.Cog.listener() 20 | async def on_socket_raw_receive(self, message): 21 | log.debug("[Receive] " + message.decode("utf-8")) 22 | 23 | @commands.Cog.listener() 24 | async def on_socket_raw_send(self, message): 25 | log.debug("[Send] " + message.decode("utf-8")) 26 | 27 | @commands.Cog.listener() 28 | async def on_reaction_add(self, reaction, member): 29 | if reaction.emoji not in ["◀️", "▶️"]: 30 | return 31 | if member.bot: 32 | return 33 | menus = await self.bot._connection._get("reaction_menus") or [] 34 | for (index, menu) in enumerate(menus): 35 | channel = menu["channel"] 36 | message = menu["message"] 37 | if reaction.message.channel.id != channel or reaction.message.id != message: 38 | continue 39 | page = menu["page"] 40 | all_pages = menu["all_pages"] 41 | if reaction.emoji == "◀️" and page > 0: 42 | page -= 1 43 | elif reaction.emoji == "▶️" and page < len(all_pages) - 1: 44 | page += 1 45 | await self.bot.http.edit_message(channel, message, content=all_pages[page]) 46 | menu["page"] = page 47 | menus[index] = menu 48 | await self.bot._connection.redis.set("reaction_menus", orjson.dumps(menus).decode("utf-8")) 49 | break 50 | 51 | def setup(bot): 52 | bot.add_cog(Events(bot)) 53 | -------------------------------------------------------------------------------- /examples/discordpy/cogs/general.py: -------------------------------------------------------------------------------- 1 | import io 2 | import logging 3 | import orjson 4 | import textwrap 5 | import traceback 6 | 7 | from contextlib import redirect_stdout 8 | from discord.ext import commands 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class General(commands.Cog): 14 | def __init__(self, bot): 15 | self.bot = bot 16 | 17 | @commands.command(description="Get started with the bot.") 18 | async def help(self, ctx): 19 | await ctx.send("This is a sample bot using twilight-dispatch: https://github.com/chamburr/twilight-dispatch") 20 | 21 | @commands.command(description="Play ping pong!") 22 | async def ping(self, ctx): 23 | await ctx.send("Pong! 🏓") 24 | 25 | @commands.command(description="Reaction menu example.") 26 | async def menu(self, ctx): 27 | msg = await ctx.send("Page 1") 28 | await msg.add_reaction("◀️") 29 | await msg.add_reaction("▶️") 30 | menus = await self.bot._connection._get("reaction_menus") or [] 31 | menus.append({"channel": msg.channel.id, "message": msg.id, "page": 0, "all_pages": ["Page 1", "Page 2"]}) 32 | await self.bot._connection.redis.set("reaction_menus", orjson.dumps(menus).decode("utf-8")) 33 | 34 | @commands.is_owner() 35 | @commands.command(name="eval", description="Evaluate code and play around.") 36 | async def _eval(self, ctx, *, code: str): 37 | env = {"bot": self.bot, "ctx": ctx} 38 | env.update(globals()) 39 | stdout = io.StringIO() 40 | 41 | try: 42 | exec(f"async def func():\n{textwrap.indent(code, ' ')}", env) 43 | except Exception as e: 44 | await ctx.send(f"```py\n{e.__class__.__name__}: {e}\n```") 45 | return 46 | 47 | try: 48 | with redirect_stdout(stdout): 49 | result = await env["func"]() 50 | except Exception: 51 | await ctx.send(f"```py\n{stdout.getvalue()}{traceback.format_exc()}\n```") 52 | else: 53 | await ctx.send(f"```py\n{stdout.getvalue()}{result}\n```") 54 | 55 | 56 | def setup(bot): 57 | bot.add_cog(General(bot)) 58 | -------------------------------------------------------------------------------- /examples/discordpy/config.example.py: -------------------------------------------------------------------------------- 1 | token = "NjAzODc5NDE2NDE3ODc4MDQ3.XTl0iA.PTZPdcnhpiV0Zm3iNmaMUgpphPg" 2 | 3 | prefix = "~" 4 | 5 | amqp_url = "amqp://guest:guest@127.0.0.1:5672/%2f" 6 | redis_url = "redis://127.0.0.1:6379/0" 7 | 8 | owner = 446290930723717120 9 | 10 | cogs = [ 11 | "events", 12 | "general", 13 | ] 14 | -------------------------------------------------------------------------------- /examples/discordpy/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import config 3 | import logging 4 | 5 | from classes.bot import Bot 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | bot = Bot( 14 | command_prefix=config.prefix, 15 | case_insensitive=True, 16 | help_command=None, 17 | owner_id=config.owner, 18 | ) 19 | 20 | loop = asyncio.get_event_loop() 21 | loop.run_until_complete(bot.start()) 22 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "crate" 2 | -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::CONFIG, 3 | constants::{ 4 | channel_key, emoji_key, guild_key, member_key, message_key, presence_key, role_key, 5 | voice_key, BOT_USER_KEY, CACHE_JOB_INTERVAL, CHANNEL_KEY, EMOJI_KEY, EXPIRY_KEYS, 6 | GUILD_KEY, KEYS_SUFFIX, MESSAGE_KEY, SESSIONS_KEY, STATUSES_KEY, 7 | }, 8 | models::{ApiError, ApiResult, FormattedDateTime, GuildItem, SessionInfo, StatusInfo}, 9 | utils::to_value, 10 | }; 11 | 12 | use redis::{aio::MultiplexedConnection, AsyncCommands, FromRedisValue, ToRedisArgs}; 13 | use serde::{de::DeserializeOwned, Serialize}; 14 | use simd_json::owned::Value; 15 | use std::{collections::HashMap, hash::Hash, iter, sync::Arc}; 16 | use tokio::time::{sleep, Duration}; 17 | use tracing::warn; 18 | use twilight_gateway::Cluster; 19 | use twilight_model::{ 20 | channel::{Channel, Message}, 21 | gateway::event::Event, 22 | guild::{Emoji, Member}, 23 | id::{ 24 | marker::{GuildMarker, UserMarker}, 25 | Id, 26 | }, 27 | }; 28 | 29 | pub async fn get(conn: &mut MultiplexedConnection, key: K) -> ApiResult> 30 | where 31 | K: ToRedisArgs + Send + Sync, 32 | T: DeserializeOwned, 33 | { 34 | let res: Option = conn.get(key).await?; 35 | 36 | Ok(res 37 | .map(|mut value| unsafe { simd_json::from_str(value.as_mut_str()) }) 38 | .transpose()?) 39 | } 40 | 41 | pub async fn get_all( 42 | conn: &mut MultiplexedConnection, 43 | keys: &[K], 44 | ) -> ApiResult>> 45 | where 46 | K: ToRedisArgs + Send + Sync, 47 | T: DeserializeOwned, 48 | { 49 | if keys.is_empty() { 50 | return Ok(vec![]); 51 | } 52 | 53 | let res: Vec> = conn.get(keys).await?; 54 | 55 | res.into_iter() 56 | .map(|option| { 57 | option 58 | .map(|mut value| unsafe { 59 | simd_json::from_str(value.as_mut_str()).map_err(ApiError::from) 60 | }) 61 | .transpose() 62 | }) 63 | .collect() 64 | } 65 | 66 | pub async fn get_members(conn: &mut MultiplexedConnection, key: K) -> ApiResult> 67 | where 68 | K: ToRedisArgs + Send + Sync, 69 | T: FromRedisValue, 70 | { 71 | let res = conn.smembers(key).await?; 72 | 73 | Ok(res) 74 | } 75 | 76 | pub async fn get_members_len(conn: &mut MultiplexedConnection, key: K) -> ApiResult 77 | where 78 | K: ToRedisArgs + Send + Sync, 79 | { 80 | let res = conn.scard(key).await?; 81 | 82 | Ok(res) 83 | } 84 | 85 | pub async fn get_hashmap( 86 | conn: &mut MultiplexedConnection, 87 | key: K, 88 | ) -> ApiResult> 89 | where 90 | K: ToRedisArgs + Send + Sync, 91 | T: FromRedisValue + Eq + Hash, 92 | U: FromRedisValue, 93 | { 94 | let res = conn.hgetall(key).await?; 95 | 96 | Ok(res) 97 | } 98 | 99 | pub async fn set(conn: &mut MultiplexedConnection, key: K, value: T) -> ApiResult<()> 100 | where 101 | K: AsRef, 102 | T: Serialize, 103 | { 104 | set_all(conn, iter::once((key, value))).await?; 105 | 106 | Ok(()) 107 | } 108 | 109 | pub async fn set_all(conn: &mut MultiplexedConnection, keys: I) -> ApiResult<()> 110 | where 111 | I: IntoIterator, 112 | K: AsRef, 113 | T: Serialize, 114 | { 115 | let mut members = HashMap::new(); 116 | 117 | let keys = keys 118 | .into_iter() 119 | .map(|(key, value)| { 120 | let key = key.as_ref(); 121 | let parts: Vec<&str> = key.split(':').collect(); 122 | 123 | let new_key = if parts.len() > 2 && parts[0] == CHANNEL_KEY { 124 | format!("{}:{}", parts[0], parts[2]) 125 | } else { 126 | key.to_owned() 127 | }; 128 | 129 | if parts.len() > 1 { 130 | members 131 | .entry(format!("{}{}", parts[0], KEYS_SUFFIX)) 132 | .or_insert_with(Vec::new) 133 | .push(new_key.clone()); 134 | } 135 | 136 | if parts.len() > 2 && parts[0] != MESSAGE_KEY { 137 | members 138 | .entry(format!("{}{}:{}", GUILD_KEY, KEYS_SUFFIX, parts[1])) 139 | .or_insert_with(Vec::new) 140 | .push(new_key.clone()); 141 | } else if parts.len() > 2 { 142 | members 143 | .entry(format!("{}{}:{}", CHANNEL_KEY, KEYS_SUFFIX, parts[1])) 144 | .or_insert_with(Vec::new) 145 | .push(new_key.clone()); 146 | } 147 | 148 | simd_json::to_string(&value) 149 | .map(|value| (new_key, value)) 150 | .map_err(ApiError::from) 151 | }) 152 | .collect::>>()?; 153 | 154 | if keys.is_empty() { 155 | return Ok(()); 156 | } 157 | 158 | conn.set_multiple(keys.as_slice()).await?; 159 | 160 | for (key, value) in members { 161 | conn.sadd(key, value.as_slice()).await?; 162 | } 163 | 164 | Ok(()) 165 | } 166 | 167 | pub async fn expire(conn: &mut MultiplexedConnection, key: K, expiry: u64) -> ApiResult<()> 168 | where 169 | K: ToRedisArgs + Send + Sync, 170 | { 171 | expire_all(conn, iter::once((key, expiry))).await?; 172 | 173 | Ok(()) 174 | } 175 | 176 | pub async fn expire_all(conn: &mut MultiplexedConnection, keys: I) -> ApiResult<()> 177 | where 178 | I: IntoIterator, 179 | K: ToRedisArgs + Send + Sync, 180 | { 181 | let keys = keys 182 | .into_iter() 183 | .map(|(key, value)| { 184 | let timestamp = FormattedDateTime::now() + time::Duration::milliseconds(value as i64); 185 | 186 | simd_json::to_string(×tamp) 187 | .map(|value| (key, value)) 188 | .map_err(ApiError::from) 189 | }) 190 | .collect::>>()?; 191 | 192 | if keys.is_empty() { 193 | return Ok(()); 194 | } 195 | 196 | conn.hset_multiple(EXPIRY_KEYS, keys.as_slice()).await?; 197 | 198 | Ok(()) 199 | } 200 | 201 | pub async fn del_all(conn: &mut MultiplexedConnection, keys: I) -> ApiResult<()> 202 | where 203 | I: IntoIterator, 204 | K: AsRef, 205 | { 206 | let mut members = HashMap::new(); 207 | 208 | let keys = keys 209 | .into_iter() 210 | .map(|key| { 211 | let key = key.as_ref(); 212 | let parts: Vec<&str> = key.split(':').collect(); 213 | 214 | let new_key = if parts.len() > 2 && parts[0] == CHANNEL_KEY { 215 | format!("{}:{}", parts[0], parts[2]) 216 | } else { 217 | key.to_owned() 218 | }; 219 | 220 | if parts.len() > 1 { 221 | members 222 | .entry(format!("{}{}", parts[0], KEYS_SUFFIX)) 223 | .or_insert_with(Vec::new) 224 | .push(new_key.clone()); 225 | } 226 | 227 | if parts.len() > 2 { 228 | if parts[0] != MESSAGE_KEY { 229 | members 230 | .entry(format!("{}{}:{}", GUILD_KEY, KEYS_SUFFIX, parts[1])) 231 | .or_insert_with(Vec::new) 232 | .push(new_key.clone()); 233 | } else { 234 | members 235 | .entry(format!("{}{}:{}", CHANNEL_KEY, KEYS_SUFFIX, parts[1])) 236 | .or_insert_with(Vec::new) 237 | .push(new_key.clone()); 238 | } 239 | } 240 | 241 | new_key 242 | }) 243 | .collect::>(); 244 | 245 | if keys.is_empty() { 246 | return Ok(()); 247 | } 248 | 249 | conn.del(keys).await?; 250 | 251 | for (key, value) in members { 252 | conn.srem(key, value).await?; 253 | } 254 | 255 | Ok(()) 256 | } 257 | 258 | pub async fn del(conn: &mut MultiplexedConnection, key: impl AsRef) -> ApiResult<()> { 259 | del_all(conn, iter::once(key)).await?; 260 | 261 | Ok(()) 262 | } 263 | 264 | pub async fn del_hashmap( 265 | conn: &mut MultiplexedConnection, 266 | key: K, 267 | keys: &[String], 268 | ) -> ApiResult<()> 269 | where 270 | K: ToRedisArgs + Send + Sync, 271 | { 272 | if keys.is_empty() { 273 | return Ok(()); 274 | } 275 | 276 | conn.hdel(key, keys).await?; 277 | 278 | Ok(()) 279 | } 280 | 281 | pub async fn run_jobs(mut conn: MultiplexedConnection, clusters: &[Arc]) { 282 | loop { 283 | let mut statuses = vec![]; 284 | let mut sessions = HashMap::new(); 285 | 286 | for cluster in clusters { 287 | let mut status: Vec = cluster 288 | .info() 289 | .into_iter() 290 | .map(|(key, value)| StatusInfo { 291 | shard: key, 292 | status: format!("{}", value.stage()), 293 | latency: value 294 | .latency() 295 | .recent() 296 | .back() 297 | .map(|value| value.as_millis() as u64) 298 | .unwrap_or_default(), 299 | last_ack: value 300 | .latency() 301 | .received() 302 | .map(|value| { 303 | FormattedDateTime::now() 304 | - time::Duration::milliseconds(value.elapsed().as_millis() as i64) 305 | }) 306 | .unwrap_or_else(FormattedDateTime::now), 307 | }) 308 | .collect(); 309 | 310 | statuses.append(&mut status); 311 | 312 | for (shard, info) in cluster.info() { 313 | sessions.insert( 314 | shard.to_string(), 315 | SessionInfo { 316 | session_id: info.session_id().unwrap_or_default().to_owned(), 317 | sequence: info.seq(), 318 | }, 319 | ); 320 | } 321 | } 322 | 323 | statuses.sort_by(|a, b| a.shard.cmp(&b.shard)); 324 | 325 | if let Err(err) = set(&mut conn, STATUSES_KEY, &statuses).await { 326 | warn!("Failed to dump gateway statuses: {:?}", err); 327 | } 328 | 329 | if let Err(err) = set(&mut conn, SESSIONS_KEY, &sessions).await { 330 | warn!("Failed to dump gateway sessions: {:?}", err); 331 | } 332 | 333 | let hashmap: ApiResult> = get_hashmap(&mut conn, EXPIRY_KEYS).await; 334 | 335 | match hashmap { 336 | Ok(hashmap) => { 337 | let mut keys = vec![]; 338 | 339 | for (key, mut value) in hashmap { 340 | match unsafe { simd_json::from_str::(value.as_mut_str()) } { 341 | Ok(timestamp) => { 342 | if (timestamp - FormattedDateTime::now()).is_negative() { 343 | keys.push(key); 344 | } 345 | } 346 | Err(err) => { 347 | warn!("Failed to get expiry timestamp: {:?}", err); 348 | } 349 | } 350 | } 351 | 352 | if let Err(err) = del_all(&mut conn, keys.as_slice()).await { 353 | warn!("Failed to delete expired keys: {:?}", err); 354 | } else if let Err(err) = del_hashmap(&mut conn, EXPIRY_KEYS, keys.as_slice()).await 355 | { 356 | warn!("Failed to delete expired keys hashmap: {:?}", err); 357 | } 358 | } 359 | Err(err) => { 360 | warn!("Failed to get expiry keys: {:?}", err); 361 | } 362 | } 363 | 364 | sleep(Duration::from_millis(CACHE_JOB_INTERVAL as u64)).await; 365 | } 366 | } 367 | 368 | async fn clear_guild( 369 | conn: &mut MultiplexedConnection, 370 | guild_id: Id, 371 | ) -> ApiResult> { 372 | let members: ApiResult> = 373 | get_members(conn, format!("{}{}:{}", GUILD_KEY, KEYS_SUFFIX, guild_id)).await; 374 | 375 | if let Ok(members) = members { 376 | del_all(conn, members).await?; 377 | } 378 | 379 | let guild: ApiResult> = get(conn, guild_key(guild_id)).await; 380 | 381 | if let Ok(guild) = guild { 382 | del(conn, guild_key(guild_id)).await?; 383 | return Ok(guild); 384 | } 385 | 386 | Ok(None) 387 | } 388 | 389 | pub async fn update( 390 | conn: &mut MultiplexedConnection, 391 | event: &Event, 392 | bot_id: Id, 393 | ) -> ApiResult> { 394 | let mut old: Option = None; 395 | 396 | match event { 397 | Event::ChannelCreate(data) => { 398 | set(conn, channel_key(data.guild_id, data.id), &data).await?; 399 | } 400 | Event::ChannelDelete(data) => { 401 | let key = channel_key(data.guild_id, data.id); 402 | if CONFIG.state_old { 403 | old = get(conn, &key).await?; 404 | } 405 | del(conn, &key).await?; 406 | } 407 | Event::ChannelPinsUpdate(data) => { 408 | let key = channel_key(data.guild_id, data.channel_id); 409 | let channel: Option = get(conn, &key).await?; 410 | if let Some(mut channel) = channel { 411 | channel.last_pin_timestamp = data.last_pin_timestamp; 412 | set(conn, &key, &channel).await?; 413 | } 414 | } 415 | Event::ChannelUpdate(data) => { 416 | let key = channel_key(data.guild_id, data.id); 417 | if CONFIG.state_old { 418 | old = get(conn, &key).await?; 419 | } 420 | set(conn, &key, &data).await?; 421 | } 422 | Event::GuildCreate(data) => { 423 | old = clear_guild(conn, data.id).await?; 424 | 425 | let mut items = vec![]; 426 | let mut guild = data.clone(); 427 | for mut channel in guild.channels.drain(..) { 428 | channel.guild_id = Some(data.id); 429 | items.push(( 430 | channel_key(Some(data.id), channel.id), 431 | GuildItem::Channel(channel), 432 | )); 433 | } 434 | for role in guild.roles.drain(..) { 435 | items.push((role_key(data.id, role.id), GuildItem::Role(role))); 436 | } 437 | for emoji in guild.emojis.drain(..) { 438 | if CONFIG.state_emoji { 439 | items.push((emoji_key(data.id, emoji.id), GuildItem::Emoji(emoji))); 440 | } 441 | } 442 | for voice in guild.voice_states.drain(..) { 443 | if CONFIG.state_voice { 444 | items.push((voice_key(data.id, voice.user_id), GuildItem::Voice(voice))); 445 | } 446 | } 447 | for member in guild.members.drain(..) { 448 | if CONFIG.state_member || member.user.id == bot_id { 449 | items.push(( 450 | member_key(data.id, member.user.id), 451 | GuildItem::Member(member), 452 | )); 453 | } 454 | } 455 | for presence in guild.presences.drain(..) { 456 | if CONFIG.state_presence { 457 | items.push(( 458 | presence_key(data.id, presence.user.id()), 459 | GuildItem::Presence(presence), 460 | )); 461 | } 462 | } 463 | items.push((guild_key(data.id), GuildItem::Guild(guild))); 464 | 465 | set_all(conn, items).await?; 466 | if CONFIG.state_member { 467 | expire_all( 468 | conn, 469 | data.members.iter().map(|member| { 470 | (member_key(data.id, member.user.id), CONFIG.state_member_ttl) 471 | }), 472 | ) 473 | .await?; 474 | } 475 | } 476 | Event::GuildDelete(data) => { 477 | old = clear_guild(conn, data.id).await?; 478 | } 479 | Event::GuildEmojisUpdate(data) => { 480 | if CONFIG.state_emoji { 481 | let keys: Vec = get_members( 482 | conn, 483 | format!("{}{}:{}", GUILD_KEY, KEYS_SUFFIX, data.guild_id), 484 | ) 485 | .await?; 486 | let emoji_keys: Vec = keys 487 | .into_iter() 488 | .filter(|key| key.split(':').next().unwrap_or_default() == EMOJI_KEY) 489 | .collect(); 490 | let emojis: Vec = get_all(conn, emoji_keys.as_slice()) 491 | .await? 492 | .into_iter() 493 | .flatten() 494 | .collect(); 495 | del_all( 496 | conn, 497 | emojis 498 | .iter() 499 | .filter(|emoji| !data.emojis.iter().any(|e| e.id == emoji.id)) 500 | .map(|emoji| emoji_key(data.guild_id, emoji.id)), 501 | ) 502 | .await?; 503 | set_all( 504 | conn, 505 | data.emojis 506 | .iter() 507 | .map(|emoji| (emoji_key(data.guild_id, emoji.id), emoji)), 508 | ) 509 | .await?; 510 | if CONFIG.state_old { 511 | old = Some(to_value(&emojis)?); 512 | } 513 | } 514 | } 515 | Event::GuildUpdate(data) => { 516 | let key = guild_key(data.id); 517 | if CONFIG.state_old { 518 | old = get(conn, &key).await?; 519 | } 520 | set(conn, &key, &data).await?; 521 | } 522 | Event::MemberAdd(data) => { 523 | if CONFIG.state_member { 524 | let key = member_key(data.guild_id, data.user.id); 525 | set(conn, &key, &data).await?; 526 | expire(conn, &key, CONFIG.state_member_ttl).await?; 527 | } 528 | } 529 | Event::MemberRemove(data) => { 530 | if CONFIG.state_member { 531 | let key = member_key(data.guild_id, data.user.id); 532 | if CONFIG.state_old { 533 | old = get(conn, &key).await?; 534 | } 535 | del(conn, &key).await?; 536 | } 537 | if CONFIG.state_presence { 538 | del(conn, presence_key(data.guild_id, data.user.id)).await?; 539 | } 540 | } 541 | Event::MemberUpdate(data) => { 542 | if CONFIG.state_member || data.user.id == bot_id { 543 | let key = member_key(data.guild_id, data.user.id); 544 | let member: Option = get(conn, &key).await?; 545 | if let Some(mut member) = member { 546 | if CONFIG.state_old { 547 | old = Some(to_value(&member)?); 548 | } 549 | member.joined_at = data.joined_at; 550 | member.nick = data.nick.clone(); 551 | member.premium_since = data.premium_since; 552 | member.roles = data.roles.clone(); 553 | member.user = data.user.clone(); 554 | set(conn, &key, &member).await?; 555 | expire(conn, &key, CONFIG.state_member_ttl).await?; 556 | } 557 | } 558 | } 559 | Event::MemberChunk(data) => { 560 | if CONFIG.state_member { 561 | set_all( 562 | conn, 563 | data.members 564 | .iter() 565 | .map(|member| (member_key(data.guild_id, member.user.id), member)), 566 | ) 567 | .await?; 568 | expire_all( 569 | conn, 570 | data.members.iter().map(|member| { 571 | ( 572 | member_key(data.guild_id, member.user.id), 573 | CONFIG.state_member_ttl, 574 | ) 575 | }), 576 | ) 577 | .await?; 578 | } 579 | } 580 | Event::MessageCreate(data) => { 581 | if CONFIG.state_message { 582 | let key = message_key(data.channel_id, data.id); 583 | set(conn, &key, &data).await?; 584 | expire(conn, &key, CONFIG.state_message_ttl).await?; 585 | } 586 | } 587 | Event::MessageDelete(data) => { 588 | if CONFIG.state_message { 589 | let key = message_key(data.channel_id, data.id); 590 | if CONFIG.state_old { 591 | old = get(conn, &key).await?; 592 | } 593 | del(conn, &key).await?; 594 | } 595 | } 596 | Event::MessageDeleteBulk(data) => { 597 | if CONFIG.state_message { 598 | let message_keys: Vec = data 599 | .ids 600 | .iter() 601 | .map(|id| message_key(data.channel_id, *id)) 602 | .collect(); 603 | let messages: Vec = get_all(conn, message_keys.as_slice()) 604 | .await? 605 | .into_iter() 606 | .flatten() 607 | .collect(); 608 | del_all(conn, message_keys).await?; 609 | if CONFIG.state_old { 610 | old = Some(to_value(&messages)?); 611 | } 612 | } 613 | } 614 | Event::MessageUpdate(data) => { 615 | if CONFIG.state_message { 616 | let key = message_key(data.channel_id, data.id); 617 | let message: Option = get(conn, &key).await?; 618 | if let Some(mut message) = message { 619 | if CONFIG.state_old { 620 | old = Some(to_value(&message)?); 621 | } 622 | if let Some(attachments) = &data.attachments { 623 | message.attachments = attachments.clone(); 624 | } 625 | if let Some(content) = &data.content { 626 | message.content = content.clone(); 627 | } 628 | if let Some(edited_timestamp) = data.edited_timestamp { 629 | message.edited_timestamp = Some(edited_timestamp); 630 | } 631 | if let Some(embeds) = &data.embeds { 632 | message.embeds = embeds.clone(); 633 | } 634 | if let Some(mention_everyone) = data.mention_everyone { 635 | message.mention_everyone = mention_everyone; 636 | } 637 | if let Some(mention_roles) = &data.mention_roles { 638 | message.mention_roles = mention_roles.clone(); 639 | } 640 | if let Some(mentions) = &data.mentions { 641 | message.mentions = mentions.clone(); 642 | } 643 | if let Some(pinned) = data.pinned { 644 | message.pinned = pinned; 645 | } 646 | if let Some(timestamp) = data.timestamp { 647 | message.timestamp = timestamp; 648 | } 649 | if let Some(tts) = data.tts { 650 | message.tts = tts; 651 | } 652 | set(conn, &key, &message).await?; 653 | expire(conn, &key, CONFIG.state_message_ttl).await?; 654 | } 655 | } 656 | } 657 | Event::PresenceUpdate(data) => { 658 | if CONFIG.state_presence { 659 | let key = presence_key(data.guild_id, data.user.id()); 660 | if CONFIG.state_old { 661 | old = get(conn, &key).await?; 662 | } 663 | set(conn, &key, &data).await?; 664 | } 665 | } 666 | Event::Ready(data) => { 667 | set(conn, BOT_USER_KEY, &data.user).await?; 668 | set_all( 669 | conn, 670 | data.guilds.iter().map(|guild| (guild_key(guild.id), guild)), 671 | ) 672 | .await?; 673 | } 674 | Event::RoleCreate(data) => { 675 | set(conn, role_key(data.guild_id, data.role.id), &data.role).await?; 676 | } 677 | Event::RoleDelete(data) => { 678 | let key = role_key(data.guild_id, data.role_id); 679 | if CONFIG.state_old { 680 | old = get(conn, &key).await?; 681 | } 682 | del(conn, &key).await?; 683 | } 684 | Event::RoleUpdate(data) => { 685 | let key = role_key(data.guild_id, data.role.id); 686 | if CONFIG.state_old { 687 | old = get(conn, &key).await?; 688 | } 689 | set(conn, &key, &data.role).await?; 690 | } 691 | Event::UnavailableGuild(data) => { 692 | old = clear_guild(conn, data.id).await?; 693 | set(conn, guild_key(data.id), data).await?; 694 | } 695 | Event::UserUpdate(data) => { 696 | if CONFIG.state_old { 697 | old = get(conn, BOT_USER_KEY).await?; 698 | } 699 | set(conn, BOT_USER_KEY, &data).await?; 700 | } 701 | Event::VoiceStateUpdate(data) => { 702 | if CONFIG.state_voice { 703 | if let Some(guild_id) = data.0.guild_id { 704 | let key = voice_key(guild_id, data.0.user_id); 705 | if CONFIG.state_old { 706 | old = get(conn, &key).await?; 707 | } 708 | match data.0.channel_id { 709 | Some(_) => set(conn, &key, &data.0).await?, 710 | None => del(conn, &key).await?, 711 | } 712 | } 713 | } 714 | } 715 | _ => {} 716 | } 717 | 718 | Ok(old) 719 | } 720 | -------------------------------------------------------------------------------- /src/cluster.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cache, 3 | config::CONFIG, 4 | constants::{SESSIONS_KEY, SHARDS_KEY}, 5 | models::{ApiResult, SessionInfo}, 6 | }; 7 | 8 | use futures_util::Stream; 9 | use redis::aio::MultiplexedConnection; 10 | use std::{collections::HashMap, fmt::Debug, future::Future, pin::Pin, sync::Arc, time::Duration}; 11 | use tokio::{ 12 | sync::{ 13 | mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, 14 | oneshot::{self, Sender}, 15 | }, 16 | time::sleep, 17 | }; 18 | use tracing::warn; 19 | use twilight_gateway::{ 20 | cluster::ShardScheme, queue::Queue, shard::ResumeSession, Cluster, Event, EventTypeFlags, 21 | Intents, 22 | }; 23 | use twilight_model::gateway::{ 24 | payload::outgoing::update_presence::UpdatePresencePayload, presence::Activity, 25 | }; 26 | 27 | #[derive(Clone, Debug)] 28 | pub struct ClusterInfo { 29 | pub clusters: u64, 30 | pub shards: u64, 31 | pub resumes: u64, 32 | } 33 | 34 | pub async fn get_clusters( 35 | conn: &mut MultiplexedConnection, 36 | ) -> ApiResult<( 37 | Vec>, 38 | Vec + Send + Sync + Unpin + 'static>, 39 | ClusterInfo, 40 | )> { 41 | let queue: Arc = if CONFIG.shards_concurrency == 1 { 42 | Arc::new(LocalQueue::new(Duration::from_secs(CONFIG.shards_wait))) 43 | } else { 44 | Arc::new(LargeBotQueue::new( 45 | CONFIG.shards_concurrency as usize, 46 | Duration::from_secs(CONFIG.shards_wait), 47 | )) 48 | }; 49 | 50 | let mut resumes = HashMap::new(); 51 | if CONFIG.resume && cache::get(conn, SHARDS_KEY).await? == Some(CONFIG.shards_total) { 52 | let sessions: HashMap = 53 | cache::get(conn, SESSIONS_KEY).await?.unwrap_or_default(); 54 | 55 | for (key, value) in sessions.into_iter() { 56 | resumes.insert( 57 | key.parse().unwrap(), 58 | ResumeSession { 59 | resume_url: None, 60 | session_id: value.session_id, 61 | sequence: value.sequence, 62 | }, 63 | ); 64 | } 65 | }; 66 | 67 | let shards = CONFIG.shards_end - CONFIG.shards_start + 1; 68 | let base = shards / CONFIG.clusters; 69 | let extra = shards % CONFIG.clusters; 70 | 71 | let mut clusters = Vec::with_capacity(CONFIG.clusters as usize); 72 | let mut events = Vec::with_capacity(CONFIG.clusters as usize); 73 | let mut last_index = CONFIG.shards_start; 74 | 75 | for i in 0..CONFIG.clusters { 76 | let index = if i < extra { 77 | last_index + base 78 | } else { 79 | last_index + base - 1 80 | }; 81 | 82 | let (cluster, event) = Cluster::builder( 83 | CONFIG.bot_token.clone(), 84 | Intents::from_bits(CONFIG.intents).unwrap(), 85 | ) 86 | .gateway_url("wss://gateway.discord.gg".to_owned()) 87 | .shard_scheme(ShardScheme::Range { 88 | from: last_index, 89 | to: index, 90 | total: CONFIG.shards_total, 91 | }) 92 | .queue(queue.clone()) 93 | .presence( 94 | UpdatePresencePayload::new( 95 | vec![Activity { 96 | application_id: None, 97 | assets: None, 98 | buttons: Vec::new(), 99 | created_at: None, 100 | details: None, 101 | emoji: None, 102 | flags: None, 103 | id: None, 104 | instance: None, 105 | kind: CONFIG.activity_type, 106 | name: CONFIG.activity_name.clone(), 107 | party: None, 108 | secrets: None, 109 | state: None, 110 | timestamps: None, 111 | url: None, 112 | }], 113 | false, 114 | None, 115 | CONFIG.status, 116 | ) 117 | .unwrap(), 118 | ) 119 | .large_threshold(CONFIG.large_threshold) 120 | .resume_sessions(resumes.clone()) 121 | .event_types(EventTypeFlags::all()) 122 | .build() 123 | .await?; 124 | 125 | clusters.push(Arc::new(cluster)); 126 | events.push(event); 127 | 128 | last_index = index + 1; 129 | } 130 | 131 | Ok(( 132 | clusters, 133 | events, 134 | ClusterInfo { 135 | clusters: CONFIG.clusters, 136 | shards, 137 | resumes: resumes.len() as u64, 138 | }, 139 | )) 140 | } 141 | 142 | #[derive(Debug)] 143 | struct LocalQueue(UnboundedSender>); 144 | 145 | impl LocalQueue { 146 | fn new(duration: Duration) -> Self { 147 | let (tx, rx) = unbounded_channel(); 148 | tokio::spawn(waiter(rx, duration)); 149 | 150 | Self(tx) 151 | } 152 | } 153 | 154 | impl Queue for LocalQueue { 155 | fn request(&'_ self, [_, _]: [u64; 2]) -> Pin + Send + '_>> { 156 | Box::pin(async move { 157 | let (tx, rx) = oneshot::channel(); 158 | 159 | if let Err(err) = self.0.clone().send(tx) { 160 | warn!("skipping, send failed: {:?}", err); 161 | return; 162 | } 163 | 164 | let _ = rx.await; 165 | }) 166 | } 167 | } 168 | 169 | #[derive(Debug)] 170 | struct LargeBotQueue(Vec>>); 171 | 172 | impl LargeBotQueue { 173 | fn new(buckets: usize, duration: Duration) -> Self { 174 | let mut queues = Vec::with_capacity(buckets); 175 | for _ in 0..buckets { 176 | let (tx, rx) = unbounded_channel(); 177 | tokio::spawn(waiter(rx, duration)); 178 | queues.push(tx) 179 | } 180 | 181 | Self(queues) 182 | } 183 | } 184 | 185 | impl Queue for LargeBotQueue { 186 | fn request(&'_ self, shard_id: [u64; 2]) -> Pin + Send + '_>> { 187 | #[allow(clippy::cast_possible_truncation)] 188 | let bucket = (shard_id[0] % (self.0.len() as u64)) as usize; 189 | let (tx, rx) = oneshot::channel(); 190 | 191 | Box::pin(async move { 192 | if let Err(err) = self.0[bucket].clone().send(tx) { 193 | warn!("skipping, send failed: {:?}", err); 194 | return; 195 | } 196 | 197 | let _ = rx.await; 198 | }) 199 | } 200 | } 201 | 202 | async fn waiter(mut rx: UnboundedReceiver>, duration: Duration) { 203 | while let Some(req) = rx.recv().await { 204 | if let Err(err) = req.send(()) { 205 | warn!("skipping, send failed: {:?}", err); 206 | } 207 | sleep(duration).await; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use serde::de::DeserializeOwned; 3 | use std::env; 4 | use twilight_model::gateway::presence::{ActivityType, Status}; 5 | 6 | lazy_static! { 7 | pub static ref CONFIG: Config = { 8 | Config { 9 | rust_log: get_env("RUST_LOG"), 10 | bot_token: get_env("BOT_TOKEN"), 11 | shards_start: get_env_as("SHARDS_START"), 12 | shards_end: get_env_as("SHARDS_END"), 13 | shards_total: get_env_as("SHARDS_TOTAL"), 14 | shards_concurrency: get_env_as("SHARDS_CONCURRENCY"), 15 | shards_wait: get_env_as("SHARDS_WAIT"), 16 | clusters: get_env_as("CLUSTERS"), 17 | default_queue: get_env_as("DEFAULT_QUEUE"), 18 | resume: get_env_as("RESUME"), 19 | intents: get_env_as("INTENTS"), 20 | large_threshold: get_env_as("LARGE_THRESHOLD"), 21 | status: get_env_as("STATUS"), 22 | activity_type: get_env_as("ACTIVITY_TYPE"), 23 | activity_name: get_env("ACTIVITY_NAME"), 24 | log_channel: get_env_as("LOG_CHANNEL"), 25 | log_guild_channel: get_env_as("LOG_GUILD_CHANNEL"), 26 | state_enabled: get_env_as("STATE_ENABLED"), 27 | state_member: get_env_as("STATE_MEMBER"), 28 | state_member_ttl: get_env_as("STATE_MEMBER_TTL"), 29 | state_message: get_env_as("STATE_MESSAGE"), 30 | state_message_ttl: get_env_as("STATE_MESSAGE_TTL"), 31 | state_presence: get_env_as("STATE_PRESENCE"), 32 | state_emoji: get_env_as("STATE_EMOJI"), 33 | state_voice: get_env_as("STATE_VOICE"), 34 | state_old: get_env_as("STATE_OLD"), 35 | rabbit_host: get_env("RABBIT_HOST"), 36 | rabbit_port: get_env_as("RABBIT_PORT"), 37 | rabbit_username: get_env("RABBIT_USERNAME"), 38 | rabbit_password: get_env("RABBIT_PASSWORD"), 39 | redis_host: get_env("REDIS_HOST"), 40 | redis_port: get_env_as("REDIS_PORT"), 41 | prometheus_host: get_env("PROMETHEUS_HOST"), 42 | prometheus_port: get_env_as("PROMETHEUS_PORT"), 43 | } 44 | }; 45 | } 46 | 47 | #[derive(Clone, Debug)] 48 | pub struct Config { 49 | pub rust_log: String, 50 | pub bot_token: String, 51 | pub shards_start: u64, 52 | pub shards_end: u64, 53 | pub shards_total: u64, 54 | pub shards_concurrency: u64, 55 | pub shards_wait: u64, 56 | pub clusters: u64, 57 | pub default_queue: bool, 58 | pub resume: bool, 59 | pub intents: u64, 60 | pub large_threshold: u64, 61 | pub status: Status, 62 | pub activity_type: ActivityType, 63 | pub activity_name: String, 64 | pub log_channel: u64, 65 | pub log_guild_channel: u64, 66 | pub state_enabled: bool, 67 | pub state_member: bool, 68 | pub state_member_ttl: u64, 69 | pub state_message: bool, 70 | pub state_message_ttl: u64, 71 | pub state_presence: bool, 72 | pub state_emoji: bool, 73 | pub state_voice: bool, 74 | pub state_old: bool, 75 | pub rabbit_host: String, 76 | pub rabbit_port: u64, 77 | pub rabbit_username: String, 78 | pub rabbit_password: String, 79 | pub redis_host: String, 80 | pub redis_port: u64, 81 | pub prometheus_host: String, 82 | pub prometheus_port: u64, 83 | } 84 | 85 | fn get_env(name: &str) -> String { 86 | env::var(name).unwrap_or_else(|_| panic!("Missing environmental variable: {}", name)) 87 | } 88 | 89 | fn get_env_as(name: &str) -> T { 90 | let mut variable = get_env(name); 91 | 92 | unsafe { simd_json::from_str(variable.as_mut_str()) } 93 | .or_else(|_| unsafe { simd_json::from_str(format!(r#""{}""#, variable).as_mut_str()) }) 94 | .unwrap_or_else(|_| panic!("Invalid environmental variable: {}", name)) 95 | } 96 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | use twilight_model::id::{ 2 | marker::{ChannelMarker, EmojiMarker, GuildMarker, MessageMarker, RoleMarker, UserMarker}, 3 | Id, 4 | }; 5 | 6 | pub const EXCHANGE: &str = "gateway"; 7 | pub const QUEUE_RECV: &str = "gateway.recv"; 8 | pub const QUEUE_SEND: &str = "gateway.send"; 9 | 10 | pub const SESSIONS_KEY: &str = "gateway_sessions"; 11 | pub const STATUSES_KEY: &str = "gateway_statuses"; 12 | pub const STARTED_KEY: &str = "gateway_started"; 13 | pub const SHARDS_KEY: &str = "gateway_shards"; 14 | 15 | pub const BOT_USER_KEY: &str = "bot_user"; 16 | pub const GUILD_KEY: &str = "guild"; 17 | pub const CHANNEL_KEY: &str = "channel"; 18 | pub const MESSAGE_KEY: &str = "message"; 19 | pub const ROLE_KEY: &str = "role"; 20 | pub const EMOJI_KEY: &str = "emoji"; 21 | pub const MEMBER_KEY: &str = "member"; 22 | pub const PRESENCE_KEY: &str = "presence"; 23 | pub const VOICE_KEY: &str = "voice"; 24 | 25 | pub const KEYS_SUFFIX: &str = "_keys"; 26 | pub const EXPIRY_KEYS: &str = "expiry_keys"; 27 | 28 | pub const CACHE_JOB_INTERVAL: usize = 1000; 29 | pub const METRICS_JOB_INTERVAL: usize = 1000; 30 | 31 | pub const CONNECT_COLOR: usize = 0x00FF00; 32 | pub const DISCONNECT_COLOR: usize = 0xFF0000; 33 | pub const READY_COLOR: usize = 0x00FF00; 34 | pub const RESUME_COLOR: usize = 0x1E90FF; 35 | pub const JOIN_COLOR: usize = 0x00FF00; 36 | pub const LEAVE_COLOR: usize = 0xFF0000; 37 | 38 | pub fn guild_key(guild: Id) -> String { 39 | format!("{}:{}", GUILD_KEY, guild) 40 | } 41 | 42 | pub fn channel_key(guild: Option>, channel: Id) -> String { 43 | if let Some(guild) = guild { 44 | guild_channel_key(guild, channel) 45 | } else { 46 | private_channel_key(channel) 47 | } 48 | } 49 | 50 | fn guild_channel_key(guild: Id, channel: Id) -> String { 51 | format!("{}:{}:{}", CHANNEL_KEY, guild, channel) 52 | } 53 | 54 | fn private_channel_key(channel: Id) -> String { 55 | format!("{}:{}", CHANNEL_KEY, channel) 56 | } 57 | 58 | pub fn message_key(channel: Id, message: Id) -> String { 59 | format!("{}:{}:{}", MESSAGE_KEY, channel, message) 60 | } 61 | 62 | pub fn role_key(guild: Id, role: Id) -> String { 63 | format!("{}:{}:{}", ROLE_KEY, guild, role) 64 | } 65 | 66 | pub fn emoji_key(guild: Id, emoji: Id) -> String { 67 | format!("{}:{}:{}", EMOJI_KEY, guild, emoji) 68 | } 69 | 70 | pub fn member_key(guild: Id, member: Id) -> String { 71 | format!("{}:{}:{}", MEMBER_KEY, guild, member) 72 | } 73 | 74 | pub fn presence_key(guild: Id, member: Id) -> String { 75 | format!("{}:{}:{}", PRESENCE_KEY, guild, member) 76 | } 77 | 78 | pub fn voice_key(guild: Id, member: Id) -> String { 79 | format!("{}:{}:{}", VOICE_KEY, guild, member) 80 | } 81 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cache, 3 | config::CONFIG, 4 | constants::{ 5 | CONNECT_COLOR, DISCONNECT_COLOR, EXCHANGE, JOIN_COLOR, LEAVE_COLOR, QUEUE_SEND, 6 | READY_COLOR, RESUME_COLOR, 7 | }, 8 | metrics::{GATEWAY_EVENTS, GUILD_EVENTS, SHARD_EVENTS}, 9 | models::{DeliveryInfo, DeliveryOpcode, PayloadInfo}, 10 | utils::{log_discord, log_discord_guild}, 11 | }; 12 | 13 | use futures_util::{Stream, StreamExt}; 14 | use lapin::{ 15 | options::{BasicAckOptions, BasicConsumeOptions, BasicPublishOptions}, 16 | types::FieldTable, 17 | BasicProperties, Channel, 18 | }; 19 | use redis::aio::MultiplexedConnection; 20 | use simd_json::{json, ValueAccess}; 21 | use std::sync::Arc; 22 | use tracing::{info, warn}; 23 | use twilight_gateway::{shard::raw_message::Message, Cluster, Event, EventType}; 24 | use twilight_model::id::{marker::UserMarker, Id}; 25 | 26 | pub async fn outgoing( 27 | conn: MultiplexedConnection, 28 | cluster: Arc, 29 | channel: Channel, 30 | bot_id: Id, 31 | mut events: impl Stream + Send + Sync + Unpin + 'static, 32 | ) { 33 | while let Some((shard, event)) = events.next().await { 34 | let mut conn_clone = conn.clone(); 35 | let cluster_clone = cluster.clone(); 36 | let channel_clone = channel.clone(); 37 | 38 | if CONFIG.state_enabled && event.kind() == EventType::Ready { 39 | if let Err(err) = cache::update(&mut conn_clone, &event, bot_id).await { 40 | warn!("[Shard {}] Failed to update state: {:?}", shard, err); 41 | } 42 | } 43 | 44 | tokio::spawn(async move { 45 | let mut old = None; 46 | 47 | if CONFIG.state_enabled 48 | && event.kind() != EventType::ShardPayload 49 | && event.kind() != EventType::Ready 50 | { 51 | match cache::update(&mut conn_clone, &event, bot_id).await { 52 | Ok(value) => { 53 | old = value; 54 | } 55 | Err(err) => { 56 | warn!("[Shard {}] Failed to update state: {:?}", shard, err); 57 | } 58 | } 59 | } 60 | 61 | match event { 62 | Event::GatewayHello(data) => { 63 | info!("[Shard {}] Hello (heartbeat interval: {})", shard, data); 64 | } 65 | Event::GatewayInvalidateSession(data) => { 66 | info!("[Shard {}] Invalid Session (resumable: {})", shard, data); 67 | } 68 | Event::Ready(data) => { 69 | info!("[Shard {}] Ready (session: {})", shard, data.session_id); 70 | log_discord(READY_COLOR, format!("[Shard {}] Ready", shard)); 71 | SHARD_EVENTS.with_label_values(&["Ready"]).inc(); 72 | } 73 | Event::Resumed => { 74 | if let Some(Ok(info)) = cluster_clone.shard(shard).map(|shard| shard.info()) { 75 | info!( 76 | "[Shard {}] Resumed (session: {})", 77 | shard, 78 | info.session_id().unwrap_or_default() 79 | ); 80 | } else { 81 | info!("[Shard {}] Resumed", shard); 82 | } 83 | log_discord(RESUME_COLOR, format!("[Shard {}] Resumed", shard)); 84 | SHARD_EVENTS.with_label_values(&["Resumed"]).inc(); 85 | } 86 | Event::ShardConnected(_) => { 87 | info!("[Shard {}] Connected", shard); 88 | log_discord(CONNECT_COLOR, format!("[Shard {}] Connected", shard)); 89 | SHARD_EVENTS.with_label_values(&["Connected"]).inc(); 90 | } 91 | Event::ShardConnecting(data) => { 92 | info!("[Shard {}] Connecting (url: {})", shard, data.gateway); 93 | SHARD_EVENTS.with_label_values(&["Connecting"]).inc(); 94 | } 95 | Event::ShardDisconnected(data) => { 96 | if let Some(code) = data.code { 97 | let reason = data.reason.unwrap_or_default(); 98 | if !reason.is_empty() { 99 | info!( 100 | "[Shard {}] Disconnected (code: {}, reason: {})", 101 | shard, code, reason 102 | ); 103 | } else { 104 | info!("[Shard {}] Disconnected (code: {})", shard, code); 105 | } 106 | } else { 107 | info!("[Shard {}] Disconnected", shard); 108 | } 109 | log_discord(DISCONNECT_COLOR, format!("[Shard {}] Disconnected", shard)); 110 | SHARD_EVENTS.with_label_values(&["Disconnected"]).inc(); 111 | } 112 | Event::ShardIdentifying(_) => { 113 | info!("[Shard {}] Identifying", shard); 114 | SHARD_EVENTS.with_label_values(&["Identifying"]).inc(); 115 | } 116 | Event::ShardReconnecting(_) => { 117 | info!("[Shard {}] Reconnecting", shard); 118 | SHARD_EVENTS.with_label_values(&["Reconnecting"]).inc(); 119 | } 120 | Event::ShardResuming(data) => { 121 | info!("[Shard {}] Resuming (sequence: {})", shard, data.seq); 122 | SHARD_EVENTS.with_label_values(&["Resuming"]).inc(); 123 | } 124 | Event::GuildCreate(data) => { 125 | if old.is_none() { 126 | GUILD_EVENTS.with_label_values(&["Join"]).inc(); 127 | log_discord_guild( 128 | JOIN_COLOR, 129 | "Guild Join", 130 | format!("{} ({})", data.name, data.id), 131 | ); 132 | } 133 | } 134 | Event::GuildDelete(data) => { 135 | if !data.unavailable { 136 | GUILD_EVENTS.with_label_values(&["Leave"]).inc(); 137 | let old_data = old.unwrap_or(json!({})); 138 | let guild = old_data.as_object().unwrap(); 139 | log_discord_guild( 140 | LEAVE_COLOR, 141 | "Guild Leave", 142 | format!( 143 | "{} ({})", 144 | guild 145 | .get("name") 146 | .and_then(|name| name.as_str()) 147 | .unwrap_or("Unknown"), 148 | guild.get("id").and_then(|id| id.as_str()).unwrap_or("0") 149 | ), 150 | ); 151 | } 152 | } 153 | Event::ShardPayload(mut data) => { 154 | match simd_json::from_slice::(data.bytes.as_mut_slice()) { 155 | Ok(mut payload) => { 156 | if let Some(kind) = payload.t.as_deref() { 157 | GATEWAY_EVENTS 158 | .with_label_values(&[kind, shard.to_string().as_str()]) 159 | .inc(); 160 | 161 | payload.old = old; 162 | 163 | match simd_json::to_vec(&payload) { 164 | Ok(payload) => { 165 | let result = channel_clone 166 | .basic_publish( 167 | EXCHANGE, 168 | kind, 169 | BasicPublishOptions::default(), 170 | &payload, 171 | BasicProperties::default(), 172 | ) 173 | .await; 174 | 175 | if let Err(err) = result { 176 | warn!( 177 | "[Shard {}] Failed to publish event: {:?}", 178 | shard, err 179 | ); 180 | } 181 | } 182 | Err(err) => { 183 | warn!( 184 | "[Shard {}] Failed to serialize payload: {:?}", 185 | shard, err 186 | ); 187 | } 188 | } 189 | } 190 | } 191 | Err(err) => { 192 | warn!("[Shard {}] Could not decode payload: {:?}", shard, err); 193 | } 194 | } 195 | } 196 | _ => {} 197 | } 198 | }); 199 | } 200 | } 201 | 202 | pub async fn incoming(clusters: &[Arc], channel: &Channel) { 203 | let mut consumer = match channel 204 | .basic_consume( 205 | QUEUE_SEND, 206 | "", 207 | BasicConsumeOptions::default(), 208 | FieldTable::default(), 209 | ) 210 | .await 211 | { 212 | Ok(channel) => channel, 213 | Err(err) => { 214 | warn!("Failed to consume delivery channel: {:?}", err); 215 | return; 216 | } 217 | }; 218 | 219 | while let Some(message) = consumer.next().await { 220 | match message { 221 | Ok(mut delivery) => { 222 | let _ = channel 223 | .basic_ack(delivery.delivery_tag, BasicAckOptions::default()) 224 | .await; 225 | match simd_json::from_slice::(delivery.data.as_mut_slice()) { 226 | Ok(payload) => { 227 | let cluster = clusters 228 | .iter() 229 | .find(|cluster| cluster.shard(payload.shard).is_some()); 230 | if let Some(cluster) = cluster { 231 | match payload.op { 232 | DeliveryOpcode::Send => { 233 | if let Err(err) = cluster 234 | .send( 235 | payload.shard, 236 | Message::Binary( 237 | simd_json::to_vec( 238 | &payload.data.unwrap_or_default(), 239 | ) 240 | .unwrap_or_default(), 241 | ), 242 | ) 243 | .await 244 | { 245 | warn!("Failed to send gateway command: {:?}", err); 246 | } 247 | } 248 | DeliveryOpcode::Reconnect => { 249 | info!("Shutting down shard {}", payload.shard); 250 | cluster.shard(payload.shard).unwrap().shutdown(); 251 | } 252 | } 253 | } else { 254 | warn!("Delivery received for invalid shard: {}", payload.shard) 255 | } 256 | } 257 | Err(err) => { 258 | warn!("Failed to deserialize payload: {:?}", err); 259 | } 260 | } 261 | } 262 | Err(err) => { 263 | warn!("Failed to consume delivery: {:?}", err); 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | #![deny(clippy::all, nonstandard_style, rust_2018_idioms, unused, warnings)] 3 | 4 | use crate::{ 5 | cluster::get_clusters, 6 | config::CONFIG, 7 | constants::{SESSIONS_KEY, SHARDS_KEY, STARTED_KEY}, 8 | models::{ApiResult, FormattedDateTime, SessionInfo}, 9 | utils::{declare_queues, get_current_user}, 10 | }; 11 | 12 | use dotenv::dotenv; 13 | use lapin::ConnectionProperties; 14 | use std::collections::HashMap; 15 | use tokio::{join, signal::ctrl_c}; 16 | use tracing::{error, info}; 17 | 18 | mod cache; 19 | mod cluster; 20 | mod config; 21 | mod constants; 22 | mod handler; 23 | mod metrics; 24 | mod models; 25 | mod utils; 26 | 27 | #[tokio::main] 28 | async fn main() { 29 | dotenv().ok(); 30 | tracing_subscriber::fmt::init(); 31 | 32 | let result = real_main().await; 33 | 34 | if let Err(err) = result { 35 | error!("{:?}", err); 36 | } 37 | } 38 | 39 | async fn real_main() -> ApiResult<()> { 40 | let redis = redis::Client::open(format!( 41 | "redis://{}:{}/", 42 | CONFIG.redis_host, CONFIG.redis_port 43 | ))?; 44 | 45 | let mut conn = redis.get_multiplexed_tokio_connection().await?; 46 | 47 | let amqp = lapin::Connection::connect( 48 | format!( 49 | "amqp://{}:{}@{}:{}/%2f", 50 | CONFIG.rabbit_username, CONFIG.rabbit_password, CONFIG.rabbit_host, CONFIG.rabbit_port 51 | ) 52 | .as_str(), 53 | ConnectionProperties::default(), 54 | ) 55 | .await?; 56 | 57 | let channel = amqp.create_channel().await?; 58 | let channel_send = amqp.create_channel().await?; 59 | declare_queues(&channel, &channel_send).await?; 60 | 61 | let user = get_current_user().await?; 62 | let (clusters, events, info) = get_clusters(&mut conn).await?; 63 | 64 | info!("Starting up {} clusters", info.clusters); 65 | info!("Starting up {} shards", info.shards); 66 | info!("Resuming {} sessions", info.resumes); 67 | 68 | cache::set(&mut conn, STARTED_KEY, &FormattedDateTime::now()).await?; 69 | cache::set(&mut conn, SHARDS_KEY, &CONFIG.shards_total).await?; 70 | 71 | tokio::spawn(async { 72 | let _ = metrics::run_server().await; 73 | }); 74 | 75 | let conn_clone = conn.clone(); 76 | let clusters_clone = clusters.clone(); 77 | tokio::spawn(async move { 78 | join!( 79 | cache::run_jobs(conn_clone.clone(), clusters_clone.as_slice()), 80 | metrics::run_jobs(conn_clone.clone(), clusters_clone.as_slice()), 81 | ) 82 | }); 83 | 84 | for (cluster, events) in clusters.clone().into_iter().zip(events.into_iter()) { 85 | let cluster_clone = cluster.clone(); 86 | tokio::spawn(async move { 87 | cluster_clone.up().await; 88 | }); 89 | 90 | let conn_clone = redis.get_multiplexed_tokio_connection().await?; 91 | let cluster_clone = cluster.clone(); 92 | let channel_clone = channel.clone(); 93 | tokio::spawn(async move { 94 | handler::outgoing(conn_clone, cluster_clone, channel_clone, user.id, events).await; 95 | }); 96 | } 97 | 98 | let channel_clone = channel_send.clone(); 99 | let clusters_clone = clusters.clone(); 100 | tokio::spawn(async move { 101 | handler::incoming(clusters_clone.as_slice(), &channel_clone).await; 102 | }); 103 | 104 | ctrl_c().await?; 105 | 106 | info!("Shutting down"); 107 | 108 | let mut sessions = HashMap::new(); 109 | for cluster in clusters { 110 | for (key, value) in cluster.down_resumable().into_iter() { 111 | sessions.insert( 112 | key.to_string(), 113 | SessionInfo { 114 | session_id: value.session_id, 115 | sequence: value.sequence, 116 | }, 117 | ); 118 | } 119 | } 120 | 121 | cache::set(&mut conn, SESSIONS_KEY, &sessions).await?; 122 | 123 | Ok(()) 124 | } 125 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cache, 3 | config::CONFIG, 4 | constants::{ 5 | CHANNEL_KEY, EMOJI_KEY, GUILD_KEY, KEYS_SUFFIX, MEMBER_KEY, MESSAGE_KEY, 6 | METRICS_JOB_INTERVAL, PRESENCE_KEY, ROLE_KEY, VOICE_KEY, 7 | }, 8 | models::ApiResult, 9 | }; 10 | 11 | use hyper::{ 12 | header::CONTENT_TYPE, 13 | server::Server, 14 | service::{make_service_fn, service_fn}, 15 | Body, Method, Request, Response, StatusCode, 16 | }; 17 | use lazy_static::lazy_static; 18 | use prometheus::{ 19 | register_int_counter_vec, register_int_gauge, register_int_gauge_vec, Encoder, IntCounterVec, 20 | IntGauge, IntGaugeVec, TextEncoder, 21 | }; 22 | use redis::aio::MultiplexedConnection; 23 | use std::{ 24 | collections::HashMap, 25 | net::{IpAddr, SocketAddr}, 26 | str::FromStr, 27 | sync::Arc, 28 | }; 29 | use tokio::time::{sleep, Duration}; 30 | use tracing::warn; 31 | use twilight_gateway::{shard::Stage, Cluster}; 32 | 33 | lazy_static! { 34 | pub static ref GATEWAY_EVENTS: IntCounterVec = register_int_counter_vec!( 35 | "gateway_events", 36 | "Events received through the Discord gateway", 37 | &["type", "shard"] 38 | ) 39 | .unwrap(); 40 | pub static ref SHARD_EVENTS: IntCounterVec = register_int_counter_vec!( 41 | "gateway_shard_events", 42 | "Discord shard connection events", 43 | &["type"] 44 | ) 45 | .unwrap(); 46 | pub static ref GUILD_EVENTS: IntCounterVec = register_int_counter_vec!( 47 | "gateway_guild_events", 48 | "Discord guild join and leave events", 49 | &["type"] 50 | ) 51 | .unwrap(); 52 | pub static ref GATEWAY_SHARDS: IntGauge = register_int_gauge!( 53 | "gateway_shards", 54 | "Number of gateway connections with Discord" 55 | ) 56 | .unwrap(); 57 | pub static ref GATEWAY_STATUSES: IntGaugeVec = register_int_gauge_vec!( 58 | "gateway_statuses", 59 | "Status of the gateway connections", 60 | &["type"] 61 | ) 62 | .unwrap(); 63 | pub static ref GATEWAY_LATENCIES: IntGaugeVec = register_int_gauge_vec!( 64 | "gateway_latencies", 65 | "API latency with the Discord gateway", 66 | &["shard"] 67 | ) 68 | .unwrap(); 69 | pub static ref STATE_GUILDS: IntGauge = 70 | register_int_gauge!("state_guilds", "Number of guilds in state cache").unwrap(); 71 | pub static ref STATE_CHANNELS: IntGauge = 72 | register_int_gauge!("state_channels", "Number of channels in state cache").unwrap(); 73 | pub static ref STATE_MESSAGES: IntGauge = 74 | register_int_gauge!("state_messages", "Number of messages in state cache").unwrap(); 75 | pub static ref STATE_ROLES: IntGauge = 76 | register_int_gauge!("state_roles", "Number of roles in state cache").unwrap(); 77 | pub static ref STATE_EMOJIS: IntGauge = 78 | register_int_gauge!("state_emojis", "Number of emojis in state cache").unwrap(); 79 | pub static ref STATE_MEMBERS: IntGauge = 80 | register_int_gauge!("state_members", "Number of members in state cache").unwrap(); 81 | pub static ref STATE_PRESENCES: IntGauge = 82 | register_int_gauge!("state_presences", "Number of presences in state cache").unwrap(); 83 | pub static ref STATE_VOICES: IntGauge = 84 | register_int_gauge!("state_voices", "Number of voices in state cache").unwrap(); 85 | } 86 | 87 | async fn serve(req: Request) -> ApiResult> { 88 | if req.method() == Method::GET && req.uri().path() == "/metrics" { 89 | let mut buffer = vec![]; 90 | let metrics = prometheus::gather(); 91 | 92 | let encoder = TextEncoder::new(); 93 | encoder.encode(metrics.as_slice(), &mut buffer)?; 94 | 95 | Ok(Response::builder() 96 | .status(StatusCode::OK) 97 | .header(CONTENT_TYPE, encoder.format_type()) 98 | .body(Body::from(buffer))?) 99 | } else if req.method() == Method::GET && req.uri().path() == "/healthcheck" { 100 | Ok(Response::builder() 101 | .status(StatusCode::OK) 102 | .header(CONTENT_TYPE, "application/json") 103 | .body(Body::from("{\"status\":\"OK\"}"))?) 104 | } else { 105 | Ok(Response::builder() 106 | .status(StatusCode::NOT_FOUND) 107 | .body(Body::empty())?) 108 | } 109 | } 110 | 111 | pub async fn run_server() -> ApiResult<()> { 112 | let addr = SocketAddr::new( 113 | IpAddr::from_str(CONFIG.prometheus_host.as_str())?, 114 | CONFIG.prometheus_port as u16, 115 | ); 116 | 117 | let make_svc = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(serve)) }); 118 | 119 | Server::bind(&addr).serve(make_svc).await?; 120 | 121 | Err(().into()) 122 | } 123 | 124 | struct StateStats { 125 | guilds: u64, 126 | channels: u64, 127 | messages: u64, 128 | roles: u64, 129 | emojis: u64, 130 | members: u64, 131 | presences: u64, 132 | voices: u64, 133 | } 134 | 135 | async fn get_state_stats(conn: &mut MultiplexedConnection) -> ApiResult { 136 | let guilds = cache::get_members_len(conn, format!("{}{}", GUILD_KEY, KEYS_SUFFIX)).await?; 137 | let channels = cache::get_members_len(conn, format!("{}{}", CHANNEL_KEY, KEYS_SUFFIX)).await?; 138 | let messages = cache::get_members_len(conn, format!("{}{}", MESSAGE_KEY, KEYS_SUFFIX)).await?; 139 | let roles = cache::get_members_len(conn, format!("{}{}", ROLE_KEY, KEYS_SUFFIX)).await?; 140 | let emojis = cache::get_members_len(conn, format!("{}{}", EMOJI_KEY, KEYS_SUFFIX)).await?; 141 | let members = cache::get_members_len(conn, format!("{}{}", MEMBER_KEY, KEYS_SUFFIX)).await?; 142 | let presences = 143 | cache::get_members_len(conn, format!("{}{}", PRESENCE_KEY, KEYS_SUFFIX)).await?; 144 | let voices = cache::get_members_len(conn, format!("{}{}", VOICE_KEY, KEYS_SUFFIX)).await?; 145 | 146 | Ok(StateStats { 147 | guilds, 148 | channels, 149 | messages, 150 | roles, 151 | emojis, 152 | members, 153 | presences, 154 | voices, 155 | }) 156 | } 157 | 158 | pub async fn run_jobs(mut conn: MultiplexedConnection, clusters: &[Arc]) { 159 | loop { 160 | GATEWAY_SHARDS.set( 161 | clusters 162 | .iter() 163 | .fold(0, |acc, cluster| acc + cluster.shards().len() as i64), 164 | ); 165 | 166 | let mut statuses = HashMap::new(); 167 | statuses.insert(format!("{}", Stage::Connected), 0); 168 | statuses.insert(format!("{}", Stage::Disconnected), 0); 169 | statuses.insert(format!("{}", Stage::Handshaking), 0); 170 | statuses.insert(format!("{}", Stage::Identifying), 0); 171 | statuses.insert(format!("{}", Stage::Resuming), 0); 172 | 173 | for cluster in clusters { 174 | for shard in cluster.shards() { 175 | if let Ok(info) = shard.info() { 176 | GATEWAY_LATENCIES 177 | .with_label_values(&[info.id().to_string().as_str()]) 178 | .set( 179 | info.latency() 180 | .recent() 181 | .back() 182 | .map(|value| value.as_millis() as i64) 183 | .unwrap_or_default(), 184 | ); 185 | 186 | if let Some(count) = statuses.get_mut(&info.stage().to_string()) { 187 | *count += 1; 188 | } 189 | } 190 | } 191 | } 192 | 193 | for (stage, amount) in statuses { 194 | GATEWAY_STATUSES 195 | .with_label_values(&[stage.as_str()]) 196 | .set(amount); 197 | } 198 | 199 | match get_state_stats(&mut conn).await { 200 | Ok(stats) => { 201 | STATE_GUILDS.set(stats.guilds as i64); 202 | STATE_CHANNELS.set(stats.channels as i64); 203 | STATE_MESSAGES.set(stats.messages as i64); 204 | STATE_ROLES.set(stats.roles as i64); 205 | STATE_EMOJIS.set(stats.emojis as i64); 206 | STATE_MEMBERS.set(stats.members as i64); 207 | STATE_PRESENCES.set(stats.presences as i64); 208 | STATE_VOICES.set(stats.voices as i64); 209 | } 210 | Err(err) => { 211 | warn!("Failed to get state stats: {:?}", err); 212 | } 213 | } 214 | 215 | sleep(Duration::from_millis(METRICS_JOB_INTERVAL as u64)).await; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | use hyper::{http::Error as HyperHttpError, Error as HyperError}; 2 | use lapin::Error as LapinError; 3 | use prometheus::Error as PrometheusError; 4 | use redis::RedisError; 5 | use serde::{de::Error as SerdeDeError, Deserialize, Deserializer, Serialize, Serializer}; 6 | use serde_repr::{Deserialize_repr, Serialize_repr}; 7 | use simd_json::{owned::Value, Error as SimdJsonError}; 8 | use std::{ 9 | env::VarError, 10 | error::Error, 11 | fmt::{self, Display, Formatter}, 12 | io::Error as IoError, 13 | net::AddrParseError, 14 | num::ParseIntError, 15 | ops::{Add, Sub}, 16 | }; 17 | use time::{format_description, Duration, OffsetDateTime}; 18 | use twilight_gateway::cluster::ClusterStartError; 19 | use twilight_http::{response::DeserializeBodyError, Error as TwilightHttpError}; 20 | use twilight_model::{ 21 | channel::Channel, 22 | gateway::{payload::incoming::GuildCreate, presence::Presence, OpCode}, 23 | guild::{Emoji, Member, Role}, 24 | voice::VoiceState, 25 | }; 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct FormattedDateTime(OffsetDateTime); 29 | 30 | impl FormattedDateTime { 31 | pub fn now() -> Self { 32 | Self(OffsetDateTime::now_utc()) 33 | } 34 | } 35 | 36 | impl Sub for FormattedDateTime { 37 | type Output = Self; 38 | 39 | fn sub(self, rhs: Duration) -> Self::Output { 40 | Self(self.0.sub(rhs)) 41 | } 42 | } 43 | 44 | impl Sub for FormattedDateTime { 45 | type Output = Duration; 46 | 47 | fn sub(self, rhs: FormattedDateTime) -> Self::Output { 48 | self.0.sub(rhs.0) 49 | } 50 | } 51 | 52 | impl Add for FormattedDateTime { 53 | type Output = Self; 54 | 55 | fn add(self, rhs: Duration) -> Self::Output { 56 | Self(self.0.add(rhs)) 57 | } 58 | } 59 | 60 | impl Serialize for FormattedDateTime { 61 | fn serialize(&self, serializer: S) -> Result 62 | where 63 | S: Serializer, 64 | { 65 | let format = 66 | format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]") 67 | .unwrap(); 68 | serializer.serialize_str(&self.0.format(&format).unwrap()) 69 | } 70 | } 71 | 72 | impl<'de> Deserialize<'de> for FormattedDateTime { 73 | fn deserialize(deserializer: D) -> Result 74 | where 75 | D: Deserializer<'de>, 76 | { 77 | let string = String::deserialize(deserializer)? + "+00"; 78 | let format = format_description::parse( 79 | "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]+[offset_hour]", 80 | ) 81 | .unwrap(); 82 | match OffsetDateTime::parse(string.as_str(), &format) { 83 | Ok(dt) => Ok(Self(dt)), 84 | Err(_) => Err(SerdeDeError::custom("not a valid formatted timestamp")), 85 | } 86 | } 87 | 88 | fn deserialize_in_place(deserializer: D, place: &mut Self) -> Result<(), D::Error> 89 | where 90 | D: Deserializer<'de>, 91 | { 92 | let string = String::deserialize(deserializer)? + "+00"; 93 | let format = format_description::parse( 94 | "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]+[offset_hour]", 95 | ) 96 | .unwrap(); 97 | match OffsetDateTime::parse(string.as_str(), &format) { 98 | Ok(dt) => { 99 | place.0 = dt; 100 | Ok(()) 101 | } 102 | Err(_) => Err(SerdeDeError::custom("not a valid formatted timestamp")), 103 | } 104 | } 105 | } 106 | 107 | #[derive(Clone, Debug, Deserialize, Serialize)] 108 | pub struct SessionInfo { 109 | pub session_id: String, 110 | pub sequence: u64, 111 | } 112 | 113 | #[derive(Clone, Debug, Deserialize, Serialize)] 114 | pub struct StatusInfo { 115 | pub shard: u64, 116 | pub status: String, 117 | pub latency: u64, 118 | pub last_ack: FormattedDateTime, 119 | } 120 | 121 | #[derive(Clone, Debug, Deserialize, Serialize)] 122 | pub struct PayloadInfo { 123 | pub op: OpCode, 124 | pub t: Option, 125 | pub d: Value, 126 | #[serde(default, skip_serializing_if = "Option::is_none")] 127 | pub old: Option, 128 | } 129 | 130 | #[derive(Clone, Debug, Deserialize_repr, Serialize_repr)] 131 | #[repr(u8)] 132 | pub enum DeliveryOpcode { 133 | Send, 134 | Reconnect, 135 | } 136 | 137 | #[derive(Clone, Debug, Deserialize, Serialize)] 138 | pub struct DeliveryInfo { 139 | pub op: DeliveryOpcode, 140 | pub shard: u64, 141 | pub data: Option, 142 | } 143 | 144 | #[allow(clippy::large_enum_variant)] 145 | #[derive(Clone, Debug, Deserialize, Serialize)] 146 | #[serde(untagged)] 147 | pub enum GuildItem { 148 | Guild(Box), 149 | Channel(Channel), 150 | Role(Role), 151 | Emoji(Emoji), 152 | Voice(VoiceState), 153 | Member(Member), 154 | Presence(Presence), 155 | } 156 | 157 | pub type ApiResult = Result; 158 | 159 | macro_rules! make_api_errors { 160 | ($($name:ident($ty:ty)),*) => { 161 | #[derive(Debug)] 162 | pub enum ApiError { 163 | $($name($ty)),* 164 | } 165 | 166 | $( 167 | impl From<$ty> for ApiError { 168 | fn from(err: $ty) -> Self { 169 | Self::$name(err) 170 | } 171 | } 172 | )* 173 | } 174 | } 175 | 176 | make_api_errors! { 177 | Empty(()), 178 | SimdJson(SimdJsonError), 179 | Redis(RedisError), 180 | Var(VarError), 181 | ParseInt(ParseIntError), 182 | Lapin(LapinError), 183 | ClusterStart(ClusterStartError), 184 | Hyper(HyperError), 185 | HyperHttp(HyperHttpError), 186 | AddrParse(AddrParseError), 187 | Prometheus(PrometheusError), 188 | Io(IoError), 189 | TwilightHttp(TwilightHttpError), 190 | DeserializeBody(DeserializeBodyError) 191 | } 192 | 193 | impl Error for ApiError {} 194 | 195 | impl Display for ApiError { 196 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 197 | write!(f, "{:?}", self) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::CONFIG, 3 | constants::{EXCHANGE, QUEUE_RECV, QUEUE_SEND}, 4 | models::ApiResult, 5 | }; 6 | 7 | use lapin::{ 8 | options::{ExchangeDeclareOptions, QueueBindOptions, QueueDeclareOptions}, 9 | types::FieldTable, 10 | Channel, ExchangeKind, 11 | }; 12 | use lazy_static::lazy_static; 13 | use serde::Serialize; 14 | use simd_json::owned::Value; 15 | use time::OffsetDateTime; 16 | use tracing::warn; 17 | use twilight_http::client::Client; 18 | use twilight_model::{ 19 | channel::message::Embed, id::Id, user::CurrentUser, util::datetime::Timestamp, 20 | }; 21 | 22 | lazy_static! { 23 | static ref CLIENT: Client = Client::new(CONFIG.bot_token.clone()); 24 | } 25 | 26 | pub async fn declare_queues(channel: &Channel, channel_send: &Channel) -> ApiResult<()> { 27 | channel 28 | .exchange_declare( 29 | EXCHANGE, 30 | ExchangeKind::Topic, 31 | ExchangeDeclareOptions { 32 | passive: false, 33 | durable: true, 34 | auto_delete: false, 35 | internal: false, 36 | nowait: false, 37 | }, 38 | FieldTable::default(), 39 | ) 40 | .await?; 41 | channel_send 42 | .queue_declare( 43 | QUEUE_SEND, 44 | QueueDeclareOptions { 45 | passive: false, 46 | durable: true, 47 | exclusive: false, 48 | auto_delete: false, 49 | nowait: false, 50 | }, 51 | FieldTable::default(), 52 | ) 53 | .await?; 54 | 55 | if CONFIG.default_queue { 56 | channel 57 | .queue_declare( 58 | QUEUE_RECV, 59 | QueueDeclareOptions { 60 | passive: false, 61 | durable: true, 62 | exclusive: false, 63 | auto_delete: false, 64 | nowait: false, 65 | }, 66 | FieldTable::default(), 67 | ) 68 | .await?; 69 | 70 | channel 71 | .queue_bind( 72 | QUEUE_RECV, 73 | EXCHANGE, 74 | "#", 75 | QueueBindOptions::default(), 76 | FieldTable::default(), 77 | ) 78 | .await?; 79 | } 80 | 81 | Ok(()) 82 | } 83 | 84 | pub async fn get_current_user() -> ApiResult { 85 | let user = CLIENT.current_user().await?.model().await?; 86 | 87 | Ok(user) 88 | } 89 | 90 | pub fn log_discord(color: usize, message: impl Into) { 91 | if CONFIG.log_channel == 0 { 92 | return; 93 | } 94 | 95 | let message = message.into(); 96 | 97 | tokio::spawn(async move { 98 | let embeds = &[Embed { 99 | author: None, 100 | color: Some(color as u32), 101 | description: None, 102 | fields: vec![], 103 | footer: None, 104 | image: None, 105 | kind: "".to_owned(), 106 | provider: None, 107 | thumbnail: None, 108 | timestamp: Some( 109 | Timestamp::from_secs(OffsetDateTime::now_utc().unix_timestamp()).unwrap(), 110 | ), 111 | title: Some(message), 112 | url: None, 113 | video: None, 114 | }]; 115 | 116 | let message = CLIENT 117 | .create_message(Id::new(CONFIG.log_channel)) 118 | .embeds(embeds); 119 | 120 | if let Ok(message) = message { 121 | if let Err(err) = message.await { 122 | warn!("Failed to post message to Discord: {:?}", err) 123 | } 124 | } 125 | }); 126 | } 127 | 128 | pub fn log_discord_guild(color: usize, title: impl Into, message: impl Into) { 129 | if CONFIG.log_guild_channel == 0 { 130 | return; 131 | } 132 | 133 | let title = title.into(); 134 | let message = message.into(); 135 | 136 | tokio::spawn(async move { 137 | let embeds = &[Embed { 138 | author: None, 139 | color: Some(color as u32), 140 | description: Some(message), 141 | fields: vec![], 142 | footer: None, 143 | image: None, 144 | kind: "".to_owned(), 145 | provider: None, 146 | thumbnail: None, 147 | timestamp: Some( 148 | Timestamp::from_secs(OffsetDateTime::now_utc().unix_timestamp()).unwrap(), 149 | ), 150 | title: Some(title), 151 | url: None, 152 | video: None, 153 | }]; 154 | 155 | let message = CLIENT 156 | .create_message(Id::new(CONFIG.log_guild_channel)) 157 | .embeds(embeds); 158 | 159 | if let Ok(message) = message { 160 | if let Err(err) = message.await { 161 | warn!("Failed to post message to Discord: {:?}", err) 162 | } 163 | } 164 | }); 165 | } 166 | 167 | pub fn to_value(value: &T) -> ApiResult 168 | where 169 | T: Serialize + ?Sized, 170 | { 171 | let mut bytes = simd_json::to_vec(value)?; 172 | let result = simd_json::owned::to_value(bytes.as_mut_slice())?; 173 | 174 | Ok(result) 175 | } 176 | --------------------------------------------------------------------------------