Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553437e1df | ||
|
|
6a421dcd37 | ||
|
|
f3a64200cf | ||
|
|
9c62568132 | ||
|
|
b7ed543156 | ||
|
|
7ec3b07f6c | ||
|
|
fb487130c8 | ||
|
|
3fd170a7de | ||
|
|
ab159ccaa3 | ||
|
|
d97260f2fe | ||
|
|
f977432e14 | ||
|
|
8009b3bf53 | ||
|
|
d9643ebd8d | ||
|
|
d568b4369e | ||
|
|
3b5491a438 | ||
|
|
083f071c30 | ||
|
|
19df5c5ef5 | ||
|
|
1745826d2f | ||
|
|
61a9fab37f | ||
|
|
96efc76866 |
@@ -12,7 +12,7 @@ permissions:
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: docker-test-${{ github.head_ref || github.ref }}
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: rust-build-test-${{ github.head_ref || github.ref }}
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
node = "25.4.0"
|
||||
pnpm = "10.28.1"
|
||||
|
||||
gh = "2.25.0"
|
||||
gh = "2.95.0"
|
||||
|
||||
rust = "1.92.0"
|
||||
"cargo:cargo-nextest" = "0.9.122"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.14.0"
|
||||
".": "0.13.7"
|
||||
}
|
||||
@@ -1,28 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [0.14.0](https://github.com/stoatchat/stoatchat/compare/v0.13.7...v0.14.0) (2026-07-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add pronouns to user and server members field ([#811](https://github.com/stoatchat/stoatchat/issues/811)) ([ffab236](https://github.com/stoatchat/stoatchat/commit/ffab2369ab5c9b88e007dcd74b91bb48e1988d26))
|
||||
* Audit Logs ([#466](https://github.com/stoatchat/stoatchat/issues/466)) ([502203d](https://github.com/stoatchat/stoatchat/commit/502203d37c63e486c32e33078e0021bf6390fe97))
|
||||
* replace tenor with gifbox ([#844](https://github.com/stoatchat/stoatchat/issues/844)) ([59f6e01](https://github.com/stoatchat/stoatchat/commit/59f6e012f827ab08a8e326354bfe6c9026e2cb2d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow removing channel slowmode ([#836](https://github.com/stoatchat/stoatchat/issues/836)) ([21daf3a](https://github.com/stoatchat/stoatchat/commit/21daf3aec693beae55bff51235e5e4b7d90f2362))
|
||||
* allow true server owner to bypass rank check on channel role-permission overrides ([0af376c](https://github.com/stoatchat/stoatchat/commit/0af376c26b149a5a0286608ebe3869587780a949))
|
||||
* channel role permissions fail with 400 InvalidOperation for server owners/admins ([#802](https://github.com/stoatchat/stoatchat/issues/802)) ([a7af24b](https://github.com/stoatchat/stoatchat/commit/a7af24b38d0a38d6f04187464a89e67d459d1708))
|
||||
* channel role permissions fail with InvalidOperation for owners/admins ([a7af24b](https://github.com/stoatchat/stoatchat/commit/a7af24b38d0a38d6f04187464a89e67d459d1708))
|
||||
* **docs:** update react version ([#842](https://github.com/stoatchat/stoatchat/issues/842)) ([a22378c](https://github.com/stoatchat/stoatchat/commit/a22378c35c2c6c84f8897ce897b9c4df420871d9))
|
||||
* migration script would panic on fresh installations since it couldn't find invites collection ([#820](https://github.com/stoatchat/stoatchat/issues/820)) ([784f35e](https://github.com/stoatchat/stoatchat/commit/784f35ebfa8568593812683b5fa399ca87af2d6b))
|
||||
* openapi using old naming ([#777](https://github.com/stoatchat/stoatchat/issues/777)) ([c70459b](https://github.com/stoatchat/stoatchat/commit/c70459b10ce107611b9d478add26db372361baf2))
|
||||
* point docs favicon to correct location ([#789](https://github.com/stoatchat/stoatchat/issues/789)) ([bebfe34](https://github.com/stoatchat/stoatchat/commit/bebfe349227d8cc555e1b488eb343f2c28b28b88))
|
||||
* server owner should bypass rank check on channel role-permission overrides ([#805](https://github.com/stoatchat/stoatchat/issues/805)) ([0af376c](https://github.com/stoatchat/stoatchat/commit/0af376c26b149a5a0286608ebe3869587780a949))
|
||||
* voice system messages and call notifs by fetching participant list ([#846](https://github.com/stoatchat/stoatchat/issues/846)) ([0b53db9](https://github.com/stoatchat/stoatchat/commit/0b53db9921f5ee5992d57a6316cd4e75d241726a))
|
||||
|
||||
## [0.13.7](https://github.com/stoatchat/stoatchat/compare/v0.13.6...v0.13.7) (2026-05-21)
|
||||
|
||||
|
||||
|
||||
Generated
+217
-45
@@ -300,6 +300,18 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
|
||||
dependencies = [
|
||||
"compression-codecs",
|
||||
"compression-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
@@ -1325,6 +1337,15 @@ dependencies = [
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bs58"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bson"
|
||||
version = "2.15.0"
|
||||
@@ -1491,9 +1512,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
@@ -1509,6 +1530,7 @@ dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -1581,6 +1603,23 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
|
||||
dependencies = [
|
||||
"compression-core",
|
||||
"flate2",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-core"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -2501,6 +2540,41 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "elasticsearch"
|
||||
version = "9.1.0-alpha.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12bb303aa6e1d28c0c86b6fbfe484fd0fd3f512629aeed1ac4f6b85f81d9834a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"dyn-clone",
|
||||
"flate2",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"reqwest 0.12.28",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"tokio",
|
||||
"url",
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elasticsearch-dsl"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9445aba099fdf52dea22534cf953b918d1e4cbb799c99507c9cc96188d9c230"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
@@ -3759,6 +3833,22 @@ dependencies = [
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper 1.10.1",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -4141,7 +4231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d4e5d712dd664b11e778d1cfc06c79ba2700d6bc1771e44fb7b6a4656b487d"
|
||||
dependencies = [
|
||||
"generic-array 1.4.3",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
@@ -4265,9 +4355,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.102"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -7035,7 +7125,7 @@ dependencies = [
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper-tls",
|
||||
"hyper-tls 0.5.0",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -7074,9 +7164,11 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper 1.10.1",
|
||||
"hyper-rustls 0.27.9",
|
||||
"hyper-tls 0.6.0",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
@@ -7088,6 +7180,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower",
|
||||
"tower-http 0.6.11",
|
||||
@@ -7164,7 +7257,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-autumn"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-macros",
|
||||
@@ -7205,7 +7298,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-tungstenite",
|
||||
@@ -7236,7 +7329,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-coalesced"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"lru",
|
||||
@@ -7245,7 +7338,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-config"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"cached",
|
||||
"config",
|
||||
@@ -7261,7 +7354,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-crond"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"futures-lite",
|
||||
@@ -7282,8 +7375,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-database"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-lock 2.8.0",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
@@ -7325,7 +7419,7 @@ dependencies = [
|
||||
"revolt_rocket_okapi",
|
||||
"rocket",
|
||||
"rust-argon2",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1 0.10.6",
|
||||
@@ -7340,7 +7434,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-delta"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"bitfield",
|
||||
@@ -7370,12 +7464,13 @@ dependencies = [
|
||||
"revolt-presence",
|
||||
"revolt-ratelimits",
|
||||
"revolt-result",
|
||||
"revolt-search",
|
||||
"revolt_rocket_okapi",
|
||||
"rocket",
|
||||
"rocket_cors",
|
||||
"rocket_empty",
|
||||
"rocket_prometheus",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -7387,7 +7482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-files"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -7415,7 +7510,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-gifbox"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-extra",
|
||||
@@ -7438,7 +7533,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-january"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"axum",
|
||||
@@ -7468,7 +7563,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-models"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"iso8601-timestamp",
|
||||
@@ -7479,7 +7574,7 @@ dependencies = [
|
||||
"revolt-permissions",
|
||||
"revolt_optional_struct",
|
||||
"rocket",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa",
|
||||
@@ -7488,14 +7583,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-parser"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"logos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-permissions"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"auto_ops",
|
||||
@@ -7503,14 +7598,14 @@ dependencies = [
|
||||
"num_enum",
|
||||
"once_cell",
|
||||
"revolt-result",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-presence"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -7522,7 +7617,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-pushd"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -7552,7 +7647,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-ratelimits"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -7568,23 +7663,55 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-result"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"log",
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi",
|
||||
"rocket",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"sentry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-search"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"elasticsearch",
|
||||
"elasticsearch-dsl",
|
||||
"futures",
|
||||
"iso8601-timestamp",
|
||||
"linkify",
|
||||
"revolt-config",
|
||||
"revolt-database",
|
||||
"revolt-models",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ulid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-searchd"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"lapin",
|
||||
"log",
|
||||
"revolt-config",
|
||||
"revolt-database",
|
||||
"revolt-search",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-voice-ingress"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -7646,7 +7773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23bfdf7ae769c3042fe727f6e5c17363b02a64b4b33ad60c3e5f73b26df7835b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -7672,7 +7799,7 @@ dependencies = [
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi_codegen",
|
||||
"rocket",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -8247,6 +8374,30 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
@@ -8661,8 +8812,18 @@ version = "3.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.14.0",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.2.1",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9634,12 +9795,17 @@ version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"bitflags 2.13.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.4.2",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -10080,9 +10246,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.3"
|
||||
version = "1.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
|
||||
checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
@@ -10179,6 +10345,12 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
|
||||
[[package]]
|
||||
name = "vsimd"
|
||||
version = "0.8.0"
|
||||
@@ -10236,9 +10408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.125"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -10249,9 +10421,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.75"
|
||||
version = "0.4.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
|
||||
checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -10259,9 +10431,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.125"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
dependencies = [
|
||||
"quote 1.0.46",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -10269,9 +10441,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.125"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -10282,9 +10454,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.125"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -10313,9 +10485,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.102"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
|
||||
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
||||
+15
-10
@@ -164,6 +164,10 @@ livekit-api = "=0.4.23"
|
||||
livekit-protocol = "=0.7.7"
|
||||
livekit-runtime = "0.4.0"
|
||||
|
||||
# Elasticsearch
|
||||
elasticsearch = "9.1.0-alpha.1"
|
||||
elasticsearch-dsl = "0.4"
|
||||
|
||||
# Other Utilities
|
||||
once_cell = "1.9.0"
|
||||
config = "0.13.3"
|
||||
@@ -193,13 +197,14 @@ futures-lite = "2.6.1"
|
||||
vergen = "7.5.0"
|
||||
|
||||
# Local packages
|
||||
revolt-coalesced = { version = "0.14.0", path = "crates/core/coalesced" }
|
||||
revolt-config = { version = "0.14.0", path = "crates/core/config" }
|
||||
revolt-database = { version = "0.14.0", path = "crates/core/database" }
|
||||
revolt-files = { version = "0.14.0", path = "crates/core/files" }
|
||||
revolt-models = { version = "0.14.0", path = "crates/core/models" }
|
||||
revolt-parser = { version = "0.14.0", path = "crates/core/parser" }
|
||||
revolt-permissions = { version = "0.14.0", path = "crates/core/permissions" }
|
||||
revolt-presence = { version = "0.14.0", path = "crates/core/presence" }
|
||||
revolt-ratelimits = { version = "0.14.0", path = "crates/core/ratelimits" }
|
||||
revolt-result = { version = "0.14.0", path = "crates/core/result" }
|
||||
revolt-coalesced = { version = "0.13.7", path = "crates/core/coalesced" }
|
||||
revolt-config = { version = "0.13.7", path = "crates/core/config" }
|
||||
revolt-database = { version = "0.13.7", path = "crates/core/database" }
|
||||
revolt-files = { version = "0.13.7", path = "crates/core/files" }
|
||||
revolt-models = { version = "0.13.7", path = "crates/core/models" }
|
||||
revolt-parser = { version = "0.13.7", path = "crates/core/parser" }
|
||||
revolt-permissions = { version = "0.13.7", path = "crates/core/permissions" }
|
||||
revolt-presence = { version = "0.13.7", path = "crates/core/presence" }
|
||||
revolt-ratelimits = { version = "0.13.7", path = "crates/core/ratelimits" }
|
||||
revolt-result = { version = "0.13.7", path = "crates/core/result" }
|
||||
revolt-search = { version = "0.13.7", path = "crates/core/search" }
|
||||
+4
-1
@@ -4,7 +4,7 @@
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://127.0.0.1:27017"
|
||||
mongodb = "mongodb://127.0.0.1:27017?directConnection=true&replicaSet=rs0"
|
||||
# Redis connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
redis = "redis://127.0.0.1:6379/"
|
||||
@@ -68,3 +68,6 @@ access_key_id = "minioautumn"
|
||||
secret_access_key = "minioautumn"
|
||||
# Bucket to upload to by default
|
||||
default_bucket = "revolt-uploads"
|
||||
|
||||
[elasticsearch]
|
||||
api_key = ""
|
||||
+31
@@ -86,3 +86,34 @@ services:
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./livekit.yml:/etc/livekit.yml
|
||||
|
||||
elasticsearch:
|
||||
image: elasticsearch:9.3.0
|
||||
volumes:
|
||||
- ./.data/elasticsearch:/usr/share/elasticsearch/data
|
||||
ports:
|
||||
- 9200:9200
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
- model-runner.docker.internal:host-gateway
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
- xpack.security.http.ssl.enabled=false
|
||||
- indices.id_field_data.enabled=true
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl --output /dev/null --silent --head --fail http://elasticsearch:9200",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
kibana:
|
||||
image: kibana:9.3.1
|
||||
ports:
|
||||
- 5601:5601
|
||||
environment:
|
||||
ELASTICSEARCH_HOSTS: '["http://elasticsearch:9200"]'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-coalesced"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
environment = "test"
|
||||
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
mongodb = "mongodb://localhost?directConnection=true&replicaSet=rs0"
|
||||
redis = "redis://localhost/"
|
||||
|
||||
[rabbit]
|
||||
|
||||
@@ -5,7 +5,7 @@ environment = "dev"
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://database"
|
||||
mongodb = "mongodb://database?directConnection=true&replicaSet=rs0"
|
||||
# Redis connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
redis = "redis://redis/"
|
||||
@@ -88,10 +88,6 @@ max_concurrent_connections = 50
|
||||
# How long to ring devices for when calling in dms/groups, in seconds
|
||||
call_ring_duration = 30
|
||||
|
||||
[api.audit_logs]
|
||||
# How long audit log entries last before being removed, in seconds
|
||||
expires_after = 2592000 # 30d
|
||||
|
||||
[api.livekit.nodes]
|
||||
|
||||
[api.users]
|
||||
@@ -350,3 +346,14 @@ proxy = ""
|
||||
pushd = ""
|
||||
crond = ""
|
||||
gifbox = ""
|
||||
|
||||
[elasticsearch]
|
||||
host = "http://elasticsearch"
|
||||
port = 9200
|
||||
api_key = ""
|
||||
|
||||
exchange = "revolt.messages"
|
||||
message_queue = "messages.message"
|
||||
message_edit_queue = "messages.message_edit"
|
||||
message_delete_queue = "messages.message_delete"
|
||||
channel_delete_queue = "messages.channel_delete"
|
||||
@@ -260,12 +260,6 @@ pub struct ApiUsers {
|
||||
pub min_username_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiAuditLogs {
|
||||
/// How long audit log entries last before being removed, in seconds
|
||||
pub expires_after: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Api {
|
||||
pub registration: ApiRegistration,
|
||||
@@ -274,7 +268,6 @@ pub struct Api {
|
||||
pub workers: ApiWorkers,
|
||||
pub livekit: ApiLiveKit,
|
||||
pub users: ApiUsers,
|
||||
pub audit_logs: ApiAuditLogs,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -463,6 +456,19 @@ pub struct Sentry {
|
||||
pub gifbox: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Elasticsearch {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub api_key: String,
|
||||
|
||||
pub exchange: String,
|
||||
pub message_queue: String,
|
||||
pub message_edit_queue: String,
|
||||
pub message_delete_queue: String,
|
||||
pub channel_delete_queue: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Settings {
|
||||
pub database: Database,
|
||||
@@ -477,6 +483,7 @@ pub struct Settings {
|
||||
pub production: bool,
|
||||
pub environment: String,
|
||||
pub disable_events_dont_use: bool,
|
||||
pub elasticsearch: Elasticsearch,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -31,7 +31,7 @@ default = ["mongodb", "tokio-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { workspace = true, features = ["report-macros"] }
|
||||
revolt-config = { workspace = true, features = ["report-macros", "anyhow"] }
|
||||
revolt-result = { workspace = true }
|
||||
revolt-models = { workspace = true, features = ["validator"] }
|
||||
revolt-presence = { workspace = true }
|
||||
@@ -56,6 +56,7 @@ validator = { workspace = true, features = ["derive"] }
|
||||
isahc = { workspace = true, features = ["json"], optional = true }
|
||||
base32 = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
# Serialisation
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::events::rabbit::*;
|
||||
use crate::{Message, events::rabbit::*};
|
||||
use crate::User;
|
||||
use lapin::{
|
||||
options::BasicPublishOptions,
|
||||
@@ -25,6 +25,10 @@ pub struct AMQP {
|
||||
ack_notification_message: Arc<Channel>,
|
||||
dm_call_updated: Arc<Channel>,
|
||||
process_ack: Arc<Channel>,
|
||||
message_search: Arc<Channel>,
|
||||
edit_message_search: Arc<Channel>,
|
||||
delete_message_search: Arc<Channel>,
|
||||
delete_channel_search: Arc<Channel>,
|
||||
#[allow(unused)]
|
||||
connection: Arc<Connection>,
|
||||
}
|
||||
@@ -40,6 +44,10 @@ impl AMQP {
|
||||
ack_notification_message: Self::create_channel(&connection).await,
|
||||
dm_call_updated: Self::create_channel(&connection).await,
|
||||
process_ack: Self::create_channel(&connection).await,
|
||||
message_search: Self::create_channel(&connection).await,
|
||||
edit_message_search: Self::create_channel(&connection).await,
|
||||
delete_message_search: Self::create_channel(&connection).await,
|
||||
delete_channel_search: Self::create_channel(&connection).await,
|
||||
connection,
|
||||
}
|
||||
}
|
||||
@@ -379,4 +387,112 @@ impl AMQP {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn new_message_search(
|
||||
&self,
|
||||
message: Message,
|
||||
user: Option<User>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = to_string(&MessageCreatePayload { message, user }).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending new message search payload on channel {}: {}",
|
||||
config.elasticsearch.message_queue, payload
|
||||
);
|
||||
|
||||
self.message_search
|
||||
.basic_publish(
|
||||
config.elasticsearch.exchange.clone().into(),
|
||||
config.elasticsearch.message_queue.into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn edit_message_search(
|
||||
&self,
|
||||
message: Message,
|
||||
user: Option<User>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = to_string(&MessageEditPayload { message, user }).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending edit message search payload on channel {}: {}",
|
||||
config.elasticsearch.message_edit_queue, payload
|
||||
);
|
||||
|
||||
self.edit_message_search
|
||||
.basic_publish(
|
||||
config.elasticsearch.exchange.clone().into(),
|
||||
config.elasticsearch.message_edit_queue.into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_message_search(&self, message_id: String) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = to_string(&MessageDeletePayload { message_id }).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending delete message search payload on channel {}: {}",
|
||||
config.elasticsearch.message_delete_queue, payload
|
||||
);
|
||||
|
||||
self.delete_message_search
|
||||
.basic_publish(
|
||||
config.elasticsearch.exchange.clone().into(),
|
||||
config.elasticsearch.message_delete_queue.into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_channel_search(&self, channel_id: String) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = to_string(&ChannelDeletePayload { channel_id }).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending delete channel search payload on channel {}: {}",
|
||||
config.elasticsearch.channel_delete_queue, payload
|
||||
);
|
||||
|
||||
self.delete_channel_search
|
||||
.basic_publish(
|
||||
config.elasticsearch.exchange.clone().into(),
|
||||
config.elasticsearch.channel_delete_queue.into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+12
-7
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
future::{ready, Future},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
future::{Future, ready}, marker::PhantomData, pin::Pin, sync::Arc
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -12,14 +10,15 @@ use lapin::{
|
||||
BasicProperties, Channel, Connection, ConsumerDelegate, Error as AMQPError,
|
||||
};
|
||||
use log::debug;
|
||||
use revolt_database::Database;
|
||||
use crate::Database;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Consumer: Clone + Send + Sync + 'static {
|
||||
pub trait Consumer<T: Clone = ()>: Clone + Send + Sync + 'static {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
data: T,
|
||||
) -> Self;
|
||||
fn channel(&self) -> &Arc<Channel>;
|
||||
async fn consume(&self, delivery: Delivery) -> Result<()>;
|
||||
@@ -65,9 +64,15 @@ pub trait Consumer: Clone + Send + Sync + 'static {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Delegate<C: Consumer>(pub C);
|
||||
pub struct Delegate<C: Consumer<D>, D: Clone>(C, PhantomData<D>);
|
||||
|
||||
impl<C: Consumer> ConsumerDelegate for Delegate<C> {
|
||||
impl<C: Consumer<D>, D: Clone> Delegate<C, D> {
|
||||
pub fn new(consumer: C) -> Self {
|
||||
Self(consumer, PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Consumer<D>, D: Clone + Send + Sync> ConsumerDelegate for Delegate<C, D> {
|
||||
fn on_new_delivery(
|
||||
&self,
|
||||
delivery: DeliveryResult,
|
||||
@@ -1,2 +1,3 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod amqp;
|
||||
pub mod consumer;
|
||||
@@ -3,16 +3,15 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Account, AccountInvite, AuditLogEntry, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji,
|
||||
File, FileHash, Invite, MFATicket, Member, MemberCompositeKey, Message, PolicyChange,
|
||||
RatelimitEvent, Report, Server, ServerBan, Session, Snapshot, User, UserSettings, Webhook,
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
|
||||
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
|
||||
User, UserSettings, Webhook, Account, AccountInvite, Session, MFATicket
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ReferenceDb {
|
||||
pub audit_logs: Arc<Mutex<HashMap<String, AuditLogEntry>>>,
|
||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::HashMap;
|
||||
use revolt_models::v0::PushNotification;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::User;
|
||||
use crate::{Message, User};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MessageSentPayload {
|
||||
@@ -79,6 +79,28 @@ pub struct AckPayload {
|
||||
pub message_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageDeletePayload {
|
||||
pub message_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChannelDeletePayload {
|
||||
pub channel_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageCreatePayload {
|
||||
pub message: Message,
|
||||
pub user: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageEditPayload {
|
||||
pub message: Message,
|
||||
pub user: Option<User>,
|
||||
}
|
||||
|
||||
/// This is not the same as the AckPayload above, as the state for this event is stored in redis to allow for state updates while the event is queued.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AckEventPayload {
|
||||
|
||||
@@ -77,78 +77,6 @@ macro_rules! auto_derived_partial {
|
||||
};
|
||||
}
|
||||
|
||||
/// Internal macro for `generate_diff!`, you should not need to use this yourself.
|
||||
macro_rules! generate_field_diff {
|
||||
(optional, $remove:ident, $fieldsmember:path, $self:ident, $before:ident, $partial:ident, $field:ident) => {
|
||||
if $partial.$field.is_some() || $remove.contains(&$fieldsmember) {
|
||||
$before.$field = $self.$field.clone();
|
||||
};
|
||||
};
|
||||
|
||||
(optional, default, $remove:ident, $fieldsmember:path, $self:ident, $before:ident, $partial:ident, $field:ident) => {
|
||||
if $partial.$field.is_some() || $remove.contains(&$fieldsmember) {
|
||||
$before.$field = Some($self.$field.clone());
|
||||
};
|
||||
};
|
||||
|
||||
($self:ident, $before:ident, $partial:ident, $field:ident) => {
|
||||
if $partial.$field.is_some() {
|
||||
$before.$field = Some($self.$field.clone());
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a partial model containing the data which has changed in an update
|
||||
///
|
||||
/// ## Usage:
|
||||
/// `before` is the "output" containing what the model had before being updated,
|
||||
/// this will corraspond to `partial` which is what the data is being changed too.
|
||||
///
|
||||
/// ```rs
|
||||
/// let mut before = PartialModel::default();
|
||||
///
|
||||
/// generate_diff!(
|
||||
/// self, // database model
|
||||
/// before, // mutable empty partial corrasponding to the current model
|
||||
/// partial, // partial containing what is being updated
|
||||
/// remove, // slice of fields being removed
|
||||
/// (
|
||||
/// name, // regular non-nullable non-removable field
|
||||
/// (FieldsEnum::Nickname) nickname, // optional removable field
|
||||
/// ((default) FieldsEnum::Roles) roles, // optional removable field with custom default
|
||||
/// )
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// See `Member::generate_diff` `Server::generate_diff` `Role::generate_diff` for full examples
|
||||
macro_rules! generate_diff {
|
||||
(
|
||||
$self:ident,
|
||||
$before:ident,
|
||||
$partial:ident,
|
||||
$remove:ident,
|
||||
(
|
||||
$(
|
||||
$(
|
||||
$(@$optional:tt)? (
|
||||
$($(@$default:tt)? (default))?
|
||||
$fieldsmember:path
|
||||
)
|
||||
)?
|
||||
$field: ident
|
||||
),*
|
||||
$(,)?
|
||||
)
|
||||
) => {
|
||||
$(
|
||||
generate_field_diff!(
|
||||
$( $($optional)? optional, $($($default)? default,)? $remove, $fieldsmember,)?
|
||||
$self, $before, $partial, $field
|
||||
);
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
mod drivers;
|
||||
pub use drivers::*;
|
||||
|
||||
@@ -181,12 +109,13 @@ pub mod events;
|
||||
#[cfg(feature = "tasks")]
|
||||
pub mod tasks;
|
||||
|
||||
mod amqp;
|
||||
pub mod amqp;
|
||||
pub use amqp::amqp::AMQP;
|
||||
|
||||
#[cfg(feature = "voice")]
|
||||
pub mod voice;
|
||||
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
|
||||
@@ -98,9 +98,6 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create pubsub collection.");
|
||||
|
||||
db.create_collection("audit_logs")
|
||||
.await
|
||||
.expect("Failed to create audit_logs collection");
|
||||
db.create_collection("sessions")
|
||||
.await
|
||||
.expect("Failed to create sessions collection.");
|
||||
@@ -278,58 +275,6 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "audit_logs",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"expires_at": 1_i32,
|
||||
},
|
||||
"name": "expires_at_ttl",
|
||||
// We set the expire after to 0 because we store when it expires instead of when the document was inserted,
|
||||
// this is because mongo cant read the timestamp from the ulid so we need to do this workaround.
|
||||
// relevant docs: https://www.mongodb.com/docs/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time
|
||||
"expireAfterSeconds": 0
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"server": 1_i32,
|
||||
"user": 1_i32,
|
||||
"action.type": 1_i32,
|
||||
},
|
||||
"name": "audit_log_filters",
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create audit_logs index");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "audit_logs",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"expires_at": 1_i32,
|
||||
},
|
||||
"name": "expires_at_ttl",
|
||||
// We set the expire after to 0 because we store when it expires instead of when the document was inserted,
|
||||
// this is because mongo cant read the timestamp from the ulid so we need to do this workaround.
|
||||
// relevant docs: https://www.mongodb.com/docs/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time
|
||||
"expireAfterSeconds": 0
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"server": 1_i32,
|
||||
"user": 1_i32,
|
||||
"action.type": 1_i32,
|
||||
},
|
||||
"name": "audit_log_filters",
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create audit_logs index");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "accounts",
|
||||
"indexes": [
|
||||
|
||||
@@ -26,7 +26,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 52; // MUST BE +1 to last migration
|
||||
pub const LATEST_REVISION: i32 = 51; // MUST BE +1 to last migration
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -1478,56 +1478,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
if revision <= 50 {
|
||||
info!("Running migration [revision 50 / 13-04-2026]: Rename invites collection to account_invites");
|
||||
|
||||
let result = db.db()
|
||||
db.db()
|
||||
.client()
|
||||
.database("admin")
|
||||
.run_command(doc! {
|
||||
"renameCollection": "revolt.invites",
|
||||
"to": "revolt.account_invites",
|
||||
"dropTarget": true
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
// NamespaceNotFound (26) = source collection doesn't exist, safe to ignore
|
||||
if !matches!(e.kind.as_ref(), mongodb::error::ErrorKind::Command(ce) if ce.code == 26) {
|
||||
panic!("Failed to rename invites collection: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if revision >= 51 {
|
||||
info!("Running migration [revision 51 / 28-11-2025]: Add audit logs collection");
|
||||
|
||||
db.db()
|
||||
.create_collection("audit_logs")
|
||||
.await
|
||||
.expect("Failed to create audit_logs collection");
|
||||
|
||||
db.db()
|
||||
.run_command(doc! {
|
||||
"createIndexes": "audit_logs",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"expires_at": 1_i32,
|
||||
},
|
||||
"name": "expires_at_ttl",
|
||||
"expireAfterSeconds": 0
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"server": 1_i32,
|
||||
"user": 1_i32,
|
||||
"action.type": 1_i32,
|
||||
},
|
||||
"name": "audit_log_filters",
|
||||
},
|
||||
]
|
||||
"renameCollection": "revolt.invites",
|
||||
"to": "revolt.account_invites",
|
||||
"dropTarget": true
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create audit_logs index");
|
||||
};
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
LATEST_REVISION.max(revision)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -1,275 +0,0 @@
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::config;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{Database, PartialChannel, PartialMember, PartialRole, PartialServer, User, PartialEmoji};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
auto_derived!(
|
||||
/// Audit log entry
|
||||
pub struct AuditLogEntry {
|
||||
/// Unique ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// When the audit log entry gets auto-deleted
|
||||
///
|
||||
/// This is only stored in the database and not given to users.
|
||||
pub expires_at: Timestamp,
|
||||
|
||||
/// The server the entry happened in
|
||||
pub server: String,
|
||||
/// User provided reason
|
||||
pub reason: Option<String>,
|
||||
/// User who ran the action
|
||||
pub user: String,
|
||||
/// User this action is targetting
|
||||
pub target: Option<String>,
|
||||
/// The action ran
|
||||
pub action: AuditLogEntryAction,
|
||||
}
|
||||
|
||||
/// Indivual audit log action
|
||||
#[serde(tag = "type")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum AuditLogEntryAction {
|
||||
MessageDelete {
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
MessageBulkDelete {
|
||||
channel: String,
|
||||
count: usize,
|
||||
},
|
||||
MessagePin {
|
||||
message: String,
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
MessageUnpin {
|
||||
message: String,
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
BanCreate {
|
||||
user: String,
|
||||
},
|
||||
BanDelete {
|
||||
user: String,
|
||||
},
|
||||
ChannelCreate {
|
||||
channel: String,
|
||||
name: String,
|
||||
},
|
||||
ChannelEdit {
|
||||
channel: String,
|
||||
before: PartialChannel,
|
||||
after: PartialChannel,
|
||||
},
|
||||
ChannelRolePermissionsEdit {
|
||||
channel: String,
|
||||
role: String,
|
||||
permissions: OverrideField,
|
||||
},
|
||||
ChannelDelete {
|
||||
channel: String,
|
||||
name: String,
|
||||
},
|
||||
MemberEdit {
|
||||
user: String,
|
||||
before: PartialMember,
|
||||
after: PartialMember,
|
||||
},
|
||||
MemberKick {
|
||||
user: String,
|
||||
},
|
||||
ServerEdit {
|
||||
before: PartialServer,
|
||||
after: PartialServer,
|
||||
},
|
||||
RoleEdit {
|
||||
role: String,
|
||||
before: PartialRole,
|
||||
after: PartialRole,
|
||||
},
|
||||
RoleCreate {
|
||||
role: String,
|
||||
name: String,
|
||||
},
|
||||
RoleDelete {
|
||||
role: String,
|
||||
name: String,
|
||||
},
|
||||
RolesReorder {
|
||||
before: Vec<String>,
|
||||
after: Vec<String>,
|
||||
},
|
||||
InviteCreate {
|
||||
invite: String,
|
||||
channel: String,
|
||||
},
|
||||
InviteDelete {
|
||||
invite: String,
|
||||
channel: String,
|
||||
},
|
||||
WebhookCreate {
|
||||
webhook: String,
|
||||
name: String,
|
||||
channel: String,
|
||||
},
|
||||
WebhookDelete {
|
||||
webhook: String,
|
||||
name: String,
|
||||
channel: String,
|
||||
},
|
||||
EmojiCreate {
|
||||
emoji: String,
|
||||
name: String,
|
||||
},
|
||||
EmojiUpdate {
|
||||
emoji: String,
|
||||
before: PartialEmoji,
|
||||
after: PartialEmoji,
|
||||
},
|
||||
EmojiDelete {
|
||||
emoji: String,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Audit Log Query
|
||||
pub struct AuditLogQuery {
|
||||
/// Filter by who ran the action
|
||||
pub user: Option<String>,
|
||||
/// Filter by who the action is targetting
|
||||
pub target: Option<String>,
|
||||
/// Filter by the action type
|
||||
pub r#type: Option<Vec<String>>,
|
||||
/// Entries before a certain entry id
|
||||
pub before: Option<String>,
|
||||
/// Entries after a certain entry id
|
||||
pub after: Option<String>,
|
||||
/// Maximum number of entries to fetch
|
||||
pub limit: i64,
|
||||
}
|
||||
);
|
||||
|
||||
impl AuditLogEntryAction {
|
||||
// TODO: migrate this to a rabbitmq queue to avoid spawning lots of tasks
|
||||
/// Generates an `AuditLogEntry` for the current action and inserts it into the database
|
||||
pub async fn insert<R: Into<Option<String>>>(
|
||||
self,
|
||||
db: &Database,
|
||||
server: String,
|
||||
reason: R,
|
||||
user: String,
|
||||
target: Option<String>,
|
||||
) -> AuditLogEntry {
|
||||
let config = config().await;
|
||||
|
||||
let id = Ulid::new();
|
||||
let expires_at = id
|
||||
.datetime()
|
||||
.checked_add(Duration::from_secs(config.api.audit_logs.expires_after))
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
let entry = AuditLogEntry {
|
||||
id: id.to_string(),
|
||||
expires_at,
|
||||
server,
|
||||
reason: reason.into(),
|
||||
user,
|
||||
target,
|
||||
action: self,
|
||||
};
|
||||
|
||||
// running the insert inside a task can cause race conditions in the test so for now just dont use a task for tests for now
|
||||
// this will need to be redone for when we migrate to using rabbitmq here anyway.
|
||||
#[cfg(not(test))]
|
||||
tokio::task::spawn({
|
||||
let db = db.clone();
|
||||
let entry = entry.clone();
|
||||
|
||||
async move { revolt_config::report_internal_error!(db.insert_audit_log_entry(&entry).await) }
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
db.insert_audit_log_entry(&entry).await.unwrap();
|
||||
|
||||
entry
|
||||
}
|
||||
}
|
||||
|
||||
impl AuditLogEntry {
|
||||
/// Fetches the corrasponding users and members for each audit log entry
|
||||
pub async fn with_users(
|
||||
db: &Database,
|
||||
server_id: &str,
|
||||
user: &User,
|
||||
entries: &[Self],
|
||||
) -> Result<(Vec<v0::User>, Vec<v0::Member>)> {
|
||||
let mut user_ids = HashSet::new();
|
||||
|
||||
for entry in entries {
|
||||
user_ids.insert(entry.user.clone());
|
||||
|
||||
match &entry.action {
|
||||
AuditLogEntryAction::MessageDelete { author, .. } => {
|
||||
user_ids.insert(author.clone());
|
||||
}
|
||||
AuditLogEntryAction::BanCreate { user } => {
|
||||
user_ids.insert(user.clone());
|
||||
}
|
||||
AuditLogEntryAction::BanDelete { user } => {
|
||||
user_ids.insert(user.clone());
|
||||
}
|
||||
AuditLogEntryAction::ChannelCreate { .. } => {}
|
||||
AuditLogEntryAction::MemberEdit { user, .. } => {
|
||||
user_ids.insert(user.clone());
|
||||
}
|
||||
AuditLogEntryAction::MemberKick { user } => {
|
||||
user_ids.insert(user.clone());
|
||||
}
|
||||
AuditLogEntryAction::MessagePin { author, .. } => {
|
||||
user_ids.insert(author.clone());
|
||||
}
|
||||
AuditLogEntryAction::MessageUnpin { author, .. } => {
|
||||
user_ids.insert(author.clone());
|
||||
}
|
||||
AuditLogEntryAction::ServerEdit { .. } => {}
|
||||
AuditLogEntryAction::RoleEdit { .. } => {}
|
||||
AuditLogEntryAction::RoleCreate { .. } => {}
|
||||
AuditLogEntryAction::RoleDelete { .. } => {}
|
||||
AuditLogEntryAction::RolesReorder { .. } => {}
|
||||
AuditLogEntryAction::MessageBulkDelete { .. } => {}
|
||||
AuditLogEntryAction::ChannelEdit { .. } => {}
|
||||
AuditLogEntryAction::ChannelRolePermissionsEdit { .. } => {}
|
||||
AuditLogEntryAction::ChannelDelete { .. } => {}
|
||||
AuditLogEntryAction::InviteCreate { .. } => {}
|
||||
AuditLogEntryAction::InviteDelete { .. } => {}
|
||||
AuditLogEntryAction::WebhookCreate { .. } => {}
|
||||
AuditLogEntryAction::WebhookDelete { .. } => {}
|
||||
AuditLogEntryAction::EmojiCreate { .. } => {}
|
||||
AuditLogEntryAction::EmojiUpdate { .. } => {}
|
||||
AuditLogEntryAction::EmojiDelete { .. } => {}
|
||||
};
|
||||
}
|
||||
|
||||
let user_ids = user_ids.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let users = User::fetch_many_ids_as_mutuals(db, user, &user_ids).await?;
|
||||
let members = db
|
||||
.fetch_members(server_id, &user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok((users, members))
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AuditLogEntry, AuditLogQuery};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAuditLogs: Sync + Send {
|
||||
/// Inserts an entry into the server's audit log
|
||||
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()>;
|
||||
|
||||
/// Fetches a server's audit logs using the provided query options
|
||||
async fn get_server_audit_logs(
|
||||
&self,
|
||||
server: &str,
|
||||
query: AuditLogQuery,
|
||||
) -> Result<Vec<AuditLogEntry>>;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use mongodb::options::FindOptions;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AuditLogEntry, AuditLogQuery, MongoDb};
|
||||
|
||||
use super::AbstractAuditLogs;
|
||||
|
||||
static COL: &str = "audit_logs";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAuditLogs for MongoDb {
|
||||
/// Inserts an entry into the server's audit log
|
||||
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()> {
|
||||
query!(self, insert_one, COL, entry).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetches a server's audit logs using the provided query options
|
||||
async fn get_server_audit_logs(
|
||||
&self,
|
||||
server: &str,
|
||||
query: AuditLogQuery,
|
||||
) -> Result<Vec<AuditLogEntry>> {
|
||||
let mut filter = doc! {
|
||||
"server": server
|
||||
};
|
||||
|
||||
if let Some(user) = query.user {
|
||||
filter.insert("user", user);
|
||||
};
|
||||
|
||||
if let Some(target) = query.target {
|
||||
filter.insert("target", target);
|
||||
}
|
||||
|
||||
if let Some(types) = query.r#type {
|
||||
filter.insert("action.type", doc! { "$in": types });
|
||||
};
|
||||
|
||||
if let Some(doc) = match (query.before, query.after) {
|
||||
(Some(before), Some(after)) => Some(doc! {
|
||||
"$lt": before,
|
||||
"$gt": after
|
||||
}),
|
||||
(Some(before), _) => Some(doc! {
|
||||
"$lt": before
|
||||
}),
|
||||
(_, Some(after)) => Some(doc! {
|
||||
"$gt": after
|
||||
}),
|
||||
_ => None,
|
||||
} {
|
||||
filter.insert("_id", doc);
|
||||
};
|
||||
|
||||
self.find_with_options(
|
||||
COL,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.limit(query.limit)
|
||||
.sort(doc! { "_id": -1 })
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AuditLogEntry, AuditLogQuery, ReferenceDb};
|
||||
|
||||
use super::AbstractAuditLogs;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAuditLogs for ReferenceDb {
|
||||
/// Inserts an entry into the server's audit log
|
||||
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()> {
|
||||
self.audit_logs
|
||||
.lock()
|
||||
.await
|
||||
.insert(entry.id.clone(), entry.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetches a server's audit logs using the provided query options
|
||||
async fn get_server_audit_logs(
|
||||
&self,
|
||||
server: &str,
|
||||
query: AuditLogQuery,
|
||||
) -> Result<Vec<AuditLogEntry>> {
|
||||
let lock = self.audit_logs.lock().await;
|
||||
|
||||
let mut logs = lock
|
||||
.values()
|
||||
.filter(|entry| {
|
||||
if entry.server != server {
|
||||
return false;
|
||||
};
|
||||
|
||||
if let Some(user) = &query.user {
|
||||
if &entry.user != user {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if query.target.is_some() && entry.target != query.target {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(before) = &query.before {
|
||||
if &entry.id > before {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
if let Some(after) = &query.after {
|
||||
if &entry.id < after {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
if let Some(action_types) = &query.r#type {
|
||||
let entry_type = serde_json::to_value(entry.action.clone())
|
||||
.unwrap()
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("type")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
if !action_types.contains(&entry_type) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
logs.sort_by(|a, b| b.id.cmp(&a.id));
|
||||
logs.truncate(query.limit as usize);
|
||||
Ok(logs)
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,6 @@ auto_derived!(
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
Voice,
|
||||
Slowmode,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -341,7 +340,7 @@ impl Channel {
|
||||
pub async fn add_user_to_group(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
amqp: &AMQP,
|
||||
amqp: Option<&AMQP>,
|
||||
user: &User,
|
||||
by_id: &str,
|
||||
) -> Result<()> {
|
||||
@@ -378,7 +377,7 @@ impl Channel {
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
amqp,
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
@@ -555,12 +554,6 @@ impl Channel {
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Slowmode => match self {
|
||||
Self::TextChannel { slowmode, .. } => {
|
||||
slowmode.take();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,122 +648,6 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialChannel containing the data which has changed in an update
|
||||
pub fn generate_diff(
|
||||
&self,
|
||||
partial: &PartialChannel,
|
||||
remove: &[FieldsChannel],
|
||||
) -> PartialChannel {
|
||||
let mut before = PartialChannel::default();
|
||||
|
||||
match self {
|
||||
Channel::SavedMessages { .. } => {}
|
||||
Channel::DirectMessage {
|
||||
active,
|
||||
last_message_id,
|
||||
..
|
||||
} => {
|
||||
if partial.active.is_some() {
|
||||
before.active = Some(*active);
|
||||
};
|
||||
|
||||
if partial.last_message_id.is_some() {
|
||||
before.last_message_id = last_message_id.clone()
|
||||
};
|
||||
}
|
||||
Channel::Group {
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
icon,
|
||||
last_message_id,
|
||||
permissions,
|
||||
nsfw,
|
||||
..
|
||||
} => {
|
||||
if partial.name.is_some() {
|
||||
before.name = Some(name.clone());
|
||||
};
|
||||
|
||||
if partial.owner.is_some() {
|
||||
before.owner = Some(owner.clone());
|
||||
};
|
||||
|
||||
if partial.description.is_some() || remove.contains(&FieldsChannel::Description) {
|
||||
before.description = description.clone();
|
||||
};
|
||||
|
||||
if partial.icon.is_some() || remove.contains(&FieldsChannel::Icon) {
|
||||
before.icon = icon.clone();
|
||||
};
|
||||
|
||||
if partial.last_message_id.is_some() {
|
||||
before.last_message_id = last_message_id.clone()
|
||||
};
|
||||
|
||||
if partial.permissions.is_some() {
|
||||
before.permissions = *permissions;
|
||||
};
|
||||
|
||||
if partial.nsfw.is_some() {
|
||||
before.nsfw = Some(*nsfw);
|
||||
};
|
||||
}
|
||||
Channel::TextChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
last_message_id,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
voice,
|
||||
slowmode,
|
||||
..
|
||||
} => {
|
||||
if partial.name.is_some() {
|
||||
before.name = Some(name.clone());
|
||||
};
|
||||
|
||||
if partial.description.is_some() || remove.contains(&FieldsChannel::Description) {
|
||||
before.description = description.clone();
|
||||
};
|
||||
|
||||
if partial.icon.is_some() || remove.contains(&FieldsChannel::Icon) {
|
||||
before.icon = icon.clone();
|
||||
};
|
||||
|
||||
if partial.last_message_id.is_some() {
|
||||
before.last_message_id = last_message_id.clone()
|
||||
};
|
||||
|
||||
if partial.default_permissions.is_some()
|
||||
|| remove.contains(&FieldsChannel::DefaultPermissions)
|
||||
{
|
||||
before.default_permissions = *default_permissions;
|
||||
};
|
||||
|
||||
if partial.role_permissions.is_some() {
|
||||
before.role_permissions = Some(role_permissions.clone());
|
||||
};
|
||||
|
||||
if partial.nsfw.is_some() {
|
||||
before.nsfw = Some(*nsfw);
|
||||
};
|
||||
|
||||
if partial.voice.is_some() || remove.contains(&FieldsChannel::Voice) {
|
||||
before.voice = voice.clone();
|
||||
};
|
||||
|
||||
if partial.slowmode.is_some() {
|
||||
before.slowmode = *slowmode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
before
|
||||
}
|
||||
|
||||
/// Acknowledge a message
|
||||
pub async fn ack(&self, user: &str, message: &str, amqp: &AMQP) -> Result<()> {
|
||||
EventV1::ChannelAck {
|
||||
@@ -788,7 +665,7 @@ impl Channel {
|
||||
pub async fn remove_user_from_group(
|
||||
&self,
|
||||
db: &Database,
|
||||
amqp: &AMQP,
|
||||
amqp: Option<&AMQP>,
|
||||
user: &User,
|
||||
by_id: Option<&str>,
|
||||
silent: bool,
|
||||
@@ -820,7 +697,7 @@ impl Channel {
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
amqp,
|
||||
MessageAuthor::System {
|
||||
username: name,
|
||||
avatar: None,
|
||||
@@ -833,7 +710,7 @@ impl Channel {
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
return self.delete(db).await;
|
||||
return self.delete(db, amqp).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,7 +737,7 @@ impl Channel {
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
amqp,
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
@@ -882,13 +759,22 @@ impl Channel {
|
||||
}
|
||||
|
||||
/// Delete a channel
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
pub async fn delete(&self, db: &Database, amqp: Option<&AMQP>) -> Result<()> {
|
||||
let id = self.id().to_string();
|
||||
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
|
||||
// TODO: missing functionality:
|
||||
// - group invites
|
||||
// - channels list / categories list on server
|
||||
db.delete_channel(self).await
|
||||
db.delete_channel(self).await?;
|
||||
|
||||
if let Some(amqp) = amqp {
|
||||
if let Err(e) = amqp.delete_channel_search(self.id().to_string()).await {
|
||||
log::error!("Error pushing message to RabbitMQ: {e}");
|
||||
revolt_config::capture_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,7 +786,6 @@ impl IntoDocumentPath for FieldsChannel {
|
||||
FieldsChannel::Icon => "icon",
|
||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
||||
FieldsChannel::Voice => "voice",
|
||||
FieldsChannel::Slowmode => "slowmode",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
.collect()
|
||||
});
|
||||
|
||||
auto_derived_partial!(
|
||||
auto_derived!(
|
||||
/// Emoji
|
||||
pub struct Emoji {
|
||||
/// Unique Id
|
||||
@@ -34,17 +34,20 @@ auto_derived_partial!(
|
||||
/// Whether the emoji is marked as nsfw
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub nsfw: bool,
|
||||
},
|
||||
"PartialEmoji"
|
||||
);
|
||||
}
|
||||
|
||||
auto_derived!(
|
||||
/// Parent Id of the emoji
|
||||
#[serde(tag = "type")]
|
||||
pub enum EmojiParent {
|
||||
Server { id: String },
|
||||
Detached,
|
||||
}
|
||||
|
||||
/// Partial representation of an emoji
|
||||
pub struct PartialEmoji {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
@@ -69,14 +72,14 @@ impl Emoji {
|
||||
}
|
||||
|
||||
/// Delete an emoji
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
EventV1::EmojiDelete {
|
||||
id: self.id.to_string(),
|
||||
}
|
||||
.p(self.parent().to_string())
|
||||
.await;
|
||||
|
||||
db.detach_emoji(self).await
|
||||
db.detach_emoji(&self).await
|
||||
}
|
||||
|
||||
/// Update an emoji
|
||||
@@ -109,18 +112,4 @@ impl Emoji {
|
||||
Ok(PERMISSIBLE_EMOJIS.contains(&sanitized_emoji))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialEmoji containing the data which has changed in an update
|
||||
pub fn generate_diff(&self, partial: &PartialEmoji) -> PartialEmoji {
|
||||
let mut before = PartialEmoji::default();
|
||||
|
||||
generate_diff!(
|
||||
self, before, partial, remove,
|
||||
(
|
||||
name,
|
||||
)
|
||||
);
|
||||
|
||||
before
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_config::{capture_error, config, FeaturesLimits};
|
||||
use revolt_models::v0::{
|
||||
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
|
||||
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text,
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
|
||||
permissions::DatabasePermissionQuery,
|
||||
},
|
||||
Channel, Database, Emoji, File, User, AMQP,
|
||||
Channel, Database, Emoji, File, Member, User, AMQP,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tasks")]
|
||||
@@ -210,6 +210,13 @@ auto_derived!(
|
||||
pub enum FieldsMessage {
|
||||
Pinned,
|
||||
}
|
||||
|
||||
/// Message along with the user for the author
|
||||
pub struct MessageWithUser {
|
||||
#[serde(flatten)]
|
||||
pub message: Message,
|
||||
pub user: Option<User>,
|
||||
}
|
||||
);
|
||||
|
||||
pub struct MessageFlagsValue(pub u32);
|
||||
@@ -272,8 +279,8 @@ impl Message {
|
||||
channel: Channel,
|
||||
data: DataMessageSend,
|
||||
author: MessageAuthor<'_>,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
user: Option<User>,
|
||||
member: Option<Member>,
|
||||
limits: FeaturesLimits,
|
||||
mut idempotency: IdempotencyKey,
|
||||
generate_embeds: bool,
|
||||
@@ -610,10 +617,10 @@ impl Message {
|
||||
|
||||
/// Send a message without any notifications
|
||||
pub async fn send_without_notifications(
|
||||
&mut self,
|
||||
&self,
|
||||
db: &Database,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
user: Option<User>,
|
||||
member: Option<Member>,
|
||||
is_dm: bool,
|
||||
generate_embeds: bool,
|
||||
// This determines if this function should queue the mentions task or if somewhere else will.
|
||||
@@ -623,9 +630,18 @@ impl Message {
|
||||
db.insert_message(self).await?;
|
||||
|
||||
// Fan out events
|
||||
EventV1::Message(self.clone().into_model(user, member))
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
EventV1::Message(self.clone().into_model(
|
||||
match user {
|
||||
Some(user) => {
|
||||
let is_online = revolt_presence::is_online(&user.id).await;
|
||||
Some(user.into_known_static(is_online).await)
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
member.map(Into::into),
|
||||
))
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Update last_message_id
|
||||
#[cfg(feature = "tasks")]
|
||||
@@ -671,10 +687,10 @@ impl Message {
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
_amqp: Option<&AMQP>, // this is optional mostly for tests.
|
||||
amqp: Option<&AMQP>, // this is optional mostly for tests.
|
||||
author: MessageAuthor<'_>,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
user: Option<User>,
|
||||
member: Option<Member>,
|
||||
channel: &Channel,
|
||||
generate_embeds: bool,
|
||||
) -> Result<()> {
|
||||
@@ -704,7 +720,17 @@ impl Message {
|
||||
messages: vec![(
|
||||
Some(
|
||||
PushNotification::from(
|
||||
self.clone().into_model(user, member),
|
||||
self.clone().into_model(
|
||||
match user.clone() {
|
||||
Some(user) => {
|
||||
let is_online =
|
||||
revolt_presence::is_online(&user.id).await;
|
||||
Some(user.into_known_static(is_online).await)
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
member.map(Into::into),
|
||||
),
|
||||
Some(author.clone()),
|
||||
channel.to_owned().into(),
|
||||
)
|
||||
@@ -730,6 +756,13 @@ impl Message {
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(amqp) = amqp {
|
||||
if let Err(e) = amqp.new_message_search(self.clone(), user).await {
|
||||
log::error!("Error pushing message to RabbitMQ: {e}");
|
||||
capture_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -782,6 +815,7 @@ impl Message {
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
amqp: Option<&AMQP>,
|
||||
partial: PartialMessage,
|
||||
remove: Vec<FieldsMessage>,
|
||||
) -> Result<()> {
|
||||
@@ -803,9 +837,74 @@ impl Message {
|
||||
.p(self.channel.clone())
|
||||
.await;
|
||||
|
||||
if let Some(amqp) = amqp {
|
||||
if let Err(e) = amqp
|
||||
.edit_message_search(self.clone(), self.fetch_author(db).await)
|
||||
.await
|
||||
{
|
||||
log::error!("Error pushing message to RabbitMQ: {e}");
|
||||
capture_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_users(
|
||||
db: &Database,
|
||||
messages: &[Message],
|
||||
server_id: Option<&str>,
|
||||
) -> Result<(Vec<User>, Vec<Member>)> {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.flat_map(|m| {
|
||||
let mut users = vec![m.author.clone()];
|
||||
if let Some(system) = &m.system {
|
||||
match system {
|
||||
SystemMessage::ChannelDescriptionChanged { by } => users.push(by.clone()),
|
||||
SystemMessage::ChannelIconChanged { by } => users.push(by.clone()),
|
||||
SystemMessage::ChannelOwnershipChanged { from, to, .. } => {
|
||||
users.push(from.clone());
|
||||
users.push(to.clone())
|
||||
}
|
||||
SystemMessage::ChannelRenamed { by, .. } => users.push(by.clone()),
|
||||
SystemMessage::UserAdded { by, id, .. }
|
||||
| SystemMessage::UserRemove { by, id, .. } => {
|
||||
users.push(by.clone());
|
||||
users.push(id.clone());
|
||||
}
|
||||
SystemMessage::UserBanned { id, .. }
|
||||
| SystemMessage::UserKicked { id, .. }
|
||||
| SystemMessage::UserJoined { id, .. }
|
||||
| SystemMessage::UserLeft { id, .. } => {
|
||||
users.push(id.clone());
|
||||
}
|
||||
SystemMessage::Text { .. } => {}
|
||||
SystemMessage::MessagePinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
SystemMessage::MessageUnpinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
SystemMessage::CallStarted { by, .. } => users.push(by.clone()),
|
||||
}
|
||||
}
|
||||
users
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let users = db.fetch_users(&user_ids).await?;
|
||||
let members = if let Some(server_id) = server_id {
|
||||
db.fetch_members(server_id, &user_ids).await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Ok((users, members))
|
||||
}
|
||||
|
||||
/// Helper function to fetch many messages with users
|
||||
pub async fn fetch_with_users(
|
||||
db: &Database,
|
||||
@@ -814,93 +913,55 @@ impl Message {
|
||||
include_users: Option<bool>,
|
||||
server_id: Option<&str>,
|
||||
) -> Result<BulkMessageResponse> {
|
||||
let messages: Vec<v0::Message> = db
|
||||
.fetch_messages(query)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|msg| msg.into_model(None, None))
|
||||
.collect();
|
||||
let messages = db.fetch_messages(query).await?;
|
||||
|
||||
if let Some(true) = include_users {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.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());
|
||||
}
|
||||
v0::SystemMessage::CallStarted { by, .. } => users.push(by.clone()),
|
||||
}
|
||||
}
|
||||
users
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
let users = User::fetch_many_ids_as_mutuals(db, perspective, &user_ids).await?;
|
||||
let (users, members) = Message::fetch_users(db, &messages, server_id).await?;
|
||||
|
||||
Ok(BulkMessageResponse::MessagesAndUsers {
|
||||
messages,
|
||||
users,
|
||||
members: if let Some(server_id) = server_id {
|
||||
Some(
|
||||
db.fetch_members(server_id, &user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
messages: messages
|
||||
.into_iter()
|
||||
.map(|msg| msg.into_model(None, None))
|
||||
.collect(),
|
||||
users: User::into_mutuals(perspective, users).await,
|
||||
members: Some(members.into_iter().map(Into::into).collect()),
|
||||
})
|
||||
} else {
|
||||
Ok(BulkMessageResponse::JustMessages(messages))
|
||||
Ok(BulkMessageResponse::JustMessages(
|
||||
messages
|
||||
.into_iter()
|
||||
.map(|msg| msg.into_model(None, None))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Append content to message
|
||||
pub async fn append(
|
||||
db: &Database,
|
||||
amqp: Option<&AMQP>,
|
||||
id: String,
|
||||
channel: String,
|
||||
append: AppendMessage,
|
||||
) -> Result<()> {
|
||||
db.append_message(&id, &append).await?;
|
||||
if let Some(message) = db.append_message(&id, &append).await? {
|
||||
if let Some(amqp) = amqp {
|
||||
let author = message.fetch_author(db).await;
|
||||
|
||||
EventV1::MessageAppend {
|
||||
id,
|
||||
channel: channel.to_string(),
|
||||
append: append.into(),
|
||||
if let Err(e) = amqp.edit_message_search(message, author).await {
|
||||
log::error!("Error pushing message to RabbitMQ: {e}");
|
||||
capture_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::MessageAppend {
|
||||
id,
|
||||
channel: channel.to_string(),
|
||||
append: append.into(),
|
||||
}
|
||||
.p(channel)
|
||||
.await;
|
||||
}
|
||||
.p(channel)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -998,13 +1059,12 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Delete a message
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
let file_ids = self
|
||||
pub async fn delete(&self, db: &Database, amqp: Option<&AMQP>) -> Result<()> {
|
||||
let file_ids: Vec<String> = self
|
||||
.attachments
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|file| file.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
.as_ref()
|
||||
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
if !file_ids.is_empty() {
|
||||
db.mark_attachments_as_deleted(&file_ids).await?;
|
||||
@@ -1012,12 +1072,20 @@ impl Message {
|
||||
|
||||
db.delete_message(&self.id).await?;
|
||||
|
||||
if let Some(amqp) = amqp {
|
||||
if let Err(e) = amqp.delete_message_search(self.id.clone()).await {
|
||||
log::error!("Error pushing message to RabbitMQ: {e}");
|
||||
capture_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::MessageDelete {
|
||||
id: self.id.clone(),
|
||||
channel: self.channel.clone(),
|
||||
}
|
||||
.p(self.channel.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1118,6 +1186,14 @@ impl Message {
|
||||
FieldsMessage::Pinned => self.pinned = None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_author(&self, db: &Database) -> Option<User> {
|
||||
if self.webhook.is_some() {
|
||||
None
|
||||
} else {
|
||||
db.fetch_user(&self.author).await.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemMessage {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use revolt_result::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||
use crate::{
|
||||
util::ChunkedDatabaseGenerator, AppendMessage, FieldsMessage, Message, MessageQuery,
|
||||
MessageWithUser, PartialMessage,
|
||||
};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
@@ -23,10 +26,15 @@ 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, remove: Vec<FieldsMessage>) -> 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<()>;
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<Option<Message>>;
|
||||
|
||||
/// Add a new reaction to a message
|
||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
|
||||
@@ -48,8 +56,11 @@ pub trait AbstractMessages: Sync + Send {
|
||||
&self,
|
||||
channels: &[String],
|
||||
author: &str,
|
||||
since: SystemTime
|
||||
since: SystemTime,
|
||||
) -> Result<HashMap<String, Vec<String>>>;
|
||||
|
||||
/// Fetches all messages along with their author from every message in decending order
|
||||
async fn fetch_all_messages(&self) -> Result<ChunkedDatabaseGenerator<MessageWithUser>>;
|
||||
|
||||
async fn delete_messages_by_user(&self, user_id: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bson::{to_bson, Document};
|
||||
use futures::try_join;
|
||||
use futures::StreamExt;
|
||||
use mongodb::options::FindOptions;
|
||||
use mongodb::options::{FindOptions, ReadConcern, ReturnDocument};
|
||||
use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -9,8 +9,8 @@ use std::time::SystemTime;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath, Message, MessageQuery,
|
||||
MessageTimePeriod, MongoDb, PartialMessage,
|
||||
util::ChunkedDatabaseGenerator, AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath,
|
||||
Message, MessageQuery, MessageTimePeriod, MessageWithUser, MongoDb, PartialMessage,
|
||||
};
|
||||
|
||||
use super::AbstractMessages;
|
||||
@@ -205,7 +205,7 @@ impl AbstractMessages for MongoDb {
|
||||
}
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<Option<Message>> {
|
||||
let mut query = doc! {};
|
||||
|
||||
if let Some(embeds) = &append.embeds {
|
||||
@@ -223,18 +223,18 @@ impl AbstractMessages for MongoDb {
|
||||
}
|
||||
|
||||
if query.is_empty() {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
self.col::<Message>(COL)
|
||||
.find_one_and_update(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
query,
|
||||
)
|
||||
.return_document(ReturnDocument::After)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
@@ -417,6 +417,53 @@ impl AbstractMessages for MongoDb {
|
||||
Ok(deleted_messages)
|
||||
}
|
||||
|
||||
/// Fetches all messages along with their author from every message in decending order
|
||||
async fn fetch_all_messages(&self) -> Result<ChunkedDatabaseGenerator<MessageWithUser>> {
|
||||
let mut session = self
|
||||
.start_session()
|
||||
.await
|
||||
.map_err(|_| create_database_error!("start_session", COL))?;
|
||||
|
||||
session
|
||||
.start_transaction()
|
||||
.read_concern(ReadConcern::snapshot())
|
||||
.await
|
||||
.map_err(|_| create_database_error!("start_transaction", COL))?;
|
||||
|
||||
let cursor = self
|
||||
.col::<Message>(COL)
|
||||
.aggregate([
|
||||
doc! {
|
||||
"$lookup": {
|
||||
"from": "users",
|
||||
"localField": "author",
|
||||
"foreignField": "_id",
|
||||
"as": "user"
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"user": {
|
||||
"$first": "$user"
|
||||
}
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$sort": {
|
||||
"_id": -1
|
||||
}
|
||||
},
|
||||
])
|
||||
.with_type::<MessageWithUser>()
|
||||
.session(&mut session)
|
||||
.batch_size(1000)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("{e}"))
|
||||
.map_err(|_| create_database_error!("aggregate", COL))?;
|
||||
|
||||
Ok(ChunkedDatabaseGenerator::new_mongo(session, cursor))
|
||||
}
|
||||
|
||||
async fn delete_messages_by_user(&self, user_id: &str) -> Result<()> {
|
||||
self.delete_bulk_messages(doc! {
|
||||
"author": user_id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
AppendMessage, FieldsMessage, Message, MessageQuery,
|
||||
PartialMessage, ReferenceDb,
|
||||
util::ChunkedDatabaseGenerator, AppendMessage, FieldsMessage, Message, MessageQuery,
|
||||
MessageWithUser, PartialMessage, ReferenceDb,
|
||||
};
|
||||
use futures::future::try_join_all;
|
||||
use indexmap::IndexSet;
|
||||
@@ -215,7 +215,7 @@ impl AbstractMessages for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<Option<Message>> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message_data) = messages.get_mut(id) {
|
||||
if let Some(embeds) = &append.embeds {
|
||||
@@ -226,9 +226,11 @@ impl AbstractMessages for ReferenceDb {
|
||||
message_data.embeds = Some(embeds.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(Some(message_data.clone()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
@@ -353,6 +355,24 @@ impl AbstractMessages for ReferenceDb {
|
||||
Ok(deleted_messages)
|
||||
}
|
||||
|
||||
/// Fetches all messages along with their author from every message in decending order
|
||||
async fn fetch_all_messages(&self) -> Result<ChunkedDatabaseGenerator<MessageWithUser>> {
|
||||
let users = self.users.lock().await;
|
||||
|
||||
Ok(ChunkedDatabaseGenerator::new_reference(
|
||||
self.messages
|
||||
.lock()
|
||||
.await
|
||||
.values()
|
||||
.cloned()
|
||||
.map(|message| MessageWithUser {
|
||||
user: users.get(&message.author).cloned(),
|
||||
message,
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_messages_by_user(&self, user_id: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
mod admin_migrations;
|
||||
mod audit_logs;
|
||||
mod bots;
|
||||
mod channel_invites;
|
||||
mod channel_unreads;
|
||||
@@ -24,7 +23,6 @@ mod sessions;
|
||||
mod mfa_tickets;
|
||||
|
||||
pub use admin_migrations::*;
|
||||
pub use audit_logs::*;
|
||||
pub use bots::*;
|
||||
pub use channel_invites::*;
|
||||
pub use channel_unreads::*;
|
||||
@@ -57,7 +55,6 @@ pub trait AbstractDatabase:
|
||||
Sync
|
||||
+ Send
|
||||
+ admin_migrations::AbstractMigrations
|
||||
+ audit_logs::AbstractAuditLogs
|
||||
+ bots::AbstractBots
|
||||
+ channels::AbstractChannels
|
||||
+ channel_invites::AbstractChannelInvites
|
||||
|
||||
@@ -245,26 +245,6 @@ impl Member {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialMember containing the data which has changed in an update
|
||||
pub fn generate_diff(&self, partial: &PartialMember, remove: &[FieldsMember]) -> PartialMember {
|
||||
let mut before = PartialMember::default();
|
||||
|
||||
generate_diff!(
|
||||
self, before, partial, remove,
|
||||
(
|
||||
(FieldsMember::Nickname) nickname,
|
||||
(FieldsMember::Avatar) avatar,
|
||||
(FieldsMember::Timeout) timeout,
|
||||
(FieldsMember::Pronouns) pronouns,
|
||||
((default) FieldsMember::Roles) roles,
|
||||
((default) FieldsMember::CanPublish) can_publish,
|
||||
((default) FieldsMember::CanReceive) can_receive,
|
||||
)
|
||||
);
|
||||
|
||||
before
|
||||
}
|
||||
|
||||
/// Get this user's current ranking
|
||||
pub fn get_ranking(&self, server: &Server) -> i64 {
|
||||
let mut value = i64::MAX;
|
||||
@@ -290,7 +270,7 @@ impl Member {
|
||||
|
||||
/// Remove member from server
|
||||
pub async fn remove(
|
||||
&self,
|
||||
self,
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
intention: RemovalIntention,
|
||||
@@ -317,9 +297,9 @@ impl Member {
|
||||
})
|
||||
{
|
||||
match intention {
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user.clone() },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user.clone() },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user.clone() },
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user },
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
// TODO: support notifications here in the future?
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use revolt_config::capture_error;
|
||||
use revolt_models::v0::{self, DataCreateServerChannel};
|
||||
use revolt_permissions::{OverrideField, DEFAULT_PERMISSION_SERVER};
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{events::client::EventV1, Channel, Database, File, User};
|
||||
use crate::{AMQP, Channel, Database, File, User, events::client::EventV1};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server
|
||||
@@ -214,7 +215,16 @@ impl Server {
|
||||
}
|
||||
|
||||
/// Delete a server
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
pub async fn delete(self, db: &Database, amqp: Option<&AMQP>) -> Result<()> {
|
||||
if let Some(amqp) = amqp {
|
||||
for channel_id in self.channels {
|
||||
if let Err(e) = amqp.delete_channel_search(channel_id).await {
|
||||
log::error!("Error pushing message to RabbitMQ: {e}");
|
||||
capture_error(&e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::ServerDelete {
|
||||
id: self.id.clone(),
|
||||
}
|
||||
@@ -235,31 +245,6 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialServer containing the data which has changed in an update
|
||||
pub fn generate_diff(&self, partial: &PartialServer, remove: &[FieldsServer]) -> PartialServer {
|
||||
let mut before = PartialServer::default();
|
||||
|
||||
generate_diff!(
|
||||
self, before, partial, remove,
|
||||
(
|
||||
owner,
|
||||
name,
|
||||
(FieldsServer::Description) description,
|
||||
(FieldsServer::Categories) categories,
|
||||
(FieldsServer::SystemMessages) system_messages,
|
||||
roles,
|
||||
default_permissions,
|
||||
(FieldsServer::Icon) icon,
|
||||
(FieldsServer::Banner) banner,
|
||||
nsfw,
|
||||
analytics,
|
||||
discoverable,
|
||||
)
|
||||
);
|
||||
|
||||
before
|
||||
}
|
||||
|
||||
/// Ordered roles list
|
||||
pub fn ordered_roles(&self) -> Vec<(String, Role)> {
|
||||
let mut ordered_roles = self.roles.clone().into_iter().collect::<Vec<_>>();
|
||||
@@ -402,27 +387,8 @@ impl Role {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialRole containing the data which has changed in an update
|
||||
pub fn generate_diff(&self, partial: &PartialRole, remove: &[FieldsRole]) -> PartialRole {
|
||||
let mut before = PartialRole::default();
|
||||
|
||||
generate_diff!(
|
||||
self, before, partial, remove,
|
||||
(
|
||||
name,
|
||||
permissions,
|
||||
(FieldsRole::Colour) colour,
|
||||
hoist,
|
||||
rank,
|
||||
(FieldsRole::Icon) icon,
|
||||
)
|
||||
);
|
||||
|
||||
before
|
||||
}
|
||||
|
||||
/// Delete a role
|
||||
pub async fn delete(&self, db: &Database, server_id: &str) -> Result<()> {
|
||||
pub async fn delete(self, db: &Database, server_id: &str) -> Result<()> {
|
||||
EventV1::ServerRoleDelete {
|
||||
id: server_id.to_string(),
|
||||
role_id: self.id.clone(),
|
||||
|
||||
@@ -259,13 +259,6 @@ impl MongoDb {
|
||||
})
|
||||
.await?;
|
||||
|
||||
self.col::<Document>("audit_logs")
|
||||
.delete_many(doc! {
|
||||
"server": &server_id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "audit_logs"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +381,17 @@ impl User {
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn into_mutuals(perspective: &User, users: Vec<User>) -> Vec<v0::User> {
|
||||
let online_ids =
|
||||
filter_online(&users.iter().map(|user| user.id.clone()).collect::<Vec<_>>()).await;
|
||||
|
||||
join_all(users.into_iter().map(|user| async {
|
||||
let is_online = online_ids.contains(&user.id);
|
||||
user.into_known(perspective, is_online).await
|
||||
}))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find a free discriminator for a given username
|
||||
pub async fn find_discriminator(
|
||||
db: &Database,
|
||||
@@ -531,7 +542,7 @@ impl User {
|
||||
pub async fn add_friend(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
amqp: &AMQP,
|
||||
amqp: Option<&AMQP>,
|
||||
target: &mut User,
|
||||
) -> Result<()> {
|
||||
match self.relationship_with(&target.id) {
|
||||
@@ -541,8 +552,10 @@ impl User {
|
||||
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
|
||||
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
|
||||
RelationshipStatus::Incoming => {
|
||||
// Accept incoming friend request
|
||||
_ = amqp.friend_request_accepted(self, target).await;
|
||||
if let Some(amqp) = amqp {
|
||||
// Accept incoming friend request
|
||||
_ = amqp.friend_request_accepted(self, target).await;
|
||||
};
|
||||
|
||||
self.apply_relationship(
|
||||
db,
|
||||
@@ -572,7 +585,9 @@ impl User {
|
||||
}));
|
||||
}
|
||||
|
||||
_ = amqp.friend_request_received(target, self).await;
|
||||
if let Some(amqp) = amqp {
|
||||
_ = amqp.friend_request_received(target, self).await;
|
||||
};
|
||||
|
||||
// Send the friend request
|
||||
self.apply_relationship(
|
||||
@@ -867,13 +882,13 @@ impl User {
|
||||
/// - deletes owned bots, servers and messages
|
||||
/// - removes user from all groups
|
||||
/// - clears relationships
|
||||
pub async fn delete(&mut self, db: &Database) -> Result<()> {
|
||||
pub async fn delete(&mut self, db: &Database, amqp: Option<&AMQP>) -> Result<()> {
|
||||
for bot in db.fetch_bots_by_user(&self.id).await? {
|
||||
bot.delete(db).await?;
|
||||
}
|
||||
|
||||
for server in db.fetch_owned_servers(&self.id).await? {
|
||||
server.delete(db).await?;
|
||||
server.delete(db, amqp).await?;
|
||||
}
|
||||
|
||||
self.remove_from_all_groups(db).await?;
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn start_workers(db: Database, amqp: AMQP) {
|
||||
for _ in 0..WORKER_COUNT {
|
||||
task::spawn(ack::worker(db.clone(), amqp.clone()));
|
||||
task::spawn(last_message_id::worker(db.clone()));
|
||||
task::spawn(process_embeds::worker(db.clone()));
|
||||
task::spawn(process_embeds::worker(db.clone(), amqp.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{models::Message, AppendMessage, Database};
|
||||
use crate::{AMQP, AppendMessage, Database, models::Message};
|
||||
|
||||
use futures::future::join_all;
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
@@ -41,7 +41,7 @@ pub async fn queue(channel: String, id: String, content: String) {
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
pub async fn worker(db: Database, amqp: AMQP) {
|
||||
let semaphore = Arc::new(Semaphore::new(
|
||||
config().await.api.workers.max_concurrent_connections,
|
||||
));
|
||||
@@ -49,6 +49,7 @@ pub async fn worker(db: Database) {
|
||||
loop {
|
||||
let task = Q.pop().await;
|
||||
let db = db.clone();
|
||||
let amqp = amqp.clone();
|
||||
let semaphore = semaphore.clone();
|
||||
|
||||
spawn(async move {
|
||||
@@ -64,6 +65,7 @@ pub async fn worker(db: Database) {
|
||||
if let Ok(embeds) = embeds {
|
||||
if let Err(err) = Message::append(
|
||||
&db,
|
||||
Some(&amqp),
|
||||
task.id,
|
||||
task.channel,
|
||||
AppendMessage {
|
||||
|
||||
@@ -319,7 +319,6 @@ impl From<FieldsChannel> for crate::FieldsChannel {
|
||||
FieldsChannel::Icon => crate::FieldsChannel::Icon,
|
||||
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
|
||||
FieldsChannel::Voice => crate::FieldsChannel::Voice,
|
||||
FieldsChannel::Slowmode => crate::FieldsChannel::Slowmode,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,7 +330,6 @@ impl From<crate::FieldsChannel> for FieldsChannel {
|
||||
crate::FieldsChannel::Icon => FieldsChannel::Icon,
|
||||
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
|
||||
crate::FieldsChannel::Voice => FieldsChannel::Voice,
|
||||
crate::FieldsChannel::Slowmode => FieldsChannel::Slowmode,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1423,14 +1421,6 @@ impl From<FieldsMessage> for crate::FieldsMessage {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::VoiceInformation> for VoiceInformation {
|
||||
fn from(value: crate::VoiceInformation) -> Self {
|
||||
VoiceInformation {
|
||||
max_users: value.max_users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VoiceInformation> for crate::VoiceInformation {
|
||||
fn from(value: VoiceInformation) -> Self {
|
||||
crate::VoiceInformation {
|
||||
@@ -1439,151 +1429,10 @@ impl From<VoiceInformation> for crate::VoiceInformation {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::AuditLogEntryAction> for AuditLogEntryAction {
|
||||
fn from(value: crate::AuditLogEntryAction) -> Self {
|
||||
match value {
|
||||
crate::AuditLogEntryAction::MessageDelete { author, channel } => {
|
||||
AuditLogEntryAction::MessageDelete { author, channel }
|
||||
}
|
||||
crate::AuditLogEntryAction::BanCreate { user } => {
|
||||
AuditLogEntryAction::BanCreate { user }
|
||||
}
|
||||
crate::AuditLogEntryAction::BanDelete { user } => {
|
||||
AuditLogEntryAction::BanDelete { user }
|
||||
}
|
||||
crate::AuditLogEntryAction::ChannelCreate { channel, name } => {
|
||||
AuditLogEntryAction::ChannelCreate { channel, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::MemberEdit {
|
||||
user,
|
||||
before,
|
||||
after,
|
||||
} => AuditLogEntryAction::MemberEdit {
|
||||
user,
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::MemberKick { user } => {
|
||||
AuditLogEntryAction::MemberKick { user }
|
||||
}
|
||||
crate::AuditLogEntryAction::ServerEdit { before, after } => {
|
||||
AuditLogEntryAction::ServerEdit {
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
}
|
||||
}
|
||||
crate::AuditLogEntryAction::RoleEdit {
|
||||
role,
|
||||
before,
|
||||
after,
|
||||
} => AuditLogEntryAction::RoleEdit {
|
||||
role,
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::RoleCreate { role, name } => {
|
||||
AuditLogEntryAction::RoleCreate { role, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::RoleDelete { role, name } => {
|
||||
AuditLogEntryAction::RoleDelete { role, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::RolesReorder { before, after } => {
|
||||
AuditLogEntryAction::RolesReorder { before, after }
|
||||
}
|
||||
crate::AuditLogEntryAction::MessageBulkDelete { channel, count } => {
|
||||
AuditLogEntryAction::MessageBulkDelete { channel, count }
|
||||
}
|
||||
crate::AuditLogEntryAction::ChannelEdit {
|
||||
channel,
|
||||
before,
|
||||
after,
|
||||
} => AuditLogEntryAction::ChannelEdit {
|
||||
channel,
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::ChannelRolePermissionsEdit {
|
||||
channel,
|
||||
role,
|
||||
permissions,
|
||||
} => AuditLogEntryAction::ChannelRolePermissionsEdit {
|
||||
channel,
|
||||
role,
|
||||
permissions: permissions.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::ChannelDelete { channel, name } => {
|
||||
AuditLogEntryAction::ChannelDelete { channel, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::InviteDelete { invite, channel } => {
|
||||
AuditLogEntryAction::InviteDelete { invite, channel }
|
||||
}
|
||||
crate::AuditLogEntryAction::WebhookCreate {
|
||||
webhook,
|
||||
name,
|
||||
channel,
|
||||
} => AuditLogEntryAction::WebhookCreate {
|
||||
webhook,
|
||||
name,
|
||||
channel,
|
||||
},
|
||||
crate::AuditLogEntryAction::WebhookDelete {
|
||||
webhook,
|
||||
name,
|
||||
channel,
|
||||
} => AuditLogEntryAction::WebhookDelete {
|
||||
webhook,
|
||||
name,
|
||||
channel,
|
||||
},
|
||||
crate::AuditLogEntryAction::EmojiCreate { emoji, name } => {
|
||||
AuditLogEntryAction::EmojiCreate { emoji, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::EmojiUpdate {
|
||||
emoji,
|
||||
before,
|
||||
after,
|
||||
} => AuditLogEntryAction::EmojiUpdate {
|
||||
emoji,
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::EmojiDelete { emoji, name } => {
|
||||
AuditLogEntryAction::EmojiDelete { emoji, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::MessagePin {
|
||||
message,
|
||||
author,
|
||||
channel,
|
||||
} => AuditLogEntryAction::MessagePin {
|
||||
message,
|
||||
author,
|
||||
channel,
|
||||
},
|
||||
crate::AuditLogEntryAction::MessageUnpin {
|
||||
message,
|
||||
author,
|
||||
channel,
|
||||
} => AuditLogEntryAction::MessageUnpin {
|
||||
message,
|
||||
author,
|
||||
channel,
|
||||
},
|
||||
crate::AuditLogEntryAction::InviteCreate { invite, channel } => {
|
||||
AuditLogEntryAction::InviteCreate { invite, channel }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::AuditLogEntry> for AuditLogEntry {
|
||||
fn from(value: crate::AuditLogEntry) -> Self {
|
||||
AuditLogEntry {
|
||||
id: value.id,
|
||||
server: value.server,
|
||||
reason: value.reason,
|
||||
user: value.user,
|
||||
target: value.target,
|
||||
action: value.action.into(),
|
||||
impl From<crate::VoiceInformation> for VoiceInformation {
|
||||
fn from(value: crate::VoiceInformation) -> Self {
|
||||
VoiceInformation {
|
||||
max_users: value.max_users,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1676,9 +1525,3 @@ impl From<WebPushSubscription> for crate::WebPushSubscription {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialEmoji> for PartialEmoji {
|
||||
fn from(value: crate::PartialEmoji) -> Self {
|
||||
PartialEmoji { name: value.name }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ pub mod reference;
|
||||
pub mod shield;
|
||||
pub mod test_fixtures;
|
||||
|
||||
pub use chunked::ChunkedDatabaseGenerator;
|
||||
pub use funcs::*;
|
||||
pub use chunked::ChunkedDatabaseGenerator;
|
||||
@@ -159,17 +159,4 @@ impl VoiceClient {
|
||||
.await
|
||||
.to_internal_error()
|
||||
}
|
||||
|
||||
pub async fn get_room_participants(
|
||||
&self,
|
||||
node: &str,
|
||||
channel_id: &str,
|
||||
) -> Result<Vec<ParticipantInfo>> {
|
||||
let room = self.get_node(node)?;
|
||||
|
||||
room.client
|
||||
.list_participants(channel_id)
|
||||
.await
|
||||
.to_internal_error()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-files"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
use crate::v0::{Member, PartialChannel, PartialEmoji, PartialMember, PartialRole, PartialServer, User};
|
||||
use revolt_permissions::Override;
|
||||
|
||||
auto_derived!(
|
||||
/// Audit log entry
|
||||
pub struct AuditLogEntry {
|
||||
/// Unique ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// The server the entry happened in
|
||||
pub server: String,
|
||||
/// User provided reason
|
||||
pub reason: Option<String>,
|
||||
/// User who ran the action
|
||||
pub user: String,
|
||||
/// User this action is targetting
|
||||
pub target: Option<String>,
|
||||
/// The action ran
|
||||
pub action: AuditLogEntryAction,
|
||||
}
|
||||
|
||||
/// Indivual action stored on the audit log
|
||||
#[serde(tag = "type")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum AuditLogEntryAction {
|
||||
MessageDelete {
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
MessageBulkDelete {
|
||||
channel: String,
|
||||
count: usize,
|
||||
},
|
||||
MessagePin {
|
||||
message: String,
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
MessageUnpin {
|
||||
message: String,
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
BanCreate {
|
||||
user: String,
|
||||
},
|
||||
BanDelete {
|
||||
user: String,
|
||||
},
|
||||
ChannelCreate {
|
||||
channel: String,
|
||||
name: String,
|
||||
},
|
||||
ChannelEdit {
|
||||
channel: String,
|
||||
before: PartialChannel,
|
||||
after: PartialChannel,
|
||||
},
|
||||
ChannelRolePermissionsEdit {
|
||||
channel: String,
|
||||
role: String,
|
||||
permissions: Override,
|
||||
},
|
||||
ChannelDelete {
|
||||
channel: String,
|
||||
name: String,
|
||||
},
|
||||
MemberEdit {
|
||||
user: String,
|
||||
before: PartialMember,
|
||||
after: PartialMember,
|
||||
},
|
||||
MemberKick {
|
||||
user: String,
|
||||
},
|
||||
ServerEdit {
|
||||
before: PartialServer,
|
||||
after: PartialServer,
|
||||
},
|
||||
RoleEdit {
|
||||
role: String,
|
||||
before: PartialRole,
|
||||
after: PartialRole,
|
||||
},
|
||||
RoleCreate {
|
||||
role: String,
|
||||
name: String,
|
||||
},
|
||||
RoleDelete {
|
||||
role: String,
|
||||
name: String,
|
||||
},
|
||||
RolesReorder {
|
||||
before: Vec<String>,
|
||||
after: Vec<String>,
|
||||
},
|
||||
InviteCreate {
|
||||
invite: String,
|
||||
channel: String,
|
||||
},
|
||||
InviteDelete {
|
||||
invite: String,
|
||||
channel: String,
|
||||
},
|
||||
WebhookCreate {
|
||||
webhook: String,
|
||||
name: String,
|
||||
channel: String,
|
||||
},
|
||||
WebhookDelete {
|
||||
webhook: String,
|
||||
name: String,
|
||||
channel: String,
|
||||
},
|
||||
EmojiCreate {
|
||||
emoji: String,
|
||||
name: String,
|
||||
},
|
||||
EmojiUpdate {
|
||||
emoji: String,
|
||||
before: PartialEmoji,
|
||||
after: PartialEmoji,
|
||||
},
|
||||
EmojiDelete {
|
||||
emoji: String,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Audit log query filters
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
#[cfg_attr(feature = "rocket", derive(rocket::FromForm))]
|
||||
pub struct OptionsAuditLogQuery {
|
||||
/// Filter by who ran the action
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub user: Option<String>,
|
||||
/// Filter by who the action is targetting
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub target: Option<String>,
|
||||
/// Filter by the action type
|
||||
pub r#type: Option<Vec<String>>,
|
||||
/// Entries before a certain entry id
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub before: Option<String>,
|
||||
/// Entries after a certain entry id
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub after: Option<String>,
|
||||
/// Maximum number of entries to fetch
|
||||
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
/// Response containing the audit log entries and the users involved
|
||||
pub struct AuditLogQueryResponse {
|
||||
/// List of audit logs
|
||||
pub audit_logs: Vec<AuditLogEntry>,
|
||||
/// List of users
|
||||
pub users: Vec<User>,
|
||||
/// List of members
|
||||
pub members: Vec<Member>,
|
||||
}
|
||||
);
|
||||
@@ -164,7 +164,6 @@ auto_derived!(
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
Voice,
|
||||
Slowmode,
|
||||
}
|
||||
|
||||
/// New webhook information
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
mod audit_logs;
|
||||
mod bots;
|
||||
mod channel_invites;
|
||||
mod channel_unreads;
|
||||
@@ -15,11 +14,11 @@ mod server_members;
|
||||
mod servers;
|
||||
mod user_settings;
|
||||
mod users;
|
||||
mod search;
|
||||
mod accounts;
|
||||
mod mfa_tickets;
|
||||
mod sessions;
|
||||
|
||||
pub use audit_logs::*;
|
||||
pub use bots::*;
|
||||
pub use channel_invites::*;
|
||||
pub use channel_unreads::*;
|
||||
@@ -36,6 +35,7 @@ pub use server_members::*;
|
||||
pub use servers::*;
|
||||
pub use user_settings::*;
|
||||
pub use users::*;
|
||||
pub use search::*;
|
||||
pub use accounts::*;
|
||||
pub use mfa_tickets::*;
|
||||
pub use sessions::*;
|
||||
pub use sessions::*;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use std::collections::HashSet;
|
||||
|
||||
auto_derived!(
|
||||
/// Options for searching messages in a server or channel
|
||||
pub struct DataChannelMessagesSearch {
|
||||
/// Channel to search in
|
||||
pub channel: Option<String>,
|
||||
/// Server to search in
|
||||
pub server: Option<String>,
|
||||
|
||||
/// Filter options
|
||||
pub filters: Option<DataChannelMessagesSearchFilters>,
|
||||
|
||||
/// What index to start the search at
|
||||
pub offset: Option<u64>,
|
||||
/// Max amount of messages to return
|
||||
pub limit: Option<u64>,
|
||||
/// Sort order
|
||||
pub sort: Option<SortOrder>,
|
||||
}
|
||||
|
||||
/// Message search filters
|
||||
pub struct DataChannelMessagesSearchFilters {
|
||||
/// Message content
|
||||
pub content: Option<String>,
|
||||
/// Specific user
|
||||
pub author: Option<HashSet<String>>,
|
||||
|
||||
/// Mentions a user
|
||||
pub mentions: Option<HashSet<String>>,
|
||||
/// Mentions a role
|
||||
pub role_mentions: Option<HashSet<String>>,
|
||||
|
||||
/// Send before a specific date
|
||||
pub before_date: Option<Timestamp>,
|
||||
/// Sent after a specific date
|
||||
pub after_date: Option<Timestamp>,
|
||||
|
||||
/// What type of user sent the message
|
||||
pub author_type: Option<HashSet<AuthorType>>,
|
||||
/// Whether the message is pinned or not
|
||||
pub pinned: Option<bool>,
|
||||
/// Require message to have a specific component type
|
||||
pub components: Option<HashSet<MessageComponent>>,
|
||||
}
|
||||
|
||||
/// Message author type
|
||||
#[derive(Copy, Hash)]
|
||||
pub enum AuthorType {
|
||||
User,
|
||||
Bot,
|
||||
Webhook,
|
||||
}
|
||||
|
||||
/// Message component
|
||||
#[derive(Copy, Hash)]
|
||||
pub enum MessageComponent {
|
||||
Image,
|
||||
Video,
|
||||
Link,
|
||||
File,
|
||||
Embed,
|
||||
}
|
||||
|
||||
/// Message sort order
|
||||
#[derive(Copy, Default)]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
|
||||
pub enum SortOrder {
|
||||
Asc,
|
||||
#[default]
|
||||
Desc,
|
||||
}
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-parser"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-permissions"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -100,11 +100,8 @@ pub enum ChannelPermission {
|
||||
/// Mention roles
|
||||
MentionRoles = 1 << 38,
|
||||
|
||||
/// Access server audit logs
|
||||
ViewAuditLogs = 1 << 40,
|
||||
|
||||
// * Misc. permissions
|
||||
// % Bits 41 to 52: free area
|
||||
// % Bits 39 to 52: free area
|
||||
// % Bits 53 to 64: do not use
|
||||
|
||||
// * Grant all permissions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-presence"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-ratelimits"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-result"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -90,7 +90,6 @@ impl IntoResponse for Error {
|
||||
ErrorType::UnknownNode => StatusCode::BAD_REQUEST,
|
||||
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
|
||||
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
|
||||
ErrorType::HeaderTooLarge => StatusCode::BAD_REQUEST,
|
||||
|
||||
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
|
||||
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
|
||||
@@ -164,7 +164,6 @@ pub enum ErrorType {
|
||||
FailedValidation {
|
||||
error: String,
|
||||
},
|
||||
HeaderTooLarge,
|
||||
OperationFailed,
|
||||
IncorrectData {
|
||||
with: String,
|
||||
|
||||
@@ -91,7 +91,6 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::NotConnected => Status::BadRequest,
|
||||
ErrorType::UnknownNode => Status::BadRequest,
|
||||
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
|
||||
ErrorType::HeaderTooLarge => Status::BadRequest,
|
||||
|
||||
ErrorType::ProxyError => Status::BadRequest,
|
||||
ErrorType::FileTooSmall => Status::UnprocessableEntity,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "revolt-search"
|
||||
version = "0.13.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
revolt-config = { workspace = true }
|
||||
revolt-database = { workspace = true }
|
||||
revolt-models = { workspace = true }
|
||||
elasticsearch = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
iso8601-timestamp = { workspace = true, features = ["serde", "bson"] }
|
||||
futures = { workspace = true }
|
||||
elasticsearch-dsl = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
linkify = { workspace = true }
|
||||
@@ -0,0 +1,413 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use elasticsearch::{
|
||||
BulkOperation, BulkParts, CreateParts, DeleteByQueryParts, DeleteParts, Elasticsearch,
|
||||
IndexParts, SearchParts,
|
||||
auth::Credentials,
|
||||
http::{
|
||||
response::Exception,
|
||||
transport::{SingleNodeConnectionPool, TransportBuilder},
|
||||
},
|
||||
indices::{IndicesCreateParts, IndicesDeleteParts},
|
||||
};
|
||||
use elasticsearch_dsl::{FieldSort, Query, Search, SearchResponse, Sort};
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use revolt_database::{Database, Message, MessageWithUser, User};
|
||||
use serde_json::{Map, Value, json, to_value};
|
||||
|
||||
pub use elasticsearch;
|
||||
|
||||
use crate::{AuthorType, MessageComponent, SearchTerms};
|
||||
|
||||
/// Elasticsearch errors
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Http(elasticsearch::Error),
|
||||
Exception(Exception),
|
||||
}
|
||||
|
||||
impl From<elasticsearch::Error> for Error {
|
||||
fn from(value: elasticsearch::Error) -> Self {
|
||||
Self::Http(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Exception> for Error {
|
||||
fn from(value: Exception) -> Self {
|
||||
Self::Exception(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::Http(error) => write!(f, "Http error: {error}"),
|
||||
Error::Exception(exception) => write!(f, "Elasticsearch error: {exception:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Higher level elasticsearch API more fit for our specific usecase
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ElasticsearchClient {
|
||||
pub inner: Elasticsearch,
|
||||
}
|
||||
|
||||
impl ElasticsearchClient {
|
||||
pub fn new(host: &str, port: u16, key: String) -> Self {
|
||||
let pool =
|
||||
SingleNodeConnectionPool::new(format!("{host}:{port}").as_str().try_into().unwrap());
|
||||
let transport = TransportBuilder::new(pool)
|
||||
.auth(Credentials::EncodedApiKey(key))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let inner = Elasticsearch::new(transport);
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// Delete messages index along with all documents
|
||||
pub async fn delete_indexes(&self) -> Result<(), Error> {
|
||||
let exception = self
|
||||
.inner
|
||||
.indices()
|
||||
.delete(IndicesDeleteParts::Index(&["messages"]))
|
||||
.send()
|
||||
.await?
|
||||
.exception()
|
||||
.await?;
|
||||
|
||||
if let Some(exception) = exception {
|
||||
Err(exception.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the messages index
|
||||
pub async fn setup_indexes(&self) -> Result<(), Error> {
|
||||
let exception = self
|
||||
.inner
|
||||
.indices()
|
||||
.create(IndicesCreateParts::Index("messages"))
|
||||
.body(json!({
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"content": {"type": "text"},
|
||||
"author": {"type": "keyword"},
|
||||
"author_type": {"type": "keyword"},
|
||||
"channel": {"type": "keyword"},
|
||||
"mentions": {"type": "keyword"},
|
||||
"role_mentions": {"type": "keyword"},
|
||||
"pinned": {"type": "boolean"},
|
||||
"embeds": {
|
||||
"properties": {}
|
||||
},
|
||||
"attachments": {
|
||||
"type": "nested",
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"metadata.type": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"has_link": { "type": "boolean" },
|
||||
}
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await?
|
||||
.exception()
|
||||
.await?;
|
||||
|
||||
if let Some(exception) = exception {
|
||||
Err(exception.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a search for messages, returns a vec of message ids
|
||||
pub async fn search(&self, terms: SearchTerms) -> Result<Vec<String>, Error> {
|
||||
let mut query = Query::bool().filter(Query::terms("channel", terms.channels));
|
||||
|
||||
if let Some(content) = terms.filters.content {
|
||||
query = query.filter(Query::r#match("content", content))
|
||||
}
|
||||
|
||||
if let Some(author) = terms.filters.author {
|
||||
query = query.filter(Query::terms("author", author))
|
||||
}
|
||||
|
||||
if let Some(mentions) = terms.filters.mentions {
|
||||
query = query.filter(Query::terms("mentions", mentions))
|
||||
}
|
||||
|
||||
if let Some(author_type) = terms.filters.author_type {
|
||||
query = query.filter(Query::terms("author_type", author_type))
|
||||
}
|
||||
|
||||
if let Some(pinned) = terms.filters.pinned {
|
||||
if pinned {
|
||||
query = query.filter(Query::exists("pinned"))
|
||||
} else {
|
||||
query = query.filter(Query::bool().must_not(Query::exists("pinned")))
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(components) = terms.filters.components {
|
||||
let mut components_query = Query::bool();
|
||||
let mut attachments_query = Query::bool();
|
||||
|
||||
for component in components {
|
||||
match component {
|
||||
MessageComponent::Image => {
|
||||
attachments_query = attachments_query
|
||||
.should(Query::term("attachments.metadata.type", "Image"))
|
||||
}
|
||||
MessageComponent::Video => {
|
||||
attachments_query = attachments_query
|
||||
.should(Query::term("attachments.metadata.type", "Video"))
|
||||
}
|
||||
MessageComponent::Link => {
|
||||
components_query = components_query.should(Query::exists("has_link"))
|
||||
}
|
||||
MessageComponent::File => {
|
||||
attachments_query = attachments_query.should(Query::exists("attachments"))
|
||||
}
|
||||
MessageComponent::Embed => {
|
||||
components_query = components_query.should(Query::exists("embeds"))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
query = query
|
||||
.filter(components_query.should(Query::nested("attachments", attachments_query)));
|
||||
}
|
||||
|
||||
let search = Search::new()
|
||||
.query(query)
|
||||
.stats(false)
|
||||
.sort(Sort::FieldSort(
|
||||
FieldSort::new("_id".to_string()).order(terms.sort.unwrap_or_default().into()),
|
||||
));
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.search(SearchParts::Index(&["messages"]))
|
||||
.stored_fields(&[])
|
||||
.body(search)
|
||||
.size(terms.limit.unwrap_or(100) as i64)
|
||||
.from(terms.offset.unwrap_or(0) as i64)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status_code().is_success() {
|
||||
let messages = response.json::<SearchResponse>().await?;
|
||||
Ok(messages.hits.hits.into_iter().map(|hit| hit.id).collect())
|
||||
} else {
|
||||
Err(response
|
||||
.exception()
|
||||
.await?
|
||||
.expect("No exception with error response.")
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a source for a message which can be stored and indexed into elasticsearch
|
||||
fn create_message_source(
|
||||
&self,
|
||||
_db: &Database,
|
||||
message: Message,
|
||||
author: Option<User>,
|
||||
) -> Value {
|
||||
let mut map = Map::new();
|
||||
|
||||
map.insert("channel".to_string(), Value::String(message.channel));
|
||||
|
||||
map.insert("author".to_string(), Value::String(message.author));
|
||||
|
||||
if let Some(content) = message.content {
|
||||
// Is there a better way to handle this? can elasticsearch index links itself?
|
||||
// Maybe in the future store the domains and be able to filter by that as well
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.kinds(&[LinkKind::Url]);
|
||||
|
||||
if finder.links(&content).next().is_some() {
|
||||
map.insert("has_link".to_string(), Value::Bool(true));
|
||||
}
|
||||
|
||||
map.insert("content".to_string(), Value::String(content));
|
||||
}
|
||||
|
||||
if let Some(attachments) = message.attachments {
|
||||
// TODO: fetch the file metadata from FileHash because of File.metadata deprecation
|
||||
// let metadata = attachment.as_hash(db).await.expect("Failed to fetch FileHash").metadata;
|
||||
|
||||
map.insert(
|
||||
"attachments".to_string(),
|
||||
serde_json::to_value(attachments).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(embeds) = message.embeds {
|
||||
map.insert("embeds".to_string(), serde_json::to_value(embeds).unwrap());
|
||||
}
|
||||
|
||||
if let Some(mentions) = message.mentions {
|
||||
map.insert(
|
||||
"mentions".to_string(),
|
||||
serde_json::to_value(mentions).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(role_mentions) = message.role_mentions {
|
||||
map.insert(
|
||||
"role_mentions".to_string(),
|
||||
serde_json::to_value(role_mentions).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(pinned) = message.pinned {
|
||||
map.insert("pinned".to_string(), Value::Bool(pinned));
|
||||
}
|
||||
|
||||
// This will turn bot author type to user author type if this is ran on a deleted message,
|
||||
// due to the author not existing anymore so fetching will fail, this is probably niche enough
|
||||
// to not really matter, might try fix in the future.
|
||||
map.insert(
|
||||
"author_type".to_string(),
|
||||
to_value(if message.webhook.is_some() {
|
||||
AuthorType::Webhook
|
||||
} else if author.is_some_and(|user| user.bot.is_some()) {
|
||||
AuthorType::Bot
|
||||
} else {
|
||||
AuthorType::User
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
Value::Object(map)
|
||||
}
|
||||
|
||||
/// Bulk uploads and indexes messages to elasticsearch
|
||||
pub async fn bulk_index_messages(
|
||||
&self,
|
||||
db: &Database,
|
||||
messages: Vec<MessageWithUser>,
|
||||
) -> Result<(), Error> {
|
||||
let mut ops = Vec::<BulkOperation<Value>>::new();
|
||||
|
||||
for message in messages {
|
||||
let id = message.message.id.clone();
|
||||
let source = self.create_message_source(db, message.message, message.user);
|
||||
|
||||
ops.push(BulkOperation::create(source).id(id).into());
|
||||
}
|
||||
|
||||
let exception = self
|
||||
.inner
|
||||
.bulk(BulkParts::Index("messages"))
|
||||
.body(ops)
|
||||
.send()
|
||||
.await?
|
||||
.exception()
|
||||
.await?;
|
||||
|
||||
if let Some(exception) = exception {
|
||||
Err(exception.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Uploads and indexes a single message to elasticsearch
|
||||
pub async fn index_message(
|
||||
&self,
|
||||
db: &Database,
|
||||
message: Message,
|
||||
author: Option<User>,
|
||||
) -> Result<(), Error> {
|
||||
let id = message.id.clone();
|
||||
let source = self.create_message_source(db, message, author);
|
||||
|
||||
let exception = self
|
||||
.inner
|
||||
.create(CreateParts::IndexId("messages", &id))
|
||||
.body(source)
|
||||
.send()
|
||||
.await?
|
||||
.exception()
|
||||
.await?;
|
||||
|
||||
if let Some(exception) = exception {
|
||||
Err(exception.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates or upserts an existing message to elasticsearch
|
||||
pub async fn edit_message(
|
||||
&self,
|
||||
db: &Database,
|
||||
message: Message,
|
||||
author: Option<User>,
|
||||
) -> Result<(), Error> {
|
||||
let id = message.id.clone();
|
||||
let source = self.create_message_source(db, message, author);
|
||||
|
||||
let exception = self
|
||||
.inner
|
||||
.index(IndexParts::IndexId("messages", &id))
|
||||
.body(source)
|
||||
.send()
|
||||
.await?
|
||||
.exception()
|
||||
.await?;
|
||||
|
||||
if let Some(exception) = exception {
|
||||
Err(exception.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a message from elasticsearch
|
||||
pub async fn delete_message(&self, message_id: &str) -> Result<(), Error> {
|
||||
let exception = self
|
||||
.inner
|
||||
.delete(DeleteParts::IndexId("messages", message_id))
|
||||
.send()
|
||||
.await?
|
||||
.exception()
|
||||
.await?;
|
||||
|
||||
if let Some(exception) = exception {
|
||||
Err(exception.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes all messages in a channel from elasticsearch
|
||||
pub async fn delete_channel(&self, channel_id: &str) -> Result<(), Error> {
|
||||
let exception = self
|
||||
.inner
|
||||
.delete_by_query(DeleteByQueryParts::Index(&["messages"]))
|
||||
.body(Search::new().query(Query::term("channel", channel_id)))
|
||||
.send()
|
||||
.await?
|
||||
.exception()
|
||||
.await?;
|
||||
|
||||
if let Some(exception) = exception {
|
||||
Err(exception.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod client;
|
||||
mod types;
|
||||
|
||||
pub use client::*;
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,134 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_models::v0;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Message author type
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Hash)]
|
||||
pub enum AuthorType {
|
||||
User,
|
||||
Bot,
|
||||
Webhook,
|
||||
}
|
||||
|
||||
/// Message component
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum MessageComponent {
|
||||
Image,
|
||||
Video,
|
||||
Link,
|
||||
File,
|
||||
Embed,
|
||||
}
|
||||
|
||||
/// Message search filters
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct SearchFilters {
|
||||
/// Message content
|
||||
pub content: Option<String>,
|
||||
/// Specific user
|
||||
pub author: Option<HashSet<String>>,
|
||||
|
||||
/// Mentions a user
|
||||
pub mentions: Option<HashSet<String>>,
|
||||
/// Mentions a role
|
||||
pub role_mentions: Option<HashSet<String>>,
|
||||
|
||||
/// Send before a specific date
|
||||
pub before_date: Option<Timestamp>,
|
||||
/// Sent after a specific date
|
||||
pub after_date: Option<Timestamp>,
|
||||
|
||||
/// What type of user sent the message
|
||||
pub author_type: Option<HashSet<AuthorType>>,
|
||||
/// Whether the message is pinned or not
|
||||
pub pinned: Option<bool>,
|
||||
/// Require message to have a specific component type
|
||||
pub components: Option<HashSet<MessageComponent>>,
|
||||
}
|
||||
|
||||
/// Message sort order
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum SortOrder {
|
||||
Asc,
|
||||
#[default]
|
||||
Desc,
|
||||
}
|
||||
|
||||
/// Options for searching messages in a server or channel
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SearchTerms {
|
||||
/// Channels to search in
|
||||
pub channels: Vec<String>,
|
||||
|
||||
/// Filter options
|
||||
pub filters: SearchFilters,
|
||||
|
||||
/// What index to start the search at
|
||||
pub offset: Option<u64>,
|
||||
/// Max amount of messages to return
|
||||
pub limit: Option<u64>,
|
||||
/// Sort order
|
||||
pub sort: Option<SortOrder>,
|
||||
}
|
||||
|
||||
impl From<v0::AuthorType> for AuthorType {
|
||||
fn from(value: v0::AuthorType) -> Self {
|
||||
match value {
|
||||
v0::AuthorType::User => AuthorType::User,
|
||||
v0::AuthorType::Bot => AuthorType::Bot,
|
||||
v0::AuthorType::Webhook => AuthorType::Webhook,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v0::MessageComponent> for MessageComponent {
|
||||
fn from(value: v0::MessageComponent) -> Self {
|
||||
match value {
|
||||
v0::MessageComponent::Image => MessageComponent::Image,
|
||||
v0::MessageComponent::Video => MessageComponent::Video,
|
||||
v0::MessageComponent::Link => MessageComponent::Link,
|
||||
v0::MessageComponent::File => MessageComponent::File,
|
||||
v0::MessageComponent::Embed => MessageComponent::Embed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v0::SortOrder> for SortOrder {
|
||||
fn from(value: v0::SortOrder) -> Self {
|
||||
match value {
|
||||
v0::SortOrder::Asc => SortOrder::Asc,
|
||||
v0::SortOrder::Desc => SortOrder::Desc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v0::DataChannelMessagesSearchFilters> for SearchFilters {
|
||||
fn from(value: v0::DataChannelMessagesSearchFilters) -> Self {
|
||||
Self {
|
||||
content: value.content,
|
||||
author: value.author,
|
||||
mentions: value.mentions,
|
||||
role_mentions: value.role_mentions,
|
||||
before_date: value.before_date,
|
||||
after_date: value.after_date,
|
||||
author_type: value
|
||||
.author_type
|
||||
.map(|types| types.into_iter().map(Into::into).collect()),
|
||||
pinned: value.pinned,
|
||||
components: value
|
||||
.components
|
||||
.map(|types| types.into_iter().map(Into::into).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SortOrder> for elasticsearch_dsl::SortOrder {
|
||||
fn from(value: SortOrder) -> Self {
|
||||
match value {
|
||||
SortOrder::Asc => elasticsearch_dsl::SortOrder::Asc,
|
||||
SortOrder::Desc => elasticsearch_dsl::SortOrder::Desc,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-crond"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use revolt_database::Database;
|
||||
use revolt_database::{Database, AMQP};
|
||||
use revolt_result::Result;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub async fn task(db: Database, _: revolt_database::AMQP) -> Result<()> {
|
||||
pub async fn task(db: Database, amqp: AMQP) -> Result<()> {
|
||||
loop {
|
||||
let accounts = db.fetch_accounts_due_for_deletion().await?;
|
||||
let count = accounts.len();
|
||||
@@ -12,7 +12,7 @@ pub async fn task(db: Database, _: revolt_database::AMQP) -> Result<()> {
|
||||
for mut account in accounts {
|
||||
let mut user = db.fetch_user(&account.id).await?;
|
||||
|
||||
user.delete(&db).await?;
|
||||
user.delete(&db, Some(&amqp)).await?;
|
||||
account.mark_deleted(&db).await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-pushd"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::utils::Consumer;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use lapin::{message::Delivery, Channel, Connection};
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{amqp::consumer::Consumer, events::rabbit::*, Database};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(unused)]
|
||||
@@ -16,11 +15,7 @@ pub struct AckConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for AckConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<Channel>, _: ()) -> Self {
|
||||
Self {
|
||||
db,
|
||||
connection,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::utils::Consumer;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use lapin::{message::Delivery, Channel, Connection};
|
||||
use log::debug;
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{amqp::consumer::Consumer, events::rabbit::*, Database};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(unused)]
|
||||
@@ -17,11 +16,7 @@ pub struct DmCallConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for DmCallConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<Channel>, _: ()) -> Self {
|
||||
Self {
|
||||
db,
|
||||
connection,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::utils::Consumer;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use lapin::{message::Delivery, Channel, Connection};
|
||||
use log::debug;
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{amqp::consumer::Consumer, events::rabbit::*, Database};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(unused)]
|
||||
@@ -17,11 +16,7 @@ pub struct FRAcceptedConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for FRAcceptedConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<Channel>, _: ()) -> Self {
|
||||
Self {
|
||||
db,
|
||||
connection,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::utils::Consumer;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use lapin::{message::Delivery, Channel, Connection};
|
||||
use log::debug;
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{amqp::consumer::Consumer, events::rabbit::*, Database};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(unused)]
|
||||
@@ -17,11 +16,7 @@ pub struct FRReceivedConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for FRReceivedConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<Channel>, _: ()) -> Self {
|
||||
Self {
|
||||
db,
|
||||
connection,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::utils::Consumer;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use lapin::{message::Delivery, Channel, Connection};
|
||||
use log::debug;
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{amqp::consumer::Consumer, events::rabbit::*, Database};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(unused)]
|
||||
@@ -17,11 +16,7 @@ pub struct GenericConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for GenericConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<Channel>, _: ()) -> Self {
|
||||
Self {
|
||||
db,
|
||||
connection,
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::utils::{render_notification_content, Consumer};
|
||||
use crate::utils::render_notification_content;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use lapin::{message::Delivery, Channel, Connection};
|
||||
use revolt_database::{
|
||||
events::rabbit::*, util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member,
|
||||
MessageFlagsValue,
|
||||
amqp::consumer::Consumer, events::rabbit::*,
|
||||
util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member, MessageFlagsValue,
|
||||
};
|
||||
use revolt_models::v0::{MessageFlags, PushNotification};
|
||||
use revolt_result::ToRevoltError;
|
||||
@@ -29,11 +29,7 @@ impl MassMessageConsumer {
|
||||
push: &PushNotification,
|
||||
users: &[String],
|
||||
) -> Result<()> {
|
||||
if let Ok(sessions) = self
|
||||
.db
|
||||
.fetch_sessions_with_subscription(users)
|
||||
.await
|
||||
{
|
||||
if let Ok(sessions) = self.db.fetch_sessions_with_subscription(users).await {
|
||||
let config = revolt_config::config().await;
|
||||
for session in sessions {
|
||||
if let Some(sub) = session.subscription {
|
||||
@@ -72,11 +68,7 @@ impl MassMessageConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for MassMessageConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<Channel>, _: ()) -> Self {
|
||||
Self {
|
||||
db,
|
||||
connection,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::utils::{render_notification_content, Consumer};
|
||||
use crate::utils::render_notification_content;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use lapin::{message::Delivery, Channel, Connection};
|
||||
use log::debug;
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{amqp::consumer::Consumer, events::rabbit::*, Database};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(unused)]
|
||||
@@ -17,11 +17,7 @@ pub struct MessageConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for MessageConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<Channel>, _: ()) -> Self {
|
||||
Self {
|
||||
db,
|
||||
connection,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap, io::Cursor, sync::Arc};
|
||||
|
||||
use crate::utils::Consumer;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use base64::{
|
||||
@@ -15,7 +14,7 @@ use revolt_a2::{
|
||||
},
|
||||
Client, ClientConfig, Endpoint, Error, ErrorBody, ErrorReason, Priority, PushType, Response,
|
||||
};
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{amqp::consumer::Consumer, events::rabbit::*, Database};
|
||||
use revolt_models::v0::{Channel, Message, PushNotification};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -123,11 +122,7 @@ impl ApnsOutboundConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for ApnsOutboundConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<AMQPChannel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<AMQPChannel>, _: ()) -> Self {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
if config.pushd.apn.pkcs8.is_empty()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use crate::utils::Consumer;
|
||||
use anyhow::{bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use fcm_v1::{
|
||||
@@ -10,7 +9,7 @@ use fcm_v1::{
|
||||
};
|
||||
use lapin::{message::Delivery, Channel as AMQPChannel, Connection};
|
||||
use revolt_config::config;
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{amqp::consumer::Consumer, events::rabbit::*, Database};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Custom notification data
|
||||
@@ -126,11 +125,7 @@ pub struct FcmOutboundConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for FcmOutboundConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<AMQPChannel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<AMQPChannel>, _: ()) -> Self {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
Self {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::utils::Consumer;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use base64::{
|
||||
@@ -9,7 +7,9 @@ use base64::{
|
||||
Engine as _,
|
||||
};
|
||||
use lapin::{message::Delivery, Channel as AMQPChannel, Connection};
|
||||
use revolt_database::{events::rabbit::*, util::format_display_name, Database};
|
||||
use revolt_database::{
|
||||
amqp::consumer::Consumer, events::rabbit::*, util::format_display_name, Database,
|
||||
};
|
||||
use web_push::{
|
||||
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
|
||||
WebPushClient, WebPushError, WebPushMessageBuilder,
|
||||
@@ -27,11 +27,7 @@ pub struct VapidOutboundConsumer {
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer for VapidOutboundConsumer {
|
||||
async fn create(
|
||||
db: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<AMQPChannel>,
|
||||
) -> Self {
|
||||
async fn create(db: Database, connection: Arc<Connection>, channel: Arc<AMQPChannel>, _: ()) -> Self {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
if config.pushd.vapid.private_key.is_empty() || config.pushd.vapid.public_key.is_empty() {
|
||||
|
||||
@@ -9,7 +9,7 @@ use lapin::{
|
||||
Channel, Connection, ConnectionProperties,
|
||||
};
|
||||
use revolt_config::{config, Settings};
|
||||
use revolt_database::Database;
|
||||
use revolt_database::{Database, amqp::consumer::{Consumer, Delegate}};
|
||||
use tokio::signal::ctrl_c;
|
||||
|
||||
mod consumers;
|
||||
@@ -23,8 +23,6 @@ use consumers::{
|
||||
outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer},
|
||||
};
|
||||
|
||||
use crate::utils::{Consumer, Delegate};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn main() {
|
||||
// Configure logging and environment
|
||||
@@ -275,11 +273,12 @@ where
|
||||
consumer.tag()
|
||||
);
|
||||
|
||||
let delegate = Delegate(
|
||||
let delegate = Delegate::new(
|
||||
F::create(
|
||||
db.clone(),
|
||||
connection.clone(),
|
||||
channel.clone(),
|
||||
()
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
mod renderer;
|
||||
mod consumer;
|
||||
|
||||
pub use renderer::render_notification_content;
|
||||
pub use consumer::{Consumer, Delegate};
|
||||
pub use renderer::render_notification_content;
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "revolt-searchd"
|
||||
version = "0.13.7"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
revolt-database = { workspace = true }
|
||||
revolt-config = { workspace = true }
|
||||
revolt-search = { workspace = true }
|
||||
lapin = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
@@ -0,0 +1,59 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use lapin::{Channel, Connection, message::Delivery, options::{BasicAckOptions, BasicRejectOptions}};
|
||||
use async_trait::async_trait;
|
||||
use revolt_database::{Database, events::rabbit::ChannelDeletePayload, amqp::consumer::Consumer};
|
||||
use revolt_search::ElasticsearchClient;
|
||||
use anyhow::Result;
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Clone)]
|
||||
pub struct ChannelDeleteConsumer {
|
||||
client: ElasticsearchClient,
|
||||
database: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer<ElasticsearchClient> for ChannelDeleteConsumer {
|
||||
async fn create(database: Database, connection: Arc<Connection>, channel: Arc<Channel>, client: ElasticsearchClient) -> Self {
|
||||
Self {
|
||||
client,
|
||||
database,
|
||||
connection,
|
||||
channel,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel(&self) -> &Arc<Channel> {
|
||||
&self.channel
|
||||
}
|
||||
|
||||
async fn consume(&self, delivery: Delivery) -> Result<()> {
|
||||
let payload = serde_json::from_slice::<ChannelDeletePayload>(&delivery.data)
|
||||
.expect("Failed to decode message");
|
||||
log::debug!("Received channel delete {payload:?}");
|
||||
|
||||
if self
|
||||
.client
|
||||
.delete_channel(&payload.channel_id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
self.channel
|
||||
.basic_ack(delivery.delivery_tag, BasicAckOptions::default())
|
||||
.await
|
||||
.expect("Failed to ack");
|
||||
} else {
|
||||
self.channel
|
||||
.basic_reject(delivery.delivery_tag, BasicRejectOptions {
|
||||
requeue: true,
|
||||
})
|
||||
.await
|
||||
.expect("Failed to reject");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use lapin::{Channel, Connection, message::Delivery, options::{BasicAckOptions, BasicRejectOptions}};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use revolt_database::{Database, events::rabbit::MessageCreatePayload, amqp::consumer::Consumer};
|
||||
use revolt_search::ElasticsearchClient;
|
||||
use anyhow::Result;
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Clone)]
|
||||
pub struct MessageConsumer {
|
||||
client: ElasticsearchClient,
|
||||
database: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer<ElasticsearchClient> for MessageConsumer {
|
||||
async fn create(database: Database, connection: Arc<Connection>, channel: Arc<Channel>, client: ElasticsearchClient) -> Self {
|
||||
Self {
|
||||
client,
|
||||
database,
|
||||
connection,
|
||||
channel,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel(&self) -> &Arc<Channel> {
|
||||
&self.channel
|
||||
}
|
||||
|
||||
async fn consume(&self, delivery: Delivery) -> Result<()> {
|
||||
let payload = serde_json::from_slice::<MessageCreatePayload>(&delivery.data)
|
||||
.expect("Failed to decode message");
|
||||
log::debug!("Received message {payload:?}");
|
||||
|
||||
if self
|
||||
.client
|
||||
.index_message(&self.database, payload.message, payload.user)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
self.channel
|
||||
.basic_ack(delivery.delivery_tag, BasicAckOptions::default())
|
||||
.await
|
||||
.expect("Failed to ack");
|
||||
} else {
|
||||
self.channel
|
||||
.basic_reject(delivery.delivery_tag, BasicRejectOptions {
|
||||
requeue: true,
|
||||
})
|
||||
.await
|
||||
.expect("Failed to reject");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use lapin::{Channel, Connection, message::Delivery, options::{BasicAckOptions, BasicRejectOptions}};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use revolt_database::{Database, events::rabbit::MessageDeletePayload, amqp::consumer::Consumer};
|
||||
use revolt_search::ElasticsearchClient;
|
||||
use anyhow::Result;
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Clone)]
|
||||
pub struct MessageDeleteConsumer {
|
||||
client: ElasticsearchClient,
|
||||
database: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
}
|
||||
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer<ElasticsearchClient> for MessageDeleteConsumer {
|
||||
async fn create(database: Database, connection: Arc<Connection>, channel: Arc<Channel>, client: ElasticsearchClient) -> Self {
|
||||
Self {
|
||||
client,
|
||||
database,
|
||||
connection,
|
||||
channel,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel(&self) -> &Arc<Channel> {
|
||||
&self.channel
|
||||
}
|
||||
|
||||
async fn consume(&self, delivery: Delivery) -> Result<()> {
|
||||
let payload = serde_json::from_slice::<MessageDeletePayload>(&delivery.data)
|
||||
.expect("Failed to decode message");
|
||||
log::debug!("Received message delete {payload:?}");
|
||||
|
||||
if self
|
||||
.client
|
||||
.delete_message(&payload.message_id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
self.channel
|
||||
.basic_ack(delivery.delivery_tag, BasicAckOptions::default())
|
||||
.await
|
||||
.expect("Failed to ack");
|
||||
} else {
|
||||
self.channel
|
||||
.basic_reject(delivery.delivery_tag, BasicRejectOptions {
|
||||
requeue: true,
|
||||
})
|
||||
.await
|
||||
.expect("Failed to reject");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
use lapin::{Channel, Connection, message::Delivery, options::{BasicAckOptions, BasicRejectOptions}};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use revolt_database::{Database, events::rabbit::MessageEditPayload, amqp::consumer::Consumer};
|
||||
use revolt_search::ElasticsearchClient;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessageEditConsumer {
|
||||
client: ElasticsearchClient,
|
||||
database: Database,
|
||||
connection: Arc<Connection>,
|
||||
channel: Arc<Channel>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Consumer<ElasticsearchClient> for MessageEditConsumer {
|
||||
async fn create(database: Database, connection: Arc<Connection>, channel: Arc<Channel>, client: ElasticsearchClient) -> Self {
|
||||
Self {
|
||||
client,
|
||||
database,
|
||||
connection,
|
||||
channel,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel(&self) -> &Arc<Channel> {
|
||||
&self.channel
|
||||
}
|
||||
|
||||
async fn consume(&self, delivery: Delivery) -> Result<()> {
|
||||
let payload = serde_json::from_slice::<MessageEditPayload>(&delivery.data)
|
||||
.expect("Failed to decode message");
|
||||
log::debug!("Received edit message {payload:?}");
|
||||
|
||||
if self
|
||||
.client
|
||||
.edit_message(&self.database, payload.message, payload.user)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
self.channel
|
||||
.basic_ack(delivery.delivery_tag, BasicAckOptions::default())
|
||||
.await
|
||||
.expect("Failed to ack");
|
||||
} else {
|
||||
self.channel
|
||||
.basic_reject(delivery.delivery_tag, BasicRejectOptions {
|
||||
requeue: true,
|
||||
})
|
||||
.await
|
||||
.expect("Failed to reject");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mod channel_delete;
|
||||
mod message;
|
||||
mod message_delete;
|
||||
mod message_edit;
|
||||
|
||||
pub use channel_delete::*;
|
||||
pub use message::*;
|
||||
pub use message_delete::*;
|
||||
pub use message_edit::*;
|
||||
@@ -0,0 +1,36 @@
|
||||
use revolt_config::capture_error;
|
||||
use revolt_database::Database;
|
||||
use revolt_search::ElasticsearchClient;
|
||||
|
||||
pub async fn index_existing_messages(db: Database, client: ElasticsearchClient) {
|
||||
log::info!("Starting bulk indexing.");
|
||||
|
||||
let mut generator = db
|
||||
.fetch_all_messages()
|
||||
.await
|
||||
.expect("Database query failed");
|
||||
|
||||
let mut chunk = Vec::new();
|
||||
|
||||
while let Some(message) = generator.next().await.expect("Failed to fetch batch") {
|
||||
chunk.push(message);
|
||||
|
||||
if chunk.len() >= 1000
|
||||
&& let Err(e) = client
|
||||
.bulk_index_messages(&db, std::mem::take(&mut chunk))
|
||||
.await
|
||||
{
|
||||
log::error!("Error bulk indexing messages: {e}");
|
||||
capture_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
if !chunk.is_empty()
|
||||
&& let Err(e) = client.bulk_index_messages(&db, chunk).await
|
||||
{
|
||||
log::error!("Error bulk indexing messages: {e}");
|
||||
capture_error(&e);
|
||||
}
|
||||
|
||||
log::info!("Finished bulk indexing.")
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
use lapin::{
|
||||
options::{BasicConsumeOptions, ExchangeDeclareOptions, QueueBindOptions, QueueDeclareOptions},
|
||||
types::{AMQPValue, FieldTable},
|
||||
Channel, Connection, ConnectionProperties,
|
||||
};
|
||||
use log::info;
|
||||
use revolt_config::{Settings, config, configure};
|
||||
use revolt_database::{Database, DatabaseInfo, amqp::consumer::{Consumer, Delegate}};
|
||||
use revolt_search::ElasticsearchClient;
|
||||
use tokio::{signal::ctrl_c, spawn};
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
mod consumers;
|
||||
mod index;
|
||||
|
||||
async fn _main() {
|
||||
configure!(api);
|
||||
let config = config().await;
|
||||
|
||||
let db = DatabaseInfo::Auto.connect().await.unwrap();
|
||||
|
||||
let client = ElasticsearchClient::new(
|
||||
&config.elasticsearch.host,
|
||||
config.elasticsearch.port,
|
||||
config.elasticsearch.api_key.clone(),
|
||||
);
|
||||
|
||||
if std::env::var("REMAKE_MESSAGES_INDEX")
|
||||
.as_deref()
|
||||
.is_ok_and(|v| v == "1")
|
||||
{
|
||||
if client.delete_indexes().await.is_err() {
|
||||
log::info!("Index does not existing, skipping.")
|
||||
};
|
||||
|
||||
client.setup_indexes().await.unwrap();
|
||||
};
|
||||
|
||||
let connection = Arc::new(
|
||||
Connection::connect(
|
||||
&format!(
|
||||
"amqp://{}:{}@{}:{}",
|
||||
&config.rabbit.username,
|
||||
&config.rabbit.password,
|
||||
&config.rabbit.host,
|
||||
&config.rabbit.port,
|
||||
),
|
||||
ConnectionProperties::default(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to connect to RabbitMQ"),
|
||||
);
|
||||
|
||||
let mut channels = Vec::new();
|
||||
|
||||
channels.push(
|
||||
make_queue_and_consume::<consumers::MessageConsumer>(
|
||||
&client,
|
||||
&db,
|
||||
&connection,
|
||||
&config,
|
||||
&config.elasticsearch.message_queue,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
channels.push(
|
||||
make_queue_and_consume::<consumers::MessageEditConsumer>(
|
||||
&client,
|
||||
&db,
|
||||
&connection,
|
||||
&config,
|
||||
&config.elasticsearch.message_edit_queue,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
channels.push(
|
||||
make_queue_and_consume::<consumers::MessageDeleteConsumer>(
|
||||
&client,
|
||||
&db,
|
||||
&connection,
|
||||
&config,
|
||||
&config.elasticsearch.message_delete_queue,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
channels.push(
|
||||
make_queue_and_consume::<consumers::ChannelDeleteConsumer>(
|
||||
&client,
|
||||
&db,
|
||||
&connection,
|
||||
&config,
|
||||
&config.elasticsearch.channel_delete_queue,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
let mut task = None;
|
||||
|
||||
if std::env::var("INDEX_ALL_MESSAGES")
|
||||
.as_deref()
|
||||
.is_ok_and(|v| v == "1")
|
||||
{
|
||||
task = Some(spawn(index::index_existing_messages(db, client.clone())));
|
||||
}
|
||||
|
||||
ctrl_c().await.unwrap();
|
||||
|
||||
for channel in channels {
|
||||
let _ = channel.close(0, "close".into()).await;
|
||||
}
|
||||
|
||||
if let Some(task) = task {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn make_queue_and_consume<F: Consumer<ElasticsearchClient>>(
|
||||
client: &ElasticsearchClient,
|
||||
db: &Database,
|
||||
connection: &Arc<Connection>,
|
||||
config: &Settings,
|
||||
queue_name: &str,
|
||||
) -> Arc<Channel> {
|
||||
let channel = Arc::new(connection.create_channel().await.unwrap());
|
||||
|
||||
channel
|
||||
.exchange_declare(
|
||||
config.elasticsearch.exchange.clone().into(),
|
||||
lapin::ExchangeKind::Direct,
|
||||
ExchangeDeclareOptions {
|
||||
durable: true,
|
||||
..Default::default()
|
||||
},
|
||||
FieldTable::default(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to declare exchange");
|
||||
|
||||
let mut table = FieldTable::default();
|
||||
table.insert("x-queue-type".try_into().unwrap(), AMQPValue::LongString("quorum".into()));
|
||||
|
||||
let args = QueueDeclareOptions {
|
||||
durable: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
channel
|
||||
.queue_declare(queue_name.into(), args, table)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
channel
|
||||
.queue_bind(
|
||||
queue_name.into(),
|
||||
config.elasticsearch.exchange.clone().into(),
|
||||
queue_name.into(),
|
||||
QueueBindOptions::default(),
|
||||
FieldTable::default(),
|
||||
)
|
||||
.await
|
||||
.expect(
|
||||
"This probably means the revolt.messages exchange does not exist in rabbitmq!",
|
||||
);
|
||||
|
||||
|
||||
let consumer = channel
|
||||
.basic_consume(
|
||||
queue_name.into(),
|
||||
"".into(),
|
||||
BasicConsumeOptions {
|
||||
no_ack: true,
|
||||
..Default::default()
|
||||
},
|
||||
FieldTable::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
info!(
|
||||
"Consuming routing key {} as queue {}, tag {}",
|
||||
queue_name,
|
||||
queue_name,
|
||||
consumer.tag()
|
||||
);
|
||||
|
||||
let delegate = Delegate::new(
|
||||
F::create(
|
||||
db.clone(),
|
||||
connection.clone(),
|
||||
channel.clone(),
|
||||
client.clone(),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
consumer.set_delegate(delegate);
|
||||
|
||||
channel
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
_main().await
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-voice-ingress"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver};
|
||||
use livekit_protocol::TrackType;
|
||||
use revolt_database::{
|
||||
AMQP, Database, PartialMessage, SystemMessage, events::client::EventV1, iso8601_timestamp::{Duration, Timestamp}, util::reference::Reference, voice::{
|
||||
RoomMetadata, UserVoiceChannel, VoiceClient, create_voice_state, delete_channel_voice_state, delete_voice_state, get_call_notification_recipients, get_user_moved_from_voice, get_user_moved_to_voice, get_voice_channel_members, set_channel_call_started_system_message, take_channel_call_started_system_message, update_voice_state_tracks
|
||||
}
|
||||
events::client::EventV1,
|
||||
iso8601_timestamp::{Duration, Timestamp},
|
||||
util::reference::Reference,
|
||||
voice::{
|
||||
create_voice_state, delete_channel_voice_state, delete_voice_state,
|
||||
get_user_moved_from_voice, get_user_moved_to_voice, update_voice_state_tracks,
|
||||
RoomMetadata, UserVoiceChannel, VoiceClient,
|
||||
},
|
||||
Database, AMQP,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::{Result, ToRevoltError};
|
||||
use rocket::{post, State};
|
||||
use rocket_empty::EmptyResponse;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::guard::AuthHeader;
|
||||
|
||||
@@ -17,12 +21,12 @@ use crate::guard::AuthHeader;
|
||||
pub async fn ingress(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
amqp: &State<AMQP>,
|
||||
_amqp: &State<AMQP>,
|
||||
node: &str,
|
||||
auth_header: AuthHeader<'_>,
|
||||
body: &str,
|
||||
) -> Result<EmptyResponse> {
|
||||
log::debug!("received event: {body}");
|
||||
log::debug!("received event: {body:?}");
|
||||
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
@@ -59,18 +63,16 @@ pub async fn ingress(
|
||||
let channel_id = channel_id.to_internal_error()?;
|
||||
let user_id = user_id.to_internal_error()?;
|
||||
let server_id = room_metadata.to_internal_error()?.server;
|
||||
let voice_channel = UserVoiceChannel {
|
||||
let channel = UserVoiceChannel {
|
||||
id: channel_id.clone(),
|
||||
server_id: server_id.clone(),
|
||||
};
|
||||
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
let joined_at = Timestamp::UNIX_EPOCH
|
||||
.checked_add(Duration::seconds(event.created_at))
|
||||
.unwrap();
|
||||
|
||||
let voice_state = create_voice_state(&voice_channel, user_id, joined_at).await?;
|
||||
let voice_state = create_voice_state(&channel, user_id, joined_at).await?;
|
||||
|
||||
// Only publish one event when a user is moved from one channel to another.
|
||||
if let Some(moved_from) = get_user_moved_to_voice(channel_id, user_id).await? {
|
||||
@@ -91,66 +93,63 @@ pub async fn ingress(
|
||||
.await;
|
||||
};
|
||||
|
||||
let participants = voice_client.get_room_participants(node, channel_id).await?;
|
||||
// TODO: fix `num_participants` being incorrect sometimes see (#457)
|
||||
// First user who joined - send call started system message.
|
||||
// if event.room.as_ref().unwrap().num_participants == 1 {
|
||||
// let user = Reference::from_unchecked(user_id).as_user(db).await?;
|
||||
|
||||
if participants.len() == 1 {
|
||||
let user = Reference::from_unchecked(user_id).as_user(db).await?;
|
||||
let message_id = Ulid::from_datetime(
|
||||
Timestamp::UNIX_EPOCH
|
||||
.checked_add(Duration::seconds(event.created_at))
|
||||
.unwrap()
|
||||
.into(),
|
||||
)
|
||||
.to_string();
|
||||
// let message_id =
|
||||
// Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap())
|
||||
// .to_string();
|
||||
|
||||
let mut call_started_message = SystemMessage::CallStarted {
|
||||
by: user_id.to_string(),
|
||||
finished_at: None,
|
||||
}
|
||||
.into_message(channel_id.clone());
|
||||
// let mut call_started_message = SystemMessage::CallStarted {
|
||||
// by: user_id.to_string(),
|
||||
// finished_at: None,
|
||||
// }
|
||||
// .into_message(channel.id().to_string());
|
||||
|
||||
call_started_message.id = message_id;
|
||||
// call_started_message.id = message_id;
|
||||
|
||||
set_channel_call_started_system_message(channel_id, &call_started_message.id)
|
||||
.await?;
|
||||
// set_channel_call_started_system_message(channel.id(), &call_started_message.id)
|
||||
// .await?;
|
||||
|
||||
call_started_message
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
v0::MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
// call_started_message
|
||||
// .send(
|
||||
// db,
|
||||
// Some(amqp),
|
||||
// v0::MessageAuthor::System {
|
||||
// username: &user.username,
|
||||
// avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
// },
|
||||
// None,
|
||||
// None,
|
||||
// &channel,
|
||||
// false,
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
let recipients = get_call_notification_recipients(channel_id, user_id).await?;
|
||||
let now = joined_at.format_short().to_string();
|
||||
// let recipients = get_call_notification_recipients(&channel_id, &user_id).await?;
|
||||
// let now = joined_at.format_short().to_string();
|
||||
|
||||
if let Err(e) = amqp
|
||||
.dm_call_updated(&user.id, channel_id, Some(&now), false, recipients)
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_error(&e);
|
||||
}
|
||||
}
|
||||
// if let Err(e) = amqp
|
||||
// .dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients)
|
||||
// .await
|
||||
// {
|
||||
// revolt_config::capture_error(&e);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// User left a channel
|
||||
"participant_left" => {
|
||||
let channel_id = channel_id.to_internal_error()?;
|
||||
let user_id = user_id.to_internal_error()?;
|
||||
let server_id = room_metadata.to_internal_error()?.server;
|
||||
let voice_channel = UserVoiceChannel {
|
||||
let channel = UserVoiceChannel {
|
||||
id: channel_id.clone(),
|
||||
server_id: server_id.clone(),
|
||||
};
|
||||
|
||||
delete_voice_state(&voice_channel, user_id).await?;
|
||||
delete_voice_state(&channel, user_id).await?;
|
||||
|
||||
// Dont send leave event when a user is moved
|
||||
if get_user_moved_from_voice(channel_id, user_id)
|
||||
@@ -165,47 +164,49 @@ pub async fn ingress(
|
||||
.await;
|
||||
};
|
||||
|
||||
// See above for why this is commented out
|
||||
|
||||
// // Update CallStarted system message if everyone has left with the end time
|
||||
let members = get_voice_channel_members(&voice_channel).await?;
|
||||
// let members = get_voice_channel_members(channel_id).await?;
|
||||
|
||||
if members.is_none_or(|m| m.is_empty()) {
|
||||
// The channel is empty so send out an "end" message for ringing
|
||||
if let Err(e) = amqp
|
||||
.dm_call_updated(user_id, channel_id, None, true, None)
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_internal_error!(&e);
|
||||
}
|
||||
// if members.is_none_or(|m| m.is_empty()) {
|
||||
// // The channel is empty so send out an "end" message for ringing
|
||||
// if let Err(e) = amqp
|
||||
// .dm_call_updated(user_id, channel_id, None, true, None)
|
||||
// .await
|
||||
// {
|
||||
// revolt_config::capture_internal_error!(&e);
|
||||
// }
|
||||
|
||||
if let Some(system_message_id) =
|
||||
take_channel_call_started_system_message(channel_id).await?
|
||||
{
|
||||
// Could have been deleted
|
||||
if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
|
||||
.as_message(db)
|
||||
.await
|
||||
{
|
||||
if let Some(SystemMessage::CallStarted { finished_at, .. }) =
|
||||
&mut message.system
|
||||
{
|
||||
*finished_at = Some(Timestamp::now_utc());
|
||||
// if let Some(system_message_id) =
|
||||
// take_channel_call_started_system_message(channel_id).await?
|
||||
// {
|
||||
// // Could have been deleted
|
||||
// if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
|
||||
// .as_message(db)
|
||||
// .await
|
||||
// {
|
||||
// if let Some(SystemMessage::CallStarted { finished_at, .. }) =
|
||||
// &mut message.system
|
||||
// {
|
||||
// *finished_at = Some(Timestamp::now_utc());
|
||||
|
||||
message
|
||||
.update(
|
||||
db,
|
||||
PartialMessage {
|
||||
system: message.system.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
Vec::new(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
// message
|
||||
// .update(
|
||||
// db,
|
||||
// PartialMessage {
|
||||
// system: message.system.clone(),
|
||||
// ..Default::default()
|
||||
// },
|
||||
// Vec::new(),
|
||||
// )
|
||||
// .await?;
|
||||
// } else {
|
||||
// log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
|
||||
// }
|
||||
// };
|
||||
// };
|
||||
// }
|
||||
}
|
||||
// Audio/video track was started/stopped/unmuted/muted
|
||||
"track_published" | "track_unpublished" | "track_unmuted" | "track_muted" => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.14.0"
|
||||
version = "0.13.7"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
@@ -77,6 +77,7 @@ revolt-presence = { workspace = true }
|
||||
revolt-result = { workspace = true, features = ["rocket", "okapi"] }
|
||||
revolt-permissions = { workspace = true, features = ["schemas"] }
|
||||
revolt-ratelimits = { workspace = true, features = ["rocket"] }
|
||||
revolt-search = { workspace = true }
|
||||
|
||||
# voice
|
||||
livekit-api = { workspace = true }
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod util;
|
||||
use revolt_config::config;
|
||||
use revolt_database::AMQP;
|
||||
use revolt_ratelimits::rocket as ratelimiter;
|
||||
use revolt_search::ElasticsearchClient;
|
||||
use rocket::{Build, Rocket};
|
||||
use rocket_cors::{AllowedOrigins, CorsOptions};
|
||||
use rocket_prometheus::PrometheusMetrics;
|
||||
@@ -79,6 +80,13 @@ pub async fn web() -> Rocket<Build> {
|
||||
// Ratelimits
|
||||
let ratelimits = ratelimiter::RatelimitStorage::new(util::ratelimits::DeltaRatelimits);
|
||||
|
||||
// Search
|
||||
let elasticsearch = ElasticsearchClient::new(
|
||||
&config.elasticsearch.host,
|
||||
config.elasticsearch.port,
|
||||
config.elasticsearch.api_key.clone(),
|
||||
);
|
||||
|
||||
routes::mount(config, rocket)
|
||||
.attach(prometheus.clone())
|
||||
.mount("/metrics", prometheus)
|
||||
@@ -90,6 +98,7 @@ pub async fn web() -> Rocket<Build> {
|
||||
.manage(cors.clone())
|
||||
.manage(voice_client)
|
||||
.manage(ratelimits)
|
||||
.manage(elasticsearch)
|
||||
.attach(ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
.configure(rocket::Config {
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn invite_bot(
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
|
||||
|
||||
channel
|
||||
.add_user_to_group(db, amqp, &bot_user, &user.id)
|
||||
.add_user_to_group(db, Some(amqp), &bot_user, &user.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use revolt_database::{
|
||||
delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel,
|
||||
UserVoiceChannel, VoiceClient,
|
||||
},
|
||||
AuditLogEntryAction, Channel, Database, PartialChannel, User, AMQP,
|
||||
Channel, Database, PartialChannel, User, AMQP,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -12,8 +12,6 @@ use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
use crate::util::audit_log_reason::AuditLogReason;
|
||||
|
||||
/// # Close Channel
|
||||
///
|
||||
/// Deletes a server channel, leaves a group or closes a group.
|
||||
@@ -24,7 +22,6 @@ pub async fn delete(
|
||||
voice_client: &State<VoiceClient>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
reason: AuditLogReason,
|
||||
target: Reference<'_>,
|
||||
options: v0::OptionsChannelDelete,
|
||||
) -> Result<EmptyResponse> {
|
||||
@@ -53,7 +50,7 @@ pub async fn delete(
|
||||
channel
|
||||
.remove_user_from_group(
|
||||
db,
|
||||
amqp,
|
||||
Some(amqp),
|
||||
&user,
|
||||
None,
|
||||
options.leave_silently.unwrap_or_default(),
|
||||
@@ -66,16 +63,9 @@ pub async fn delete(
|
||||
remove_user_from_voice_channel(voice_client, &user_voice_channel, &user.id).await?;
|
||||
};
|
||||
}
|
||||
Channel::TextChannel { name, server, .. } => {
|
||||
Channel::TextChannel { .. } => {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
||||
channel.delete(db).await?;
|
||||
|
||||
AuditLogEntryAction::ChannelDelete {
|
||||
channel: channel.id().to_string(),
|
||||
name: name.clone(),
|
||||
}
|
||||
.insert(db, server.clone(), reason, user.id, None)
|
||||
.await;
|
||||
channel.delete(db, Some(amqp)).await?;
|
||||
|
||||
delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
voice::{delete_voice_channel, UserVoiceChannel, VoiceClient},
|
||||
AuditLogEntryAction, Channel, Database, FieldsChannel, File, PartialChannel, SystemMessage,
|
||||
User, AMQP,
|
||||
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -10,8 +9,6 @@ use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use validator::Validate;
|
||||
|
||||
use crate::util::audit_log_reason::AuditLogReason;
|
||||
|
||||
/// # Edit Channel
|
||||
///
|
||||
/// Edit a channel object by its id.
|
||||
@@ -22,7 +19,6 @@ pub async fn edit(
|
||||
voice_client: &State<VoiceClient>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
reason: AuditLogReason,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::DataEditChannel>,
|
||||
) -> Result<Json<v0::Channel>> {
|
||||
@@ -95,8 +91,6 @@ pub async fn edit(
|
||||
.ok();
|
||||
}
|
||||
|
||||
let before_channel = channel.clone();
|
||||
|
||||
match &mut channel {
|
||||
Channel::Group {
|
||||
id,
|
||||
@@ -227,9 +221,6 @@ pub async fn edit(
|
||||
v0::FieldsChannel::Voice => {
|
||||
voice.take();
|
||||
}
|
||||
v0::FieldsChannel::Slowmode => {
|
||||
slowmode.take();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -267,33 +258,17 @@ pub async fn edit(
|
||||
_ => return Err(create_error!(InvalidOperation)),
|
||||
};
|
||||
|
||||
let remove = data
|
||||
.remove
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect::<Vec<FieldsChannel>>();
|
||||
|
||||
let before = if before_channel.server().is_some() {
|
||||
Some(before_channel.generate_diff(&partial, &remove))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
channel.update(db, partial.clone(), remove).await?;
|
||||
channel
|
||||
.update(
|
||||
db,
|
||||
partial,
|
||||
data.remove.into_iter().map(|f| f.into()).collect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if channel.voice().is_none() {
|
||||
delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
|
||||
}
|
||||
|
||||
if let Some(before) = before {
|
||||
AuditLogEntryAction::ChannelEdit {
|
||||
channel: channel.id().to_string(),
|
||||
before,
|
||||
after: partial,
|
||||
}
|
||||
.insert(db, channel.server().unwrap().to_string(), reason, user.id, None)
|
||||
.await;
|
||||
};
|
||||
|
||||
Ok(Json(channel.into()))
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ pub async fn add_member(
|
||||
}
|
||||
|
||||
channel
|
||||
.add_user_to_group(db, amqp, &member, &user.id)
|
||||
.add_user_to_group(db, Some(amqp), &member, &user.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use revolt_database::{
|
||||
util::reference::Reference,
|
||||
voice::{is_in_voice_channel, remove_user_from_voice_channel, UserVoiceChannel, VoiceClient},
|
||||
Channel, Database, User, AMQP,
|
||||
AMQP, Channel, Database, User, util::reference::Reference, voice::{UserVoiceChannel, VoiceClient, is_in_voice_channel, remove_user_from_voice_channel}
|
||||
};
|
||||
use revolt_permissions::ChannelPermission;
|
||||
use revolt_result::{create_error, Result};
|
||||
@@ -48,7 +46,7 @@ pub async fn remove_member(
|
||||
}
|
||||
|
||||
channel
|
||||
.remove_user_from_group(db, amqp, &member, Some(&user.id), false)
|
||||
.remove_user_from_group(db, Some(amqp), &member, Some(&user.id), false)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
AuditLogEntryAction, Database, Invite, User,
|
||||
Database, Invite, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -8,8 +8,6 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
use crate::util::audit_log_reason::AuditLogReason;
|
||||
|
||||
/// # Create Invite
|
||||
///
|
||||
/// Creates an invite to this channel.
|
||||
@@ -20,7 +18,6 @@ use crate::util::audit_log_reason::AuditLogReason;
|
||||
pub async fn create_invite(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
reason: AuditLogReason,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<v0::Invite>> {
|
||||
if user.bot.is_some() {
|
||||
@@ -33,16 +30,8 @@ pub async fn create_invite(
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
|
||||
|
||||
let invite = Invite::create_channel_invite(db, &user, &channel).await?;
|
||||
|
||||
if let Some(server_id) = channel.server() {
|
||||
AuditLogEntryAction::InviteCreate {
|
||||
invite: invite.code().to_string(),
|
||||
channel: channel.id().to_string(),
|
||||
}
|
||||
.insert(db, server_id.to_string(), reason, user.id, None)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(Json(invite.into()))
|
||||
Invite::create_channel_invite(db, &user, &channel)
|
||||
.await
|
||||
.map(|invite| invite.into())
|
||||
.map(Json)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::time::Duration;
|
||||
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
AuditLogEntryAction, Database, Message, User,
|
||||
Database, Message, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -11,8 +11,6 @@ use rocket::{serde::json::Json, State};
|
||||
use rocket_empty::EmptyResponse;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::util::audit_log_reason::AuditLogReason;
|
||||
|
||||
/// # Bulk Delete Messages
|
||||
///
|
||||
/// Delete multiple messages you've sent or one you have permission to delete.
|
||||
@@ -25,7 +23,6 @@ use crate::util::audit_log_reason::AuditLogReason;
|
||||
pub async fn bulk_delete_messages(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
reason: AuditLogReason,
|
||||
target: Reference<'_>,
|
||||
options: Json<v0::OptionsBulkDelete>,
|
||||
) -> Result<EmptyResponse> {
|
||||
@@ -54,16 +51,7 @@ pub async fn bulk_delete_messages(
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
|
||||
Message::bulk_delete(db, target.id, options.ids.clone()).await?;
|
||||
|
||||
if let Some(server) = channel.server() {
|
||||
AuditLogEntryAction::MessageBulkDelete {
|
||||
channel: channel.id().to_string(),
|
||||
count: options.ids.len(),
|
||||
}
|
||||
.insert(db, server.to_string(), reason, user.id, None)
|
||||
.await;
|
||||
};
|
||||
|
||||
Ok(EmptyResponse)
|
||||
Message::bulk_delete(db, target.id, options.ids)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, PartialMessage, User,
|
||||
Database, PartialMessage, User, AMQP,
|
||||
};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::Result;
|
||||
@@ -16,6 +16,7 @@ use rocket_empty::EmptyResponse;
|
||||
#[delete("/<target>/messages/<msg>/reactions")]
|
||||
pub async fn clear_reactions(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
@@ -33,11 +34,12 @@ pub async fn clear_reactions(
|
||||
message
|
||||
.update(
|
||||
db,
|
||||
Some(amqp),
|
||||
PartialMessage {
|
||||
reactions: Some(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![]
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
AuditLogEntryAction, Database, User,
|
||||
AMQP, Database, User, util::{permissions::DatabasePermissionQuery, reference::Reference}
|
||||
};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::Result;
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
use crate::util::audit_log_reason::AuditLogReason;
|
||||
|
||||
/// # Delete Message
|
||||
///
|
||||
/// Delete a message you've sent or one you have permission to delete.
|
||||
@@ -16,41 +13,20 @@ use crate::util::audit_log_reason::AuditLogReason;
|
||||
#[delete("/<target>/messages/<msg>", rank = 2)]
|
||||
pub async fn delete(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
reason: AuditLogReason,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let message = msg.as_message_in_channel(db, target.id).await?;
|
||||
|
||||
let channel = if message.author != user.id {
|
||||
if message.author != user.id {
|
||||
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)?;
|
||||
}
|
||||
|
||||
Some(channel)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
message.delete(db).await?;
|
||||
|
||||
if let Some(server) = channel.and_then(|c| c.server().map(|s| s.to_string())) {
|
||||
AuditLogEntryAction::MessageDelete {
|
||||
author: message.author.clone(),
|
||||
channel: message.channel.clone(),
|
||||
}
|
||||
.insert(
|
||||
db,
|
||||
server.to_string(),
|
||||
reason,
|
||||
user.id.clone(),
|
||||
Some(message.author),
|
||||
)
|
||||
.await;
|
||||
};
|
||||
|
||||
Ok(EmptyResponse)
|
||||
message.delete(db, Some(amqp)).await.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user