Compare commits
79
Commits
20240604-1
...
20240805-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f513a9cb5e | ||
|
|
535f01604b | ||
|
|
f8ec6ba5ff | ||
|
|
b8fdc07508 | ||
|
|
5ce7530206 | ||
|
|
b36bac3225 | ||
|
|
beef06ebe3 | ||
|
|
4fc46f765b | ||
|
|
b45ae2cd4d | ||
|
|
0a8089787b | ||
|
|
475fa6f2e6 | ||
|
|
96bf73cb57 | ||
|
|
87a9fb79f2 | ||
|
|
c50435d499 | ||
|
|
d2f4823ceb | ||
|
|
39230c559a | ||
|
|
32d1d5df2e | ||
|
|
7547fbe245 | ||
|
|
9f13cb6f47 | ||
|
|
d94571fbf9 | ||
|
|
389ecc0e5c | ||
|
|
412f4a99d7 | ||
|
|
caa0795d07 | ||
|
|
5c82343bb3 | ||
|
|
e17af1c064 | ||
|
|
a9e309395e | ||
|
|
8468ce19cb | ||
|
|
27f15f7b02 | ||
|
|
78b9fa168b | ||
|
|
e290d168ac | ||
|
|
78cd89ec32 | ||
|
|
f9d9059e73 | ||
|
|
0954d71f51 | ||
|
|
9ea2bd9f2f | ||
|
|
d6bcb844db | ||
|
|
ce20e689cc | ||
|
|
d9cfc9d0e1 | ||
|
|
64a07d09f4 | ||
|
|
00e881799f | ||
|
|
fa598dd6f8 | ||
|
|
ac4e2cb10b | ||
|
|
ec578ab0ef | ||
|
|
ea6ba59841 | ||
|
|
f903f716e3 | ||
|
|
c5f4b94aa5 | ||
|
|
1d5dae4751 | ||
|
|
ac20b6bc99 | ||
|
|
6ec8007e4e | ||
|
|
e5eea267cf | ||
|
|
de5add09d0 | ||
|
|
1ec8f46c1d | ||
|
|
80666848cc | ||
|
|
e8e9613169 | ||
|
|
eda36436a8 | ||
|
|
93e05e9f18 | ||
|
|
090d8cba71 | ||
|
|
f16e72e329 | ||
|
|
8248a4a5b8 | ||
|
|
cf0b4274c8 | ||
|
|
36ecb48c7b | ||
|
|
ba411fef43 | ||
|
|
4055363cff | ||
|
|
d179d1e695 | ||
|
|
fc9ceb7540 | ||
|
|
a31a3e2e65 | ||
|
|
b12e728514 | ||
|
|
962c7d62c7 | ||
|
|
d677716f93 | ||
|
|
8099310f89 | ||
|
|
5c40f66010 | ||
|
|
8e7dd21bce | ||
|
|
4868205df4 | ||
|
|
96fb0eecca | ||
|
|
2cb20618da | ||
|
|
92e948aabc | ||
|
|
cb4435d700 | ||
|
|
4c6e78e1a5 | ||
|
|
da9a91e05f | ||
|
|
506968634e |
@@ -2,8 +2,8 @@ name: Docker Test & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
# branches:
|
||||
# - "main"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
|
||||
Generated
+401
-86
@@ -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",
|
||||
@@ -138,11 +138,23 @@ version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener",
|
||||
"concurrent-queue 1.2.2",
|
||||
"event-listener 2.5.2",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue 2.5.0",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite 0.2.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.4.1"
|
||||
@@ -150,7 +162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"concurrent-queue 1.2.2",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
@@ -163,7 +175,7 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
@@ -180,7 +192,7 @@ version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"concurrent-queue 1.2.2",
|
||||
"futures-lite",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -199,7 +211,7 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener 2.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -211,7 +223,7 @@ dependencies = [
|
||||
"async-io",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"event-listener 2.5.2",
|
||||
"futures-lite",
|
||||
"libc",
|
||||
"once_cell",
|
||||
@@ -237,7 +249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
|
||||
dependencies = [
|
||||
"async-attributes",
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
@@ -445,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"
|
||||
@@ -531,7 +549,7 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"async-task",
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
@@ -549,7 +567,7 @@ dependencies = [
|
||||
"base64 0.13.0",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"lazy_static",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
@@ -729,6 +747,15 @@ dependencies = [
|
||||
"cache-padded",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.13.3"
|
||||
@@ -833,13 +860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.8"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"lazy_static",
|
||||
]
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
@@ -881,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",
|
||||
]
|
||||
@@ -1280,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"
|
||||
@@ -1295,6 +1324,27 @@ version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
|
||||
dependencies = [
|
||||
"concurrent-queue 2.5.0",
|
||||
"parking",
|
||||
"pin-project-lite 0.2.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
|
||||
dependencies = [
|
||||
"event-listener 5.3.1",
|
||||
"pin-project-lite 0.2.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
version = "0.1.2"
|
||||
@@ -1587,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]]
|
||||
@@ -1677,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",
|
||||
@@ -1818,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"
|
||||
@@ -1825,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",
|
||||
]
|
||||
|
||||
@@ -1860,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",
|
||||
@@ -1874,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"
|
||||
@@ -1881,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"
|
||||
@@ -1937,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"
|
||||
@@ -1976,15 +2145,15 @@ version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"castaway",
|
||||
"crossbeam-utils",
|
||||
"curl",
|
||||
"curl-sys",
|
||||
"encoding_rs",
|
||||
"event-listener",
|
||||
"event-listener 2.5.2",
|
||||
"futures-lite",
|
||||
"http",
|
||||
"http 0.2.7",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
@@ -2266,6 +2435,12 @@ dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru_time_cache"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
@@ -2417,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",
|
||||
@@ -2429,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]]
|
||||
@@ -2447,7 +2622,7 @@ dependencies = [
|
||||
"bytes 1.5.0",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 0.2.7",
|
||||
"httparse",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -2812,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"
|
||||
@@ -3371,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",
|
||||
@@ -3408,15 +3593,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-std",
|
||||
"async-tungstenite",
|
||||
"authifier",
|
||||
"bincode",
|
||||
"fred",
|
||||
"futures",
|
||||
"log",
|
||||
"lru 0.7.6",
|
||||
"lru_time_cache",
|
||||
"once_cell",
|
||||
"querystring",
|
||||
"redis-kiss",
|
||||
@@ -3435,7 +3623,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-config"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"cached",
|
||||
@@ -3451,7 +3639,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-database"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"async-recursion",
|
||||
@@ -3464,7 +3652,7 @@ dependencies = [
|
||||
"decancer",
|
||||
"fcm",
|
||||
"futures",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"isahc",
|
||||
"iso8601-timestamp 0.2.11",
|
||||
"linkify 0.8.1",
|
||||
@@ -3481,6 +3669,7 @@ dependencies = [
|
||||
"revolt-permissions",
|
||||
"revolt-presence",
|
||||
"revolt-result",
|
||||
"revolt_a2",
|
||||
"revolt_okapi",
|
||||
"revolt_optional_struct",
|
||||
"revolt_rocket_okapi",
|
||||
@@ -3497,9 +3686,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-delta"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"async-std",
|
||||
"authifier",
|
||||
"bitfield",
|
||||
@@ -3525,6 +3714,7 @@ dependencies = [
|
||||
"revolt-database",
|
||||
"revolt-models",
|
||||
"revolt-permissions",
|
||||
"revolt-presence",
|
||||
"revolt-result",
|
||||
"revolt_rocket_okapi",
|
||||
"rocket",
|
||||
@@ -3543,9 +3733,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-models"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"iso8601-timestamp 0.2.11",
|
||||
"once_cell",
|
||||
"regex",
|
||||
@@ -3560,7 +3750,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-permissions"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -3575,7 +3765,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-presence"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"log",
|
||||
@@ -3586,7 +3776,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-result"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
dependencies = [
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi",
|
||||
@@ -3596,6 +3786,30 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt_a2"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edbe1f79cb41271d3cd8f932d75dddeba963c19dc93d1ee6cbe0391b495ab2f5"
|
||||
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"
|
||||
@@ -3668,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"
|
||||
@@ -3710,7 +3938,7 @@ dependencies = [
|
||||
"either",
|
||||
"figment",
|
||||
"futures",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"log",
|
||||
"memchr",
|
||||
"multer",
|
||||
@@ -3758,7 +3986,7 @@ checksum = "d6aeb6bb9c61e9cd2c00d70ea267bf36f76a4cc615e5908b349c2f9d93999b47"
|
||||
dependencies = [
|
||||
"devise",
|
||||
"glob",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"proc-macro2",
|
||||
"quote 1.0.26",
|
||||
"rocket_http",
|
||||
@@ -3771,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",
|
||||
@@ -3802,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",
|
||||
@@ -3925,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"
|
||||
@@ -3939,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"
|
||||
@@ -3968,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",
|
||||
@@ -4004,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]]
|
||||
@@ -4231,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",
|
||||
@@ -4361,16 +4630,16 @@ version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -4450,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"
|
||||
@@ -4690,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"
|
||||
@@ -4756,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"
|
||||
@@ -4764,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",
|
||||
@@ -4777,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",
|
||||
]
|
||||
|
||||
@@ -4895,7 +5195,7 @@ dependencies = [
|
||||
"base64 0.13.0",
|
||||
"byteorder",
|
||||
"bytes 1.5.0",
|
||||
"http",
|
||||
"http 0.2.7",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
@@ -5046,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",
|
||||
@@ -5060,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"
|
||||
@@ -5334,7 +5640,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"ece",
|
||||
"futures-lite",
|
||||
"http",
|
||||
"http 0.2.7",
|
||||
"isahc",
|
||||
"jwt-simple",
|
||||
"log",
|
||||
@@ -5361,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]]
|
||||
@@ -5374,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"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
With the exception of crates that specify their own LICENSE file,
|
||||
the following license applies to the source code of this project.
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
|
||||
@@ -17,10 +17,12 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
||||
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
Rust 1.70 or higher.
|
||||
Rust 1.76 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)
|
||||
@@ -48,7 +50,29 @@ cp .env.example .env
|
||||
# (optionally) copy the default configuration file
|
||||
cp crates/core/config/Revolt.toml Revolt.toml
|
||||
# configure as necessary...
|
||||
```
|
||||
|
||||
You may want to copy the following configuration:
|
||||
|
||||
```toml
|
||||
# Revolt.toml
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost"
|
||||
|
||||
[hosts]
|
||||
app = "http://local.revolt.chat"
|
||||
api = "http://local.revolt.chat:8000"
|
||||
events = "ws://local.revolt.chat:9000"
|
||||
autumn = "http://local.revolt.chat:3000"
|
||||
january = "http://local.revolt.chat:7000"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
```
|
||||
|
||||
Then continue:
|
||||
|
||||
```bash
|
||||
# start other necessary services
|
||||
docker compose up -d
|
||||
|
||||
@@ -105,6 +129,23 @@ Tag and push a new release by running:
|
||||
just release
|
||||
```
|
||||
|
||||
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/revoltchat/backend/releases/new) to create a changelog.
|
||||
|
||||
## Testing
|
||||
|
||||
First, start the required services:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.db.yml up -d
|
||||
```
|
||||
|
||||
Now run tests for whichever database:
|
||||
|
||||
```sh
|
||||
TEST_DB=REFERENCE cargo nextest run
|
||||
TEST_DB=MONGODB cargo nextest run
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -14,6 +14,8 @@ lru = "0.7.6"
|
||||
ulid = "0.5.0"
|
||||
once_cell = "1.9.0"
|
||||
redis-kiss = "0.1.4"
|
||||
lru_time_cache = "0.11.11"
|
||||
async-channel = "2.3.1"
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
@@ -34,11 +36,12 @@ async-std = { version = "1.8.0", features = [
|
||||
] }
|
||||
|
||||
# core
|
||||
authifier = { version = "1.0.8" }
|
||||
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.2", path = "../core/permissions" }
|
||||
revolt-permissions = { version = "0.7.15", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_database::events::client::ReadyPayloadFields;
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -83,6 +84,17 @@ impl ProtocolConfiguration {
|
||||
pub fn get_protocol_format(&self) -> &ProtocolFormat {
|
||||
&self.format
|
||||
}
|
||||
|
||||
/// Get ready payload fields
|
||||
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
|
||||
vec![
|
||||
ReadyPayloadFields::Users,
|
||||
ReadyPayloadFields::Servers,
|
||||
ReadyPayloadFields::Channels,
|
||||
ReadyPayloadFields::Members,
|
||||
ReadyPayloadFields::Emoji,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Object holding one side of a channel for receiving the parsed information
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_database::{
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
|
||||
MemberCompositeKey, Presence, RelationshipStatus,
|
||||
events::client::{EventV1, ReadyPayloadFields},
|
||||
util::permissions::DatabasePermissionQuery,
|
||||
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -21,11 +22,11 @@ impl Cache {
|
||||
let server = self.servers.get(server);
|
||||
let mut query =
|
||||
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
|
||||
.channel(&channel);
|
||||
.channel(channel);
|
||||
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
||||
|
||||
if let Some(member) = member {
|
||||
query = query.member(&member);
|
||||
query = query.member(member);
|
||||
}
|
||||
|
||||
if let Some(server) = server {
|
||||
@@ -90,8 +91,13 @@ impl Cache {
|
||||
/// State Manager
|
||||
impl State {
|
||||
/// Generate a Ready packet for the current user
|
||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
||||
pub async fn generate_ready_payload(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
fields: Vec<ReadyPayloadFields>,
|
||||
) -> Result<EventV1> {
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
@@ -149,14 +155,36 @@ impl State {
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = db
|
||||
.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
|
||||
Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch user settings
|
||||
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
|
||||
.iter()
|
||||
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
|
||||
{
|
||||
Some(db.fetch_user_settings(&user.id, &keys).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch channel unreads
|
||||
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
|
||||
Some(db.fetch_unreads(&user.id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Copy data into local state cache.
|
||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
@@ -179,30 +207,52 @@ 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();
|
||||
self.insert_subscription(self.private_topic.clone());
|
||||
self.reset_state().await;
|
||||
self.insert_subscription(self.private_topic.clone()).await;
|
||||
|
||||
for user in &users {
|
||||
self.insert_subscription(user.id.clone());
|
||||
self.insert_subscription(user.id.clone()).await;
|
||||
}
|
||||
|
||||
for server in &servers {
|
||||
self.insert_subscription(server.id.clone());
|
||||
self.insert_subscription(server.id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", server.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
for channel in &channels {
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
}
|
||||
|
||||
Ok(EventV1::Ready {
|
||||
users,
|
||||
servers: servers.into_iter().map(Into::into).collect(),
|
||||
channels: channels.into_iter().map(Into::into).collect(),
|
||||
members: members.into_iter().map(Into::into).collect(),
|
||||
emojis: emojis.into_iter().map(Into::into).collect(),
|
||||
users: if fields.contains(&ReadyPayloadFields::Users) {
|
||||
Some(users)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
servers: if fields.contains(&ReadyPayloadFields::Servers) {
|
||||
Some(servers.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
channels: if fields.contains(&ReadyPayloadFields::Channels) {
|
||||
Some(channels.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
members: if fields.contains(&ReadyPayloadFields::Members) {
|
||||
Some(members.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
|
||||
user_settings,
|
||||
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -236,11 +286,11 @@ impl State {
|
||||
let mut bulk_events = vec![];
|
||||
|
||||
for id in added_channels {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
for id in removed_channels {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
self.cache.channels.remove(&id);
|
||||
|
||||
bulk_events.push(EventV1::ChannelDelete { id });
|
||||
@@ -263,7 +313,7 @@ impl State {
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
bulk_events.push(EventV1::ChannelCreate(channel.into()));
|
||||
}
|
||||
}
|
||||
@@ -336,7 +386,7 @@ impl State {
|
||||
match event {
|
||||
EventV1::ChannelCreate(channel) => {
|
||||
let id = channel.id().to_string();
|
||||
self.insert_subscription(id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
self.cache.channels.insert(id, channel.clone().into());
|
||||
}
|
||||
EventV1::ChannelUpdate {
|
||||
@@ -376,17 +426,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
EventV1::ChannelDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
self.cache.channels.remove(id);
|
||||
}
|
||||
EventV1::ChannelGroupJoin { user, .. } => {
|
||||
self.insert_subscription(user.clone());
|
||||
self.insert_subscription(user.clone()).await;
|
||||
}
|
||||
EventV1::ChannelGroupLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
} else if !self.cache.can_subscribe_to_user(user) {
|
||||
self.remove_subscription(user);
|
||||
self.remove_subscription(user).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +446,12 @@ impl State {
|
||||
channels,
|
||||
emojis: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", id)).await;
|
||||
}
|
||||
|
||||
self.cache.servers.insert(id.clone(), server.clone().into());
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
@@ -433,13 +488,13 @@ impl State {
|
||||
EventV1::ServerMemberJoin { .. } => {
|
||||
// We will always receive ServerCreate when joining a new server.
|
||||
}
|
||||
EventV1::ServerMemberLeave { id, user } => {
|
||||
EventV1::ServerMemberLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel);
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
@@ -447,11 +502,11 @@ impl State {
|
||||
}
|
||||
}
|
||||
EventV1::ServerDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel);
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
@@ -524,9 +579,23 @@ impl State {
|
||||
self.cache.users.insert(id.clone(), user.clone().into());
|
||||
|
||||
if self.cache.can_subscribe_to_user(id) {
|
||||
self.insert_subscription(id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
} else {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,11 +609,11 @@ impl State {
|
||||
|
||||
// Sub / unsub accordingly.
|
||||
if let Some(id) = queue_add {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
if let Some(id) = queue_remove {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
}
|
||||
|
||||
true
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use lru::LruCache;
|
||||
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
@@ -30,6 +36,7 @@ pub enum SubscriptionStateChange {
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub user_id: String,
|
||||
pub is_bot: bool,
|
||||
|
||||
pub users: HashMap<String, User>,
|
||||
pub channels: HashMap<String, Channel>,
|
||||
@@ -43,6 +50,7 @@ impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Cache {
|
||||
user_id: Default::default(),
|
||||
is_bot: false,
|
||||
|
||||
users: Default::default(),
|
||||
channels: Default::default(),
|
||||
@@ -58,14 +66,17 @@ impl Default for Cache {
|
||||
pub struct State {
|
||||
pub cache: Cache,
|
||||
|
||||
pub session_id: String,
|
||||
pub private_topic: String,
|
||||
subscribed: HashSet<String>,
|
||||
state: SubscriptionStateChange,
|
||||
pub state: SubscriptionStateChange,
|
||||
|
||||
pub subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
pub active_servers: Arc<Mutex<LruTimeCache<String, ()>>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Create state from User
|
||||
pub fn from(user: User) -> State {
|
||||
pub fn from(user: User, session_id: String) -> State {
|
||||
let mut subscribed = HashSet::new();
|
||||
let private_topic = format!("{}!", user.id);
|
||||
subscribed.insert(private_topic.clone());
|
||||
@@ -80,22 +91,61 @@ impl State {
|
||||
|
||||
State {
|
||||
cache,
|
||||
subscribed,
|
||||
subscribed: Arc::new(RwLock::new(subscribed)),
|
||||
active_servers: Arc::new(Mutex::new(LruTimeCache::with_expiry_duration_and_capacity(
|
||||
Duration::from_secs(900),
|
||||
5,
|
||||
))),
|
||||
session_id,
|
||||
private_topic,
|
||||
state: SubscriptionStateChange::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply currently queued state
|
||||
pub fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
pub async fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
// Check if we need to change subscriptions to member event topics
|
||||
if !self.cache.is_bot {
|
||||
enum Server {
|
||||
Subscribe(String),
|
||||
Unsubscribe(String),
|
||||
}
|
||||
|
||||
let active_server_changes: Vec<Server> = {
|
||||
let mut active_servers = self.active_servers.lock().await;
|
||||
active_servers
|
||||
.notify_iter()
|
||||
.map(|e| match e {
|
||||
TimedEntry::Valid(k, _) => Server::Subscribe(format!("{}u", k)),
|
||||
TimedEntry::Expired(k, _) => Server::Unsubscribe(format!("{}u", k)),
|
||||
})
|
||||
.collect()
|
||||
// It is bad practice to open more than one Mutex at once and could
|
||||
// lead to a deadlock, so instead we choose to collect the changes.
|
||||
};
|
||||
|
||||
for entry in active_server_changes {
|
||||
match entry {
|
||||
Server::Subscribe(k) => {
|
||||
self.insert_subscription(k).await;
|
||||
}
|
||||
Server::Unsubscribe(k) => {
|
||||
self.remove_subscription(&k).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush changes to subscriptions
|
||||
let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None);
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if let SubscriptionStateChange::Change { add, remove } = &state {
|
||||
for id in add {
|
||||
self.subscribed.insert(id.clone());
|
||||
subscribed.insert(id.clone());
|
||||
}
|
||||
|
||||
for id in remove {
|
||||
self.subscribed.remove(id);
|
||||
subscribed.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,20 +157,16 @@ impl State {
|
||||
self.cache.users.get(&self.cache.user_id).unwrap().clone()
|
||||
}
|
||||
|
||||
/// Iterate through all subscriptions
|
||||
pub fn iter_subscriptions(&self) -> std::collections::hash_set::Iter<'_, std::string::String> {
|
||||
self.subscribed.iter()
|
||||
}
|
||||
|
||||
/// Reset the current state
|
||||
pub fn reset_state(&mut self) {
|
||||
pub async fn reset_state(&mut self) {
|
||||
self.state = SubscriptionStateChange::Reset;
|
||||
self.subscribed.clear();
|
||||
self.subscribed.write().await.clear();
|
||||
}
|
||||
|
||||
/// Add a new subscription
|
||||
pub fn insert_subscription(&mut self, subscription: String) {
|
||||
if self.subscribed.contains(&subscription) {
|
||||
pub async fn insert_subscription(&mut self, subscription: String) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if subscribed.contains(&subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,12 +183,13 @@ impl State {
|
||||
SubscriptionStateChange::Reset => {}
|
||||
}
|
||||
|
||||
self.subscribed.insert(subscription);
|
||||
subscribed.insert(subscription);
|
||||
}
|
||||
|
||||
/// Remove existing subscription
|
||||
pub fn remove_subscription(&mut self, subscription: &str) {
|
||||
if !self.subscribed.contains(&subscription.to_string()) {
|
||||
pub async fn remove_subscription(&mut self, subscription: &str) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if !subscribed.contains(&subscription.to_string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,6 +206,6 @@ impl State {
|
||||
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
|
||||
}
|
||||
|
||||
self.subscribed.remove(subscription);
|
||||
subscribed.remove(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
+364
-116
@@ -1,13 +1,15 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::{RedisError, RedisErrorKind},
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
pin_mut, select,
|
||||
join, pin_mut, select,
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
@@ -18,7 +20,13 @@ use revolt_database::{
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
|
||||
use async_std::{net::TcpStream, sync::Mutex};
|
||||
use async_std::{
|
||||
net::TcpStream,
|
||||
sync::{Mutex, RwLock},
|
||||
task::spawn,
|
||||
};
|
||||
use revolt_result::create_error;
|
||||
use sentry::Level;
|
||||
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
use crate::events::state::{State, SubscriptionStateChange};
|
||||
@@ -42,10 +50,12 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
@@ -57,10 +67,8 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
while let Ok(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
while let Ok(Some(message)) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
@@ -69,35 +77,49 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
write
|
||||
.send(config.encode(&create_error!(InvalidSession)))
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
let user = match User::from_token(db, token, UserHint::Any).await {
|
||||
|
||||
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
write.send(config.encode(&err)).await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user);
|
||||
let mut state = State::from(user, session_id);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
if write
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if let Err(err) = write.send(config.encode(&EventV1::Authenticated)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
let Ok(ready_payload) = state.generate_ready_payload(db).await else {
|
||||
return;
|
||||
let ready_payload = match state
|
||||
.generate_ready_payload(db, config.get_ready_payload_fields())
|
||||
.await
|
||||
{
|
||||
Ok(ready_payload) => ready_payload,
|
||||
Err(err) => {
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if write.send(config.encode(&ready_payload)).await.is_err() {
|
||||
|
||||
if let Err(err) = write.send(config.encode(&ready_payload)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,20 +132,44 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
}
|
||||
|
||||
{
|
||||
// Setup channels and mutexes
|
||||
let write = Mutex::new(write);
|
||||
let subscribed = state.subscribed.clone();
|
||||
let active_servers = state.active_servers.clone();
|
||||
let (topic_signal_s, topic_signal_r) = async_channel::unbounded();
|
||||
|
||||
// TODO: this needs to be rewritten
|
||||
// Create channels through which the tasks can signal to each other they need to clean up
|
||||
let (kill_signal_1_s, kill_signal_1_r) = async_channel::bounded(1);
|
||||
let (kill_signal_2_s, kill_signal_2_r) = async_channel::bounded(1);
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener(db, &mut state, addr, &config, &write).fuse();
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker(addr, user_id.clone(), &config, read, &write).fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
|
||||
// Wait for either disconnect or for listener to die.
|
||||
select!(
|
||||
() = listener => {},
|
||||
() = worker => {}
|
||||
let listener = listener_with_kill_signal(
|
||||
db,
|
||||
&mut state,
|
||||
addr,
|
||||
&config,
|
||||
topic_signal_r,
|
||||
kill_signal_1_r,
|
||||
&write,
|
||||
kill_signal_2_s,
|
||||
);
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker_with_kill_signal(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id.clone(),
|
||||
&config,
|
||||
topic_signal_s,
|
||||
kill_signal_2_r,
|
||||
read,
|
||||
&write,
|
||||
kill_signal_1_s,
|
||||
);
|
||||
|
||||
join!(listener, worker);
|
||||
}
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
@@ -134,29 +180,88 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn listener_with_kill_signal(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
listener(
|
||||
db,
|
||||
state,
|
||||
addr,
|
||||
config,
|
||||
topic_signal_r,
|
||||
kill_signal_r,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
async fn listener(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
||||
let Ok(subscriber) = fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
else {
|
||||
return;
|
||||
let subscriber = match fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
{
|
||||
Ok(subscriber) => subscriber,
|
||||
Err(err) => {
|
||||
error!("Failed to build a subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if subscriber.init().await.is_err() {
|
||||
|
||||
if let Err(err) = subscriber.init().await {
|
||||
error!("Failed to init subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
};
|
||||
|
||||
// Handle Redis connection dropping
|
||||
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
||||
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
|
||||
subscriber.on_error(move |err| {
|
||||
if let RedisErrorKind::Canceled = err.kind() {
|
||||
let clean_up_s = clean_up_s.clone();
|
||||
spawn(async move {
|
||||
clean_up_s.lock().await.send(()).await.ok();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
loop {
|
||||
'out: loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state() {
|
||||
match state.apply_state().await {
|
||||
SubscriptionStateChange::Reset => {
|
||||
subscriber.unsubscribe_all().await.unwrap();
|
||||
for id in state.iter_subscriptions() {
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.unsubscribe_all().await {
|
||||
error!("Unsubscribe all failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
|
||||
let subscribed = state.subscribed.read().await;
|
||||
for id in subscribed.iter() {
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -167,112 +272,255 @@ async fn listener(
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
subscriber.unsubscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.unsubscribe(id).await {
|
||||
error!("Unsubscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
}
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
// Handle incoming events.
|
||||
let Ok(message) = message_rx.recv().await.map_err(|e| {
|
||||
warn!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let event = match *REDIS_PAYLOAD_TYPE {
|
||||
PayloadType::Json => message
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
|
||||
PayloadType::Msgpack => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
|
||||
PayloadType::Bincode => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
|
||||
};
|
||||
let Some(mut event) = event else {
|
||||
warn!("Failed to deserialise an event for {}!", message.channel);
|
||||
return;
|
||||
};
|
||||
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
let t1 = message_rx.recv().fuse();
|
||||
let t2 = topic_signal_r.recv().fuse();
|
||||
let t3 = kill_signal_r.recv().fuse();
|
||||
let t4 = clean_up_r.recv().fuse();
|
||||
|
||||
let result = write.lock().await.send(config.encode(&event)).await;
|
||||
if let Err(e) = result {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
warn!("Error while sending an event to {addr:?}: {e:?}");
|
||||
pin_mut!(t1, t2, t3, t4);
|
||||
|
||||
select! {
|
||||
_ = t4 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t3 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t2 => {},
|
||||
message = t1 => {
|
||||
// Handle incoming events.
|
||||
let message = match message {
|
||||
Ok(message) => message,
|
||||
Err(e) => {
|
||||
error!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
break 'out;
|
||||
}
|
||||
};
|
||||
|
||||
let event = match *REDIS_PAYLOAD_TYPE {
|
||||
PayloadType::Json => message
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
|
||||
PayloadType::Msgpack => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
|
||||
PayloadType::Bincode => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
|
||||
};
|
||||
|
||||
let Some(mut event) = event else {
|
||||
let err = format!(
|
||||
"Failed to deserialise an event for {}! Introspection: `{:?}`",
|
||||
message.channel,
|
||||
message
|
||||
.value
|
||||
.as_string()
|
||||
.map(|x| x.chars().take(32).collect::<String>())
|
||||
);
|
||||
|
||||
error!("{}", err);
|
||||
sentry::capture_message(&err, Level::Error);
|
||||
break 'out;
|
||||
};
|
||||
|
||||
if let EventV1::Auth(auth) = &event {
|
||||
if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
|
||||
if &state.session_id == session_id {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else if let AuthifierEvent::DeleteAllSessions {
|
||||
exclude_session_id, ..
|
||||
} = auth
|
||||
{
|
||||
if let Some(excluded) = exclude_session_id {
|
||||
if &state.session_id != excluded {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let result = write.lock().await.send(config.encode(&event)).await;
|
||||
if let Err(e) = result {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while sending an event to {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
break 'out;
|
||||
}
|
||||
|
||||
if let EventV1::Logout = event {
|
||||
info!("User {addr:?} received log out event!");
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = subscriber.quit().await {
|
||||
error!("{}", err);
|
||||
sentry::capture_error(&err);
|
||||
}
|
||||
}
|
||||
|
||||
async fn worker(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker_with_kill_signal(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
worker(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id,
|
||||
config,
|
||||
topic_signal_s,
|
||||
kill_signal_r,
|
||||
read,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
loop {
|
||||
let result = read.try_next().await;
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => return,
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
warn!("Error while reading an event from {addr:?}: {e:?}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
let t1 = read.try_next().fuse();
|
||||
let t2 = kill_signal_r.recv().fuse();
|
||||
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Pong { data }))
|
||||
.await
|
||||
.ok();
|
||||
pin_mut!(t1, t2);
|
||||
|
||||
select! {
|
||||
_ = t2 => {
|
||||
return;
|
||||
},
|
||||
result = t1 => {
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => {
|
||||
warn!("Received a None message!");
|
||||
sentry::capture_message("Received a None message!", Level::Warning);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while reading an event from {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Subscribe { server_id } => {
|
||||
let mut servers = active_servers.lock().await;
|
||||
let has_item = servers.contains_key(&server_id);
|
||||
servers.insert(server_id, ());
|
||||
|
||||
if !has_item {
|
||||
// Poke the listener to adjust subscriptions
|
||||
topic_signal_s.send(()).await.ok();
|
||||
}
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Pong { data }))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Configuration"
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -21,6 +21,9 @@ host = ""
|
||||
username = ""
|
||||
password = ""
|
||||
from_address = ""
|
||||
# reply_to = ""
|
||||
# port = 587
|
||||
# use_tls = true
|
||||
|
||||
[api.vapid]
|
||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||
@@ -29,6 +32,12 @@ public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQIS
|
||||
[api.fcm]
|
||||
api_key = ""
|
||||
|
||||
[api.apn]
|
||||
sandbox = false
|
||||
pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
[api.security]
|
||||
authifier_shield_key = ""
|
||||
voso_legacy_token = ""
|
||||
@@ -46,19 +55,40 @@ webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.default]
|
||||
[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
|
||||
|
||||
@@ -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,14 @@ pub struct ApiFcm {
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiApn {
|
||||
pub sandbox: bool,
|
||||
pub pkcs8: String,
|
||||
pub key_id: String,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurityCaptcha {
|
||||
pub hcaptcha_key: String,
|
||||
@@ -100,24 +110,33 @@ 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 FeaturesLimits {
|
||||
pub struct GlobalLimits {
|
||||
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_replies: usize,
|
||||
pub message_reactions: usize,
|
||||
pub servers: 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 bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
|
||||
pub attachment_size: usize,
|
||||
pub avatar_size: usize,
|
||||
pub background_size: usize,
|
||||
@@ -128,6 +147,9 @@ pub struct FeaturesLimits {
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimitsCollection {
|
||||
pub global: GlobalLimits,
|
||||
|
||||
pub new_user: FeaturesLimits,
|
||||
pub default: FeaturesLimits,
|
||||
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
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.2", path = "../config" }
|
||||
revolt-result = { version = "0.7.2", path = "../result" }
|
||||
revolt-models = { version = "0.7.2", path = "../models", features = [
|
||||
revolt-config = { version = "0.7.15", path = "../config" }
|
||||
revolt-result = { version = "0.7.15", path = "../result" }
|
||||
revolt-models = { version = "0.7.15", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.2", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.2", path = "../permissions", features = [
|
||||
revolt-presence = { version = "0.7.15", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.15", 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", default-features = false, features = [
|
||||
"ring",
|
||||
] }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0.8" }
|
||||
|
||||
@@ -2,10 +2,7 @@ use authifier::AuthifierEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
|
||||
FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
|
||||
UserSettings, Webhook,
|
||||
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, Webhook
|
||||
};
|
||||
use revolt_result::Error;
|
||||
|
||||
@@ -39,6 +36,19 @@ pub enum ErrorEvent {
|
||||
APIError(Error),
|
||||
}
|
||||
|
||||
/// Fields provided in Ready payload
|
||||
#[derive(PartialEq)]
|
||||
pub enum ReadyPayloadFields {
|
||||
Users,
|
||||
Servers,
|
||||
Channels,
|
||||
Members,
|
||||
Emoji,
|
||||
|
||||
UserSettings(Vec<String>),
|
||||
ChannelUnreads,
|
||||
}
|
||||
|
||||
/// Protocol Events
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -48,13 +58,25 @@ pub enum EventV1 {
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
/// Logged out
|
||||
Logout,
|
||||
/// Basic data to cache
|
||||
Ready {
|
||||
users: Vec<User>,
|
||||
servers: Vec<Server>,
|
||||
channels: Vec<Channel>,
|
||||
members: Vec<Member>,
|
||||
emojis: Vec<Emoji>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
users: Option<Vec<User>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
servers: Option<Vec<Server>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channels: Option<Vec<Channel>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
members: Option<Vec<Member>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_settings: Option<UserSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
@@ -67,6 +89,7 @@ pub enum EventV1 {
|
||||
id: String,
|
||||
channel: String,
|
||||
data: PartialMessage,
|
||||
clear: Vec<FieldsMessage>,
|
||||
},
|
||||
|
||||
/// Append information to existing message
|
||||
@@ -134,7 +157,11 @@ pub enum EventV1 {
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave { id: String, user: String },
|
||||
ServerMemberLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
reason: RemovalIntention,
|
||||
},
|
||||
|
||||
/// Server role created or updated
|
||||
ServerRoleUpdate {
|
||||
@@ -247,7 +274,7 @@ impl EventV1 {
|
||||
// TODO: this should be captured by member list in the future and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().p(member.id.server).await;
|
||||
self.clone().server(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +284,11 @@ impl EventV1 {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish server member event
|
||||
pub async fn server(self, id: String) {
|
||||
self.p(format!("{id}u")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
|
||||
@@ -8,5 +8,6 @@ pub enum ClientMessage {
|
||||
Authenticate { token: String },
|
||||
BeginTyping { channel: String },
|
||||
EndTyping { channel: String },
|
||||
Subscribe { server_id: String },
|
||||
Ping { data: Ping, responded: Option<()> },
|
||||
}
|
||||
|
||||
@@ -87,3 +87,8 @@ pub mod tasks;
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
}
|
||||
|
||||
/// Utility function to check if an option doesnt contain true
|
||||
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||
t != &Some(true)
|
||||
}
|
||||
@@ -146,7 +146,14 @@ pub async fn create_database(db: &MongoDb) {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
"name": "channel_pinned_compound"
|
||||
},
|
||||
]
|
||||
},
|
||||
None,
|
||||
|
||||
@@ -20,7 +20,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 27;
|
||||
pub const LATEST_REVISION: i32 = 28;
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -1071,6 +1071,29 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
if revision <= 27 {
|
||||
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "messages",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
"name": "channel_pinned_compound"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create message index.");
|
||||
}
|
||||
|
||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
|
||||
@@ -2,7 +2,7 @@ use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{BotInformation, Database, PartialUser, User};
|
||||
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Bot
|
||||
@@ -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
|
||||
@@ -142,6 +146,10 @@ impl Bot {
|
||||
|
||||
db.update_bot(&self.id, &partial, remove).await?;
|
||||
|
||||
if partial.token.is_some() {
|
||||
EventV1::Logout.private(self.id.clone()).await;
|
||||
}
|
||||
|
||||
self.apply_options(partial);
|
||||
Ok(())
|
||||
}
|
||||
@@ -164,7 +172,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bot = Bot::create(
|
||||
let (bot, _) = Bot::create(
|
||||
&db,
|
||||
"Bot Name".to_string(),
|
||||
&owner,
|
||||
|
||||
@@ -7,13 +7,13 @@ mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelUnreads: Sync + Send {
|
||||
/// Acknowledge a message.
|
||||
/// Acknowledge a message, and returns updated channel unread.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()>;
|
||||
) -> Result<Option<ChannelUnread>>;
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
|
||||
@@ -28,4 +28,7 @@ pub trait AbstractChannelUnreads: Sync + Send {
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use bson::Document;
|
||||
use mongodb::options::FindOneAndUpdateOptions;
|
||||
use mongodb::options::ReturnDocument;
|
||||
use mongodb::options::UpdateOptions;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
@@ -12,31 +14,35 @@ static COL: &str = "channel_unreads";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelUnreads for MongoDb {
|
||||
/// Acknowledge a message.
|
||||
/// Acknowledge a message, and returns updated channel unread.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
) -> Result<Option<ChannelUnread>> {
|
||||
self.col::<ChannelUnread>(COL)
|
||||
.find_one_and_update(
|
||||
doc! {
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": user_id,
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
"mentions": 1_i32
|
||||
"$pull": {
|
||||
"mentions": {
|
||||
"$lt": message_id
|
||||
}
|
||||
},
|
||||
"$set": {
|
||||
"last_id": message_id
|
||||
}
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
FindOneAndUpdateOptions::builder()
|
||||
.upsert(true)
|
||||
.return_document(ReturnDocument::After)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
@@ -116,4 +122,18 @@ impl AbstractChannelUnreads for MongoDb {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.user": user_id,
|
||||
"_id.channel": channel_id
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()> {
|
||||
) -> Result<Option<ChannelUnread>> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
@@ -27,14 +27,14 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
unreads.insert(
|
||||
key.clone(),
|
||||
ChannelUnread {
|
||||
id: key,
|
||||
id: key.clone(),
|
||||
last_id: Some(message_id.to_string()),
|
||||
mentions: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(unreads.get(&key).cloned())
|
||||
}
|
||||
|
||||
/// Acknowledge many channels.
|
||||
@@ -87,4 +87,14 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||
let unreads = self.channel_unreads.lock().await;
|
||||
|
||||
Ok(unreads.get(&ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string()
|
||||
}).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -415,13 +417,13 @@ impl Channel {
|
||||
}
|
||||
|
||||
/// Clone this channel's id
|
||||
pub fn id(&self) -> String {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Channel::DirectMessage { id, .. }
|
||||
| Channel::Group { id, .. }
|
||||
| Channel::SavedMessages { id, .. }
|
||||
| Channel::TextChannel { id, .. }
|
||||
| Channel::VoiceChannel { id, .. } => id.clone(),
|
||||
| Channel::VoiceChannel { id, .. } => id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,6 +690,8 @@ impl Channel {
|
||||
username: name,
|
||||
avatar: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
@@ -698,6 +702,8 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
db.remove_user_from_group(id, &user.id).await?;
|
||||
|
||||
EventV1::ChannelGroupLeave {
|
||||
id: id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
@@ -723,6 +729,8 @@ impl Channel {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -266,7 +266,7 @@ impl AbstractChannels for MongoDb {
|
||||
.await?;
|
||||
|
||||
// Delete the channel itself
|
||||
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
|
||||
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ impl AbstractChannels for ReferenceDb {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
|
||||
if let Entry::Vacant(entry) = channels.entry(channel.id().to_string()) {
|
||||
entry.insert(channel.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -148,7 +148,7 @@ impl AbstractChannels for ReferenceDb {
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if channels.remove(&channel.id()).is_some() {
|
||||
if channels.remove(channel.id()).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
|
||||
@@ -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,13 @@ auto_derived_partial!(
|
||||
/// Name and / or avatar overrides for this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub masquerade: Option<Masquerade>,
|
||||
/// Whether or not the message in pinned
|
||||
#[serde(skip_serializing_if = "crate::if_option_false")]
|
||||
pub pinned: Option<bool>,
|
||||
|
||||
/// Bitfield of message flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
},
|
||||
"PartialMessage"
|
||||
);
|
||||
@@ -95,6 +102,10 @@ auto_derived!(
|
||||
ChannelIconChanged { by: String },
|
||||
#[serde(rename = "channel_ownership_changed")]
|
||||
ChannelOwnershipChanged { from: String, to: String },
|
||||
#[serde(rename = "message_pinned")]
|
||||
MessagePinned { id: String, by: String },
|
||||
#[serde(rename = "message_unpinned")]
|
||||
MessageUnpinned { id: String, by: String },
|
||||
}
|
||||
|
||||
/// Name and / or avatar override information
|
||||
@@ -164,6 +175,8 @@ auto_derived!(
|
||||
pub author: Option<String>,
|
||||
/// Search query
|
||||
pub query: Option<String>,
|
||||
/// Search for pinned
|
||||
pub pinned: Option<bool>,
|
||||
}
|
||||
|
||||
/// Message Query
|
||||
@@ -179,6 +192,11 @@ auto_derived!(
|
||||
#[serde(flatten)]
|
||||
pub time_period: MessageTimePeriod,
|
||||
}
|
||||
|
||||
/// Optional fields on message
|
||||
pub enum FieldsMessage {
|
||||
Pinned,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
@@ -200,6 +218,8 @@ impl Default for Message {
|
||||
reactions: Default::default(),
|
||||
interactions: Default::default(),
|
||||
masquerade: None,
|
||||
flags: None,
|
||||
pinned: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,11 +227,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 +245,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 +261,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 {
|
||||
@@ -262,7 +293,7 @@ impl Message {
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: channel.id(),
|
||||
channel: channel.id().to_string(),
|
||||
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
|
||||
interactions: data
|
||||
.interactions
|
||||
@@ -270,6 +301,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 +320,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 +352,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 +392,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 +403,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 +452,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.clone(),
|
||||
member.clone(),
|
||||
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(user, member),
|
||||
Some(author),
|
||||
channel.id(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -470,15 +517,37 @@ 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<()> {
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialMessage,
|
||||
remove: Vec<FieldsMessage>,
|
||||
) -> Result<()> {
|
||||
self.apply_options(partial.clone());
|
||||
db.update_message(&self.id, &partial).await?;
|
||||
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
db.update_message(&self.id, &partial, remove.clone())
|
||||
.await?;
|
||||
|
||||
EventV1::MessageUpdate {
|
||||
id: self.id.clone(),
|
||||
channel: self.channel.clone(),
|
||||
data: partial.into(),
|
||||
clear: remove.into_iter().map(|field| field.into()).collect(),
|
||||
}
|
||||
.p(self.channel.clone())
|
||||
.await;
|
||||
@@ -498,13 +567,47 @@ 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 {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.map(|m| m.author.clone())
|
||||
.flat_map(|m| {
|
||||
let mut users = vec![m.author.clone()];
|
||||
if let Some(system) = &m.system {
|
||||
match system {
|
||||
v0::SystemMessage::ChannelDescriptionChanged { by } => {
|
||||
users.push(by.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelIconChanged { by } => users.push(by.clone()),
|
||||
v0::SystemMessage::ChannelOwnershipChanged { from, to, .. } => {
|
||||
users.push(from.clone());
|
||||
users.push(to.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelRenamed { by, .. } => users.push(by.clone()),
|
||||
v0::SystemMessage::UserAdded { by, id, .. }
|
||||
| v0::SystemMessage::UserRemove { by, id, .. } => {
|
||||
users.push(by.clone());
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::UserBanned { id, .. }
|
||||
| v0::SystemMessage::UserKicked { id, .. }
|
||||
| v0::SystemMessage::UserJoined { id, .. }
|
||||
| v0::SystemMessage::UserLeft { id, .. } => {
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::Text { .. } => {}
|
||||
v0::SystemMessage::MessagePinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
v0::SystemMessage::MessageUnpinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
users
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
@@ -588,7 +691,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));
|
||||
@@ -730,6 +833,12 @@ impl Message {
|
||||
// Write to database
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
}
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsMessage) {
|
||||
match field {
|
||||
FieldsMessage::Pinned => self.pinned = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemMessage {
|
||||
@@ -753,7 +862,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));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
@@ -20,7 +20,7 @@ pub trait AbstractMessages: Sync + Send {
|
||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()>;
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
|
||||
|
||||
@@ -5,7 +5,8 @@ use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{
|
||||
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
|
||||
AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath, Message, MessageQuery,
|
||||
MessageTimePeriod, MongoDb, PartialMessage,
|
||||
};
|
||||
|
||||
use super::AbstractMessages;
|
||||
@@ -50,6 +51,10 @@ impl AbstractMessages for MongoDb {
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(pinned) = query.filter.pinned {
|
||||
filter.insert("pinned", pinned);
|
||||
};
|
||||
|
||||
// 2. Find query limit
|
||||
let limit = query.limit.unwrap_or(50);
|
||||
|
||||
@@ -177,8 +182,22 @@ impl AbstractMessages for MongoDb {
|
||||
}
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
|
||||
async fn update_message(
|
||||
&self,
|
||||
id: &str,
|
||||
message: &PartialMessage,
|
||||
remove: Vec<FieldsMessage>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
message,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Append information to a given message
|
||||
@@ -296,6 +315,14 @@ impl AbstractMessages for MongoDb {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsMessage {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsMessage::Pinned => "pinned",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
|
||||
let mut for_attachments = projection.clone();
|
||||
|
||||
@@ -2,7 +2,7 @@ use futures::future::try_join_all;
|
||||
use indexmap::IndexSet;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
||||
|
||||
use super::AbstractMessages;
|
||||
|
||||
@@ -56,6 +56,12 @@ impl AbstractMessages for ReferenceDb {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pinned) = query.filter.pinned {
|
||||
if message.pinned.unwrap_or_default() == pinned {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
@@ -183,10 +189,15 @@ impl AbstractMessages for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message_data) = messages.get_mut(id) {
|
||||
message_data.apply_options(message.to_owned());
|
||||
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
message_data.remove_field(&field);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Member {
|
||||
server: &Server,
|
||||
user: &User,
|
||||
channels: Option<Vec<Channel>>,
|
||||
) -> Result<Vec<Channel>> {
|
||||
) -> Result<(Member, Vec<Channel>)> {
|
||||
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
|
||||
return Err(create_error!(Banned));
|
||||
}
|
||||
@@ -150,12 +150,12 @@ 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();
|
||||
}
|
||||
|
||||
Ok(channels)
|
||||
Ok((member, channels))
|
||||
}
|
||||
|
||||
/// Update member data
|
||||
@@ -229,6 +229,7 @@ impl Member {
|
||||
EventV1::ServerMemberLeave {
|
||||
id: self.id.server.to_string(),
|
||||
user: self.id.user.to_string(),
|
||||
reason: intention.clone().into(),
|
||||
}
|
||||
.p(self.id.server.to_string())
|
||||
.await;
|
||||
@@ -250,7 +251,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();
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ impl Server {
|
||||
vec![]
|
||||
};
|
||||
|
||||
server.channels = channels.iter().map(|c| c.id()).collect();
|
||||
server.channels = channels.iter().map(|c| c.id().to_string()).collect();
|
||||
db.insert_server(&server).await?;
|
||||
Ok((server, channels))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -276,23 +292,27 @@ impl User {
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
/// Find a user from a given token and hint
|
||||
/// Find a user and session ID from a given token and hint
|
||||
#[async_recursion]
|
||||
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> {
|
||||
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<(User, String)> {
|
||||
match hint {
|
||||
UserHint::Bot => {
|
||||
UserHint::Bot => Ok((
|
||||
db.fetch_user(
|
||||
&db.fetch_bot_by_token(token)
|
||||
.await
|
||||
.map_err(|_| create_error!(InvalidSession))?
|
||||
.id,
|
||||
)
|
||||
.await
|
||||
.await?,
|
||||
String::new(),
|
||||
)),
|
||||
UserHint::User => {
|
||||
let session = db.fetch_session_by_token(token).await?;
|
||||
Ok((db.fetch_user(&session.user_id).await?, session.id))
|
||||
}
|
||||
UserHint::User => db.fetch_user_by_token(token).await,
|
||||
UserHint::Any => {
|
||||
if let Ok(user) = User::from_token(db, token, UserHint::User).await {
|
||||
Ok(user)
|
||||
if let Ok(result) = User::from_token(db, token, UserHint::User).await {
|
||||
Ok(result)
|
||||
} else {
|
||||
User::from_token(db, token, UserHint::Bot).await
|
||||
}
|
||||
@@ -473,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,
|
||||
@@ -482,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,
|
||||
@@ -617,6 +658,7 @@ impl User {
|
||||
x.background = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::DisplayName => self.display_name = None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use authifier::models::Session;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
@@ -16,8 +17,8 @@ pub trait AbstractUsers: Sync + Send {
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, token: &str) -> Result<Session>;
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
|
||||
@@ -57,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<()>;
|
||||
}
|
||||
|
||||
@@ -46,10 +46,9 @@ impl AbstractUsers for MongoDb {
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
|
||||
let session = self
|
||||
.col::<Session>("sessions")
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
|
||||
self.col::<Session>("sessions")
|
||||
.find_one(
|
||||
doc! {
|
||||
"token": token
|
||||
@@ -58,9 +57,7 @@ impl AbstractUsers for MongoDb {
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find_one", "sessions"))?
|
||||
.ok_or_else(|| create_error!(InvalidSession))?;
|
||||
|
||||
self.fetch_user(&session.user_id).await
|
||||
.ok_or_else(|| create_error!(InvalidSession))
|
||||
}
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
@@ -319,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 {
|
||||
@@ -329,6 +345,7 @@ impl IntoDocumentPath for FieldsUser {
|
||||
FieldsUser::ProfileContent => "profile.content",
|
||||
FieldsUser::StatusPresence => "status.presence",
|
||||
FieldsUser::StatusText => "status.text",
|
||||
FieldsUser::DisplayName => "display_name",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use authifier::models::Session;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
@@ -40,8 +41,8 @@ impl AbstractUsers for ReferenceDb {
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> {
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, _token: &str) -> Result<Session> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -162,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!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ use deadqueue::limited::Queue;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use super::DelayedTask;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::{apple_notifications::{self, ApnJob}, DelayedTask};
|
||||
|
||||
/// Enumeration of possible events
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@@ -52,8 +54,43 @@ pub async fn queue(channel: String, user: String, event: AckEvent) {
|
||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||
}
|
||||
|
||||
pub async fn handle_ack_event(event: &AckEvent, db: &Database, authifier_db: &authifier::Database, user: &str, channel: &str) -> Result<()> {
|
||||
match &event {
|
||||
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
|
||||
AckEvent::AckMessage { id } => {
|
||||
let unread = db.fetch_unread(user, channel).await?;
|
||||
let updated = db.acknowledge_message(channel, user, id).await?;
|
||||
|
||||
if let (Some(before), Some(after)) = (unread, updated) {
|
||||
let before_mentions = before.mentions.unwrap_or_default().len();
|
||||
let after_mentions = after.mentions.unwrap_or_default().len();
|
||||
|
||||
let mentions_acked = before_mentions - after_mentions;
|
||||
|
||||
if mentions_acked > 0 {
|
||||
if let Ok(sessions) = authifier_db.find_sessions(user).await {
|
||||
for session in sessions {
|
||||
if let Some(sub) = session.subscription {
|
||||
if sub.endpoint == "apn" {
|
||||
apple_notifications::queue(ApnJob::from_ack(session.id, user.to_string(), sub.auth)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
},
|
||||
AckEvent::AddMention { ids } => {
|
||||
db.add_mention_to_unread(channel, user, ids).await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
pub async fn worker(db: Database, authifier_db: authifier::Database) {
|
||||
let mut tasks = HashMap::<(String, String), DelayedTask<Task>>::new();
|
||||
let mut keys = vec![];
|
||||
|
||||
@@ -71,13 +108,7 @@ pub async fn worker(db: Database) {
|
||||
let Task { event } = task.data;
|
||||
let (user, channel) = key;
|
||||
|
||||
if let Err(err) = match &event {
|
||||
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
|
||||
AckEvent::AckMessage { id } => db.acknowledge_message(channel, user, id).await,
|
||||
AckEvent::AddMention { ids } => {
|
||||
db.add_mention_to_unread(channel, user, ids).await
|
||||
}
|
||||
} {
|
||||
if let Err(err) = handle_ack_event(&event, &db, &authifier_db, user, channel).await {
|
||||
error!("{err:?} for {event:?}. ({user}, {channel})");
|
||||
} else {
|
||||
info!("User {user} ack in {channel} with {event:?}");
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use base64::{
|
||||
engine::{self},
|
||||
Engine as _,
|
||||
};
|
||||
use deadqueue::limited::Queue;
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_a2::{
|
||||
request::{
|
||||
notification::{DefaultAlert, NotificationOptions},
|
||||
payload::{APSAlert, APSSound, PayloadLike, APS},
|
||||
},
|
||||
Client, ClientConfig, Endpoint, Error, ErrorBody, ErrorReason, Priority, PushType, Response,
|
||||
};
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::{Message, PushNotification};
|
||||
|
||||
use crate::Database;
|
||||
|
||||
/// Payload information, before assembly
|
||||
#[derive(Debug)]
|
||||
pub struct ApnPayload {
|
||||
message: Message,
|
||||
url: String,
|
||||
authorAvatar: String,
|
||||
authorDisplayName: String,
|
||||
channelName: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct Payload<'a> {
|
||||
aps: APS<'a>,
|
||||
#[serde(skip_serializing)]
|
||||
options: NotificationOptions<'a>,
|
||||
#[serde(skip_serializing)]
|
||||
device_token: &'a str,
|
||||
|
||||
message: &'a Message,
|
||||
url: &'a str,
|
||||
authorAvatar: &'a str,
|
||||
authorDisplayName: &'a str,
|
||||
channelName: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> PayloadLike for Payload<'a> {
|
||||
fn get_device_token(&self) -> &'a str {
|
||||
self.device_token
|
||||
}
|
||||
fn get_options(&self) -> &NotificationOptions {
|
||||
&self.options
|
||||
}
|
||||
}
|
||||
|
||||
/// Task information
|
||||
#[derive(Debug)]
|
||||
pub struct AlertJob {
|
||||
/// Session Id
|
||||
session_id: String,
|
||||
|
||||
/// Device token
|
||||
device_token: String,
|
||||
|
||||
/// User Id
|
||||
user_id: String,
|
||||
|
||||
/// Title
|
||||
title: String,
|
||||
|
||||
/// Body
|
||||
body: String,
|
||||
|
||||
/// Thread Id
|
||||
thread_id: String,
|
||||
|
||||
/// Category (informs the client what kind of notification is being sent.)
|
||||
category: String,
|
||||
|
||||
/// Payload used by the iOS client to modify the notification
|
||||
custom_payload: ApnPayload,
|
||||
}
|
||||
|
||||
impl AlertJob {
|
||||
fn format_title(notification: &PushNotification) -> String {
|
||||
// ideally this changes depending on context
|
||||
// in a server, it would look like "Sendername, #channelname in servername"
|
||||
// in a group, it would look like "Sendername in groupname"
|
||||
// in a dm it should just be "Sendername".
|
||||
// not sure how feasible all those are given the PushNotification object as it currently stands.
|
||||
format!(
|
||||
"{} in {}",
|
||||
notification.author, notification.message.channel
|
||||
) // TODO: this absolutely needs a channel name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BadgeJob {
|
||||
/// Session Id
|
||||
session_id: String,
|
||||
|
||||
/// Device token
|
||||
device_token: String,
|
||||
|
||||
/// User Id
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum JobType {
|
||||
Alert(AlertJob),
|
||||
Badge(BadgeJob),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApnJob {
|
||||
job_type: JobType,
|
||||
}
|
||||
|
||||
impl ApnJob {
|
||||
pub fn from_notification(
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
device_token: String,
|
||||
notification: &PushNotification,
|
||||
) -> ApnJob {
|
||||
ApnJob {
|
||||
job_type: JobType::Alert(AlertJob {
|
||||
session_id,
|
||||
device_token,
|
||||
user_id,
|
||||
title: AlertJob::format_title(notification),
|
||||
body: notification.body.to_string(),
|
||||
thread_id: notification.tag.to_string(),
|
||||
category: "ALERT_MESSAGE".to_string(),
|
||||
custom_payload: ApnPayload {
|
||||
message: notification.message.clone(),
|
||||
url: notification.url.clone(),
|
||||
authorAvatar: notification.icon.clone(),
|
||||
authorDisplayName: notification.author.clone(),
|
||||
channelName: "#fetchchannelnamehere".to_string(), // TODO: get actual channel name
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_ack(session_id: String, user_id: String, device_token: String) -> ApnJob {
|
||||
ApnJob {
|
||||
job_type: JobType::Badge(BadgeJob {
|
||||
session_id,
|
||||
device_token,
|
||||
user_id,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AssembledPayload<'a> {
|
||||
Alert(Payload<'a>),
|
||||
Default(revolt_a2::request::payload::Payload<'a>),
|
||||
}
|
||||
|
||||
static Q: Lazy<Queue<ApnJob>> = Lazy::new(|| Queue::new(10_000));
|
||||
|
||||
/// Queue a new task for a worker
|
||||
pub async fn queue(task: ApnJob) {
|
||||
Q.try_push(task).ok();
|
||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||
}
|
||||
|
||||
async fn get_badge_count(db: &Database, user: &str) -> Option<u32> {
|
||||
if let Ok(unreads) = db.fetch_unreads(user).await {
|
||||
let mut mention_count = 0;
|
||||
for channel in unreads {
|
||||
if let Some(mentions) = channel.mentions {
|
||||
mention_count += mentions.len() as u32
|
||||
}
|
||||
}
|
||||
|
||||
return Some(mention_count);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 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 endpoint = if config.api.apn.sandbox {
|
||||
Endpoint::Sandbox
|
||||
} else {
|
||||
Endpoint::Production
|
||||
};
|
||||
|
||||
let pkcs8 = engine::general_purpose::STANDARD
|
||||
.decode(config.api.apn.pkcs8)
|
||||
.expect("valid `pcks8`");
|
||||
|
||||
let client_config = ClientConfig::new(endpoint);
|
||||
|
||||
let client = Client::token(
|
||||
&mut Cursor::new(pkcs8),
|
||||
config.api.apn.key_id,
|
||||
config.api.apn.team_id,
|
||||
client_config,
|
||||
)
|
||||
.expect("could not create APN client");
|
||||
|
||||
let payload_options = NotificationOptions {
|
||||
apns_id: None,
|
||||
apns_push_type: Some(PushType::Alert),
|
||||
apns_expiration: None,
|
||||
apns_priority: Some(Priority::High),
|
||||
apns_topic: Some("chat.revolt.app"),
|
||||
apns_collapse_id: None,
|
||||
};
|
||||
|
||||
loop {
|
||||
let task = Q.pop().await;
|
||||
let payload: AssembledPayload;
|
||||
|
||||
match task.job_type {
|
||||
JobType::Alert(ref alert) => {
|
||||
payload = AssembledPayload::Alert(Payload {
|
||||
aps: APS {
|
||||
alert: Some(APSAlert::Default(DefaultAlert {
|
||||
title: Some(&alert.title),
|
||||
subtitle: None,
|
||||
body: Some(&alert.body),
|
||||
title_loc_key: None,
|
||||
title_loc_args: None,
|
||||
action_loc_key: None,
|
||||
loc_key: None,
|
||||
loc_args: None,
|
||||
launch_image: None,
|
||||
})),
|
||||
badge: get_badge_count(&db, &alert.user_id).await,
|
||||
sound: Some(APSSound::Sound("default")),
|
||||
thread_id: Some(&alert.thread_id),
|
||||
content_available: None,
|
||||
category: Some(&alert.category),
|
||||
mutable_content: Some(1),
|
||||
url_args: None,
|
||||
},
|
||||
device_token: &alert.device_token,
|
||||
options: payload_options.clone(),
|
||||
message: &alert.custom_payload.message,
|
||||
url: &alert.custom_payload.url,
|
||||
authorAvatar: &alert.custom_payload.authorAvatar,
|
||||
authorDisplayName: &alert.custom_payload.authorDisplayName,
|
||||
channelName: &alert.custom_payload.channelName,
|
||||
});
|
||||
}
|
||||
JobType::Badge(ref alert) => {
|
||||
payload = AssembledPayload::Default(revolt_a2::request::payload::Payload {
|
||||
aps: APS {
|
||||
alert: None,
|
||||
badge: get_badge_count(&db, &alert.user_id).await,
|
||||
sound: None,
|
||||
thread_id: None,
|
||||
content_available: None,
|
||||
category: None,
|
||||
mutable_content: None,
|
||||
url_args: None,
|
||||
},
|
||||
device_token: &alert.device_token,
|
||||
options: payload_options.clone(),
|
||||
data: std::collections::BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let resp = match payload {
|
||||
AssembledPayload::Alert(p) => client.send(p).await,
|
||||
AssembledPayload::Default(p) => client.send(p).await,
|
||||
};
|
||||
//println!("response from APNS: {:?}", resp);
|
||||
|
||||
if let Err(err) = resp {
|
||||
match err {
|
||||
Error::ResponseError(Response {
|
||||
error:
|
||||
Some(ErrorBody {
|
||||
reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered,
|
||||
..
|
||||
}),
|
||||
..
|
||||
}) => {
|
||||
if let Err(err) = db
|
||||
.remove_push_subscription_by_session_id(match task.job_type {
|
||||
JobType::Alert(ref a) => &a.session_id.as_str(),
|
||||
JobType::Badge(ref a) => &a.session_id.as_str(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
}
|
||||
err => {
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,17 @@ 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(ack::worker(db.clone(), authifier_db.clone()));
|
||||
task::spawn(last_message_id::worker(db.clone()));
|
||||
task::spawn(process_embeds::worker(db.clone()));
|
||||
task::spawn(web_push::worker(authifier_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;
|
||||
|
||||
@@ -16,6 +16,8 @@ use web_push::{
|
||||
WebPushClient, WebPushMessageBuilder,
|
||||
};
|
||||
|
||||
use super::apple_notifications;
|
||||
|
||||
/// Task information
|
||||
#[derive(Debug)]
|
||||
struct PushTask {
|
||||
@@ -79,6 +81,7 @@ pub async fn worker(db: Database) {
|
||||
tag,
|
||||
timestamp: _,
|
||||
url: _,
|
||||
message: _,
|
||||
} = &task.payload;
|
||||
|
||||
let mut notification = fcm::NotificationBuilder::new();
|
||||
@@ -101,6 +104,14 @@ pub async fn worker(db: Database) {
|
||||
} else {
|
||||
info!("No FCM token was specified!");
|
||||
}
|
||||
} else if sub.endpoint == "apn" {
|
||||
apple_notifications::queue(apple_notifications::ApnJob::from_notification(
|
||||
session.id,
|
||||
session.user_id,
|
||||
sub.auth,
|
||||
&task.payload,
|
||||
))
|
||||
.await;
|
||||
} else {
|
||||
// Use Web Push Standard
|
||||
let subscription = SubscriptionInfo {
|
||||
|
||||
@@ -459,26 +459,30 @@ 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(),
|
||||
pinned: self.pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,9 +494,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 +507,10 @@ 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),
|
||||
pinned: value.pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -525,6 +533,8 @@ impl From<crate::SystemMessage> for SystemMessage {
|
||||
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
|
||||
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
|
||||
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
|
||||
crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by },
|
||||
crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -995,26 +1005,31 @@ impl crate::User {
|
||||
vec![]
|
||||
},
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: can_see_profile
|
||||
&& revolt_presence::is_online(&self.id).await
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: if can_see_profile {
|
||||
self.status.map(|status| status.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
profile: if can_see_profile {
|
||||
self.profile.map(|profile| profile.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && revolt_presence::is_online(&self.id).await,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert user object into user model assuming mutual connection
|
||||
///
|
||||
/// Relations will never be included, i.e. when we process ourselves
|
||||
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
|
||||
where
|
||||
P: Into<Option<&'a crate::User>>,
|
||||
@@ -1050,40 +1065,57 @@ impl crate::User {
|
||||
discriminator: self.discriminator,
|
||||
display_name: self.display_name,
|
||||
avatar: self.avatar.map(|file| file.into()),
|
||||
relations: if let Some(crate::User { id, .. }) = perspective {
|
||||
if id == &self.id {
|
||||
self.relations
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|relation| relation.into())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
relations: vec![],
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: can_see_profile
|
||||
&& is_online
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: if can_see_profile {
|
||||
self.status.map(|status| status.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
profile: if can_see_profile {
|
||||
self.profile.map(|profile| profile.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && is_online,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -1099,13 +1131,19 @@ impl crate::User {
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: (force_online || revolt_presence::is_online(&self.id).await)
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: self.status.map(|status| status.into()),
|
||||
profile: self.profile.map(|profile| profile.into()),
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship: RelationshipStatus::User,
|
||||
online: revolt_presence::is_online(&self.id).await,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
@@ -1129,7 +1167,7 @@ impl From<User> for crate::User {
|
||||
relations: None,
|
||||
badges: Some(value.badges as i32),
|
||||
status: value.status.map(Into::into),
|
||||
profile: value.profile.map(Into::into),
|
||||
profile: None,
|
||||
flags: Some(value.flags as i32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(Into::into),
|
||||
@@ -1152,7 +1190,6 @@ impl From<crate::PartialUser> for PartialUser {
|
||||
}),
|
||||
badges: value.badges.map(|badges| badges as u32),
|
||||
status: value.status.map(|status| status.into()),
|
||||
profile: value.profile.map(|profile| profile.into()),
|
||||
flags: value.flags.map(|flags| flags as u32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(|bot| bot.into()),
|
||||
@@ -1171,6 +1208,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1183,6 +1221,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1285,3 +1324,18 @@ impl From<BotInformation> for crate::BotInformation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsMessage> for FieldsMessage {
|
||||
fn from(value: crate::FieldsMessage) -> Self {
|
||||
match value {
|
||||
crate::FieldsMessage::Pinned => FieldsMessage::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<FieldsMessage> for crate::FieldsMessage {
|
||||
fn from(value: FieldsMessage) -> Self {
|
||||
match value {
|
||||
FieldsMessage::Pinned => crate::FieldsMessage::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
|
||||
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
|
||||
|
||||
impl IdempotencyKey {
|
||||
pub fn unchecked_from_string(key: String) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
|
||||
// Backwards compatibility.
|
||||
// Issue #109
|
||||
pub async fn consume_nonce(&mut self, v: Option<String>) -> Result<()> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use revolt_result::Result;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use rocket::request::FromParam;
|
||||
@@ -44,9 +46,27 @@ impl Reference {
|
||||
db.fetch_channel(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch invite from Ref
|
||||
/// Fetch invite from Ref or create invite to server if discoverable
|
||||
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
||||
db.fetch_invite(&self.id).await
|
||||
if ulid::Ulid::from_str(&self.id).is_ok() {
|
||||
let server = self.as_server(db).await?;
|
||||
if !server.discoverable {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
Ok(Invite::Server {
|
||||
code: self.id.to_string(),
|
||||
server: server.id,
|
||||
creator: server.owner,
|
||||
channel: server
|
||||
.channels
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(create_error!(NotFound))?,
|
||||
})
|
||||
} else {
|
||||
db.fetch_invite(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch message from Ref
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: API Models"
|
||||
|
||||
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.7.2", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.2", path = "../permissions" }
|
||||
revolt-config = { version = "0.7.15", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.15", path = "../permissions" }
|
||||
|
||||
# Utility
|
||||
regex = "1"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -66,3 +66,8 @@ pub fn if_false(t: &bool) -> bool {
|
||||
pub fn if_zero_u32(t: &u32) -> bool {
|
||||
t == &0
|
||||
}
|
||||
|
||||
/// Utility function to check if an option doesnt contain true
|
||||
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||
t != &Some(true)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -84,6 +84,12 @@ auto_derived!(
|
||||
content_type: BandcampType,
|
||||
id: String,
|
||||
},
|
||||
AppleMusic {
|
||||
album_id: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
track_id: Option<String>,
|
||||
},
|
||||
/// Streamable Video
|
||||
Streamable { id: String },
|
||||
}
|
||||
@@ -92,38 +98,38 @@ auto_derived!(
|
||||
pub struct WebsiteMetadata {
|
||||
/// Direct URL to web page
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
url: Option<String>,
|
||||
pub url: Option<String>,
|
||||
/// Original direct URL
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
original_url: Option<String>,
|
||||
pub original_url: Option<String>,
|
||||
/// Remote content
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
special: Option<Special>,
|
||||
pub special: Option<Special>,
|
||||
|
||||
/// Title of website
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
title: Option<String>,
|
||||
pub title: Option<String>,
|
||||
/// Description of website
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
pub description: Option<String>,
|
||||
/// Embedded image
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
image: Option<Image>,
|
||||
pub image: Option<Image>,
|
||||
/// Embedded video
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
video: Option<Video>,
|
||||
pub video: Option<Video>,
|
||||
|
||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||
// opengraph_type: Option<String>,
|
||||
/// Site name
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
site_name: Option<String>,
|
||||
pub site_name: Option<String>,
|
||||
/// URL to site icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon_url: Option<String>,
|
||||
pub icon_url: Option<String>,
|
||||
/// CSS Colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
colour: Option<String>,
|
||||
pub colour: Option<String>,
|
||||
}
|
||||
|
||||
/// Text Embed
|
||||
|
||||
@@ -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,18 @@ auto_derived_partial!(
|
||||
/// Name and / or avatar overrides for this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub masquerade: Option<Masquerade>,
|
||||
/// Whether or not the message in pinned
|
||||
#[serde(skip_serializing_if = "crate::if_option_false")]
|
||||
pub pinned: Option<bool>,
|
||||
|
||||
/// 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"
|
||||
);
|
||||
@@ -112,6 +130,10 @@ auto_derived!(
|
||||
ChannelIconChanged { by: String },
|
||||
#[serde(rename = "channel_ownership_changed")]
|
||||
ChannelOwnershipChanged { from: String, to: String },
|
||||
#[serde(rename = "message_pinned")]
|
||||
MessagePinned { id: String, by: String },
|
||||
#[serde(rename = "message_unpinned")]
|
||||
MessageUnpinned { id: String, by: String },
|
||||
}
|
||||
|
||||
/// Name and / or avatar override information
|
||||
@@ -185,6 +207,8 @@ auto_derived!(
|
||||
pub timestamp: u64,
|
||||
/// URL to open when clicking notification
|
||||
pub url: String,
|
||||
/// The message object itself, to send to clients for processing
|
||||
pub message: Message,
|
||||
}
|
||||
|
||||
/// Representation of a text embed before it is sent.
|
||||
@@ -241,6 +265,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
|
||||
@@ -278,7 +307,9 @@ auto_derived!(
|
||||
///
|
||||
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
|
||||
pub query: String,
|
||||
pub query: Option<String>,
|
||||
/// Whether to only search for pinned messages, cannot be sent with `query`.
|
||||
pub pinned: Option<bool>,
|
||||
|
||||
/// Maximum number of messages to fetch
|
||||
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
||||
@@ -328,6 +359,18 @@ 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,
|
||||
}
|
||||
|
||||
/// Optional fields on message
|
||||
pub enum FieldsMessage {
|
||||
Pinned
|
||||
}
|
||||
);
|
||||
|
||||
/// Message Author Abstraction
|
||||
@@ -391,6 +434,8 @@ impl From<SystemMessage> for String {
|
||||
SystemMessage::ChannelOwnershipChanged { .. } => {
|
||||
"Channel ownership changed.".to_string()
|
||||
}
|
||||
SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(),
|
||||
SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,15 +455,30 @@ impl PushNotification {
|
||||
format!("{}/assets/logo.png", config.hosts.app)
|
||||
};
|
||||
|
||||
let image = msg.attachments.and_then(|attachments| {
|
||||
let image = msg.attachments.as_ref().and_then(|attachments| {
|
||||
attachments
|
||||
.first()
|
||||
.map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
|
||||
});
|
||||
|
||||
let body = if let Some(sys) = msg.system {
|
||||
sys.into()
|
||||
} else if let Some(text) = msg.content {
|
||||
let body = if let Some(ref sys) = msg.system {
|
||||
sys.clone().into()
|
||||
} else if let Some(ref text) = msg.content {
|
||||
text.clone()
|
||||
} else if let Some(text) = msg.embeds.as_ref().and_then(|embeds| match embeds.first() {
|
||||
Some(Embed::Image(_)) => Some("Sent an image".to_string()),
|
||||
Some(Embed::Video(_)) => Some("Sent a video".to_string()),
|
||||
Some(Embed::Text(e)) => e
|
||||
.description
|
||||
.clone()
|
||||
.or(e.title.clone().or(Some("Empty Embed".to_string()))),
|
||||
Some(Embed::Website(e)) => e.title.clone().or(e
|
||||
.description
|
||||
.clone()
|
||||
.or(e.site_name.clone().or(Some("Empty Embed".to_string())))),
|
||||
Some(Embed::None) => Some("Empty Message".to_string()), // ???
|
||||
None => Some("Empty Message".to_string()), // ??
|
||||
}) {
|
||||
text
|
||||
} else {
|
||||
"Empty Message".to_string()
|
||||
@@ -439,6 +499,7 @@ impl PushNotification {
|
||||
tag: channel_id.to_string(),
|
||||
timestamp,
|
||||
url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id),
|
||||
message: msg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -50,11 +52,10 @@ auto_derived_partial!(
|
||||
/// User's current status
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub status: Option<UserStatus>,
|
||||
/// User's profile page
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub profile: Option<UserProfile>,
|
||||
|
||||
/// 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)
|
||||
@@ -86,6 +87,7 @@ auto_derived!(
|
||||
StatusPresence,
|
||||
ProfileContent,
|
||||
ProfileBackground,
|
||||
DisplayName,
|
||||
}
|
||||
|
||||
/// User's relationship with another user (or themselves)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-permissions"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Permission Logic"
|
||||
|
||||
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.7.2", path = "../result" }
|
||||
revolt-result = { version = "0.7.15", path = "../result" }
|
||||
|
||||
# Utility
|
||||
auto_ops = "0.3.0"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-presence"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-result"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Result and Error types"
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
@@ -28,6 +30,14 @@ pub struct Error {
|
||||
pub location: String,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?} occurred in {}", self.error_type, self.location)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
/// Possible error types
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(tag = "type"))]
|
||||
@@ -50,6 +60,9 @@ pub enum ErrorType {
|
||||
Blocked,
|
||||
BlockedByOther,
|
||||
NotFriends,
|
||||
TooManyPendingFriendRequests {
|
||||
max: usize,
|
||||
},
|
||||
|
||||
// ? Channel related errors
|
||||
UnknownChannel,
|
||||
@@ -77,6 +90,8 @@ pub enum ErrorType {
|
||||
},
|
||||
AlreadyInGroup,
|
||||
NotInGroup,
|
||||
AlreadyPinned,
|
||||
NotPinned,
|
||||
|
||||
// ? Server related errors
|
||||
UnknownServer,
|
||||
|
||||
@@ -25,6 +25,7 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::Blocked => Status::Conflict,
|
||||
ErrorType::BlockedByOther => Status::Forbidden,
|
||||
ErrorType::NotFriends => Status::Forbidden,
|
||||
ErrorType::TooManyPendingFriendRequests { .. } => Status::BadRequest,
|
||||
|
||||
ErrorType::UnknownChannel => Status::NotFound,
|
||||
ErrorType::UnknownMessage => Status::NotFound,
|
||||
@@ -39,6 +40,8 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::GroupTooLarge { .. } => Status::Forbidden,
|
||||
ErrorType::AlreadyInGroup => Status::Conflict,
|
||||
ErrorType::NotInGroup => Status::NotFound,
|
||||
ErrorType::AlreadyPinned => Status::BadRequest,
|
||||
ErrorType::NotPinned => Status::BadRequest,
|
||||
|
||||
ErrorType::UnknownServer => Status::NotFound,
|
||||
ErrorType::InvalidRole => Status::NotFound,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.7.2"
|
||||
version = "0.7.15"
|
||||
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"] }
|
||||
|
||||
|
||||
@@ -47,8 +47,7 @@ pub async fn web() -> Rocket<Build> {
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: Default::default(),
|
||||
// config: authifier_config().await,
|
||||
config: authifier_config().await,
|
||||
event_channel: Some(sender),
|
||||
};
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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`");
|
||||
|
||||
|
||||
@@ -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`");
|
||||
|
||||
|
||||
@@ -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`");
|
||||
|
||||
|
||||
@@ -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`");
|
||||
|
||||
|
||||
@@ -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`");
|
||||
|
||||
|
||||
@@ -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`");
|
||||
|
||||
@@ -93,7 +93,9 @@ mod test {
|
||||
.client
|
||||
.post(format!("/bots/{}/invite", bot.id))
|
||||
.header(ContentType::JSON)
|
||||
.body(json!(v0::InviteBotDestination::Group { group: group.id() }).to_string())
|
||||
.body(json!(v0::InviteBotDestination::Group {
|
||||
group: group.id().to_string()
|
||||
}).to_string())
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.dispatch()
|
||||
.await;
|
||||
@@ -102,8 +104,8 @@ mod test {
|
||||
drop(response);
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(&group.id(), |event| match event {
|
||||
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
|
||||
.wait_for_event(group.id(), |event| match event {
|
||||
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
@@ -121,7 +123,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`");
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ mod test {
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(&format!("{}!", user.id), |event| match event {
|
||||
EventV1::ChannelAck { id, .. } => id == &group.id(),
|
||||
EventV1::ChannelAck { id, .. } => id == group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -82,8 +82,8 @@ mod test {
|
||||
drop(response);
|
||||
|
||||
harness
|
||||
.wait_for_event(&group.id(), |event| match event {
|
||||
EventV1::ChannelDelete { id, .. } => id == &group.id(),
|
||||
.wait_for_event(group.id(), |event| match event {
|
||||
EventV1::ChannelDelete { id, .. } => id == group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ mod test {
|
||||
.await;
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(&group.id(), |event| match event {
|
||||
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
|
||||
.wait_for_event(group.id(), |event| match event {
|
||||
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
@@ -113,7 +113,7 @@ mod test {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let message = harness.wait_for_message(&group.id()).await;
|
||||
let message = harness.wait_for_message(group.id()).await;
|
||||
|
||||
assert_eq!(
|
||||
message.system,
|
||||
|
||||
@@ -106,8 +106,8 @@ mod test {
|
||||
.await;
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(&group.id(), |event| match event {
|
||||
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
|
||||
.wait_for_event(group.id(), |event| match event {
|
||||
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
@@ -117,7 +117,7 @@ mod test {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let message = harness.wait_for_message(&group.id()).await;
|
||||
let message = harness.wait_for_message(group.id()).await;
|
||||
|
||||
assert_eq!(
|
||||
message.system,
|
||||
|
||||
@@ -27,7 +27,7 @@ pub async fn clear_reactions(
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
|
||||
// Fetch relevant message
|
||||
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
|
||||
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||
|
||||
// Clear reactions
|
||||
message
|
||||
@@ -37,6 +37,7 @@ pub async fn clear_reactions(
|
||||
reactions: Some(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![]
|
||||
)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
|
||||
@@ -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
|
||||
@@ -44,7 +43,7 @@ pub async fn edit(
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?;
|
||||
|
||||
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
|
||||
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||
if message.author != user.id {
|
||||
return Err(create_error!(CannotEditMessage));
|
||||
}
|
||||
@@ -84,7 +83,7 @@ pub async fn edit(
|
||||
|
||||
partial.embeds = Some(new_embeds);
|
||||
|
||||
message.update(db, partial).await?;
|
||||
message.update(db, partial, vec![]).await?;
|
||||
|
||||
// Queue up a task for processing embeds if the we have sufficient permissions
|
||||
if permissions.has_channel_permission(ChannelPermission::SendEmbeds) {
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, PartialMessage, SystemMessage, User
|
||||
};
|
||||
use revolt_models::v0::MessageAuthor;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Pins a message
|
||||
///
|
||||
/// Pins a message by its id.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[post("/<target>/messages/<msg>/pin")]
|
||||
pub async fn message_pin(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
|
||||
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||
|
||||
if message.pinned.unwrap_or_default() {
|
||||
return Err(create_error!(AlreadyPinned))
|
||||
}
|
||||
|
||||
message.update(db, PartialMessage {
|
||||
pinned: Some(true),
|
||||
..Default::default()
|
||||
}, vec![]).await?;
|
||||
|
||||
SystemMessage::MessagePinned {
|
||||
id: message.id.clone(),
|
||||
by: user.id.clone()
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref())
|
||||
},
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false
|
||||
).await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{rocket, util::test::TestHarness};
|
||||
use revolt_database::{events::client::EventV1, util::{idempotency::IdempotencyKey, reference::Reference}, Member, Message, Server};
|
||||
use revolt_models::v0::{self, SystemMessage};
|
||||
use rocket::http::{Header, Status};
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn pin_message() {
|
||||
let mut harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
|
||||
let (server, channels) = Server::create(
|
||||
&harness.db,
|
||||
v0::DataCreateServer {
|
||||
name: "Test Server".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
&user,
|
||||
true
|
||||
).await.expect("Failed to create test server");
|
||||
|
||||
let (member, channels) = Member::create(&harness.db, &server, &user, Some(channels)).await.expect("Failed to create member");
|
||||
let channel = &channels[0];
|
||||
|
||||
let message = Message::create_from_api(
|
||||
&harness.db,
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content:Some("Test message".to_string()),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: None,
|
||||
embeds: None,
|
||||
masquerade: None,
|
||||
interactions: None,
|
||||
flags: None
|
||||
},
|
||||
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(member.into()),
|
||||
user.limits().await,
|
||||
IdempotencyKey::unchecked_from_string("0".to_string()),
|
||||
false,
|
||||
false
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create message");
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
.post(format!("/channels/{}/messages/{}/pin", channel.id(), &message.id))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::NoContent);
|
||||
drop(response);
|
||||
|
||||
harness.wait_for_event(channel.id(), |event| {
|
||||
match event {
|
||||
EventV1::Message(message) => {
|
||||
match &message.system {
|
||||
Some(SystemMessage::MessagePinned { by, .. }) => {
|
||||
assert_eq!(by, &user.id);
|
||||
|
||||
true
|
||||
},
|
||||
_ => false
|
||||
}
|
||||
},
|
||||
_ => false
|
||||
}
|
||||
}).await;
|
||||
|
||||
harness.wait_for_event(channel.id(), |event| {
|
||||
match event {
|
||||
EventV1::MessageUpdate { id, channel: channel_id, data, .. } => {
|
||||
assert_eq!(id, &message.id);
|
||||
assert_eq!(channel_id, channel.id());
|
||||
assert_eq!(data.pinned, Some(true));
|
||||
|
||||
true
|
||||
},
|
||||
_ => false
|
||||
}
|
||||
}).await;
|
||||
|
||||
let updated_message = Reference::from_unchecked(message.id)
|
||||
.as_message(&harness.db)
|
||||
.await
|
||||
.expect("Failed to find updated message");
|
||||
|
||||
assert_eq!(updated_message.pinned, Some(true));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub async fn react_message(
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
||||
|
||||
// Fetch relevant message
|
||||
let message = msg.as_message_in_channel(db, &channel.id()).await?;
|
||||
let message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||
|
||||
// Add the reaction
|
||||
message
|
||||
|
||||
@@ -30,6 +30,10 @@ pub async fn search(
|
||||
})
|
||||
})?;
|
||||
|
||||
if options.query.is_some() && options.pinned.is_some() {
|
||||
return Err(create_error!(InvalidOperation))
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
@@ -39,6 +43,7 @@ pub async fn search(
|
||||
|
||||
let v0::DataMessageSearch {
|
||||
query,
|
||||
pinned,
|
||||
limit,
|
||||
before,
|
||||
after,
|
||||
@@ -51,7 +56,8 @@ pub async fn search(
|
||||
MessageQuery {
|
||||
filter: MessageFilter {
|
||||
channel: Some(channel.id().to_string()),
|
||||
query: Some(query),
|
||||
query,
|
||||
pinned,
|
||||
..Default::default()
|
||||
},
|
||||
time_period: MessageTimePeriod::Absolute {
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, FieldsMessage, PartialMessage, SystemMessage, User
|
||||
};
|
||||
use revolt_models::v0::MessageAuthor;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Unpins a message
|
||||
///
|
||||
/// Unpins a message by its id.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[delete("/<target>/messages/<msg>/pin")]
|
||||
pub async fn message_unpin(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
|
||||
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||
|
||||
if !message.pinned.unwrap_or_default() {
|
||||
return Err(create_error!(NotPinned))
|
||||
}
|
||||
|
||||
message.update(db, PartialMessage::default(), vec![FieldsMessage::Pinned]).await?;
|
||||
|
||||
SystemMessage::MessageUnpinned {
|
||||
id: message.id.clone(),
|
||||
by: user.id.clone()
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref())
|
||||
},
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false
|
||||
).await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{rocket, util::test::TestHarness};
|
||||
use revolt_database::{events::client::EventV1, util::{idempotency::IdempotencyKey, reference::Reference}, Member, Message, PartialMessage, Server};
|
||||
use revolt_models::v0::{self, FieldsMessage, SystemMessage};
|
||||
use rocket::http::{Header, Status};
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn unpin_message() {
|
||||
let mut harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
|
||||
let (server, channels) = Server::create(
|
||||
&harness.db,
|
||||
v0::DataCreateServer {
|
||||
name: "Test Server".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
&user,
|
||||
true
|
||||
).await.expect("Failed to create test server");
|
||||
|
||||
let channel = &channels[0];
|
||||
|
||||
Member::create(&harness.db, &server, &user, Some(channels.clone())).await.expect("Failed to create member");
|
||||
let member = Reference::from_unchecked(user.id.clone()).as_member(&harness.db, &server.id).await.expect("Failed to get member");
|
||||
|
||||
let message = Message::create_from_api(
|
||||
&harness.db,
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content:Some("Test message".to_string()),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: None,
|
||||
embeds: None,
|
||||
masquerade: None,
|
||||
interactions: None,
|
||||
flags: None
|
||||
},
|
||||
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(member.into()),
|
||||
user.limits().await,
|
||||
IdempotencyKey::unchecked_from_string("0".to_string()),
|
||||
false,
|
||||
false
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create message");
|
||||
|
||||
harness.db.update_message(
|
||||
&message.id,
|
||||
&PartialMessage {
|
||||
pinned: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
vec![]
|
||||
).await.expect("Failed to update message");
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
.delete(format!("/channels/{}/messages/{}/pin", channel.id(), &message.id))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::NoContent);
|
||||
drop(response);
|
||||
|
||||
harness.wait_for_event(channel.id(), |event| {
|
||||
match event {
|
||||
EventV1::Message(message) => {
|
||||
match &message.system {
|
||||
Some(SystemMessage::MessageUnpinned { by, .. }) => {
|
||||
assert_eq!(by, &user.id);
|
||||
|
||||
true
|
||||
},
|
||||
_ => false
|
||||
}
|
||||
},
|
||||
_ => false
|
||||
}
|
||||
}).await;
|
||||
|
||||
harness.wait_for_event(channel.id(), |event| {
|
||||
match event {
|
||||
EventV1::MessageUpdate { id, clear, .. } => {
|
||||
assert_eq!(&message.id, id);
|
||||
assert_eq!(clear, &[FieldsMessage::Pinned]);
|
||||
|
||||
true
|
||||
},
|
||||
_ => false
|
||||
}
|
||||
}).await;
|
||||
|
||||
let updated_message = Reference::from_unchecked(message.id)
|
||||
.as_message(&harness.db)
|
||||
.await
|
||||
.expect("Failed to find updated message");
|
||||
|
||||
assert_eq!(updated_message.pinned, None);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub async fn unreact_message(
|
||||
}
|
||||
|
||||
// Fetch relevant message
|
||||
let message = msg.as_message_in_channel(db, &channel.id()).await?;
|
||||
let message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||
|
||||
// Check if we should wipe all of this reaction
|
||||
if remove_all {
|
||||
|
||||
@@ -15,10 +15,12 @@ mod message_clear_reactions;
|
||||
mod message_delete;
|
||||
mod message_edit;
|
||||
mod message_fetch;
|
||||
mod message_pin;
|
||||
mod message_query;
|
||||
mod message_react;
|
||||
mod message_search;
|
||||
mod message_send;
|
||||
mod message_unpin;
|
||||
mod message_unreact;
|
||||
mod permissions_set;
|
||||
mod permissions_set_default;
|
||||
@@ -37,10 +39,12 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
message_send::message_send,
|
||||
message_query::query,
|
||||
message_search::search,
|
||||
message_pin::message_pin,
|
||||
message_fetch::fetch,
|
||||
message_edit::edit,
|
||||
message_bulk_delete::bulk_delete_messages,
|
||||
message_delete::delete,
|
||||
message_unpin::message_unpin,
|
||||
group_create::create_group,
|
||||
group_add_member::add_member,
|
||||
group_remove_member::remove_member,
|
||||
|
||||
@@ -54,7 +54,7 @@ pub async fn create_webhook(
|
||||
id: webhook_id,
|
||||
name: data.name,
|
||||
avatar,
|
||||
channel_id: channel.id(),
|
||||
channel_id: channel.id().to_string(),
|
||||
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
|
||||
token: Some(nanoid::nanoid!(64)),
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ pub async fn fetch_webhooks(
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||
|
||||
Ok(Json(
|
||||
db.fetch_webhooks_for_channel(&channel.id())
|
||||
db.fetch_webhooks_for_channel(channel.id())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ pub async fn join(
|
||||
match &invite {
|
||||
Invite::Server { server, .. } => {
|
||||
let server = db.fetch_server(server).await?;
|
||||
let channels = Member::create(db, &server, &user, None).await?;
|
||||
let (_, channels) = Member::create(db, &server, &user, None).await?;
|
||||
|
||||
Ok(Json(InviteJoinResponse::Server {
|
||||
channels: channels.into_iter().map(|c| c.into()).collect(),
|
||||
server: server.into(),
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub async fn list(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|u| u.into_self()),
|
||||
.map(|u| u.into_self(false)),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -30,12 +30,12 @@ pub async fn edit(
|
||||
})
|
||||
})?;
|
||||
|
||||
// Fetch server, target member and current permissions
|
||||
// Fetch server and target member
|
||||
let mut server = server.as_server(db).await?;
|
||||
let mut member = target.as_member(db, &server.id).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user)
|
||||
.server(&server)
|
||||
.member(&member);
|
||||
|
||||
// Fetch our currrent permissions
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
let permissions = calculate_server_permissions(&mut query).await;
|
||||
|
||||
// Check permissions in server
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use revolt_database::{util::reference::Reference, Database};
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::PermissionQuery;
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
@@ -10,10 +14,16 @@ use rocket::{serde::json::Json, State};
|
||||
#[get("/<target>/roles/<role_id>")]
|
||||
pub async fn fetch(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
role_id: String,
|
||||
) -> Result<Json<v0::Role>> {
|
||||
let mut server = target.as_server(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
if !query.are_we_a_member().await {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
let role = server.roles.remove(&role_id);
|
||||
|
||||
if let Some(role) = role {
|
||||
|
||||
@@ -30,7 +30,7 @@ pub async fn create_server(
|
||||
user.can_acquire_server(db).await?;
|
||||
|
||||
let (server, channels) = Server::create(db, data, &user, true).await?;
|
||||
let channels = Member::create(db, &server, &user, Some(channels)).await?;
|
||||
let (_, channels) = Member::create(db, &server, &user, Some(channels)).await?;
|
||||
|
||||
Ok(Json(v0::CreateServerLegacyResponse {
|
||||
server: server.into(),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
let
|
||||
# Pinned nixpkgs, deterministic. Last updated: 11-08-2023.
|
||||
pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/bb9707ef2ea4a5b749b362d5cf81ada3ded2c53f.tar.gz")) {};
|
||||
# Pinned nixpkgs, deterministic. Last updated: 28-07-2024.
|
||||
pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/9b34ca580417e1ebc56c4df57d8b387dad686665.tar.gz")) {};
|
||||
|
||||
# Rolling updates, not deterministic.
|
||||
# pkgs = import (fetchTarball("channel:nixpkgs-unstable")) {};
|
||||
@@ -29,7 +29,7 @@ in pkgs.mkShell {
|
||||
pkgs.rustc
|
||||
pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
pkgs.pkgconfig
|
||||
pkgs.pkg-config
|
||||
pkgs.openssl.dev
|
||||
|
||||
# mdbook
|
||||
|
||||
Reference in New Issue
Block a user