refactor: delete quark crate (closes #239)
510
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
|
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-bonfire"
|
name = "revolt-bonfire"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ revolt-result = { path = "../core/result" }
|
|||||||
revolt-models = { path = "../core/models" }
|
revolt-models = { path = "../core/models" }
|
||||||
revolt-config = { path = "../core/config" }
|
revolt-config = { path = "../core/config" }
|
||||||
revolt-database = { path = "../core/database" }
|
revolt-database = { path = "../core/database" }
|
||||||
revolt-permissions = { version = "0.7.0", path = "../core/permissions" }
|
revolt-permissions = { version = "0.7.1", path = "../core/permissions" }
|
||||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||||
|
|
||||||
# redis
|
# redis
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-config"
|
name = "revolt-config"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-database"
|
name = "revolt-database"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-config = { version = "0.7.0", path = "../config" }
|
revolt-config = { version = "0.7.1", path = "../config" }
|
||||||
revolt-result = { version = "0.7.0", path = "../result" }
|
revolt-result = { version = "0.7.1", path = "../result" }
|
||||||
revolt-models = { version = "0.7.0", path = "../models", features = [
|
revolt-models = { version = "0.7.1", path = "../models", features = [
|
||||||
"validator",
|
"validator",
|
||||||
] }
|
] }
|
||||||
revolt-presence = { version = "0.7.0", path = "../presence" }
|
revolt-presence = { version = "0.7.1", path = "../presence" }
|
||||||
revolt-permissions = { version = "0.7.0", path = "../permissions", features = [
|
revolt-permissions = { version = "0.7.1", path = "../permissions", features = [
|
||||||
"serde",
|
"serde",
|
||||||
"bson",
|
"bson",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-models"
|
name = "revolt-models"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-config = { version = "0.7.0", path = "../config" }
|
revolt-config = { version = "0.7.1", path = "../config" }
|
||||||
revolt-permissions = { version = "0.7.0", path = "../permissions" }
|
revolt-permissions = { version = "0.7.1", path = "../permissions" }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-permissions"
|
name = "revolt-permissions"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-result = { version = "0.7.0", path = "../result" }
|
revolt-result = { version = "0.7.1", path = "../result" }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
auto_ops = "0.3.0"
|
auto_ops = "0.3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-presence"
|
name = "revolt-presence"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-result"
|
name = "revolt-result"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -12,7 +12,7 @@ description = "Revolt Backend: Result and Error types"
|
|||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
schemas = ["dep:schemars"]
|
schemas = ["dep:schemars"]
|
||||||
rocket = ["dep:rocket", "dep:serde_json"]
|
rocket = ["dep:rocket", "dep:serde_json"]
|
||||||
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi"]
|
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
|
||||||
|
|
||||||
default = ["serde"]
|
default = ["serde"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-delta"
|
name = "revolt-delta"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ impl TestHarness {
|
|||||||
|
|
||||||
let mut stream = self.sub.on_message();
|
let mut stream = self.sub.on_message();
|
||||||
while let Some(item) = stream.next().await {
|
while let Some(item) = stream.next().await {
|
||||||
let item = item.unwrap();
|
|
||||||
let msg_topic = item.get_channel_name();
|
let msg_topic = item.get_channel_name();
|
||||||
let payload: EventV1 = redis_kiss::decode_payload(&item).unwrap();
|
let payload: EventV1 = redis_kiss::decode_payload(&item).unwrap();
|
||||||
|
|
||||||
|
|||||||
14
crates/quark/.gitignore
vendored
@@ -1,14 +0,0 @@
|
|||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
/target/
|
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
|
||||||
|
|
||||||
# Database
|
|
||||||
.minio
|
|
||||||
.data*
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "revolt-quark"
|
|
||||||
version = "0.7.0"
|
|
||||||
edition = "2021"
|
|
||||||
license = "AGPL-3.0-or-later"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[features]
|
|
||||||
mongo = ["mongodb"]
|
|
||||||
rocket_impl = [
|
|
||||||
"rocket",
|
|
||||||
"rocket_empty",
|
|
||||||
"rocket_cors",
|
|
||||||
|
|
||||||
"lru",
|
|
||||||
"dashmap",
|
|
||||||
|
|
||||||
"authifier/database-mongodb",
|
|
||||||
"authifier/rocket_impl",
|
|
||||||
"authifier/okapi_impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
test = ["async-std", "mongo", "mongodb/async-std-runtime", "rocket_impl"]
|
|
||||||
default = ["test"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# Serialisation
|
|
||||||
revolt_optional_struct = "0.2.0"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
validator = { version = "0.16", features = ["derive"] }
|
|
||||||
iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] }
|
|
||||||
|
|
||||||
# Formats
|
|
||||||
bincode = "1.3.3"
|
|
||||||
serde_json = "1.0.78"
|
|
||||||
bson = { version = "2.1.0", features = ["chrono-0_4"] }
|
|
||||||
|
|
||||||
# Spec Generation
|
|
||||||
schemars = "0.8.8"
|
|
||||||
revolt_okapi = "0.9.1"
|
|
||||||
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
|
|
||||||
|
|
||||||
# Databases
|
|
||||||
redis-kiss = { version = "0.1.4" }
|
|
||||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
|
||||||
|
|
||||||
# Async
|
|
||||||
futures = "0.3.19"
|
|
||||||
deadqueue = "0.2.1"
|
|
||||||
async-trait = "0.1.51"
|
|
||||||
async-recursion = "1.0.0"
|
|
||||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
log = "0.4.14"
|
|
||||||
pretty_env_logger = "0.4.0"
|
|
||||||
|
|
||||||
# Util
|
|
||||||
rand = "0.8.5"
|
|
||||||
ulid = "0.5.0"
|
|
||||||
regex = "1.5.5"
|
|
||||||
nanoid = "0.4.0"
|
|
||||||
linkify = "0.8.1"
|
|
||||||
dotenv = "0.15.0"
|
|
||||||
indexmap = "1.9.1"
|
|
||||||
decancer = "1.6.2"
|
|
||||||
impl_ops = "0.1.1"
|
|
||||||
num_enum = "0.5.6"
|
|
||||||
reqwest = "0.11.10"
|
|
||||||
bitfield = "0.13.2"
|
|
||||||
once_cell = "1.17.1"
|
|
||||||
async-lock = "2.6.0"
|
|
||||||
|
|
||||||
lru = { version = "0.7.6", optional = true }
|
|
||||||
dashmap = { version = "5.2.0", optional = true }
|
|
||||||
|
|
||||||
# Web Push
|
|
||||||
base64 = "0.13.0"
|
|
||||||
web-push = "0.7.2"
|
|
||||||
|
|
||||||
# Implementations
|
|
||||||
rocket_http = { optional = true, version = "0.5.0-rc.2" }
|
|
||||||
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false, features = [
|
|
||||||
"json",
|
|
||||||
] }
|
|
||||||
rocket_empty = { version = "0.1.1", optional = true, features = ["schema"] }
|
|
||||||
rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" }
|
|
||||||
|
|
||||||
# Authifier
|
|
||||||
authifier = { version = "1.0.8", features = ["async-std-runtime"] }
|
|
||||||
|
|
||||||
# Sentry
|
|
||||||
sentry = "0.31.5"
|
|
||||||
|
|
||||||
# Core
|
|
||||||
revolt-result = { path = "../core/result", features = ["serde", "schemas"] }
|
|
||||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
|
||||||
revolt-database = { path = "../core/database" }
|
|
||||||
revolt-models = { path = "../core/models" }
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../LICENSE
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
||||||
<style type="text/css">
|
|
||||||
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
|
||||||
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
|
|
||||||
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
|
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
|
||||||
<table align="center" role="presentation">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td width="600">
|
|
||||||
<![endif]-->
|
|
||||||
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
|
|
||||||
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
|
|
||||||
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
|
|
||||||
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Account Deletion</h1>
|
|
||||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately.</p>
|
|
||||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
|
|
||||||
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Confirm</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="text-muted text-center" style="color: #718096;" align="center">
|
|
||||||
This email is intended for {{email}}<br>
|
|
||||||
Sent from Revolt<br>
|
|
||||||
Made in the EU
|
|
||||||
</div>
|
|
||||||
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<style>
|
|
||||||
/* Add custom classes and styles that you want inlined here */
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
alt="Revolt Logo"
|
|
||||||
class="ax-center my-10 w-24"
|
|
||||||
src="https://app.revolt.chat/assets/logo_round.png"
|
|
||||||
/>
|
|
||||||
<div class="card p-6 p-lg-10 space-y-4">
|
|
||||||
<h1 class="h3 fw-700">Account Deletion</h1>
|
|
||||||
<p>
|
|
||||||
You requested to have your account deleted, if you did not perform
|
|
||||||
this action please take measures to secure your account immediately.
|
|
||||||
</p>
|
|
||||||
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted text-center my-6">
|
|
||||||
This email is intended for {{email}}<br />
|
|
||||||
Sent from Revolt<br />
|
|
||||||
Made in the EU
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately.
|
|
||||||
|
|
||||||
Please navigate to: {{url}}
|
|
||||||
|
|
||||||
This email is intended for {{email}}
|
|
||||||
Sent by Revolt
|
|
||||||
Made in the EU
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
||||||
<style type="text/css">
|
|
||||||
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
|
||||||
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
|
|
||||||
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
|
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
|
||||||
<table align="center" role="presentation">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td width="600">
|
|
||||||
<![endif]-->
|
|
||||||
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
|
|
||||||
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
|
|
||||||
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
|
|
||||||
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Password Reset</h1>
|
|
||||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">You requested a password reset, click below to continue.</p>
|
|
||||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
|
|
||||||
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Reset</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="text-muted text-center" style="color: #718096;" align="center">
|
|
||||||
This email is intended for {{email}}<br>
|
|
||||||
Sent from Revolt<br>
|
|
||||||
Made in the EU
|
|
||||||
</div>
|
|
||||||
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<style>
|
|
||||||
/* Add custom classes and styles that you want inlined here */
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
class="ax-center my-10 w-24"
|
|
||||||
src="https://app.revolt.chat/assets/logo_round.png"
|
|
||||||
/>
|
|
||||||
<div class="card p-6 p-lg-10 space-y-4">
|
|
||||||
<h1 class="h3 fw-700">Password Reset</h1>
|
|
||||||
<p>You requested a password reset, click below to continue.</p>
|
|
||||||
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Reset</a>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted text-center my-6">
|
|
||||||
This email is intended for {{email}}<br />
|
|
||||||
Sent from Revolt<br />
|
|
||||||
Made in the EU
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
You requested a password reset, if you did not perform this action you can safely ignore this email.
|
|
||||||
|
|
||||||
Please navigate to: {{url}}
|
|
||||||
|
|
||||||
This email is intended for {{email}}
|
|
||||||
Sent by Revolt
|
|
||||||
Made in the EU
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
||||||
<style type="text/css">
|
|
||||||
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
|
||||||
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
|
|
||||||
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
|
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
|
||||||
<table align="center" role="presentation">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td width="600">
|
|
||||||
<![endif]-->
|
|
||||||
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
|
|
||||||
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
|
|
||||||
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
|
|
||||||
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Almost there!</h1>
|
|
||||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">To complete your sign up, we just need to verify your email.</p>
|
|
||||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
|
|
||||||
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Confirm</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="text-muted text-center" style="color: #718096;" align="center">
|
|
||||||
This email is intended for {{email}}<br>
|
|
||||||
Sent from Revolt<br>
|
|
||||||
Made in the EU
|
|
||||||
</div>
|
|
||||||
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
|
|
||||||
 
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<style>
|
|
||||||
/* Add custom classes and styles that you want inlined here */
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-light">
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
alt="Revolt Logo"
|
|
||||||
class="ax-center my-10 w-24"
|
|
||||||
src="https://app.revolt.chat/assets/logo_round.png"
|
|
||||||
/>
|
|
||||||
<div class="card p-6 p-lg-10 space-y-4">
|
|
||||||
<h1 class="h3 fw-700">Almost there!</h1>
|
|
||||||
<p>To complete your sign up, we just need to verify your email.</p>
|
|
||||||
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted text-center my-6">
|
|
||||||
This email is intended for {{email}}<br />
|
|
||||||
Sent from Revolt<br />
|
|
||||||
Made in the EU
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Almost there!
|
|
||||||
To complete your sign up, we just need to verify your email.
|
|
||||||
|
|
||||||
Please navigate to: {{url}}
|
|
||||||
|
|
||||||
This email is intended for {{email}}
|
|
||||||
Sent by Revolt
|
|
||||||
Made in the EU
|
|
||||||
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1,85 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use crate::r#impl::{DummyDb, MongoDb};
|
|
||||||
use crate::AbstractDatabase;
|
|
||||||
|
|
||||||
/// Database information to use to create a client
|
|
||||||
pub enum DatabaseInfo {
|
|
||||||
/// Auto-detect the database in use
|
|
||||||
Auto,
|
|
||||||
/// Use the mock database
|
|
||||||
Dummy,
|
|
||||||
/// Connect to MongoDB
|
|
||||||
MongoDb(String),
|
|
||||||
/// Use existing MongoDB connection
|
|
||||||
MongoDbFromClient(mongodb::Client),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Database
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Database {
|
|
||||||
/// Mock database
|
|
||||||
Dummy(DummyDb),
|
|
||||||
/// MongoDB database
|
|
||||||
MongoDb(MongoDb),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DatabaseInfo {
|
|
||||||
/// Create a database client from the given database information
|
|
||||||
#[async_recursion]
|
|
||||||
pub async fn connect(self) -> Result<Database, String> {
|
|
||||||
Ok(match self {
|
|
||||||
DatabaseInfo::Auto => {
|
|
||||||
if let Ok(test_db) = env::var("TEST_DB") {
|
|
||||||
return match test_db.as_str() {
|
|
||||||
"REFERENCE" => DatabaseInfo::Dummy.connect().await,
|
|
||||||
"MONGODB" => {
|
|
||||||
DatabaseInfo::MongoDb(env::var("MONGODB").expect("`MONGODB` env"))
|
|
||||||
.connect()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(uri) = env::var("MONGODB") {
|
|
||||||
return DatabaseInfo::MongoDb(uri).connect().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseInfo::Dummy.connect().await?
|
|
||||||
}
|
|
||||||
DatabaseInfo::Dummy => Database::Dummy(DummyDb),
|
|
||||||
DatabaseInfo::MongoDb(uri) => {
|
|
||||||
let client = mongodb::Client::with_uri_str(uri)
|
|
||||||
.await
|
|
||||||
.map_err(|_| "Failed to init db connection.".to_string())?;
|
|
||||||
|
|
||||||
Database::MongoDb(MongoDb(client))
|
|
||||||
}
|
|
||||||
DatabaseInfo::MongoDbFromClient(client) => Database::MongoDb(MongoDb(client)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Database {
|
|
||||||
type Target = dyn AbstractDatabase;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
match self {
|
|
||||||
Database::Dummy(dummy) => dummy,
|
|
||||||
Database::MongoDb(mongo) => mongo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Database> for revolt_database::Database {
|
|
||||||
fn from(val: Database) -> Self {
|
|
||||||
match val {
|
|
||||||
Database::Dummy(_) => revolt_database::Database::Reference(Default::default()),
|
|
||||||
Database::MongoDb(MongoDb(client)) => revolt_database::Database::MongoDb(
|
|
||||||
revolt_database::MongoDb(client, "revolt".to_string()),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
use authifier::AuthifierEvent;
|
|
||||||
use revolt_models::v0::{FieldsWebhook, PartialWebhook, Webhook};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::models::channel::{FieldsChannel, PartialChannel};
|
|
||||||
use crate::models::message::{AppendMessage, PartialMessage};
|
|
||||||
use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer};
|
|
||||||
use crate::models::server_member::{FieldsMember, MemberCompositeKey, PartialMember};
|
|
||||||
use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus};
|
|
||||||
use crate::models::{Channel, Emoji, Member, Message, Report, Server, User, UserSettings};
|
|
||||||
use crate::Error;
|
|
||||||
|
|
||||||
/// WebSocket Client Errors
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
#[serde(tag = "error")]
|
|
||||||
pub enum WebSocketError {
|
|
||||||
LabelMe,
|
|
||||||
InternalError { at: String },
|
|
||||||
InvalidSession,
|
|
||||||
OnboardingNotFinished,
|
|
||||||
AlreadyAuthenticated,
|
|
||||||
MalformedData { msg: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ping Packet
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum Ping {
|
|
||||||
Binary(Vec<u8>),
|
|
||||||
Number(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Untagged Error
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum ErrorEvent {
|
|
||||||
Error(WebSocketError),
|
|
||||||
APIError(Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Protocol Events
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum EventV1 {
|
|
||||||
/// Multiple events
|
|
||||||
Bulk { v: Vec<EventV1> },
|
|
||||||
|
|
||||||
/// Successfully authenticated
|
|
||||||
Authenticated,
|
|
||||||
|
|
||||||
/// Basic data to cache
|
|
||||||
Ready {
|
|
||||||
users: Vec<User>,
|
|
||||||
servers: Vec<Server>,
|
|
||||||
channels: Vec<Channel>,
|
|
||||||
members: Vec<Member>,
|
|
||||||
emojis: Option<Vec<Emoji>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Ping response
|
|
||||||
Pong { data: Ping },
|
|
||||||
|
|
||||||
/// New message
|
|
||||||
Message(Message),
|
|
||||||
|
|
||||||
/// Update existing message
|
|
||||||
MessageUpdate {
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
data: PartialMessage,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Append information to existing message
|
|
||||||
MessageAppend {
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
append: AppendMessage,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Delete message
|
|
||||||
MessageDelete { id: String, channel: String },
|
|
||||||
|
|
||||||
/// New reaction to a message
|
|
||||||
MessageReact {
|
|
||||||
id: String,
|
|
||||||
channel_id: String,
|
|
||||||
user_id: String,
|
|
||||||
emoji_id: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Remove user's reaction from message
|
|
||||||
MessageUnreact {
|
|
||||||
id: String,
|
|
||||||
channel_id: String,
|
|
||||||
user_id: String,
|
|
||||||
emoji_id: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Remove a reaction from message
|
|
||||||
MessageRemoveReaction {
|
|
||||||
id: String,
|
|
||||||
channel_id: String,
|
|
||||||
emoji_id: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Bulk delete messages
|
|
||||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
|
||||||
|
|
||||||
/// New channel
|
|
||||||
ChannelCreate(Channel),
|
|
||||||
|
|
||||||
/// Update existing channel
|
|
||||||
ChannelUpdate {
|
|
||||||
id: String,
|
|
||||||
data: PartialChannel,
|
|
||||||
clear: Vec<FieldsChannel>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Delete channel
|
|
||||||
ChannelDelete { id: String },
|
|
||||||
|
|
||||||
/// User joins a group
|
|
||||||
ChannelGroupJoin { id: String, user: String },
|
|
||||||
|
|
||||||
/// User leaves a group
|
|
||||||
ChannelGroupLeave { id: String, user: String },
|
|
||||||
|
|
||||||
/// User started typing in a channel
|
|
||||||
ChannelStartTyping { id: String, user: String },
|
|
||||||
|
|
||||||
/// User stopped typing in a channel
|
|
||||||
ChannelStopTyping { id: String, user: String },
|
|
||||||
|
|
||||||
/// User acknowledged message in channel
|
|
||||||
ChannelAck {
|
|
||||||
id: String,
|
|
||||||
user: String,
|
|
||||||
message_id: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// New server
|
|
||||||
ServerCreate {
|
|
||||||
id: String,
|
|
||||||
server: Server,
|
|
||||||
channels: Vec<Channel>,
|
|
||||||
emojis: Vec<Emoji>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Update existing server
|
|
||||||
ServerUpdate {
|
|
||||||
id: String,
|
|
||||||
data: PartialServer,
|
|
||||||
clear: Vec<FieldsServer>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Delete server
|
|
||||||
ServerDelete { id: String },
|
|
||||||
|
|
||||||
/// Update existing server member
|
|
||||||
ServerMemberUpdate {
|
|
||||||
id: MemberCompositeKey,
|
|
||||||
data: PartialMember,
|
|
||||||
clear: Vec<FieldsMember>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// User joins server
|
|
||||||
ServerMemberJoin { id: String, user: String },
|
|
||||||
|
|
||||||
/// User left server
|
|
||||||
ServerMemberLeave { id: String, user: String },
|
|
||||||
|
|
||||||
/// Server role created or updated
|
|
||||||
ServerRoleUpdate {
|
|
||||||
id: String,
|
|
||||||
role_id: String,
|
|
||||||
data: PartialRole,
|
|
||||||
clear: Vec<FieldsRole>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Server role deleted
|
|
||||||
ServerRoleDelete { id: String, role_id: String },
|
|
||||||
|
|
||||||
/// Update existing user
|
|
||||||
UserUpdate {
|
|
||||||
id: String,
|
|
||||||
data: PartialUser,
|
|
||||||
clear: Vec<FieldsUser>,
|
|
||||||
event_id: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Relationship with another user changed
|
|
||||||
UserRelationship {
|
|
||||||
id: String,
|
|
||||||
user: User,
|
|
||||||
// ! this field can be deprecated
|
|
||||||
status: RelationshipStatus,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Settings updated remotely
|
|
||||||
UserSettingsUpdate { id: String, update: UserSettings },
|
|
||||||
|
|
||||||
/// User has been platform banned or deleted their account
|
|
||||||
///
|
|
||||||
/// Clients should remove the following associated data:
|
|
||||||
/// - Messages
|
|
||||||
/// - DM Channels
|
|
||||||
/// - Relationships
|
|
||||||
/// - Server Memberships
|
|
||||||
///
|
|
||||||
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
|
||||||
UserPlatformWipe { user_id: String, flags: i32 },
|
|
||||||
|
|
||||||
/// New emoji
|
|
||||||
EmojiCreate(Emoji),
|
|
||||||
|
|
||||||
/// Delete emoji
|
|
||||||
EmojiDelete { id: String },
|
|
||||||
|
|
||||||
/// New webhook
|
|
||||||
WebhookCreate(Webhook),
|
|
||||||
|
|
||||||
/// Update existing webhook
|
|
||||||
WebhookUpdate {
|
|
||||||
id: String,
|
|
||||||
data: PartialWebhook,
|
|
||||||
remove: Vec<FieldsWebhook>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Delete webhook
|
|
||||||
WebhookDelete { id: String },
|
|
||||||
|
|
||||||
/// New report
|
|
||||||
ReportCreate(Report),
|
|
||||||
|
|
||||||
/// Auth events
|
|
||||||
Auth(AuthifierEvent),
|
|
||||||
}
|
|
||||||
@@ -1,589 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
get_relationship,
|
|
||||||
models::{
|
|
||||||
server_member::FieldsMember,
|
|
||||||
user::{PartialUser, Presence, RelationshipStatus},
|
|
||||||
Channel, Member, User,
|
|
||||||
},
|
|
||||||
perms, Database, Permission, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
use revolt_presence::filter_online;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
client::EventV1,
|
|
||||||
state::{Cache, State},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Cache Manager
|
|
||||||
impl Cache {
|
|
||||||
/// Check whether the current user can view a channel
|
|
||||||
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
|
|
||||||
match &channel {
|
|
||||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
|
||||||
let member = self.members.get(server);
|
|
||||||
let server = self.servers.get(server);
|
|
||||||
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
|
||||||
|
|
||||||
if let Some(member) = member {
|
|
||||||
perms.member.set_ref(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(server) = server {
|
|
||||||
perms.server.set_ref(server);
|
|
||||||
}
|
|
||||||
|
|
||||||
perms
|
|
||||||
.has_permission(db, Permission::ViewChannel)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
_ => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Filter a given vector of channels to only include the ones we can access
|
|
||||||
pub async fn filter_accessible_channels(
|
|
||||||
&self,
|
|
||||||
db: &Database,
|
|
||||||
channels: Vec<Channel>,
|
|
||||||
) -> Vec<Channel> {
|
|
||||||
let mut viewable_channels = vec![];
|
|
||||||
for channel in channels {
|
|
||||||
if self.can_view_channel(db, &channel).await {
|
|
||||||
viewable_channels.push(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewable_channels
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether we can subscribe to another user
|
|
||||||
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
|
|
||||||
if let Some(user) = self.users.get(&self.user_id) {
|
|
||||||
match get_relationship(user, user_id) {
|
|
||||||
RelationshipStatus::Friend
|
|
||||||
| RelationshipStatus::Incoming
|
|
||||||
| RelationshipStatus::Outgoing
|
|
||||||
| RelationshipStatus::User => true,
|
|
||||||
_ => {
|
|
||||||
let user_id = &user_id.to_string();
|
|
||||||
for channel in self.channels.values() {
|
|
||||||
match channel {
|
|
||||||
Channel::DirectMessage { recipients, .. }
|
|
||||||
| Channel::Group { recipients, .. } => {
|
|
||||||
if recipients.contains(user_id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State Manager
|
|
||||||
impl State {
|
|
||||||
/// Generate a Ready packet for the current user
|
|
||||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
|
||||||
let mut user = self.clone_user();
|
|
||||||
|
|
||||||
// Find all relationships to the user.
|
|
||||||
let mut user_ids: HashSet<String> = user
|
|
||||||
.relations
|
|
||||||
.as_ref()
|
|
||||||
.map(|arr| arr.iter().map(|x| x.id.to_string()).collect())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Fetch all memberships with their corresponding servers.
|
|
||||||
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
|
|
||||||
self.cache.members = members
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|x| (x.id.server.clone(), x))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
|
|
||||||
let servers = db.fetch_servers(&server_ids).await?;
|
|
||||||
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
|
||||||
|
|
||||||
// Collect channel ids from servers.
|
|
||||||
let mut channel_ids = vec![];
|
|
||||||
for server in &servers {
|
|
||||||
channel_ids.append(&mut server.channels.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch DMs and server channels.
|
|
||||||
let mut channels = db.find_direct_messages(&user.id).await?;
|
|
||||||
channels.append(&mut db.fetch_channels(&channel_ids).await?);
|
|
||||||
|
|
||||||
// Filter server channels by permission.
|
|
||||||
let channels = self.cache.filter_accessible_channels(db, channels).await;
|
|
||||||
|
|
||||||
// Append known user IDs from DMs.
|
|
||||||
for channel in &channels {
|
|
||||||
match channel {
|
|
||||||
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
|
|
||||||
user_ids.extend(&mut recipients.clone().into_iter());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch presence data for known users.
|
|
||||||
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
|
|
||||||
user.online = Some(true);
|
|
||||||
|
|
||||||
// Fetch user data.
|
|
||||||
let users = db
|
|
||||||
.fetch_users(
|
|
||||||
&user_ids
|
|
||||||
.into_iter()
|
|
||||||
.filter(|x| x != &user.id)
|
|
||||||
.collect::<Vec<String>>(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Fetch customisations.
|
|
||||||
let emojis = Some(
|
|
||||||
db.fetch_emoji_by_parent_ids(
|
|
||||||
&servers
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.id.to_string())
|
|
||||||
.collect::<Vec<String>>(),
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Copy data into local state cache.
|
|
||||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
|
||||||
self.cache
|
|
||||||
.users
|
|
||||||
.insert(self.cache.user_id.clone(), user.clone());
|
|
||||||
self.cache.channels = channels
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|x| (x.id().to_string(), x))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Make all users appear from our perspective.
|
|
||||||
let mut users: Vec<User> = users
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut x| {
|
|
||||||
x.online = Some(online_ids.contains(&x.id));
|
|
||||||
x.with_relationship(&user)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Make sure we see our own user correctly.
|
|
||||||
user.relationship = Some(RelationshipStatus::User);
|
|
||||||
users.push(user.foreign());
|
|
||||||
|
|
||||||
// Set subscription state internally.
|
|
||||||
self.reset_state();
|
|
||||||
self.insert_subscription(self.private_topic.clone());
|
|
||||||
|
|
||||||
for user in &users {
|
|
||||||
self.insert_subscription(user.id.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
for server in &servers {
|
|
||||||
self.insert_subscription(server.id.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
for channel in &channels {
|
|
||||||
self.insert_subscription(channel.id().to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(EventV1::Ready {
|
|
||||||
users,
|
|
||||||
servers,
|
|
||||||
channels,
|
|
||||||
members,
|
|
||||||
emojis,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-determine the currently accessible server channels
|
|
||||||
pub async fn recalculate_server(&mut self, db: &Database, id: &str, event: &mut EventV1) {
|
|
||||||
if let Some(server) = self.cache.servers.get(id) {
|
|
||||||
let mut channel_ids = HashSet::new();
|
|
||||||
let mut added_channels = vec![];
|
|
||||||
let mut removed_channels = vec![];
|
|
||||||
|
|
||||||
let id = &id.to_string();
|
|
||||||
for (channel_id, channel) in &self.cache.channels {
|
|
||||||
match channel {
|
|
||||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
|
||||||
if server == id {
|
|
||||||
channel_ids.insert(channel_id.clone());
|
|
||||||
|
|
||||||
if self.cache.can_view_channel(db, channel).await {
|
|
||||||
added_channels.push(channel_id.clone());
|
|
||||||
} else {
|
|
||||||
removed_channels.push(channel_id.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let known_ids = server.channels.iter().cloned().collect::<HashSet<String>>();
|
|
||||||
|
|
||||||
let mut bulk_events = vec![];
|
|
||||||
|
|
||||||
for id in added_channels {
|
|
||||||
self.insert_subscription(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for id in removed_channels {
|
|
||||||
self.remove_subscription(&id);
|
|
||||||
self.cache.channels.remove(&id);
|
|
||||||
|
|
||||||
bulk_events.push(EventV1::ChannelDelete { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// * NOTE: currently all channels should be cached
|
|
||||||
// * provided that a server was loaded from payload
|
|
||||||
let unknowns = known_ids
|
|
||||||
.difference(&channel_ids)
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
if !unknowns.is_empty() {
|
|
||||||
if let Ok(channels) = db.fetch_channels(&unknowns).await {
|
|
||||||
let viewable_channels =
|
|
||||||
self.cache.filter_accessible_channels(db, channels).await;
|
|
||||||
|
|
||||||
for channel in viewable_channels {
|
|
||||||
self.cache
|
|
||||||
.channels
|
|
||||||
.insert(channel.id().to_string(), channel.clone());
|
|
||||||
|
|
||||||
self.insert_subscription(channel.id().to_string());
|
|
||||||
bulk_events.push(EventV1::ChannelCreate(channel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bulk_events.is_empty() {
|
|
||||||
let mut new_event = EventV1::Bulk { v: bulk_events };
|
|
||||||
std::mem::swap(&mut new_event, event);
|
|
||||||
|
|
||||||
if let EventV1::Bulk { v } = event {
|
|
||||||
v.push(new_event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push presence change to the user and all associated server topics
|
|
||||||
pub async fn broadcast_presence_change(&self, target: bool) {
|
|
||||||
if if let Some(status) = &self.cache.users.get(&self.cache.user_id).unwrap().status {
|
|
||||||
status.presence != Some(Presence::Invisible)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
} {
|
|
||||||
let event = EventV1::UserUpdate {
|
|
||||||
id: self.cache.user_id.clone(),
|
|
||||||
data: PartialUser {
|
|
||||||
online: Some(target),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
clear: vec![],
|
|
||||||
event_id: Some(ulid::Ulid::new().to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
for server in self.cache.servers.keys() {
|
|
||||||
event.clone().p(server.clone()).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.p(self.cache.user_id.clone()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle an incoming event for protocol version 1
|
|
||||||
pub async fn handle_incoming_event_v1(&mut self, db: &Database, event: &mut EventV1) -> bool {
|
|
||||||
/* Superseded by private topics.
|
|
||||||
if match event {
|
|
||||||
EventV1::UserRelationship { id, .. }
|
|
||||||
| EventV1::UserSettingsUpdate { id, .. }
|
|
||||||
| EventV1::ChannelAck { id, .. } => id != &self.cache.user_id,
|
|
||||||
EventV1::ServerCreate { server, .. } => server.owner != self.cache.user_id,
|
|
||||||
EventV1::ChannelCreate(channel) => match channel {
|
|
||||||
Channel::SavedMessages { user, .. } => user != &self.cache.user_id,
|
|
||||||
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
|
|
||||||
!recipients.contains(&self.cache.user_id)
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
_ => false,
|
|
||||||
} {
|
|
||||||
return false;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// An event may trigger recalculation of an entire server's permission.
|
|
||||||
// Keep track of whether we need to do anything.
|
|
||||||
let mut queue_server = None;
|
|
||||||
|
|
||||||
// It may also need to sub or unsub a single value.
|
|
||||||
let mut queue_add = None;
|
|
||||||
let mut queue_remove = None;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
EventV1::ChannelCreate(channel) => {
|
|
||||||
let id = channel.id().to_string();
|
|
||||||
self.insert_subscription(id.clone());
|
|
||||||
self.cache.channels.insert(id, channel.clone());
|
|
||||||
}
|
|
||||||
EventV1::ChannelUpdate {
|
|
||||||
id, data, clear, ..
|
|
||||||
} => {
|
|
||||||
let could_view: bool = if let Some(channel) = self.cache.channels.get(id) {
|
|
||||||
self.cache.can_view_channel(db, channel).await
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(channel) = self.cache.channels.get_mut(id) {
|
|
||||||
for field in clear {
|
|
||||||
channel.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.apply_options(data.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.cache.channels.contains_key(id) {
|
|
||||||
if let Ok(channel) = db.fetch_channel(id).await {
|
|
||||||
self.cache.channels.insert(id.clone(), channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(channel) = self.cache.channels.get(id) {
|
|
||||||
let can_view = self.cache.can_view_channel(db, channel).await;
|
|
||||||
if could_view != can_view {
|
|
||||||
if can_view {
|
|
||||||
queue_add = Some(id.clone());
|
|
||||||
*event = EventV1::ChannelCreate(channel.clone());
|
|
||||||
} else {
|
|
||||||
queue_remove = Some(id.clone());
|
|
||||||
*event = EventV1::ChannelDelete { id: id.clone() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventV1::ChannelDelete { id } => {
|
|
||||||
self.remove_subscription(id);
|
|
||||||
self.cache.channels.remove(id);
|
|
||||||
}
|
|
||||||
EventV1::ChannelGroupJoin { user, .. } => {
|
|
||||||
self.insert_subscription(user.clone());
|
|
||||||
}
|
|
||||||
EventV1::ChannelGroupLeave { id, user, .. } => {
|
|
||||||
if user == &self.cache.user_id {
|
|
||||||
self.remove_subscription(id);
|
|
||||||
} else if !self.cache.can_subscribe_to_user(user) {
|
|
||||||
self.remove_subscription(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventV1::ServerCreate {
|
|
||||||
id,
|
|
||||||
server,
|
|
||||||
channels,
|
|
||||||
emojis: _,
|
|
||||||
} => {
|
|
||||||
self.insert_subscription(id.clone());
|
|
||||||
self.cache.servers.insert(id.clone(), server.clone());
|
|
||||||
let member = Member::new(id.clone(), self.cache.user_id.clone());
|
|
||||||
self.cache.members.insert(id.clone(), member);
|
|
||||||
|
|
||||||
for channel in channels {
|
|
||||||
self.cache
|
|
||||||
.channels
|
|
||||||
.insert(channel.id().to_string(), channel.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
queue_server = Some(id.clone());
|
|
||||||
}
|
|
||||||
EventV1::ServerUpdate {
|
|
||||||
id, data, clear, ..
|
|
||||||
} => {
|
|
||||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
|
||||||
for field in clear {
|
|
||||||
server.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.apply_options(data.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.default_permissions.is_some() {
|
|
||||||
queue_server = Some(id.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventV1::ServerMemberJoin { .. } => {
|
|
||||||
// We will always receive ServerCreate when joining a new server.
|
|
||||||
}
|
|
||||||
EventV1::ServerMemberLeave { id, user } => {
|
|
||||||
if user == &self.cache.user_id {
|
|
||||||
self.remove_subscription(id);
|
|
||||||
|
|
||||||
if let Some(server) = self.cache.servers.remove(id) {
|
|
||||||
for channel in &server.channels {
|
|
||||||
self.remove_subscription(channel);
|
|
||||||
self.cache.channels.remove(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.cache.members.remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventV1::ServerDelete { id } => {
|
|
||||||
self.remove_subscription(id);
|
|
||||||
|
|
||||||
if let Some(server) = self.cache.servers.remove(id) {
|
|
||||||
for channel in &server.channels {
|
|
||||||
self.remove_subscription(channel);
|
|
||||||
self.cache.channels.remove(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.cache.members.remove(id);
|
|
||||||
}
|
|
||||||
EventV1::ServerMemberUpdate { id, data, clear } => {
|
|
||||||
if id.user == self.cache.user_id {
|
|
||||||
if let Some(member) = self.cache.members.get_mut(&id.server) {
|
|
||||||
for field in &clear.clone() {
|
|
||||||
member.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
member.apply_options(data.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.roles.is_some() || clear.contains(&FieldsMember::Roles) {
|
|
||||||
queue_server = Some(id.server.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventV1::ServerRoleUpdate {
|
|
||||||
id,
|
|
||||||
role_id,
|
|
||||||
data,
|
|
||||||
clear,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
|
||||||
if let Some(role) = server.roles.get_mut(role_id) {
|
|
||||||
for field in &clear.clone() {
|
|
||||||
role.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
role.apply_options(data.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.rank.is_some() || data.permissions.is_some() {
|
|
||||||
if let Some(member) = self.cache.members.get(id) {
|
|
||||||
if member.roles.contains(role_id) {
|
|
||||||
queue_server = Some(id.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventV1::ServerRoleDelete { id, role_id } => {
|
|
||||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
|
||||||
server.roles.remove(role_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(member) = self.cache.members.get(id) {
|
|
||||||
if member.roles.contains(role_id) {
|
|
||||||
queue_server = Some(id.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventV1::UserUpdate { event_id, .. } => {
|
|
||||||
if let Some(id) = event_id {
|
|
||||||
if self.cache.seen_events.contains(id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cache.seen_events.put(id.to_string(), ());
|
|
||||||
}
|
|
||||||
|
|
||||||
*event_id = None;
|
|
||||||
}
|
|
||||||
EventV1::UserRelationship { id, user, .. } => {
|
|
||||||
self.cache.users.insert(id.clone(), user.clone());
|
|
||||||
|
|
||||||
if self.cache.can_subscribe_to_user(id) {
|
|
||||||
self.insert_subscription(id.clone());
|
|
||||||
} else {
|
|
||||||
self.remove_subscription(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate server permissions if requested.
|
|
||||||
if let Some(server_id) = queue_server {
|
|
||||||
self.recalculate_server(db, &server_id, event).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sub / unsub accordingly.
|
|
||||||
if let Some(id) = queue_add {
|
|
||||||
self.insert_subscription(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(id) = queue_remove {
|
|
||||||
self.remove_subscription(&id);
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventV1 {
|
|
||||||
/// Publish helper wrapper
|
|
||||||
pub async fn p(self, channel: String) {
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
redis_kiss::p(channel, self).await;
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
info!("Publishing event to {channel}: {self:?}");
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
redis_kiss::publish(channel, self).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish user event
|
|
||||||
pub async fn p_user(self, id: String, db: &Database) {
|
|
||||||
self.clone().p(id.clone()).await;
|
|
||||||
|
|
||||||
// TODO: this should be captured by member list in the future and not immediately fanned out to users
|
|
||||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
|
||||||
for member in members {
|
|
||||||
self.clone().p(member.id.server).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish private event
|
|
||||||
pub async fn private(self, id: String) {
|
|
||||||
self.p(format!("{id}!")).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish internal global event
|
|
||||||
pub async fn global(self) {
|
|
||||||
self.p("global".to_string()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
pub mod client;
|
|
||||||
pub mod r#impl;
|
|
||||||
pub mod server;
|
|
||||||
pub mod state;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use super::client::Ping;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum ClientMessage {
|
|
||||||
Authenticate { token: String },
|
|
||||||
BeginTyping { channel: String },
|
|
||||||
EndTyping { channel: String },
|
|
||||||
Ping { data: Ping, responded: Option<()> },
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
use lru::LruCache;
|
|
||||||
|
|
||||||
use crate::models::{Channel, Member, Server, User};
|
|
||||||
|
|
||||||
/// Enumeration representing some change in subscriptions
|
|
||||||
pub enum SubscriptionStateChange {
|
|
||||||
/// No change
|
|
||||||
None,
|
|
||||||
/// Clear all subscriptions
|
|
||||||
Reset,
|
|
||||||
/// Append or remove subscriptions
|
|
||||||
Change {
|
|
||||||
add: Vec<String>,
|
|
||||||
remove: Vec<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dumb per-state cache implementation
|
|
||||||
///
|
|
||||||
/// Ideally this would use a global cache that
|
|
||||||
/// allows for mutations and could use Rc<> to
|
|
||||||
/// track usage. If Rc<> == 1, then it only
|
|
||||||
/// remains in global cache, hence should be
|
|
||||||
/// dropped.
|
|
||||||
///
|
|
||||||
/// ------------------------------------------------
|
|
||||||
/// We can strip these objects to core information!!
|
|
||||||
/// ------------------------------------------------
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Cache {
|
|
||||||
pub user_id: String,
|
|
||||||
|
|
||||||
pub users: HashMap<String, User>,
|
|
||||||
pub channels: HashMap<String, Channel>,
|
|
||||||
pub members: HashMap<String, Member>,
|
|
||||||
pub servers: HashMap<String, Server>,
|
|
||||||
|
|
||||||
pub seen_events: LruCache<String, ()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Cache {
|
|
||||||
fn default() -> Self {
|
|
||||||
Cache {
|
|
||||||
user_id: Default::default(),
|
|
||||||
|
|
||||||
users: Default::default(),
|
|
||||||
channels: Default::default(),
|
|
||||||
members: Default::default(),
|
|
||||||
servers: Default::default(),
|
|
||||||
|
|
||||||
seen_events: LruCache::new(20),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Client state
|
|
||||||
pub struct State {
|
|
||||||
pub cache: Cache,
|
|
||||||
|
|
||||||
pub private_topic: String,
|
|
||||||
subscribed: HashSet<String>,
|
|
||||||
state: SubscriptionStateChange,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
/// Create state from User
|
|
||||||
pub fn from(user: User) -> State {
|
|
||||||
let mut subscribed = HashSet::new();
|
|
||||||
let private_topic = format!("{}!", user.id);
|
|
||||||
subscribed.insert(private_topic.clone());
|
|
||||||
subscribed.insert(user.id.clone());
|
|
||||||
|
|
||||||
let mut cache: Cache = Cache {
|
|
||||||
user_id: user.id.clone(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.users.insert(user.id.clone(), user);
|
|
||||||
|
|
||||||
State {
|
|
||||||
cache,
|
|
||||||
subscribed,
|
|
||||||
private_topic,
|
|
||||||
state: SubscriptionStateChange::Reset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply currently queued state
|
|
||||||
pub fn apply_state(&mut self) -> SubscriptionStateChange {
|
|
||||||
let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None);
|
|
||||||
if let SubscriptionStateChange::Change { add, remove } = &state {
|
|
||||||
for id in add {
|
|
||||||
self.subscribed.insert(id.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
for id in remove {
|
|
||||||
self.subscribed.remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clone the active user
|
|
||||||
pub fn clone_user(&self) -> User {
|
|
||||||
self.cache.users.get(&self.cache.user_id).unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterate through all subscriptions
|
|
||||||
pub fn iter_subscriptions(&self) -> std::collections::hash_set::Iter<'_, std::string::String> {
|
|
||||||
self.subscribed.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset the current state
|
|
||||||
pub fn reset_state(&mut self) {
|
|
||||||
self.state = SubscriptionStateChange::Reset;
|
|
||||||
self.subscribed.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new subscription
|
|
||||||
pub fn insert_subscription(&mut self, subscription: String) {
|
|
||||||
if self.subscribed.contains(&subscription) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match &mut self.state {
|
|
||||||
SubscriptionStateChange::None => {
|
|
||||||
self.state = SubscriptionStateChange::Change {
|
|
||||||
add: vec![subscription.clone()],
|
|
||||||
remove: vec![],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
SubscriptionStateChange::Change { add, .. } => {
|
|
||||||
add.push(subscription.clone());
|
|
||||||
}
|
|
||||||
SubscriptionStateChange::Reset => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.subscribed.insert(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove existing subscription
|
|
||||||
pub fn remove_subscription(&mut self, subscription: &str) {
|
|
||||||
if !self.subscribed.contains(&subscription.to_string()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match &mut self.state {
|
|
||||||
SubscriptionStateChange::None => {
|
|
||||||
self.state = SubscriptionStateChange::Change {
|
|
||||||
add: vec![],
|
|
||||||
remove: vec![subscription.to_string()],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
SubscriptionStateChange::Change { remove, .. } => {
|
|
||||||
remove.push(subscription.to_string());
|
|
||||||
}
|
|
||||||
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.subscribed.remove(subscription);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
use crate::{models::stats::Stats, AbstractStats, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractStats for DummyDb {
|
|
||||||
async fn generate_stats(&self) -> Result<Stats> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
use crate::models::{channel::{Channel, FieldsChannel, PartialChannel}};
|
|
||||||
use crate::{AbstractAttachment, AbstractChannel, Error, OverrideField, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannel for DummyDb {
|
|
||||||
async fn fetch_channel(&self, id: &str) -> Result<Channel> {
|
|
||||||
Ok(Channel::Group {
|
|
||||||
id: id.into(),
|
|
||||||
|
|
||||||
name: "group".into(),
|
|
||||||
owner: "owner".into(),
|
|
||||||
description: None,
|
|
||||||
recipients: vec!["owner".into()],
|
|
||||||
|
|
||||||
icon: Some(
|
|
||||||
self.find_and_use_attachment("dummy", "dummy", "dummy", "dummy")
|
|
||||||
.await?,
|
|
||||||
),
|
|
||||||
last_message_id: None,
|
|
||||||
|
|
||||||
permissions: None,
|
|
||||||
|
|
||||||
nsfw: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_channels<'a>(&self, _ids: &'a [String]) -> Result<Vec<Channel>> {
|
|
||||||
Ok(vec![self.fetch_channel("sus").await.unwrap()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
|
||||||
info!("Insert {channel:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_channel(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
channel: &PartialChannel,
|
|
||||||
remove: Vec<FieldsChannel>,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Update {id} with {channel:?} and remove {remove:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
|
||||||
info!("Delete {channel:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
|
||||||
Ok(vec![self.fetch_channel(user_id).await?])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_saved_messages_channel(&self, user: &str) -> Result<Channel> {
|
|
||||||
self.fetch_channel(user).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_direct_message_channel(&self, _user_a: &str, _user_b: &str) -> Result<Channel> {
|
|
||||||
Err(Error::NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
|
|
||||||
info!("Added {user} to {channel}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
|
||||||
info!("Removed {user} from {channel}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_channel_role_permission(
|
|
||||||
&self,
|
|
||||||
channel: &str,
|
|
||||||
role: &str,
|
|
||||||
permissions: OverrideField,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Updating permissions for role {role} in {channel} with {permissions:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
use crate::models::Invite;
|
|
||||||
use crate::{AbstractChannelInvite, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannelInvite for DummyDb {
|
|
||||||
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
|
|
||||||
Ok(Invite::Server {
|
|
||||||
code: code.into(),
|
|
||||||
server: "server".into(),
|
|
||||||
creator: "creator".into(),
|
|
||||||
channel: "channel".into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
|
||||||
info!("Insert {invite:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_invite(&self, code: &str) -> Result<()> {
|
|
||||||
info!("Delete {code}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_invites_for_server(&self, server: &str) -> Result<Vec<Invite>> {
|
|
||||||
Ok(vec![self.fetch_invite(server).await.unwrap()])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
use crate::models::channel_unread::ChannelUnread;
|
|
||||||
use crate::{AbstractChannelUnread, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannelUnread for DummyDb {
|
|
||||||
async fn acknowledge_message(&self, channel: &str, user: &str, message: &str) -> Result<()> {
|
|
||||||
info!("Acknowledged {message} in {channel} for {user}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> {
|
|
||||||
info!("Acknowledged {channels:?} for {user}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_mention_to_unread<'a>(
|
|
||||||
&self,
|
|
||||||
channel: &str,
|
|
||||||
user: &str,
|
|
||||||
ids: &[String],
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Added mentions for {user} in {channel}: {ids:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_unreads(&self, _user: &str) -> Result<Vec<ChannelUnread>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
use crate::models::message::{AppendMessage, Message, MessageQuery, PartialMessage};
|
|
||||||
use crate::{AbstractMessage, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractMessage for DummyDb {
|
|
||||||
async fn fetch_message(&self, id: &str) -> Result<Message> {
|
|
||||||
Ok(Message {
|
|
||||||
id: id.into(),
|
|
||||||
channel: "channel".into(),
|
|
||||||
author: "author".into(),
|
|
||||||
content: Some("message content".into()),
|
|
||||||
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_message(&self, message: &Message) -> Result<()> {
|
|
||||||
info!("Insert {message:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
|
||||||
info!("Update {id} with {message:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
|
||||||
info!("Append {id} with {append:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_message(&self, id: &str) -> Result<()> {
|
|
||||||
info!("Delete {id}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> {
|
|
||||||
info!("Delete {ids:?} in {channel}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_messages(&self, _query: MessageQuery) -> Result<Vec<Message>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new reaction to a message
|
|
||||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
|
||||||
info!("Add to {id} with {emoji} and {user}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a reaction from a message
|
|
||||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
|
||||||
info!("Remove {emoji} from {id} for {user}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove reaction from a message
|
|
||||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
|
||||||
info!("Clear {emoji} on {id}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
use crate::models::attachment::File;
|
|
||||||
use crate::{AbstractAttachment, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractAttachment for DummyDb {
|
|
||||||
async fn find_and_use_attachment(
|
|
||||||
&self,
|
|
||||||
attachment_id: &str,
|
|
||||||
tag: &str,
|
|
||||||
_parent_type: &str,
|
|
||||||
parent_id: &str,
|
|
||||||
) -> Result<File> {
|
|
||||||
Ok(File {
|
|
||||||
id: attachment_id.into(),
|
|
||||||
tag: tag.into(),
|
|
||||||
filename: "file.txt".into(),
|
|
||||||
content_type: "plain/text".into(),
|
|
||||||
size: 100,
|
|
||||||
|
|
||||||
object_id: Some(parent_id.into()),
|
|
||||||
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
|
||||||
info!("Insert {attachment:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
|
|
||||||
info!("Marked {id} as reported");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
|
|
||||||
info!("Marked {id} as deleted");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
|
|
||||||
info!("Marked {ids:?} as deleted");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
use crate::models::emoji::EmojiParent;
|
|
||||||
use crate::models::Emoji;
|
|
||||||
use crate::{AbstractEmoji, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractEmoji for DummyDb {
|
|
||||||
/// Fetch an emoji by its id
|
|
||||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
|
||||||
Ok(Emoji {
|
|
||||||
id: id.into(),
|
|
||||||
name: id.into(),
|
|
||||||
parent: EmojiParent::Server { id: id.into() },
|
|
||||||
creator_id: id.into(),
|
|
||||||
animated: false,
|
|
||||||
nsfw: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch emoji by their ids
|
|
||||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
|
|
||||||
Ok(vec![self.fetch_emoji(parent_id).await?])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch emoji by their parent ids
|
|
||||||
async fn fetch_emoji_by_parent_ids(&self, _parent_ids: &[String]) -> Result<Vec<Emoji>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert emoji into database.
|
|
||||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
|
|
||||||
info!("Insert {emoji:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detach an emoji by its id
|
|
||||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
|
||||||
info!("Detach {emoji:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
use crate::AbstractDatabase;
|
|
||||||
|
|
||||||
pub mod admin {
|
|
||||||
pub mod stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod media {
|
|
||||||
pub mod attachment;
|
|
||||||
pub mod emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod channels {
|
|
||||||
pub mod channel;
|
|
||||||
pub mod channel_invite;
|
|
||||||
pub mod channel_unread;
|
|
||||||
pub mod message;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod servers {
|
|
||||||
pub mod server;
|
|
||||||
pub mod server_ban;
|
|
||||||
pub mod server_member;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod users {
|
|
||||||
pub mod user;
|
|
||||||
pub mod user_settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod safety {
|
|
||||||
pub mod report;
|
|
||||||
pub mod snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DummyDb;
|
|
||||||
|
|
||||||
impl AbstractDatabase for DummyDb {}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use crate::models::report::PartialReport;
|
|
||||||
use crate::models::Report;
|
|
||||||
use crate::{AbstractReport, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractReport for DummyDb {
|
|
||||||
async fn insert_report(&self, report: &Report) -> Result<()> {
|
|
||||||
info!("Insert {:?}", report);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_report(&self, _id: &str, _report: &PartialReport) -> Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_report(&self, _report_id: &str) -> Result<Report> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_reports(&self) -> Result<Vec<Report>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
use crate::models::Snapshot;
|
|
||||||
use crate::{AbstractSnapshot, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractSnapshot for DummyDb {
|
|
||||||
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
|
|
||||||
info!("Insert {:?}", snapshot);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_snapshots(&self, _report_id: &str) -> Result<Vec<Snapshot>> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
|
||||||
use crate::{AbstractServer, Result, DEFAULT_PERMISSION_SERVER};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractServer for DummyDb {
|
|
||||||
async fn fetch_server(&self, id: &str) -> Result<Server> {
|
|
||||||
Ok(Server {
|
|
||||||
id: id.into(),
|
|
||||||
owner: "owner".into(),
|
|
||||||
|
|
||||||
name: "server".into(),
|
|
||||||
description: Some("server description".into()),
|
|
||||||
|
|
||||||
channels: vec!["channel".into()],
|
|
||||||
categories: None,
|
|
||||||
system_messages: None,
|
|
||||||
|
|
||||||
roles: std::collections::HashMap::new(),
|
|
||||||
default_permissions: *DEFAULT_PERMISSION_SERVER as i64,
|
|
||||||
|
|
||||||
icon: None,
|
|
||||||
banner: None,
|
|
||||||
|
|
||||||
flags: None,
|
|
||||||
|
|
||||||
nsfw: false,
|
|
||||||
analytics: true,
|
|
||||||
discoverable: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_servers<'a>(&self, _ids: &'a [String]) -> Result<Vec<Server>> {
|
|
||||||
Ok(vec![self.fetch_server("sus").await.unwrap()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_server(&self, server: &Server) -> Result<()> {
|
|
||||||
info!("Insert {server:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_server(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
server: &PartialServer,
|
|
||||||
remove: Vec<FieldsServer>,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Update {id} with {server:?} and remove {remove:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_server(&self, server: &Server) -> Result<()> {
|
|
||||||
info!("Delete {server:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
|
|
||||||
info!("Create {role:?} on {server_id} as {role_id}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_role(
|
|
||||||
&self,
|
|
||||||
server_id: &str,
|
|
||||||
role_id: &str,
|
|
||||||
role: &PartialRole,
|
|
||||||
remove: Vec<FieldsRole>,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Update {role_id} on {server_id} with {role:?} and remove {remove:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
|
|
||||||
info!("Delete {role_id} on {server_id}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
use crate::models::server_member::MemberCompositeKey;
|
|
||||||
use crate::models::ServerBan;
|
|
||||||
use crate::{AbstractServerBan, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractServerBan for DummyDb {
|
|
||||||
async fn fetch_ban(&self, server: &str, user: &str) -> Result<ServerBan> {
|
|
||||||
Ok(ServerBan {
|
|
||||||
id: MemberCompositeKey {
|
|
||||||
server: server.into(),
|
|
||||||
user: user.into(),
|
|
||||||
},
|
|
||||||
reason: Some("ban reason".into()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_bans(&self, server: &str) -> Result<Vec<ServerBan>> {
|
|
||||||
Ok(vec![self.fetch_ban(server, "user").await.unwrap()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
|
|
||||||
info!("Insert {ban:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
|
|
||||||
info!("Delete {id:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
use crate::models::server_member::{
|
|
||||||
FieldsMember, Member, MemberCompositeKey, MemberWithRoles, PartialMember,
|
|
||||||
};
|
|
||||||
use crate::{AbstractServerMember, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractServerMember for DummyDb {
|
|
||||||
async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> {
|
|
||||||
Ok(Member::new(server.into(), user.into()))
|
|
||||||
}
|
|
||||||
async fn fetch_member_with_roles(&self, server: &str, user: &str) -> Result<MemberWithRoles> {
|
|
||||||
Ok(MemberWithRoles::new(server.into(), user.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
|
||||||
info!("Create {member:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_member(
|
|
||||||
&self,
|
|
||||||
id: &MemberCompositeKey,
|
|
||||||
member: &PartialMember,
|
|
||||||
remove: Vec<FieldsMember>,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Update {id:?} with {member:?} and remove {remove:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
|
||||||
info!("Delete {id:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_all_members<'a>(&self, server: &str) -> Result<Vec<Member>> {
|
|
||||||
Ok(vec![self.fetch_member(server, "member").await.unwrap()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_all_memberships<'a>(&self, user: &str) -> Result<Vec<Member>> {
|
|
||||||
Ok(vec![self.fetch_member("server", user).await.unwrap()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_members<'a>(&self, server: &str, _ids: &'a [String]) -> Result<Vec<Member>> {
|
|
||||||
Ok(vec![self.fetch_member(server, "member").await.unwrap()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_member_count(&self, _server: &str) -> Result<usize> {
|
|
||||||
Ok(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_server_count(&self, _user: &str) -> Result<usize> {
|
|
||||||
Ok(5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus, User};
|
|
||||||
use crate::{AbstractUser, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractUser for DummyDb {
|
|
||||||
async fn fetch_user(&self, id: &str) -> Result<User> {
|
|
||||||
Ok(User {
|
|
||||||
id: id.into(),
|
|
||||||
username: "username".into(),
|
|
||||||
discriminator: "0000".into(),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_user_by_username(&self, username: &str, _discriminator: &str) -> Result<User> {
|
|
||||||
self.fetch_user(username).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
|
|
||||||
self.fetch_user(token).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_user(&self, user: &User) -> Result<()> {
|
|
||||||
info!("Insert {:?}", user);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_user(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
user: &PartialUser,
|
|
||||||
remove: Vec<FieldsUser>,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Update {id} with {user:?} and remove {remove:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_user(&self, id: &str) -> Result<()> {
|
|
||||||
info!("Delete {id}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_users<'a>(&self, _id: &'a [String]) -> Result<Vec<User>> {
|
|
||||||
Ok(vec![self.fetch_user("id").await.unwrap()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_discriminators_in_use(&self, _username: &str) -> Result<Vec<String>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
|
||||||
Ok(vec!["a".into()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_mutual_channel_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
|
||||||
Ok(vec!["b".into()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_mutual_server_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
|
||||||
Ok(vec!["c".into()])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_relationship(
|
|
||||||
&self,
|
|
||||||
user_id: &str,
|
|
||||||
target_id: &str,
|
|
||||||
relationship: &RelationshipStatus,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Set relationship from {user_id} to {target_id} as {relationship:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
|
|
||||||
info!("Removing relationship from {user_id} to {target_id}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use crate::models::UserSettings;
|
|
||||||
use crate::{AbstractUserSettings, Result};
|
|
||||||
|
|
||||||
use super::super::DummyDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractUserSettings for DummyDb {
|
|
||||||
async fn fetch_user_settings(
|
|
||||||
&'_ self,
|
|
||||||
_id: &str,
|
|
||||||
_filter: &'_ [String],
|
|
||||||
) -> Result<UserSettings> {
|
|
||||||
Ok(std::collections::HashMap::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()> {
|
|
||||||
info!("Set {id} to {settings:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_user_settings(&self, id: &str) -> Result<()> {
|
|
||||||
info!("Delete {id}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,566 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use revolt_database::util::idempotency::IdempotencyKey;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
events::client::EventV1,
|
|
||||||
models::{
|
|
||||||
channel::{FieldsChannel, PartialChannel},
|
|
||||||
message::{DataMessageSend, Message, Reply, SystemMessage, RE_MENTION},
|
|
||||||
Channel,
|
|
||||||
},
|
|
||||||
tasks::{ack::AckEvent, process_embeds},
|
|
||||||
types::push::MessageAuthor,
|
|
||||||
variables::delta::{MAX_ATTACHMENT_COUNT, MAX_EMBED_COUNT, MAX_REPLY_COUNT},
|
|
||||||
Database, Error, OverrideField, Ref, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Channel {
|
|
||||||
/// Get a reference to this channel's id
|
|
||||||
pub fn id(&'_ self) -> &'_ str {
|
|
||||||
match self {
|
|
||||||
Channel::DirectMessage { id, .. }
|
|
||||||
| Channel::Group { id, .. }
|
|
||||||
| Channel::SavedMessages { id, .. }
|
|
||||||
| Channel::TextChannel { id, .. }
|
|
||||||
| Channel::VoiceChannel { id, .. } => id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represent channel as its id
|
|
||||||
pub fn as_id(self) -> String {
|
|
||||||
match self {
|
|
||||||
Channel::DirectMessage { id, .. }
|
|
||||||
| Channel::Group { id, .. }
|
|
||||||
| Channel::SavedMessages { id, .. }
|
|
||||||
| Channel::TextChannel { id, .. }
|
|
||||||
| Channel::VoiceChannel { id, .. } => id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map out whether it is a direct DM
|
|
||||||
pub fn is_direct_dm(&self) -> bool {
|
|
||||||
matches!(self, Channel::DirectMessage { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a channel
|
|
||||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
|
||||||
db.insert_channel(self).await?;
|
|
||||||
|
|
||||||
let event = EventV1::ChannelCreate(self.clone());
|
|
||||||
match self {
|
|
||||||
Self::SavedMessages { user, .. } => event.private(user.clone()).await,
|
|
||||||
Self::DirectMessage { recipients, .. } | Self::Group { recipients, .. } => {
|
|
||||||
for recipient in recipients {
|
|
||||||
event.clone().private(recipient.clone()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => {
|
|
||||||
event.p(server.clone()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update channel data
|
|
||||||
pub async fn update<'a>(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
partial: PartialChannel,
|
|
||||||
remove: Vec<FieldsChannel>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for field in &remove {
|
|
||||||
self.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
let id = self.id().to_string();
|
|
||||||
db.update_channel(&id, &partial, remove.clone()).await?;
|
|
||||||
|
|
||||||
EventV1::ChannelUpdate {
|
|
||||||
id: id.clone(),
|
|
||||||
data: partial,
|
|
||||||
clear: remove,
|
|
||||||
}
|
|
||||||
.p(match self {
|
|
||||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
|
|
||||||
_ => id,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a channel
|
|
||||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
|
||||||
let id = self.id().to_string();
|
|
||||||
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
|
|
||||||
db.delete_channel(&self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a field from Channel object
|
|
||||||
pub fn remove(&mut self, field: &FieldsChannel) {
|
|
||||||
match field {
|
|
||||||
FieldsChannel::Description => match self {
|
|
||||||
Self::Group { description, .. }
|
|
||||||
| Self::TextChannel { description, .. }
|
|
||||||
| Self::VoiceChannel { description, .. } => {
|
|
||||||
description.take();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
FieldsChannel::Icon => match self {
|
|
||||||
Self::Group { icon, .. }
|
|
||||||
| Self::TextChannel { icon, .. }
|
|
||||||
| Self::VoiceChannel { icon, .. } => {
|
|
||||||
icon.take();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
FieldsChannel::DefaultPermissions => match self {
|
|
||||||
Self::TextChannel {
|
|
||||||
default_permissions,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| Self::VoiceChannel {
|
|
||||||
default_permissions,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
default_permissions.take();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply partial channel to channel
|
|
||||||
pub fn apply_options(&mut self, partial: PartialChannel) {
|
|
||||||
// ! FIXME: maybe flatten channel object?
|
|
||||||
match self {
|
|
||||||
Self::DirectMessage { active, .. } => {
|
|
||||||
if let Some(v) = partial.active {
|
|
||||||
*active = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Group {
|
|
||||||
name,
|
|
||||||
owner,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
nsfw,
|
|
||||||
permissions,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Some(v) = partial.name {
|
|
||||||
*name = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.owner {
|
|
||||||
*owner = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.description {
|
|
||||||
description.replace(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.icon {
|
|
||||||
icon.replace(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.nsfw {
|
|
||||||
*nsfw = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.permissions {
|
|
||||||
permissions.replace(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::TextChannel {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
nsfw,
|
|
||||||
default_permissions,
|
|
||||||
role_permissions,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| Self::VoiceChannel {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
nsfw,
|
|
||||||
default_permissions,
|
|
||||||
role_permissions,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Some(v) = partial.name {
|
|
||||||
*name = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.description {
|
|
||||||
description.replace(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.icon {
|
|
||||||
icon.replace(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.nsfw {
|
|
||||||
*nsfw = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.role_permissions {
|
|
||||||
*role_permissions = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.default_permissions {
|
|
||||||
default_permissions.replace(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Acknowledge a message
|
|
||||||
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
|
|
||||||
EventV1::ChannelAck {
|
|
||||||
id: self.id().to_string(),
|
|
||||||
user: user.to_string(),
|
|
||||||
message_id: message.to_string(),
|
|
||||||
}
|
|
||||||
.private(user.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
crate::tasks::ack::queue(
|
|
||||||
self.id().to_string(),
|
|
||||||
user.to_string(),
|
|
||||||
AckEvent::AckMessage {
|
|
||||||
id: message.to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add user to a group
|
|
||||||
pub async fn add_user_to_group(&mut self, db: &Database, user: &str, by: &str) -> Result<()> {
|
|
||||||
if let Channel::Group { recipients, .. } = self {
|
|
||||||
let user = user.to_string();
|
|
||||||
if recipients.contains(&user) {
|
|
||||||
return Err(Error::AlreadyInGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
recipients.push(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
match &self {
|
|
||||||
Channel::Group { id, .. } => {
|
|
||||||
db.add_user_to_group(id, user).await?;
|
|
||||||
|
|
||||||
EventV1::ChannelGroupJoin {
|
|
||||||
id: id.to_string(),
|
|
||||||
user: user.to_string(),
|
|
||||||
}
|
|
||||||
.p(id.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
EventV1::ChannelCreate(self.clone())
|
|
||||||
.private(user.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
SystemMessage::UserAdded {
|
|
||||||
id: user.to_string(),
|
|
||||||
by: by.to_string(),
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.create(db, self, None)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err(Error::InvalidOperation),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove user from a group
|
|
||||||
pub async fn remove_user_from_group(
|
|
||||||
&self,
|
|
||||||
db: &Database,
|
|
||||||
user: &str,
|
|
||||||
by: Option<&str>,
|
|
||||||
silent: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
match &self {
|
|
||||||
Channel::Group {
|
|
||||||
id,
|
|
||||||
owner,
|
|
||||||
recipients,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if user == owner {
|
|
||||||
if let Some(new_owner) = recipients.iter().find(|x| *x != user) {
|
|
||||||
db.update_channel(
|
|
||||||
id,
|
|
||||||
&PartialChannel {
|
|
||||||
owner: Some(new_owner.into()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
SystemMessage::ChannelOwnershipChanged {
|
|
||||||
from: owner.to_string(),
|
|
||||||
to: new_owner.into(),
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.create(db, self, None)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
db.delete_channel(self).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db.remove_user_from_group(id, user).await?;
|
|
||||||
|
|
||||||
EventV1::ChannelGroupLeave {
|
|
||||||
id: id.to_string(),
|
|
||||||
user: user.to_string(),
|
|
||||||
}
|
|
||||||
.p(id.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !silent {
|
|
||||||
if let Some(by) = by {
|
|
||||||
SystemMessage::UserRemove {
|
|
||||||
id: user.to_string(),
|
|
||||||
by: by.to_string(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SystemMessage::UserLeft {
|
|
||||||
id: user.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.create(db, self, None)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err(Error::InvalidOperation),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set role permission on a channel
|
|
||||||
pub async fn set_role_permission(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
role: &str,
|
|
||||||
permissions: OverrideField,
|
|
||||||
) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
Channel::TextChannel {
|
|
||||||
id,
|
|
||||||
server,
|
|
||||||
role_permissions,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| Channel::VoiceChannel {
|
|
||||||
id,
|
|
||||||
server,
|
|
||||||
role_permissions,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
db.set_channel_role_permission(id, role, permissions)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
role_permissions.insert(role.to_string(), permissions);
|
|
||||||
|
|
||||||
EventV1::ChannelUpdate {
|
|
||||||
id: id.clone(),
|
|
||||||
data: PartialChannel {
|
|
||||||
role_permissions: Some(role_permissions.clone()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
clear: vec![],
|
|
||||||
}
|
|
||||||
.p(server.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err(Error::InvalidOperation),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a message in a channel
|
|
||||||
pub async fn send_message(
|
|
||||||
&self,
|
|
||||||
db: &Database,
|
|
||||||
data: DataMessageSend,
|
|
||||||
author: MessageAuthor<'_>,
|
|
||||||
mut idempotency: IdempotencyKey,
|
|
||||||
generate_embeds: bool,
|
|
||||||
) -> Result<Message> {
|
|
||||||
Message::validate_sum(&data.content, data.embeds.as_deref().unwrap_or_default())?;
|
|
||||||
|
|
||||||
idempotency
|
|
||||||
.consume_nonce(data.nonce)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::InvalidOperation)?;
|
|
||||||
|
|
||||||
// Check the message is not empty
|
|
||||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
|
||||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
|
||||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
|
||||||
{
|
|
||||||
return Err(Error::EmptyMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure restrict_reactions is not specified without reactions list
|
|
||||||
if let Some(interactions) = &data.interactions {
|
|
||||||
if interactions.restrict_reactions {
|
|
||||||
let disallowed = if let Some(list) = &interactions.reactions {
|
|
||||||
list.is_empty()
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if disallowed {
|
|
||||||
return Err(Error::InvalidProperty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (author_id, webhook) = match &author {
|
|
||||||
MessageAuthor::User(user) => (user.id.clone(), None),
|
|
||||||
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start constructing the message
|
|
||||||
let message_id = Ulid::new().to_string();
|
|
||||||
let mut message = Message {
|
|
||||||
id: message_id.clone(),
|
|
||||||
channel: self.id().to_string(),
|
|
||||||
masquerade: data.masquerade,
|
|
||||||
interactions: data.interactions.unwrap_or_default(),
|
|
||||||
author: author_id,
|
|
||||||
webhook: webhook.map(|w| w.into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse mentions in message.
|
|
||||||
let mut mentions = HashSet::new();
|
|
||||||
if let Some(content) = &data.content {
|
|
||||||
for capture in RE_MENTION.captures_iter(content) {
|
|
||||||
if let Some(mention) = capture.get(1) {
|
|
||||||
mentions.insert(mention.as_str().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify replies are valid.
|
|
||||||
let mut replies = HashSet::new();
|
|
||||||
if let Some(entries) = data.replies {
|
|
||||||
if entries.len() > *MAX_REPLY_COUNT {
|
|
||||||
return Err(Error::TooManyReplies {
|
|
||||||
max: *MAX_REPLY_COUNT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for Reply { id, mention } in entries {
|
|
||||||
let message = Ref::from_unchecked(id).as_message(db).await?;
|
|
||||||
|
|
||||||
if mention {
|
|
||||||
mentions.insert(message.author.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
replies.insert(message.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mentions.is_empty() {
|
|
||||||
message.mentions.replace(mentions.into_iter().collect());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !replies.is_empty() {
|
|
||||||
message
|
|
||||||
.replies
|
|
||||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add attachments to message.
|
|
||||||
let mut attachments = vec![];
|
|
||||||
if data
|
|
||||||
.attachments
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|v| v.len() > *MAX_ATTACHMENT_COUNT)
|
|
||||||
{
|
|
||||||
return Err(Error::TooManyAttachments {
|
|
||||||
max: *MAX_ATTACHMENT_COUNT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if data
|
|
||||||
.embeds
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|v| v.len() > *MAX_EMBED_COUNT)
|
|
||||||
{
|
|
||||||
return Err(Error::TooManyEmbeds {
|
|
||||||
max: *MAX_EMBED_COUNT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
|
|
||||||
attachments.push(
|
|
||||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !attachments.is_empty() {
|
|
||||||
message.attachments.replace(attachments);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process included embeds.
|
|
||||||
let mut embeds = vec![];
|
|
||||||
for sendable_embed in data.embeds.unwrap_or_default() {
|
|
||||||
embeds.push(sendable_embed.into_embed(db, &message_id).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !embeds.is_empty() {
|
|
||||||
message.embeds.replace(embeds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set content
|
|
||||||
message.content = data.content;
|
|
||||||
|
|
||||||
// Pass-through nonce value for clients
|
|
||||||
message.nonce = Some(idempotency.into_key());
|
|
||||||
|
|
||||||
message.create(db, self, Some(author)).await?;
|
|
||||||
|
|
||||||
// Queue up a task for processing embeds
|
|
||||||
if generate_embeds {
|
|
||||||
if let Some(content) = &message.content {
|
|
||||||
process_embeds::queue(
|
|
||||||
self.id().to_string(),
|
|
||||||
message.id.to_string(),
|
|
||||||
content.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
use nanoid::nanoid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
models::{Channel, Invite, User},
|
|
||||||
Database, Error, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
static ALPHABET: [char; 54] = [
|
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
|
||||||
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
|
|
||||||
'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'
|
|
||||||
];
|
|
||||||
|
|
||||||
impl Invite {
|
|
||||||
/// Get the invite code for this invite
|
|
||||||
pub fn code(&'_ self) -> &'_ str {
|
|
||||||
match self {
|
|
||||||
Invite::Server { code, .. } | Invite::Group { code, .. } => code,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the ID of the user who created this invite
|
|
||||||
pub fn creator(&'_ self) -> &'_ str {
|
|
||||||
match self {
|
|
||||||
Invite::Server { creator, .. } | Invite::Group { creator, .. } => creator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new invite from given information
|
|
||||||
pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result<Invite> {
|
|
||||||
let code = nanoid!(8, &ALPHABET);
|
|
||||||
let invite = match &target {
|
|
||||||
Channel::Group { id, .. } => Ok(Invite::Group {
|
|
||||||
code,
|
|
||||||
creator: creator.id.clone(),
|
|
||||||
channel: id.clone(),
|
|
||||||
}),
|
|
||||||
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
|
|
||||||
Ok(Invite::Server {
|
|
||||||
code,
|
|
||||||
creator: creator.id.clone(),
|
|
||||||
server: server.clone(),
|
|
||||||
channel: id.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(Error::InvalidOperation),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
db.insert_invite(&invite).await?;
|
|
||||||
Ok(invite)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve an invite by its ID or by a public server ID
|
|
||||||
pub async fn find(db: &Database, code: &str) -> Result<Invite> {
|
|
||||||
if let Ok(invite) = db.fetch_invite(code).await {
|
|
||||||
return Ok(invite);
|
|
||||||
} else if let Ok(server) = db.fetch_server(code).await {
|
|
||||||
if server.discoverable {
|
|
||||||
if let Some(channel) = server.channels.into_iter().next() {
|
|
||||||
return Ok(Invite::Server {
|
|
||||||
code: code.to_string(),
|
|
||||||
server: server.id,
|
|
||||||
creator: server.owner,
|
|
||||||
channel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(Error::NotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,494 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use revolt_presence::filter_online;
|
|
||||||
use serde_json::json;
|
|
||||||
use ulid::Ulid;
|
|
||||||
use validator::Validate;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
events::client::EventV1,
|
|
||||||
models::{
|
|
||||||
message::{
|
|
||||||
AppendMessage, BulkMessageResponse, Interactions, PartialMessage, SendableEmbed,
|
|
||||||
SystemMessage, DataMessageSend,
|
|
||||||
},
|
|
||||||
Channel, Emoji, Message, User,
|
|
||||||
},
|
|
||||||
permissions::PermissionCalculator,
|
|
||||||
tasks::ack::AckEvent,
|
|
||||||
types::{
|
|
||||||
january::{Embed, Text},
|
|
||||||
push::{MessageAuthor, PushNotification},
|
|
||||||
},
|
|
||||||
Database, Error, Permission, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
/// Create a message
|
|
||||||
pub async fn create_no_web_push(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
channel: &str,
|
|
||||||
is_direct_dm: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
db.insert_message(self).await?;
|
|
||||||
|
|
||||||
// Fan out events
|
|
||||||
EventV1::Message(self.clone()).p(channel.to_string()).await;
|
|
||||||
|
|
||||||
// Update last_message_id
|
|
||||||
crate::tasks::last_message_id::queue(
|
|
||||||
channel.to_string(),
|
|
||||||
self.id.to_string(),
|
|
||||||
is_direct_dm,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Add mentions for affected users
|
|
||||||
if let Some(mentions) = &self.mentions {
|
|
||||||
for user in mentions {
|
|
||||||
crate::tasks::ack::queue(
|
|
||||||
channel.to_string(),
|
|
||||||
user.to_string(),
|
|
||||||
AckEvent::AddMention {
|
|
||||||
ids: vec![self.id.to_string()],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a message and Web Push events
|
|
||||||
pub async fn create(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
channel: &Channel,
|
|
||||||
sender: Option<MessageAuthor<'_>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.create_no_web_push(db, channel.id(), channel.is_direct_dm())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Push out Web Push notifications
|
|
||||||
crate::tasks::web_push::queue(
|
|
||||||
{
|
|
||||||
let mut target_ids = vec![];
|
|
||||||
match &channel {
|
|
||||||
Channel::DirectMessage { recipients, .. }
|
|
||||||
| Channel::Group { recipients, .. } => {
|
|
||||||
target_ids = (&recipients.iter().cloned().collect::<HashSet<String>>()
|
|
||||||
- &filter_online(recipients).await)
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
}
|
|
||||||
Channel::TextChannel { .. } => {
|
|
||||||
if let Some(mentions) = &self.mentions {
|
|
||||||
target_ids.append(&mut mentions.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
target_ids
|
|
||||||
},
|
|
||||||
json!(PushNotification::new(self.clone(), sender, channel.id())).to_string(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update message data
|
|
||||||
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
db.update_message(&self.id, &partial).await?;
|
|
||||||
|
|
||||||
EventV1::MessageUpdate {
|
|
||||||
id: self.id.clone(),
|
|
||||||
channel: self.channel.clone(),
|
|
||||||
data: partial,
|
|
||||||
}
|
|
||||||
.p(self.channel.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append message data
|
|
||||||
pub async fn append(
|
|
||||||
db: &Database,
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
append: AppendMessage,
|
|
||||||
) -> Result<()> {
|
|
||||||
db.append_message(&id, &append).await?;
|
|
||||||
|
|
||||||
EventV1::MessageAppend {
|
|
||||||
id,
|
|
||||||
channel: channel.to_string(),
|
|
||||||
append,
|
|
||||||
}
|
|
||||||
.p(channel)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a message
|
|
||||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
|
||||||
let file_ids: Vec<String> = self
|
|
||||||
.attachments
|
|
||||||
.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?;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.delete_message(&self.id).await?;
|
|
||||||
|
|
||||||
EventV1::MessageDelete {
|
|
||||||
id: self.id,
|
|
||||||
channel: self.channel.clone(),
|
|
||||||
}
|
|
||||||
.p(self.channel)
|
|
||||||
.await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bulk delete messages
|
|
||||||
pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec<String>) -> Result<()> {
|
|
||||||
db.delete_messages(channel, ids.clone()).await?;
|
|
||||||
EventV1::BulkMessageDelete {
|
|
||||||
channel: channel.to_string(),
|
|
||||||
ids,
|
|
||||||
}
|
|
||||||
.p(channel.to_string())
|
|
||||||
.await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate the sum of content of a message is under threshold
|
|
||||||
pub fn validate_sum(content: &Option<String>, embeds: &[SendableEmbed]) -> Result<()> {
|
|
||||||
let mut running_total = 0;
|
|
||||||
if let Some(content) = content {
|
|
||||||
running_total += content.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
for embed in embeds {
|
|
||||||
if let Some(desc) = &embed.description {
|
|
||||||
running_total += desc.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if running_total <= 2000 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::PayloadTooLarge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a reaction to a message
|
|
||||||
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
|
|
||||||
// Check how many reactions are already on the message
|
|
||||||
if self.reactions.len() >= 20 && !self.reactions.contains_key(emoji) {
|
|
||||||
return Err(Error::InvalidOperation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the emoji is whitelisted
|
|
||||||
if !self.interactions.can_use(emoji) {
|
|
||||||
return Err(Error::InvalidOperation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the emoji is usable by us
|
|
||||||
if !Emoji::can_use(db, emoji).await? {
|
|
||||||
return Err(Error::InvalidOperation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send reaction event
|
|
||||||
EventV1::MessageReact {
|
|
||||||
id: self.id.to_string(),
|
|
||||||
channel_id: self.channel.to_string(),
|
|
||||||
user_id: user.id.to_string(),
|
|
||||||
emoji_id: emoji.to_string(),
|
|
||||||
}
|
|
||||||
.p(self.channel.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Add emoji
|
|
||||||
db.add_reaction(&self.id, emoji, &user.id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a reaction from a message
|
|
||||||
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
|
|
||||||
// Check if it actually exists
|
|
||||||
let empty = if let Some(users) = self.reactions.get(emoji) {
|
|
||||||
if !users.contains(user) {
|
|
||||||
return Err(Error::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
users.len() == 1
|
|
||||||
} else {
|
|
||||||
return Err(Error::NotFound);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send reaction event
|
|
||||||
EventV1::MessageUnreact {
|
|
||||||
id: self.id.to_string(),
|
|
||||||
channel_id: self.channel.to_string(),
|
|
||||||
user_id: user.to_string(),
|
|
||||||
emoji_id: emoji.to_string(),
|
|
||||||
}
|
|
||||||
.p(self.channel.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if empty {
|
|
||||||
// If empty, remove the reaction entirely
|
|
||||||
db.clear_reaction(&self.id, emoji).await
|
|
||||||
} else {
|
|
||||||
// Otherwise only remove that one reaction
|
|
||||||
db.remove_reaction(&self.id, emoji, user).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a reaction from a message
|
|
||||||
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
|
|
||||||
// Send reaction event
|
|
||||||
EventV1::MessageRemoveReaction {
|
|
||||||
id: self.id.to_string(),
|
|
||||||
channel_id: self.channel.to_string(),
|
|
||||||
emoji_id: emoji.to_string(),
|
|
||||||
}
|
|
||||||
.p(self.channel.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Write to database
|
|
||||||
db.clear_reaction(&self.id, emoji).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_webhook(&self) -> bool {
|
|
||||||
self.webhook.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoUsers {
|
|
||||||
fn get_user_ids(&self) -> Vec<String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoUsers for Message {
|
|
||||||
fn get_user_ids(&self) -> Vec<String> {
|
|
||||||
let mut ids = Vec::new();
|
|
||||||
|
|
||||||
if !self.is_webhook() {
|
|
||||||
ids.push(self.author.clone());
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(msg) = &self.system {
|
|
||||||
match msg {
|
|
||||||
SystemMessage::UserAdded { id, by, .. }
|
|
||||||
| SystemMessage::UserRemove { id, by, .. } => {
|
|
||||||
ids.push(id.clone());
|
|
||||||
ids.push(by.clone());
|
|
||||||
}
|
|
||||||
SystemMessage::UserJoined { id, .. }
|
|
||||||
| SystemMessage::UserLeft { id, .. }
|
|
||||||
| SystemMessage::UserKicked { id, .. }
|
|
||||||
| SystemMessage::UserBanned { id, .. } => ids.push(id.clone()),
|
|
||||||
SystemMessage::ChannelRenamed { by, .. }
|
|
||||||
| SystemMessage::ChannelDescriptionChanged { by, .. }
|
|
||||||
| SystemMessage::ChannelIconChanged { by, .. } => ids.push(by.clone()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoUsers for Vec<Message> {
|
|
||||||
fn get_user_ids(&self) -> Vec<String> {
|
|
||||||
let mut ids = HashSet::new();
|
|
||||||
for message in self {
|
|
||||||
ids.extend(&mut message.get_user_ids().into_iter());
|
|
||||||
}
|
|
||||||
|
|
||||||
ids.into_iter().collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SystemMessage {
|
|
||||||
pub fn into_message(self, channel: String) -> Message {
|
|
||||||
Message {
|
|
||||||
id: Ulid::new().to_string(),
|
|
||||||
channel,
|
|
||||||
author: "00000000000000000000000000".to_string(),
|
|
||||||
system: Some(self),
|
|
||||||
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SystemMessage> for String {
|
|
||||||
fn from(s: SystemMessage) -> String {
|
|
||||||
match s {
|
|
||||||
SystemMessage::Text { content } => content,
|
|
||||||
SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(),
|
|
||||||
SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(),
|
|
||||||
SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(),
|
|
||||||
SystemMessage::UserLeft { .. } => "User left the channel.".to_string(),
|
|
||||||
SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(),
|
|
||||||
SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(),
|
|
||||||
SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(),
|
|
||||||
SystemMessage::ChannelDescriptionChanged { .. } => {
|
|
||||||
"Channel description changed.".to_string()
|
|
||||||
}
|
|
||||||
SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(),
|
|
||||||
SystemMessage::ChannelOwnershipChanged { .. } => {
|
|
||||||
"Channel ownership changed.".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SendableEmbed {
|
|
||||||
pub async fn into_embed(self, db: &Database, message_id: &str) -> Result<Embed> {
|
|
||||||
self.validate()
|
|
||||||
.map_err(|error| Error::FailedValidation { error })?;
|
|
||||||
|
|
||||||
let media = if let Some(id) = self.media {
|
|
||||||
Some(
|
|
||||||
db.find_and_use_attachment(&id, "attachments", "message", message_id)
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Embed::Text(Text {
|
|
||||||
icon_url: self.icon_url,
|
|
||||||
url: self.url,
|
|
||||||
title: self.title,
|
|
||||||
description: self.description,
|
|
||||||
media,
|
|
||||||
colour: self.colour,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BulkMessageResponse {
|
|
||||||
pub async fn transform(
|
|
||||||
db: &Database,
|
|
||||||
channel: Option<&Channel>,
|
|
||||||
messages: Vec<Message>,
|
|
||||||
user: &User,
|
|
||||||
include_users: Option<bool>,
|
|
||||||
) -> Result<BulkMessageResponse> {
|
|
||||||
if let Some(true) = include_users {
|
|
||||||
let user_ids = messages.get_user_ids();
|
|
||||||
let users = User::fetch_foreign_users(db, &user_ids)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|x| x.with_relationship(user))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(match channel {
|
|
||||||
Some(Channel::TextChannel { server, .. })
|
|
||||||
| Some(Channel::VoiceChannel { server, .. }) => {
|
|
||||||
BulkMessageResponse::MessagesAndUsers {
|
|
||||||
messages,
|
|
||||||
users,
|
|
||||||
members: Some(db.fetch_members(server, &user_ids).await?),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => BulkMessageResponse::MessagesAndUsers {
|
|
||||||
messages,
|
|
||||||
users,
|
|
||||||
members: None,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(BulkMessageResponse::JustMessages(messages))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Interactions {
|
|
||||||
/// Validate interactions info is correct
|
|
||||||
pub async fn validate(
|
|
||||||
&self,
|
|
||||||
db: &Database,
|
|
||||||
permissions: &mut PermissionCalculator<'_>,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Some(reactions) = &self.reactions {
|
|
||||||
permissions.throw_permission(db, Permission::React).await?;
|
|
||||||
|
|
||||||
if reactions.len() > 20 {
|
|
||||||
return Err(Error::InvalidOperation);
|
|
||||||
}
|
|
||||||
|
|
||||||
for reaction in reactions {
|
|
||||||
if !Emoji::can_use(db, reaction).await? {
|
|
||||||
return Err(Error::InvalidOperation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if we can use a given emoji to react
|
|
||||||
pub fn can_use(&self, emoji: &str) -> bool {
|
|
||||||
if self.restrict_reactions {
|
|
||||||
if let Some(reactions) = &self.reactions {
|
|
||||||
reactions.contains(emoji)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if default initialisation of fields
|
|
||||||
pub fn is_default(&self) -> bool {
|
|
||||||
!self.restrict_reactions && self.reactions.is_none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn throw_permission(permissions: u64, permission: Permission) -> Result<()> {
|
|
||||||
if (permission as u64) & permissions == (permission as u64) {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::MissingPermission { permission })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataMessageSend {
|
|
||||||
pub fn validate_webhook_permissions(
|
|
||||||
&self,
|
|
||||||
permissions: u64,
|
|
||||||
) -> Result<()> {
|
|
||||||
throw_permission(permissions, Permission::SendMessage)?;
|
|
||||||
|
|
||||||
if self.attachments.as_ref().map_or(false, |v| !v.is_empty()) {
|
|
||||||
throw_permission(permissions, Permission::UploadFiles)?;
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.embeds.as_ref().map_or(false, |v| !v.is_empty()) {
|
|
||||||
throw_permission(permissions, Permission::SendEmbeds)?;
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.masquerade.is_some() {
|
|
||||||
throw_permission(permissions, Permission::Masquerade)?;
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.interactions.is_some() {
|
|
||||||
throw_permission(permissions, Permission::React)?;
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
use crate::{models::File, Database, Result};
|
|
||||||
|
|
||||||
impl File {
|
|
||||||
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "attachments", "message", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_background(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "backgrounds", "user", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_avatar(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "avatars", "user", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_server_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_banner(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "banners", "server", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "emojis", "object", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
use std::{collections::HashSet, str::FromStr};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
events::client::EventV1,
|
|
||||||
models::{emoji::EmojiParent, Emoji},
|
|
||||||
Database, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| include_str!(crate::asset!("emojis.txt"))
|
|
||||||
.split('\n')
|
|
||||||
.map(|x| x.into())
|
|
||||||
.collect());
|
|
||||||
|
|
||||||
impl Emoji {
|
|
||||||
/// Get parent id
|
|
||||||
fn parent(&self) -> &str {
|
|
||||||
match &self.parent {
|
|
||||||
EmojiParent::Server { id } => id,
|
|
||||||
EmojiParent::Detached => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an emoji
|
|
||||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
|
||||||
db.insert_emoji(self).await?;
|
|
||||||
EventV1::EmojiCreate(self.clone())
|
|
||||||
.p(self.parent().to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete an emoji
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether we can use a given emoji
|
|
||||||
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
|
||||||
if Ulid::from_str(emoji).is_ok() {
|
|
||||||
db.fetch_emoji(emoji).await?;
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//! Database agnostic implementations.
|
|
||||||
|
|
||||||
pub mod media {
|
|
||||||
pub mod attachment;
|
|
||||||
pub mod emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod channels {
|
|
||||||
pub mod channel;
|
|
||||||
pub mod channel_invite;
|
|
||||||
pub mod channel_unread;
|
|
||||||
pub mod message;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod servers {
|
|
||||||
pub mod server;
|
|
||||||
pub mod server_ban;
|
|
||||||
pub mod server_member;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod users {
|
|
||||||
pub mod user;
|
|
||||||
pub mod user_settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod safety {
|
|
||||||
pub mod report;
|
|
||||||
pub mod snapshot;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
use iso8601_timestamp::Timestamp;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
models::report::PartialReport,
|
|
||||||
models::{report::ReportStatus, Report},
|
|
||||||
Database, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Report {
|
|
||||||
/// Update report data
|
|
||||||
pub async fn update(&mut self, db: &Database, partial: PartialReport) -> Result<()> {
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
match &mut self.status {
|
|
||||||
ReportStatus::Created {} => {}
|
|
||||||
ReportStatus::Rejected { closed_at, .. } => {
|
|
||||||
if closed_at.is_none() {
|
|
||||||
closed_at.replace(Timestamp::now_utc());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ReportStatus::Resolved { closed_at } => {
|
|
||||||
if closed_at.is_none() {
|
|
||||||
closed_at.replace(Timestamp::now_utc());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db.update_report(&self.id, &partial).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
models::{
|
|
||||||
message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod},
|
|
||||||
snapshot::SnapshotContent,
|
|
||||||
Message, Server, User,
|
|
||||||
},
|
|
||||||
Database, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl SnapshotContent {
|
|
||||||
pub async fn generate_from_message(
|
|
||||||
db: &Database,
|
|
||||||
message: Message,
|
|
||||||
) -> Result<(SnapshotContent, Vec<String>)> {
|
|
||||||
// Collect message attachments
|
|
||||||
let files = message
|
|
||||||
.attachments
|
|
||||||
.as_ref()
|
|
||||||
.map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Collect prior context
|
|
||||||
let prior_context = db
|
|
||||||
.fetch_messages(MessageQuery {
|
|
||||||
filter: MessageFilter {
|
|
||||||
channel: Some(message.channel.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
limit: Some(15),
|
|
||||||
time_period: MessageTimePeriod::Absolute {
|
|
||||||
before: Some(message.id.to_string()),
|
|
||||||
after: None,
|
|
||||||
sort: Some(MessageSort::Latest),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Collect leading context
|
|
||||||
let leading_context = db
|
|
||||||
.fetch_messages(MessageQuery {
|
|
||||||
filter: MessageFilter {
|
|
||||||
channel: Some(message.channel.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
limit: Some(15),
|
|
||||||
time_period: MessageTimePeriod::Absolute {
|
|
||||||
before: None,
|
|
||||||
after: Some(message.id.to_string()),
|
|
||||||
sort: Some(MessageSort::Oldest),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
SnapshotContent::Message {
|
|
||||||
message,
|
|
||||||
prior_context,
|
|
||||||
leading_context,
|
|
||||||
},
|
|
||||||
files,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec<String>)> {
|
|
||||||
// Collect server's icon and banner
|
|
||||||
let files = [&server.icon, &server.banner]
|
|
||||||
.iter()
|
|
||||||
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok((SnapshotContent::Server(server), files))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> {
|
|
||||||
// Collect user's avatar and profile background
|
|
||||||
let files = [
|
|
||||||
user.avatar.as_ref(),
|
|
||||||
user.profile
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|profile| profile.background.as_ref()),
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok((SnapshotContent::User(user), files))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
events::client::EventV1,
|
|
||||||
models::{
|
|
||||||
message::SystemMessage,
|
|
||||||
server::{
|
|
||||||
FieldsRole, FieldsServer, PartialRole, PartialServer, Role, SystemMessageChannels,
|
|
||||||
},
|
|
||||||
server_member::{MemberCompositeKey, RemovalIntention},
|
|
||||||
Channel, Member, Server, ServerBan, User,
|
|
||||||
},
|
|
||||||
perms, Database, Error, OverrideField, Permission, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Role {
|
|
||||||
/// Into optional struct
|
|
||||||
pub fn into_optional(self) -> PartialRole {
|
|
||||||
PartialRole {
|
|
||||||
name: Some(self.name),
|
|
||||||
permissions: Some(self.permissions),
|
|
||||||
colour: self.colour,
|
|
||||||
hoist: Some(self.hoist),
|
|
||||||
rank: Some(self.rank),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a role
|
|
||||||
pub async fn create(&self, db: &Database, server_id: &str) -> Result<String> {
|
|
||||||
let role_id = Ulid::new().to_string();
|
|
||||||
db.insert_role(server_id, &role_id, self).await?;
|
|
||||||
|
|
||||||
EventV1::ServerRoleUpdate {
|
|
||||||
id: server_id.to_string(),
|
|
||||||
role_id: role_id.to_string(),
|
|
||||||
data: self.clone().into_optional(),
|
|
||||||
clear: vec![],
|
|
||||||
}
|
|
||||||
.p(server_id.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(role_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update server data
|
|
||||||
pub async fn update<'a>(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
server_id: &str,
|
|
||||||
role_id: &str,
|
|
||||||
partial: PartialRole,
|
|
||||||
remove: Vec<FieldsRole>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for field in &remove {
|
|
||||||
self.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
db.update_role(server_id, role_id, &partial, remove.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
EventV1::ServerRoleUpdate {
|
|
||||||
id: server_id.to_string(),
|
|
||||||
role_id: role_id.to_string(),
|
|
||||||
data: partial,
|
|
||||||
clear: vec![],
|
|
||||||
}
|
|
||||||
.p(server_id.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a role
|
|
||||||
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
|
|
||||||
EventV1::ServerRoleDelete {
|
|
||||||
id: server_id.to_string(),
|
|
||||||
role_id: role_id.to_string(),
|
|
||||||
}
|
|
||||||
.p(server_id.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
db.delete_role(server_id, role_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove field from Role
|
|
||||||
pub fn remove(&mut self, field: &FieldsRole) {
|
|
||||||
match field {
|
|
||||||
FieldsRole::Colour => self.colour = None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
/// Create a server
|
|
||||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
|
||||||
db.insert_server(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update server data
|
|
||||||
pub async fn update<'a>(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
partial: PartialServer,
|
|
||||||
remove: Vec<FieldsServer>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for field in &remove {
|
|
||||||
self.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
db.update_server(&self.id, &partial, remove.clone()).await?;
|
|
||||||
|
|
||||||
EventV1::ServerUpdate {
|
|
||||||
id: self.id.clone(),
|
|
||||||
data: partial,
|
|
||||||
clear: remove,
|
|
||||||
}
|
|
||||||
.p(self.id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a server
|
|
||||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
|
||||||
EventV1::ServerDelete {
|
|
||||||
id: self.id.clone(),
|
|
||||||
}
|
|
||||||
.p(self.id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
db.delete_server(&self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a field from Server
|
|
||||||
pub fn remove(&mut self, field: &FieldsServer) {
|
|
||||||
match field {
|
|
||||||
FieldsServer::Description => self.description = None,
|
|
||||||
FieldsServer::Categories => self.categories = None,
|
|
||||||
FieldsServer::SystemMessages => self.system_messages = None,
|
|
||||||
FieldsServer::Icon => self.icon = None,
|
|
||||||
FieldsServer::Banner => self.banner = None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set role permission on a server
|
|
||||||
pub async fn set_role_permission(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
role_id: &str,
|
|
||||||
permissions: OverrideField,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Some(role) = self.roles.get_mut(role_id) {
|
|
||||||
role.update(
|
|
||||||
db,
|
|
||||||
&self.id,
|
|
||||||
role_id,
|
|
||||||
PartialRole {
|
|
||||||
permissions: Some(permissions),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::NotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new member in a server
|
|
||||||
pub async fn create_member(
|
|
||||||
&self,
|
|
||||||
db: &Database,
|
|
||||||
user: User,
|
|
||||||
channels: Option<Vec<Channel>>,
|
|
||||||
) -> Result<Vec<Channel>> {
|
|
||||||
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
|
|
||||||
return Err(Error::Banned);
|
|
||||||
}
|
|
||||||
|
|
||||||
let member = Member::new(self.id.clone(), user.id.clone());
|
|
||||||
db.insert_member(&member).await?;
|
|
||||||
|
|
||||||
let should_fetch = channels.is_none();
|
|
||||||
let mut channels = channels.unwrap_or_default();
|
|
||||||
|
|
||||||
if should_fetch {
|
|
||||||
let perm = perms(&user).server(self).member(&member);
|
|
||||||
let existing_channels = db.fetch_channels(&self.channels).await?;
|
|
||||||
for channel in existing_channels {
|
|
||||||
if perm
|
|
||||||
.clone()
|
|
||||||
.channel(&channel)
|
|
||||||
.has_permission(db, Permission::ViewChannel)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
channels.push(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let emojis = db.fetch_emoji_by_parent_id(&self.id).await?;
|
|
||||||
|
|
||||||
EventV1::ServerMemberJoin {
|
|
||||||
id: self.id.clone(),
|
|
||||||
user: user.id.clone(),
|
|
||||||
}
|
|
||||||
.p(self.id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
EventV1::ServerCreate {
|
|
||||||
id: self.id.clone(),
|
|
||||||
server: self.clone(),
|
|
||||||
channels: channels.clone(),
|
|
||||||
emojis,
|
|
||||||
}
|
|
||||||
.private(user.id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Some(id) = self
|
|
||||||
.system_messages
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|x| x.user_joined.as_ref())
|
|
||||||
{
|
|
||||||
SystemMessage::UserJoined {
|
|
||||||
id: user.id.clone(),
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.create_no_web_push(db, id, false)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(channels)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a member from a server
|
|
||||||
pub async fn remove_member(
|
|
||||||
&self,
|
|
||||||
db: &Database,
|
|
||||||
member: Member,
|
|
||||||
intention: RemovalIntention,
|
|
||||||
silent: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
db.delete_member(&member.id).await?;
|
|
||||||
|
|
||||||
EventV1::ServerMemberLeave {
|
|
||||||
id: self.id.to_string(),
|
|
||||||
user: member.id.user.clone(),
|
|
||||||
}
|
|
||||||
.p(member.id.server)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !silent {
|
|
||||||
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
|
|
||||||
RemovalIntention::Leave => x.user_left.as_ref(),
|
|
||||||
RemovalIntention::Kick => x.user_kicked.as_ref(),
|
|
||||||
RemovalIntention::Ban => x.user_banned.as_ref(),
|
|
||||||
}) {
|
|
||||||
match intention {
|
|
||||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
|
|
||||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
|
|
||||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.create_no_web_push(db, id, false)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create ban
|
|
||||||
pub async fn ban_user(
|
|
||||||
self,
|
|
||||||
db: &Database,
|
|
||||||
id: MemberCompositeKey,
|
|
||||||
reason: Option<String>,
|
|
||||||
) -> Result<ServerBan> {
|
|
||||||
let ban = ServerBan { id, reason };
|
|
||||||
db.insert_ban(&ban).await?;
|
|
||||||
Ok(ban)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ban a member from a server
|
|
||||||
pub async fn ban_member(
|
|
||||||
self,
|
|
||||||
db: &Database,
|
|
||||||
member: Member,
|
|
||||||
reason: Option<String>,
|
|
||||||
) -> Result<ServerBan> {
|
|
||||||
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.ban_user(db, member.id, reason).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SystemMessageChannels {
|
|
||||||
pub fn into_channel_ids(self) -> HashSet<String> {
|
|
||||||
let mut ids = HashSet::new();
|
|
||||||
|
|
||||||
if let Some(id) = self.user_joined {
|
|
||||||
ids.insert(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(id) = self.user_left {
|
|
||||||
ids.insert(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(id) = self.user_kicked {
|
|
||||||
ids.insert(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(id) = self.user_banned {
|
|
||||||
ids.insert(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use iso8601_timestamp::Timestamp;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
events::client::EventV1,
|
|
||||||
models::{
|
|
||||||
server_member::{FieldsMember, MemberCompositeKey, MemberWithRoles, PartialMember},
|
|
||||||
Member, Server,
|
|
||||||
},
|
|
||||||
Database, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Member {
|
|
||||||
pub fn new(server_id: String, user_id: String) -> Self {
|
|
||||||
Self {
|
|
||||||
id: MemberCompositeKey {
|
|
||||||
server: server_id,
|
|
||||||
user: user_id,
|
|
||||||
},
|
|
||||||
joined_at: Timestamp::now_utc(),
|
|
||||||
nickname: None,
|
|
||||||
avatar: None,
|
|
||||||
roles: vec![],
|
|
||||||
timeout: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update member data
|
|
||||||
pub async fn update<'a>(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
partial: PartialMember,
|
|
||||||
remove: Vec<FieldsMember>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for field in &remove {
|
|
||||||
self.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
db.update_member(&self.id, &partial, remove.clone()).await?;
|
|
||||||
|
|
||||||
EventV1::ServerMemberUpdate {
|
|
||||||
id: self.id.clone(),
|
|
||||||
data: partial,
|
|
||||||
clear: remove,
|
|
||||||
}
|
|
||||||
.p(self.id.server.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get this user's current ranking
|
|
||||||
pub fn get_ranking(&self, server: &Server) -> i64 {
|
|
||||||
let mut value = i64::MAX;
|
|
||||||
for role in &self.roles {
|
|
||||||
if let Some(role) = server.roles.get(role) {
|
|
||||||
if role.rank < value {
|
|
||||||
value = role.rank;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether this member is in timeout
|
|
||||||
pub fn in_timeout(&self) -> bool {
|
|
||||||
if let Some(timeout) = self.timeout {
|
|
||||||
*timeout > *Timestamp::now_utc()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, field: &FieldsMember) {
|
|
||||||
match field {
|
|
||||||
FieldsMember::Avatar => self.avatar = None,
|
|
||||||
FieldsMember::Nickname => self.nickname = None,
|
|
||||||
FieldsMember::Roles => self.roles.clear(),
|
|
||||||
FieldsMember::Timeout => self.timeout = None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemberWithRoles {
|
|
||||||
pub fn new(server_id: String, user_id: String) -> Self {
|
|
||||||
Self {
|
|
||||||
member: Member::new(server_id, user_id),
|
|
||||||
roles: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
use crate::events::client::EventV1;
|
|
||||||
use crate::models::user::{
|
|
||||||
Badges, FieldsUser, PartialUser, Presence, RelationshipStatus, User, UserHint,
|
|
||||||
};
|
|
||||||
use crate::permissions::defn::UserPerms;
|
|
||||||
use crate::permissions::r#impl::user::get_relationship;
|
|
||||||
use crate::{perms, Database, Error, Result};
|
|
||||||
|
|
||||||
use futures::try_join;
|
|
||||||
use impl_ops::impl_op_ex_commutative;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
use revolt_database::RatelimitEventType;
|
|
||||||
use revolt_presence::filter_online;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::ops;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
/// Update user data
|
|
||||||
pub async fn update<'a>(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
partial: PartialUser,
|
|
||||||
remove: Vec<FieldsUser>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for field in &remove {
|
|
||||||
self.remove(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
db.update_user(&self.id, &partial, remove.clone()).await?;
|
|
||||||
|
|
||||||
EventV1::UserUpdate {
|
|
||||||
id: self.id.clone(),
|
|
||||||
data: partial,
|
|
||||||
clear: remove,
|
|
||||||
event_id: Some(ulid::Ulid::new().to_string()),
|
|
||||||
}
|
|
||||||
.p_user(self.id.clone(), db)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a field from User object
|
|
||||||
pub fn remove(&mut self, field: &FieldsUser) {
|
|
||||||
match field {
|
|
||||||
FieldsUser::Avatar => self.avatar = None,
|
|
||||||
FieldsUser::StatusText => {
|
|
||||||
if let Some(x) = self.status.as_mut() {
|
|
||||||
x.text = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FieldsUser::StatusPresence => {
|
|
||||||
if let Some(x) = self.status.as_mut() {
|
|
||||||
x.presence = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FieldsUser::ProfileContent => {
|
|
||||||
if let Some(x) = self.profile.as_mut() {
|
|
||||||
x.content = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FieldsUser::ProfileBackground => {
|
|
||||||
if let Some(x) = self.profile.as_mut() {
|
|
||||||
x.background = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FieldsUser::DisplayName => self.display_name = None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutate the user object to remove redundant information
|
|
||||||
#[must_use]
|
|
||||||
pub fn foreign(mut self) -> User {
|
|
||||||
self.profile = None;
|
|
||||||
self.relations = None;
|
|
||||||
|
|
||||||
let mut badges = self.badges.unwrap_or(0);
|
|
||||||
if let Ok(id) = ulid::Ulid::from_string(&self.id) {
|
|
||||||
// Yes, this is hard-coded
|
|
||||||
// No, I don't care + ratio
|
|
||||||
if id.datetime().timestamp_millis() < 1629638578431 {
|
|
||||||
badges = badges + Badges::EarlyAdopter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.badges = Some(badges);
|
|
||||||
|
|
||||||
if let Some(status) = &self.status {
|
|
||||||
if let Some(presence) = &status.presence {
|
|
||||||
if presence == &Presence::Invisible {
|
|
||||||
self.status = None;
|
|
||||||
self.online = Some(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch foreign users by a list of IDs
|
|
||||||
pub async fn fetch_foreign_users(db: &Database, user_ids: &[String]) -> Result<Vec<User>> {
|
|
||||||
let online_ids = filter_online(user_ids).await;
|
|
||||||
|
|
||||||
Ok(db
|
|
||||||
.fetch_users(user_ids)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut user| {
|
|
||||||
user.online = Some(online_ids.contains(&user.id));
|
|
||||||
user.foreign()
|
|
||||||
})
|
|
||||||
.collect::<Vec<User>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutate the user object to include relationship (if it does not already exist)
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_relationship(self, perspective: &User) -> User {
|
|
||||||
let mut user = self.foreign();
|
|
||||||
|
|
||||||
if user.relationship.is_none() {
|
|
||||||
user.relationship = Some(get_relationship(perspective, &user.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
user
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutate user object with given permission
|
|
||||||
#[must_use]
|
|
||||||
pub fn apply_permission(mut self, permission: &UserPerms) -> User {
|
|
||||||
if !permission.get_view_profile() {
|
|
||||||
self.status = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to apply relationship and permission
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_perspective(self, perspective: &User, permission: &UserPerms) -> User {
|
|
||||||
self.with_relationship(perspective)
|
|
||||||
.apply_permission(permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to calculate perspective
|
|
||||||
pub async fn with_auto_perspective(self, db: &Database, perspective: &User) -> User {
|
|
||||||
let user = self.with_relationship(perspective);
|
|
||||||
let permissions = perms(perspective).user(&user).calc_user(db).await;
|
|
||||||
user.apply_permission(&permissions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether two users have a mutual connection
|
|
||||||
///
|
|
||||||
/// This will check if user and user_b share a server or a group.
|
|
||||||
pub async fn has_mutual_connection(&self, db: &Database, user_b: &str) -> Result<bool> {
|
|
||||||
Ok(!db
|
|
||||||
.fetch_mutual_server_ids(&self.id, user_b)
|
|
||||||
.await?
|
|
||||||
.is_empty()
|
|
||||||
|| !db
|
|
||||||
.fetch_mutual_channel_ids(&self.id, user_b)
|
|
||||||
.await?
|
|
||||||
.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this user can acquire another server
|
|
||||||
pub async fn can_acquire_server(&self, db: &Database) -> Result<bool> {
|
|
||||||
// hardcoded max server count
|
|
||||||
// NB. fixed in new crate
|
|
||||||
Ok(db.fetch_server_count(&self.id).await? <= 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sanitise and validate a username can be used
|
|
||||||
pub fn validate_username(username: String) -> Result<String> {
|
|
||||||
// Copy the username for validation
|
|
||||||
let username_lowercase = username.to_lowercase();
|
|
||||||
|
|
||||||
// Block homoglyphs
|
|
||||||
if decancer::cure(&username_lowercase).into_str() != username_lowercase {
|
|
||||||
return Err(Error::InvalidUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the username itself isn't blocked
|
|
||||||
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"];
|
|
||||||
|
|
||||||
for username in BLOCKED_USERNAMES {
|
|
||||||
if username_lowercase == *username {
|
|
||||||
return Err(Error::InvalidUsername);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure none of the following substrings show up in the username
|
|
||||||
const BLOCKED_SUBSTRINGS: &[&str] = &["```"];
|
|
||||||
|
|
||||||
for substr in BLOCKED_SUBSTRINGS {
|
|
||||||
if username_lowercase.contains(substr) {
|
|
||||||
return Err(Error::InvalidUsername);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a free discriminator for a given username
|
|
||||||
pub async fn find_discriminator(
|
|
||||||
db: &Database,
|
|
||||||
username: &str,
|
|
||||||
preferred: Option<(String, String)>,
|
|
||||||
) -> Result<String> {
|
|
||||||
let search_space: &HashSet<String> = &DISCRIMINATOR_SEARCH_SPACE_QUARK;
|
|
||||||
let used_discriminators: HashSet<String> = db
|
|
||||||
.fetch_discriminators_in_use(username)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let available_discriminators: Vec<&String> =
|
|
||||||
search_space.difference(&used_discriminators).collect();
|
|
||||||
|
|
||||||
if available_discriminators.is_empty() {
|
|
||||||
return Err(Error::UsernameTaken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((preferred, target_id)) = preferred {
|
|
||||||
if available_discriminators.contains(&&preferred) {
|
|
||||||
return Ok(preferred);
|
|
||||||
} else {
|
|
||||||
let rvdb: revolt_database::Database = db.clone().into();
|
|
||||||
if rvdb
|
|
||||||
.has_ratelimited(
|
|
||||||
&target_id,
|
|
||||||
RatelimitEventType::DiscriminatorChange,
|
|
||||||
Duration::from_secs(60 * 60 * 24),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(Error::from_core)?
|
|
||||||
{
|
|
||||||
return Err(Error::DiscriminatorChangeRatelimited);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: don't access directly?
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
rvdb.insert_ratelimit_event(&revolt_database::RatelimitEvent {
|
|
||||||
id: ulid::Ulid::new().to_string(),
|
|
||||||
target_id,
|
|
||||||
event_type: RatelimitEventType::DiscriminatorChange,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(Error::from_core)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
Ok(available_discriminators
|
|
||||||
.choose(&mut rng)
|
|
||||||
.expect("we can assert this has an element")
|
|
||||||
.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update a user's username
|
|
||||||
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
|
|
||||||
let username = User::validate_username(username)?;
|
|
||||||
if self.username.to_lowercase() == username.to_lowercase() {
|
|
||||||
self.update(
|
|
||||||
db,
|
|
||||||
PartialUser {
|
|
||||||
username: Some(username),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.update(
|
|
||||||
db,
|
|
||||||
PartialUser {
|
|
||||||
discriminator: Some(
|
|
||||||
User::find_discriminator(
|
|
||||||
db,
|
|
||||||
&username,
|
|
||||||
Some((self.discriminator.to_string(), self.id.clone())),
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
),
|
|
||||||
username: Some(username),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply a certain relationship between two users
|
|
||||||
pub async fn apply_relationship(
|
|
||||||
&self,
|
|
||||||
db: &Database,
|
|
||||||
target: &mut User,
|
|
||||||
local: RelationshipStatus,
|
|
||||||
remote: RelationshipStatus,
|
|
||||||
) -> Result<()> {
|
|
||||||
if try_join!(
|
|
||||||
db.set_relationship(&self.id, &target.id, &local),
|
|
||||||
db.set_relationship(&target.id, &self.id, &remote)
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return Err(Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
EventV1::UserRelationship {
|
|
||||||
id: target.id.clone(),
|
|
||||||
user: self.clone().with_relationship(target),
|
|
||||||
status: remote,
|
|
||||||
}
|
|
||||||
.private(target.id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
EventV1::UserRelationship {
|
|
||||||
id: self.id.clone(),
|
|
||||||
user: target.clone().with_relationship(self),
|
|
||||||
status: local.clone(),
|
|
||||||
}
|
|
||||||
.private(self.id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
target.relationship.replace(local);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add another user as a friend
|
|
||||||
pub async fn add_friend(&self, db: &Database, target: &mut User) -> Result<()> {
|
|
||||||
match get_relationship(self, &target.id) {
|
|
||||||
RelationshipStatus::User => Err(Error::NoEffect),
|
|
||||||
RelationshipStatus::Friend => Err(Error::AlreadyFriends),
|
|
||||||
RelationshipStatus::Outgoing => Err(Error::AlreadySentRequest),
|
|
||||||
RelationshipStatus::Blocked => Err(Error::Blocked),
|
|
||||||
RelationshipStatus::BlockedOther => Err(Error::BlockedByOther),
|
|
||||||
RelationshipStatus::Incoming => {
|
|
||||||
self.apply_relationship(
|
|
||||||
db,
|
|
||||||
target,
|
|
||||||
RelationshipStatus::Friend,
|
|
||||||
RelationshipStatus::Friend,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
RelationshipStatus::None => {
|
|
||||||
self.apply_relationship(
|
|
||||||
db,
|
|
||||||
target,
|
|
||||||
RelationshipStatus::Outgoing,
|
|
||||||
RelationshipStatus::Incoming,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove another user as a friend
|
|
||||||
pub async fn remove_friend(&self, db: &Database, target: &mut User) -> Result<()> {
|
|
||||||
match get_relationship(self, &target.id) {
|
|
||||||
RelationshipStatus::Friend
|
|
||||||
| RelationshipStatus::Outgoing
|
|
||||||
| RelationshipStatus::Incoming => {
|
|
||||||
self.apply_relationship(
|
|
||||||
db,
|
|
||||||
target,
|
|
||||||
RelationshipStatus::None,
|
|
||||||
RelationshipStatus::None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
_ => Err(Error::NoEffect),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Block another user
|
|
||||||
pub async fn block_user(&self, db: &Database, target: &mut User) -> Result<()> {
|
|
||||||
match get_relationship(self, &target.id) {
|
|
||||||
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
|
|
||||||
RelationshipStatus::BlockedOther => {
|
|
||||||
self.apply_relationship(
|
|
||||||
db,
|
|
||||||
target,
|
|
||||||
RelationshipStatus::Blocked,
|
|
||||||
RelationshipStatus::Blocked,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
RelationshipStatus::None
|
|
||||||
| RelationshipStatus::Friend
|
|
||||||
| RelationshipStatus::Incoming
|
|
||||||
| RelationshipStatus::Outgoing => {
|
|
||||||
self.apply_relationship(
|
|
||||||
db,
|
|
||||||
target,
|
|
||||||
RelationshipStatus::Blocked,
|
|
||||||
RelationshipStatus::BlockedOther,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unblock another user
|
|
||||||
pub async fn unblock_user(&self, db: &Database, target: &mut User) -> Result<()> {
|
|
||||||
match get_relationship(self, &target.id) {
|
|
||||||
RelationshipStatus::Blocked => match get_relationship(target, &self.id) {
|
|
||||||
RelationshipStatus::Blocked => {
|
|
||||||
self.apply_relationship(
|
|
||||||
db,
|
|
||||||
target,
|
|
||||||
RelationshipStatus::BlockedOther,
|
|
||||||
RelationshipStatus::Blocked,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
RelationshipStatus::BlockedOther => {
|
|
||||||
self.apply_relationship(
|
|
||||||
db,
|
|
||||||
target,
|
|
||||||
RelationshipStatus::None,
|
|
||||||
RelationshipStatus::None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
_ => Err(Error::InternalError),
|
|
||||||
},
|
|
||||||
_ => Err(Error::NoEffect),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether this user has another user blocked
|
|
||||||
pub fn has_blocked(&self, user: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
get_relationship(self, user),
|
|
||||||
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark as deleted
|
|
||||||
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
|
|
||||||
self.update(
|
|
||||||
db,
|
|
||||||
PartialUser {
|
|
||||||
username: Some(format!("Deleted User {}", self.id)),
|
|
||||||
flags: Some(2),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![
|
|
||||||
FieldsUser::Avatar,
|
|
||||||
FieldsUser::StatusText,
|
|
||||||
FieldsUser::StatusPresence,
|
|
||||||
FieldsUser::ProfileContent,
|
|
||||||
FieldsUser::ProfileBackground,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find a user from a given token and hint
|
|
||||||
#[async_recursion]
|
|
||||||
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> {
|
|
||||||
match hint {
|
|
||||||
UserHint::Bot => {
|
|
||||||
let rvdb: revolt_database::Database = db.clone().into();
|
|
||||||
db.fetch_user(
|
|
||||||
&rvdb
|
|
||||||
.fetch_bot_by_token(token)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::InternalError)?
|
|
||||||
.id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
UserHint::User => db.fetch_user_by_token(token).await,
|
|
||||||
UserHint::Any => {
|
|
||||||
if let Ok(user) = User::from_token(db, token, UserHint::User).await {
|
|
||||||
Ok(user)
|
|
||||||
} else {
|
|
||||||
User::from_token(db, token, UserHint::Bot).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static DISCRIMINATOR_SEARCH_SPACE_QUARK: Lazy<HashSet<String>> = Lazy::new(|| {
|
|
||||||
let mut set = (2..9999)
|
|
||||||
.map(|v| format!("{:0>4}", v))
|
|
||||||
.collect::<HashSet<String>>();
|
|
||||||
|
|
||||||
for discrim in [
|
|
||||||
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
|
|
||||||
] {
|
|
||||||
set.remove(&format!("{:0>4}", discrim));
|
|
||||||
}
|
|
||||||
|
|
||||||
set.into_iter().collect()
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
use crate::{events::client::EventV1, models::UserSettings, Database, Result};
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait UserSettingsImpl {
|
|
||||||
async fn set(self, db: &Database, user: &str) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl UserSettingsImpl for UserSettings {
|
|
||||||
async fn set(self, db: &Database, user: &str) -> Result<()> {
|
|
||||||
db.set_user_settings(user, &self).await?;
|
|
||||||
|
|
||||||
EventV1::UserSettingsUpdate {
|
|
||||||
id: user.to_string(),
|
|
||||||
update: self,
|
|
||||||
}
|
|
||||||
.private(user.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
mod dummy;
|
|
||||||
mod generic;
|
|
||||||
mod mongo;
|
|
||||||
|
|
||||||
#[cfg(feature = "rocket_impl")]
|
|
||||||
mod rocket;
|
|
||||||
|
|
||||||
pub use self::generic::users::user_settings::UserSettingsImpl;
|
|
||||||
pub use dummy::DummyDb;
|
|
||||||
pub use mongo::MongoDb;
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use bson::{from_document, Document};
|
|
||||||
use futures::StreamExt;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
models::stats::{Index, Stats},
|
|
||||||
AbstractStats, Error, Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractStats for MongoDb {
|
|
||||||
async fn generate_stats(&self) -> Result<Stats> {
|
|
||||||
let mut indices = HashMap::new();
|
|
||||||
let mut coll_stats = HashMap::new();
|
|
||||||
|
|
||||||
let collection_names =
|
|
||||||
self.db()
|
|
||||||
.list_collection_names(None)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "list_collection_names",
|
|
||||||
with: "database",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for collection in collection_names {
|
|
||||||
indices.insert(
|
|
||||||
collection.to_string(),
|
|
||||||
self.col::<Document>(&collection)
|
|
||||||
.aggregate(
|
|
||||||
vec![doc! {
|
|
||||||
"$indexStats": { }
|
|
||||||
}],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "aggregate",
|
|
||||||
with: "col",
|
|
||||||
})?
|
|
||||||
.filter_map(|s| async { s.ok() })
|
|
||||||
.collect::<Vec<Document>>()
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|doc| from_document(doc).ok())
|
|
||||||
.collect::<Vec<Index>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
coll_stats.insert(
|
|
||||||
collection.to_string(),
|
|
||||||
self.col::<Document>(&collection)
|
|
||||||
.aggregate(
|
|
||||||
vec![doc! {
|
|
||||||
"$collStats": {
|
|
||||||
"latencyStats": {
|
|
||||||
"histograms": true
|
|
||||||
},
|
|
||||||
"storageStats": {},
|
|
||||||
"count": {},
|
|
||||||
"queryExecStats": {}
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "aggregate",
|
|
||||||
with: "col",
|
|
||||||
})?
|
|
||||||
.filter_map(|s| async { s.ok() })
|
|
||||||
.collect::<Vec<Document>>()
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|doc| from_document(doc).ok())
|
|
||||||
.next()
|
|
||||||
.ok_or(Error::DatabaseError {
|
|
||||||
operation: "next aggregation",
|
|
||||||
with: "col",
|
|
||||||
})?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Stats {
|
|
||||||
indices,
|
|
||||||
coll_stats,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
use bson::{Bson, Document};
|
|
||||||
|
|
||||||
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
|
|
||||||
use crate::r#impl::mongo::IntoDocumentPath;
|
|
||||||
use crate::{AbstractChannel, AbstractServer, Error, OverrideField, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "channels";
|
|
||||||
|
|
||||||
impl MongoDb {
|
|
||||||
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
|
|
||||||
// Delete all invites to these channels.
|
|
||||||
self.col::<Document>("channel_invites")
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"channel": &id
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_many",
|
|
||||||
with: "channel_invites",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Delete unread message objects on channels.
|
|
||||||
self.col::<Document>("channel_unreads")
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"_id.channel": &id
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_many",
|
|
||||||
with: "channel_unreads",
|
|
||||||
})
|
|
||||||
.map(|_| ())?;
|
|
||||||
|
|
||||||
// update many attachments with parent id
|
|
||||||
|
|
||||||
// Delete all webhooks on this channel.
|
|
||||||
self.col::<Document>("webhooks")
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"channel": &id
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_many",
|
|
||||||
with: "webhooks",
|
|
||||||
})
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannel for MongoDb {
|
|
||||||
async fn fetch_channel(&self, id: &str) -> Result<Channel> {
|
|
||||||
self.find_one_by_id(COL, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
|
||||||
self.insert_one(COL, channel).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_channel(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
channel: &PartialChannel,
|
|
||||||
remove: Vec<FieldsChannel>,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.update_one_by_id(
|
|
||||||
COL,
|
|
||||||
id,
|
|
||||||
channel,
|
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
|
||||||
let id = channel.id().to_string();
|
|
||||||
let server_id = match channel {
|
|
||||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
|
||||||
Some(server)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete invites and unreads.
|
|
||||||
self.delete_associated_channel_objects(Bson::String(id.to_string()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Delete messages.
|
|
||||||
self.delete_bulk_messages(doc! {
|
|
||||||
"channel": &id
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Remove from server object.
|
|
||||||
if let Some(server) = server_id {
|
|
||||||
let server = self.fetch_server(server).await?;
|
|
||||||
let mut update = doc! {
|
|
||||||
"$pull": {
|
|
||||||
"channels": &id
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(sys) = &server.system_messages {
|
|
||||||
let mut unset = doc! {};
|
|
||||||
|
|
||||||
if let Some(cid) = &sys.user_joined {
|
|
||||||
if &id == cid {
|
|
||||||
unset.insert("system_messages.user_joined", 1_i32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cid) = &sys.user_left {
|
|
||||||
if &id == cid {
|
|
||||||
unset.insert("system_messages.user_left", 1_i32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cid) = &sys.user_kicked {
|
|
||||||
if &id == cid {
|
|
||||||
unset.insert("system_messages.user_kicked", 1_i32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cid) = &sys.user_banned {
|
|
||||||
if &id == cid {
|
|
||||||
unset.insert("system_messages.user_banned", 1_i32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !unset.is_empty() {
|
|
||||||
update.insert("$unset", unset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.col::<Document>("servers")
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": server.id
|
|
||||||
},
|
|
||||||
update,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "servers",
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete associated attachments
|
|
||||||
self.delete_many_attachments(doc! {
|
|
||||||
"object_id": &id
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Delete the channel itself
|
|
||||||
self.delete_one_by_id(COL, &id).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"$or": [
|
|
||||||
{
|
|
||||||
"$or": [
|
|
||||||
{
|
|
||||||
"channel_type": "DirectMessage"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"channel_type": "Group"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"recipients": user_id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"channel_type": "SavedMessages",
|
|
||||||
"user": user_id
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
|
||||||
self.find_one(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"channel_type": "SavedMessages",
|
|
||||||
"user": user_id
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
|
||||||
self.find_one(
|
|
||||||
COL,
|
|
||||||
if user_a == user_b {
|
|
||||||
doc! {
|
|
||||||
"channel_type": "SavedMessages",
|
|
||||||
"user": user_a
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
doc! {
|
|
||||||
"channel_type": "DirectMessage",
|
|
||||||
"recipients": {
|
|
||||||
"$all": [ user_a, user_b ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": channel
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$push": {
|
|
||||||
"recipients": user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "channel",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": channel
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$pull": {
|
|
||||||
"recipients": user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "channel",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_channel_role_permission(
|
|
||||||
&self,
|
|
||||||
channel: &str,
|
|
||||||
role: &str,
|
|
||||||
permissions: OverrideField,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! { "_id": channel },
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"role_permissions.".to_owned() + role: permissions
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "channel",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsChannel {
|
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
|
||||||
Some(match self {
|
|
||||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
|
||||||
FieldsChannel::Description => "description",
|
|
||||||
FieldsChannel::Icon => "icon",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
use crate::models::Invite;
|
|
||||||
use crate::{AbstractChannelInvite, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "channel_invites";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannelInvite for MongoDb {
|
|
||||||
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
|
|
||||||
self.find_one_by_id(COL, code).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
|
||||||
self.insert_one(COL, invite).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_invite(&self, code: &str) -> Result<()> {
|
|
||||||
self.delete_one_by_id(COL, code).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_invites_for_server(&self, server: &str) -> Result<Vec<Invite>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"server": server
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
use bson::Document;
|
|
||||||
use mongodb::options::UpdateOptions;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::models::channel_unread::ChannelUnread;
|
|
||||||
use crate::{AbstractChannelUnread, Error, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "channel_unreads";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannelUnread for MongoDb {
|
|
||||||
async fn acknowledge_message(&self, channel: &str, user: &str, message: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id.channel": channel,
|
|
||||||
"_id.user": user,
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$unset": {
|
|
||||||
"mentions": 1_i32
|
|
||||||
},
|
|
||||||
"$set": {
|
|
||||||
"last_id": message
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UpdateOptions::builder().upsert(true).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "channel_unread",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> {
|
|
||||||
let current_time = Ulid::new().to_string();
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"_id.channel": {
|
|
||||||
"$in": channels
|
|
||||||
},
|
|
||||||
"_id.user": user
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_many",
|
|
||||||
with: "channel_unreads",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.insert_many(
|
|
||||||
channels
|
|
||||||
.iter()
|
|
||||||
.map(|channel| {
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"channel": channel,
|
|
||||||
"user": user
|
|
||||||
},
|
|
||||||
"last_id": ¤t_time
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<Document>>(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_many",
|
|
||||||
with: "channel_unreads",
|
|
||||||
})
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_mention_to_unread<'a>(
|
|
||||||
&self,
|
|
||||||
channel: &str,
|
|
||||||
user: &str,
|
|
||||||
ids: &[String],
|
|
||||||
) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id.channel": channel,
|
|
||||||
"_id.user": user,
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$push": {
|
|
||||||
"mentions": {
|
|
||||||
"$each": ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UpdateOptions::builder().upsert(true).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "channel_unread",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_unreads(&self, user: &str) -> Result<Vec<ChannelUnread>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.user": user
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
use bson::{to_bson, Document};
|
|
||||||
use futures::try_join;
|
|
||||||
use mongodb::options::FindOptions;
|
|
||||||
|
|
||||||
use crate::models::message::{
|
|
||||||
AppendMessage, Message, MessageQuery, MessageSort, MessageTimePeriod, PartialMessage,
|
|
||||||
};
|
|
||||||
use crate::r#impl::mongo::DocumentId;
|
|
||||||
use crate::{AbstractMessage, Error, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "messages";
|
|
||||||
|
|
||||||
impl MongoDb {
|
|
||||||
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
|
|
||||||
let mut for_attachments = projection.clone();
|
|
||||||
for_attachments.insert(
|
|
||||||
"attachments",
|
|
||||||
doc! {
|
|
||||||
"$exists": 1_i32
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if there are any attachments we need to delete.
|
|
||||||
let message_ids_with_attachments = self
|
|
||||||
.find_with_options::<_, DocumentId>(
|
|
||||||
COL,
|
|
||||||
for_attachments,
|
|
||||||
FindOptions::builder()
|
|
||||||
.projection(doc! { "_id": 1_i32 })
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|x| x.id)
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
// If we found any, mark them as deleted.
|
|
||||||
if !message_ids_with_attachments.is_empty() {
|
|
||||||
self.col::<Document>("attachments")
|
|
||||||
.update_many(
|
|
||||||
doc! {
|
|
||||||
"message_id": {
|
|
||||||
"$in": message_ids_with_attachments
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"deleted": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_many",
|
|
||||||
with: "attachments",
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// And then delete said messages.
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.delete_many(projection, None)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_many",
|
|
||||||
with: "messages",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractMessage for MongoDb {
|
|
||||||
async fn fetch_message(&self, id: &str) -> Result<Message> {
|
|
||||||
self.find_one_by_id(COL, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_message(&self, message: &Message) -> Result<()> {
|
|
||||||
self.insert_one(COL, message).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
|
||||||
self.update_one_by_id(COL, id, message, vec![], None)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
|
||||||
let mut query = doc! {};
|
|
||||||
|
|
||||||
if let Some(embeds) = &append.embeds {
|
|
||||||
if !embeds.is_empty() {
|
|
||||||
query.insert(
|
|
||||||
"$push",
|
|
||||||
doc! {
|
|
||||||
"embeds": {
|
|
||||||
"$each": to_bson(embeds)
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "to_bson",
|
|
||||||
with: "embeds"
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "message",
|
|
||||||
})
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_message(&self, id: &str) -> Result<()> {
|
|
||||||
self.delete_one_by_id(COL, id).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> {
|
|
||||||
self.delete_bulk_messages(doc! {
|
|
||||||
"channel": channel,
|
|
||||||
"_id": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
|
|
||||||
let mut filter = doc! {};
|
|
||||||
|
|
||||||
// 1. Apply message filters
|
|
||||||
if let Some(channel) = query.filter.channel {
|
|
||||||
filter.insert("channel", channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(author) = query.filter.author {
|
|
||||||
filter.insert("author", author);
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_search_query = if let Some(query) = query.filter.query {
|
|
||||||
filter.insert(
|
|
||||||
"$text",
|
|
||||||
doc! {
|
|
||||||
"$search": query
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. Find query limit
|
|
||||||
let limit = query.limit.unwrap_or(50);
|
|
||||||
|
|
||||||
// 3. Apply message time period
|
|
||||||
match query.time_period {
|
|
||||||
MessageTimePeriod::Relative { nearby } => {
|
|
||||||
// 3.1. Prepare filters
|
|
||||||
let mut older_message_filter = filter.clone();
|
|
||||||
let mut newer_message_filter = filter;
|
|
||||||
|
|
||||||
older_message_filter.insert(
|
|
||||||
"_id",
|
|
||||||
doc! {
|
|
||||||
"$lt": &nearby
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
newer_message_filter.insert(
|
|
||||||
"_id",
|
|
||||||
doc! {
|
|
||||||
"$gte": &nearby
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3.2. Execute in both directions
|
|
||||||
let (a, b) = try_join!(
|
|
||||||
self.find_with_options::<_, Message>(
|
|
||||||
COL,
|
|
||||||
newer_message_filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit / 2 + 1)
|
|
||||||
.sort(doc! {
|
|
||||||
"_id": 1_i32
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
),
|
|
||||||
self.find_with_options::<_, Message>(
|
|
||||||
COL,
|
|
||||||
older_message_filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit / 2)
|
|
||||||
.sort(doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok([a, b].concat())
|
|
||||||
}
|
|
||||||
MessageTimePeriod::Absolute {
|
|
||||||
before,
|
|
||||||
after,
|
|
||||||
sort,
|
|
||||||
} => {
|
|
||||||
// 3.1. Apply message ID filter
|
|
||||||
if let Some(doc) = match (before, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. Execute with given message sort
|
|
||||||
self.find_with_options(
|
|
||||||
COL,
|
|
||||||
filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit)
|
|
||||||
.sort(match sort.unwrap_or(MessageSort::Latest) {
|
|
||||||
// Sort by relevance, fallback to latest
|
|
||||||
MessageSort::Relevance => {
|
|
||||||
if is_search_query {
|
|
||||||
doc! {
|
|
||||||
"score": {
|
|
||||||
"$meta": "textScore"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort by latest first
|
|
||||||
MessageSort::Latest => doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
},
|
|
||||||
// Sort by oldest first
|
|
||||||
MessageSort::Oldest => doc! {
|
|
||||||
"_id": 1_i32
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new reaction to a message
|
|
||||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$addToSet": {
|
|
||||||
format!("reactions.{emoji}"): user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "message",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a reaction from a message
|
|
||||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$pull": {
|
|
||||||
format!("reactions.{emoji}"): user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "message",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove reaction from a message
|
|
||||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$unset": {
|
|
||||||
format!("reactions.{emoji}"): 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "message",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
use bson::Document;
|
|
||||||
|
|
||||||
use crate::models::attachment::File;
|
|
||||||
use crate::{AbstractAttachment, Error, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "attachments";
|
|
||||||
|
|
||||||
impl MongoDb {
|
|
||||||
pub async fn delete_many_attachments(&self, projection: Document) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_many(
|
|
||||||
projection,
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"deleted": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_many",
|
|
||||||
with: "attachment",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractAttachment for MongoDb {
|
|
||||||
async fn find_and_use_attachment(
|
|
||||||
&self,
|
|
||||||
attachment_id: &str,
|
|
||||||
tag: &str,
|
|
||||||
parent_type: &str,
|
|
||||||
parent_id: &str,
|
|
||||||
) -> Result<File> {
|
|
||||||
let key = format!("{parent_type}_id");
|
|
||||||
match self
|
|
||||||
.find_one::<File>(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id": attachment_id,
|
|
||||||
"tag": tag,
|
|
||||||
&key: {
|
|
||||||
"$exists": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(file) => {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": &file.id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
key: parent_id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "attachment",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(file)
|
|
||||||
}
|
|
||||||
Err(Error::NotFound) => Err(Error::UnknownAttachment),
|
|
||||||
Err(error) => Err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
|
||||||
self.insert_one(COL, attachment).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"reported": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "attachment",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"deleted": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "attachment",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_many(
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"deleted": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update",
|
|
||||||
with: "attachments",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
use bson::Document;
|
|
||||||
|
|
||||||
use crate::models::Emoji;
|
|
||||||
use crate::{AbstractEmoji, Error, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "emojis";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractEmoji for MongoDb {
|
|
||||||
/// Fetch an emoji by its id
|
|
||||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
|
||||||
self.find_one_by_id(COL, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch emoji by their ids
|
|
||||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"parent.id": parent_id
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch emoji by their parent ids
|
|
||||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"parent.id": {
|
|
||||||
"$in": parent_ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert emoji into database.
|
|
||||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
|
|
||||||
self.insert_one(COL, emoji).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete an emoji by its id
|
|
||||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": &emoji.id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"parent": {
|
|
||||||
"type": "Detached"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "emojis",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use bson::{to_document, Document};
|
|
||||||
use futures::StreamExt;
|
|
||||||
use mongodb::{
|
|
||||||
options::{FindOneOptions, FindOptions},
|
|
||||||
results::{DeleteResult, InsertOneResult, UpdateResult},
|
|
||||||
};
|
|
||||||
use rocket::serde::DeserializeOwned;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result};
|
|
||||||
|
|
||||||
pub mod admin {
|
|
||||||
pub mod stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod media {
|
|
||||||
pub mod attachment;
|
|
||||||
pub mod emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod channels {
|
|
||||||
pub mod channel;
|
|
||||||
pub mod channel_invite;
|
|
||||||
pub mod channel_unread;
|
|
||||||
pub mod message;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod servers {
|
|
||||||
pub mod server;
|
|
||||||
pub mod server_ban;
|
|
||||||
pub mod server_member;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod users {
|
|
||||||
pub mod user;
|
|
||||||
pub mod user_settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod safety {
|
|
||||||
pub mod report;
|
|
||||||
pub mod snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MongoDb(pub mongodb::Client);
|
|
||||||
|
|
||||||
impl AbstractDatabase for MongoDb {}
|
|
||||||
|
|
||||||
impl Deref for MongoDb {
|
|
||||||
type Target = mongodb::Client;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MongoDb {
|
|
||||||
pub fn db(&self) -> mongodb::Database {
|
|
||||||
self.database("revolt")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
|
|
||||||
self.db().collection(collection)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_one<T: Serialize>(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
document: T,
|
|
||||||
) -> Result<InsertOneResult> {
|
|
||||||
self.col::<T>(collection)
|
|
||||||
.insert_one(document, None)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "insert_one",
|
|
||||||
with: collection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
projection: Document,
|
|
||||||
options: O,
|
|
||||||
) -> Result<Vec<T>>
|
|
||||||
where
|
|
||||||
O: Into<Option<FindOptions>>,
|
|
||||||
{
|
|
||||||
let result = self.col::<T>(collection).find(projection, options).await;
|
|
||||||
Ok(if cfg!(debug_assertions) {
|
|
||||||
result.unwrap()
|
|
||||||
} else {
|
|
||||||
result.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "find",
|
|
||||||
with: collection,
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
.filter_map(|s| async {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
// Hard fail on invalid documents
|
|
||||||
Some(s.unwrap())
|
|
||||||
} else {
|
|
||||||
s.ok()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<T>>()
|
|
||||||
.await)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
projection: Document,
|
|
||||||
) -> Result<Vec<T>> {
|
|
||||||
self.find_with_options(collection, projection, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_one_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
projection: Document,
|
|
||||||
options: O,
|
|
||||||
) -> Result<T>
|
|
||||||
where
|
|
||||||
O: Into<Option<FindOneOptions>>,
|
|
||||||
{
|
|
||||||
self.col::<T>(collection)
|
|
||||||
.find_one(projection, options)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "find_one",
|
|
||||||
with: collection,
|
|
||||||
})?
|
|
||||||
.ok_or(Error::NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
projection: Document,
|
|
||||||
) -> Result<T> {
|
|
||||||
self.find_one_with_options(collection, projection, None)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_one_by_id<T: DeserializeOwned + Unpin + Send + Sync>(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
id: &str,
|
|
||||||
) -> Result<T> {
|
|
||||||
self.find_one(
|
|
||||||
collection,
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_one<P, T: Serialize>(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
projection: Document,
|
|
||||||
partial: T,
|
|
||||||
remove: Vec<&dyn IntoDocumentPath>,
|
|
||||||
prefix: P,
|
|
||||||
) -> Result<UpdateResult>
|
|
||||||
where
|
|
||||||
P: Into<Option<String>>,
|
|
||||||
{
|
|
||||||
let prefix = prefix.into();
|
|
||||||
|
|
||||||
let mut unset = doc! {};
|
|
||||||
for field in remove {
|
|
||||||
if let Some(path) = field.as_path() {
|
|
||||||
if let Some(prefix) = &prefix {
|
|
||||||
unset.insert(prefix.to_owned() + path, 1_i32);
|
|
||||||
} else {
|
|
||||||
unset.insert(path, 1_i32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = doc! {
|
|
||||||
"$unset": unset,
|
|
||||||
"$set": if let Some(prefix) = &prefix {
|
|
||||||
to_document(&prefix_keys(&partial, prefix))
|
|
||||||
} else {
|
|
||||||
to_document(&partial)
|
|
||||||
}.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "to_document",
|
|
||||||
with: collection
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
|
|
||||||
self.col::<Document>(collection)
|
|
||||||
.update_one(projection, query, None)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: collection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_one_by_id<P, T: Serialize>(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
id: &str,
|
|
||||||
partial: T,
|
|
||||||
remove: Vec<&dyn IntoDocumentPath>,
|
|
||||||
prefix: P,
|
|
||||||
) -> Result<UpdateResult>
|
|
||||||
where
|
|
||||||
P: Into<Option<String>>,
|
|
||||||
{
|
|
||||||
self.update_one(
|
|
||||||
collection,
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
partial,
|
|
||||||
remove,
|
|
||||||
prefix,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_one(
|
|
||||||
&self,
|
|
||||||
collection: &'static str,
|
|
||||||
projection: Document,
|
|
||||||
) -> Result<DeleteResult> {
|
|
||||||
self.col::<Document>(collection)
|
|
||||||
.delete_one(projection, None)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_one",
|
|
||||||
with: collection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_one_by_id(&self, collection: &'static str, id: &str) -> Result<DeleteResult> {
|
|
||||||
self.delete_one(
|
|
||||||
collection,
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct DocumentId {
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoDocumentPath: Send + Sync {
|
|
||||||
fn as_path(&self) -> Option<&'static str>;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
use crate::models::report::PartialReport;
|
|
||||||
use crate::models::Report;
|
|
||||||
use crate::{AbstractReport, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "safety_reports";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractReport for MongoDb {
|
|
||||||
async fn insert_report(&self, report: &Report) -> Result<()> {
|
|
||||||
self.insert_one(COL, report).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_report(&self, id: &str, report: &PartialReport) -> Result<()> {
|
|
||||||
self.update_one_by_id(COL, id, report, vec![], None)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_report(&self, report_id: &str) -> Result<Report> {
|
|
||||||
self.find_one_by_id(COL, report_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_reports(&self) -> Result<Vec<Report>> {
|
|
||||||
self.find(COL, doc! {}).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
use crate::models::Snapshot;
|
|
||||||
use crate::{AbstractSnapshot, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "safety_snapshots";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractSnapshot for MongoDb {
|
|
||||||
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
|
|
||||||
self.insert_one(COL, snapshot).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"report_id": report_id
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
use bson::{to_document, Bson, Document};
|
|
||||||
|
|
||||||
use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
|
||||||
use crate::r#impl::mongo::IntoDocumentPath;
|
|
||||||
use crate::{AbstractServer, Database, Error, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "servers";
|
|
||||||
|
|
||||||
impl MongoDb {
|
|
||||||
pub async fn delete_associated_server_objects(&self, server: &Server) -> Result<()> {
|
|
||||||
// Check if there are any attachments we need to delete.
|
|
||||||
self.delete_bulk_messages(doc! {
|
|
||||||
"channel": {
|
|
||||||
"$in": &server.channels
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Delete all emoji.
|
|
||||||
self.col::<Document>("emojis")
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"parent.id": &server.id
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_many",
|
|
||||||
with: "emojis",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Delete all channels.
|
|
||||||
self.col::<Document>("channels")
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"server": &server.id
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_many",
|
|
||||||
with: "channels",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Delete any associated objects, e.g. unreads and invites.
|
|
||||||
self.delete_associated_channel_objects(Bson::Document(doc! { "$in": &server.channels }))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Delete members and bans.
|
|
||||||
for with in &["server_members", "server_bans"] {
|
|
||||||
self.col::<Document>(with)
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"_id.server": &server.id
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "delete_many",
|
|
||||||
with,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update many attachments with parent id.
|
|
||||||
self.delete_many_attachments(doc! {
|
|
||||||
"object_id": &server.id
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractServer for MongoDb {
|
|
||||||
async fn fetch_server(&self, id: &str) -> Result<Server> {
|
|
||||||
self.find_one_by_id(COL, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_server(&self, server: &Server) -> Result<()> {
|
|
||||||
self.insert_one(COL, server).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_server(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
server: &PartialServer,
|
|
||||||
remove: Vec<FieldsServer>,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.update_one_by_id(
|
|
||||||
COL,
|
|
||||||
id,
|
|
||||||
server,
|
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_server(&self, server: &Server) -> Result<()> {
|
|
||||||
self.delete_associated_server_objects(server).await?;
|
|
||||||
self.delete_one_by_id(COL, &server.id).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
|
|
||||||
self.col::<Database>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": server_id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"roles.".to_owned() + role_id: to_document(role)
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "to_document",
|
|
||||||
with: "role"
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "server",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_role(
|
|
||||||
&self,
|
|
||||||
server_id: &str,
|
|
||||||
role_id: &str,
|
|
||||||
role: &PartialRole,
|
|
||||||
remove: Vec<FieldsRole>,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.update_one_by_id(
|
|
||||||
COL,
|
|
||||||
server_id,
|
|
||||||
role,
|
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
|
||||||
"roles.".to_owned() + role_id + ".",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
|
|
||||||
self.col::<Document>("server_members")
|
|
||||||
.update_many(
|
|
||||||
doc! {
|
|
||||||
"_id.server": server_id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$pull": {
|
|
||||||
"roles": &role_id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_many",
|
|
||||||
with: "server_members",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.col::<Document>("channels")
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"server": server_id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$unset": {
|
|
||||||
"role_permissions.".to_owned() + role_id: 1_i32
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "channels",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.col::<Document>("servers")
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": server_id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$unset": {
|
|
||||||
"roles.".to_owned() + role_id: 1_i32
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "servers",
|
|
||||||
})
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsServer {
|
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
|
||||||
Some(match self {
|
|
||||||
FieldsServer::Banner => "banner",
|
|
||||||
FieldsServer::Categories => "categories",
|
|
||||||
FieldsServer::Description => "description",
|
|
||||||
FieldsServer::Icon => "icon",
|
|
||||||
FieldsServer::SystemMessages => "system_messages",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsRole {
|
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
|
||||||
Some(match self {
|
|
||||||
FieldsRole::Colour => "colour",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
use crate::models::server_member::MemberCompositeKey;
|
|
||||||
use crate::models::ServerBan;
|
|
||||||
use crate::{AbstractServerBan, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "server_bans";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractServerBan for MongoDb {
|
|
||||||
async fn fetch_ban(&self, server: &str, user: &str) -> Result<ServerBan> {
|
|
||||||
self.find_one(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": server,
|
|
||||||
"_id.user": user
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_bans(&self, server: &str) -> Result<Vec<ServerBan>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": server
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
|
|
||||||
self.insert_one(COL, ban).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
|
|
||||||
self.delete_one(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": &id.server,
|
|
||||||
"_id.user": &id.user
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
use bson::Document;
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
use crate::models::server_member::MemberWithRoles;
|
|
||||||
use crate::models::server_member::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
|
||||||
use crate::r#impl::mongo::IntoDocumentPath;
|
|
||||||
use crate::{AbstractServer, AbstractServerMember, Error, Result};
|
|
||||||
|
|
||||||
static COL: &str = "server_members";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractServerMember for MongoDb {
|
|
||||||
async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> {
|
|
||||||
self.find_one(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": server,
|
|
||||||
"_id.user": user
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_member_with_roles(&self, server: &str, user: &str) -> Result<MemberWithRoles> {
|
|
||||||
let member = self.fetch_member(server, user).await?;
|
|
||||||
let server_roles = self.fetch_server(server).await?.roles;
|
|
||||||
let roles = member
|
|
||||||
.roles
|
|
||||||
.iter()
|
|
||||||
.filter_map(|id| server_roles.get(id).map(|r| (id.clone(), r.clone())))
|
|
||||||
.collect();
|
|
||||||
Ok(MemberWithRoles { member, roles })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
|
||||||
self.insert_one(COL, member).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_member(
|
|
||||||
&self,
|
|
||||||
id: &MemberCompositeKey,
|
|
||||||
member: &PartialMember,
|
|
||||||
remove: Vec<FieldsMember>,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.update_one(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": &id.server,
|
|
||||||
"_id.user": &id.user
|
|
||||||
},
|
|
||||||
member,
|
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
|
||||||
self.delete_one(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": &id.server,
|
|
||||||
"_id.user": &id.user
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_all_members<'a>(&self, server: &str) -> Result<Vec<Member>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": server
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_all_memberships<'a>(&self, user: &str) -> Result<Vec<Member>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.user": user
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_members<'a>(&self, server: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
|
||||||
self.find(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": server,
|
|
||||||
"_id.user": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_member_count(&self, server: &str) -> Result<usize> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.count_documents(
|
|
||||||
doc! {
|
|
||||||
"_id.server": server
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|c| c as usize)
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "count_documents",
|
|
||||||
with: "server_members",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_server_count(&self, user: &str) -> Result<usize> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.count_documents(
|
|
||||||
doc! {
|
|
||||||
"_id.user": user
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|c| c as usize)
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "count_documents",
|
|
||||||
with: "server_members",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsMember {
|
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
|
||||||
Some(match self {
|
|
||||||
FieldsMember::Avatar => "avatar",
|
|
||||||
FieldsMember::Nickname => "nickname",
|
|
||||||
FieldsMember::Roles => "roles",
|
|
||||||
FieldsMember::Timeout => "timeout",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
use bson::Document;
|
|
||||||
use futures::StreamExt;
|
|
||||||
use mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus, User};
|
|
||||||
use crate::r#impl::mongo::IntoDocumentPath;
|
|
||||||
use crate::{AbstractUser, Error, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static FIND_USERNAME_OPTIONS: Lazy<FindOneOptions> = Lazy::new(|| {
|
|
||||||
FindOneOptions::builder()
|
|
||||||
.collation(
|
|
||||||
Collation::builder()
|
|
||||||
.locale("en")
|
|
||||||
.strength(CollationStrength::Secondary)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
});
|
|
||||||
|
|
||||||
static COL: &str = "users";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractUser for MongoDb {
|
|
||||||
async fn fetch_user(&self, id: &str) -> Result<User> {
|
|
||||||
self.find_one_by_id(COL, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
|
|
||||||
self.find_one_with_options(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"username": username,
|
|
||||||
"discriminator": discriminator
|
|
||||||
},
|
|
||||||
FIND_USERNAME_OPTIONS.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
|
|
||||||
let session = self
|
|
||||||
.col::<Document>("sessions")
|
|
||||||
.find_one(
|
|
||||||
doc! {
|
|
||||||
"token": token
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "find_one",
|
|
||||||
with: "sessions",
|
|
||||||
})?
|
|
||||||
.ok_or(Error::InvalidSession)?;
|
|
||||||
|
|
||||||
self.fetch_user(session.get_str("user_id").unwrap()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_user(&self, user: &User) -> Result<()> {
|
|
||||||
self.insert_one(COL, user).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_user(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
user: &PartialUser,
|
|
||||||
remove: Vec<FieldsUser>,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.update_one_by_id(
|
|
||||||
COL,
|
|
||||||
id,
|
|
||||||
user,
|
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_user(&self, id: &str) -> Result<()> {
|
|
||||||
self.delete_one_by_id(COL, id).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
|
|
||||||
let mut cursor = self
|
|
||||||
.col::<User>(COL)
|
|
||||||
.find(
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "find",
|
|
||||||
with: "users",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut users = vec![];
|
|
||||||
while let Some(Ok(user)) = cursor.next().await {
|
|
||||||
users.push(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(users)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
|
|
||||||
Ok(self
|
|
||||||
.col::<Document>(COL)
|
|
||||||
.find(
|
|
||||||
doc! {
|
|
||||||
"username": username
|
|
||||||
},
|
|
||||||
FindOptions::builder()
|
|
||||||
.collation(
|
|
||||||
Collation::builder()
|
|
||||||
.locale("en")
|
|
||||||
.strength(CollationStrength::Secondary)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.projection(doc! { "_id": 0, "discriminator": 1 })
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "find",
|
|
||||||
with: "users",
|
|
||||||
})?
|
|
||||||
.filter_map(|s| async { s.ok() })
|
|
||||||
.collect::<Vec<Document>>()
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|x| x.get_str("discriminator").ok().map(|x| x.to_string()))
|
|
||||||
.collect::<Vec<String>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
|
|
||||||
Ok(self
|
|
||||||
.col::<Document>(COL)
|
|
||||||
.find(
|
|
||||||
doc! {
|
|
||||||
"$and": [
|
|
||||||
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
|
|
||||||
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "find",
|
|
||||||
with: "users",
|
|
||||||
})?
|
|
||||||
.filter_map(|s| async { s.ok() })
|
|
||||||
.collect::<Vec<Document>>()
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
|
|
||||||
.collect::<Vec<String>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
|
|
||||||
Ok(self
|
|
||||||
.col::<Document>("channels")
|
|
||||||
.find(
|
|
||||||
doc! {
|
|
||||||
"channel_type": {
|
|
||||||
"$in": ["Group", "DirectMessage"]
|
|
||||||
},
|
|
||||||
"recipients": {
|
|
||||||
"$all": [ user_a, user_b ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "find",
|
|
||||||
with: "channels",
|
|
||||||
})?
|
|
||||||
.filter_map(|s| async { s.ok() })
|
|
||||||
.collect::<Vec<Document>>()
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
|
|
||||||
.collect::<Vec<String>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
|
|
||||||
Ok(self
|
|
||||||
.col::<Document>("server_members")
|
|
||||||
.aggregate(
|
|
||||||
vec![
|
|
||||||
doc! {
|
|
||||||
"$match": {
|
|
||||||
"_id.user": user_a
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$lookup": {
|
|
||||||
"from": "server_members",
|
|
||||||
"as": "members",
|
|
||||||
"let": {
|
|
||||||
"server": "$_id.server"
|
|
||||||
},
|
|
||||||
"pipeline": [
|
|
||||||
{
|
|
||||||
"$match": {
|
|
||||||
"$expr": {
|
|
||||||
"$and": [
|
|
||||||
{ "$eq": [ "$_id.user", user_b ] },
|
|
||||||
{ "$eq": [ "$_id.server", "$$server" ] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$match": {
|
|
||||||
"members": {
|
|
||||||
"$size": 1_i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$project": {
|
|
||||||
"_id": "$_id.server"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "aggregate",
|
|
||||||
with: "server_members",
|
|
||||||
})?
|
|
||||||
.filter_map(|s| async { s.ok() })
|
|
||||||
.collect::<Vec<Document>>()
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|x| x.get_str("_id").ok().map(|i| i.to_string()))
|
|
||||||
.collect::<Vec<String>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_relationship(
|
|
||||||
&self,
|
|
||||||
user_id: &str,
|
|
||||||
target_id: &str,
|
|
||||||
relationship: &RelationshipStatus,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let RelationshipStatus::None = relationship {
|
|
||||||
return self.pull_relationship(user_id, target_id).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": user_id
|
|
||||||
},
|
|
||||||
vec![doc! {
|
|
||||||
"$set": {
|
|
||||||
"relations": {
|
|
||||||
"$concatArrays": [
|
|
||||||
{
|
|
||||||
"$ifNull": [
|
|
||||||
{
|
|
||||||
"$filter": {
|
|
||||||
"input": "$relations",
|
|
||||||
"cond": {
|
|
||||||
"$ne": [
|
|
||||||
"$$this._id",
|
|
||||||
target_id
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"_id": target_id,
|
|
||||||
"status": format!("{relationship:?}")
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "user",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": user_id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$pull": {
|
|
||||||
"relations": {
|
|
||||||
"_id": target_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "user",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsUser {
|
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
|
||||||
Some(match self {
|
|
||||||
FieldsUser::Avatar => "avatar",
|
|
||||||
FieldsUser::ProfileBackground => "profile.background",
|
|
||||||
FieldsUser::ProfileContent => "profile.content",
|
|
||||||
FieldsUser::StatusPresence => "status.presence",
|
|
||||||
FieldsUser::StatusText => "status.text",
|
|
||||||
FieldsUser::DisplayName => "display_name",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
use bson::{to_bson, Document};
|
|
||||||
use mongodb::options::{FindOneOptions, UpdateOptions};
|
|
||||||
|
|
||||||
use crate::models::UserSettings;
|
|
||||||
use crate::{AbstractUserSettings, Error, Result};
|
|
||||||
|
|
||||||
use super::super::MongoDb;
|
|
||||||
|
|
||||||
static COL: &str = "user_settings";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractUserSettings for MongoDb {
|
|
||||||
async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result<UserSettings> {
|
|
||||||
let mut projection = doc! {
|
|
||||||
"_id": 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
for key in filter {
|
|
||||||
projection.insert(key, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.find_one_with_options(
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
FindOneOptions::builder().projection(projection).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()> {
|
|
||||||
let mut set = doc! {};
|
|
||||||
for (key, data) in settings {
|
|
||||||
set.insert(
|
|
||||||
key,
|
|
||||||
vec![to_bson(&data.0).unwrap(), to_bson(&data.1).unwrap()],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": set
|
|
||||||
},
|
|
||||||
UpdateOptions::builder().upsert(true).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "update_one",
|
|
||||||
with: "user_settings",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_user_settings(&self, id: &str) -> Result<()> {
|
|
||||||
self.delete_one_by_id(COL, id).await.map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
use authifier::models::Session;
|
|
||||||
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
|
|
||||||
use revolt_rocket_okapi::gen::OpenApiGenerator;
|
|
||||||
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
|
|
||||||
|
|
||||||
use rocket::http::Status;
|
|
||||||
use rocket::request::{self, FromRequest, Outcome, Request};
|
|
||||||
|
|
||||||
use crate::models::user::UserHint;
|
|
||||||
use crate::models::User;
|
|
||||||
use crate::Database;
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for User {
|
|
||||||
type Error = authifier::Error;
|
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
||||||
let user: &Option<User> = request
|
|
||||||
.local_cache_async(async {
|
|
||||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
|
||||||
|
|
||||||
let header_bot_token = request
|
|
||||||
.headers()
|
|
||||||
.get("x-bot-token")
|
|
||||||
.next()
|
|
||||||
.map(|x| x.to_string());
|
|
||||||
|
|
||||||
if let Some(bot_token) = header_bot_token {
|
|
||||||
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await {
|
|
||||||
return Some(user);
|
|
||||||
}
|
|
||||||
} else if let Outcome::Success(session) = request.guard::<Session>().await {
|
|
||||||
// This uses a guard so can't really easily be refactored into from_token at this stage.
|
|
||||||
if let Ok(user) = db.fetch_user(&session.user_id).await {
|
|
||||||
return Some(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Some(user) = user {
|
|
||||||
Outcome::Success(user.clone())
|
|
||||||
} else {
|
|
||||||
Outcome::Failure((Status::Unauthorized, authifier::Error::InvalidSession))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'r> OpenApiFromRequest<'r> for User {
|
|
||||||
fn from_request_input(
|
|
||||||
_gen: &mut OpenApiGenerator,
|
|
||||||
_name: String,
|
|
||||||
_required: bool,
|
|
||||||
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
|
||||||
let mut requirements = schemars::Map::new();
|
|
||||||
requirements.insert("Session Token".to_owned(), vec![]);
|
|
||||||
|
|
||||||
Ok(RequestHeaderInput::Security(
|
|
||||||
"Session Token".to_owned(),
|
|
||||||
SecurityScheme {
|
|
||||||
data: SecuritySchemeData::ApiKey {
|
|
||||||
name: "x-session-token".to_owned(),
|
|
||||||
location: "header".to_owned(),
|
|
||||||
},
|
|
||||||
description: Some("Used to authenticate as a user.".to_owned()),
|
|
||||||
extensions: schemars::Map::new(),
|
|
||||||
},
|
|
||||||
requirements,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#[macro_use]
|
|
||||||
extern crate async_trait;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate schemars;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate async_recursion;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate log;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate impl_ops;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate revolt_optional_struct;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate bitfield;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate bson;
|
|
||||||
|
|
||||||
pub use authifier;
|
|
||||||
pub use iso8601_timestamp::Timestamp;
|
|
||||||
pub use redis_kiss;
|
|
||||||
|
|
||||||
pub mod events;
|
|
||||||
pub mod r#impl;
|
|
||||||
pub mod models;
|
|
||||||
pub mod tasks;
|
|
||||||
pub mod types;
|
|
||||||
pub mod util;
|
|
||||||
|
|
||||||
#[cfg(feature = "rocket_impl")]
|
|
||||||
pub mod web;
|
|
||||||
|
|
||||||
mod database;
|
|
||||||
mod permissions;
|
|
||||||
mod traits;
|
|
||||||
|
|
||||||
pub use database::*;
|
|
||||||
pub use traits::*;
|
|
||||||
|
|
||||||
pub use permissions::defn::*;
|
|
||||||
pub use permissions::{get_relationship, perms};
|
|
||||||
|
|
||||||
pub use util::{
|
|
||||||
r#ref::Ref,
|
|
||||||
result::{Error, Result},
|
|
||||||
variables,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "rocket_impl")]
|
|
||||||
pub use web::{Db, EmptyResponse};
|
|
||||||
|
|
||||||
/// Resolve asset
|
|
||||||
macro_rules! asset {
|
|
||||||
($path:literal) => {
|
|
||||||
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $path)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) use asset;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Simple database model for testing
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct SimpleModel {
|
|
||||||
pub number: i32,
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use iso8601_timestamp::Timestamp;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Index access information
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
pub struct IndexAccess {
|
|
||||||
/// Operations since timestamp
|
|
||||||
ops: i32,
|
|
||||||
|
|
||||||
/// Timestamp at which data keeping begun
|
|
||||||
since: Timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection index
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
pub struct Index {
|
|
||||||
/// Index name
|
|
||||||
name: String,
|
|
||||||
|
|
||||||
/// Access information
|
|
||||||
accesses: IndexAccess,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Histogram entry
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
pub struct LatencyHistogramEntry {
|
|
||||||
/// Time
|
|
||||||
micros: i64,
|
|
||||||
|
|
||||||
/// Count
|
|
||||||
count: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection latency stats
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
pub struct LatencyStats {
|
|
||||||
/// Total operations
|
|
||||||
ops: i64,
|
|
||||||
|
|
||||||
/// Timestamp at which data keeping begun
|
|
||||||
latency: i64,
|
|
||||||
|
|
||||||
/// Histogram representation of latency data
|
|
||||||
histogram: Vec<LatencyHistogramEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection storage stats
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct StorageStats {
|
|
||||||
/// Uncompressed data size
|
|
||||||
size: i64,
|
|
||||||
|
|
||||||
/// Data size on disk
|
|
||||||
storage_size: i64,
|
|
||||||
|
|
||||||
/// Total size of all indexes
|
|
||||||
total_index_size: i64,
|
|
||||||
|
|
||||||
/// Sum of storage size and total index size
|
|
||||||
total_size: i64,
|
|
||||||
|
|
||||||
/// Individual index sizes
|
|
||||||
index_sizes: HashMap<String, i64>,
|
|
||||||
|
|
||||||
/// Number of documents in collection
|
|
||||||
count: i64,
|
|
||||||
|
|
||||||
/// Average size of each document
|
|
||||||
avg_obj_size: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Query collection scan stats
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CollectionScans {
|
|
||||||
/// Number of total collection scans
|
|
||||||
total: i64,
|
|
||||||
|
|
||||||
/// Number of total collection scans not using a tailable cursor
|
|
||||||
non_tailable: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection query execution stats
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct QueryExecStats {
|
|
||||||
/// Stats regarding collection scans
|
|
||||||
collection_scans: CollectionScans,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection stats
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CollectionStats {
|
|
||||||
/// Namespace
|
|
||||||
ns: String,
|
|
||||||
|
|
||||||
/// Local time
|
|
||||||
local_time: Timestamp,
|
|
||||||
|
|
||||||
/// Latency stats
|
|
||||||
latency_stats: HashMap<String, LatencyStats>,
|
|
||||||
|
|
||||||
/// Query exec stats
|
|
||||||
query_exec_stats: QueryExecStats,
|
|
||||||
|
|
||||||
/// Number of documents in collection
|
|
||||||
count: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Server Stats
|
|
||||||
#[derive(Serialize, JsonSchema, Debug)]
|
|
||||||
pub struct Stats {
|
|
||||||
/// Index usage information
|
|
||||||
pub indices: HashMap<String, Vec<Index>>,
|
|
||||||
|
|
||||||
/// Collection stats
|
|
||||||
pub coll_stats: HashMap<String, CollectionStats>,
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{models::attachment::File, OverrideField};
|
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
|
||||||
pub fn if_false(t: &bool) -> bool {
|
|
||||||
!t
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a channel on Revolt
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
#[serde(tag = "channel_type")]
|
|
||||||
pub enum Channel {
|
|
||||||
/// Personal "Saved Notes" channel which allows users to save messages
|
|
||||||
SavedMessages {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
/// Id of the user this channel belongs to
|
|
||||||
user: String,
|
|
||||||
},
|
|
||||||
/// Direct message channel between two users
|
|
||||||
DirectMessage {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
|
|
||||||
/// Whether this direct message channel is currently open on both sides
|
|
||||||
active: bool,
|
|
||||||
/// 2-tuple of user ids participating in direct message
|
|
||||||
recipients: Vec<String>,
|
|
||||||
/// Id of the last message sent in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
last_message_id: Option<String>,
|
|
||||||
},
|
|
||||||
/// Group channel between 1 or more participants
|
|
||||||
Group {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
|
|
||||||
/// Display name of the channel
|
|
||||||
name: String,
|
|
||||||
/// User id of the owner of the group
|
|
||||||
owner: String,
|
|
||||||
/// Channel description
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
description: Option<String>,
|
|
||||||
/// Array of user ids participating in channel
|
|
||||||
recipients: Vec<String>,
|
|
||||||
|
|
||||||
/// Custom icon attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
icon: Option<File>,
|
|
||||||
/// Id of the last message sent in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
last_message_id: Option<String>,
|
|
||||||
|
|
||||||
/// Permissions assigned to members of this group
|
|
||||||
/// (does not apply to the owner of the group)
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
permissions: Option<i64>,
|
|
||||||
|
|
||||||
/// Whether this group is marked as not safe for work
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
nsfw: bool,
|
|
||||||
},
|
|
||||||
/// Text channel belonging to a server
|
|
||||||
TextChannel {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
/// Id of the server this channel belongs to
|
|
||||||
server: String,
|
|
||||||
|
|
||||||
/// Display name of the channel
|
|
||||||
name: String,
|
|
||||||
/// Channel description
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
description: Option<String>,
|
|
||||||
|
|
||||||
/// Custom icon attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
icon: Option<File>,
|
|
||||||
/// Id of the last message sent in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
last_message_id: Option<String>,
|
|
||||||
|
|
||||||
/// Default permissions assigned to users in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
default_permissions: Option<OverrideField>,
|
|
||||||
/// Permissions assigned based on role to this channel
|
|
||||||
#[serde(
|
|
||||||
default = "HashMap::<String, OverrideField>::new",
|
|
||||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
|
||||||
)]
|
|
||||||
role_permissions: HashMap<String, OverrideField>,
|
|
||||||
|
|
||||||
/// Whether this channel is marked as not safe for work
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
nsfw: bool,
|
|
||||||
},
|
|
||||||
/// Voice channel belonging to a server
|
|
||||||
VoiceChannel {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
/// Id of the server this channel belongs to
|
|
||||||
server: String,
|
|
||||||
|
|
||||||
/// Display name of the channel
|
|
||||||
name: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
/// Channel description
|
|
||||||
description: Option<String>,
|
|
||||||
/// Custom icon attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
icon: Option<File>,
|
|
||||||
|
|
||||||
/// Default permissions assigned to users in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
default_permissions: Option<OverrideField>,
|
|
||||||
/// Permissions assigned based on role to this channel
|
|
||||||
#[serde(
|
|
||||||
default = "HashMap::<String, OverrideField>::new",
|
|
||||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
|
||||||
)]
|
|
||||||
role_permissions: HashMap<String, OverrideField>,
|
|
||||||
|
|
||||||
/// Whether this channel is marked as not safe for work
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
nsfw: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Partial values of [Channel]
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
|
|
||||||
pub struct PartialChannel {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub name: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub owner: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub icon: Option<File>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nsfw: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub active: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub permissions: Option<i64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub role_permissions: Option<HashMap<String, OverrideField>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub default_permissions: Option<OverrideField>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub last_message_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional fields on channel object
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum FieldsChannel {
|
|
||||||
Description,
|
|
||||||
Icon,
|
|
||||||
DefaultPermissions,
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Representation of an invite to a channel on Revolt
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum Invite {
|
|
||||||
/// Invite to a specific server channel
|
|
||||||
Server {
|
|
||||||
/// Invite code
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
code: String,
|
|
||||||
/// Id of the server this invite points to
|
|
||||||
server: String,
|
|
||||||
/// Id of user who created this invite
|
|
||||||
creator: String,
|
|
||||||
/// Id of the server channel this invite points to
|
|
||||||
channel: String,
|
|
||||||
},
|
|
||||||
/// Invite to a group channel
|
|
||||||
Group {
|
|
||||||
/// Invite code
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
code: String,
|
|
||||||
/// Id of user who created this invite
|
|
||||||
creator: String,
|
|
||||||
/// Id of the group channel this invite points to
|
|
||||||
channel: String,
|
|
||||||
}, /* User {
|
|
||||||
code: String,
|
|
||||||
user: String
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Composite primary key consisting of channel and user id
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct ChannelCompositeKey {
|
|
||||||
/// Channel Id
|
|
||||||
pub channel: String,
|
|
||||||
/// User Id
|
|
||||||
pub user: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of the state of a channel from the perspective of a user
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct ChannelUnread {
|
|
||||||
/// Composite key pointing to a user's view of a channel
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: ChannelCompositeKey,
|
|
||||||
|
|
||||||
/// Id of the last message read in this channel by a user
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub last_id: Option<String>,
|
|
||||||
/// Array of message ids that mention the user
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub mentions: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
use crate::util::regex::RE_COLOUR;
|
|
||||||
|
|
||||||
use indexmap::{IndexMap, IndexSet};
|
|
||||||
use iso8601_timestamp::Timestamp;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
|
||||||
use revolt_models::v0::MessageWebhook;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use validator::Validate;
|
|
||||||
|
|
||||||
#[cfg(feature = "rocket_impl")]
|
|
||||||
use rocket::FromFormField;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
models::{attachment::File, Member, User},
|
|
||||||
types::january::Embed,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
|
||||||
pub fn if_false(t: &bool) -> bool {
|
|
||||||
!t
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static RE_MENTION: Lazy<Regex> =
|
|
||||||
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
|
|
||||||
|
|
||||||
/// # Reply
|
|
||||||
///
|
|
||||||
/// Representation of a message reply before it is sent.
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)]
|
|
||||||
pub struct Reply {
|
|
||||||
/// Message Id
|
|
||||||
pub id: String,
|
|
||||||
/// Whether this reply should mention the message's author
|
|
||||||
pub mention: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a text embed before it is sent.
|
|
||||||
#[derive(Validate, Serialize, Deserialize, JsonSchema, Clone, Debug, Default)]
|
|
||||||
pub struct SendableEmbed {
|
|
||||||
#[validate(length(min = 1, max = 128))]
|
|
||||||
pub icon_url: Option<String>,
|
|
||||||
#[validate(length(min = 1, max = 256))]
|
|
||||||
pub url: Option<String>,
|
|
||||||
#[validate(length(min = 1, max = 100))]
|
|
||||||
pub title: Option<String>,
|
|
||||||
#[validate(length(min = 1, max = 2000))]
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub media: Option<String>,
|
|
||||||
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
|
|
||||||
pub colour: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a system event message
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum SystemMessage {
|
|
||||||
#[serde(rename = "text")]
|
|
||||||
Text { content: String },
|
|
||||||
#[serde(rename = "user_added")]
|
|
||||||
UserAdded { id: String, by: String },
|
|
||||||
#[serde(rename = "user_remove")]
|
|
||||||
UserRemove { id: String, by: String },
|
|
||||||
#[serde(rename = "user_joined")]
|
|
||||||
UserJoined { id: String },
|
|
||||||
#[serde(rename = "user_left")]
|
|
||||||
UserLeft { id: String },
|
|
||||||
#[serde(rename = "user_kicked")]
|
|
||||||
UserKicked { id: String },
|
|
||||||
#[serde(rename = "user_banned")]
|
|
||||||
UserBanned { id: String },
|
|
||||||
#[serde(rename = "channel_renamed")]
|
|
||||||
ChannelRenamed { name: String, by: String },
|
|
||||||
#[serde(rename = "channel_description_changed")]
|
|
||||||
ChannelDescriptionChanged { by: String },
|
|
||||||
#[serde(rename = "channel_icon_changed")]
|
|
||||||
ChannelIconChanged { by: String },
|
|
||||||
#[serde(rename = "channel_ownership_changed")]
|
|
||||||
ChannelOwnershipChanged { from: String, to: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Name and / or avatar override information
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Validate)]
|
|
||||||
pub struct Masquerade {
|
|
||||||
/// Replace the display name shown on this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[validate(length(min = 1, max = 32))]
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// Replace the avatar shown on this message (URL to image file)
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[validate(length(min = 1, max = 256))]
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
/// Replace the display role colour shown on this message
|
|
||||||
///
|
|
||||||
/// Must have `ManageRole` permission to use
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
|
|
||||||
pub colour: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information to guide interactions on this message
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Validate, Default)]
|
|
||||||
pub struct Interactions {
|
|
||||||
/// Reactions which should always appear and be distinct
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
|
||||||
pub reactions: Option<IndexSet<String>>,
|
|
||||||
/// Whether reactions should be restricted to the given list
|
|
||||||
///
|
|
||||||
/// Can only be set to true if reactions list is of at least length 1
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub restrict_reactions: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a Message on Revolt
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)]
|
|
||||||
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
|
|
||||||
#[optional_name = "PartialMessage"]
|
|
||||||
#[opt_skip_serializing_none]
|
|
||||||
#[opt_some_priority]
|
|
||||||
pub struct Message {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// Unique value generated by client sending this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nonce: Option<String>,
|
|
||||||
/// Id of the channel this message was sent in
|
|
||||||
pub channel: String,
|
|
||||||
/// Id of the user or webhook that sent this message
|
|
||||||
pub author: String,
|
|
||||||
/// The webhook that sent this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub webhook: Option<MessageWebhook>,
|
|
||||||
/// Message content
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub content: Option<String>,
|
|
||||||
/// System message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub system: Option<SystemMessage>,
|
|
||||||
/// Array of attachments
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub attachments: Option<Vec<File>>,
|
|
||||||
/// Time at which this message was last edited
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub edited: Option<Timestamp>,
|
|
||||||
/// Attached embeds to this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub embeds: Option<Vec<Embed>>,
|
|
||||||
/// Array of user ids mentioned in this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub mentions: Option<Vec<String>>,
|
|
||||||
/// Array of message ids this message is replying to
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub replies: Option<Vec<String>>,
|
|
||||||
/// Hashmap of emoji IDs to array of user IDs
|
|
||||||
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
|
|
||||||
pub reactions: IndexMap<String, IndexSet<String>>,
|
|
||||||
/// Information about how this message should be interacted with
|
|
||||||
#[serde(skip_serializing_if = "Interactions::is_default", default)]
|
|
||||||
pub interactions: Interactions,
|
|
||||||
/// Name and / or avatar overrides for this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub masquerade: Option<Masquerade>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Message Sort
|
|
||||||
///
|
|
||||||
/// Sort used for retrieving messages
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Default)]
|
|
||||||
#[cfg_attr(feature = "rocket_impl", derive(FromFormField))]
|
|
||||||
pub enum MessageSort {
|
|
||||||
/// Sort by the most relevant messages
|
|
||||||
#[default]
|
|
||||||
Relevance,
|
|
||||||
/// Sort by the newest messages first
|
|
||||||
Latest,
|
|
||||||
/// Sort by the oldest messages first
|
|
||||||
Oldest,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Message Time Period
|
|
||||||
///
|
|
||||||
/// Filter and sort messages by time
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum MessageTimePeriod {
|
|
||||||
Relative {
|
|
||||||
/// Message id to search around
|
|
||||||
///
|
|
||||||
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
|
|
||||||
/// It will also take half of limit rounded as the limits to each side.
|
|
||||||
/// It also fetches the message ID specified.
|
|
||||||
nearby: String,
|
|
||||||
},
|
|
||||||
Absolute {
|
|
||||||
/// Message id before which messages should be fetched
|
|
||||||
before: Option<String>,
|
|
||||||
/// Message id after which messages should be fetched
|
|
||||||
after: Option<String>,
|
|
||||||
/// Message sort direction
|
|
||||||
sort: Option<MessageSort>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Message Filter
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Default)]
|
|
||||||
pub struct MessageFilter {
|
|
||||||
/// Parent channel ID
|
|
||||||
pub channel: Option<String>,
|
|
||||||
/// Message author ID
|
|
||||||
pub author: Option<String>,
|
|
||||||
/// Search query
|
|
||||||
pub query: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Message Query
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct MessageQuery {
|
|
||||||
/// Maximum number of messages to fetch
|
|
||||||
///
|
|
||||||
/// For fetching nearby messages, this is \`(limit + 1)\`.
|
|
||||||
pub limit: Option<i64>,
|
|
||||||
/// Filter to apply
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub filter: MessageFilter,
|
|
||||||
/// Time period to fetch
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub time_period: MessageTimePeriod,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Bulk Message Response
|
|
||||||
///
|
|
||||||
/// Response used when multiple messages are fetched
|
|
||||||
#[derive(Serialize, JsonSchema)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum BulkMessageResponse {
|
|
||||||
JustMessages(
|
|
||||||
/// List of messages
|
|
||||||
Vec<Message>,
|
|
||||||
),
|
|
||||||
MessagesAndUsers {
|
|
||||||
/// List of messages
|
|
||||||
messages: Vec<Message>,
|
|
||||||
/// List of users
|
|
||||||
users: Vec<User>,
|
|
||||||
/// List of members
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
members: Option<Vec<Member>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Appended Information
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct AppendMessage {
|
|
||||||
/// Additional embeds to include in this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub embeds: Option<Vec<Embed>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct DataMessageSend {
|
|
||||||
/// Unique token to prevent duplicate message sending
|
|
||||||
///
|
|
||||||
/// **This is deprecated and replaced by `Idempotency-Key`!**
|
|
||||||
#[validate(length(min = 1, max = 64))]
|
|
||||||
pub nonce: Option<String>,
|
|
||||||
|
|
||||||
/// Message content to send
|
|
||||||
#[validate(length(min = 0, max = 2000))]
|
|
||||||
pub content: Option<String>,
|
|
||||||
/// Attachments to include in message
|
|
||||||
pub attachments: Option<Vec<String>>,
|
|
||||||
/// Messages to reply to
|
|
||||||
pub replies: Option<Vec<Reply>>,
|
|
||||||
/// Embeds to include in message
|
|
||||||
///
|
|
||||||
/// Text embed content contributes to the content length cap
|
|
||||||
#[validate]
|
|
||||||
pub embeds: Option<Vec<SendableEmbed>>,
|
|
||||||
/// Masquerade to apply to this message
|
|
||||||
#[validate]
|
|
||||||
pub masquerade: Option<Masquerade>,
|
|
||||||
/// Information about how this message should be interacted with
|
|
||||||
pub interactions: Option<Interactions>,
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Metadata associated with file
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum Metadata {
|
|
||||||
/// File is just a generic uncategorised file
|
|
||||||
#[default]
|
|
||||||
File,
|
|
||||||
/// File contains textual data and should be displayed as such
|
|
||||||
Text,
|
|
||||||
/// File is an image with specific dimensions
|
|
||||||
Image { width: isize, height: isize },
|
|
||||||
/// File is a video with specific dimensions
|
|
||||||
Video { width: isize, height: isize },
|
|
||||||
/// File is audio
|
|
||||||
Audio,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a File on Revolt
|
|
||||||
/// Generated by Autumn
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)]
|
|
||||||
pub struct File {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// Tag / bucket this file was uploaded to
|
|
||||||
pub tag: String,
|
|
||||||
/// Original filename
|
|
||||||
pub filename: String,
|
|
||||||
/// Parsed metadata of this file
|
|
||||||
pub metadata: Metadata,
|
|
||||||
/// Raw content type of this file
|
|
||||||
pub content_type: String,
|
|
||||||
/// Size of this file (in bytes)
|
|
||||||
pub size: isize,
|
|
||||||
|
|
||||||
/// Whether this file was deleted
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub deleted: Option<bool>,
|
|
||||||
/// Whether this file was reported
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub reported: Option<bool>,
|
|
||||||
|
|
||||||
// ! THE FOLLOWING SHOULD BE DEPRECATED
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub message_id: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user_id: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub server_id: Option<String>,
|
|
||||||
|
|
||||||
/// Id of the object this file is associated with
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub object_id: Option<String>,
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
|
||||||
pub fn if_false(t: &bool) -> bool {
|
|
||||||
!t
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about what owns this emoji
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum EmojiParent {
|
|
||||||
Server { id: String },
|
|
||||||
Detached,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of an Emoji on Revolt
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct Emoji {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// What owns this emoji
|
|
||||||
pub parent: EmojiParent,
|
|
||||||
/// Uploader user id
|
|
||||||
pub creator_id: String,
|
|
||||||
/// Emoji name
|
|
||||||
pub name: String,
|
|
||||||
/// Whether the emoji is animated
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub animated: bool,
|
|
||||||
/// Whether the emoji is marked as nsfw
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub nsfw: bool,
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
mod admin {
|
|
||||||
pub mod simple;
|
|
||||||
pub mod stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod media {
|
|
||||||
pub mod attachment;
|
|
||||||
pub mod emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod channels {
|
|
||||||
pub mod channel;
|
|
||||||
pub mod channel_invite;
|
|
||||||
pub mod channel_unread;
|
|
||||||
pub mod message;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod servers {
|
|
||||||
pub mod server;
|
|
||||||
pub mod server_ban;
|
|
||||||
pub mod server_member;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod users {
|
|
||||||
pub mod user;
|
|
||||||
pub mod user_settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod safety {
|
|
||||||
pub mod report;
|
|
||||||
pub mod snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use admin::*;
|
|
||||||
pub use channels::*;
|
|
||||||
pub use media::*;
|
|
||||||
pub use safety::*;
|
|
||||||
pub use servers::*;
|
|
||||||
pub use users::*;
|
|
||||||
|
|
||||||
pub use attachment::File;
|
|
||||||
pub use channel::Channel;
|
|
||||||
pub use channel_invite::Invite;
|
|
||||||
pub use channel_unread::ChannelUnread;
|
|
||||||
pub use emoji::Emoji;
|
|
||||||
pub use message::Message;
|
|
||||||
pub use report::Report;
|
|
||||||
pub use server::Server;
|
|
||||||
pub use server_ban::ServerBan;
|
|
||||||
pub use server_member::Member;
|
|
||||||
pub use simple::SimpleModel;
|
|
||||||
pub use snapshot::Snapshot;
|
|
||||||
pub use user::User;
|
|
||||||
pub use user_settings::UserSettings;
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
use iso8601_timestamp::Timestamp;
|
|
||||||
use rocket::FromFormField;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Reason for reporting content (message or server)
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub enum ContentReportReason {
|
|
||||||
/// No reason has been specified
|
|
||||||
NoneSpecified,
|
|
||||||
|
|
||||||
/// Illegal content catch-all reason
|
|
||||||
Illegal,
|
|
||||||
|
|
||||||
/// Selling or facilitating use of drugs or other illegal goods
|
|
||||||
IllegalGoods,
|
|
||||||
|
|
||||||
/// Extortion or blackmail
|
|
||||||
IllegalExtortion,
|
|
||||||
|
|
||||||
/// Revenge or child pornography
|
|
||||||
IllegalPornography,
|
|
||||||
|
|
||||||
/// Illegal hacking activity
|
|
||||||
IllegalHacking,
|
|
||||||
|
|
||||||
/// Extreme violence, gore, or animal cruelty
|
|
||||||
/// With exception to violence potrayed in media / creative arts
|
|
||||||
ExtremeViolence,
|
|
||||||
|
|
||||||
/// Content that promotes harm to others / self
|
|
||||||
PromotesHarm,
|
|
||||||
|
|
||||||
/// Unsolicited advertisements
|
|
||||||
UnsolicitedSpam,
|
|
||||||
|
|
||||||
/// This is a raid
|
|
||||||
Raid,
|
|
||||||
|
|
||||||
/// Spam or platform abuse
|
|
||||||
SpamAbuse,
|
|
||||||
|
|
||||||
/// Scams or fraud
|
|
||||||
ScamsFraud,
|
|
||||||
|
|
||||||
/// Distribution of malware or malicious links
|
|
||||||
Malware,
|
|
||||||
|
|
||||||
/// Harassment or abuse targeted at another user
|
|
||||||
Harassment,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reason for reporting a user
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub enum UserReportReason {
|
|
||||||
/// No reason has been specified
|
|
||||||
NoneSpecified,
|
|
||||||
|
|
||||||
/// Unsolicited advertisements
|
|
||||||
UnsolicitedSpam,
|
|
||||||
|
|
||||||
/// User is sending spam or otherwise abusing the platform
|
|
||||||
SpamAbuse,
|
|
||||||
|
|
||||||
/// User's profile contains inappropriate content for a general audience
|
|
||||||
InappropriateProfile,
|
|
||||||
|
|
||||||
/// User is impersonating another user
|
|
||||||
Impersonation,
|
|
||||||
|
|
||||||
/// User is evading a ban
|
|
||||||
BanEvasion,
|
|
||||||
|
|
||||||
/// User is not of minimum age to use the platform
|
|
||||||
Underage,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The content being reported
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum ReportedContent {
|
|
||||||
/// Report a message
|
|
||||||
Message {
|
|
||||||
/// ID of the message
|
|
||||||
id: String,
|
|
||||||
/// Reason for reporting message
|
|
||||||
report_reason: ContentReportReason,
|
|
||||||
},
|
|
||||||
/// Report a server
|
|
||||||
Server {
|
|
||||||
/// ID of the server
|
|
||||||
id: String,
|
|
||||||
/// Reason for reporting server
|
|
||||||
report_reason: ContentReportReason,
|
|
||||||
},
|
|
||||||
/// Report a user
|
|
||||||
User {
|
|
||||||
/// ID of the user
|
|
||||||
id: String,
|
|
||||||
/// Reason for reporting a user
|
|
||||||
report_reason: UserReportReason,
|
|
||||||
/// Message context
|
|
||||||
message_id: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Status of the report
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
#[serde(tag = "status")]
|
|
||||||
pub enum ReportStatus {
|
|
||||||
/// Report is waiting for triage / action
|
|
||||||
Created {},
|
|
||||||
|
|
||||||
/// Report was rejected
|
|
||||||
Rejected {
|
|
||||||
rejection_reason: String,
|
|
||||||
closed_at: Option<Timestamp>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Report was actioned and resolved
|
|
||||||
Resolved { closed_at: Option<Timestamp> },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Just the status of the report
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, FromFormField)]
|
|
||||||
pub enum ReportStatusString {
|
|
||||||
/// Report is waiting for triage / action
|
|
||||||
Created,
|
|
||||||
|
|
||||||
/// Report was rejected
|
|
||||||
Rejected,
|
|
||||||
|
|
||||||
/// Report was actioned and resolved
|
|
||||||
Resolved,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User-generated platform moderation report.
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, OptionalStruct, Clone)]
|
|
||||||
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
|
|
||||||
#[optional_name = "PartialReport"]
|
|
||||||
#[opt_skip_serializing_none]
|
|
||||||
pub struct Report {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// Id of the user creating this report
|
|
||||||
pub author_id: String,
|
|
||||||
/// Reported content
|
|
||||||
pub content: ReportedContent,
|
|
||||||
/// Additional report context
|
|
||||||
pub additional_context: String,
|
|
||||||
/// Status of the report
|
|
||||||
#[opt_passthrough]
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub status: ReportStatus,
|
|
||||||
/// Additional notes included on the report
|
|
||||||
#[serde(default)]
|
|
||||||
pub notes: String,
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::models::{Channel, Message, Server, User};
|
|
||||||
|
|
||||||
/// Enum to map into different models
|
|
||||||
/// that can be saved in a snapshot
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
#[serde(tag = "_type")]
|
|
||||||
pub enum SnapshotContent {
|
|
||||||
Message {
|
|
||||||
/// Context before the message
|
|
||||||
#[serde(rename = "_prior_context", default)]
|
|
||||||
prior_context: Vec<Message>,
|
|
||||||
|
|
||||||
/// Context after the message
|
|
||||||
#[serde(rename = "_leading_context", default)]
|
|
||||||
leading_context: Vec<Message>,
|
|
||||||
|
|
||||||
/// Message
|
|
||||||
#[serde(flatten)]
|
|
||||||
message: Message,
|
|
||||||
},
|
|
||||||
Server(Server),
|
|
||||||
User(User),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Snapshot of some content
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
pub struct Snapshot {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// Report parent Id
|
|
||||||
pub report_id: String,
|
|
||||||
/// Snapshot of content
|
|
||||||
pub content: SnapshotContent,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Snapshot of some content with required data to render
|
|
||||||
#[derive(Serialize, JsonSchema, Debug)]
|
|
||||||
pub struct SnapshotWithContext {
|
|
||||||
/// Snapshot itself
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub snapshot: Snapshot,
|
|
||||||
/// Users involved in snapshot
|
|
||||||
#[serde(rename = "_users", skip_serializing_if = "Vec::is_empty")]
|
|
||||||
pub users: Vec<User>,
|
|
||||||
/// Channels involved in snapshot
|
|
||||||
#[serde(rename = "_channels", skip_serializing_if = "Vec::is_empty")]
|
|
||||||
pub channels: Vec<Channel>,
|
|
||||||
/// Server involved in snapshot
|
|
||||||
#[serde(rename = "_server", skip_serializing_if = "Option::is_none")]
|
|
||||||
pub server: Option<Server>,
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use validator::Validate;
|
|
||||||
|
|
||||||
use crate::{models::attachment::File, OverrideField};
|
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
|
||||||
pub fn if_false(t: &bool) -> bool {
|
|
||||||
!t
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a server role
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)]
|
|
||||||
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)]
|
|
||||||
#[optional_name = "PartialRole"]
|
|
||||||
#[opt_skip_serializing_none]
|
|
||||||
#[opt_some_priority]
|
|
||||||
pub struct Role {
|
|
||||||
/// Role name
|
|
||||||
pub name: String,
|
|
||||||
/// Permissions available to this role
|
|
||||||
pub permissions: OverrideField,
|
|
||||||
/// Colour used for this role
|
|
||||||
///
|
|
||||||
/// This can be any valid CSS colour
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub colour: Option<String>,
|
|
||||||
/// Whether this role should be shown separately on the member sidebar
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub hoist: bool,
|
|
||||||
/// Ranking of this role
|
|
||||||
#[serde(default)]
|
|
||||||
pub rank: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Channel category
|
|
||||||
#[derive(Validate, Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct Category {
|
|
||||||
/// Unique ID for this category
|
|
||||||
#[validate(length(min = 1, max = 32))]
|
|
||||||
pub id: String,
|
|
||||||
/// Title for this category
|
|
||||||
#[validate(length(min = 1, max = 32))]
|
|
||||||
pub title: String,
|
|
||||||
/// Channels in this category
|
|
||||||
pub channels: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// System message channel assignments
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct SystemMessageChannels {
|
|
||||||
/// ID of channel to send user join messages in
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user_joined: Option<String>,
|
|
||||||
/// ID of channel to send user left messages in
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user_left: Option<String>,
|
|
||||||
/// ID of channel to send user kicked messages in
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user_kicked: Option<String>,
|
|
||||||
/// ID of channel to send user banned messages in
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user_banned: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Server flag enum
|
|
||||||
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum ServerFlags {
|
|
||||||
Verified = 1,
|
|
||||||
Official = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a server on Revolt
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)]
|
|
||||||
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
|
|
||||||
#[optional_name = "PartialServer"]
|
|
||||||
#[opt_skip_serializing_none]
|
|
||||||
#[opt_some_priority]
|
|
||||||
pub struct Server {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// User id of the owner
|
|
||||||
pub owner: String,
|
|
||||||
|
|
||||||
/// Name of the server
|
|
||||||
pub name: String,
|
|
||||||
/// Description for the server
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
|
|
||||||
/// Channels within this server
|
|
||||||
pub channels: Vec<String>,
|
|
||||||
/// Categories for this server
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub categories: Option<Vec<Category>>,
|
|
||||||
/// Configuration for sending system event messages
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub system_messages: Option<SystemMessageChannels>,
|
|
||||||
|
|
||||||
/// Roles for this server
|
|
||||||
#[serde(
|
|
||||||
default = "HashMap::<String, Role>::new",
|
|
||||||
skip_serializing_if = "HashMap::<String, Role>::is_empty"
|
|
||||||
)]
|
|
||||||
pub roles: HashMap<String, Role>,
|
|
||||||
/// Default set of server and channel permissions
|
|
||||||
pub default_permissions: i64,
|
|
||||||
|
|
||||||
/// Icon attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub icon: Option<File>,
|
|
||||||
/// Banner attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub banner: Option<File>,
|
|
||||||
|
|
||||||
/// Bitfield of server flags
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub flags: Option<i32>,
|
|
||||||
|
|
||||||
/// Whether this server is flagged as not safe for work
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub nsfw: bool,
|
|
||||||
/// Whether to enable analytics
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub analytics: bool,
|
|
||||||
/// Whether this server should be publicly discoverable
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub discoverable: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional fields on server object
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum FieldsServer {
|
|
||||||
Description,
|
|
||||||
Categories,
|
|
||||||
SystemMessages,
|
|
||||||
Icon,
|
|
||||||
Banner,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional fields on server object
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum FieldsRole {
|
|
||||||
Colour,
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::server_member::MemberCompositeKey;
|
|
||||||
|
|
||||||
/// Representation of a server ban on Revolt
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct ServerBan {
|
|
||||||
/// Unique member id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: MemberCompositeKey,
|
|
||||||
/// Reason for ban creation
|
|
||||||
pub reason: Option<String>,
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use iso8601_timestamp::Timestamp;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::models::attachment::File;
|
|
||||||
|
|
||||||
use super::server::Role;
|
|
||||||
|
|
||||||
/// Composite primary key consisting of server and user id
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)]
|
|
||||||
pub struct MemberCompositeKey {
|
|
||||||
/// Server Id
|
|
||||||
pub server: String,
|
|
||||||
/// User Id
|
|
||||||
pub user: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a member of a server on Revolt
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct)]
|
|
||||||
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
|
|
||||||
#[optional_name = "PartialMember"]
|
|
||||||
#[opt_skip_serializing_none]
|
|
||||||
#[opt_some_priority]
|
|
||||||
pub struct Member {
|
|
||||||
/// Unique member id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: MemberCompositeKey,
|
|
||||||
|
|
||||||
/// Time at which this user joined the server
|
|
||||||
pub joined_at: Timestamp,
|
|
||||||
|
|
||||||
/// Member's nickname
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nickname: Option<String>,
|
|
||||||
/// Avatar attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub avatar: Option<File>,
|
|
||||||
|
|
||||||
/// Member's roles
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
|
||||||
pub roles: Vec<String>,
|
|
||||||
/// Timestamp this member is timed out until
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub timeout: Option<Timestamp>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a member of a server on Revolt With Role Data
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct MemberWithRoles {
|
|
||||||
pub member: Member,
|
|
||||||
pub roles: HashMap<String, Role>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum MemberResponse {
|
|
||||||
Member(Member),
|
|
||||||
MemberWithRoles(MemberWithRoles),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Member> for MemberResponse {
|
|
||||||
fn from(value: Member) -> Self {
|
|
||||||
Self::Member(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<MemberWithRoles> for MemberResponse {
|
|
||||||
fn from(value: MemberWithRoles) -> Self {
|
|
||||||
Self::MemberWithRoles(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Optional fields on server member object
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum FieldsMember {
|
|
||||||
Nickname,
|
|
||||||
Avatar,
|
|
||||||
Roles,
|
|
||||||
Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Member removal intention
|
|
||||||
pub enum RemovalIntention {
|
|
||||||
Leave,
|
|
||||||
Kick,
|
|
||||||
Ban,
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
|
||||||
pub fn if_false(t: &bool) -> bool {
|
|
||||||
!t
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bot flag enum
|
|
||||||
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum BotFlags {
|
|
||||||
Verified = 1,
|
|
||||||
Official = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a bot on Revolt
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)]
|
|
||||||
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
|
|
||||||
#[optional_name = "PartialBot"]
|
|
||||||
#[opt_skip_serializing_none]
|
|
||||||
#[opt_some_priority]
|
|
||||||
pub struct Bot {
|
|
||||||
/// Bot Id
|
|
||||||
///
|
|
||||||
/// This equals the associated bot user's id.
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// User Id of the bot owner
|
|
||||||
pub owner: String,
|
|
||||||
/// Token used to authenticate requests for this bot
|
|
||||||
pub token: String,
|
|
||||||
/// Whether the bot is public
|
|
||||||
/// (may be invited by anyone)
|
|
||||||
pub public: bool,
|
|
||||||
|
|
||||||
/// Whether to enable analytics
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub analytics: bool,
|
|
||||||
/// Whether this bot should be publicly discoverable
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub discoverable: bool,
|
|
||||||
/// Reserved; URL for handling interactions
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub interactions_url: Option<String>,
|
|
||||||
/// URL for terms of service
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub terms_of_service_url: Option<String>,
|
|
||||||
/// URL for privacy policy
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub privacy_policy_url: Option<String>,
|
|
||||||
|
|
||||||
/// Enum of bot flags
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub flags: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional fields on bot object
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq)]
|
|
||||||
pub enum FieldsBot {
|
|
||||||
Token,
|
|
||||||
InteractionsURL,
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use validator::Validate;
|
|
||||||
|
|
||||||
use crate::models::attachment::File;
|
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
|
||||||
pub fn if_false(t: &bool) -> bool {
|
|
||||||
!t
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User's relationship with another user (or themselves)
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum RelationshipStatus {
|
|
||||||
None,
|
|
||||||
User,
|
|
||||||
Friend,
|
|
||||||
Outgoing,
|
|
||||||
Incoming,
|
|
||||||
Blocked,
|
|
||||||
BlockedOther,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Relationship entry indicating current status with other user
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct Relationship {
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
pub status: RelationshipStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Presence status
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Presence {
|
|
||||||
/// User is online
|
|
||||||
Online,
|
|
||||||
/// User is not currently available
|
|
||||||
Idle,
|
|
||||||
/// User is focusing / will only receive mentions
|
|
||||||
Focus,
|
|
||||||
/// User is busy / will not receive any notifications
|
|
||||||
Busy,
|
|
||||||
/// User appears to be offline
|
|
||||||
Invisible,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User's active status
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Validate, Default)]
|
|
||||||
pub struct UserStatus {
|
|
||||||
/// Custom status text
|
|
||||||
#[validate(length(min = 1, max = 128))]
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub text: Option<String>,
|
|
||||||
/// Current presence option
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub presence: Option<Presence>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User's profile
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)]
|
|
||||||
pub struct UserProfile {
|
|
||||||
/// Text content on user's profile
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub content: Option<String>,
|
|
||||||
/// Background visible on user's profile
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub background: Option<File>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User badge bitfield
|
|
||||||
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum Badges {
|
|
||||||
/// Revolt Developer
|
|
||||||
Developer = 1,
|
|
||||||
/// Helped translate Revolt
|
|
||||||
Translator = 2,
|
|
||||||
/// Monetarily supported Revolt
|
|
||||||
Supporter = 4,
|
|
||||||
/// Responsibly disclosed a security issue
|
|
||||||
ResponsibleDisclosure = 8,
|
|
||||||
/// Revolt Founder
|
|
||||||
Founder = 16,
|
|
||||||
/// Platform moderator
|
|
||||||
PlatformModeration = 32,
|
|
||||||
/// Active monetary supporter
|
|
||||||
ActiveSupporter = 64,
|
|
||||||
/// 🦊🦝
|
|
||||||
Paw = 128,
|
|
||||||
/// Joined as one of the first 1000 users in 2021
|
|
||||||
EarlyAdopter = 256,
|
|
||||||
/// Amogus
|
|
||||||
ReservedRelevantJokeBadge1 = 512,
|
|
||||||
/// Low resolution troll face
|
|
||||||
ReservedRelevantJokeBadge2 = 1024,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User flag enum
|
|
||||||
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum Flags {
|
|
||||||
/// User has been suspended from the platform
|
|
||||||
Suspended = 1,
|
|
||||||
/// User has deleted their account
|
|
||||||
Deleted = 2,
|
|
||||||
/// User was banned off the platform
|
|
||||||
Banned = 4,
|
|
||||||
/// User was marked as spam and removed from platform
|
|
||||||
Spam = 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bot information for if the user is a bot
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
|
|
||||||
pub struct BotInformation {
|
|
||||||
/// Id of the owner of this bot
|
|
||||||
pub owner: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representiation of a User on Revolt.
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)]
|
|
||||||
#[optional_derive(Serialize, Deserialize, Debug, Default, Clone)]
|
|
||||||
#[optional_name = "PartialUser"]
|
|
||||||
#[opt_skip_serializing_none]
|
|
||||||
#[opt_some_priority]
|
|
||||||
pub struct User {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// Username
|
|
||||||
pub username: String,
|
|
||||||
/// Discriminator
|
|
||||||
pub discriminator: String,
|
|
||||||
/// Display name
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
/// Avatar attachment
|
|
||||||
pub avatar: Option<File>,
|
|
||||||
/// Relationships with other users
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub relations: Option<Vec<Relationship>>,
|
|
||||||
|
|
||||||
/// Bitfield of user badges
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub badges: Option<i32>,
|
|
||||||
/// User's current status
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub status: Option<UserStatus>,
|
|
||||||
/// User's profile page
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub profile: Option<UserProfile>,
|
|
||||||
|
|
||||||
/// Enum of user flags
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub flags: Option<i32>,
|
|
||||||
/// Whether this user is privileged
|
|
||||||
#[serde(skip_serializing_if = "if_false", default)]
|
|
||||||
pub privileged: bool,
|
|
||||||
/// Bot information
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub bot: Option<BotInformation>,
|
|
||||||
|
|
||||||
// ? Entries below should never be pushed to the database
|
|
||||||
/// Current session user's relationship with this user
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub relationship: Option<RelationshipStatus>,
|
|
||||||
/// Whether this user is currently online
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub online: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional fields on user object
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum FieldsUser {
|
|
||||||
Avatar,
|
|
||||||
StatusText,
|
|
||||||
StatusPresence,
|
|
||||||
ProfileContent,
|
|
||||||
ProfileBackground,
|
|
||||||
DisplayName,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enumeration providing a hint to the type of user we are handling
|
|
||||||
pub enum UserHint {
|
|
||||||
/// Could be either a user or a bot
|
|
||||||
Any,
|
|
||||||
/// Only match bots
|
|
||||||
Bot,
|
|
||||||
/// Only match users
|
|
||||||
User,
|
|
||||||
}
|
|
||||||