Compare commits
67
Commits
20230515-1
...
20230611-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8deec1f80a | ||
|
|
26afbeed84 | ||
|
|
9975de01bc | ||
|
|
ac525466b8 | ||
|
|
c7a04e4559 | ||
|
|
5bbe30edbc | ||
|
|
31c7dc0577 | ||
|
|
aba5c7d8af | ||
|
|
2f4ea4cabb | ||
|
|
7e801a31bd | ||
|
|
eb4870c9d5 | ||
|
|
b1fdd7fc74 | ||
|
|
bf3e027067 | ||
|
|
a7a7e49e18 | ||
|
|
3c834d06d4 | ||
|
|
f5e6086d2a | ||
|
|
3365a8e482 | ||
|
|
268c2d1355 | ||
|
|
e9d6b963df | ||
|
|
f9d9447203 | ||
|
|
a0002d0b43 | ||
|
|
23188032ca | ||
|
|
f9f5a30e2c | ||
|
|
e393e17b59 | ||
|
|
a29d8f6aab | ||
|
|
1dbbc3ed8d | ||
|
|
410acb5b5c | ||
|
|
53c9160cd6 | ||
|
|
28b1170633 | ||
|
|
4abd4070f7 | ||
|
|
bca17b11a7 | ||
|
|
2a6d532852 | ||
|
|
a9a5af8cc8 | ||
|
|
edfa8e5256 | ||
|
|
2ebcfdc770 | ||
|
|
fc47df786b | ||
|
|
f6aa405607 | ||
|
|
101e8feaa2 | ||
|
|
df8f588e15 | ||
|
|
3ef549537e | ||
|
|
d81d08f1ce | ||
|
|
a77b7717b8 | ||
|
|
d5f903781d | ||
|
|
175866ce00 | ||
|
|
8c5de18991 | ||
|
|
57f00a283b | ||
|
|
312e1dd41f | ||
|
|
426d836a09 | ||
|
|
2fad66d24a | ||
|
|
83429f9f65 | ||
|
|
a60ae78176 | ||
|
|
7b39f4e9d9 | ||
|
|
01577fd798 | ||
|
|
fca91e1560 | ||
|
|
3c881a40a6 | ||
|
|
c7849a82bf | ||
|
|
781d51df6f | ||
|
|
758b1e43e7 | ||
|
|
5cc8231c36 | ||
|
|
7b447faefa | ||
|
|
a017256e95 | ||
|
|
1050891d87 | ||
|
|
a246cff7f0 | ||
|
|
c218db5047 | ||
|
|
c7f2f3614f | ||
|
|
c826d8cf07 | ||
|
|
5cb2320760 |
Generated
+54
-34
@@ -2837,7 +2837,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.5.20"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-tungstenite",
|
||||
@@ -2854,7 +2854,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-database"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"async-std",
|
||||
@@ -2867,7 +2867,12 @@ dependencies = [
|
||||
"mongodb",
|
||||
"nanoid",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"redis-kiss",
|
||||
"regex",
|
||||
"revolt-models",
|
||||
"revolt-permissions",
|
||||
"revolt-presence",
|
||||
"revolt-result",
|
||||
"revolt_optional_struct",
|
||||
"rocket",
|
||||
@@ -2875,11 +2880,12 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ulid 1.0.0",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-delta"
|
||||
version = "0.5.20"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-std",
|
||||
@@ -2901,7 +2907,9 @@ dependencies = [
|
||||
"reqwest",
|
||||
"revolt-database",
|
||||
"revolt-models",
|
||||
"revolt-permissions",
|
||||
"revolt-quark",
|
||||
"revolt-result",
|
||||
"revolt_rocket_okapi",
|
||||
"rocket",
|
||||
"rocket_authifier",
|
||||
@@ -2911,27 +2919,29 @@ dependencies = [
|
||||
"serde_json",
|
||||
"ulid 0.4.1",
|
||||
"url",
|
||||
"validator 0.14.0",
|
||||
"validator 0.16.0",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-models"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"revolt-database",
|
||||
"revolt-presence",
|
||||
"revolt-permissions",
|
||||
"revolt_optional_struct",
|
||||
"schemars",
|
||||
"serde",
|
||||
"validator 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-permissions"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"auto_ops",
|
||||
"bson",
|
||||
"num_enum 0.6.1",
|
||||
"once_cell",
|
||||
"schemars",
|
||||
@@ -2940,7 +2950,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-presence"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"log",
|
||||
@@ -2951,7 +2961,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-quark"
|
||||
version = "0.5.20"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"async-recursion",
|
||||
@@ -2981,6 +2991,7 @@ dependencies = [
|
||||
"redis-kiss",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"revolt-models",
|
||||
"revolt-presence",
|
||||
"revolt-result",
|
||||
"revolt_okapi",
|
||||
@@ -2995,16 +3006,20 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ulid 0.5.0",
|
||||
"validator 0.14.0",
|
||||
"validator 0.16.0",
|
||||
"web-push",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-result"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi",
|
||||
"rocket",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4315,6 +4330,12 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.0.4"
|
||||
@@ -4378,23 +4399,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d0f08911ab0fee2c5009580f04615fa868898ee57de10692a45da0c3bcc3e5e"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"url",
|
||||
"validator_derive",
|
||||
"validator_types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.15.0"
|
||||
@@ -4411,10 +4415,26 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_derive"
|
||||
version = "0.14.0"
|
||||
name = "validator"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d85135714dba11a1bd0b3eb1744169266f1a38977bf4e3ff5e2e1acb8c2b7eee"
|
||||
checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"url",
|
||||
"validator_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_derive"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af"
|
||||
dependencies = [
|
||||
"if_chain",
|
||||
"lazy_static",
|
||||
@@ -4428,9 +4448,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "validator_types"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded9d97e1d42327632f5f3bae6403c04886e2de3036261ef42deebd931a6a291"
|
||||
checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 1.0.107",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM --platform="${BUILDPLATFORM}" rustlang/rust:nightly-slim
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
|
||||
+9
-1
@@ -2,6 +2,14 @@ disallowed-methods = [
|
||||
# Shouldn't need to access these directly
|
||||
"revolt_database::models::bots::model::Bot::remove_field",
|
||||
|
||||
# Prefer to use Object::delete()
|
||||
# Prefer to use Object::create()
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::insert_account_strike",
|
||||
|
||||
# Prefer to use Object::update(&self)
|
||||
"revolt_database::models::bots::ops::AbstractBots::update_bot",
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
|
||||
|
||||
# Prefer to use Object::delete(&self)
|
||||
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.5.20"
|
||||
version = "0.6.0"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
@@ -15,17 +15,21 @@ mongodb = [ "dep:mongodb", "bson" ]
|
||||
# ... Other
|
||||
async-std-runtime = [ "async-std" ]
|
||||
rocket-impl = [ "rocket", "schemars" ]
|
||||
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
|
||||
|
||||
# Default Features
|
||||
default = [ "mongodb", "async-std-runtime" ]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.0.2", path = "../result" }
|
||||
revolt-permissions = { version = "0.0.2", path = "../permissions", features = [ "serde" ] }
|
||||
revolt-result = { version = "0.6.0", path = "../result" }
|
||||
revolt-models = { version = "0.6.0", path = "../models" }
|
||||
revolt-presence = { version = "0.6.0", path = "../presence" }
|
||||
revolt-permissions = { version = "0.6.0", path = "../permissions", features = [ "serde", "bson" ] }
|
||||
|
||||
# Utility
|
||||
log = "0.4"
|
||||
rand = "0.8.5"
|
||||
ulid = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
once_cell = "1.17"
|
||||
@@ -36,10 +40,17 @@ revolt_optional_struct = "0.2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
|
||||
|
||||
# Events
|
||||
redis-kiss = { version = "0.1.4" }
|
||||
|
||||
# Database
|
||||
bson = { optional = true, version = "2.1.0" }
|
||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
||||
|
||||
# Database Migration
|
||||
unicode-segmentation = "1.10.1"
|
||||
regex = "1"
|
||||
|
||||
# Async Language Features
|
||||
futures = "0.3.19"
|
||||
async-trait = "0.1.51"
|
||||
|
||||
@@ -2,25 +2,29 @@ use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{Bot, File, Member, MemberCompositeKey, Server, User, UserSettings};
|
||||
use crate::{
|
||||
AccountStrike, Bot, Channel, File, Member, MemberCompositeKey, Server, 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 channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
|
||||
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
||||
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, ()>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
use authifier::AuthifierEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
Channel, FieldsChannel, FieldsWebhook, PartialChannel, PartialWebhook, Webhook,
|
||||
};
|
||||
use revolt_result::Error;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
/// WebSocket Client Errors
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "error")]
|
||||
pub enum WebSocketError {
|
||||
LabelMe,
|
||||
InternalError { at: String },
|
||||
InvalidSession,
|
||||
OnboardingNotFinished,
|
||||
AlreadyAuthenticated,
|
||||
MalformedData { msg: String },
|
||||
}
|
||||
|
||||
/// Ping Packet
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum Ping {
|
||||
Binary(Vec<u8>),
|
||||
Number(usize),
|
||||
}
|
||||
|
||||
/// Untagged Error
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ErrorEvent {
|
||||
Error(WebSocketError),
|
||||
APIError(Error),
|
||||
}
|
||||
|
||||
/// Protocol Events
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum EventV1 {
|
||||
/// Multiple events
|
||||
Bulk { v: Vec<EventV1> },
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
/* /// Basic data to cache
|
||||
Ready {
|
||||
users: Vec<User>,
|
||||
servers: Vec<Server>,
|
||||
channels: Vec<Channel>,
|
||||
members: Vec<Member>,
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping },
|
||||
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
/// Update existing message
|
||||
MessageUpdate {
|
||||
id: String,
|
||||
channel: String,
|
||||
data: PartialMessage,
|
||||
},
|
||||
|
||||
/// Append information to existing message
|
||||
MessageAppend {
|
||||
id: String,
|
||||
channel: String,
|
||||
append: AppendMessage,
|
||||
},
|
||||
|
||||
/// Delete message
|
||||
MessageDelete { id: String, channel: String },
|
||||
|
||||
/// New reaction to a message
|
||||
MessageReact {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
user_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Remove user's reaction from message
|
||||
MessageUnreact {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
user_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Remove a reaction from message
|
||||
MessageRemoveReaction {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Bulk delete messages
|
||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
||||
|
||||
/// New server
|
||||
ServerCreate {
|
||||
id: String,
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
ServerUpdate {
|
||||
id: String,
|
||||
data: PartialServer,
|
||||
clear: Vec<FieldsServer>,
|
||||
},
|
||||
|
||||
/// Delete server
|
||||
ServerDelete { id: String },
|
||||
|
||||
/// Update existing server member
|
||||
ServerMemberUpdate {
|
||||
id: MemberCompositeKey,
|
||||
data: PartialMember,
|
||||
clear: Vec<FieldsMember>,
|
||||
},
|
||||
|
||||
/// User joins server
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave { id: String, user: String },
|
||||
|
||||
/// Server role created or updated
|
||||
ServerRoleUpdate {
|
||||
id: String,
|
||||
role_id: String,
|
||||
data: PartialRole,
|
||||
clear: Vec<FieldsRole>,
|
||||
},
|
||||
|
||||
/// Server role deleted
|
||||
ServerRoleDelete { id: String, role_id: String },
|
||||
|
||||
/// Update existing user
|
||||
UserUpdate {
|
||||
id: String,
|
||||
data: PartialUser,
|
||||
clear: Vec<FieldsUser>,
|
||||
event_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Relationship with another user changed
|
||||
UserRelationship {
|
||||
id: String,
|
||||
user: User,
|
||||
// ! this field can be deprecated
|
||||
status: RelationshipStatus,
|
||||
},
|
||||
|
||||
/// Settings updated remotely
|
||||
UserSettingsUpdate { id: String, update: UserSettings },
|
||||
|
||||
/// User has been platform banned or deleted their account
|
||||
///
|
||||
/// Clients should remove the following associated data:
|
||||
/// - Messages
|
||||
/// - DM Channels
|
||||
/// - Relationships
|
||||
/// - Server Memberships
|
||||
///
|
||||
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
||||
UserPlatformWipe { user_id: String, flags: i32 },
|
||||
|
||||
/// New emoji
|
||||
EmojiCreate(Emoji),
|
||||
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
|
||||
/// New report
|
||||
ReportCreate(Report), */
|
||||
/// New channel
|
||||
ChannelCreate(Channel),
|
||||
|
||||
/// Update existing channel
|
||||
ChannelUpdate {
|
||||
id: String,
|
||||
data: PartialChannel,
|
||||
clear: Vec<FieldsChannel>,
|
||||
},
|
||||
|
||||
/// Delete channel
|
||||
ChannelDelete { id: String },
|
||||
|
||||
/// User joins a group
|
||||
ChannelGroupJoin { id: String, user: String },
|
||||
|
||||
/// User leaves a group
|
||||
ChannelGroupLeave { id: String, user: String },
|
||||
|
||||
/// User started typing in a channel
|
||||
ChannelStartTyping { id: String, user: String },
|
||||
|
||||
/// User stopped typing in a channel
|
||||
ChannelStopTyping { id: String, user: String },
|
||||
|
||||
/// User acknowledged message in channel
|
||||
ChannelAck {
|
||||
id: String,
|
||||
user: String,
|
||||
message_id: String,
|
||||
},
|
||||
|
||||
/// New webhook
|
||||
WebhookCreate(Webhook),
|
||||
|
||||
/// Update existing webhook
|
||||
WebhookUpdate {
|
||||
id: String,
|
||||
data: PartialWebhook,
|
||||
remove: Vec<FieldsWebhook>,
|
||||
},
|
||||
|
||||
/// Delete webhook
|
||||
WebhookDelete { id: String },
|
||||
|
||||
/// Auth events
|
||||
Auth(AuthifierEvent),
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
/// Publish helper wrapper
|
||||
pub async fn p(self, channel: String) {
|
||||
#[cfg(not(debug_assertions))]
|
||||
redis_kiss::p(channel, self).await;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("Publishing event to {channel}: {self:?}");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
redis_kiss::publish(channel, self).await.unwrap();
|
||||
}
|
||||
|
||||
/// Publish user event
|
||||
pub async fn p_user(self, id: String, db: &Database) {
|
||||
self.clone().p(id.clone()).await;
|
||||
|
||||
// ! FIXME: this should be captured by member list in the future
|
||||
// ! and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().p(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish private event
|
||||
pub async fn private(self, id: String) {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod client;
|
||||
@@ -80,6 +80,8 @@ mod models;
|
||||
pub mod util;
|
||||
pub use models::*;
|
||||
|
||||
pub mod events;
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
|
||||
@@ -44,6 +44,10 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create channel_unreads collection.");
|
||||
|
||||
db.create_collection("channel_webhooks", None)
|
||||
.await
|
||||
.expect("Failed to create channel_webhooks collection.");
|
||||
|
||||
db.create_collection("migrations", None)
|
||||
.await
|
||||
.expect("Failed to create migrations collection.");
|
||||
@@ -64,6 +68,10 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create safety_snapshots collection.");
|
||||
|
||||
db.create_collection("safety_strikes", None)
|
||||
.await
|
||||
.expect("Failed to create safety_strikes collection.");
|
||||
|
||||
db.create_collection("bots", None)
|
||||
.await
|
||||
.expect("Failed to create bots collection.");
|
||||
@@ -87,6 +95,18 @@ pub async fn create_database(db: &MongoDb) {
|
||||
"username": 1_i32
|
||||
},
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::{ops::BitXor, time::Duration};
|
||||
use std::{collections::HashSet, ops::BitXor, time::Duration};
|
||||
|
||||
use crate::{
|
||||
mongodb::{
|
||||
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
||||
options::FindOptions,
|
||||
},
|
||||
MongoDb,
|
||||
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use rand::seq::SliceRandom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MigrationInfo {
|
||||
@@ -16,7 +18,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 21;
|
||||
pub const LATEST_REVISION: i32 = 25;
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -696,26 +698,15 @@ 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.");
|
||||
|
||||
// TODO: make these fail once production is migrated
|
||||
if db
|
||||
.db()
|
||||
db.db()
|
||||
.create_collection("safety_reports", None)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
info!("Failed to create safety_reports collection but this is expected in production.");
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
if db
|
||||
.db()
|
||||
db.db()
|
||||
.create_collection("safety_snapshots", None)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
info!(
|
||||
"Failed to create safety_snapshots collection but this is expected in production."
|
||||
);
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
db.col::<Document>("safety_reports")
|
||||
.update_many(
|
||||
@@ -753,6 +744,209 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.expect("Failed to create safety snapshot index.");
|
||||
}
|
||||
|
||||
if revision <= 21 {
|
||||
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
|
||||
|
||||
db.db()
|
||||
.create_collection("safety_strikes", None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if revision <= 22 {
|
||||
info!("Running migration [revision 22 / 31-05-2023]: Add moderator_id to account strikes.");
|
||||
|
||||
db.col::<Document>("safety_strikes")
|
||||
.update_many(
|
||||
doc! {},
|
||||
doc! {
|
||||
"$set": {
|
||||
"moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4"
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update server members.");
|
||||
}
|
||||
|
||||
if revision <= 23 {
|
||||
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"dropIndexes": "users",
|
||||
"index": "username"
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to drop existing username index.");
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct UserInformation {
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
username: String,
|
||||
}
|
||||
|
||||
let re_username = regex::Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap();
|
||||
|
||||
let users: Vec<UserInformation> = db
|
||||
.col::<UserInformation>("users")
|
||||
.find(doc! {}, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.map(|doc| doc.expect("id and username"))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let search_space: Vec<String> = DISCRIMINATOR_SEARCH_SPACE.iter().cloned().collect();
|
||||
let mut claimed: HashSet<String> = HashSet::new();
|
||||
|
||||
for i in 0..users.len() {
|
||||
let info = &users[i];
|
||||
let mut discriminator = {
|
||||
let mut rng = rand::thread_rng();
|
||||
search_space.choose(&mut rng).unwrap()
|
||||
};
|
||||
|
||||
if re_username.is_match(&info.username) {
|
||||
while claimed.contains(&format!("{}#{}", info.username, discriminator)) {
|
||||
let new_discriminator = {
|
||||
let mut rng = rand::thread_rng();
|
||||
search_space.choose(&mut rng).unwrap()
|
||||
};
|
||||
|
||||
info!(
|
||||
"Re-rolled {} to {new_discriminator} from {discriminator}",
|
||||
info.username
|
||||
);
|
||||
|
||||
discriminator = new_discriminator;
|
||||
}
|
||||
|
||||
claimed.insert(format!("{}#{}", info.username, discriminator));
|
||||
|
||||
info!(
|
||||
"({}/{}) Migrating user \"{}\" to #{} - compliant",
|
||||
i + 1,
|
||||
users.len(),
|
||||
info.username,
|
||||
discriminator
|
||||
);
|
||||
|
||||
db.col::<UserInformation>("users")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &info.id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"discriminator": discriminator
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
let mut sanitised = info
|
||||
.username
|
||||
.graphemes(true)
|
||||
.filter(|s| re_username.is_match(s))
|
||||
.collect::<String>();
|
||||
|
||||
while sanitised.len() < 2 {
|
||||
sanitised += "_";
|
||||
}
|
||||
|
||||
while claimed.contains(&format!("{}#{}", sanitised, discriminator)) {
|
||||
let new_discriminator = {
|
||||
let mut rng = rand::thread_rng();
|
||||
search_space.choose(&mut rng).unwrap()
|
||||
};
|
||||
|
||||
info!("Re-rolled {sanitised} to {new_discriminator} from {discriminator}");
|
||||
discriminator = new_discriminator;
|
||||
}
|
||||
|
||||
claimed.insert(format!("{}#{}", sanitised, discriminator));
|
||||
|
||||
info!(
|
||||
"({}/{}) Migrating user \"{}\" to #{} - sanitised: \"{}\"",
|
||||
i + 1,
|
||||
users.len(),
|
||||
info.username,
|
||||
discriminator,
|
||||
sanitised
|
||||
);
|
||||
|
||||
db.col::<UserInformation>("users")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &info.id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"username": sanitised,
|
||||
"discriminator": discriminator,
|
||||
"display_name": &info.username
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if revision <= 24 {
|
||||
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
|
||||
|
||||
db.db()
|
||||
.create_collection("channel_webhooks", None)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "users",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32
|
||||
},
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create username index.");
|
||||
}
|
||||
|
||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
|
||||
@@ -91,7 +91,7 @@ impl Bot {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
use crate::{Bot, FieldsBot, PartialBot, User};
|
||||
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
@@ -100,6 +100,14 @@ mod tests {
|
||||
let user_id = "user";
|
||||
let token = "my_token";
|
||||
|
||||
let user = User {
|
||||
id: bot_id.to_string(),
|
||||
username: "Bot Name".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_user(&user).await.unwrap();
|
||||
|
||||
let bot = Bot {
|
||||
id: bot_id.to_string(),
|
||||
owner: user_id.to_string(),
|
||||
@@ -139,7 +147,8 @@ mod tests {
|
||||
|
||||
bot.delete(&db).await.unwrap();
|
||||
assert!(db.fetch_bot(bot_id).await.is_err());
|
||||
assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap())
|
||||
assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap());
|
||||
assert_eq!(db.fetch_user(bot_id).await.unwrap().flags, Some(2))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -0,0 +1,158 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::events::client::EventV1;
|
||||
use crate::{Database, File};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Webhook
|
||||
pub struct Webhook {
|
||||
/// Webhook Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// The name of the webhook
|
||||
pub name: String,
|
||||
|
||||
/// The avatar of the webhook
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<File>,
|
||||
|
||||
/// The channel this webhook belongs to
|
||||
pub channel_id: String,
|
||||
|
||||
/// The private token for the webhook
|
||||
pub token: Option<String>,
|
||||
},
|
||||
"PartialWebhook"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Optional fields on webhook object
|
||||
pub enum FieldsWebhook {
|
||||
Avatar,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Webhook {
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_webhook(self).await?;
|
||||
|
||||
// Avoid leaking the token to people who receive the event
|
||||
let mut webhook = self.clone();
|
||||
webhook.token = None;
|
||||
|
||||
EventV1::WebhookCreate(webhook.into())
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn assert_token(&self, token: &str) -> Result<()> {
|
||||
if self.token.as_deref() == Some(token) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(InvalidCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
mut partial: PartialWebhook,
|
||||
remove: Vec<FieldsWebhook>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field)
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_webhook(&self.id, &partial, &remove).await?;
|
||||
|
||||
partial.token = None; // Avoid leaking the token to people who receive the event
|
||||
|
||||
EventV1::WebhookUpdate {
|
||||
id: self.id.clone(),
|
||||
data: partial.into(),
|
||||
remove: remove.into_iter().map(|v| v.into()).collect(),
|
||||
}
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsWebhook) {
|
||||
match field {
|
||||
FieldsWebhook::Avatar => self.avatar = None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
db.delete_webhook(&self.id).await?;
|
||||
|
||||
EventV1::WebhookDelete {
|
||||
id: self.id.clone(),
|
||||
}
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let webhook_id = "webhook";
|
||||
let channel_id = "channel";
|
||||
|
||||
let webhook = Webhook {
|
||||
id: webhook_id.to_string(),
|
||||
name: "Webhook Name".to_string(),
|
||||
channel_id: channel_id.to_string(),
|
||||
avatar: Some(Default::default()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_webhook(&webhook).await.unwrap();
|
||||
|
||||
let mut updated_webhook = webhook.clone();
|
||||
updated_webhook
|
||||
.update(
|
||||
&db,
|
||||
PartialWebhook {
|
||||
name: Some("New Name".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![FieldsWebhook::Avatar],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched_webhook = db.fetch_webhook(webhook_id).await.unwrap();
|
||||
let fetched_webhooks = db.fetch_webhooks_for_channel(channel_id).await.unwrap();
|
||||
|
||||
assert_eq!(updated_webhook, fetched_webhook);
|
||||
assert_ne!(webhook, fetched_webhook);
|
||||
assert_eq!(1, fetched_webhooks.len());
|
||||
assert_eq!(fetched_webhook, fetched_webhooks[0]);
|
||||
|
||||
webhook.delete(&db).await.unwrap();
|
||||
assert!(db.fetch_webhook(webhook_id).await.is_err());
|
||||
assert_eq!(
|
||||
0,
|
||||
db.fetch_webhooks_for_channel(channel_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractWebhooks: Sync + Send {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>;
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook>;
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>>;
|
||||
|
||||
/// Update webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractWebhooks;
|
||||
|
||||
static COL: &str = "channel_webhooks";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractWebhooks for MongoDb {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
|
||||
query!(self, insert_one, COL, &webhook).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
|
||||
query!(self, find_one_by_id, COL, webhook_id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
||||
Ok(self
|
||||
.col::<Webhook>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"channel_id": channel_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 webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
webhook_id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, webhook_id).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsWebhook {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsWebhook::Avatar => "avatar",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
use super::AbstractWebhooks;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractWebhooks for ReferenceDb {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if webhooks.contains_key(&webhook.id) {
|
||||
Err(create_database_error!("insert", "webhook"))
|
||||
} else {
|
||||
webhooks.insert(webhook.id.to_string(), webhook.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
|
||||
let webhooks = self.channel_webhooks.lock().await;
|
||||
webhooks
|
||||
.get(webhook_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
||||
let webhooks = self.channel_webhooks.lock().await;
|
||||
Ok(webhooks
|
||||
.values()
|
||||
.filter(|webhook| webhook.channel_id == channel_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Update webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if let Some(webhook) = webhooks.get_mut(webhook_id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
webhook.remove_field(field);
|
||||
}
|
||||
|
||||
webhook.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if webhooks.remove(webhook_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,529 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{events::client::EventV1, Database, File, IntoDocumentPath};
|
||||
|
||||
auto_derived!(
|
||||
pub enum Channel {
|
||||
/// Personal "Saved Notes" channel which allows users to save messages
|
||||
SavedMessages {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the user this channel belongs to
|
||||
user: String,
|
||||
},
|
||||
/// Direct message channel between two users
|
||||
DirectMessage {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
|
||||
/// Whether this direct message channel is currently open on both sides
|
||||
active: bool,
|
||||
/// 2-tuple of user ids participating in direct message
|
||||
recipients: Vec<String>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
},
|
||||
/// Group channel between 1 or more participants
|
||||
Group {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
/// User id of the owner of the group
|
||||
owner: String,
|
||||
/// Channel description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
/// Array of user ids participating in channel
|
||||
recipients: Vec<String>,
|
||||
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
|
||||
/// Permissions assigned to members of this group
|
||||
/// (does not apply to the owner of the group)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
permissions: Option<i64>,
|
||||
|
||||
/// Whether this group is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
/// Text channel belonging to a server
|
||||
TextChannel {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
/// Channel description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
/// Voice channel belonging to a server
|
||||
VoiceChannel {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Channel description
|
||||
description: Option<String>,
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
#[derive(Default)]
|
||||
pub struct PartialChannel {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub owner: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<File>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nsfw: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub permissions: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role_permissions: Option<HashMap<String, OverrideField>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_permissions: Option<OverrideField>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_message_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Optional fields on channel object
|
||||
pub enum FieldsChannel {
|
||||
Description,
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
}
|
||||
);
|
||||
|
||||
impl Channel {
|
||||
/// Create a channel
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_channel(self).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add user to a group
|
||||
pub async fn add_user_to_group(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
user_id: &str,
|
||||
_by_id: &str,
|
||||
) -> Result<()> {
|
||||
if let Channel::Group { recipients, .. } = self {
|
||||
if recipients.contains(&String::from(user_id)) {
|
||||
return Err(create_error!(AlreadyInGroup));
|
||||
}
|
||||
|
||||
recipients.push(String::from(user_id));
|
||||
}
|
||||
|
||||
match &self {
|
||||
Channel::Group { id, .. } => {
|
||||
db.add_user_to_group(id, user_id).await?;
|
||||
|
||||
EventV1::ChannelGroupJoin {
|
||||
id: id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user_id.to_string())
|
||||
.await;
|
||||
|
||||
/* TODO: SystemMessage::UserAdded {
|
||||
id: user.to_string(),
|
||||
by: by.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.await
|
||||
.ok(); */
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map out whether it is a direct DM
|
||||
pub fn is_direct_dm(&self) -> bool {
|
||||
matches!(self, Channel::DirectMessage { .. })
|
||||
}
|
||||
|
||||
/// Check whether has a user as a recipient
|
||||
pub fn contains_user(&self, user_id: &str) -> bool {
|
||||
match self {
|
||||
Channel::Group { recipients, .. } => recipients.contains(&String::from(user_id)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of recipients
|
||||
pub fn users(&self) -> Result<Vec<String>> {
|
||||
match self {
|
||||
Channel::Group { recipients, .. } => Ok(recipients.to_owned()),
|
||||
_ => Err(create_error!(NotFound)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to this channel's id
|
||||
pub fn id(&self) -> String {
|
||||
match self {
|
||||
Channel::DirectMessage { id, .. }
|
||||
| Channel::Group { id, .. }
|
||||
| Channel::SavedMessages { id, .. }
|
||||
| Channel::TextChannel { id, .. }
|
||||
| Channel::VoiceChannel { id, .. } => id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set role permission on a channel
|
||||
pub async fn set_role_permission(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
db.set_channel_role_permission(id, role_id, permissions)
|
||||
.await?;
|
||||
|
||||
role_permissions.insert(role_id.to_string(), permissions);
|
||||
|
||||
EventV1::ChannelUpdate {
|
||||
id: id.clone(),
|
||||
data: PartialChannel {
|
||||
role_permissions: Some(role_permissions.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
clear: vec![],
|
||||
}
|
||||
.p(server.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update channel data
|
||||
pub async fn update<'a>(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_channel(&self.id(), &partial, remove.clone())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a field from Channel object
|
||||
pub fn remove_field(&mut self, field: &FieldsChannel) {
|
||||
match field {
|
||||
FieldsChannel::Description => match self {
|
||||
Self::Group { description, .. }
|
||||
| Self::TextChannel { description, .. }
|
||||
| Self::VoiceChannel { description, .. } => {
|
||||
description.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Icon => match self {
|
||||
Self::Group { icon, .. }
|
||||
| Self::TextChannel { icon, .. }
|
||||
| Self::VoiceChannel { icon, .. } => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::DefaultPermissions => match self {
|
||||
Self::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
} => {
|
||||
default_permissions.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove multiple fields from Channel object
|
||||
pub fn remove_fields(&mut self, partial: Vec<FieldsChannel>) {
|
||||
for field in partial {
|
||||
self.remove_field(&field)
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply partial channel to channel
|
||||
pub fn apply_options(&mut self, partial: PartialChannel) {
|
||||
match self {
|
||||
Self::SavedMessages { .. } => {}
|
||||
Self::DirectMessage { active, .. } => {
|
||||
if let Some(v) = partial.active {
|
||||
*active = v;
|
||||
}
|
||||
}
|
||||
Self::Group {
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
permissions,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
*name = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.owner {
|
||||
*owner = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.description {
|
||||
description.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.icon {
|
||||
icon.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.nsfw {
|
||||
*nsfw = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.permissions {
|
||||
permissions.replace(v);
|
||||
}
|
||||
}
|
||||
Self::TextChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
*name = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.description {
|
||||
description.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.icon {
|
||||
icon.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.nsfw {
|
||||
*nsfw = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.role_permissions {
|
||||
*role_permissions = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.default_permissions {
|
||||
default_permissions.replace(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove user from a group
|
||||
pub async fn remove_user_from_group(
|
||||
&self,
|
||||
db: &Database,
|
||||
user_id: &str,
|
||||
_by_id: Option<&str>,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
match &self {
|
||||
Channel::Group {
|
||||
id,
|
||||
owner,
|
||||
recipients,
|
||||
..
|
||||
} => {
|
||||
if user_id == owner {
|
||||
if let Some(new_owner) = recipients.iter().find(|x| *x != user_id) {
|
||||
db.update_channel(
|
||||
id,
|
||||
&PartialChannel {
|
||||
owner: Some(new_owner.into()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
/* TODO: SystemMessage::ChannelOwnershipChanged {
|
||||
from: owner.to_string(),
|
||||
to: new_owner.into(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.await
|
||||
.ok(); */
|
||||
} else {
|
||||
db.delete_channel(self).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::ChannelGroupLeave {
|
||||
id: id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
if !silent {
|
||||
/* TODO: if let Some(_by) = by_id {
|
||||
SystemMessage::UserRemove {
|
||||
id: user_id.to_string(),
|
||||
by: by.to_string(),
|
||||
}
|
||||
} else {
|
||||
SystemMessage::UserLeft {
|
||||
id: user_id.to_string(),
|
||||
}
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.await
|
||||
.ok(); */
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a channel
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
db.delete_channel(self).await
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsChannel {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsChannel::Description => "description",
|
||||
FieldsChannel::Icon => "icon",
|
||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannels: Sync + Send {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()>;
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel>;
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>>;
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>>;
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel>;
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel>;
|
||||
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()>;
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel_id: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()>;
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
use super::AbstractChannels;
|
||||
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
||||
use bson::Document;
|
||||
use futures::StreamExt;
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
static COL: &str = "channels";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannels for MongoDb {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
query!(self, insert_one, COL, &channel).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
|
||||
query!(self, find_one_by_id, COL, channel_id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
||||
Ok(self
|
||||
.col::<Channel>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("fetch", "channels"))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"$or": [
|
||||
{
|
||||
"$or": [
|
||||
{
|
||||
"channel_type": "DirectMessage"
|
||||
},
|
||||
{
|
||||
"channel_type": "Group"
|
||||
}
|
||||
],
|
||||
"recipients": user_id
|
||||
},
|
||||
{
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_id
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_id
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(InternalError))
|
||||
}
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
||||
let doc = match (user_a, user_b) {
|
||||
self_user if self_user.0 == self_user.1 => {
|
||||
doc! {
|
||||
"channel_type": "SavedMessages",
|
||||
"user": self_user.0
|
||||
}
|
||||
}
|
||||
users => {
|
||||
doc! {
|
||||
"channel_type": "DirectMessage",
|
||||
"recipients": {
|
||||
"$all": [ users.0, users.1 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
query!(self, find_one, COL, doc)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": channel
|
||||
},
|
||||
doc! {
|
||||
"$push": {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channel"))
|
||||
}
|
||||
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel: &str,
|
||||
role: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! { "_id": channel },
|
||||
doc! {
|
||||
"$set": {
|
||||
"role_permissions.".to_owned() + role: permissions
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channel"))
|
||||
}
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
channel,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": channel
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channels"))
|
||||
}
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use super::AbstractChannels;
|
||||
use crate::ReferenceDb;
|
||||
use crate::{Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannels for ReferenceDb {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
|
||||
entry.insert(channel.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_database_error!("insert", "channel"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
channels
|
||||
.get(channel_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
||||
let channels = self.channels.lock().await;
|
||||
ids.iter()
|
||||
.map(|id| {
|
||||
channels
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
||||
let channels = self.channels.lock().await;
|
||||
Ok(channels
|
||||
.values()
|
||||
.filter(|channel| channel.contains_user(user_id))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
channels
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_database_error!("fetch", "channel"))
|
||||
}
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
for (_, data) in channels.iter() {
|
||||
if data.contains_user(user_a) && data.contains_user(user_b) {
|
||||
return Ok(data.to_owned());
|
||||
}
|
||||
}
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
|
||||
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(channel_id) {
|
||||
recipients.push(String::from(user_id));
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(InvalidOperation))
|
||||
}
|
||||
}
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
|
||||
if let Some(mut channel) = channels.get_mut(channel_id) {
|
||||
match &mut channel {
|
||||
Channel::TextChannel {
|
||||
role_permissions, ..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
role_permissions, ..
|
||||
} => {
|
||||
if role_permissions.get(role_id).is_some() {
|
||||
role_permissions.remove(role_id);
|
||||
role_permissions.insert(String::from(role_id), permissions);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
_ => Err(create_error!(NotFound)),
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Some(channel_data) = channels.get_mut(id) {
|
||||
channel_data.apply_options(channel.to_owned());
|
||||
channel_data.remove_fields(remove);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Some(channel_data) = channels.get_mut(channel) {
|
||||
if channel_data.users()?.contains(&String::from(user)) {
|
||||
channel_data.users()?.retain(|x| x != user);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
}
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if channels.remove(&channel.id()).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
mod admin_migrations;
|
||||
mod bots;
|
||||
mod channel_webhooks;
|
||||
mod channels;
|
||||
mod files;
|
||||
mod safety_strikes;
|
||||
mod server_members;
|
||||
mod servers;
|
||||
mod user_settings;
|
||||
@@ -8,7 +11,10 @@ mod users;
|
||||
|
||||
pub use admin_migrations::*;
|
||||
pub use bots::*;
|
||||
pub use channel_webhooks::*;
|
||||
pub use channels::*;
|
||||
pub use files::*;
|
||||
pub use safety_strikes::*;
|
||||
pub use server_members::*;
|
||||
pub use servers::*;
|
||||
pub use user_settings::*;
|
||||
@@ -21,7 +27,10 @@ pub trait AbstractDatabase:
|
||||
+ Send
|
||||
+ admin_migrations::AbstractMigrations
|
||||
+ bots::AbstractBots
|
||||
+ channels::AbstractChannels
|
||||
+ channel_webhooks::AbstractWebhooks
|
||||
+ files::AbstractAttachments
|
||||
+ safety_strikes::AbstractAccountStrikes
|
||||
+ server_members::AbstractServerMembers
|
||||
+ servers::AbstractServers
|
||||
+ user_settings::AbstractUserSettings
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -0,0 +1,124 @@
|
||||
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()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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<()>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{Database, File};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_result::{Error, ErrorType, Result};
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -10,6 +13,11 @@ auto_derived_partial!(
|
||||
pub id: String,
|
||||
/// Username
|
||||
pub username: String,
|
||||
/// Discriminator
|
||||
pub discriminator: String,
|
||||
/// Display name
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Avatar attachment
|
||||
pub avatar: Option<File>,
|
||||
@@ -76,7 +84,7 @@ auto_derived!(
|
||||
/// User's active status
|
||||
pub struct UserStatus {
|
||||
/// Custom status text
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub text: String,
|
||||
/// Current presence option
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -86,7 +94,7 @@ auto_derived!(
|
||||
/// User's profile
|
||||
pub struct UserProfile {
|
||||
/// Text content on user's profile
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub content: String,
|
||||
/// Background visible on user's profile
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -111,6 +119,7 @@ auto_derived!(
|
||||
|
||||
impl User {
|
||||
/// Check whether a username is already in use by another user
|
||||
#[allow(dead_code)]
|
||||
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
|
||||
match db.fetch_user_by_username(username).await {
|
||||
Ok(_) => Ok(true),
|
||||
@@ -194,3 +203,17 @@ impl User {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
let mut set = (2..9999)
|
||||
.map(|v| format!("{:0>4}", v))
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
for discrim in [
|
||||
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
|
||||
] {
|
||||
set.remove(&format!("{:0>4}", discrim));
|
||||
}
|
||||
|
||||
set.into_iter().collect()
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod v0;
|
||||
@@ -0,0 +1,336 @@
|
||||
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)]
|
||||
assert_eq!(self.id, user.id);
|
||||
|
||||
PublicBot {
|
||||
id: self.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
|
||||
description: user
|
||||
.profile
|
||||
.map(|profile| profile.content)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Bot> for Bot {
|
||||
fn from(value: crate::Bot) -> Self {
|
||||
Bot {
|
||||
id: value.id,
|
||||
owner_id: value.owner,
|
||||
token: value.token,
|
||||
public: value.public,
|
||||
analytics: value.analytics,
|
||||
discoverable: value.discoverable,
|
||||
interactions_url: value.interactions_url,
|
||||
terms_of_service_url: value.terms_of_service_url,
|
||||
privacy_policy_url: value.privacy_policy_url,
|
||||
flags: value.flags.unwrap_or_default() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Webhook> for Webhook {
|
||||
fn from(value: crate::Webhook) -> Self {
|
||||
Webhook {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
avatar: value.avatar.map(|file| file.into()),
|
||||
channel_id: value.channel_id,
|
||||
token: value.token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialWebhook> for PartialWebhook {
|
||||
fn from(value: crate::PartialWebhook) -> Self {
|
||||
PartialWebhook {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
avatar: value.avatar.map(|file| file.into()),
|
||||
channel_id: value.channel_id,
|
||||
token: value.token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsWebhook> for crate::FieldsWebhook {
|
||||
fn from(_value: FieldsWebhook) -> Self {
|
||||
Self::Avatar
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsWebhook> for FieldsWebhook {
|
||||
fn from(_value: crate::FieldsWebhook) -> Self {
|
||||
Self::Avatar
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Channel> for Channel {
|
||||
fn from(value: crate::Channel) -> Self {
|
||||
match value {
|
||||
crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user },
|
||||
crate::Channel::DirectMessage {
|
||||
id,
|
||||
active,
|
||||
recipients,
|
||||
last_message_id,
|
||||
} => Channel::DirectMessage {
|
||||
id,
|
||||
active,
|
||||
recipients,
|
||||
last_message_id,
|
||||
},
|
||||
crate::Channel::Group {
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
recipients,
|
||||
icon,
|
||||
last_message_id,
|
||||
permissions,
|
||||
nsfw,
|
||||
} => Channel::Group {
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
recipients,
|
||||
icon: icon.map(|file| file.into()),
|
||||
last_message_id,
|
||||
permissions,
|
||||
nsfw,
|
||||
},
|
||||
crate::Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
last_message_id,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
} => Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon: icon.map(|file| file.into()),
|
||||
last_message_id,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
},
|
||||
crate::Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
} => Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon: icon.map(|file| file.into()),
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialChannel> for PartialChannel {
|
||||
fn from(value: crate::PartialChannel) -> Self {
|
||||
PartialChannel {
|
||||
name: value.name,
|
||||
owner: value.owner,
|
||||
description: value.description,
|
||||
icon: value.icon.map(|file| file.into()),
|
||||
nsfw: value.nsfw,
|
||||
active: value.active,
|
||||
permissions: value.permissions,
|
||||
role_permissions: value.role_permissions,
|
||||
default_permissions: value.default_permissions,
|
||||
last_message_id: value.last_message_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsChannel> for crate::FieldsChannel {
|
||||
fn from(value: FieldsChannel) -> Self {
|
||||
match value {
|
||||
FieldsChannel::Description => crate::FieldsChannel::Description,
|
||||
FieldsChannel::Icon => crate::FieldsChannel::Icon,
|
||||
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsChannel> for FieldsChannel {
|
||||
fn from(value: crate::FieldsChannel) -> Self {
|
||||
match value {
|
||||
crate::FieldsChannel::Description => FieldsChannel::Description,
|
||||
crate::FieldsChannel::Icon => FieldsChannel::Icon,
|
||||
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::File> for File {
|
||||
fn from(value: crate::File) -> Self {
|
||||
File {
|
||||
id: value.id,
|
||||
tag: value.tag,
|
||||
filename: value.filename,
|
||||
metadata: value.metadata.into(),
|
||||
content_type: value.content_type,
|
||||
size: value.size,
|
||||
deleted: value.deleted,
|
||||
reported: value.reported,
|
||||
message_id: value.message_id,
|
||||
user_id: value.user_id,
|
||||
server_id: value.server_id,
|
||||
object_id: value.object_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Metadata> for Metadata {
|
||||
fn from(value: crate::Metadata) -> Self {
|
||||
match value {
|
||||
crate::Metadata::File => Metadata::File,
|
||||
crate::Metadata::Text => Metadata::Text,
|
||||
crate::Metadata::Image { width, height } => Metadata::Image {
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
},
|
||||
crate::Metadata::Video { width, height } => Metadata::Video {
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
},
|
||||
crate::Metadata::Audio => Metadata::Audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::User {
|
||||
pub async fn into<P>(self, perspective: P) -> User
|
||||
where
|
||||
P: Into<Option<crate::User>>,
|
||||
{
|
||||
let relationship = if let Some(perspective) = perspective.into() {
|
||||
perspective
|
||||
.relations
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.find(|relationship| relationship.id == self.id)
|
||||
.map(|relationship| relationship.status.into())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
RelationshipStatus::None
|
||||
};
|
||||
|
||||
// do permission stuff here
|
||||
// TODO: implement permissions =)
|
||||
let can_see_profile = false;
|
||||
|
||||
User {
|
||||
username: self.username,
|
||||
discriminator: self.discriminator,
|
||||
display_name: self.display_name,
|
||||
avatar: self.avatar.map(|file| file.into()),
|
||||
relations: vec![],
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
status: None,
|
||||
profile: None,
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && revolt_presence::is_online(&self.id).await,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::RelationshipStatus> for RelationshipStatus {
|
||||
fn from(value: crate::RelationshipStatus) -> Self {
|
||||
match value {
|
||||
crate::RelationshipStatus::None => RelationshipStatus::None,
|
||||
crate::RelationshipStatus::User => RelationshipStatus::User,
|
||||
crate::RelationshipStatus::Friend => RelationshipStatus::Friend,
|
||||
crate::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
|
||||
crate::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
|
||||
crate::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
|
||||
crate::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Relationship> for Relationship {
|
||||
fn from(value: crate::Relationship) -> Self {
|
||||
Self {
|
||||
user_id: value.id,
|
||||
status: value.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Presence> for Presence {
|
||||
fn from(value: crate::Presence) -> Self {
|
||||
match value {
|
||||
crate::Presence::Online => Presence::Online,
|
||||
crate::Presence::Idle => Presence::Idle,
|
||||
crate::Presence::Focus => Presence::Focus,
|
||||
crate::Presence::Busy => Presence::Busy,
|
||||
crate::Presence::Invisible => Presence::Invisible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::UserStatus> for UserStatus {
|
||||
fn from(value: crate::UserStatus) -> Self {
|
||||
UserStatus {
|
||||
text: value.text,
|
||||
presence: value.presence.map(|presence| presence.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::UserProfile> for UserProfile {
|
||||
fn from(value: crate::UserProfile) -> Self {
|
||||
UserProfile {
|
||||
content: value.content,
|
||||
background: value.background.map(|file| file.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::BotInformation> for BotInformation {
|
||||
fn from(value: crate::BotInformation) -> Self {
|
||||
BotInformation {
|
||||
owner_id: value.owner,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod bridge;
|
||||
pub mod permissions;
|
||||
pub mod reference;
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{Database, User};
|
||||
|
||||
/// Permissions calculator
|
||||
pub struct PermissionCalculator<'a> {
|
||||
#[allow(dead_code)]
|
||||
database: &'a Database,
|
||||
|
||||
perspective: &'a User,
|
||||
|
||||
@@ -7,7 +7,7 @@ use schemars::{
|
||||
JsonSchema,
|
||||
};
|
||||
|
||||
use crate::{Bot, Database};
|
||||
use crate::{Bot, Database, Webhook};
|
||||
|
||||
/// Reference to some object in the database
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -26,6 +26,11 @@ impl Reference {
|
||||
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
|
||||
db.fetch_bot(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch webhook from Ref
|
||||
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
|
||||
db.fetch_webhook(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
@@ -9,21 +9,23 @@ 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" ]
|
||||
schemas = [ "dep:schemars" ]
|
||||
from_database = [ "revolt-database", "revolt-presence" ]
|
||||
serde = [ "dep:serde", "revolt-permissions/serde" ]
|
||||
schemas = [ "dep:schemars", "revolt-permissions/schemas" ]
|
||||
validator = [ "dep:validator" ]
|
||||
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ]
|
||||
|
||||
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
|
||||
|
||||
default = [ "serde", "from_database" ]
|
||||
default = [ "serde", "partials" ]
|
||||
|
||||
[dependencies]
|
||||
# Repo
|
||||
revolt-database = { version = "0.0.2", path = "../database", optional = true }
|
||||
revolt-presence = { version = "0.0.2", path = "../presence", optional = true }
|
||||
# Core
|
||||
revolt-permissions = { version = "0.6.0", path = "../permissions" }
|
||||
|
||||
# Serialisation
|
||||
revolt_optional_struct = { version = "0.2.0", optional = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
# Spec Generation
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
|
||||
# Validation
|
||||
validator = { version = "0.16.0", optional = true, features = ["derive"] }
|
||||
|
||||
@@ -6,6 +6,13 @@ extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate schemars;
|
||||
|
||||
#[cfg(feature = "partials")]
|
||||
#[macro_use]
|
||||
extern crate revolt_optional_struct;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
pub use validator;
|
||||
|
||||
macro_rules! auto_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
@@ -17,6 +24,28 @@ macro_rules! auto_derived {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "partials")]
|
||||
macro_rules! auto_derived_partial {
|
||||
( $item:item, $name:expr ) => {
|
||||
#[derive(
|
||||
OptionalStruct, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
#[optional_derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[optional_name = $name]
|
||||
#[opt_skip_serializing_none]
|
||||
#[opt_some_priority]
|
||||
$item
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "partials"))]
|
||||
macro_rules! auto_derived_partial {
|
||||
( $item:item, $name:expr ) => {
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
$item
|
||||
};
|
||||
}
|
||||
|
||||
pub mod v0;
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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,
|
||||
}
|
||||
);
|
||||
@@ -4,11 +4,11 @@ auto_derived!(
|
||||
/// Bot
|
||||
pub struct Bot {
|
||||
/// Bot Id
|
||||
#[serde(rename = "_id")]
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
pub id: String,
|
||||
|
||||
/// User Id of the bot owner
|
||||
#[serde(rename = "owner")]
|
||||
#[cfg_attr(feature = "serde", serde(rename = "owner"))]
|
||||
pub owner_id: String,
|
||||
/// Token used to authenticate requests for this bot
|
||||
pub token: String,
|
||||
@@ -66,16 +66,16 @@ auto_derived!(
|
||||
pub struct PublicBot {
|
||||
/// Bot Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
pub id: String,
|
||||
|
||||
/// Bot Username
|
||||
username: String,
|
||||
pub username: String,
|
||||
/// Profile Avatar
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
avatar: String,
|
||||
pub avatar: String,
|
||||
/// Profile Description
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
description: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Bot Response
|
||||
@@ -86,39 +86,3 @@ auto_derived!(
|
||||
pub user: User,
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl PublicBot {
|
||||
pub fn from(bot: revolt_database::Bot, user: revolt_database::User) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(bot.id, user.id);
|
||||
|
||||
PublicBot {
|
||||
id: bot.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
|
||||
description: user
|
||||
.profile
|
||||
.map(|profile| profile.content)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::Bot> for Bot {
|
||||
fn from(value: revolt_database::Bot) -> Self {
|
||||
Bot {
|
||||
id: value.id,
|
||||
owner_id: value.owner,
|
||||
token: value.token,
|
||||
public: value.public,
|
||||
analytics: value.analytics,
|
||||
discoverable: value.discoverable,
|
||||
interactions_url: value.interactions_url,
|
||||
terms_of_service_url: value.terms_of_service_url,
|
||||
privacy_policy_url: value.privacy_policy_url,
|
||||
flags: value.flags.unwrap_or_default() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
use super::File;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Webhook
|
||||
pub struct Webhook {
|
||||
/// Webhook Id
|
||||
pub id: String,
|
||||
|
||||
/// The name of the webhook
|
||||
pub name: String,
|
||||
|
||||
/// The avatar of the webhook
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub avatar: Option<File>,
|
||||
|
||||
/// The channel this webhook belongs to
|
||||
pub channel_id: String,
|
||||
|
||||
/// The private token for the webhook
|
||||
pub token: Option<String>,
|
||||
},
|
||||
"PartialWebhook"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Information about the webhook bundled with Message
|
||||
pub struct MessageWebhook {
|
||||
// The name of the webhook - 1 to 32 chars
|
||||
pub name: String,
|
||||
|
||||
// The id of the avatar of the webhook, if it has one
|
||||
pub avatar: Option<String>,
|
||||
}
|
||||
|
||||
/// New webhook information
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct DataEditWebhook {
|
||||
/// Webhook name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Avatar ID
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub avatar: Option<String>,
|
||||
|
||||
/// Fields to remove from webhook
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Vec<FieldsWebhook>,
|
||||
}
|
||||
|
||||
/// Webhook information
|
||||
pub struct ResponseWebhook {
|
||||
/// Webhook Id
|
||||
pub id: String,
|
||||
|
||||
/// Webhook name
|
||||
pub name: String,
|
||||
|
||||
/// Avatar ID
|
||||
pub avatar: Option<String>,
|
||||
|
||||
/// The channel this webhook belongs to
|
||||
pub channel_id: String,
|
||||
}
|
||||
|
||||
/// Optional fields on webhook object
|
||||
pub enum FieldsWebhook {
|
||||
Avatar,
|
||||
}
|
||||
);
|
||||
|
||||
impl From<Webhook> for MessageWebhook {
|
||||
fn from(value: Webhook) -> Self {
|
||||
MessageWebhook {
|
||||
name: value.name,
|
||||
avatar: value.avatar.map(|file| file.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Webhook> for ResponseWebhook {
|
||||
fn from(value: Webhook) -> Self {
|
||||
ResponseWebhook {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
avatar: value.avatar.map(|file| file.id),
|
||||
channel_id: value.channel_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use super::File;
|
||||
|
||||
use revolt_permissions::OverrideField;
|
||||
use std::collections::HashMap;
|
||||
|
||||
auto_derived!(
|
||||
/// Channel
|
||||
pub enum Channel {
|
||||
/// Personal "Saved Notes" channel which allows users to save messages
|
||||
SavedMessages {
|
||||
/// Unique Id
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
id: String,
|
||||
/// Id of the user this channel belongs to
|
||||
user: String,
|
||||
},
|
||||
/// Direct message channel between two users
|
||||
DirectMessage {
|
||||
/// Unique Id
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
id: String,
|
||||
|
||||
/// Whether this direct message channel is currently open on both sides
|
||||
active: bool,
|
||||
/// 2-tuple of user ids participating in direct message
|
||||
recipients: Vec<String>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
last_message_id: Option<String>,
|
||||
},
|
||||
/// Group channel between 1 or more participants
|
||||
Group {
|
||||
/// Unique Id
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
id: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
/// User id of the owner of the group
|
||||
owner: String,
|
||||
/// Channel description
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
description: Option<String>,
|
||||
/// Array of user ids participating in channel
|
||||
recipients: Vec<String>,
|
||||
|
||||
/// Custom icon attachment
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
icon: Option<File>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
last_message_id: Option<String>,
|
||||
|
||||
/// Permissions assigned to members of this group
|
||||
/// (does not apply to the owner of the group)
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
permissions: Option<i64>,
|
||||
|
||||
/// Whether this group is marked as not safe for work
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "crate::if_false", default)
|
||||
)]
|
||||
nsfw: bool,
|
||||
},
|
||||
/// Text channel belonging to a server
|
||||
TextChannel {
|
||||
/// Unique Id
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
/// Channel description
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
description: Option<String>,
|
||||
|
||||
/// Custom icon attachment
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
icon: Option<File>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
last_message_id: Option<String>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "crate::if_false", default)
|
||||
)]
|
||||
nsfw: bool,
|
||||
},
|
||||
/// Voice channel belonging to a server
|
||||
VoiceChannel {
|
||||
/// Unique Id
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
/// Channel description
|
||||
description: Option<String>,
|
||||
/// Custom icon attachment
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
icon: Option<File>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "crate::if_false", default)
|
||||
)]
|
||||
nsfw: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Partial representation of a channel
|
||||
#[derive(Default)]
|
||||
pub struct PartialChannel {
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub name: Option<String>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub owner: Option<String>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub description: Option<String>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub icon: Option<File>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub nsfw: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub active: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub permissions: Option<i64>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub role_permissions: Option<HashMap<String, OverrideField>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub default_permissions: Option<OverrideField>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub last_message_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Optional fields on channel object
|
||||
pub enum FieldsChannel {
|
||||
Description,
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
}
|
||||
|
||||
/// New webhook information
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct DataEditChannel {
|
||||
/// Channel name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Channel description
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Group owner
|
||||
pub owner: Option<String>,
|
||||
|
||||
/// Icon
|
||||
///
|
||||
/// Provide an Autumn attachment Id.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub icon: Option<String>,
|
||||
|
||||
/// Whether this channel is age-restricted
|
||||
pub nsfw: Option<bool>,
|
||||
|
||||
/// Whether this channel is archived
|
||||
pub archived: Option<bool>,
|
||||
|
||||
/// Fields to remove from channel
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Option<Vec<FieldsChannel>>,
|
||||
}
|
||||
);
|
||||
@@ -54,42 +54,3 @@ auto_derived!(
|
||||
Audio,
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::File> for File {
|
||||
fn from(value: revolt_database::File) -> Self {
|
||||
File {
|
||||
id: value.id,
|
||||
tag: value.tag,
|
||||
filename: value.filename,
|
||||
metadata: value.metadata.into(),
|
||||
content_type: value.content_type,
|
||||
size: value.size,
|
||||
deleted: value.deleted,
|
||||
reported: value.reported,
|
||||
message_id: value.message_id,
|
||||
user_id: value.user_id,
|
||||
server_id: value.server_id,
|
||||
object_id: value.object_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::Metadata> for Metadata {
|
||||
fn from(value: revolt_database::Metadata) -> Self {
|
||||
match value {
|
||||
revolt_database::Metadata::File => Metadata::File,
|
||||
revolt_database::Metadata::Text => Metadata::Text,
|
||||
revolt_database::Metadata::Image { width, height } => Metadata::Image {
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
},
|
||||
revolt_database::Metadata::Video { width, height } => Metadata::Video {
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
},
|
||||
revolt_database::Metadata::Audio => Metadata::Audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
mod account_strikes;
|
||||
mod bots;
|
||||
mod channel_webhooks;
|
||||
mod channels;
|
||||
mod files;
|
||||
mod users;
|
||||
|
||||
pub use account_strikes::*;
|
||||
pub use bots::*;
|
||||
pub use channel_webhooks::*;
|
||||
pub use channels::*;
|
||||
pub use files::*;
|
||||
pub use users::*;
|
||||
|
||||
@@ -8,6 +8,11 @@ auto_derived!(
|
||||
pub id: String,
|
||||
/// Username
|
||||
pub username: String,
|
||||
/// Discriminator
|
||||
pub discriminator: String,
|
||||
/// Display name
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Avatar attachment
|
||||
pub avatar: Option<File>,
|
||||
@@ -158,109 +163,3 @@ impl CheckRelationship for Vec<Relationship> {
|
||||
RelationshipStatus::None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl User {
|
||||
pub async fn from<P>(user: revolt_database::User, perspective: P) -> Self
|
||||
where
|
||||
P: Into<Option<revolt_database::User>>,
|
||||
{
|
||||
let relationship = if let Some(perspective) = perspective.into() {
|
||||
perspective
|
||||
.relations
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.find(|relationship| relationship.id == user.id)
|
||||
.map(|relationship| relationship.status.into())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
RelationshipStatus::None
|
||||
};
|
||||
|
||||
// do permission stuff here
|
||||
// TODO: implement permissions =)
|
||||
let can_see_profile = false;
|
||||
|
||||
Self {
|
||||
username: user.username,
|
||||
avatar: user.avatar.map(|file| file.into()),
|
||||
relations: vec![],
|
||||
badges: user.badges.unwrap_or_default() as u32,
|
||||
status: None,
|
||||
profile: None,
|
||||
flags: user.flags.unwrap_or_default() as u32,
|
||||
privileged: user.privileged,
|
||||
bot: user.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && revolt_presence::is_online(&user.id).await,
|
||||
id: user.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::RelationshipStatus> for RelationshipStatus {
|
||||
fn from(value: revolt_database::RelationshipStatus) -> Self {
|
||||
match value {
|
||||
revolt_database::RelationshipStatus::None => RelationshipStatus::None,
|
||||
revolt_database::RelationshipStatus::User => RelationshipStatus::User,
|
||||
revolt_database::RelationshipStatus::Friend => RelationshipStatus::Friend,
|
||||
revolt_database::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
|
||||
revolt_database::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
|
||||
revolt_database::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
|
||||
revolt_database::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::Relationship> for Relationship {
|
||||
fn from(value: revolt_database::Relationship) -> Self {
|
||||
Self {
|
||||
user_id: value.id,
|
||||
status: value.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::Presence> for Presence {
|
||||
fn from(value: revolt_database::Presence) -> Self {
|
||||
match value {
|
||||
revolt_database::Presence::Online => Presence::Online,
|
||||
revolt_database::Presence::Idle => Presence::Idle,
|
||||
revolt_database::Presence::Focus => Presence::Focus,
|
||||
revolt_database::Presence::Busy => Presence::Busy,
|
||||
revolt_database::Presence::Invisible => Presence::Invisible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::UserStatus> for UserStatus {
|
||||
fn from(value: revolt_database::UserStatus) -> Self {
|
||||
UserStatus {
|
||||
text: value.text,
|
||||
presence: value.presence.map(|presence| presence.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::UserProfile> for UserProfile {
|
||||
fn from(value: revolt_database::UserProfile) -> Self {
|
||||
UserProfile {
|
||||
content: value.content,
|
||||
background: value.background.map(|file| file.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::BotInformation> for BotInformation {
|
||||
fn from(value: revolt_database::BotInformation) -> Self {
|
||||
BotInformation {
|
||||
owner_id: value.owner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-permissions"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
@@ -9,10 +9,12 @@ 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" ]
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
# Async
|
||||
async-std = { version = "1.8.0", features = ["attributes"] }
|
||||
@@ -28,6 +30,7 @@ async-trait = "0.1.51"
|
||||
|
||||
# Serialisation
|
||||
serde = { version = "1", features = ["derive"], 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 }
|
||||
@@ -1,6 +1,10 @@
|
||||
#[cfg(feature = "schemas")]
|
||||
use schemars::JsonSchema;
|
||||
|
||||
/// Representation of a single permission override
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
pub struct Override {
|
||||
/// Allow bit flags
|
||||
pub allow: u64,
|
||||
@@ -8,10 +12,43 @@ pub struct Override {
|
||||
pub deny: u64,
|
||||
}
|
||||
|
||||
/// Data permissions Field - contains both allow and deny
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
pub struct DataPermissionsField {
|
||||
pub permissions: Override,
|
||||
}
|
||||
|
||||
/// Data permissions Value - contains allow
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
pub struct DataPermissionsValue {
|
||||
pub permissions: u64,
|
||||
}
|
||||
|
||||
/// Data permissions Poly - can contain either Value or Field
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
pub enum DataPermissionPoly {
|
||||
Value {
|
||||
/// Permission values to set for members in a `Group`
|
||||
permissions: u64,
|
||||
},
|
||||
Field {
|
||||
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
|
||||
permissions: Override,
|
||||
},
|
||||
}
|
||||
|
||||
/// Representation of a single permission override
|
||||
/// as it appears on models and in the database
|
||||
#[derive(/*JsonSchema, */ Debug, Clone, Copy, Default, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
pub struct OverrideField {
|
||||
/// Allow bit flags
|
||||
a: i64,
|
||||
@@ -49,8 +86,12 @@ impl From<OverrideField> for Override {
|
||||
}
|
||||
}
|
||||
|
||||
/*impl From<OverrideField> for Bson {
|
||||
#[cfg(feature = "bson")]
|
||||
use bson::Bson;
|
||||
|
||||
#[cfg(feature = "bson")]
|
||||
impl From<OverrideField> for Bson {
|
||||
fn from(v: OverrideField) -> Self {
|
||||
Self::Document(bson::to_document(&v).unwrap())
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-presence"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-result"
|
||||
version = "0.0.2"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
@@ -11,12 +11,20 @@ description = "Revolt Backend: Result and Error types"
|
||||
[features]
|
||||
serde = [ "dep:serde" ]
|
||||
schemas = [ "dep:schemars" ]
|
||||
rocket = [ "dep:rocket", "dep:serde_json" ]
|
||||
okapi = [ "dep:revolt_rocket_okapi", "dep:revolt_okapi" ]
|
||||
|
||||
default = [ "serde" ]
|
||||
|
||||
[dependencies]
|
||||
# Serialisation
|
||||
serde_json = { version = "1", optional = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
# Spec Generation
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
|
||||
# Rocket
|
||||
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
|
||||
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_okapi = { version = "0.9.1", optional = true }
|
||||
|
||||
@@ -6,6 +6,12 @@ extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate schemars;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub mod rocket;
|
||||
|
||||
#[cfg(feature = "okapi")]
|
||||
pub mod okapi;
|
||||
|
||||
/// Result type with custom Error
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
use revolt_okapi::openapi3::SchemaObject;
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3;
|
||||
use schemars::schema::Schema;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
impl revolt_rocket_okapi::response::OpenApiResponderInner for Error {
|
||||
fn responses(
|
||||
gen: &mut revolt_rocket_okapi::gen::OpenApiGenerator,
|
||||
) -> std::result::Result<openapi3::Responses, revolt_rocket_okapi::OpenApiError> {
|
||||
let mut content = revolt_okapi::Map::new();
|
||||
|
||||
let settings = schemars::gen::SchemaSettings::default().with(|s| {
|
||||
s.option_nullable = true;
|
||||
s.option_add_null_type = false;
|
||||
s.definitions_path = "#/components/schemas/".to_string();
|
||||
});
|
||||
|
||||
let mut schema_generator = settings.into_generator();
|
||||
let schema = schema_generator.root_schema_for::<Error>();
|
||||
|
||||
let definitions = gen.schema_generator().definitions_mut();
|
||||
for (key, value) in schema.definitions {
|
||||
definitions.insert(key, value);
|
||||
}
|
||||
|
||||
definitions.insert("Error".to_string(), Schema::Object(schema.schema));
|
||||
|
||||
content.insert(
|
||||
"application/json".to_string(),
|
||||
openapi3::MediaType {
|
||||
schema: Some(SchemaObject {
|
||||
reference: Some("#/components/schemas/Error".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
Ok(openapi3::Responses {
|
||||
default: Some(openapi3::RefOr::Object(openapi3::Response {
|
||||
content,
|
||||
description: "An error occurred.".to_string(),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use rocket::{
|
||||
http::{ContentType, Status},
|
||||
response::{self, Responder},
|
||||
Request, Response,
|
||||
};
|
||||
|
||||
use crate::{Error, ErrorType};
|
||||
|
||||
/// HTTP response builder for Error enum
|
||||
impl<'r> Responder<'r, 'static> for Error {
|
||||
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
||||
let status = match self.error_type {
|
||||
ErrorType::LabelMe => Status::InternalServerError,
|
||||
|
||||
ErrorType::AlreadyOnboarded => Status::Forbidden,
|
||||
|
||||
ErrorType::UnknownUser => Status::NotFound,
|
||||
ErrorType::InvalidUsername => Status::BadRequest,
|
||||
ErrorType::UsernameTaken => Status::Conflict,
|
||||
ErrorType::AlreadyFriends => Status::Conflict,
|
||||
ErrorType::AlreadySentRequest => Status::Conflict,
|
||||
ErrorType::Blocked => Status::Conflict,
|
||||
ErrorType::BlockedByOther => Status::Forbidden,
|
||||
ErrorType::NotFriends => Status::Forbidden,
|
||||
|
||||
ErrorType::UnknownChannel => Status::NotFound,
|
||||
ErrorType::UnknownMessage => Status::NotFound,
|
||||
ErrorType::UnknownAttachment => Status::BadRequest,
|
||||
ErrorType::CannotEditMessage => Status::Forbidden,
|
||||
ErrorType::CannotJoinCall => Status::BadRequest,
|
||||
ErrorType::TooManyAttachments { .. } => Status::BadRequest,
|
||||
ErrorType::TooManyReplies { .. } => Status::BadRequest,
|
||||
ErrorType::EmptyMessage => Status::UnprocessableEntity,
|
||||
ErrorType::PayloadTooLarge => Status::UnprocessableEntity,
|
||||
ErrorType::CannotRemoveYourself => Status::BadRequest,
|
||||
ErrorType::GroupTooLarge { .. } => Status::Forbidden,
|
||||
ErrorType::AlreadyInGroup => Status::Conflict,
|
||||
ErrorType::NotInGroup => Status::NotFound,
|
||||
|
||||
ErrorType::UnknownServer => Status::NotFound,
|
||||
ErrorType::InvalidRole => Status::NotFound,
|
||||
ErrorType::Banned => Status::Forbidden,
|
||||
|
||||
ErrorType::TooManyServers { .. } => Status::BadRequest,
|
||||
ErrorType::TooManyEmoji { .. } => Status::BadRequest,
|
||||
ErrorType::TooManyChannels { .. } => Status::BadRequest,
|
||||
ErrorType::TooManyRoles { .. } => Status::BadRequest,
|
||||
|
||||
ErrorType::ReachedMaximumBots => Status::BadRequest,
|
||||
ErrorType::IsBot => Status::BadRequest,
|
||||
ErrorType::BotIsPrivate => Status::Forbidden,
|
||||
|
||||
ErrorType::CannotReportYourself => Status::BadRequest,
|
||||
|
||||
ErrorType::MissingPermission { .. } => Status::Forbidden,
|
||||
ErrorType::MissingUserPermission { .. } => Status::Forbidden,
|
||||
ErrorType::NotElevated => Status::Forbidden,
|
||||
ErrorType::NotPrivileged => Status::Forbidden,
|
||||
ErrorType::CannotGiveMissingPermissions => Status::Forbidden,
|
||||
ErrorType::NotOwner => Status::Forbidden,
|
||||
|
||||
ErrorType::DatabaseError { .. } => Status::InternalServerError,
|
||||
ErrorType::InternalError => Status::InternalServerError,
|
||||
ErrorType::InvalidOperation => Status::BadRequest,
|
||||
ErrorType::InvalidCredentials => Status::Unauthorized,
|
||||
ErrorType::InvalidProperty => Status::BadRequest,
|
||||
ErrorType::InvalidSession => Status::Unauthorized,
|
||||
ErrorType::DuplicateNonce => Status::Conflict,
|
||||
ErrorType::VosoUnavailable => Status::BadRequest,
|
||||
ErrorType::NotFound => Status::NotFound,
|
||||
ErrorType::NoEffect => Status::Ok,
|
||||
ErrorType::FailedValidation { .. } => Status::BadRequest,
|
||||
};
|
||||
|
||||
// Serialize the error data structure into JSON.
|
||||
let string = serde_json::to_string(&self).unwrap();
|
||||
|
||||
// Build and send the request.
|
||||
Response::build()
|
||||
.sized_body(string.len(), Cursor::new(string))
|
||||
.header(ContentType::new("application", "json"))
|
||||
.status(status)
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.5.20"
|
||||
version = "0.6.0"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
@@ -31,7 +31,7 @@ nanoid = "0.4.0"
|
||||
# serde
|
||||
serde_json = "1.0.57"
|
||||
serde = { version = "1.0.115", features = ["derive"] }
|
||||
validator = { version = "0.14", features = ["derive"] }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
|
||||
# async
|
||||
futures = "0.3.8"
|
||||
@@ -56,8 +56,10 @@ revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# core
|
||||
revolt-database = { path = "../core/database", features = [ "rocket-impl" ] }
|
||||
revolt-models = { path = "../core/models", features = [ "schemas", "redis-is-patched" ] }
|
||||
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"
|
||||
|
||||
@@ -8,10 +8,13 @@ extern crate serde_json;
|
||||
pub mod routes;
|
||||
pub mod util;
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use async_std::channel::unbounded;
|
||||
use revolt_quark::authifier::{Authifier, AuthifierEvent};
|
||||
use revolt_quark::events::client::EventV1;
|
||||
use revolt_quark::DatabaseInfo;
|
||||
use rocket::data::ToByteUnit;
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
@@ -72,4 +75,9 @@ async fn rocket() -> _ {
|
||||
.manage(cors.clone())
|
||||
.attach(revolt_quark::web::ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
.configure(rocket::Config {
|
||||
limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
|
||||
address: Ipv4Addr::new(0, 0, 0, 0).into(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,14 +38,12 @@ pub async fn create_bot(db: &Db, user: User, info: Json<DataCreateBot>) -> Resul
|
||||
return Err(Error::ReachedMaximumBots);
|
||||
}
|
||||
|
||||
if db.is_username_taken(&info.name).await? {
|
||||
return Err(Error::UsernameTaken);
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
let username = User::validate_username(info.name)?;
|
||||
let bot_user = User {
|
||||
id: id.clone(),
|
||||
username: info.name.trim().to_string(),
|
||||
discriminator: User::find_discriminator(db, &username, None).await?,
|
||||
username,
|
||||
bot: Some(BotInformation {
|
||||
owner: user.id.clone(),
|
||||
}),
|
||||
|
||||
@@ -58,10 +58,6 @@ pub async fn edit_bot(
|
||||
}
|
||||
|
||||
if let Some(name) = data.name {
|
||||
if db.is_username_taken(&name).await? {
|
||||
return Err(Error::UsernameTaken);
|
||||
}
|
||||
|
||||
let mut user = db.fetch_user(&bot.id).await?;
|
||||
user.update_username(db, name).await?;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,12 @@ pub async fn fetch_bot(
|
||||
}
|
||||
|
||||
Ok(Json(FetchBotResponse {
|
||||
user: revolt_models::v0::User::from(
|
||||
db.fetch_user(&bot.id).await.map_err(Error::from_core)?,
|
||||
None,
|
||||
)
|
||||
.await,
|
||||
user: db
|
||||
.fetch_user(&bot.id)
|
||||
.await
|
||||
.map_err(Error::from_core)?
|
||||
.into(None)
|
||||
.await,
|
||||
bot: bot.into(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -21,5 +21,5 @@ pub async fn fetch_public_bot(
|
||||
}
|
||||
|
||||
let user = db.fetch_user(&bot.id).await.map_err(Error::from_core)?;
|
||||
Ok(Json(PublicBot::from(bot, user)))
|
||||
Ok(Json(bot.into_public_bot(user)))
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ use rocket::serde::json::Json;
|
||||
#[openapi(tag = "Groups")]
|
||||
#[get("/<target>/members")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
|
||||
@@ -44,8 +44,6 @@ pub async fn req(
|
||||
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
||||
.await?;
|
||||
|
||||
Message::validate_sum(&edit.content, &edit.embeds)?;
|
||||
|
||||
let mut message = msg.as_message(db).await?;
|
||||
if message.channel != channel.id() {
|
||||
return Err(Error::NotFound);
|
||||
@@ -55,6 +53,8 @@ pub async fn req(
|
||||
return Err(Error::CannotEditMessage);
|
||||
}
|
||||
|
||||
Message::validate_sum(&edit.content, edit.embeds.as_deref().unwrap_or_default())?;
|
||||
|
||||
message.edited = Some(Timestamp::now_utc());
|
||||
let mut partial = PartialMessage {
|
||||
edited: message.edited,
|
||||
@@ -81,7 +81,7 @@ pub async fn req(
|
||||
new_embeds.clear();
|
||||
|
||||
for embed in embeds {
|
||||
new_embeds.push(embed.clone().into_embed(db, message.id.clone()).await?);
|
||||
new_embeds.push(embed.clone().into_embed(db, &message.id).await?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,13 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
message::{Interactions, Masquerade, Reply, SendableEmbed},
|
||||
Message, User,
|
||||
},
|
||||
models::{message::DataMessageSend, Message, User},
|
||||
perms,
|
||||
types::push::MessageAuthor,
|
||||
web::idempotency::IdempotencyKey,
|
||||
Db, Error, Permission, Ref, Result, variables::delta::{MAX_ATTACHMENT_COUNT, MAX_REPLY_COUNT},
|
||||
Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataMessageSend {
|
||||
/// Unique token to prevent duplicate message sending
|
||||
///
|
||||
/// **This is deprecated and replaced by `Idempotency-Key`!**
|
||||
#[validate(length(min = 1, max = 64))]
|
||||
nonce: Option<String>,
|
||||
|
||||
/// Message content to send
|
||||
#[validate(length(min = 0, max = 2000))]
|
||||
content: Option<String>,
|
||||
/// Attachments to include in message
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
attachments: Option<Vec<String>>,
|
||||
/// Messages to reply to
|
||||
replies: Option<Vec<Reply>>,
|
||||
/// Embeds to include in message
|
||||
///
|
||||
/// Text embed content contributes to the content length cap
|
||||
#[validate(length(min = 1, max = 10))]
|
||||
embeds: Option<Vec<SendableEmbed>>,
|
||||
/// Masquerade to apply to this message
|
||||
#[validate]
|
||||
masquerade: Option<Masquerade>,
|
||||
/// Information about how this message should be interacted with
|
||||
interactions: Option<Interactions>,
|
||||
}
|
||||
|
||||
static RE_MENTION: Lazy<Regex> = Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
|
||||
|
||||
/// # Send Message
|
||||
///
|
||||
@@ -57,71 +19,22 @@ pub async fn message_send(
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataMessageSend>,
|
||||
mut idempotency: IdempotencyKey,
|
||||
idempotency: IdempotencyKey,
|
||||
) -> Result<Json<Message>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
// Validate Message is within reasonable length limits
|
||||
Message::validate_sum(&data.content, &data.embeds)?;
|
||||
|
||||
// Ensure the request is unique
|
||||
idempotency.consume_nonce(data.nonce).await?;
|
||||
|
||||
// Ensure we have permissions to send a message
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
||||
.await?;
|
||||
|
||||
// Check the message is not empty
|
||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
||||
{
|
||||
return Err(Error::EmptyMessage);
|
||||
}
|
||||
|
||||
// Ensure restrict_reactions is not specified without reactions list
|
||||
if let Some(interactions) = &data.interactions {
|
||||
if interactions.restrict_reactions {
|
||||
let disallowed = if let Some(list) = &interactions.reactions {
|
||||
list.is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if disallowed {
|
||||
return Err(Error::InvalidProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start constructing the message
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: channel.id().to_string(),
|
||||
author: user.id.clone(),
|
||||
masquerade: data.masquerade,
|
||||
interactions: data.interactions.unwrap_or_default(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// 1. Parse mentions in message.
|
||||
let mut mentions = HashSet::new();
|
||||
if let Some(content) = &data.content {
|
||||
for capture in RE_MENTION.captures_iter(content) {
|
||||
if let Some(mention) = capture.get(1) {
|
||||
mentions.insert(mention.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Verify permissions for masquerade.
|
||||
if let Some(masq) = &message.masquerade {
|
||||
// Verify permissions for masquerade
|
||||
if let Some(masq) = &data.masquerade {
|
||||
permissions
|
||||
.throw_permission(db, Permission::Masquerade)
|
||||
.await?;
|
||||
@@ -133,97 +46,37 @@ pub async fn message_send(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Ensure interactions information is correct
|
||||
message.interactions.validate(db, &mut permissions).await?;
|
||||
|
||||
// 4. Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > *MAX_REPLY_COUNT {
|
||||
return Err(Error::TooManyReplies { max: *MAX_REPLY_COUNT });
|
||||
}
|
||||
|
||||
for Reply { id, mention } in entries {
|
||||
let message = Ref::from_unchecked(id).as_message(db).await?;
|
||||
|
||||
replies.insert(message.id);
|
||||
|
||||
if mention {
|
||||
mentions.insert(message.author);
|
||||
}
|
||||
}
|
||||
// Check permissions for embeds
|
||||
if data.embeds.as_ref().is_some_and(|v| !v.is_empty()) {
|
||||
permissions
|
||||
.throw_permission(db, Permission::SendEmbeds)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !mentions.is_empty() {
|
||||
message.mentions.replace(
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter(|id| !user.has_blocked(id))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
// Check permissions for files
|
||||
if data.attachments.as_ref().is_some_and(|v| !v.is_empty()) {
|
||||
permissions
|
||||
.throw_permission(db, Permission::UploadFiles)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !replies.is_empty() {
|
||||
message
|
||||
.replies
|
||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
||||
// Ensure interactions information is correct
|
||||
if let Some(interactions) = &data.interactions {
|
||||
interactions.validate(db, &mut permissions).await?;
|
||||
}
|
||||
|
||||
// 5. Process included embeds.
|
||||
let mut embeds = vec![];
|
||||
if let Some(sendable_embeds) = data.embeds {
|
||||
for sendable_embed in sendable_embeds {
|
||||
embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?)
|
||||
}
|
||||
}
|
||||
|
||||
if !embeds.is_empty() {
|
||||
message.embeds.replace(embeds);
|
||||
}
|
||||
|
||||
// 6. Add attachments to message.
|
||||
let mut attachments = vec![];
|
||||
if let Some(ids) = &data.attachments {
|
||||
if !ids.is_empty() {
|
||||
// Create the message
|
||||
let message = channel
|
||||
.send_message(
|
||||
db,
|
||||
data,
|
||||
MessageAuthor::User(&user),
|
||||
idempotency,
|
||||
permissions
|
||||
.throw_permission(db, Permission::UploadFiles)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// ! FIXME: move this to app config
|
||||
if ids.len() > *MAX_ATTACHMENT_COUNT {
|
||||
return Err(Error::TooManyAttachments { max: *MAX_ATTACHMENT_COUNT} );
|
||||
}
|
||||
|
||||
for attachment_id in ids {
|
||||
attachments.push(
|
||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !attachments.is_empty() {
|
||||
message.attachments.replace(attachments);
|
||||
}
|
||||
|
||||
// 7. Set content
|
||||
message.content = data.content;
|
||||
|
||||
// 8. Pass-through nonce value for clients
|
||||
message.nonce = Some(idempotency.into_key());
|
||||
|
||||
message.create(db, &channel, Some(&user)).await?;
|
||||
|
||||
// Queue up a task for processing embeds
|
||||
if let Some(content) = &message.content {
|
||||
revolt_quark::tasks::process_embeds::queue(
|
||||
channel.id().to_string(),
|
||||
message.id.to_string(),
|
||||
content.clone(),
|
||||
.has_permission(db, Permission::SendEmbeds)
|
||||
.await?,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
.await?;
|
||||
|
||||
Ok(Json(message))
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ mod message_unreact;
|
||||
mod permissions_set;
|
||||
mod permissions_set_default;
|
||||
mod voice_join;
|
||||
mod webhook_create;
|
||||
mod webhook_fetch_all;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
@@ -49,6 +51,8 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
permissions_set_default::req,
|
||||
message_react::react_message,
|
||||
message_unreact::unreact_message,
|
||||
message_clear_reactions::clear_reactions
|
||||
message_clear_reactions::clear_reactions,
|
||||
webhook_create::req,
|
||||
webhook_fetch_all::req,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use revolt_database::{Database, Webhook};
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateWebhookBody {
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
name: String,
|
||||
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
avatar: Option<String>,
|
||||
}
|
||||
|
||||
/// # Creates a webhook
|
||||
///
|
||||
/// Creates a webhook which 3rd party platforms can use to send messages
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[post("/<target>/webhooks", data = "<data>")]
|
||||
pub async fn req(
|
||||
db: &State<Database>,
|
||||
legacy_db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<CreateWebhookBody>,
|
||||
) -> Result<Json<revolt_models::v0::Webhook>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let channel = target.as_channel(legacy_db).await?;
|
||||
|
||||
if !matches!(channel, Channel::TextChannel { .. } | Channel::Group { .. }) {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
permissions
|
||||
.has_permission(legacy_db, Permission::ManageWebhooks)
|
||||
.await?;
|
||||
|
||||
let webhook_id = Ulid::new().to_string();
|
||||
|
||||
let avatar = match &data.avatar {
|
||||
Some(id) => Some(
|
||||
db.find_and_use_attachment(id, "avatars", "user", &webhook_id)
|
||||
.await
|
||||
.map_err(Error::from_core)?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let webhook = Webhook {
|
||||
id: webhook_id,
|
||||
name: data.name,
|
||||
avatar,
|
||||
channel_id: channel.id().to_string(),
|
||||
token: Some(nanoid::nanoid!(64)),
|
||||
};
|
||||
|
||||
webhook.create(db).await.map_err(Error::from_core)?;
|
||||
|
||||
Ok(Json(webhook.into()))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use revolt_database::Database;
|
||||
use revolt_models::v0::Webhook;
|
||||
use revolt_quark::{models::User, perms, Db, Error, Permission, Ref, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Gets all webhooks
|
||||
///
|
||||
/// Gets all webhooks inside the channel
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[get("/<channel_id>/webhooks")]
|
||||
pub async fn req(
|
||||
db: &State<Database>,
|
||||
legacy_db: &Db,
|
||||
user: User,
|
||||
channel_id: Ref,
|
||||
) -> Result<Json<Vec<Webhook>>> {
|
||||
let channel = channel_id.as_channel(legacy_db).await?;
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
permissions
|
||||
.has_permission(legacy_db, Permission::ManageWebhooks)
|
||||
.await?;
|
||||
|
||||
Ok(Json(
|
||||
db.fetch_webhooks_for_channel(channel.id())
|
||||
.await
|
||||
.map_err(Error::from_core)?
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.collect::<Vec<Webhook>>(),
|
||||
))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use revolt_quark::variables::delta::IS_STAGING;
|
||||
use revolt_rocket_okapi::{revolt_okapi::openapi3::OpenApi, settings::OpenApiSettings};
|
||||
pub use rocket::http::Status;
|
||||
pub use rocket::response::Redirect;
|
||||
@@ -15,29 +16,53 @@ mod safety;
|
||||
mod servers;
|
||||
mod sync;
|
||||
mod users;
|
||||
mod webhooks;
|
||||
|
||||
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
let settings = OpenApiSettings::default();
|
||||
|
||||
mount_endpoints_and_merged_docs! {
|
||||
rocket, "/".to_owned(), settings,
|
||||
"/" => (vec![], custom_openapi_spec()),
|
||||
"" => openapi_get_routes_spec![root::root, root::ping],
|
||||
"/admin" => admin::routes(),
|
||||
"/users" => users::routes(),
|
||||
"/bots" => bots::routes(),
|
||||
"/channels" => channels::routes(),
|
||||
"/servers" => servers::routes(),
|
||||
"/invites" => invites::routes(),
|
||||
"/custom" => customisation::routes(),
|
||||
"/safety" => safety::routes(),
|
||||
"/auth/account" => rocket_authifier::routes::account::routes(),
|
||||
"/auth/session" => rocket_authifier::routes::session::routes(),
|
||||
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
|
||||
"/onboard" => onboard::routes(),
|
||||
"/push" => push::routes(),
|
||||
"/sync" => sync::routes(),
|
||||
};
|
||||
if *IS_STAGING {
|
||||
mount_endpoints_and_merged_docs! {
|
||||
rocket, "/".to_owned(), settings,
|
||||
"/" => (vec![], custom_openapi_spec()),
|
||||
"" => openapi_get_routes_spec![root::root, root::ping],
|
||||
"/admin" => admin::routes(),
|
||||
"/users" => users::routes(),
|
||||
"/bots" => bots::routes(),
|
||||
"/channels" => channels::routes(),
|
||||
"/servers" => servers::routes(),
|
||||
"/invites" => invites::routes(),
|
||||
"/custom" => customisation::routes(),
|
||||
"/safety" => safety::routes(),
|
||||
"/auth/account" => rocket_authifier::routes::account::routes(),
|
||||
"/auth/session" => rocket_authifier::routes::session::routes(),
|
||||
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
|
||||
"/onboard" => onboard::routes(),
|
||||
"/push" => push::routes(),
|
||||
"/sync" => sync::routes(),
|
||||
"/webhooks" => webhooks::routes()
|
||||
};
|
||||
} else {
|
||||
mount_endpoints_and_merged_docs! {
|
||||
rocket, "/".to_owned(), settings,
|
||||
"/" => (vec![], custom_openapi_spec()),
|
||||
"" => openapi_get_routes_spec![root::root, root::ping],
|
||||
"/admin" => admin::routes(),
|
||||
"/users" => users::routes(),
|
||||
"/bots" => bots::routes(),
|
||||
"/channels" => channels::routes(),
|
||||
"/servers" => servers::routes(),
|
||||
"/invites" => invites::routes(),
|
||||
"/custom" => customisation::routes(),
|
||||
"/safety" => safety::routes(),
|
||||
"/auth/account" => rocket_authifier::routes::account::routes(),
|
||||
"/auth/session" => rocket_authifier::routes::session::routes(),
|
||||
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
|
||||
"/onboard" => onboard::routes(),
|
||||
"/push" => push::routes(),
|
||||
"/sync" => sync::routes()
|
||||
};
|
||||
}
|
||||
|
||||
rocket
|
||||
}
|
||||
@@ -86,7 +111,8 @@ fn custom_openapi_spec() -> OpenApi {
|
||||
"Messaging",
|
||||
"Interactions",
|
||||
"Groups",
|
||||
"Voice"
|
||||
"Voice",
|
||||
"Webhooks",
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -158,7 +184,12 @@ fn custom_openapi_spec() -> OpenApi {
|
||||
servers: vec![
|
||||
Server {
|
||||
url: "https://api.revolt.chat".to_owned(),
|
||||
description: Some("Revolt API".to_owned()),
|
||||
description: Some("Revolt Production".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Server {
|
||||
url: "https://revolt.chat/api".to_owned(),
|
||||
description: Some("Revolt Staging".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Server {
|
||||
@@ -288,6 +319,11 @@ fn custom_openapi_spec() -> OpenApi {
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Webhooks".to_owned(),
|
||||
description: Some("Send messages from 3rd party services".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -34,9 +34,10 @@ pub async fn req(
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let username = User::validate_username(db, data.username).await?;
|
||||
let username = User::validate_username(data.username)?;
|
||||
let user = User {
|
||||
id: session.user_id,
|
||||
discriminator: User::find_discriminator(db, &username, None).await?,
|
||||
username,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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 +1,61 @@
|
||||
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")]
|
||||
pub async fn fetch_reports(db: &Db, user: User) -> Result<Json<Vec<Report>>> {
|
||||
#[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);
|
||||
}
|
||||
|
||||
db.fetch_reports().await.map(Json)
|
||||
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,82 +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 Snapshot
|
||||
///
|
||||
/// Fetch a snapshot for a given report
|
||||
#[openapi(tag = "User Safety")]
|
||||
#[get("/snapshot/<report_id>")]
|
||||
pub async fn fetch_snapshot(
|
||||
db: &Db,
|
||||
user: User,
|
||||
report_id: String,
|
||||
) -> Result<Json<SnapshotWithContext>> {
|
||||
// Must be privileged for this route
|
||||
if !user.privileged {
|
||||
return Err(Error::NotPrivileged);
|
||||
}
|
||||
|
||||
// Fetch snapshot
|
||||
let snapshot = db.fetch_snapshot(&report_id).await?;
|
||||
|
||||
// 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
|
||||
Ok(Json(SnapshotWithContext {
|
||||
snapshot,
|
||||
users,
|
||||
channels,
|
||||
server,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
}
|
||||
@@ -6,7 +6,12 @@ mod fetch_report;
|
||||
mod fetch_reports;
|
||||
mod report_content;
|
||||
|
||||
mod fetch_snapshot;
|
||||
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![
|
||||
@@ -16,6 +21,11 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
fetch_reports::fetch_reports,
|
||||
report_content::report_content,
|
||||
// Snapshots
|
||||
fetch_snapshot::fetch_snapshot
|
||||
fetch_snapshots::fetch_snapshots,
|
||||
// Strikes
|
||||
create_strike::create_strike,
|
||||
fetch_strikes::fetch_strikes,
|
||||
edit_strike::edit_strike,
|
||||
delete_strike::delete_strike
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use revolt_quark::events::client::EventV1;
|
||||
use revolt_quark::models::message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod};
|
||||
use revolt_quark::models::report::{ReportStatus, ReportedContent};
|
||||
use revolt_quark::models::snapshot::{Snapshot, SnapshotContent};
|
||||
use revolt_quark::models::{Report, User};
|
||||
@@ -38,7 +37,7 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
|
||||
|
||||
// Find the content and create a snapshot of it
|
||||
// Also retrieve any references to Files
|
||||
let (content, files): (SnapshotContent, Vec<String>) = match &data.content {
|
||||
let (snapshots, files): (Vec<SnapshotContent>, Vec<String>) = match &data.content {
|
||||
ReportedContent::Message { id, .. } => {
|
||||
let message = db.fetch_message(id).await?;
|
||||
|
||||
@@ -47,53 +46,8 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
|
||||
return Err(Error::CannotReportYourself);
|
||||
}
|
||||
|
||||
// Collect message attachments
|
||||
let files = message
|
||||
.attachments
|
||||
.as_ref()
|
||||
.map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Collect prior context
|
||||
let prior_context = db
|
||||
.fetch_messages(MessageQuery {
|
||||
filter: MessageFilter {
|
||||
channel: Some(message.channel.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
limit: Some(15),
|
||||
time_period: MessageTimePeriod::Absolute {
|
||||
before: Some(message.id.to_string()),
|
||||
after: None,
|
||||
sort: Some(MessageSort::Latest),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Collect leading context
|
||||
let leading_context = db
|
||||
.fetch_messages(MessageQuery {
|
||||
filter: MessageFilter {
|
||||
channel: Some(message.channel.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
limit: Some(15),
|
||||
time_period: MessageTimePeriod::Absolute {
|
||||
before: None,
|
||||
after: Some(message.id.to_string()),
|
||||
sort: Some(MessageSort::Oldest),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
(
|
||||
SnapshotContent::Message {
|
||||
message,
|
||||
prior_context,
|
||||
leading_context,
|
||||
},
|
||||
files,
|
||||
)
|
||||
let (snapshot, files) = SnapshotContent::generate_from_message(db, message).await?;
|
||||
(vec![snapshot], files)
|
||||
}
|
||||
ReportedContent::Server { id, .. } => {
|
||||
let server = db.fetch_server(id).await?;
|
||||
@@ -103,15 +57,10 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
|
||||
return Err(Error::CannotReportYourself);
|
||||
}
|
||||
|
||||
// Collect server's icon and banner
|
||||
let files = [&server.icon, &server.banner]
|
||||
.iter()
|
||||
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
|
||||
.collect();
|
||||
|
||||
(SnapshotContent::Server(server), files)
|
||||
let (snapshot, files) = SnapshotContent::generate_from_server(server)?;
|
||||
(vec![snapshot], files)
|
||||
}
|
||||
ReportedContent::User { id, .. } => {
|
||||
ReportedContent::User { id, message_id, .. } => {
|
||||
let reported_user = db.fetch_user(id).await?;
|
||||
|
||||
// Users cannot report themselves
|
||||
@@ -119,19 +68,25 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
|
||||
return Err(Error::CannotReportYourself);
|
||||
}
|
||||
|
||||
// Collect user's avatar and profile background
|
||||
let files = [
|
||||
reported_user.avatar.as_ref(),
|
||||
reported_user
|
||||
.profile
|
||||
.as_ref()
|
||||
.and_then(|profile| profile.background.as_ref()),
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
|
||||
.collect();
|
||||
// Determine if there is a message provided as context
|
||||
let message = if let Some(id) = message_id {
|
||||
db.fetch_message(id).await.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
(SnapshotContent::User(reported_user), files)
|
||||
let (snapshot, files) = SnapshotContent::generate_from_user(reported_user)?;
|
||||
|
||||
if let Some(message) = message {
|
||||
let (message_snapshot, message_files) =
|
||||
SnapshotContent::generate_from_message(db, message).await?;
|
||||
(
|
||||
vec![snapshot, message_snapshot],
|
||||
[files, message_files].concat(),
|
||||
)
|
||||
} else {
|
||||
(vec![snapshot], files)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -143,14 +98,17 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
|
||||
// Generate an id for the report
|
||||
let id = Ulid::new().to_string();
|
||||
|
||||
// Save a snapshot of the content
|
||||
let snapshot = Snapshot {
|
||||
id: Ulid::new().to_string(),
|
||||
report_id: id.to_string(),
|
||||
content,
|
||||
};
|
||||
// Insert all new generated snapshots
|
||||
for content in snapshots {
|
||||
// Save a snapshot of the content
|
||||
let snapshot = Snapshot {
|
||||
id: Ulid::new().to_string(),
|
||||
report_id: id.to_string(),
|
||||
content,
|
||||
};
|
||||
|
||||
db.insert_snapshot(&snapshot).await?;
|
||||
db.insert_snapshot(&snapshot).await?;
|
||||
}
|
||||
|
||||
// Save the report
|
||||
let report = Report {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use revolt_quark::{
|
||||
models::{Member, User},
|
||||
perms, Db, Error, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Deserialize, JsonSchema, FromForm)]
|
||||
pub struct OptionsQueryMembers {
|
||||
/// String to search for
|
||||
query: String,
|
||||
|
||||
/// Discourage use of this API
|
||||
experimental_api: bool,
|
||||
}
|
||||
|
||||
/// # Query members by name
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
pub struct MemberQueryResponse {
|
||||
/// List of members
|
||||
members: Vec<Member>,
|
||||
/// List of users
|
||||
users: Vec<User>,
|
||||
}
|
||||
|
||||
/// # Query members by name
|
||||
///
|
||||
/// Query members by a given name, this API is not stable and will be removed in the future.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[get("/<target>/members_experimental_query?<options..>")]
|
||||
pub async fn member_experimental_query(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: OptionsQueryMembers,
|
||||
) -> Result<Json<MemberQueryResponse>> {
|
||||
if !options.experimental_api {
|
||||
return Err(Error::InternalError);
|
||||
}
|
||||
|
||||
let server = target.as_server(db).await?;
|
||||
perms(&user).server(&server).calc(db).await?;
|
||||
|
||||
let mut members = db.fetch_all_members(&server.id).await?;
|
||||
|
||||
let mut user_ids = vec![];
|
||||
for member in &members {
|
||||
user_ids.push(member.id.user.clone());
|
||||
}
|
||||
|
||||
let mut users = User::fetch_foreign_users(db, &user_ids).await?;
|
||||
|
||||
// Ensure the lists match up exactly
|
||||
members.sort_by(|a, b| a.id.user.cmp(&b.id.user));
|
||||
users.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
|
||||
// Filter all matches
|
||||
let mut zipped_vec: Vec<(Member, User)> = members
|
||||
.into_iter()
|
||||
.zip(users)
|
||||
.filter(|(member, user)| {
|
||||
if let Some(nickname) = &member.nickname {
|
||||
nickname.contains(&options.query)
|
||||
} else {
|
||||
user.username.contains(&options.query)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort remaining matches by length
|
||||
zipped_vec.sort_by(|(member_a, user_a), (member_b, user_b)| {
|
||||
let left = member_a.nickname.as_ref().unwrap_or(&user_a.username);
|
||||
let right = member_b.nickname.as_ref().unwrap_or(&user_b.username);
|
||||
left.len().cmp(&right.len())
|
||||
});
|
||||
|
||||
// Take the first five and return them
|
||||
let (members, users) = zipped_vec.into_iter().take(10).unzip();
|
||||
Ok(Json(MemberQueryResponse { members, users }))
|
||||
}
|
||||
@@ -8,6 +8,7 @@ mod channel_create;
|
||||
mod emoji_list;
|
||||
mod invites_fetch;
|
||||
mod member_edit;
|
||||
mod member_experimental_query;
|
||||
mod member_fetch;
|
||||
mod member_fetch_all;
|
||||
mod member_remove;
|
||||
@@ -34,6 +35,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
member_remove::req,
|
||||
member_fetch::req,
|
||||
member_edit::req,
|
||||
member_experimental_query::member_experimental_query,
|
||||
ban_create::req,
|
||||
ban_remove::req,
|
||||
ban_list::req,
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Deserialize;
|
||||
|
||||
use revolt_permissions::DataPermissionsValue;
|
||||
use revolt_quark::{
|
||||
models::{server::PartialServer, Server, User},
|
||||
perms, Db, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Permission Value
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct DataSetServerDefaultPermission {
|
||||
/// Default member permission value
|
||||
permissions: u64,
|
||||
}
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
/// # Set Default Permission
|
||||
///
|
||||
/// Sets permissions for the default role in this server.
|
||||
@@ -22,7 +13,7 @@ pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataSetServerDefaultPermission>,
|
||||
data: Json<DataPermissionsValue>,
|
||||
) -> Result<Json<Server>> {
|
||||
let data = data.into_inner();
|
||||
|
||||
|
||||
@@ -1,17 +1,65 @@
|
||||
use revolt_quark::{
|
||||
models::{Server, User},
|
||||
models::{Channel, Server, User},
|
||||
perms, Db, Ref, Result,
|
||||
};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Deserialize, JsonSchema, FromForm)]
|
||||
pub struct OptionsFetchServer {
|
||||
/// Whether to include channels
|
||||
include_channels: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Fetch server route response
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum FetchServerResponse {
|
||||
JustServer(Server),
|
||||
ServerWithChannels {
|
||||
#[serde(flatten)]
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
},
|
||||
}
|
||||
|
||||
/// # Fetch Server
|
||||
///
|
||||
/// Fetch a server by its id.
|
||||
#[openapi(tag = "Server Information")]
|
||||
#[get("/<target>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Server>> {
|
||||
#[get("/<target>?<options..>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: OptionsFetchServer,
|
||||
) -> Result<Json<FetchServerResponse>> {
|
||||
let server = target.as_server(db).await?;
|
||||
perms(&user).server(&server).calc(db).await?;
|
||||
let mut perms = perms(&user).server(&server);
|
||||
perms.calc(db).await?;
|
||||
|
||||
Ok(Json(server))
|
||||
if let Some(true) = options.include_channels {
|
||||
let all_channels = db.fetch_channels(&server.channels).await?;
|
||||
let mut visible_channels = vec![];
|
||||
|
||||
for channel in all_channels {
|
||||
if perms
|
||||
.clone()
|
||||
.channel(&channel)
|
||||
.calc(db)
|
||||
.await?
|
||||
.can_view_channel()
|
||||
{
|
||||
visible_channels.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(FetchServerResponse::ServerWithChannels {
|
||||
server,
|
||||
channels: visible_channels,
|
||||
}))
|
||||
} else {
|
||||
Ok(Json(FetchServerResponse::JustServer(server)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use rocket::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
use crate::util::regex::RE_DISPLAY_NAME;
|
||||
|
||||
/// # Profile Data
|
||||
#[derive(Validate, Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct UserProfileData {
|
||||
@@ -24,6 +26,10 @@ pub struct UserProfileData {
|
||||
/// # User Data
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataEditUser {
|
||||
/// New display name
|
||||
#[serde(rename = "displayName")]
|
||||
#[validate(length(min = 2, max = 32), regex = "RE_DISPLAY_NAME")]
|
||||
display_name: Option<String>,
|
||||
/// Attachment Id for avatar
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
avatar: Option<String>,
|
||||
@@ -66,19 +72,26 @@ pub async fn req(
|
||||
|
||||
// If we want to edit a different user than self, ensure we have
|
||||
// permissions and subsequently replace the user in question
|
||||
if target.id != "@me" {
|
||||
if !user.privileged {
|
||||
if target.id != "@me" && target.id != user.id {
|
||||
let target_user = target.as_user(db).await?;
|
||||
let is_bot_owner = target_user
|
||||
.bot
|
||||
.map(|bot| bot.owner == user.id)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !is_bot_owner && !user.privileged {
|
||||
return Err(Error::NotPrivileged);
|
||||
}
|
||||
}
|
||||
|
||||
user = target.as_user(db).await?;
|
||||
// Otherwise, filter out invalid edit fields
|
||||
} else if data.badges.is_some() || data.flags.is_some() {
|
||||
if !user.privileged && (data.badges.is_some() || data.flags.is_some()) {
|
||||
return Err(Error::NotPrivileged);
|
||||
}
|
||||
|
||||
// Exit out early if nothing is changed
|
||||
if data.status.is_none()
|
||||
if data.display_name.is_none()
|
||||
&& data.status.is_none()
|
||||
&& data.profile.is_none()
|
||||
&& data.avatar.is_none()
|
||||
&& data.badges.is_none()
|
||||
|
||||
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
/// # User Lookup Information
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataSendFriendRequest {
|
||||
/// Username and discriminator combo separated by #
|
||||
username: String,
|
||||
}
|
||||
|
||||
@@ -21,12 +22,16 @@ pub async fn req(
|
||||
user: User,
|
||||
data: Json<DataSendFriendRequest>,
|
||||
) -> Result<Json<User>> {
|
||||
let mut target = db.fetch_user_by_username(&data.username).await?;
|
||||
if let Some((username, discriminator)) = data.username.split_once('#') {
|
||||
let mut target = db.fetch_user_by_username(username, discriminator).await?;
|
||||
|
||||
if user.bot.is_some() || target.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
if user.bot.is_some() || target.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
user.add_friend(db, &mut target).await?;
|
||||
Ok(Json(target.with_auto_perspective(db, &user).await))
|
||||
} else {
|
||||
Err(Error::InvalidProperty)
|
||||
}
|
||||
|
||||
user.add_friend(db, &mut target).await?;
|
||||
Ok(Json(target.with_auto_perspective(db, &user).await))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use rocket::Route;
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
|
||||
mod webhook_delete;
|
||||
mod webhook_delete_token;
|
||||
mod webhook_edit;
|
||||
mod webhook_edit_token;
|
||||
mod webhook_execute;
|
||||
mod webhook_fetch_token;
|
||||
mod webhook_fetch;
|
||||
mod webhook_execute_github;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
webhook_delete_token::webhook_delete_token,
|
||||
webhook_delete::webhook_delete,
|
||||
webhook_edit_token::webhook_edit_token,
|
||||
webhook_edit::webhook_edit,
|
||||
webhook_execute_github::webhook_execute_github,
|
||||
webhook_execute::webhook_execute,
|
||||
webhook_fetch_token::webhook_fetch_token,
|
||||
webhook_fetch::webhook_fetch,
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use revolt_database::{util::reference::Reference, Database};
|
||||
use revolt_quark::{models::User, perms, Db, Error, Permission, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Deletes a webhook
|
||||
///
|
||||
/// Deletes a webhook
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[delete("/<webhook_id>")]
|
||||
pub async fn webhook_delete(
|
||||
db: &State<Database>,
|
||||
legacy_db: &Db,
|
||||
user: User,
|
||||
webhook_id: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
|
||||
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
|
||||
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(legacy_db, Permission::ManageWebhooks)
|
||||
.await?;
|
||||
|
||||
webhook
|
||||
.delete(db)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
.map_err(Error::from_core)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use revolt_database::{util::reference::Reference, Database};
|
||||
use revolt_result::Result;
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Deletes a webhook
|
||||
///
|
||||
/// Deletes a webhook with a token
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[delete("/<webhook_id>/<token>")]
|
||||
pub async fn webhook_delete_token(
|
||||
db: &State<Database>,
|
||||
webhook_id: Reference,
|
||||
token: String,
|
||||
) -> Result<EmptyResponse> {
|
||||
let webhook = webhook_id.as_webhook(db).await?;
|
||||
webhook.assert_token(&token)?;
|
||||
webhook.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use revolt_database::{util::reference::Reference, Database, PartialWebhook};
|
||||
use revolt_models::v0::{DataEditWebhook, Webhook};
|
||||
use revolt_quark::{models::User, perms, Db, Error, Permission, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Edits a webhook
|
||||
///
|
||||
/// Edits a webhook
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[patch("/<webhook_id>", data = "<data>")]
|
||||
pub async fn webhook_edit(
|
||||
db: &State<Database>,
|
||||
legacy_db: &Db,
|
||||
webhook_id: Reference,
|
||||
user: User,
|
||||
data: Json<DataEditWebhook>,
|
||||
) -> Result<Json<Webhook>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let mut webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
|
||||
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
|
||||
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(legacy_db, Permission::ManageWebhooks)
|
||||
.await?;
|
||||
|
||||
if data.name.is_none() && data.avatar.is_none() && data.remove.is_empty() {
|
||||
return Ok(Json(webhook.into()));
|
||||
};
|
||||
|
||||
let DataEditWebhook {
|
||||
name,
|
||||
avatar,
|
||||
remove,
|
||||
} = data;
|
||||
|
||||
let mut partial = PartialWebhook {
|
||||
name,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(avatar) = avatar {
|
||||
let file = db
|
||||
.find_and_use_attachment(&avatar, "avatars", "user", &webhook.id)
|
||||
.await
|
||||
.map_err(Error::from_core)?;
|
||||
|
||||
partial.avatar = Some(file)
|
||||
}
|
||||
|
||||
webhook
|
||||
.update(db, partial, remove.into_iter().map(|v| v.into()).collect())
|
||||
.await
|
||||
.map_err(Error::from_core)?;
|
||||
|
||||
Ok(Json(webhook.into()))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use revolt_database::util::reference::Reference;
|
||||
use revolt_database::{Database, PartialWebhook};
|
||||
use revolt_models::v0::{DataEditWebhook, Webhook};
|
||||
use revolt_models::validator::Validate;
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Edits a webhook
|
||||
///
|
||||
/// Edits a webhook with a token
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[patch("/<webhook_id>/<token>", data = "<data>")]
|
||||
pub async fn webhook_edit_token(
|
||||
db: &State<Database>,
|
||||
webhook_id: Reference,
|
||||
token: String,
|
||||
data: Json<DataEditWebhook>,
|
||||
) -> Result<Json<Webhook>> {
|
||||
let data = data.into_inner();
|
||||
data.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut webhook = webhook_id.as_webhook(db).await?;
|
||||
webhook.assert_token(&token)?;
|
||||
|
||||
if data.name.is_none() && data.avatar.is_none() && data.remove.is_empty() {
|
||||
return Ok(Json(webhook.into()));
|
||||
};
|
||||
|
||||
let DataEditWebhook {
|
||||
name,
|
||||
avatar,
|
||||
remove,
|
||||
} = data;
|
||||
|
||||
let mut partial = PartialWebhook {
|
||||
name,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(avatar) = avatar {
|
||||
let file = db
|
||||
.find_and_use_attachment(&avatar, "avatars", "user", &webhook.id)
|
||||
.await?;
|
||||
|
||||
partial.avatar = Some(file)
|
||||
}
|
||||
|
||||
webhook
|
||||
.update(db, partial, remove.into_iter().map(|v| v.into()).collect())
|
||||
.await?;
|
||||
|
||||
Ok(Json(webhook.into()))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use revolt_database::{util::reference::Reference, Database};
|
||||
use revolt_quark::{
|
||||
models::message::{DataMessageSend, Message},
|
||||
types::push::MessageAuthor,
|
||||
web::idempotency::IdempotencyKey,
|
||||
Db, Error, Result,
|
||||
};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
use validator::Validate;
|
||||
|
||||
/// # Executes a webhook
|
||||
///
|
||||
/// Executes a webhook and sends a message
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[post("/<webhook_id>/<token>", data = "<data>")]
|
||||
pub async fn webhook_execute(
|
||||
db: &State<Database>,
|
||||
legacy_db: &Db,
|
||||
webhook_id: Reference,
|
||||
token: String,
|
||||
data: Json<DataMessageSend>,
|
||||
idempotency: IdempotencyKey,
|
||||
) -> Result<Json<Message>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
|
||||
webhook.assert_token(&token).map_err(Error::from_core)?;
|
||||
|
||||
// TODO: webhooks can currently always send masquerades, files, embeds, reactions (interactions)
|
||||
// TODO: they can also mention anyone
|
||||
|
||||
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
|
||||
let message = channel
|
||||
.send_message(
|
||||
legacy_db,
|
||||
data,
|
||||
MessageAuthor::Webhook(&webhook.into()),
|
||||
idempotency,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(message))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
use revolt_database::{util::reference::Reference, Database};
|
||||
use revolt_models::v0::{ResponseWebhook, Webhook};
|
||||
use revolt_quark::{models::User, perms, Db, Error, Permission, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Gets a webhook
|
||||
///
|
||||
/// Gets a webhook
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[get("/<webhook_id>")]
|
||||
pub async fn webhook_fetch(
|
||||
db: &State<Database>,
|
||||
legacy_db: &Db,
|
||||
webhook_id: Reference,
|
||||
user: User,
|
||||
) -> Result<Json<ResponseWebhook>> {
|
||||
let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
|
||||
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
|
||||
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(legacy_db, Permission::ViewChannel)
|
||||
.await?;
|
||||
|
||||
Ok(Json(std::convert::Into::<Webhook>::into(webhook).into()))
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use revolt_database::{util::reference::Reference, Database};
|
||||
use revolt_models::v0::Webhook;
|
||||
use revolt_result::Result;
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Gets a webhook
|
||||
///
|
||||
/// Gets a webhook with a token
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[get("/<webhook_id>/<token>")]
|
||||
pub async fn webhook_fetch_token(
|
||||
db: &State<Database>,
|
||||
webhook_id: Reference,
|
||||
token: String,
|
||||
) -> Result<Json<Webhook>> {
|
||||
let webhook = webhook_id.as_webhook(db).await?;
|
||||
webhook.assert_token(&token)?;
|
||||
Ok(Json(webhook.into()))
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
/// Regex for valid display names
|
||||
///
|
||||
/// Block zero width space
|
||||
/// Block newline and carriage return
|
||||
pub static RE_DISPLAY_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap());
|
||||
|
||||
/// Regex for valid usernames
|
||||
///
|
||||
/// Block zero width space
|
||||
/// Block lookalike characters
|
||||
pub static RE_USERNAME: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^[^\u200BА-Яа-яΑ-Ωα-ω@#:\n\r\[\]]+$").unwrap());
|
||||
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
|
||||
|
||||
/// Regex for valid emoji names
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-quark"
|
||||
version = "0.5.20"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -28,7 +28,7 @@ default = [ "test" ]
|
||||
# Serialisation
|
||||
revolt_optional_struct = "0.2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
validator = { version = "0.14", features = ["derive"] }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] }
|
||||
|
||||
# Formats
|
||||
@@ -93,3 +93,4 @@ sentry = "0.25.0"
|
||||
# Core
|
||||
revolt-result = { path = "../core/result", features = [ "serde", "schemas" ] }
|
||||
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }
|
||||
revolt-models = { path = "../core/models" }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use authifier::AuthifierEvent;
|
||||
use revolt_models::v0::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::channel::{FieldsChannel, PartialChannel};
|
||||
@@ -183,6 +184,7 @@ pub enum EventV1 {
|
||||
id: String,
|
||||
data: PartialUser,
|
||||
clear: Vec<FieldsUser>,
|
||||
event_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Relationship with another user changed
|
||||
@@ -213,6 +215,19 @@ pub enum EventV1 {
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
|
||||
/// New webhook
|
||||
WebhookCreate(Webhook),
|
||||
|
||||
/// Update existing webhook
|
||||
WebhookUpdate {
|
||||
id: String,
|
||||
data: PartialWebhook,
|
||||
remove: Vec<FieldsWebhook>,
|
||||
},
|
||||
|
||||
/// Delete webhook
|
||||
WebhookDelete { id: String },
|
||||
|
||||
/// New report
|
||||
ReportCreate(Report),
|
||||
|
||||
|
||||
@@ -302,6 +302,7 @@ impl State {
|
||||
..Default::default()
|
||||
},
|
||||
clear: vec![],
|
||||
event_id: Some(ulid::Ulid::new().to_string()),
|
||||
};
|
||||
|
||||
for server in self.cache.servers.keys() {
|
||||
@@ -499,6 +500,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::UserUpdate { event_id, .. } => {
|
||||
if let Some(id) = event_id {
|
||||
if self.cache.seen_events.contains(id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.cache.seen_events.put(id.to_string(), ());
|
||||
}
|
||||
|
||||
*event_id = None;
|
||||
}
|
||||
EventV1::UserRelationship { id, user, .. } => {
|
||||
self.cache.users.insert(id.clone(), user.clone());
|
||||
|
||||
@@ -508,6 +520,7 @@ impl State {
|
||||
self.remove_subscription(id);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::models::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
@@ -26,7 +28,7 @@ pub enum SubscriptionStateChange {
|
||||
/// ------------------------------------------------
|
||||
/// We can strip these objects to core information!!
|
||||
/// ------------------------------------------------
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub user_id: String,
|
||||
|
||||
@@ -34,6 +36,23 @@ pub struct Cache {
|
||||
pub channels: HashMap<String, Channel>,
|
||||
pub members: HashMap<String, Member>,
|
||||
pub servers: HashMap<String, Server>,
|
||||
|
||||
pub seen_events: LruCache<String, ()>,
|
||||
}
|
||||
|
||||
impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Cache {
|
||||
user_id: Default::default(),
|
||||
|
||||
users: Default::default(),
|
||||
channels: Default::default(),
|
||||
members: Default::default(),
|
||||
servers: Default::default(),
|
||||
|
||||
seen_events: LruCache::new(20),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Client state
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
|
||||
use crate::models::{channel::{Channel, FieldsChannel, PartialChannel}};
|
||||
use crate::{AbstractAttachment, AbstractChannel, Error, OverrideField, Result};
|
||||
|
||||
use super::super::DummyDb;
|
||||
|
||||
@@ -10,7 +10,7 @@ impl AbstractSnapshot for DummyDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_snapshot(&self, _report_id: &str) -> Result<Snapshot> {
|
||||
async fn fetch_snapshots(&self, _report_id: &str) -> Result<Vec<Snapshot>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ impl AbstractUser for DummyDb {
|
||||
Ok(User {
|
||||
id: id.into(),
|
||||
username: "username".into(),
|
||||
discriminator: "0000".into(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
|
||||
async fn fetch_user_by_username(&self, username: &str, _discriminator: &str) -> Result<User> {
|
||||
self.fetch_user(username).await
|
||||
}
|
||||
|
||||
@@ -45,8 +46,8 @@ impl AbstractUser for DummyDb {
|
||||
Ok(vec![self.fetch_user("id").await.unwrap()])
|
||||
}
|
||||
|
||||
async fn is_username_taken(&self, _username: &str) -> Result<bool> {
|
||||
Ok(false)
|
||||
async fn fetch_discriminators_in_use(&self, _username: &str) -> Result<Vec<String>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1,
|
||||
models::{
|
||||
channel::{FieldsChannel, PartialChannel},
|
||||
message::SystemMessage,
|
||||
message::{DataMessageSend, Message, Reply, SystemMessage, RE_MENTION},
|
||||
Channel,
|
||||
},
|
||||
tasks::ack::AckEvent,
|
||||
Database, Error, OverrideField, Result,
|
||||
tasks::{ack::AckEvent, process_embeds},
|
||||
types::push::MessageAuthor,
|
||||
variables::delta::{MAX_ATTACHMENT_COUNT, MAX_REPLY_COUNT, MAX_EMBED_COUNT},
|
||||
web::idempotency::IdempotencyKey,
|
||||
Database, Error, OverrideField, Ref, Result,
|
||||
};
|
||||
|
||||
impl Channel {
|
||||
@@ -394,4 +401,155 @@ impl Channel {
|
||||
_ => Err(Error::InvalidOperation),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a message in a channel
|
||||
pub async fn send_message(
|
||||
&self,
|
||||
db: &Database,
|
||||
data: DataMessageSend,
|
||||
author: MessageAuthor<'_>,
|
||||
mut idempotency: IdempotencyKey,
|
||||
generate_embeds: bool,
|
||||
) -> Result<Message> {
|
||||
Message::validate_sum(&data.content, data.embeds.as_deref().unwrap_or_default())?;
|
||||
|
||||
idempotency.consume_nonce(data.nonce).await?;
|
||||
|
||||
// Check the message is not empty
|
||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
||||
{
|
||||
return Err(Error::EmptyMessage);
|
||||
}
|
||||
|
||||
// Ensure restrict_reactions is not specified without reactions list
|
||||
if let Some(interactions) = &data.interactions {
|
||||
if interactions.restrict_reactions {
|
||||
let disallowed = if let Some(list) = &interactions.reactions {
|
||||
list.is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if disallowed {
|
||||
return Err(Error::InvalidProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (author_id, webhook) = match &author {
|
||||
MessageAuthor::User(user) => (user.id.clone(), None),
|
||||
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
|
||||
};
|
||||
|
||||
// Start constructing the message
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: self.id().to_string(),
|
||||
masquerade: data.masquerade,
|
||||
interactions: data.interactions.unwrap_or_default(),
|
||||
author: author_id,
|
||||
webhook: webhook.map(|w| w.into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Parse mentions in message.
|
||||
let mut mentions = HashSet::new();
|
||||
if let Some(content) = &data.content {
|
||||
for capture in RE_MENTION.captures_iter(content) {
|
||||
if let Some(mention) = capture.get(1) {
|
||||
mentions.insert(mention.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > *MAX_REPLY_COUNT {
|
||||
return Err(Error::TooManyReplies {
|
||||
max: *MAX_REPLY_COUNT,
|
||||
});
|
||||
}
|
||||
|
||||
for Reply { id, mention } in entries {
|
||||
let message = Ref::from_unchecked(id).as_message(db).await?;
|
||||
|
||||
if mention {
|
||||
mentions.insert(message.author.to_owned());
|
||||
}
|
||||
|
||||
replies.insert(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
if !mentions.is_empty() {
|
||||
message.mentions.replace(mentions.into_iter().collect());
|
||||
}
|
||||
|
||||
if !replies.is_empty() {
|
||||
message
|
||||
.replies
|
||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
||||
}
|
||||
|
||||
// Add attachments to message.
|
||||
let mut attachments = vec![];
|
||||
if data.attachments.as_ref().is_some_and(|v| v.len() > *MAX_ATTACHMENT_COUNT) {
|
||||
return Err(Error::TooManyAttachments {
|
||||
max: *MAX_ATTACHMENT_COUNT,
|
||||
});
|
||||
}
|
||||
|
||||
if data.embeds.as_ref().is_some_and(|v| v.len() > *MAX_EMBED_COUNT) {
|
||||
return Err(Error::TooManyEmbeds {
|
||||
max: *MAX_EMBED_COUNT
|
||||
})
|
||||
}
|
||||
|
||||
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
|
||||
attachments.push(
|
||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
if !attachments.is_empty() {
|
||||
message.attachments.replace(attachments);
|
||||
}
|
||||
|
||||
// Process included embeds.
|
||||
let mut embeds = vec![];
|
||||
for sendable_embed in data.embeds.unwrap_or_default() {
|
||||
embeds.push(sendable_embed.into_embed(db, &message_id).await?)
|
||||
}
|
||||
|
||||
if !embeds.is_empty() {
|
||||
message.embeds.replace(embeds);
|
||||
}
|
||||
|
||||
// Set content
|
||||
message.content = data.content;
|
||||
|
||||
// Pass-through nonce value for clients
|
||||
message.nonce = Some(idempotency.into_key());
|
||||
|
||||
message.create(db, self, Some(author)).await?;
|
||||
|
||||
// Queue up a task for processing embeds
|
||||
if generate_embeds {
|
||||
if let Some(content) = &message.content {
|
||||
process_embeds::queue(
|
||||
self.id().to_string(),
|
||||
message.id.to_string(),
|
||||
content.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
tasks::ack::AckEvent,
|
||||
types::{
|
||||
january::{Embed, Text},
|
||||
push::PushNotification,
|
||||
push::{MessageAuthor, PushNotification},
|
||||
},
|
||||
Database, Error, Permission, Result,
|
||||
};
|
||||
@@ -66,7 +66,7 @@ impl Message {
|
||||
&mut self,
|
||||
db: &Database,
|
||||
channel: &Channel,
|
||||
sender: Option<&User>,
|
||||
sender: Option<MessageAuthor<'_>>,
|
||||
) -> Result<()> {
|
||||
self.create_no_web_push(db, channel.id(), channel.is_direct_dm())
|
||||
.await?;
|
||||
@@ -170,20 +170,15 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Validate the sum of content of a message is under threshold
|
||||
pub fn validate_sum(
|
||||
content: &Option<String>,
|
||||
embeds: &Option<Vec<SendableEmbed>>,
|
||||
) -> Result<()> {
|
||||
pub fn validate_sum(content: &Option<String>, embeds: &[SendableEmbed]) -> Result<()> {
|
||||
let mut running_total = 0;
|
||||
if let Some(content) = content {
|
||||
running_total += content.len();
|
||||
}
|
||||
|
||||
if let Some(embeds) = embeds {
|
||||
for embed in embeds {
|
||||
if let Some(desc) = &embed.description {
|
||||
running_total += desc.len();
|
||||
}
|
||||
for embed in embeds {
|
||||
if let Some(desc) = &embed.description {
|
||||
running_total += desc.len();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +266,10 @@ impl Message {
|
||||
// Write to database
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
}
|
||||
|
||||
pub fn is_webhook(&self) -> bool {
|
||||
self.webhook.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoUsers {
|
||||
@@ -279,7 +278,11 @@ pub trait IntoUsers {
|
||||
|
||||
impl IntoUsers for Message {
|
||||
fn get_user_ids(&self) -> Vec<String> {
|
||||
let mut ids = vec![self.author.clone()];
|
||||
let mut ids = Vec::new();
|
||||
|
||||
if !self.is_webhook() {
|
||||
ids.push(self.author.clone());
|
||||
};
|
||||
|
||||
if let Some(msg) = &self.system {
|
||||
match msg {
|
||||
@@ -350,13 +353,13 @@ impl From<SystemMessage> for String {
|
||||
}
|
||||
|
||||
impl SendableEmbed {
|
||||
pub async fn into_embed(self, db: &Database, message_id: String) -> Result<Embed> {
|
||||
pub async fn into_embed(self, db: &Database, message_id: &str) -> Result<Embed> {
|
||||
self.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let media = if let Some(id) = self.media {
|
||||
Some(
|
||||
db.find_and_use_attachment(&id, "attachments", "message", &message_id)
|
||||
db.find_and_use_attachment(&id, "attachments", "message", message_id)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -26,4 +26,5 @@ pub mod users {
|
||||
|
||||
pub mod safety {
|
||||
pub mod report;
|
||||
pub mod snapshot;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
use crate::{models::report::PartialReport, models::Report, Database, Result};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
use crate::{
|
||||
models::report::PartialReport,
|
||||
models::{report::ReportStatus, Report},
|
||||
Database, Result,
|
||||
};
|
||||
|
||||
impl Report {
|
||||
/// Update report data
|
||||
pub async fn update(&mut self, db: &Database, partial: PartialReport) -> Result<()> {
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
match &mut self.status {
|
||||
ReportStatus::Created {} => {}
|
||||
ReportStatus::Rejected { closed_at, .. } => {
|
||||
if closed_at.is_none() {
|
||||
closed_at.replace(Timestamp::now_utc());
|
||||
}
|
||||
}
|
||||
ReportStatus::Resolved { closed_at } => {
|
||||
if closed_at.is_none() {
|
||||
closed_at.replace(Timestamp::now_utc());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.update_report(&self.id, &partial).await
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user