Compare commits

...
16 Commits
Author SHA1 Message Date
Paul Makles 430df03b20 chore: bump sentry crate 2023-08-10 09:42:44 +01:00
Paul Makles f5f70287e7 feat: add SENTRY_DSN env variable 2023-08-10 09:40:41 +01:00
Paul Makles a0580c5f5a chore: remove legacy query route 2023-08-03 19:43:05 +01:00
Paul Makles 121a9cd87c feat(core): implement message model
closes #273
2023-08-03 19:32:47 +01:00
Paul Makles d87d608d9e chore(core): flesh out implementations for channels / servers 2023-08-03 18:27:13 +01:00
Paul Makles bd9ecc1519 fix: missing conversion 2023-08-02 00:31:19 +01:00
Paul Makles f5a113da47 feat(core): implement server ban
closes #274
2023-08-01 21:19:07 +01:00
Paul Makles 11fdb0c1dc feat(core): implement emoji model
closes #270
2023-08-01 20:45:58 +01:00
Paul Makles e0033ceb12 feat(core): implement channel unreads model
closes #269
2023-08-01 17:41:43 +01:00
Paul Makles 7318ec6ef6 chore(core/models): add cfg_attr where necessary 2023-08-01 15:54:56 +01:00
Paul Makles a516c7adcf feat(core): implement channel invite model
closes #268
2023-08-01 15:53:43 +01:00
Paul Makles c48109ca66 docs: add note for 'Id' casing 2023-08-01 15:51:57 +01:00
Paul Makles f0ef781cd7 chore: add MSRV to README [skip ci]
closes #254
2023-08-01 15:04:45 +01:00
Paul Makles d9dd040db4 feat(core/models): implement server members model
closes #280
2023-08-01 15:00:07 +01:00
Paul Makles fec947b4c8 feat(core/models): implement server model
closes #279
2023-08-01 14:55:25 +01:00
Paul Makles ec6df36c25 chore: strip legacy admin API 2023-08-01 14:35:36 +01:00
76 changed files with 4445 additions and 888 deletions
+1
View File
@@ -4,3 +4,4 @@ target
.env
.vercel
.DS_Store
Generated
+111 -37
View File
@@ -421,6 +421,12 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]]
name = "binascii"
version = "0.1.4"
@@ -519,7 +525,7 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"uuid",
"uuid 0.8.2",
]
[[package]]
@@ -798,12 +804,12 @@ dependencies = [
[[package]]
name = "debugid"
version = "0.7.3"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6ee87af31d84ef885378aebca32be3d682b0e0dc119d5b4860a2c5bb5046730"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid",
"uuid 1.4.1",
]
[[package]]
@@ -1001,6 +1007,18 @@ dependencies = [
"version_check",
]
[[package]]
name = "findshlibs"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
dependencies = [
"cc",
"lazy_static",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1548,9 +1566,9 @@ checksum = "90f97a5f38dd3ccfbe7aa80f4a0c00930f21b922c74195be0201c51028f22dcf"
[[package]]
name = "indexmap"
version = "1.9.1"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg 1.1.0",
"hashbrown 0.12.1",
@@ -1613,11 +1631,12 @@ dependencies = [
[[package]]
name = "iso8601-timestamp"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368d05885f276773b79b4578869a60ab91e880415d17fb2a8d193aea74188a29"
checksum = "3bd7663ae5b511b3d13c7d374dbe73b03da3da8e66b83950390173bc7c7325cc"
dependencies = [
"generic-array 0.14.5",
"schemars",
"serde",
"time 0.3.17",
]
@@ -2005,7 +2024,7 @@ dependencies = [
"trust-dns-proto",
"trust-dns-resolver",
"typed-builder",
"uuid",
"uuid 0.8.2",
"webpki-roots",
]
@@ -2231,11 +2250,12 @@ dependencies = [
[[package]]
name = "os_info"
version = "3.4.0"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eca3ecae1481e12c3d9379ec541b238a16f0b75c9a409942daa8ec20dbfdb62"
checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
dependencies = [
"log",
"serde",
"winapi 0.3.9",
]
@@ -2857,7 +2877,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"async-std",
"async-tungstenite",
@@ -2874,7 +2894,7 @@ dependencies = [
[[package]]
name = "revolt-database"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"async-recursion",
"async-std",
@@ -2882,7 +2902,8 @@ dependencies = [
"authifier",
"bson",
"futures",
"iso8601-timestamp 0.2.10",
"indexmap",
"iso8601-timestamp 0.2.11",
"log",
"mongodb",
"nanoid",
@@ -2905,7 +2926,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"async-channel",
"async-std",
@@ -2946,8 +2967,10 @@ dependencies = [
[[package]]
name = "revolt-models"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"indexmap",
"iso8601-timestamp 0.2.11",
"revolt-permissions",
"revolt_optional_struct",
"schemars",
@@ -2957,7 +2980,7 @@ dependencies = [
[[package]]
name = "revolt-permissions"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"async-std",
"async-trait",
@@ -2971,7 +2994,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"async-std",
"log",
@@ -2982,7 +3005,7 @@ dependencies = [
[[package]]
name = "revolt-quark"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"async-lock",
"async-recursion",
@@ -3035,7 +3058,7 @@ dependencies = [
[[package]]
name = "revolt-result"
version = "0.6.5"
version = "0.6.6"
dependencies = [
"revolt_okapi",
"revolt_rocket_okapi",
@@ -3456,39 +3479,44 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "sentry"
version = "0.25.0"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d23af89cf3e40dffb53f974e9a21653353b3e21cf51633aa58006f2a0caf8a"
checksum = "01b0ad16faa5d12372f914ed40d00bda21a6d1bdcc99264c5e5e1c9495cf3654"
dependencies = [
"httpdate 1.0.2",
"native-tls",
"reqwest",
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-debug-images",
"sentry-panic",
"sentry-tracing",
"tokio 1.18.2",
"ureq",
]
[[package]]
name = "sentry-backtrace"
version = "0.25.0"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8158a446429420acdf6a4f75192ee8929da16a0c41c89a1c34b2e0f1eaebcc02"
checksum = "11f2ee8f147bb5f22ac59b5c35754a759b9a6f6722402e2a14750b2a63fc59bd"
dependencies = [
"backtrace",
"lazy_static",
"once_cell",
"regex",
"sentry-core",
]
[[package]]
name = "sentry-contexts"
version = "0.25.0"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3bda8a1e3213f1944da2d42f3081ea9f3717105bb2a6b0a8fe4f5e603010a3"
checksum = "dcd133362c745151eeba0ac61e3ba8350f034e9fe7509877d08059fe1d7720c6"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version 0.4.0",
"sentry-core",
"uname",
@@ -3496,11 +3524,11 @@ dependencies = [
[[package]]
name = "sentry-core"
version = "0.25.0"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56333f11be3a78131c67637f7611339df8af7ad9af831226585a457df75f9e3b"
checksum = "7163491708804a74446642ff2c80b3acd668d4b9e9f497f85621f3d250fd012b"
dependencies = [
"lazy_static",
"once_cell",
"rand 0.8.5",
"sentry-types",
"serde",
@@ -3508,20 +3536,43 @@ dependencies = [
]
[[package]]
name = "sentry-panic"
version = "0.25.0"
name = "sentry-debug-images"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b957b1965c450acd220a27806fe1f2dec998d393973ebae797936b12df1c7416"
checksum = "6a5003d7ff08aa3b2b76994080b183e8cfa06c083e280737c9cee02ca1c70f5e"
dependencies = [
"findshlibs",
"once_cell",
"sentry-core",
]
[[package]]
name = "sentry-panic"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4dfe8371c9b2e126a8b64f6fefa54cef716ff2a50e63b5558a48b899265bccd"
dependencies = [
"sentry-backtrace",
"sentry-core",
]
[[package]]
name = "sentry-types"
version = "0.25.0"
name = "sentry-tracing"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "825fd3382e2397007499a910e0184e55f7837cb0df4af30ae62bd2123e2ebcd6"
checksum = "5aca8b88978677a27ee1a91beafe4052306c474c06f582321fde72d2e2cc2f7f"
dependencies = [
"sentry-backtrace",
"sentry-core",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "sentry-types"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e7a88e0c1922d19b3efee12a8215f6a8a806e442e665ada71cc222cab72985f"
dependencies = [
"debugid",
"getrandom",
@@ -3531,7 +3582,7 @@ dependencies = [
"thiserror",
"time 0.3.17",
"url",
"uuid",
"uuid 1.4.1",
]
[[package]]
@@ -4403,6 +4454,19 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9"
dependencies = [
"base64 0.21.2",
"log",
"native-tls",
"once_cell",
"url",
]
[[package]]
name = "url"
version = "2.2.2"
@@ -4432,6 +4496,16 @@ dependencies = [
"serde",
]
[[package]]
name = "uuid"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "validator"
version = "0.15.0"
+4
View File
@@ -21,6 +21,10 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing)
## Minimum Supported Rust Version
Rust 1.70 or higher.
## Contributing
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
+6
View File
@@ -2,6 +2,10 @@
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
## Writing Style
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
## `core/database` crate
w.r.t. `model.rs` files
@@ -22,6 +26,7 @@ w.r.t. `model.rs` files
);
```
- `auto_derived!` macro accepts multiple entries and should be used as such:
```rust
auto_derived!(
/// Optional fields on server object
@@ -31,6 +36,7 @@ w.r.t. `model.rs` files
pub enum FieldsRole { .. }
);
```
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
```rust
#[serde(skip_serializing_if = "crate::if_false", default)]
+7 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.6.5"
version = "0.6.6"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -25,7 +25,11 @@ serde = "1.0.136"
# async
futures = "0.3.21"
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
async-std = { version = "1.8.0", features = [
"tokio1",
"tokio02",
"attributes",
] }
# core
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
+18 -12
View File
@@ -1,31 +1,34 @@
[package]
name = "revolt-database"
version = "0.6.5"
version = "0.6.6"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Database Implementation"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
# Databases
mongodb = [ "dep:mongodb", "bson" ]
mongodb = ["dep:mongodb", "bson"]
# ... Other
async-std-runtime = [ "async-std" ]
rocket-impl = [ "rocket", "schemars" ]
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
async-std-runtime = ["async-std"]
rocket-impl = ["rocket", "schemars"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
# Default Features
default = [ "mongodb", "async-std-runtime" ]
default = ["mongodb", "async-std-runtime"]
[dependencies]
# Core
revolt-result = { version = "0.6.5", path = "../result" }
revolt-models = { version = "0.6.5", path = "../models" }
revolt-presence = { version = "0.6.5", path = "../presence" }
revolt-permissions = { version = "0.6.5", path = "../permissions", features = [ "serde", "bson" ] }
revolt-result = { version = "0.6.6", path = "../result" }
revolt-models = { version = "0.6.6", path = "../models" }
revolt-presence = { version = "0.6.6", path = "../presence" }
revolt-permissions = { version = "0.6.6", path = "../permissions", features = [
"serde",
"bson",
] }
# Utility
log = "0.4"
@@ -33,6 +36,7 @@ rand = "0.8.5"
ulid = "1.0.0"
nanoid = "0.4.0"
once_cell = "1.17"
indexmap = "1.9.1"
# Serialisation
serde_json = "1"
@@ -61,7 +65,9 @@ async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Rocket Impl
schemars = { version = "0.8.8", optional = true }
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"], optional = true }
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
"json",
], optional = true }
# Authifier
authifier = { version = "1.0" }
@@ -3,29 +3,28 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
AccountStrike, Bot, Channel, File, Member, MemberCompositeKey, Server, User, UserSettings,
Webhook,
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
MemberCompositeKey, Server, ServerBan, User, UserSettings, Webhook,
};
database_derived!(
/// Reference implementation
#[derive(Default)]
pub struct ReferenceDb {
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub server_bans: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
pub emoji: Arc<Mutex<HashMap<String, ()>>>,
pub messages: Arc<Mutex<HashMap<String, ()>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
}
);
+9 -10
View File
@@ -2,7 +2,9 @@ use authifier::AuthifierEvent;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{
Channel, FieldsChannel, FieldsWebhook, PartialChannel, PartialWebhook, Webhook,
Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer, FieldsWebhook,
MemberCompositeKey, PartialChannel, PartialMember, PartialRole, PartialServer, PartialWebhook,
Server, UserSettings, Webhook,
};
use revolt_result::Error;
@@ -101,8 +103,7 @@ pub enum EventV1 {
},
/// Bulk delete messages
BulkMessageDelete { channel: String, ids: Vec<String> },
BulkMessageDelete { channel: String, ids: Vec<String> },*/
/// New server
ServerCreate {
id: String,
@@ -144,7 +145,7 @@ pub enum EventV1 {
/// Server role deleted
ServerRoleDelete { id: String, role_id: String },
/// Update existing user
/*/// Update existing user
UserUpdate {
id: String,
data: PartialUser,
@@ -158,12 +159,11 @@ pub enum EventV1 {
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
/*/// User has been platform banned or deleted their account
///
/// Clients should remove the following associated data:
/// - Messages
@@ -172,15 +172,14 @@ pub enum EventV1 {
/// - 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 },
UserPlatformWipe { user_id: String, flags: i32 }, */
/// New emoji
EmojiCreate(Emoji),
/// Delete emoji
EmojiDelete { id: String },
/// New report
/*/// New report
ReportCreate(Report), */
/// New channel
ChannelCreate(Channel),
@@ -697,7 +697,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
if revision <= 19 {
info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable.");
info!(
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
);
db.db()
.create_collection("safety_reports", None)
@@ -708,19 +710,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.create_collection("safety_snapshots", None)
.await
.unwrap();
db.col::<Document>("safety_reports")
.update_many(
doc! {},
doc! {
"$set": {
"status": "Created"
}
},
None,
)
.await
.unwrap();
}
if revision <= 20 {
@@ -0,0 +1,101 @@
use revolt_result::{create_error, Result};
use crate::Database;
/* 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',
]; */
auto_derived!(
/// Invite
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
} */
}
);
#[allow(clippy::disallowed_methods)]
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_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> {
let code = nanoid::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(create_error!(NotFound))
}
}
@@ -0,0 +1,21 @@
use revolt_result::Result;
use crate::Invite;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannelInvites: Sync + Send {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
/// Fetch an invite by its id
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>>;
/// Delete an invite by its id
async fn delete_invite(&self, code: &str) -> Result<()>;
}
@@ -0,0 +1,50 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::Invite;
use crate::MongoDb;
use super::AbstractChannelInvites;
static COL: &str = "channel_invites";
#[async_trait]
impl AbstractChannelInvites for MongoDb {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
query!(self, insert_one, COL, &invite).map(|_| ())
}
/// Fetch an invite by the code
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
query!(self, find_one_by_id, COL, code)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
Ok(self
.col::<Invite>(COL)
.find(
doc! {
"server": server_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Delete an invite by its code
async fn delete_invite(&self, code: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, code).map(|_| ())
}
}
@@ -0,0 +1,52 @@
use revolt_result::Result;
use crate::Invite;
use crate::ReferenceDb;
use super::AbstractChannelInvites;
#[async_trait]
impl AbstractChannelInvites for ReferenceDb {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
let mut invites = self.channel_invites.lock().await;
if invites.contains_key(invite.code()) {
Err(create_database_error!("insert", "invite"))
} else {
invites.insert(invite.code().to_string(), invite.clone());
Ok(())
}
}
/// Fetch an invite by the code
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
let invites = self.channel_invites.lock().await;
invites
.get(code)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
let invites = self.channel_invites.lock().await;
Ok(invites
.values()
.filter(|invite| match invite {
Invite::Server { server, .. } => server == server_id,
_ => false,
})
.cloned()
.collect())
}
/// Delete an invite by its code
async fn delete_invite(&self, code: &str) -> Result<()> {
let mut invites = self.channel_invites.lock().await;
if invites.remove(code).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,24 @@
auto_derived!(
/// Channel Unread
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>>,
}
/// Composite primary key consisting of channel and user id
#[derive(Hash)]
pub struct ChannelCompositeKey {
/// Channel Id
pub channel: String,
/// User Id
pub user: String,
}
);
@@ -0,0 +1,31 @@
use revolt_result::Result;
use crate::ChannelUnread;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannelUnreads: Sync + Send {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()>;
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()>;
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
}
@@ -0,0 +1,119 @@
use bson::Document;
use mongodb::options::UpdateOptions;
use revolt_result::Result;
use ulid::Ulid;
use crate::ChannelUnread;
use crate::MongoDb;
use super::AbstractChannelUnreads;
static COL: &str = "channel_unreads";
#[async_trait]
impl AbstractChannelUnreads for MongoDb {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$unset": {
"mentions": 1_i32
},
"$set": {
"last_id": message_id
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL)
.delete_many(
doc! {
"_id.channel": {
"$in": channel_ids
},
"_id.user": user_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", COL))?;
self.col::<Document>(COL)
.insert_many(
channel_ids
.iter()
.map(|channel_id| {
doc! {
"_id": {
"channel": channel_id,
"user": user_id
},
"last_id": &current_time
}
})
.collect::<Vec<Document>>(),
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
}
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$push": {
"mentions": {
"$each": message_ids
}
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query!(
self,
find,
COL,
doc! {
"_id.user": user_id
}
)
}
}
@@ -0,0 +1,89 @@
use revolt_result::Result;
use ulid::Ulid;
use crate::{ChannelCompositeKey, ChannelUnread, ReferenceDb};
use super::AbstractChannelUnreads;
#[async_trait]
impl AbstractChannelUnreads for ReferenceDb {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(mut unread) = unreads.get_mut(&key) {
unread.mentions = None;
unread.last_id.replace(message_id.to_string());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: Some(message_id.to_string()),
mentions: None,
},
);
}
Ok(())
}
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
for channel_id in channel_ids {
self.acknowledge_message(channel_id, user_id, &current_time)
.await?;
}
Ok(())
}
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(unread) = unreads.get_mut(&key) {
unread.mentions.replace(message_ids.to_vec());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: None,
mentions: Some(message_ids.to_vec()),
},
);
}
Ok(())
}
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads
.values()
.filter(|unread| unread.id.user == user_id)
.cloned()
.collect())
}
}
@@ -169,6 +169,19 @@ impl Channel {
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_channel(self).await?;
let event = EventV1::ChannelCreate(self.clone().into());
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(())
}
@@ -293,7 +306,7 @@ impl Channel {
}
/// Update channel data
pub async fn update<'a>(
pub async fn update(
&mut self,
db: &Database,
partial: PartialChannel,
@@ -305,8 +318,19 @@ impl Channel {
self.apply_options(partial.clone());
db.update_channel(&self.id(), &partial, remove.clone())
.await?;
let id = self.id().to_string();
db.update_channel(&id, &partial, remove.clone()).await?;
EventV1::ChannelUpdate {
id: id.clone(),
data: partial.into(),
clear: remove.into_iter().map(|v| v.into()).collect(),
}
.p(match self {
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
_ => id,
})
.await;
Ok(())
}
@@ -514,6 +538,8 @@ impl Channel {
/// 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
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,87 @@
use std::collections::HashSet;
use std::str::FromStr;
use once_cell::sync::Lazy;
use revolt_result::Result;
use ulid::Ulid;
use crate::events::client::EventV1;
use crate::Database;
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
include_str!("unicode_emoji.txt")
.split('\n')
.map(|x| x.into())
.collect()
});
auto_derived!(
/// Emoji
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 = "crate::if_false", default)]
pub animated: bool,
/// Whether the emoji is marked as nsfw
#[serde(skip_serializing_if = "crate::if_false", default)]
pub nsfw: bool,
}
/// Parent Id of the emoji
#[serde(tag = "type")]
pub enum EmojiParent {
Server { id: String },
Detached,
}
);
#[allow(clippy::disallowed_methods)]
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().into())
.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))
}
}
}
@@ -0,0 +1,24 @@
use revolt_result::Result;
use crate::Emoji;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractEmojis: Sync + Send {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji>;
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>>;
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
}
@@ -0,0 +1,70 @@
use bson::Document;
use revolt_result::Result;
use crate::Emoji;
use crate::MongoDb;
use super::AbstractEmojis;
static COL: &str = "emojis";
#[async_trait]
impl AbstractEmojis for MongoDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
query!(self, insert_one, COL, &emoji).map(|_| ())
}
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
query!(
self,
find_one,
COL,
doc! {
"parent.id": parent_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
query!(
self,
find,
COL,
doc! {
"parent.id": {
"$in": parent_ids
}
}
)
}
/// Detach 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(|_| create_database_error!("update_one", COL))
}
}
@@ -0,0 +1,67 @@
use revolt_result::Result;
use crate::Emoji;
use crate::EmojiParent;
use crate::ReferenceDb;
use super::AbstractEmojis;
#[async_trait]
impl AbstractEmojis for ReferenceDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
let mut emojis = self.emojis.lock().await;
if emojis.contains_key(&emoji.id) {
Err(create_database_error!("insert", "emoji"))
} else {
emojis.insert(emoji.id.to_string(), emoji.clone());
Ok(())
}
}
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
let emojis = self.emojis.lock().await;
emojis
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
let emojis = self.emojis.lock().await;
Ok(emojis
.values()
.filter(|emoji| match &emoji.parent {
EmojiParent::Server { id } => id == parent_id,
_ => false,
})
.cloned()
.collect())
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
let emojis = self.emojis.lock().await;
Ok(emojis
.values()
.filter(|emoji| match &emoji.parent {
EmojiParent::Server { id } => parent_ids.contains(id),
_ => false,
})
.cloned()
.collect())
}
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
let mut emojis = self.emojis.lock().await;
if let Some(bot) = emojis.get_mut(&emoji.id) {
bot.parent = EmojiParent::Detached;
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,212 @@
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_models::v0::{Embed, MessageSort, MessageWebhook};
use crate::File;
auto_derived_partial!(
/// Message
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>,
},
"PartialMessage"
);
auto_derived!(
/// System Event
#[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
pub struct Masquerade {
/// Replace the display name shown on this message
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Replace the avatar shown on this message (URL to image file)
#[serde(skip_serializing_if = "Option::is_none")]
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")]
pub colour: Option<String>,
}
/// Information to guide interactions on this message
#[derive(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 = "crate::if_false", default)]
pub restrict_reactions: bool,
}
/// Appended Information
pub struct AppendMessage {
/// Additional embeds to include in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
}
/// Message Time Period
///
/// Filter and sort messages by time
#[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
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
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,
}
);
#[allow(clippy::disallowed_methods)]
impl Message {}
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()
}
}
@@ -0,0 +1,39 @@
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
// mod mongodb;
// mod reference;
#[async_trait]
pub trait AbstractMessages: Sync + Send {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()>;
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message>;
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()>;
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()>;
}
@@ -0,0 +1,70 @@
use bson::Document;
use revolt_result::Result;
use crate::Emoji;
use crate::MongoDb;
use super::AbstractEmojis;
static COL: &str = "emojis";
#[async_trait]
impl AbstractEmojis for MongoDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
query!(self, insert_one, COL, &emoji).map(|_| ())
}
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
query!(
self,
find_one,
COL,
doc! {
"parent.id": parent_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
query!(
self,
find,
COL,
doc! {
"parent.id": {
"$in": parent_ids
}
}
)
}
/// Detach 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(|_| create_database_error!("update_one", COL))
}
}
@@ -0,0 +1,67 @@
use revolt_result::Result;
use crate::Emoji;
use crate::EmojiParent;
use crate::ReferenceDb;
use super::AbstractEmojis;
#[async_trait]
impl AbstractEmojis for ReferenceDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
let mut emojis = self.emojis.lock().await;
if emojis.contains_key(&emoji.id) {
Err(create_database_error!("insert", "emoji"))
} else {
emojis.insert(emoji.id.to_string(), emoji.clone());
Ok(())
}
}
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
let emojis = self.emojis.lock().await;
emojis
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
let emojis = self.emojis.lock().await;
Ok(emojis
.values()
.filter(|emoji| match &emoji.parent {
EmojiParent::Server { id } => id == parent_id,
_ => false,
})
.cloned()
.collect())
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
let emojis = self.emojis.lock().await;
Ok(emojis
.values()
.filter(|emoji| match &emoji.parent {
EmojiParent::Server { id } => parent_ids.contains(id),
_ => false,
})
.cloned()
.collect())
}
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
let mut emojis = self.emojis.lock().await;
if let Some(bot) = emojis.get_mut(&emoji.id) {
bot.parent = EmojiParent::Detached;
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
+14 -3
View File
@@ -1,10 +1,14 @@
mod admin_migrations;
mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks;
mod channels;
mod emojis;
mod files;
mod messages;
mod ratelimit_events;
mod safety_strikes;
mod server_bans;
mod server_members;
mod servers;
mod user_settings;
@@ -12,11 +16,15 @@ mod users;
pub use admin_migrations::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
pub use channel_webhooks::*;
pub use channels::*;
pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use ratelimit_events::*;
pub use safety_strikes::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
pub use user_settings::*;
@@ -30,10 +38,13 @@ pub trait AbstractDatabase:
+ admin_migrations::AbstractMigrations
+ bots::AbstractBots
+ channels::AbstractChannels
+ channel_invites::AbstractChannelInvites
+ channel_unreads::AbstractChannelUnreads
+ channel_webhooks::AbstractWebhooks
+ emojis::AbstractEmojis
+ files::AbstractAttachments
+ ratelimit_events::AbstractRatelimitEvents
+ safety_strikes::AbstractAccountStrikes
+ server_bans::AbstractServerBans
+ server_members::AbstractServerMembers
+ servers::AbstractServers
+ user_settings::AbstractUserSettings
@@ -1,124 +0,0 @@
use revolt_result::Result;
use crate::Database;
auto_derived_partial!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[serde(rename = "_id")]
pub id: String,
/// Id of reported user
pub user_id: String,
/// Id of moderator
pub moderator_id: String,
/// Attached reason
pub reason: String,
},
"PartialAccountStrike"
);
#[allow(clippy::disallowed_methods)]
impl AccountStrike {
pub async fn create(
db: &Database,
user_id: String,
reason: String,
moderator_id: String,
) -> Result<AccountStrike> {
let strike = AccountStrike {
id: ulid::Ulid::new().to_string(),
user_id,
moderator_id,
reason,
};
db.insert_account_strike(&strike).await?;
Ok(strike)
}
/// Update this strike
pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> {
db.update_account_strike(&self.id, &partial).await?;
self.apply_options(partial);
Ok(())
}
/// Delete this strike
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_account_strike(&self.id).await
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{AccountStrike, PartialAccountStrike};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let user_id = "user";
let strike = AccountStrike::create(
&db,
user_id.to_string(),
"reason 1".to_string(),
"moderator_id".to_string(),
)
.await
.unwrap();
let mut updated_strike = strike.clone();
updated_strike
.update(
&db,
PartialAccountStrike {
reason: Some("new reason".to_string()),
..Default::default()
},
)
.await
.unwrap();
let strike2 = AccountStrike::create(
&db,
user_id.to_string(),
"reason 2".to_string(),
"moderator_id".to_string(),
)
.await
.unwrap();
let strikes = db.fetch_account_strikes_by_user(user_id).await.unwrap();
let ids = strikes
.iter()
.cloned()
.map(|strike| strike.id)
.collect::<HashSet<String>>();
assert!(ids.contains(&strike.id));
assert!(ids.contains(&strike2.id));
let fetched_strike = strikes
.into_iter()
.find(|entry| entry.id == strike.id)
.unwrap();
assert_eq!(fetched_strike, updated_strike);
assert_ne!(fetched_strike, strike);
strike.delete(&db).await.unwrap();
assert_eq!(
1,
db.fetch_account_strikes_by_user(user_id)
.await
.unwrap()
.len()
)
});
}
}
@@ -1,24 +0,0 @@
use revolt_result::Result;
use crate::{AccountStrike, PartialAccountStrike};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAccountStrikes: Sync + Send {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()>;
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike>;
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>>;
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()>;
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()>;
}
@@ -1,55 +0,0 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::MongoDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
static COL: &str = "safety_strikes";
#[async_trait]
impl AbstractAccountStrikes for MongoDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
query!(self, insert_one, COL, &strike).map(|_| ())
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
Ok(self
.col::<AccountStrike>(COL)
.find(
doc! {
"user_id": user_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
@@ -1,60 +0,0 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
#[async_trait]
impl AbstractAccountStrikes for ReferenceDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.contains_key(&strike.id) {
Err(create_database_error!("insert", "strike"))
} else {
strikes.insert(strike.id.to_string(), strike.clone());
Ok(())
}
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
let strikes = self.account_strikes.lock().await;
strikes
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
let strikes = self.account_strikes.lock().await;
Ok(strikes
.values()
.filter(|strike| strike.user_id == user_id)
.cloned()
.collect())
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if let Some(strike) = strikes.get_mut(id) {
strike.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,12 @@
use crate::MemberCompositeKey;
auto_derived!(
/// Server Ban
pub struct ServerBan {
/// Unique member id
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Reason for ban creation
pub reason: Option<String>,
}
);
@@ -0,0 +1,21 @@
use revolt_result::Result;
use crate::{MemberCompositeKey, ServerBan};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractServerBans: Sync + Send {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()>;
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan>;
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>>;
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()>;
}
@@ -0,0 +1,56 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::{MemberCompositeKey, ServerBan};
use super::AbstractServerBans;
static COL: &str = "server_bans";
#[async_trait]
impl AbstractServerBans for MongoDb {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
query!(self, insert_one, COL, &ban).map(|_| ())
}
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
query!(
self,
find_one,
COL,
doc! {
"_id.server": server_id,
"_id.user": user_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
query!(
self,
find,
COL,
doc! {
"_id.server": server_id
}
)
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
query!(
self,
delete_one,
COL,
doc! {
"_id.server": &id.server,
"_id.user": &id.user
}
)
.map(|_| ())
}
}
@@ -0,0 +1,52 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{MemberCompositeKey, ServerBan};
use super::AbstractServerBans;
#[async_trait]
impl AbstractServerBans for ReferenceDb {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
let mut server_bans = self.server_bans.lock().await;
if server_bans.contains_key(&ban.id) {
Err(create_database_error!("insert", "ban"))
} else {
server_bans.insert(ban.id.clone(), ban.clone());
Ok(())
}
}
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
let server_bans = self.server_bans.lock().await;
server_bans
.get(&MemberCompositeKey {
server: server_id.to_string(),
user: user_id.to_string(),
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
let server_bans = self.server_bans.lock().await;
Ok(server_bans
.values()
.filter(|member| member.id.server == server_id)
.cloned()
.collect())
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_bans = self.server_bans.lock().await;
if server_bans.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -4,7 +4,7 @@ use revolt_permissions::OverrideField;
use revolt_result::Result;
use ulid::Ulid;
use crate::{Database, File};
use crate::{events::client::EventV1, Database, File};
auto_derived_partial!(
/// Server
@@ -149,24 +149,24 @@ impl Server {
db.update_server(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::ServerUpdate {
EventV1::ServerUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
data: partial.into(),
clear: remove.into_iter().map(|v| v.into()).collect(),
}
.p(self.id.clone())
.await; */
.await;
Ok(())
}
/// Delete a server
pub async fn delete(self, db: &Database) -> Result<()> {
/* // TODO: EventV1::ServerDelete {
EventV1::ServerDelete {
id: self.id.clone(),
}
.p(self.id.clone())
.await; */
.await;
db.delete_server(&self.id).await
}
@@ -364,14 +364,14 @@ impl Role {
let role_id = Ulid::new().to_string();
db.insert_role(server_id, &role_id, self).await?;
/* // TODO: EventV1::ServerRoleUpdate {
EventV1::ServerRoleUpdate {
id: server_id.to_string(),
role_id: role_id.to_string(),
data: self.clone().into_optional(),
data: self.clone().into_optional().into(),
clear: vec![],
}
.p(server_id.to_string())
.await; */
.await;
Ok(role_id)
}
@@ -394,14 +394,14 @@ impl Role {
db.update_role(server_id, role_id, &partial, remove.clone())
.await?;
/* // TODO: EventV1::ServerRoleUpdate {
EventV1::ServerRoleUpdate {
id: server_id.to_string(),
role_id: role_id.to_string(),
data: partial,
data: partial.into(),
clear: vec![],
}
.p(server_id.to_string())
.await; */
.await;
Ok(())
}
@@ -415,12 +415,12 @@ impl Role {
/// Delete a role
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
/* // TODO: EventV1::ServerRoleDelete {
EventV1::ServerRoleDelete {
id: server_id.to_string(),
role_id: role_id.to_string(),
}
.p(server_id.to_string())
.await; */
.await;
db.delete_role(server_id, role_id).await
}
+343 -12
View File
@@ -1,15 +1,5 @@
use revolt_models::v0::*;
impl From<crate::AccountStrike> for AccountStrike {
fn from(value: crate::AccountStrike) -> Self {
AccountStrike {
id: value.id,
user_id: value.user_id,
reason: value.reason,
}
}
}
impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot {
#[cfg(debug_assertions)]
@@ -44,6 +34,52 @@ impl From<crate::Bot> for Bot {
}
}
impl From<crate::Invite> for Invite {
fn from(value: crate::Invite) -> Self {
match value {
crate::Invite::Group {
code,
creator,
channel,
} => Invite::Group {
code,
creator,
channel,
},
crate::Invite::Server {
code,
server,
creator,
channel,
} => Invite::Server {
code,
server,
creator,
channel,
},
}
}
}
impl From<crate::ChannelUnread> for ChannelUnread {
fn from(value: crate::ChannelUnread) -> Self {
ChannelUnread {
id: value.id.into(),
last_id: value.last_id,
mentions: value.mentions.unwrap_or_default(),
}
}
}
impl From<crate::ChannelCompositeKey> for ChannelCompositeKey {
fn from(value: crate::ChannelCompositeKey) -> Self {
ChannelCompositeKey {
channel: value.channel,
user: value.user,
}
}
}
impl From<crate::Webhook> for Webhook {
fn from(value: crate::Webhook) -> Self {
Webhook {
@@ -52,7 +88,7 @@ impl From<crate::Webhook> for Webhook {
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
permissions: value.permissions
permissions: value.permissions,
}
}
}
@@ -65,7 +101,7 @@ impl From<crate::PartialWebhook> for PartialWebhook {
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
permissions: value.permissions
permissions: value.permissions,
}
}
}
@@ -199,6 +235,28 @@ impl From<crate::FieldsChannel> for FieldsChannel {
}
}
impl From<crate::Emoji> for Emoji {
fn from(value: crate::Emoji) -> Self {
Emoji {
id: value.id,
parent: value.parent.into(),
creator_id: value.creator_id,
name: value.name,
animated: value.animated,
nsfw: value.nsfw,
}
}
}
impl From<crate::EmojiParent> for EmojiParent {
fn from(value: crate::EmojiParent) -> Self {
match value {
crate::EmojiParent::Detached => EmojiParent::Detached,
crate::EmojiParent::Server { id } => EmojiParent::Server { id },
}
}
}
impl From<crate::File> for File {
fn from(value: crate::File) -> Self {
File {
@@ -236,6 +294,279 @@ impl From<crate::Metadata> for Metadata {
}
}
impl From<crate::Message> for Message {
fn from(value: crate::Message) -> Self {
Message {
id: value.id,
nonce: value.nonce,
channel: value.channel,
author: value.author,
webhook: value.webhook,
content: value.content,
system: value.system.map(|system| system.into()),
attachments: value
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions,
interactions: value.interactions.into(),
masquerade: value.masquerade.map(|masq| masq.into()),
}
}
}
impl From<crate::PartialMessage> for PartialMessage {
fn from(value: crate::PartialMessage) -> Self {
PartialMessage {
id: value.id,
nonce: value.nonce,
channel: value.channel,
author: value.author,
webhook: value.webhook,
content: value.content,
system: value.system.map(|system| system.into()),
attachments: value
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions,
interactions: value.interactions.map(|interactions| interactions.into()),
masquerade: value.masquerade.map(|masq| masq.into()),
}
}
}
impl From<crate::SystemMessage> for SystemMessage {
fn from(value: crate::SystemMessage) -> Self {
match value {
crate::SystemMessage::ChannelDescriptionChanged { by } => {
Self::ChannelDescriptionChanged { by }
}
crate::SystemMessage::ChannelIconChanged { by } => Self::ChannelIconChanged { by },
crate::SystemMessage::ChannelOwnershipChanged { from, to } => {
Self::ChannelOwnershipChanged { from, to }
}
crate::SystemMessage::ChannelRenamed { name, by } => Self::ChannelRenamed { name, by },
crate::SystemMessage::Text { content } => Self::Text { content },
crate::SystemMessage::UserAdded { id, by } => Self::UserAdded { id, by },
crate::SystemMessage::UserBanned { id } => Self::UserBanned { id },
crate::SystemMessage::UserJoined { id } => Self::UserJoined { id },
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
}
}
}
impl From<crate::Interactions> for Interactions {
fn from(value: crate::Interactions) -> Self {
Interactions {
reactions: value.reactions,
restrict_reactions: value.restrict_reactions,
}
}
}
impl From<crate::Masquerade> for Masquerade {
fn from(value: crate::Masquerade) -> Self {
Masquerade {
name: value.name,
avatar: value.avatar,
colour: value.colour,
}
}
}
impl From<crate::ServerBan> for ServerBan {
fn from(value: crate::ServerBan) -> Self {
ServerBan {
id: value.id.into(),
reason: value.reason,
}
}
}
impl From<crate::Member> for Member {
fn from(value: crate::Member) -> Self {
Member {
id: value.id.into(),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::PartialMember> for PartialMember {
fn from(value: crate::PartialMember) -> Self {
PartialMember {
id: value.id.map(|id| id.into()),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::MemberCompositeKey> for MemberCompositeKey {
fn from(value: crate::MemberCompositeKey) -> Self {
MemberCompositeKey {
server: value.server,
user: value.user,
}
}
}
impl From<crate::FieldsMember> for FieldsMember {
fn from(value: crate::FieldsMember) -> Self {
match value {
crate::FieldsMember::Avatar => FieldsMember::Avatar,
crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout,
}
}
}
impl From<crate::RemovalIntention> for RemovalIntention {
fn from(value: crate::RemovalIntention) -> Self {
match value {
crate::RemovalIntention::Ban => RemovalIntention::Ban,
crate::RemovalIntention::Kick => RemovalIntention::Kick,
crate::RemovalIntention::Leave => RemovalIntention::Leave,
}
}
}
impl From<crate::Server> for Server {
fn from(value: crate::Server) -> Self {
Server {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.unwrap_or_default() as u32,
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::PartialServer> for PartialServer {
fn from(value: crate::PartialServer) -> Self {
PartialServer {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.map(|roles| roles.into_iter().map(|(k, v)| (k, v.into())).collect()),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.map(|v| v as u32),
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::FieldsServer> for FieldsServer {
fn from(value: crate::FieldsServer) -> Self {
match value {
crate::FieldsServer::Banner => FieldsServer::Banner,
crate::FieldsServer::Categories => FieldsServer::Categories,
crate::FieldsServer::Description => FieldsServer::Description,
crate::FieldsServer::Icon => FieldsServer::Icon,
crate::FieldsServer::SystemMessages => FieldsServer::SystemMessages,
}
}
}
impl From<crate::Category> for Category {
fn from(value: crate::Category) -> Self {
Category {
id: value.id,
title: value.title,
channels: value.channels,
}
}
}
impl From<crate::SystemMessageChannels> for SystemMessageChannels {
fn from(value: crate::SystemMessageChannels) -> Self {
SystemMessageChannels {
user_joined: value.user_joined,
user_left: value.user_left,
user_kicked: value.user_kicked,
user_banned: value.user_banned,
}
}
}
impl From<crate::Role> for Role {
fn from(value: crate::Role) -> Self {
Role {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::PartialRole> for PartialRole {
fn from(value: crate::PartialRole) -> Self {
PartialRole {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::FieldsRole> for FieldsRole {
fn from(value: crate::FieldsRole) -> Self {
match value {
crate::FieldsRole::Colour => FieldsRole::Colour,
}
}
}
impl crate::User {
pub async fn into<P>(self, perspective: P) -> User
where
+10 -8
View File
@@ -1,28 +1,30 @@
[package]
name = "revolt-models"
version = "0.6.5"
version = "0.6.6"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: API Models"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
serde = [ "dep:serde", "revolt-permissions/serde" ]
schemas = [ "dep:schemars", "revolt-permissions/schemas" ]
validator = [ "dep:validator" ]
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ]
serde = ["dep:serde", "revolt-permissions/serde"]
schemas = ["dep:schemars", "revolt-permissions/schemas"]
validator = ["dep:validator"]
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
default = [ "serde", "partials" ]
default = ["serde", "partials"]
[dependencies]
# Core
revolt-permissions = { version = "0.6.5", path = "../permissions" }
revolt-permissions = { version = "0.6.6", path = "../permissions" }
# Serialisation
indexmap = "1.9.3"
revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
# Spec Generation
schemars = { version = "0.8.8", optional = true }
@@ -1,28 +0,0 @@
auto_derived!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// New strike information
pub struct DataCreateStrike {
/// Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// New strike information
pub struct DataEditAccountStrike {
/// New attached reason
pub reason: String,
}
);
+3 -3
View File
@@ -65,16 +65,16 @@ auto_derived!(
/// Public Bot
pub struct PublicBot {
/// Bot Id
#[serde(rename = "_id")]
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// Bot Username
pub username: String,
/// Profile Avatar
#[serde(skip_serializing_if = "String::is_empty")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub avatar: String,
/// Profile Description
#[serde(skip_serializing_if = "String::is_empty")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub description: String,
}
@@ -0,0 +1,27 @@
auto_derived!(
/// Invite
pub enum Invite {
/// Invite to a specific server channel
Server {
/// Invite code
#[cfg_attr(feature = "serde", 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
#[cfg_attr(feature = "serde", 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,
},
}
);
@@ -0,0 +1,27 @@
auto_derived!(
/// Channel Unread
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
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub last_id: Option<String>,
/// Array of message ids that mention the user
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Vec::is_empty", default)
)]
pub mentions: Vec<String>,
}
/// Composite primary key consisting of channel and user id
#[derive(Hash)]
pub struct ChannelCompositeKey {
/// Channel Id
pub channel: String,
/// User Id
pub user: String,
}
);
+160
View File
@@ -0,0 +1,160 @@
use super::File;
auto_derived!(
/// Image positioning and size
pub enum ImageSize {
/// Show large preview at the bottom of the embed
Large,
/// Show small preview to the side of the embed
Preview,
}
/// Image
pub struct Image {
/// URL to the original image
pub url: String,
/// Width of the image
pub width: isize,
/// Height of the image
pub height: isize,
/// Positioning and size
pub size: ImageSize,
}
/// Video
pub struct Video {
/// URL to the original video
pub url: String,
/// Width of the video
pub width: isize,
/// Height of the video
pub height: isize,
}
/// Type of remote Twitch content
pub enum TwitchType {
Channel,
Video,
Clip,
}
/// Type of remote Lightspeed.tv content
pub enum LightspeedType {
Channel,
}
/// Type of remote Bandcamp content
pub enum BandcampType {
Album,
Track,
}
/// Information about special remote content
#[serde(tag = "type")]
pub enum Special {
/// No remote content
None,
/// Content hint that this contains a GIF
///
/// Use metadata to find video or image to play
GIF,
/// YouTube video
YouTube {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
timestamp: Option<String>,
},
/// Lightspeed.tv stream
Lightspeed {
content_type: LightspeedType,
id: String,
},
/// Twitch stream or clip
Twitch {
content_type: TwitchType,
id: String,
},
/// Spotify track
Spotify { content_type: String, id: String },
/// Soundcloud track
Soundcloud,
/// Bandcamp track
Bandcamp {
content_type: BandcampType,
id: String,
},
/// Streamable Video
Streamable { id: String },
}
/// Website metadata
pub struct WebsiteMetadata {
/// Direct URL to web page
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
/// Original direct URL
#[serde(skip_serializing_if = "Option::is_none")]
original_url: Option<String>,
/// Remote content
#[serde(skip_serializing_if = "Option::is_none")]
special: Option<Special>,
/// Title of website
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
/// Description of website
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Embedded image
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<Image>,
/// Embedded video
#[serde(skip_serializing_if = "Option::is_none")]
video: Option<Video>,
// #[serde(skip_serializing_if = "Option::is_none")]
// opengraph_type: Option<String>,
/// Site name
#[serde(skip_serializing_if = "Option::is_none")]
site_name: Option<String>,
/// URL to site icon
#[serde(skip_serializing_if = "Option::is_none")]
icon_url: Option<String>,
/// CSS Colour
#[serde(skip_serializing_if = "Option::is_none")]
colour: Option<String>,
}
/// Text Embed
pub struct Text {
/// URL to icon
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
/// URL for title
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Title of text embed
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Description of text embed
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// ID of uploaded autumn file
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<File>,
/// CSS Colour
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
}
/// Embed
#[serde(tag = "type")]
pub enum Embed {
Website(WebsiteMetadata),
Image(Image),
Video(Video),
Text(Text),
None,
}
);
+33
View File
@@ -0,0 +1,33 @@
auto_derived!(
/// Emoji
pub struct Emoji {
/// Unique Id
#[cfg_attr(feature = "serde", 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
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub animated: bool,
/// Whether the emoji is marked as nsfw
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub nsfw: bool,
}
/// Parent Id of the emoji
#[serde(tag = "type")]
pub enum EmojiParent {
Server { id: String },
Detached,
}
);
+8 -8
View File
@@ -2,7 +2,7 @@ auto_derived!(
/// File
pub struct File {
/// Unique Id
#[serde(rename = "_id")]
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// Tag / bucket this file was uploaded to
pub tag: String,
@@ -16,29 +16,29 @@ auto_derived!(
pub size: isize,
/// Whether this file was deleted
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub deleted: Option<bool>,
/// Whether this file was reported
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub reported: Option<bool>,
// TODO: migrate this mess to having:
// - author_id
// - parent: Parent { Message(id), User(id), etc }
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", 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")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub object_id: Option<String>,
}
/// Metadata associated with a file
#[serde(tag = "type")]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
#[derive(Default)]
pub enum Metadata {
/// File is just a generic uncategorised file
+139
View File
@@ -0,0 +1,139 @@
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use super::{Embed, File, MessageWebhook};
auto_derived_partial!(
/// Message
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>,
},
"PartialMessage"
);
auto_derived!(
/// System Event
#[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
pub struct Masquerade {
/// Replace the display name shown on this message
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Replace the avatar shown on this message (URL to image file)
#[serde(skip_serializing_if = "Option::is_none")]
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")]
pub colour: Option<String>,
}
/// Information to guide interactions on this message
#[derive(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 = "crate::if_false", default)]
pub restrict_reactions: bool,
}
/// Appended Information
pub struct AppendMessage {
/// Additional embeds to include in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
}
/// Message Sort
///
/// Sort used for retrieving messages
#[derive(Default)]
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,
}
);
impl Interactions {
/// Check if default initialisation of fields
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}
+18 -2
View File
@@ -1,13 +1,29 @@
mod account_strikes;
mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks;
mod channels;
mod embeds;
mod emojis;
mod files;
mod messages;
mod server_bans;
mod server_members;
mod servers;
mod user_settings;
mod users;
pub use account_strikes::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
pub use channel_webhooks::*;
pub use channels::*;
pub use embeds::*;
pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
pub use user_settings::*;
pub use users::*;
+12
View File
@@ -0,0 +1,12 @@
use super::MemberCompositeKey;
auto_derived!(
/// Server Ban
pub struct ServerBan {
/// Unique member id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: MemberCompositeKey,
/// Reason for ban creation
pub reason: Option<String>,
}
);
@@ -0,0 +1,60 @@
use super::File;
use iso8601_timestamp::Timestamp;
auto_derived_partial!(
/// Server Member
pub struct Member {
/// Unique member id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: MemberCompositeKey,
/// Time at which this user joined the server
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub joined_at: Option<Timestamp>,
/// Member's nickname
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub nickname: Option<String>,
/// Avatar attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub avatar: Option<File>,
/// Member's roles
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Vec::is_empty", default)
)]
pub roles: Vec<String>,
/// Timestamp this member is timed out until
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub timeout: Option<Timestamp>,
},
"PartialMember"
);
auto_derived!(
/// Composite primary key consisting of server and user id
#[derive(Hash, Default)]
pub struct MemberCompositeKey {
/// Server Id
pub server: String,
/// User Id
pub user: String,
}
/// Optional fields on server member object
pub enum FieldsMember {
Nickname,
Avatar,
Roles,
Timeout,
}
/// Member removal intention
pub enum RemovalIntention {
Leave,
Kick,
Ban,
}
);
+144
View File
@@ -0,0 +1,144 @@
use super::File;
use revolt_permissions::OverrideField;
use std::collections::HashMap;
auto_derived_partial!(
/// Server
pub struct Server {
/// Unique Id
#[cfg_attr(feature = "serde", 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
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<String>,
/// Channels within this server
// ! FIXME: this may be redundant
pub channels: Vec<String>,
/// Categories for this server
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub categories: Option<Vec<Category>>,
/// Configuration for sending system event messages
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub system_messages: Option<SystemMessageChannels>,
/// Roles for this server
#[cfg_attr(
feature = "serde",
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
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub icon: Option<File>,
/// Banner attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub banner: Option<File>,
/// Bitfield of server flags
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
)]
pub flags: u32,
/// Whether this server is flagged as not safe for work
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub nsfw: bool,
/// Whether to enable analytics
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub analytics: bool,
/// Whether this server should be publicly discoverable
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub discoverable: bool,
},
"PartialServer"
);
auto_derived_partial!(
/// Role
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
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub colour: Option<String>,
/// Whether this role should be shown separately on the member sidebar
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub hoist: bool,
/// Ranking of this role
#[cfg_attr(feature = "serde", serde(default))]
pub rank: i64,
},
"PartialRole"
);
auto_derived!(
/// Optional fields on server object
pub enum FieldsServer {
Description,
Categories,
SystemMessages,
Icon,
Banner,
}
/// Optional fields on server object
pub enum FieldsRole {
Colour,
}
/// Channel category
pub struct Category {
/// Unique ID for this category
pub id: String,
/// Title for this category
pub title: String,
/// Channels in this category
pub channels: Vec<String>,
}
/// System message channel assignments
pub struct SystemMessageChannels {
/// ID of channel to send user join messages in
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_joined: Option<String>,
/// ID of channel to send user left messages in
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_left: Option<String>,
/// ID of channel to send user kicked messages in
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_kicked: Option<String>,
/// ID of channel to send user banned messages in
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_banned: Option<String>,
}
);
@@ -0,0 +1,6 @@
use std::collections::HashMap;
/// HashMap of user settings
/// Each key is mapped to a tuple consisting of the
/// revision timestamp and serialised data (in JSON format)
pub type UserSettings = HashMap<String, (i64, String)>;
+37 -16
View File
@@ -4,40 +4,52 @@ auto_derived!(
/// User
pub struct User {
/// Unique Id
#[serde(rename = "_id")]
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// Username
pub username: String,
/// Discriminator
pub discriminator: String,
/// Display name
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
/// Avatar attachment
pub avatar: Option<File>,
/// Relationships with other users
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Vec::is_empty", default)
)]
pub relations: Vec<Relationship>,
/// Bitfield of user badges
#[serde(skip_serializing_if = "crate::if_zero_u32", default)]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
)]
pub badges: u32,
/// User's current status
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub status: Option<UserStatus>,
/// User's profile page
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub profile: Option<UserProfile>,
/// Enum of user flags
#[serde(skip_serializing_if = "crate::if_zero_u32", default)]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
)]
pub flags: u32,
/// Whether this user is privileged
#[serde(skip_serializing_if = "crate::if_false", default)]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub privileged: bool,
/// Bot information
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub bot: Option<BotInformation>,
/// Current session user's relationship with this user
@@ -49,20 +61,29 @@ auto_derived!(
/// User's relationship with another user (or themselves)
#[derive(Default)]
pub enum RelationshipStatus {
/// No relationship with other user
#[default]
None,
/// Other user is us
User,
/// Friends with the other user
Friend,
/// Pending friend request to user
Outgoing,
/// Incoming friend request from user
Incoming,
/// Blocked this user
Blocked,
/// Blocked by this user
BlockedOther,
}
/// Relationship entry indicating current status with other user
pub struct Relationship {
#[serde(rename = "_id")]
/// Other user's Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub user_id: String,
/// Relationship status with them
pub status: RelationshipStatus,
}
@@ -83,20 +104,20 @@ auto_derived!(
/// User's active status
pub struct UserStatus {
/// Custom status text
#[serde(skip_serializing_if = "String::is_empty")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub text: String,
/// Current presence option
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub presence: Option<Presence>,
}
/// User's profile
pub struct UserProfile {
/// Text content on user's profile
#[serde(skip_serializing_if = "String::is_empty")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub content: String,
/// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub background: Option<File>,
}
@@ -143,7 +164,7 @@ auto_derived!(
/// Bot information for if the user is a bot
pub struct BotInformation {
/// Id of the owner of this bot
#[serde(rename = "owner")]
#[cfg_attr(feature = "serde", serde(rename = "owner"))]
pub owner_id: String,
}
);
+7 -7
View File
@@ -1,18 +1,18 @@
[package]
name = "revolt-permissions"
version = "0.6.5"
version = "0.6.6"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Permission Logic"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
bson = ["dep:bson"]
serde = [ "dep:serde" ]
schemas = [ "dep:schemars" ]
try-from-primitive = [ "dep:num_enum" ]
serde = ["dep:serde"]
schemas = ["dep:schemars"]
try-from-primitive = ["dep:num_enum"]
[dev-dependencies]
@@ -30,7 +30,7 @@ async-trait = "0.1.51"
# Serialisation
serde = { version = "1", features = ["derive"], optional = true }
bson = { version = "2.1.0", optional = true}
bson = { version = "2.1.0", optional = true }
# Spec Generation
schemars = { version = "0.8.8", optional = true }
schemars = { version = "0.8.8", optional = true }
+2 -2
View File
@@ -1,9 +1,9 @@
[package]
name = "revolt-presence"
version = "0.6.5"
version = "0.6.6"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: User Presence"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+7 -7
View File
@@ -1,20 +1,20 @@
[package]
name = "revolt-result"
version = "0.6.5"
version = "0.6.6"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Result and Error types"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
serde = [ "dep:serde" ]
schemas = [ "dep:schemars" ]
rocket = [ "dep:rocket", "dep:serde_json" ]
okapi = [ "dep:revolt_rocket_okapi", "dep:revolt_okapi" ]
serde = ["dep:serde"]
schemas = ["dep:schemars"]
rocket = ["dep:rocket", "dep:serde_json"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi"]
default = [ "serde" ]
default = ["serde"]
[dependencies]
# Serialisation
+17 -8
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.6.5"
version = "0.6.6"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -38,29 +38,38 @@ futures = "0.3.8"
chrono = "0.4.15"
async-channel = "1.6.1"
reqwest = { version = "0.11.4", features = ["json"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
async-std = { version = "1.8.0", features = [
"tokio1",
"tokio02",
"attributes",
] }
# internal util
lettre = "0.10.0-alpha.4"
# web
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
"json",
] }
rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_authifier = { version = "1.0.7" }
rocket_prometheus = "0.10.0-rc.3"
# spec generation
schemars = "0.8.8"
revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
# quark
revolt-quark = { path = "../quark" }
# core
revolt-database = { path = "../core/database", features = [ "rocket-impl", "redis-is-patched" ] }
revolt-models = { path = "../core/models", features = [ "schemas", "validator" ] }
revolt-result = { path = "../core/result", features = [ "rocket", "okapi" ] }
revolt-permissions = { path = "../core/permissions", features = [ "schemas" ] }
revolt-database = { path = "../core/database", features = [
"rocket-impl",
"redis-is-patched",
] }
revolt-models = { path = "../core/models", features = ["schemas", "validator"] }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
[build-dependencies]
vergen = "7.5.0"
@@ -1,31 +0,0 @@
use revolt_quark::{
models::{
message::{BulkMessageResponse, MessageQuery},
User,
},
Db, Error, Result,
};
use rocket::serde::json::Json;
/// # Globally Fetch Messages
///
/// This is a privileged route to globally fetch messages.
#[openapi(tag = "Admin")]
#[post("/messages", data = "<data>")]
pub async fn message_query(
db: &Db,
user: User,
data: Json<MessageQuery>,
) -> Result<Json<BulkMessageResponse>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
// Fetch data using query
let data = data.into_inner();
let messages = db.fetch_messages(data).await?;
BulkMessageResponse::transform(db, None, messages, Some(true))
.await
.map(Json)
}
+1 -2
View File
@@ -1,9 +1,8 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod message_query;
mod stats;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![stats::stats, message_query::message_query]
openapi_get_routes_spec![stats::stats]
}
@@ -1,34 +0,0 @@
use revolt_database::{AccountStrike, Database};
use revolt_models::v0::{AccountStrike as AccountStrikeModel, DataCreateStrike};
use revolt_quark::models::User;
use revolt_quark::{Error, Result};
use rocket::serde::json::Json;
use rocket::State;
/// # Create Strike
///
/// Create a new account strike
#[openapi(tag = "User Safety")]
#[post("/strikes", data = "<data>")]
pub async fn create_strike(
db: &State<Database>,
user: User,
data: Json<DataCreateStrike>,
) -> Result<Json<AccountStrikeModel>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
let data = data.into_inner();
let target = db
.fetch_user(&data.user_id)
.await
.map_err(Error::from_core)?;
AccountStrike::create(db, target.id, data.reason, user.id)
.await
.map(|strike| strike.into())
.map(Json)
.map_err(Error::from_core)
}
@@ -1,23 +0,0 @@
use revolt_database::Database;
use revolt_quark::models::User;
use revolt_quark::{Error, Result};
use rocket::State;
/// # Delete Strike
///
/// Delete a strike by its ID
#[openapi(tag = "User Safety")]
#[delete("/strikes/<strike_id>")]
pub async fn delete_strike(db: &State<Database>, user: User, strike_id: String) -> Result<()> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
let strike = db
.fetch_account_strike(&strike_id)
.await
.map_err(Error::from_core)?;
strike.delete(db).await.map_err(Error::from_core)
}
@@ -1,56 +0,0 @@
use revolt_quark::{
models::{
report::{PartialReport, ReportStatus},
Report, User,
},
Db, Error, Ref, Result,
};
use rocket::serde::json::Json;
use serde::Deserialize;
use validator::Validate;
/// # Report Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataEditReport {
/// New report status
status: Option<ReportStatus>,
/// Report notes
notes: Option<String>,
}
/// # Edit Report
///
/// Edit a report.
#[openapi(tag = "User Safety")]
#[patch("/reports/<report>", data = "<edit>")]
pub async fn edit_report(
db: &Db,
user: User,
report: Ref,
edit: Json<DataEditReport>,
) -> Result<Json<Report>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
// Validate data
let edit = edit.into_inner();
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
// Create and apply update to report
let mut report = report.as_report(db).await?;
report
.update(
db,
PartialReport {
status: edit.status,
notes: edit.notes,
..Default::default()
},
)
.await?;
Ok(Json(report))
}
@@ -1,39 +0,0 @@
use revolt_database::{Database, PartialAccountStrike};
use revolt_models::v0::DataEditAccountStrike;
use revolt_quark::models::User;
use revolt_quark::{Error, Result};
use rocket::serde::json::Json;
use rocket::State;
/// # Edit Strike
///
/// Edit a strike by its ID
#[openapi(tag = "User Safety")]
#[post("/strikes/<strike_id>", data = "<data>")]
pub async fn edit_strike(
db: &State<Database>,
user: User,
strike_id: String,
data: Json<DataEditAccountStrike>,
) -> Result<()> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
let mut strike = db
.fetch_account_strike(&strike_id)
.await
.map_err(Error::from_core)?;
strike
.update(
db,
PartialAccountStrike {
reason: Some(data.0.reason),
..Default::default()
},
)
.await
.map_err(Error::from_core)
}
@@ -1,17 +0,0 @@
use revolt_quark::models::{Report, User};
use revolt_quark::{Db, Error, Result};
use rocket::serde::json::Json;
/// # Fetch Report
///
/// Fetch a report by its ID
#[openapi(tag = "User Safety")]
#[get("/report/<id>")]
pub async fn fetch_report(db: &Db, user: User, id: String) -> Result<Json<Report>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
db.fetch_report(&id).await.map(Json)
}
@@ -1,61 +0,0 @@
use revolt_quark::models::report::{ReportStatus, ReportStatusString, ReportedContent};
use revolt_quark::models::{Report, User};
use revolt_quark::{Db, Error, Result};
use rocket::serde::json::Json;
use serde::Deserialize;
/// # Query Parameters
#[derive(Deserialize, JsonSchema, FromForm)]
pub struct OptionsFetchReports {
/// Find reports against messages, servers, or users
content_id: Option<String>,
/// Find reports created by user
author_id: Option<String>,
/// Report status to include in search
status: Option<ReportStatusString>,
}
/// # Fetch Reports
///
/// Fetch all available reports
#[openapi(tag = "User Safety")]
#[get("/reports?<options..>")]
pub async fn fetch_reports(
db: &Db,
user: User,
options: OptionsFetchReports,
) -> Result<Json<Vec<Report>>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
let mut reports = db.fetch_reports().await?;
if let Some(content_id) = options.content_id {
reports.retain(|report| match &report.content {
ReportedContent::Message { id, .. }
| ReportedContent::Server { id, .. }
| ReportedContent::User { id, .. } => id == &content_id,
});
}
if let Some(author_id) = options.author_id {
reports.retain(|report| report.author_id == author_id);
}
if let Some(status) = options.status {
reports.retain(|report| {
matches!(
(&status, &report.status),
(ReportStatusString::Created, ReportStatus::Created { .. })
| (ReportStatusString::Rejected, ReportStatus::Rejected { .. })
| (ReportStatusString::Resolved, ReportStatus::Resolved { .. })
)
});
}
Ok(Json(reports))
}
@@ -1,87 +0,0 @@
use std::collections::HashSet;
use revolt_quark::models::snapshot::{SnapshotContent, SnapshotWithContext};
use revolt_quark::models::{Channel, User};
use revolt_quark::{Db, Error, Result};
use rocket::serde::json::Json;
/// # Fetch Snapshots
///
/// Fetch a snapshots for a given report
#[openapi(tag = "User Safety")]
#[get("/snapshot/<report_id>")]
pub async fn fetch_snapshots(
db: &Db,
user: User,
report_id: String,
) -> Result<Json<Vec<SnapshotWithContext>>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
// Fetch snapshots
let snapshots = db.fetch_snapshots(&report_id).await?;
let mut result = vec![];
for snapshot in snapshots {
// Resolve and fetch IDs of associated content
let mut user_ids: HashSet<&str> = HashSet::new();
let mut channel_ids: HashSet<&str> = HashSet::new();
match &snapshot.content {
SnapshotContent::Message {
prior_context,
leading_context,
message,
} => {
for msg in prior_context {
user_ids.insert(&msg.author);
}
for msg in leading_context {
user_ids.insert(&msg.author);
}
user_ids.insert(&message.author);
channel_ids.insert(&message.channel);
}
SnapshotContent::User(user) => {
user_ids.insert(&user.id);
}
SnapshotContent::Server(server) => {
for channel in &server.channels {
channel_ids.insert(channel);
}
}
}
// Collect user and channel IDs
let user_ids: Vec<String> = user_ids.into_iter().map(|s| s.to_owned()).collect();
let channel_ids: Vec<String> = channel_ids.into_iter().map(|s| s.to_owned()).collect();
// Fetch users and channels
let users = db.fetch_users(&user_ids).await?;
let channels = db.fetch_channels(&channel_ids).await?;
// Pull out first server from channels if possible
let server = if let Some(server_id) = channels.iter().find_map(|channel| match channel {
Channel::TextChannel { server, .. } => Some(server),
_ => None,
}) {
Some(db.fetch_server(server_id).await?)
} else {
None
};
// Return snapshot with context
result.push(SnapshotWithContext {
snapshot,
users,
channels,
server,
});
}
Ok(Json(result))
}
@@ -1,28 +0,0 @@
use revolt_database::Database;
use revolt_models::v0::AccountStrike;
use revolt_quark::models::User;
use revolt_quark::{Error, Result};
use rocket::serde::json::Json;
use rocket::State;
/// # Fetch Strikes
///
/// Fetch strikes for a user by their ID
#[openapi(tag = "User Safety")]
#[get("/strikes/<user_id>")]
pub async fn fetch_strikes(
db: &State<Database>,
user: User,
user_id: String,
) -> Result<Json<Vec<AccountStrike>>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
db.fetch_account_strikes_by_user(&user_id)
.await
.map(|v| v.into_iter().map(|e| e.into()).collect())
.map(Json)
.map_err(Error::from_core)
}
-20
View File
@@ -1,31 +1,11 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod edit_report;
mod fetch_report;
mod fetch_reports;
mod report_content;
mod fetch_snapshots;
mod create_strike;
mod delete_strike;
mod edit_strike;
mod fetch_strikes;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
// Reports
edit_report::edit_report,
fetch_report::fetch_report,
fetch_reports::fetch_reports,
report_content::report_content,
// Snapshots
fetch_snapshots::fetch_snapshots,
// Strikes
create_strike::create_strike,
fetch_strikes::fetch_strikes,
edit_strike::edit_strike,
delete_strike::delete_strike
]
}
+14 -12
View File
@@ -1,13 +1,13 @@
[package]
name = "revolt-quark"
version = "0.6.5"
version = "0.6.6"
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" ]
mongo = ["mongodb"]
rocket_impl = [
"rocket",
"rocket_empty",
@@ -18,11 +18,11 @@ rocket_impl = [
"authifier/database-mongodb",
"authifier/rocket_impl",
"authifier/okapi_impl"
"authifier/okapi_impl",
]
test = [ "async-std", "mongo", "mongodb/async-std-runtime", "rocket_impl" ]
default = [ "test" ]
test = ["async-std", "mongo", "mongodb/async-std-runtime", "rocket_impl"]
default = ["test"]
[dependencies]
# Serialisation
@@ -39,7 +39,7 @@ 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" ] }
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
# Databases
redis-kiss = { version = "0.1.4" }
@@ -81,18 +81,20 @@ 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 = { 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.7", features = [ "async-std-runtime" ] }
authifier = { version = "1.0.7", features = ["async-std-runtime"] }
# Sentry
sentry = "0.25.0"
sentry = "0.31.5"
# Core
revolt-result = { path = "../core/result", features = [ "serde", "schemas" ] }
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }
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" }
@@ -269,7 +269,6 @@ pub struct DataMessageSend {
#[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>>,
+12 -8
View File
@@ -1,5 +1,5 @@
/// Configure logging and common Rust variables
pub fn setup_logging(release: &'static str) -> sentry::ClientInitGuard {
pub fn setup_logging(release: &'static str) -> Option<sentry::ClientInitGuard> {
dotenv::dotenv().ok();
if std::env::var("RUST_LOG").is_err() {
@@ -13,13 +13,17 @@ pub fn setup_logging(release: &'static str) -> sentry::ClientInitGuard {
pretty_env_logger::init();
info!("Starting {release}");
sentry::init((
"https://d1d2a6f15c6245a987c532bbbcb30a04@glitchtip.insert.moe/2",
sentry::ClientOptions {
release: Some(release.into()),
..Default::default()
},
))
if let Ok(dsn) = std::env::var("SENTRY_DSN") {
Some(sentry::init((
dsn,
sentry::ClientOptions {
release: Some(release.into()),
..Default::default()
},
)))
} else {
None
}
}
#[macro_export]