Compare commits

...
18 Commits
Author SHA1 Message Date
Paul Makles 0954d71f51 chore: bump version to 0.7.13 2024-06-29 17:36:54 +01:00
Paul Makles 9ea2bd9f2f feat: implement apple push notifications 2024-06-29 17:35:47 +01:00
Paul Makles d6bcb844db feat: include user in responses when creating and editing bots 2024-06-26 19:49:26 +01:00
Paul Makles ce20e689cc refactor: fix spelling of "suppress" 2024-06-26 19:48:42 +01:00
Paul Makles d9cfc9d0e1 chore: bump version to 0.7.12 2024-06-26 18:47:40 +01:00
Paul Makles 64a07d09f4 feat: silent messages and message flags 2024-06-26 18:46:05 +01:00
Paul Makles 00e881799f fix: replace user with target user when editing someone else 2024-06-26 18:24:38 +01:00
Paul Makles fa598dd6f8 docs: update README.md
Signed-off-by: Paul Makles <me@insrt.uk>
2024-06-26 17:18:10 +01:00
Paul Makles ac4e2cb10b ci: disable image builds if they're not tagged [skip ci]
(seems like a waste of CPU)
2024-06-26 15:08:12 +01:00
Paul Makles ec578ab0ef chore: lessen restrictions on interacting with bot users as owner 2024-06-26 15:07:19 +01:00
Paul Makles ea6ba59841 fix: add DisplayName back to user fields (prevented removing name) 2024-06-26 14:54:06 +01:00
Paul Makles f903f716e3 chore: bump version to 0.7.11 2024-06-25 21:03:00 +01:00
Paul Makles c5f4b94aa5 fix(bonfire): Ready event should include user with online status true (if applicable) 2024-06-25 21:02:27 +01:00
Paul Makles 1d5dae4751 feat(core): include user and member on Message events 2024-06-25 21:01:45 +01:00
Paul Makles ac20b6bc99 chore: bump version to 0.7.10 2024-06-25 19:24:28 +01:00
Paul Makles 6ec8007e4e feat(core): separate limits for new user accounts 2024-06-25 19:20:34 +01:00
Paul Makles e5eea267cf refactor(core/config): move limits to 'global' key 2024-06-25 18:10:23 +01:00
Paul Makles de5add09d0 fix(core/database): only run outgoing friend checks if we are creating new request
closes #327
2024-06-25 18:07:16 +01:00
51 changed files with 906 additions and 228 deletions
+2 -2
View File
@@ -2,8 +2,8 @@ name: Docker Test & Publish
on:
push:
branches:
- "main"
# branches:
# - "main"
tags:
- "*"
paths-ignore:
Generated
+335 -67
View File
@@ -40,9 +40,9 @@ dependencies = [
[[package]]
name = "aes-gcm"
version = "0.9.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6"
checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f"
dependencies = [
"aead",
"aes",
@@ -457,6 +457,12 @@ version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
@@ -561,7 +567,7 @@ dependencies = [
"base64 0.13.0",
"chrono",
"hex",
"indexmap",
"indexmap 1.9.3",
"lazy_static",
"rand 0.8.5",
"serde",
@@ -898,9 +904,9 @@ dependencies = [
[[package]]
name = "ctr"
version = "0.8.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481"
dependencies = [
"cipher",
]
@@ -1297,6 +1303,12 @@ dependencies = [
"termcolor",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
version = "0.3.20"
@@ -1625,13 +1637,13 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.6"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [
"cfg-if",
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@@ -1715,8 +1727,27 @@ dependencies = [
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"http 0.2.7",
"indexmap 1.9.3",
"slab",
"tokio 1.35.1",
"tokio-util 0.7.2",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
dependencies = [
"atomic-waker",
"bytes 1.5.0",
"fnv",
"futures-core",
"futures-sink",
"http 1.1.0",
"indexmap 2.0.1",
"slab",
"tokio 1.35.1",
"tokio-util 0.7.2",
@@ -1856,6 +1887,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes 1.5.0",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.5"
@@ -1863,7 +1905,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes 1.5.0",
"http",
"http 0.2.7",
"pin-project-lite 0.2.13",
]
[[package]]
name = "http-body"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes 1.5.0",
"http 1.1.0",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes 1.5.0",
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"pin-project-lite 0.2.13",
]
@@ -1898,9 +1963,9 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"h2 0.3.13",
"http 0.2.7",
"http-body 0.4.5",
"httparse",
"httpdate",
"itoa",
@@ -1912,6 +1977,42 @@ dependencies = [
"want",
]
[[package]]
name = "hyper"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [
"bytes 1.5.0",
"futures-channel",
"futures-util",
"h2 0.4.5",
"http 1.1.0",
"http-body 1.0.0",
"pin-project-lite 0.2.13",
"smallvec",
"tokio 1.35.1",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.3.1",
"hyper-util",
"rustls 0.22.4",
"rustls-pki-types",
"tokio 1.35.1",
"tokio-rustls 0.25.0",
"tower-service",
"webpki-roots 0.26.3",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
@@ -1919,12 +2020,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.5.0",
"hyper",
"hyper 0.14.19",
"native-tls",
"tokio 1.35.1",
"tokio-native-tls",
]
[[package]]
name = "hyper-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
dependencies = [
"bytes 1.5.0",
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"hyper 1.3.1",
"pin-project-lite 0.2.13",
"socket2 0.5.5",
"tokio 1.35.1",
"tower",
"tower-service",
"tracing",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -1975,6 +2096,16 @@ dependencies = [
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
name = "inlinable_string"
version = "0.1.15"
@@ -2022,7 +2153,7 @@ dependencies = [
"encoding_rs",
"event-listener 2.5.2",
"futures-lite",
"http",
"http 0.2.7",
"log",
"mime",
"once_cell",
@@ -2461,8 +2592,8 @@ dependencies = [
"percent-encoding",
"rand 0.8.5",
"rustc_version_runtime",
"rustls",
"rustls-pemfile",
"rustls 0.20.6",
"rustls-pemfile 0.3.0",
"serde",
"serde_with",
"sha-1 0.10.0",
@@ -2473,13 +2604,13 @@ dependencies = [
"take_mut",
"thiserror",
"tokio 1.35.1",
"tokio-rustls",
"tokio-rustls 0.23.4",
"tokio-util 0.7.2",
"trust-dns-proto",
"trust-dns-resolver",
"typed-builder",
"uuid 0.8.2",
"webpki-roots",
"webpki-roots 0.22.3",
]
[[package]]
@@ -2491,7 +2622,7 @@ dependencies = [
"bytes 1.5.0",
"encoding_rs",
"futures-util",
"http",
"http 0.2.7",
"httparse",
"log",
"memchr",
@@ -2856,6 +2987,16 @@ dependencies = [
"base64 0.13.0",
]
[[package]]
name = "pem"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
dependencies = [
"base64 0.22.1",
"serde",
]
[[package]]
name = "pem-rfc7468"
version = "0.6.0"
@@ -3415,10 +3556,10 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"h2 0.3.13",
"http 0.2.7",
"http-body 0.4.5",
"hyper 0.14.19",
"hyper-tls",
"ipnet",
"js-sys",
@@ -3452,7 +3593,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "0.7.9"
version = "0.7.13"
dependencies = [
"async-channel 2.3.1",
"async-std",
@@ -3482,7 +3623,7 @@ dependencies = [
[[package]]
name = "revolt-config"
version = "0.7.9"
version = "0.7.13"
dependencies = [
"async-std",
"cached",
@@ -3498,7 +3639,7 @@ dependencies = [
[[package]]
name = "revolt-database"
version = "0.7.9"
version = "0.7.13"
dependencies = [
"async-lock",
"async-recursion",
@@ -3511,7 +3652,7 @@ dependencies = [
"decancer",
"fcm",
"futures",
"indexmap",
"indexmap 1.9.3",
"isahc",
"iso8601-timestamp 0.2.11",
"linkify 0.8.1",
@@ -3528,6 +3669,7 @@ dependencies = [
"revolt-permissions",
"revolt-presence",
"revolt-result",
"revolt_a2",
"revolt_okapi",
"revolt_optional_struct",
"revolt_rocket_okapi",
@@ -3544,7 +3686,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.7.9"
version = "0.7.13"
dependencies = [
"async-channel 1.6.1",
"async-std",
@@ -3572,6 +3714,7 @@ dependencies = [
"revolt-database",
"revolt-models",
"revolt-permissions",
"revolt-presence",
"revolt-result",
"revolt_rocket_okapi",
"rocket",
@@ -3590,9 +3733,9 @@ dependencies = [
[[package]]
name = "revolt-models"
version = "0.7.9"
version = "0.7.13"
dependencies = [
"indexmap",
"indexmap 1.9.3",
"iso8601-timestamp 0.2.11",
"once_cell",
"regex",
@@ -3607,7 +3750,7 @@ dependencies = [
[[package]]
name = "revolt-permissions"
version = "0.7.9"
version = "0.7.13"
dependencies = [
"async-std",
"async-trait",
@@ -3622,7 +3765,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
version = "0.7.9"
version = "0.7.13"
dependencies = [
"async-std",
"log",
@@ -3633,7 +3776,7 @@ dependencies = [
[[package]]
name = "revolt-result"
version = "0.7.9"
version = "0.7.13"
dependencies = [
"revolt_okapi",
"revolt_rocket_okapi",
@@ -3643,6 +3786,30 @@ dependencies = [
"serde_json",
]
[[package]]
name = "revolt_a2"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "466eb5262fcbb26e6e10b8a0acf56eb7cc095008132ff4ebddfc44d8672f8066"
dependencies = [
"base64 0.21.3",
"erased-serde",
"http 1.1.0",
"http-body-util",
"hyper 1.3.1",
"hyper-rustls",
"hyper-util",
"parking_lot",
"pem 3.0.4",
"ring 0.17.3",
"rustls 0.22.4",
"rustls-pemfile 2.1.2",
"serde",
"serde_json",
"thiserror",
"tokio 1.35.1",
]
[[package]]
name = "revolt_okapi"
version = "0.9.1"
@@ -3715,11 +3882,25 @@ dependencies = [
"libc",
"once_cell",
"spin 0.5.2",
"untrusted",
"untrusted 0.7.1",
"web-sys",
"winapi",
]
[[package]]
name = "ring"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e"
dependencies = [
"cc",
"getrandom",
"libc",
"spin 0.9.3",
"untrusted 0.9.0",
"windows-sys 0.48.0",
]
[[package]]
name = "rmp"
version = "0.8.11"
@@ -3757,7 +3938,7 @@ dependencies = [
"either",
"figment",
"futures",
"indexmap",
"indexmap 1.9.3",
"log",
"memchr",
"multer",
@@ -3805,7 +3986,7 @@ checksum = "d6aeb6bb9c61e9cd2c00d70ea267bf36f76a4cc615e5908b349c2f9d93999b47"
dependencies = [
"devise",
"glob",
"indexmap",
"indexmap 1.9.3",
"proc-macro2",
"quote 1.0.26",
"rocket_http",
@@ -3818,7 +3999,7 @@ name = "rocket_cors"
version = "0.6.0-alpha1"
source = "git+https://github.com/lawliet89/rocket_cors?rev=c17e8145baa4790319fdb6a473e465b960f55e7c#c17e8145baa4790319fdb6a473e465b960f55e7c"
dependencies = [
"http",
"http 0.2.7",
"log",
"regex",
"rocket",
@@ -3849,9 +4030,9 @@ dependencies = [
"cookie",
"either",
"futures",
"http",
"hyper",
"indexmap",
"http 0.2.7",
"hyper 0.14.19",
"indexmap 1.9.3",
"log",
"memchr",
"pear",
@@ -3972,11 +4153,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"ring 0.16.20",
"sct",
"webpki",
]
[[package]]
name = "rustls"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
dependencies = [
"log",
"ring 0.17.3",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "0.3.0"
@@ -3986,6 +4181,33 @@ dependencies = [
"base64 0.13.0",
]
[[package]]
name = "rustls-pemfile"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [
"base64 0.22.1",
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
[[package]]
name = "rustls-webpki"
version = "0.102.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
dependencies = [
"ring 0.17.3",
"rustls-pki-types",
"untrusted 0.9.0",
]
[[package]]
name = "rustversion"
version = "1.0.11"
@@ -4015,7 +4237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1847b767a3d62d95cbf3d8a9f0e421cf57a0d8aa4f411d4b16525afb0284d4ed"
dependencies = [
"dyn-clone",
"indexmap",
"indexmap 1.9.3",
"schemars_derive",
"serde",
"serde_json",
@@ -4051,8 +4273,8 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
"ring 0.16.20",
"untrusted 0.7.1",
]
[[package]]
@@ -4278,7 +4500,7 @@ version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [
"indexmap",
"indexmap 1.9.3",
"itoa",
"ryu",
"serde",
@@ -4415,9 +4637,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.11.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
@@ -4497,9 +4719,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "subtle"
version = "2.4.1"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
@@ -4737,11 +4959,22 @@ version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls",
"rustls 0.20.6",
"tokio 1.35.1",
"webpki",
]
[[package]]
name = "tokio-rustls"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [
"rustls 0.22.4",
"rustls-pki-types",
"tokio 1.35.1",
]
[[package]]
name = "tokio-stream"
version = "0.1.8"
@@ -4803,6 +5036,27 @@ dependencies = [
"sha2",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite 0.2.13",
"tokio 1.35.1",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.1"
@@ -4811,11 +5065,10 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
[[package]]
name = "tracing"
version = "0.1.34"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"cfg-if",
"log",
"pin-project-lite 0.2.13",
"tracing-attributes",
@@ -4824,22 +5077,22 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.21"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote 1.0.26",
"syn 1.0.107",
"syn 2.0.15",
]
[[package]]
name = "tracing-core"
version = "0.1.26"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"lazy_static",
"once_cell",
"valuable",
]
@@ -4942,7 +5195,7 @@ dependencies = [
"base64 0.13.0",
"byteorder",
"bytes 1.5.0",
"http",
"http 0.2.7",
"httparse",
"log",
"rand 0.8.5",
@@ -5093,9 +5346,9 @@ checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "universal-hash"
version = "0.4.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402"
dependencies = [
"generic-array 0.14.5",
"subtle",
@@ -5107,6 +5360,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.7.1"
@@ -5381,7 +5640,7 @@ dependencies = [
"chrono",
"ece",
"futures-lite",
"http",
"http 0.2.7",
"isahc",
"jwt-simple",
"log",
@@ -5408,8 +5667,8 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
"ring 0.16.20",
"untrusted 0.7.1",
]
[[package]]
@@ -5421,6 +5680,15 @@ dependencies = [
"webpki",
]
[[package]]
name = "webpki-roots"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"
+2
View File
@@ -21,6 +21,8 @@ Rust 1.70 or higher.
## Development Guide
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
Before getting started, you'll want to install:
- Rust toolchain (rustup recommended)
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.7.9"
version = "0.7.13"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -41,7 +41,7 @@ revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.7.9", path = "../core/permissions" }
revolt-permissions = { version = "0.7.13", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis
+15 -1
View File
@@ -180,7 +180,7 @@ impl State {
.collect();
// Make sure we see our own user correctly.
users.push(user.into_self().await);
users.push(user.into_self(true).await);
// Set subscription state internally.
self.reset_state().await;
@@ -540,6 +540,20 @@ impl State {
}
}
EventV1::Message(message) => {
// Since Message events are fanned out to many clients,
// we must reconstruct the relationship value at this end.
if let Some(user) = &mut message.user {
user.relationship = self
.cache
.users
.get(&self.cache.user_id)
.expect("missing self?")
.relationship_with(&message.author)
.into();
}
}
_ => {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.7.9"
version = "0.7.13"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
+31 -7
View File
@@ -29,6 +29,11 @@ public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQIS
[api.fcm]
api_key = ""
[api.apn]
pkcs8 = ""
key_id = ""
team_id = ""
[api.security]
authifier_shield_key = ""
voso_legacy_token = ""
@@ -46,21 +51,40 @@ webhooks_enabled = false
[features.limits]
[features.limits.default]
outgoing_friend_requests = 10
[features.limits.global]
group_size = 100
bots = 5
message_length = 2000
message_embeds = 5
message_replies = 5
message_attachments = 5
message_reactions = 20
servers = 100
server_emoji = 100
server_roles = 200
server_channels = 200
new_user_days = 3
[features.limits.new_user]
outgoing_friend_requests = 5
bots = 2
message_length = 2000
message_attachments = 5
servers = 100
attachment_size = 20000000
avatar_size = 4000000
background_size = 6000000
icon_size = 2500000
banner_size = 6000000
emoji_size = 500000
[features.limits.default]
outgoing_friend_requests = 10
bots = 5
message_length = 2000
message_attachments = 5
servers = 100
attachment_size = 20000000
avatar_size = 4000000
background_size = 6000000
+26 -7
View File
@@ -6,6 +6,8 @@ use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
pub use sentry::capture_error;
#[cfg(not(debug_assertions))]
use std::env;
@@ -75,6 +77,13 @@ pub struct ApiFcm {
pub api_key: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiApn {
pub pkcs8: String,
pub key_id: String,
pub team_id: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurityCaptcha {
pub hcaptcha_key: String,
@@ -100,25 +109,32 @@ pub struct Api {
pub smtp: ApiSmtp,
pub vapid: ApiVapid,
pub fcm: ApiFcm,
pub apn: ApiApn,
pub security: ApiSecurity,
pub workers: ApiWorkers,
}
#[derive(Deserialize, Debug, Clone)]
pub struct GlobalLimits {
pub group_size: usize,
pub message_embeds: usize,
pub message_replies: usize,
pub message_reactions: usize,
pub server_emoji: usize,
pub server_roles: usize,
pub server_channels: usize,
pub new_user_days: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimits {
pub outgoing_friend_requests: usize,
pub group_size: usize,
pub bots: usize,
pub message_length: usize,
pub message_replies: usize,
pub message_attachments: usize,
pub message_embeds: usize,
pub message_reactions: usize,
pub servers: usize,
pub server_emoji: usize,
pub server_roles: usize,
pub server_channels: usize,
pub attachment_size: usize,
pub avatar_size: usize,
@@ -130,6 +146,9 @@ pub struct FeaturesLimits {
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimitsCollection {
pub global: GlobalLimits,
pub new_user: FeaturesLimits,
pub default: FeaturesLimits,
#[serde(flatten)]
+9 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.7.9"
version = "0.7.13"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.7.9", path = "../config" }
revolt-result = { version = "0.7.9", path = "../result" }
revolt-models = { version = "0.7.9", path = "../models", features = [
revolt-config = { version = "0.7.13", path = "../config" }
revolt-result = { version = "0.7.13", path = "../result" }
revolt-models = { version = "0.7.13", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.7.9", path = "../presence" }
revolt-permissions = { version = "0.7.9", path = "../permissions", features = [
revolt-presence = { version = "0.7.13", path = "../presence" }
revolt-permissions = { version = "0.7.13", path = "../permissions", features = [
"serde",
"bson",
] }
@@ -87,6 +87,9 @@ revolt_rocket_okapi = { version = "0.9.1", optional = true }
# Notifications
fcm = "0.9.2"
web-push = "0.10.0"
revolt_a2 = { version = "0.10.0", default-features = false, features = [
"ring",
] }
# Authifier
authifier = { version = "1.0.8" }
@@ -72,7 +72,12 @@ impl Default for Bot {
#[allow(clippy::disallowed_methods)]
impl Bot {
/// Create a new bot
pub async fn create<D>(db: &Database, username: String, owner: &User, data: D) -> Result<Bot>
pub async fn create<D>(
db: &Database,
username: String,
owner: &User,
data: D,
) -> Result<(Bot, User)>
where
D: Into<Option<PartialBot>>,
{
@@ -80,14 +85,13 @@ impl Bot {
return Err(create_error!(IsBot));
}
let config = config().await;
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
return Err(create_error!(ReachedMaximumBots));
}
let id = Ulid::new().to_string();
User::create(
let user = User::create(
db,
username,
Some(id.to_string()),
@@ -112,7 +116,7 @@ impl Bot {
}
db.insert_bot(&bot).await?;
Ok(bot)
Ok((bot, user))
}
/// Remove a field from this object
@@ -201,9 +201,9 @@ impl Channel {
update_server: bool,
) -> Result<Channel> {
let config = config().await;
if server.channels.len() > config.features.limits.default.server_channels {
if server.channels.len() > config.features.limits.global.server_channels {
return Err(create_error!(TooManyChannels {
max: config.features.limits.default.server_channels,
max: config.features.limits.global.server_channels,
}));
};
@@ -263,9 +263,9 @@ impl Channel {
data.users.insert(owner_id.to_string());
let config = config().await;
if data.users.len() > config.features.limits.default.group_size {
if data.users.len() > config.features.limits.global.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size,
max: config.features.limits.global.group_size,
}));
}
@@ -346,9 +346,9 @@ impl Channel {
}
let config = config().await;
if recipients.len() >= config.features.limits.default.group_size {
if recipients.len() >= config.features.limits.global.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size
max: config.features.limits.global.group_size
}));
}
@@ -377,6 +377,8 @@ impl Channel {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
self,
false,
)
@@ -688,6 +690,8 @@ impl Channel {
username: name,
avatar: None,
},
None,
None,
self,
false,
)
@@ -723,6 +727,8 @@ impl Channel {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
self,
false,
)
@@ -2,10 +2,10 @@ use std::collections::HashSet;
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
@@ -65,6 +65,10 @@ auto_derived_partial!(
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,
/// Bitfield of message flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
},
"PartialMessage"
);
@@ -200,6 +204,7 @@ impl Default for Message {
reactions: Default::default(),
interactions: Default::default(),
masquerade: None,
flags: None,
}
}
}
@@ -207,11 +212,15 @@ impl Default for Message {
#[allow(clippy::disallowed_methods)]
impl Message {
/// Create message from API data
#[allow(clippy::too_many_arguments)]
pub async fn create_from_api(
db: &Database,
channel: Channel,
data: DataMessageSend,
author: MessageAuthor<'_>,
user: Option<v0::User>,
member: Option<v0::Member>,
limits: FeaturesLimits,
mut idempotency: IdempotencyKey,
generate_embeds: bool,
allow_mentions: bool,
@@ -221,7 +230,7 @@ impl Message {
Message::validate_sum(
&data.content,
data.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
limits.message_length,
)?;
idempotency
@@ -237,6 +246,13 @@ impl Message {
return Err(create_error!(EmptyMessage));
}
// Ensure flags are either not set or have permissible values
if let Some(flags) = &data.flags {
if flags != &0 && flags != &1 {
return Err(create_error!(InvalidProperty));
}
}
// Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
@@ -270,6 +286,7 @@ impl Message {
.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
flags: data.flags.map(|v| v as i32),
..Default::default()
};
@@ -288,9 +305,9 @@ impl Message {
// Verify replies are valid.
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > config.features.limits.default.message_replies {
if entries.len() > config.features.limits.global.message_replies {
return Err(create_error!(TooManyReplies {
max: config.features.limits.default.message_replies,
max: config.features.limits.global.message_replies,
}));
}
@@ -320,20 +337,20 @@ impl Message {
if data
.attachments
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
.is_some_and(|v| v.len() > limits.message_attachments)
{
return Err(create_error!(TooManyAttachments {
max: config.features.limits.default.message_attachments,
max: limits.message_attachments,
}));
}
if data
.embeds
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
.is_some_and(|v| v.len() > config.features.limits.global.message_embeds)
{
return Err(create_error!(TooManyEmbeds {
max: config.features.limits.default.message_embeds,
max: config.features.limits.global.message_embeds,
}));
}
@@ -360,7 +377,9 @@ impl Message {
message.nonce = Some(idempotency.into_key());
// Send the message
message.send(db, author, &channel, generate_embeds).await?;
message
.send(db, author, user, member, &channel, generate_embeds)
.await?;
Ok(message)
}
@@ -369,13 +388,15 @@ impl Message {
pub async fn send_without_notifications(
&mut self,
db: &Database,
user: Option<v0::User>,
member: Option<v0::Member>,
is_dm: bool,
generate_embeds: bool,
) -> Result<()> {
db.insert_message(self).await?;
// Fan out events
EventV1::Message(self.clone().into())
EventV1::Message(self.clone().into_model(user, member))
.p(self.channel.to_string())
.await;
@@ -416,29 +437,40 @@ impl Message {
&mut self,
db: &Database,
author: MessageAuthor<'_>,
user: Option<v0::User>,
member: Option<v0::Member>,
channel: &Channel,
generate_embeds: bool,
) -> Result<()> {
self.send_without_notifications(
db,
user,
member,
matches!(channel, Channel::DirectMessage { .. }),
generate_embeds,
)
.await?;
// Push out Web Push notifications
crate::tasks::web_push::queue(
{
match channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => recipients.clone(),
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
_ => vec![],
}
},
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
)
.await;
if !self.has_suppressed_notifications() {
// Push out Web Push notifications
crate::tasks::web_push::queue(
{
match channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => recipients.clone(),
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
_ => vec![],
}
},
PushNotification::from(
self.clone().into_model(None, None),
Some(author),
&channel.id(),
)
.await,
)
.await;
}
Ok(())
}
@@ -470,6 +502,16 @@ impl Message {
}))
}
/// Whether this message has suppressed notifications
pub fn has_suppressed_notifications(&self) -> bool {
if let Some(flags) = self.flags {
flags & MessageFlags::SuppressNotifications as i32
== MessageFlags::SuppressNotifications as i32
} else {
false
}
}
/// Update message data
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
self.apply_options(partial.clone());
@@ -498,7 +540,7 @@ impl Message {
.fetch_messages(query)
.await?
.into_iter()
.map(Into::into)
.map(|msg| msg.into_model(None, None))
.collect();
if let Some(true) = include_users {
@@ -616,7 +658,7 @@ impl Message {
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
// Check how many reactions are already on the message
let config = config().await;
if self.reactions.len() >= config.features.limits.default.message_reactions
if self.reactions.len() >= config.features.limits.global.message_reactions
&& !self.reactions.contains_key(emoji)
{
return Err(create_error!(InvalidOperation));
@@ -781,7 +823,7 @@ impl Interactions {
if let Some(reactions) = &self.reactions {
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
if reactions.len() > config.features.limits.default.message_reactions {
if reactions.len() > config.features.limits.global.message_reactions {
return Err(create_error!(InvalidOperation));
}
@@ -150,7 +150,7 @@ impl Member {
id: user.id.clone(),
}
.into_message(id.to_string())
.send_without_notifications(db, false, false)
.send_without_notifications(db, None, None, false, false)
.await
.ok();
}
@@ -250,7 +250,7 @@ impl Member {
}
.into_message(id.to_string())
// TODO: support notifications here in the future?
.send_without_notifications(db, false, false)
.send_without_notifications(db, None, None, false, false)
.await
.ok();
}
+43 -23
View File
@@ -1,10 +1,10 @@
use std::{collections::HashSet, time::Duration};
use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_config::config;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0;
use revolt_presence::filter_online;
use revolt_result::{create_error, Result};
@@ -61,6 +61,7 @@ auto_derived!(
StatusPresence,
ProfileContent,
ProfileBackground,
DisplayName,
}
/// User's relationship with another user (or themselves)
@@ -197,6 +198,22 @@ impl User {
Ok(user)
}
/// Get limits for this user
pub async fn limits(&self) -> FeaturesLimits {
let config = config().await;
if ulid::Ulid::from_str(&self.id)
.expect("`ulid`")
.datetime()
.elapsed()
.expect("time went backwards")
<= Duration::from_secs(86400u64 * config.features.limits.global.new_user_days as u64)
{
config.features.limits.new_user
} else {
config.features.limits.default
}
}
/// Get the relationship with another user
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
if self.id == user_b {
@@ -235,12 +252,11 @@ impl User {
/// Check if this user can acquire another server
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
let config = config().await;
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
if db.fetch_server_count(&self.id).await? <= self.limits().await.servers {
Ok(())
} else {
Err(create_error!(TooManyServers {
max: config.features.limits.default.servers
max: self.limits().await.servers
}))
}
}
@@ -470,24 +486,6 @@ impl User {
/// Add another user as a friend
pub async fn add_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
let count = self
.relations
.as_ref()
.map(|relations| {
relations
.iter()
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
.count()
})
.unwrap_or_default();
let config = config().await;
if count >= config.features.limits.default.outgoing_friend_requests {
return Err(create_error!(TooManyPendingFriendRequests {
max: config.features.limits.default.outgoing_friend_requests
}));
}
match self.relationship_with(&target.id) {
RelationshipStatus::User => Err(create_error!(NoEffect)),
RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
@@ -495,6 +493,7 @@ impl User {
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
RelationshipStatus::Incoming => {
// Accept incoming friend request
self.apply_relationship(
db,
target,
@@ -504,6 +503,26 @@ impl User {
.await
}
RelationshipStatus::None => {
// Get this user's current count of outgoing friend requests
let count = self
.relations
.as_ref()
.map(|relations| {
relations
.iter()
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
.count()
})
.unwrap_or_default();
// If we're over the limit, don't allow creating more requests
if count >= self.limits().await.outgoing_friend_requests {
return Err(create_error!(TooManyPendingFriendRequests {
max: self.limits().await.outgoing_friend_requests
}));
}
// Send the friend request
self.apply_relationship(
db,
target,
@@ -639,6 +658,7 @@ impl User {
x.background = None;
}
}
FieldsUser::DisplayName => self.display_name = None,
}
}
@@ -58,4 +58,7 @@ pub trait AbstractUsers: Sync + Send {
/// Delete a user by their id
async fn delete_user(&self, id: &str) -> Result<()>;
/// Remove push subscription for a session by session id (TODO: remove)
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()>;
}
@@ -316,6 +316,25 @@ impl AbstractUsers for MongoDb {
async fn delete_user(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
/// Remove push subscription for a session by session id (TODO: remove)
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()> {
self.col::<User>("sessions")
.update_one(
doc! {
"_id": session_id
},
doc! {
"$unset": {
"subscription": 1
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
}
impl IntoDocumentPath for FieldsUser {
@@ -326,6 +345,7 @@ impl IntoDocumentPath for FieldsUser {
FieldsUser::ProfileContent => "profile.content",
FieldsUser::StatusPresence => "status.presence",
FieldsUser::StatusText => "status.text",
FieldsUser::DisplayName => "display_name",
})
}
}
@@ -163,4 +163,9 @@ impl AbstractUsers for ReferenceDb {
Err(create_error!(NotFound))
}
}
/// Remove push subscription for a session by session id (TODO: remove)
async fn remove_push_subscription_by_session_id(&self, _session_id: &str) -> Result<()> {
todo!()
}
}
@@ -0,0 +1,113 @@
use std::io::Cursor;
use base64::{
engine::{self},
Engine as _,
};
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use revolt_a2::{Client, ClientConfig, DefaultNotificationBuilder};
use revolt_a2::{Error, ErrorBody, ErrorReason, NotificationBuilder, Response};
use revolt_config::config;
use revolt_models::v0::PushNotification;
use crate::Database;
/// Task information
#[derive(Debug)]
pub struct ApnTask {
/// Session Id
session_id: String,
/// Device token
device_token: String,
/// Title
title: String,
/// Body
body: String,
/// Thread Id
thread_id: String,
}
impl ApnTask {
pub fn from_notification(
session_id: String,
device_token: String,
notification: &PushNotification,
) -> ApnTask {
ApnTask {
session_id,
device_token,
title: notification.author.to_string(),
body: notification.body.to_string(),
thread_id: notification.tag.to_string(),
}
}
}
static Q: Lazy<Queue<ApnTask>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(task: ApnTask) {
Q.try_push(task).ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let config = config().await;
if config.api.apn.pkcs8.is_empty()
|| config.api.apn.key_id.is_empty()
|| config.api.apn.team_id.is_empty()
{
eprintln!("Missing APN keys.");
return;
}
let pkcs8 = engine::general_purpose::STANDARD
.decode(config.api.apn.pkcs8)
.expect("valid `pcks8`");
let client = Client::token(
&mut Cursor::new(pkcs8),
config.api.apn.key_id,
config.api.apn.team_id,
ClientConfig::default(),
)
.expect("could not create APN client");
loop {
let task = Q.pop().await;
let payload = DefaultNotificationBuilder::new()
.set_title(&task.title)
.set_body(&task.body)
.set_thread_id(&task.thread_id)
.build(&task.device_token, Default::default());
if let Err(err) = client.send(payload).await {
match err {
Error::ResponseError(Response {
error:
Some(ErrorBody {
reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered,
..
}),
..
}) => {
if let Err(err) = db
.remove_push_subscription_by_session_id(&task.session_id)
.await
{
revolt_config::capture_error(&err);
}
}
err => {
revolt_config::capture_error(&err);
}
}
}
}
}
+3
View File
@@ -8,12 +8,15 @@ use std::time::Instant;
const WORKER_COUNT: usize = 5;
pub mod ack;
pub mod apple_notifications;
pub mod last_message_id;
pub mod process_embeds;
pub mod web_push;
/// Spawn background workers
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
task::spawn(apple_notifications::worker(db.clone()));
for _ in 0..WORKER_COUNT {
task::spawn(ack::worker(db.clone()));
task::spawn(last_message_id::worker(db.clone()));
@@ -56,7 +56,7 @@ pub async fn worker(db: Database) {
let embeds = generate(
task.content,
&config.hosts.january,
config.features.limits.default.message_embeds,
config.features.limits.global.message_embeds,
semaphore,
)
.await;
@@ -6,6 +6,7 @@ use base64::{
Engine as _,
};
use deadqueue::limited::Queue;
use fcm::FcmError;
use once_cell::sync::Lazy;
use revolt_config::config;
use revolt_models::v0::PushNotification;
@@ -16,6 +17,8 @@ use web_push::{
WebPushClient, WebPushMessageBuilder,
};
use super::apple_notifications;
/// Task information
#[derive(Debug)]
struct PushTask {
@@ -101,6 +104,15 @@ pub async fn worker(db: Database) {
} else {
info!("No FCM token was specified!");
}
} else if sub.endpoint == "apn" {
apple_notifications::queue(
apple_notifications::ApnTask::from_notification(
session.id,
sub.auth,
&task.payload,
),
)
.await;
} else {
// Use Web Push Standard
let subscription = SubscriptionInfo {
+56 -22
View File
@@ -459,26 +459,29 @@ impl From<Metadata> for crate::Metadata {
}
}
impl From<crate::Message> for Message {
fn from(value: crate::Message) -> Self {
impl crate::Message {
pub fn into_model(self, user: Option<User>, member: Option<Member>) -> Message {
Message {
id: value.id,
nonce: value.nonce,
channel: value.channel,
author: value.author,
webhook: value.webhook,
content: value.content,
system: value.system.map(|system| system.into()),
attachments: value
id: self.id,
nonce: self.nonce,
channel: self.channel,
author: self.author,
user,
member,
webhook: self.webhook,
content: self.content,
system: self.system.map(Into::into),
attachments: self
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions,
interactions: value.interactions.into(),
masquerade: value.masquerade.map(|masq| masq.into()),
edited: self.edited,
embeds: self.embeds,
mentions: self.mentions,
replies: self.replies,
reactions: self.reactions,
interactions: self.interactions.into(),
masquerade: self.masquerade.map(Into::into),
flags: self.flags.map(|flags| flags as u32).unwrap_or_default(),
}
}
}
@@ -490,9 +493,11 @@ impl From<crate::PartialMessage> for PartialMessage {
nonce: value.nonce,
channel: value.channel,
author: value.author,
user: None,
member: None,
webhook: value.webhook,
content: value.content,
system: value.system.map(|system| system.into()),
system: value.system.map(Into::into),
attachments: value
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
@@ -501,8 +506,9 @@ impl From<crate::PartialMessage> for PartialMessage {
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions,
interactions: value.interactions.map(|interactions| interactions.into()),
masquerade: value.masquerade.map(|masq| masq.into()),
interactions: value.interactions.map(Into::into),
masquerade: value.masquerade.map(Into::into),
flags: value.flags.map(|flags| flags as u32),
}
}
}
@@ -1089,7 +1095,33 @@ impl crate::User {
}
}
pub async fn into_self(self) -> User {
/// Convert user object into user model without presence information
pub fn into_known_static<'a>(self, is_online: bool) -> User {
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
online: is_online
&& !matches!(
self.status,
Some(crate::UserStatus {
presence: Some(crate::Presence::Invisible),
..
})
),
status: self.status.map(|status| status.into()),
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship: RelationshipStatus::None, // events client will populate this from cache
id: self.id,
}
}
pub async fn into_self(self, force_online: bool) -> User {
User {
username: self.username,
discriminator: self.discriminator,
@@ -1105,7 +1137,7 @@ impl crate::User {
})
.unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32,
online: revolt_presence::is_online(&self.id).await
online: (force_online || revolt_presence::is_online(&self.id).await)
&& !matches!(
self.status,
Some(crate::UserStatus {
@@ -1182,6 +1214,7 @@ impl From<FieldsUser> for crate::FieldsUser {
FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent,
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
FieldsUser::StatusText => crate::FieldsUser::StatusText,
FieldsUser::DisplayName => crate::FieldsUser::DisplayName,
}
}
}
@@ -1194,6 +1227,7 @@ impl From<crate::FieldsUser> for FieldsUser {
crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent,
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
crate::FieldsUser::StatusText => FieldsUser::StatusText,
crate::FieldsUser::DisplayName => FieldsUser::DisplayName,
}
}
}
@@ -53,6 +53,12 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
if let Some(other_user) = &self.user {
if self.perspective.id == other_user.id {
return RelationshipStatus::User;
} else if let Some(bot) = &other_user.bot {
// For the purposes of permissions checks,
// assume owner is the same as bot
if self.perspective.id == bot.owner {
return RelationshipStatus::User;
}
}
if let Some(relations) = &self.perspective.relations {
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.7.9"
version = "0.7.13"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.7.9", path = "../config" }
revolt-permissions = { version = "0.7.9", path = "../permissions" }
revolt-config = { version = "0.7.13", path = "../config" }
revolt-permissions = { version = "0.7.13", path = "../permissions" }
# Utility
regex = "1"
+7
View File
@@ -162,4 +162,11 @@ auto_derived!(
/// User objects
pub users: Vec<User>,
}
/// Bot with user response
pub struct BotWithUserResponse {
#[serde(flatten)]
pub bot: Bot,
pub user: User,
}
);
+27
View File
@@ -31,6 +31,12 @@ auto_derived_partial!(
pub channel: String,
/// Id of the user or webhook that sent this message
pub author: String,
/// The user that sent this message
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<User>,
/// The member that sent this message
#[serde(skip_serializing_if = "Option::is_none")]
pub member: Option<Member>,
/// The webhook that sent this message
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook: Option<MessageWebhook>,
@@ -64,6 +70,15 @@ auto_derived_partial!(
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,
/// Bitfield of message flags
///
/// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
)]
pub flags: u32,
},
"PartialMessage"
);
@@ -241,6 +256,11 @@ auto_derived!(
pub masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
pub interactions: Option<Interactions>,
/// Bitfield of message flags
///
/// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html
pub flags: Option<u32>,
}
/// Options for querying messages
@@ -328,6 +348,13 @@ auto_derived!(
/// Remove all reactions
pub remove_all: Option<bool>,
}
/// Message flag bitfield
#[repr(u32)]
pub enum MessageFlags {
/// Message will not send push / desktop notifications
SuppressNotifications = 1,
}
);
/// Message Author Abstraction
+5
View File
@@ -42,6 +42,8 @@ auto_derived_partial!(
pub relations: Vec<Relationship>,
/// Bitfield of user badges
///
/// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.UserBadges.html
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
@@ -52,6 +54,8 @@ auto_derived_partial!(
pub status: Option<UserStatus>,
/// Enum of user flags
///
/// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.UserFlags.html
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
@@ -83,6 +87,7 @@ auto_derived!(
StatusPresence,
ProfileContent,
ProfileBackground,
DisplayName,
}
/// User's relationship with another user (or themselves)
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.7.9"
version = "0.7.13"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Core
revolt-result = { version = "0.7.9", path = "../result" }
revolt-result = { version = "0.7.13", path = "../result" }
# Utility
auto_ops = "0.3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.7.9"
version = "0.7.13"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.7.9"
version = "0.7.13"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.7.9"
version = "0.7.13"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -77,6 +77,7 @@ revolt-models = { path = "../core/models", features = [
"validator",
"rocket",
] }
revolt-presence = { path = "../core/presence" }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
+6 -3
View File
@@ -14,7 +14,7 @@ pub async fn create_bot(
db: &State<Database>,
user: User,
info: Json<v0::DataCreateBot>,
) -> Result<Json<v0::Bot>> {
) -> Result<Json<v0::BotWithUserResponse>> {
let info = info.into_inner();
info.validate().map_err(|error| {
create_error!(FailedValidation {
@@ -22,8 +22,11 @@ pub async fn create_bot(
})
})?;
let bot = Bot::create(db, info.name, &user, None).await?;
Ok(Json(bot.into()))
let (bot, user) = Bot::create(db, info.name, &user, None).await?;
Ok(Json(v0::BotWithUserResponse {
bot: bot.into(),
user: user.into_self(false).await,
}))
}
#[cfg(test)]
+1 -1
View File
@@ -32,7 +32,7 @@ mod test {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
.await
.expect("`Bot`");
+11 -5
View File
@@ -16,7 +16,7 @@ pub async fn edit_bot(
user: User,
target: Reference,
data: Json<DataEditBot>,
) -> Result<Json<v0::Bot>> {
) -> Result<Json<v0::BotWithUserResponse>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
@@ -29,8 +29,8 @@ pub async fn edit_bot(
return Err(create_error!(NotFound));
}
let mut user = db.fetch_user(&bot.id).await?;
if let Some(name) = data.name {
let mut user = db.fetch_user(&bot.id).await?;
user.update_username(db, name).await?;
}
@@ -39,7 +39,10 @@ pub async fn edit_bot(
&& data.interactions_url.is_none()
&& data.remove.is_none()
{
return Ok(Json(bot.into()));
return Ok(Json(v0::BotWithUserResponse {
bot: bot.into(),
user: user.into_self(false).await,
}));
}
let DataEditBot {
@@ -68,7 +71,10 @@ pub async fn edit_bot(
)
.await?;
Ok(Json(bot.into()))
Ok(Json(v0::BotWithUserResponse {
bot: bot.into(),
user: user.into_self(false).await,
}))
}
#[cfg(test)]
@@ -83,7 +89,7 @@ mod test {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
.await
.expect("`Bot`");
+1 -1
View File
@@ -40,7 +40,7 @@ mod test {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
.await
.expect("`Bot`");
+2 -2
View File
@@ -24,7 +24,7 @@ pub async fn fetch_owned_bots(db: &State<Database>, user: User) -> Result<Json<O
users.sort_by(|a, b| a.id.cmp(&b.id));
Ok(Json(OwnedBotsResponse {
users: join_all(users.into_iter().map(|user| user.into_self())).await,
users: join_all(users.into_iter().map(|user| user.into_self(false))).await,
bots: bots.into_iter().map(|bot| bot.into()).collect(),
}))
}
@@ -41,7 +41,7 @@ mod test {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
.await
.expect("`Bot`");
+1 -1
View File
@@ -35,7 +35,7 @@ mod test {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let mut bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
let (mut bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
.await
.expect("`Bot`");
+2 -2
View File
@@ -74,7 +74,7 @@ mod test {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
.await
.expect("`Bot`");
@@ -121,7 +121,7 @@ mod test {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
.await
.expect("`Bot`");
@@ -73,7 +73,7 @@ pub async fn edit(
return Err(create_error!(InvalidOperation));
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false)
.send(db, user.as_author_for_system(), None, None, &channel, false)
.await
.ok();
}
@@ -151,7 +151,7 @@ pub async fn edit(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false)
.send(db, user.as_author_for_system(), None, None, &channel, false)
.await
.ok();
}
@@ -161,7 +161,7 @@ pub async fn edit(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false)
.send(db, user.as_author_for_system(), None, None, &channel, false)
.await
.ok();
}
@@ -171,7 +171,7 @@ pub async fn edit(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false)
.send(db, user.as_author_for_system(), None, None, &channel, false)
.await
.ok();
}
@@ -30,11 +30,10 @@ pub async fn edit(
})
})?;
let config = config().await;
Message::validate_sum(
&edit.content,
edit.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
user.limits().await.message_length,
)?;
// Ensure we have permissions to send a message
@@ -98,5 +97,5 @@ pub async fn edit(
}
}
Ok(Json(message.into()))
Ok(Json(message.into_model(None, None)))
}
@@ -29,5 +29,5 @@ pub async fn fetch(
return Err(create_error!(NotFound));
}
Ok(Json(message.into()))
Ok(Json(message.into_model(None, None)))
}
@@ -5,6 +5,7 @@ use revolt_database::{
};
use revolt_database::{Interactions, Message};
use revolt_models::v0;
use revolt_permissions::PermissionQuery;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
@@ -75,17 +76,34 @@ pub async fn message_send(
// Create the message
let author: v0::User = user.clone().into(db, Some(&user)).await;
// Make sure we have server member (edge case if server owner)
query.are_we_a_member().await;
// Create model user / members
let model_user = user
.clone()
.into_known_static(revolt_presence::is_online(&user.id).await);
let model_member: Option<v0::Member> = query
.member_ref()
.as_ref()
.map(|member| member.clone().into_owned().into());
Ok(Json(
Message::create_from_api(
db,
channel,
data,
v0::MessageAuthor::User(&author),
Some(model_user.clone()),
model_member.clone(),
user.limits().await,
idempotency,
permissions.has_channel_permission(ChannelPermission::SendEmbeds),
allow_mentions,
)
.await?
.into(),
.into_model(Some(model_user), model_member),
))
}
@@ -45,9 +45,9 @@ pub async fn create_emoji(
// Check that we haven't hit the emoji limit
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
if emojis.len() >= config.features.limits.default.server_emoji {
if emojis.len() >= config.features.limits.global.server_emoji {
return Err(create_error!(TooManyEmoji {
max: config.features.limits.default.server_emoji,
max: config.features.limits.global.server_emoji,
}));
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ pub async fn complete(
Ok(Json(
User::create(db, data.username, session.user_id, None)
.await?
.into_self()
.into_self(false)
.await,
))
}
+1 -1
View File
@@ -36,7 +36,7 @@ pub async fn list(
)
.await?
.into_iter()
.map(|u| u.into_self()),
.map(|u| u.into_self(false)),
)
.await;
@@ -34,9 +34,9 @@ pub async fn create(
.throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?;
let config = config().await;
if server.roles.len() >= config.features.limits.default.server_roles {
if server.roles.len() >= config.features.limits.global.server_roles {
return Err(create_error!(TooManyRoles {
max: config.features.limits.default.server_roles,
max: config.features.limits.global.server_roles,
}));
};
+9 -6
View File
@@ -24,23 +24,26 @@ pub async fn edit(
})
})?;
// Filter out invalid edit fields
if !user.privileged && (data.badges.is_some() || data.flags.is_some()) {
return Err(create_error!(NotPrivileged));
}
// If we want to edit a different user than self, ensure we have
// permissions and subsequently replace the user in question
if target.id != "@me" && target.id != user.id {
let target_user = target.as_user(db).await?;
let is_bot_owner = target_user
.bot
.as_ref()
.map(|bot| bot.owner == user.id)
.unwrap_or_default();
if !is_bot_owner && !user.privileged {
return Err(create_error!(NotPrivileged));
}
}
// Otherwise, filter out invalid edit fields
if !user.privileged && (data.badges.is_some() || data.flags.is_some()) {
return Err(create_error!(NotPrivileged));
user = target_user;
}
// Exit out early if nothing is changed
@@ -52,7 +55,7 @@ pub async fn edit(
&& data.flags.is_none()
&& data.remove.is_none()
{
return Ok(Json(user.into_self().await));
return Ok(Json(user.into_self(false).await));
}
// 1. Remove fields from object
@@ -126,5 +129,5 @@ pub async fn edit(
)
.await?;
Ok(Json(user.into_self().await))
Ok(Json(user.into_self(false).await))
}
+1 -1
View File
@@ -9,5 +9,5 @@ use rocket::serde::json::Json;
#[openapi(tag = "User Information")]
#[get("/@me")]
pub async fn fetch(user: User) -> Result<Json<v0::User>> {
Ok(Json(user.into_self().await))
Ok(Json(user.into_self(false).await))
}
+1 -1
View File
@@ -15,7 +15,7 @@ use rocket::{serde::json::Json, State};
#[get("/<target>")]
pub async fn fetch(db: &State<Database>, user: User, target: Reference) -> Result<Json<v0::User>> {
if user.id == target.id {
return Ok(Json(user.into_self().await));
return Ok(Json(user.into_self(false).await));
}
let target = target.as_user(db).await?;
@@ -1,3 +1,4 @@
use revolt_config::config;
use revolt_database::{
util::{idempotency::IdempotencyKey, reference::Reference},
Database, Message,
@@ -58,11 +59,14 @@ pub async fn webhook_execute(
channel,
data,
v0::MessageAuthor::Webhook(&webhook.into()),
None,
None,
config().await.features.limits.default,
idempotency,
true,
true,
)
.await?
.into(),
.into_model(None, None),
))
}
@@ -1072,6 +1072,13 @@ pub async fn webhook_execute_github(
#[allow(clippy::disallowed_methods)]
message.attach_sendable_embed(db, sendable_embed).await?;
message
.send(db, MessageAuthor::Webhook(&webhook.into()), &channel, false)
.send(
db,
MessageAuthor::Webhook(&webhook.into()),
None,
None,
&channel,
false,
)
.await
}