forked from jmug/stoatchat
Compare commits
37
Commits
20230531-1
...
20230603-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3365a8e482 | ||
|
|
268c2d1355 | ||
|
|
e9d6b963df | ||
|
|
f9d9447203 | ||
|
|
a0002d0b43 | ||
|
|
23188032ca | ||
|
|
f9f5a30e2c | ||
|
|
e393e17b59 | ||
|
|
a29d8f6aab | ||
|
|
1dbbc3ed8d | ||
|
|
410acb5b5c | ||
|
|
53c9160cd6 | ||
|
|
28b1170633 | ||
|
|
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
+42
-34
@@ -2837,7 +2837,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-bonfire"
|
name = "revolt-bonfire"
|
||||||
version = "0.5.20"
|
version = "0.6.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
@@ -2854,7 +2854,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-database"
|
name = "revolt-database"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-std",
|
"async-std",
|
||||||
@@ -2867,7 +2867,10 @@ dependencies = [
|
|||||||
"mongodb",
|
"mongodb",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"redis-kiss",
|
||||||
|
"revolt-models",
|
||||||
"revolt-permissions",
|
"revolt-permissions",
|
||||||
|
"revolt-presence",
|
||||||
"revolt-result",
|
"revolt-result",
|
||||||
"revolt_optional_struct",
|
"revolt_optional_struct",
|
||||||
"rocket",
|
"rocket",
|
||||||
@@ -2879,7 +2882,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-delta"
|
name = "revolt-delta"
|
||||||
version = "0.5.20"
|
version = "0.6.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"async-std",
|
"async-std",
|
||||||
@@ -2902,6 +2905,7 @@ dependencies = [
|
|||||||
"revolt-database",
|
"revolt-database",
|
||||||
"revolt-models",
|
"revolt-models",
|
||||||
"revolt-quark",
|
"revolt-quark",
|
||||||
|
"revolt-result",
|
||||||
"revolt_rocket_okapi",
|
"revolt_rocket_okapi",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_authifier",
|
"rocket_authifier",
|
||||||
@@ -2911,23 +2915,23 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"ulid 0.4.1",
|
"ulid 0.4.1",
|
||||||
"url",
|
"url",
|
||||||
"validator 0.14.0",
|
"validator 0.16.0",
|
||||||
"vergen",
|
"vergen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-models"
|
name = "revolt-models"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"revolt-database",
|
"revolt_optional_struct",
|
||||||
"revolt-presence",
|
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
"validator 0.16.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-permissions"
|
name = "revolt-permissions"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2940,7 +2944,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-presence"
|
name = "revolt-presence"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"log",
|
"log",
|
||||||
@@ -2951,7 +2955,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-quark"
|
name = "revolt-quark"
|
||||||
version = "0.5.20"
|
version = "0.6.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
@@ -2981,6 +2985,7 @@ dependencies = [
|
|||||||
"redis-kiss",
|
"redis-kiss",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"revolt-models",
|
||||||
"revolt-presence",
|
"revolt-presence",
|
||||||
"revolt-result",
|
"revolt-result",
|
||||||
"revolt_okapi",
|
"revolt_okapi",
|
||||||
@@ -2995,16 +3000,20 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ulid 0.5.0",
|
"ulid 0.5.0",
|
||||||
"validator 0.14.0",
|
"validator 0.16.0",
|
||||||
"web-push",
|
"web-push",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-result"
|
name = "revolt-result"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"revolt_okapi",
|
||||||
|
"revolt_rocket_okapi",
|
||||||
|
"rocket",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4378,23 +4387,6 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "validator"
|
name = "validator"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@@ -4411,10 +4403,26 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "validator_derive"
|
name = "validator"
|
||||||
version = "0.14.0"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"if_chain",
|
"if_chain",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@@ -4428,9 +4436,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "validator_types"
|
name = "validator_types"
|
||||||
version = "0.14.0"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ded9d97e1d42327632f5f3bae6403c04886e2de3036261ef42deebd931a6a291"
|
checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"syn 1.0.107",
|
"syn 1.0.107",
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Build Stage
|
# Build Stage
|
||||||
FROM --platform="${BUILDPLATFORM}" rustlang/rust:nightly-slim
|
FROM --platform="${BUILDPLATFORM}" rust:slim
|
||||||
USER 0:0
|
USER 0:0
|
||||||
WORKDIR /home/rust/src
|
WORKDIR /home/rust/src
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -2,11 +2,14 @@ disallowed-methods = [
|
|||||||
# Shouldn't need to access these directly
|
# Shouldn't need to access these directly
|
||||||
"revolt_database::models::bots::model::Bot::remove_field",
|
"revolt_database::models::bots::model::Bot::remove_field",
|
||||||
|
|
||||||
# Prefer to use Object::update()
|
# 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::bots::ops::AbstractBots::update_bot",
|
||||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
|
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
|
||||||
|
|
||||||
# Prefer to use Object::delete()
|
# Prefer to use Object::delete(&self)
|
||||||
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
||||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
|
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-bonfire"
|
name = "revolt-bonfire"
|
||||||
version = "0.5.20"
|
version = "0.6.0-rc.1"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-database"
|
name = "revolt-database"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
@@ -15,14 +15,17 @@ mongodb = [ "dep:mongodb", "bson" ]
|
|||||||
# ... Other
|
# ... Other
|
||||||
async-std-runtime = [ "async-std" ]
|
async-std-runtime = [ "async-std" ]
|
||||||
rocket-impl = [ "rocket", "schemars" ]
|
rocket-impl = [ "rocket", "schemars" ]
|
||||||
|
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
|
||||||
|
|
||||||
# Default Features
|
# Default Features
|
||||||
default = [ "mongodb", "async-std-runtime" ]
|
default = [ "mongodb", "async-std-runtime" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-result = { version = "0.0.2", path = "../result" }
|
revolt-result = { version = "0.6.0-rc.1", path = "../result" }
|
||||||
revolt-permissions = { version = "0.0.2", path = "../permissions", features = [ "serde" ] }
|
revolt-models = { version = "0.6.0-rc.1", path = "../models" }
|
||||||
|
revolt-presence = { version = "0.6.0-rc.1", path = "../presence" }
|
||||||
|
revolt-permissions = { version = "0.6.0-rc.1", path = "../permissions", features = [ "serde" ] }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
@@ -36,6 +39,9 @@ revolt_optional_struct = "0.2.0"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
|
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
|
||||||
|
|
||||||
|
# Events
|
||||||
|
redis-kiss = { version = "0.1.4" }
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
bson = { optional = true, version = "2.1.0" }
|
bson = { optional = true, version = "2.1.0" }
|
||||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ use std::{collections::HashMap, sync::Arc};
|
|||||||
|
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
|
|
||||||
use crate::{AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings};
|
use crate::{
|
||||||
|
AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings, Webhook,
|
||||||
|
};
|
||||||
|
|
||||||
database_derived!(
|
database_derived!(
|
||||||
/// Reference implementation
|
/// Reference implementation
|
||||||
@@ -10,6 +12,7 @@ database_derived!(
|
|||||||
pub struct ReferenceDb {
|
pub struct ReferenceDb {
|
||||||
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
|
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
|
||||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||||
|
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
|
||||||
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
||||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
pub users: Arc<Mutex<HashMap<String, User>>>,
|
||||||
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
|
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
use authifier::AuthifierEvent;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use revolt_models::v0::{FieldsWebhook, 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 channel
|
||||||
|
ChannelCreate(Channel),
|
||||||
|
|
||||||
|
/// Update existing channel
|
||||||
|
ChannelUpdate {
|
||||||
|
id: String,
|
||||||
|
data: PartialChannel,
|
||||||
|
clear: Vec<FieldsChannel>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Delete channel
|
||||||
|
ChannelDelete { id: String },
|
||||||
|
|
||||||
|
/// User joins a group
|
||||||
|
ChannelGroupJoin { id: String, user: String },
|
||||||
|
|
||||||
|
/// User leaves a group
|
||||||
|
ChannelGroupLeave { id: String, user: String },
|
||||||
|
|
||||||
|
/// User started typing in a channel
|
||||||
|
ChannelStartTyping { id: String, user: String },
|
||||||
|
|
||||||
|
/// User stopped typing in a channel
|
||||||
|
ChannelStopTyping { id: String, user: String },
|
||||||
|
|
||||||
|
/// User acknowledged message in channel
|
||||||
|
ChannelAck {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
message_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// New server
|
||||||
|
ServerCreate {
|
||||||
|
id: String,
|
||||||
|
server: Server,
|
||||||
|
channels: Vec<Channel>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 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 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 mod util;
|
||||||
pub use models::*;
|
pub use models::*;
|
||||||
|
|
||||||
|
pub mod events;
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
/// Utility function to check if a boolean value is false
|
||||||
pub fn if_false(t: &bool) -> bool {
|
pub fn if_false(t: &bool) -> bool {
|
||||||
!t
|
!t
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct MigrationInfo {
|
|||||||
revision: i32,
|
revision: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const LATEST_REVISION: i32 = 22;
|
pub const LATEST_REVISION: i32 = 23;
|
||||||
|
|
||||||
pub async fn migrate_database(db: &MongoDb) {
|
pub async fn migrate_database(db: &MongoDb) {
|
||||||
let migrations = db.col::<Document>("migrations");
|
let migrations = db.col::<Document>("migrations");
|
||||||
@@ -751,6 +751,23 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
|||||||
.unwrap();
|
.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.");
|
||||||
|
}
|
||||||
|
|
||||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||||
|
|
||||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
mod admin_migrations;
|
mod admin_migrations;
|
||||||
mod bots;
|
mod bots;
|
||||||
|
mod channel_webhooks;
|
||||||
mod files;
|
mod files;
|
||||||
mod safety_strikes;
|
mod safety_strikes;
|
||||||
mod server_members;
|
mod server_members;
|
||||||
@@ -9,6 +10,7 @@ mod users;
|
|||||||
|
|
||||||
pub use admin_migrations::*;
|
pub use admin_migrations::*;
|
||||||
pub use bots::*;
|
pub use bots::*;
|
||||||
|
pub use channel_webhooks::*;
|
||||||
pub use files::*;
|
pub use files::*;
|
||||||
pub use safety_strikes::*;
|
pub use safety_strikes::*;
|
||||||
pub use server_members::*;
|
pub use server_members::*;
|
||||||
@@ -29,6 +31,7 @@ pub trait AbstractDatabase:
|
|||||||
+ servers::AbstractServers
|
+ servers::AbstractServers
|
||||||
+ user_settings::AbstractUserSettings
|
+ user_settings::AbstractUserSettings
|
||||||
+ users::AbstractUsers
|
+ users::AbstractUsers
|
||||||
|
+ channel_webhooks::AbstractWebhooks
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ auto_derived_partial!(
|
|||||||
/// Strike Id
|
/// Strike Id
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// User Id of reported user
|
/// Id of reported user
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
|
/// Id of moderator
|
||||||
|
pub moderator_id: String,
|
||||||
|
|
||||||
/// Attached reason
|
/// Attached reason
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
@@ -19,6 +21,23 @@ auto_derived_partial!(
|
|||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
impl AccountStrike {
|
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
|
/// Update this strike
|
||||||
pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> {
|
pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> {
|
||||||
db.update_account_strike(&self.id, &partial).await?;
|
db.update_account_strike(&self.id, &partial).await?;
|
||||||
@@ -42,16 +61,15 @@ mod tests {
|
|||||||
async fn crud() {
|
async fn crud() {
|
||||||
database_test!(|db| async move {
|
database_test!(|db| async move {
|
||||||
let user_id = "user";
|
let user_id = "user";
|
||||||
let strike_a = "a";
|
|
||||||
let strike_b = "b";
|
|
||||||
|
|
||||||
let strike = AccountStrike {
|
let strike = AccountStrike::create(
|
||||||
id: strike_a.to_string(),
|
&db,
|
||||||
user_id: user_id.to_string(),
|
user_id.to_string(),
|
||||||
reason: "reason 1".to_string(),
|
"reason 1".to_string(),
|
||||||
};
|
"moderator_id".to_string(),
|
||||||
|
)
|
||||||
db.insert_account_strike(&strike).await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut updated_strike = strike.clone();
|
let mut updated_strike = strike.clone();
|
||||||
updated_strike
|
updated_strike
|
||||||
@@ -65,11 +83,12 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
db.insert_account_strike(&AccountStrike {
|
let strike2 = AccountStrike::create(
|
||||||
id: strike_b.to_string(),
|
&db,
|
||||||
user_id: user_id.to_string(),
|
user_id.to_string(),
|
||||||
reason: "reason 2".to_string(),
|
"reason 2".to_string(),
|
||||||
})
|
"moderator_id".to_string(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -81,12 +100,12 @@ mod tests {
|
|||||||
.map(|strike| strike.id)
|
.map(|strike| strike.id)
|
||||||
.collect::<HashSet<String>>();
|
.collect::<HashSet<String>>();
|
||||||
|
|
||||||
assert!(ids.contains(strike_a));
|
assert!(ids.contains(&strike.id));
|
||||||
assert!(ids.contains(strike_b));
|
assert!(ids.contains(&strike2.id));
|
||||||
|
|
||||||
let fetched_strike = strikes
|
let fetched_strike = strikes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|strike| strike.id == strike_a)
|
.find(|strike| strike.id == strike.id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(fetched_strike, updated_strike);
|
assert_eq!(fetched_strike, updated_strike);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{AccountStrike, PartialAccountStrike};
|
|||||||
|
|
||||||
use super::AbstractAccountStrikes;
|
use super::AbstractAccountStrikes;
|
||||||
|
|
||||||
static COL: &str = "bots";
|
static COL: &str = "safety_strikes";
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AbstractAccountStrikes for MongoDb {
|
impl AbstractAccountStrikes for MongoDb {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod v0;
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
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::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,
|
||||||
|
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 permissions;
|
||||||
pub mod reference;
|
pub mod reference;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-models"
|
name = "revolt-models"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
@@ -11,19 +11,18 @@ description = "Revolt Backend: API Models"
|
|||||||
[features]
|
[features]
|
||||||
serde = [ "dep:serde" ]
|
serde = [ "dep:serde" ]
|
||||||
schemas = [ "dep:schemars" ]
|
schemas = [ "dep:schemars" ]
|
||||||
from_database = [ "revolt-database", "revolt-presence" ]
|
validator = [ "dep:validator" ]
|
||||||
|
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ]
|
||||||
|
|
||||||
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
|
default = [ "serde", "partials" ]
|
||||||
|
|
||||||
default = [ "serde", "from_database" ]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Repo
|
|
||||||
revolt-database = { version = "0.0.2", path = "../database", optional = true }
|
|
||||||
revolt-presence = { version = "0.0.2", path = "../presence", optional = true }
|
|
||||||
|
|
||||||
# Serialisation
|
# Serialisation
|
||||||
|
revolt_optional_struct = { version = "0.2.0", optional = true }
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
||||||
# Spec Generation
|
# Spec Generation
|
||||||
schemars = { version = "0.8.8", optional = true }
|
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]
|
#[macro_use]
|
||||||
extern crate schemars;
|
extern crate schemars;
|
||||||
|
|
||||||
|
#[cfg(feature = "partials")]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate revolt_optional_struct;
|
||||||
|
|
||||||
|
#[cfg(feature = "validator")]
|
||||||
|
pub use validator;
|
||||||
|
|
||||||
macro_rules! auto_derived {
|
macro_rules! auto_derived {
|
||||||
( $( $item:item )+ ) => {
|
( $( $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;
|
pub mod v0;
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
/// Utility function to check if a boolean value is false
|
||||||
|
|||||||
@@ -2,29 +2,27 @@ auto_derived!(
|
|||||||
/// Account Strike
|
/// Account Strike
|
||||||
pub struct AccountStrike {
|
pub struct AccountStrike {
|
||||||
/// Strike Id
|
/// Strike Id
|
||||||
#[serde(rename = "_id")]
|
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// User Id of reported user
|
/// Id of reported user
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
|
|
||||||
/// Attached reason
|
/// Attached reason
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Strike Data
|
/// 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 {
|
pub struct DataEditAccountStrike {
|
||||||
/// New attached reason
|
/// New attached reason
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "from_database")]
|
|
||||||
impl From<revolt_database::AccountStrike> for AccountStrike {
|
|
||||||
fn from(value: revolt_database::AccountStrike) -> Self {
|
|
||||||
AccountStrike {
|
|
||||||
id: value.id,
|
|
||||||
user_id: value.user_id,
|
|
||||||
reason: value.reason,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ auto_derived!(
|
|||||||
/// Bot
|
/// Bot
|
||||||
pub struct Bot {
|
pub struct Bot {
|
||||||
/// Bot Id
|
/// Bot Id
|
||||||
#[serde(rename = "_id")]
|
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
/// User Id of the bot owner
|
/// User Id of the bot owner
|
||||||
#[serde(rename = "owner")]
|
#[cfg_attr(feature = "serde", serde(rename = "owner"))]
|
||||||
pub owner_id: String,
|
pub owner_id: String,
|
||||||
/// Token used to authenticate requests for this bot
|
/// Token used to authenticate requests for this bot
|
||||||
pub token: String,
|
pub token: String,
|
||||||
@@ -66,16 +66,16 @@ auto_derived!(
|
|||||||
pub struct PublicBot {
|
pub struct PublicBot {
|
||||||
/// Bot Id
|
/// Bot Id
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
id: String,
|
pub id: String,
|
||||||
|
|
||||||
/// Bot Username
|
/// Bot Username
|
||||||
username: String,
|
pub username: String,
|
||||||
/// Profile Avatar
|
/// Profile Avatar
|
||||||
#[serde(skip_serializing_if = "String::is_empty")]
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
avatar: String,
|
pub avatar: String,
|
||||||
/// Profile Description
|
/// Profile Description
|
||||||
#[serde(skip_serializing_if = "String::is_empty")]
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bot Response
|
/// Bot Response
|
||||||
@@ -86,39 +86,3 @@ auto_derived!(
|
|||||||
pub user: User,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,42 +54,3 @@ auto_derived!(
|
|||||||
Audio,
|
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,9 +1,11 @@
|
|||||||
mod account_strikes;
|
mod account_strikes;
|
||||||
mod bots;
|
mod bots;
|
||||||
|
mod channel_webhooks;
|
||||||
mod files;
|
mod files;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
pub use account_strikes::*;
|
pub use account_strikes::*;
|
||||||
pub use bots::*;
|
pub use bots::*;
|
||||||
|
pub use channel_webhooks::*;
|
||||||
pub use files::*;
|
pub use files::*;
|
||||||
pub use users::*;
|
pub use users::*;
|
||||||
|
|||||||
@@ -158,109 +158,3 @@ impl CheckRelationship for Vec<Relationship> {
|
|||||||
RelationshipStatus::None
|
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]
|
[package]
|
||||||
name = "revolt-permissions"
|
name = "revolt-permissions"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-presence"
|
name = "revolt-presence"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-result"
|
name = "revolt-result"
|
||||||
version = "0.0.2"
|
version = "0.6.0-rc.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
@@ -11,12 +11,20 @@ description = "Revolt Backend: Result and Error types"
|
|||||||
[features]
|
[features]
|
||||||
serde = [ "dep:serde" ]
|
serde = [ "dep:serde" ]
|
||||||
schemas = [ "dep:schemars" ]
|
schemas = [ "dep:schemars" ]
|
||||||
|
rocket = [ "dep:rocket", "dep:serde_json" ]
|
||||||
|
okapi = [ "dep:revolt_rocket_okapi", "dep:revolt_okapi" ]
|
||||||
|
|
||||||
default = [ "serde" ]
|
default = [ "serde" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Serialisation
|
# Serialisation
|
||||||
|
serde_json = { version = "1", optional = true }
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
||||||
# Spec Generation
|
# Spec Generation
|
||||||
schemars = { version = "0.8.8", optional = true }
|
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]
|
#[macro_use]
|
||||||
extern crate schemars;
|
extern crate schemars;
|
||||||
|
|
||||||
|
#[cfg(feature = "rocket")]
|
||||||
|
pub mod rocket;
|
||||||
|
|
||||||
|
#[cfg(feature = "okapi")]
|
||||||
|
pub mod okapi;
|
||||||
|
|
||||||
/// Result type with custom Error
|
/// Result type with custom Error
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
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]
|
[package]
|
||||||
name = "revolt-delta"
|
name = "revolt-delta"
|
||||||
version = "0.5.20"
|
version = "0.6.0-rc.1"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -31,7 +31,7 @@ nanoid = "0.4.0"
|
|||||||
# serde
|
# serde
|
||||||
serde_json = "1.0.57"
|
serde_json = "1.0.57"
|
||||||
serde = { version = "1.0.115", features = ["derive"] }
|
serde = { version = "1.0.115", features = ["derive"] }
|
||||||
validator = { version = "0.14", features = ["derive"] }
|
validator = { version = "0.16", features = ["derive"] }
|
||||||
|
|
||||||
# async
|
# async
|
||||||
futures = "0.3.8"
|
futures = "0.3.8"
|
||||||
@@ -56,8 +56,9 @@ revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
|
|||||||
revolt-quark = { path = "../quark" }
|
revolt-quark = { path = "../quark" }
|
||||||
|
|
||||||
# core
|
# core
|
||||||
revolt-database = { path = "../core/database", features = [ "rocket-impl" ] }
|
revolt-database = { path = "../core/database", features = [ "rocket-impl", "redis-is-patched" ] }
|
||||||
revolt-models = { path = "../core/models", features = [ "schemas", "redis-is-patched" ] }
|
revolt-models = { path = "../core/models", features = [ "schemas", "validator" ] }
|
||||||
|
revolt-result = { path = "../core/result", features = [ "rocket", "okapi" ] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
vergen = "7.5.0"
|
vergen = "7.5.0"
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ extern crate serde_json;
|
|||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
use async_std::channel::unbounded;
|
use async_std::channel::unbounded;
|
||||||
use revolt_quark::authifier::{Authifier, AuthifierEvent};
|
use revolt_quark::authifier::{Authifier, AuthifierEvent};
|
||||||
use revolt_quark::events::client::EventV1;
|
use revolt_quark::events::client::EventV1;
|
||||||
use revolt_quark::DatabaseInfo;
|
use revolt_quark::DatabaseInfo;
|
||||||
|
use rocket::data::ToByteUnit;
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
@@ -72,4 +75,9 @@ async fn rocket() -> _ {
|
|||||||
.manage(cors.clone())
|
.manage(cors.clone())
|
||||||
.attach(revolt_quark::web::ratelimiter::RatelimitFairing)
|
.attach(revolt_quark::web::ratelimiter::RatelimitFairing)
|
||||||
.attach(cors)
|
.attach(cors)
|
||||||
|
.configure(rocket::Config {
|
||||||
|
limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
|
||||||
|
address: Ipv4Addr::new(0, 0, 0, 0).into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ pub async fn fetch_bot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(FetchBotResponse {
|
Ok(Json(FetchBotResponse {
|
||||||
user: revolt_models::v0::User::from(
|
user: db
|
||||||
db.fetch_user(&bot.id).await.map_err(Error::from_core)?,
|
.fetch_user(&bot.id)
|
||||||
None,
|
.await
|
||||||
)
|
.map_err(Error::from_core)?
|
||||||
.await,
|
.into(None)
|
||||||
|
.await,
|
||||||
bot: bot.into(),
|
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)?;
|
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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ pub async fn req(
|
|||||||
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Message::validate_sum(&edit.content, &edit.embeds)?;
|
|
||||||
|
|
||||||
let mut message = msg.as_message(db).await?;
|
let mut message = msg.as_message(db).await?;
|
||||||
if message.channel != channel.id() {
|
if message.channel != channel.id() {
|
||||||
return Err(Error::NotFound);
|
return Err(Error::NotFound);
|
||||||
@@ -55,6 +53,12 @@ pub async fn req(
|
|||||||
return Err(Error::CannotEditMessage);
|
return Err(Error::CannotEditMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(new_embeds) = &edit.embeds {
|
||||||
|
Message::validate_sum(&edit.content, new_embeds)?;
|
||||||
|
} else {
|
||||||
|
Message::validate_sum(&edit.content, &vec![])?;
|
||||||
|
}
|
||||||
|
|
||||||
message.edited = Some(Timestamp::now_utc());
|
message.edited = Some(Timestamp::now_utc());
|
||||||
let mut partial = PartialMessage {
|
let mut partial = PartialMessage {
|
||||||
edited: message.edited,
|
edited: message.edited,
|
||||||
|
|||||||
@@ -1,51 +1,13 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use revolt_quark::{
|
use revolt_quark::{
|
||||||
models::{
|
models::{message::DataMessageSend, Message, User},
|
||||||
message::{Interactions, Masquerade, Reply, SendableEmbed},
|
|
||||||
Message, User,
|
|
||||||
},
|
|
||||||
perms,
|
perms,
|
||||||
|
types::push::MessageAuthor,
|
||||||
web::idempotency::IdempotencyKey,
|
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 rocket::serde::json::Json;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use ulid::Ulid;
|
|
||||||
use validator::Validate;
|
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
|
/// # Send Message
|
||||||
///
|
///
|
||||||
@@ -57,71 +19,22 @@ pub async fn message_send(
|
|||||||
user: User,
|
user: User,
|
||||||
target: Ref,
|
target: Ref,
|
||||||
data: Json<DataMessageSend>,
|
data: Json<DataMessageSend>,
|
||||||
mut idempotency: IdempotencyKey,
|
idempotency: IdempotencyKey,
|
||||||
) -> Result<Json<Message>> {
|
) -> Result<Json<Message>> {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
data.validate()
|
data.validate()
|
||||||
.map_err(|error| Error::FailedValidation { error })?;
|
.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
|
// Ensure we have permissions to send a message
|
||||||
let channel = target.as_channel(db).await?;
|
let channel = target.as_channel(db).await?;
|
||||||
|
|
||||||
let mut permissions = perms(&user).channel(&channel);
|
let mut permissions = perms(&user).channel(&channel);
|
||||||
permissions
|
permissions
|
||||||
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Check the message is not empty
|
// Verify permissions for masquerade
|
||||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
if let Some(masq) = &data.masquerade {
|
||||||
&& (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 {
|
|
||||||
permissions
|
permissions
|
||||||
.throw_permission(db, Permission::Masquerade)
|
.throw_permission(db, Permission::Masquerade)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -133,97 +46,37 @@ pub async fn message_send(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Ensure interactions information is correct
|
// Check permissions for embeds
|
||||||
message.interactions.validate(db, &mut permissions).await?;
|
if !data.embeds.is_empty() {
|
||||||
|
permissions
|
||||||
// 4. Verify replies are valid.
|
.throw_permission(db, Permission::SendEmbeds)
|
||||||
let mut replies = HashSet::new();
|
.await?;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mentions.is_empty() {
|
// Check permissions for files
|
||||||
message.mentions.replace(
|
if !data.attachments.is_empty() {
|
||||||
mentions
|
permissions
|
||||||
.into_iter()
|
.throw_permission(db, Permission::UploadFiles)
|
||||||
.filter(|id| !user.has_blocked(id))
|
.await?;
|
||||||
.collect::<Vec<String>>(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !replies.is_empty() {
|
// Ensure interactions information is correct
|
||||||
message
|
if let Some(interactions) = &data.interactions {
|
||||||
.replies
|
interactions.validate(db, &mut permissions).await?;
|
||||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Process included embeds.
|
// Create the message
|
||||||
let mut embeds = vec![];
|
let message = channel
|
||||||
if let Some(sendable_embeds) = data.embeds {
|
.send_message(
|
||||||
for sendable_embed in sendable_embeds {
|
db,
|
||||||
embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?)
|
data,
|
||||||
}
|
MessageAuthor::User(&user),
|
||||||
}
|
idempotency,
|
||||||
|
|
||||||
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() {
|
|
||||||
permissions
|
permissions
|
||||||
.throw_permission(db, Permission::UploadFiles)
|
.has_permission(db, Permission::SendEmbeds)
|
||||||
.await?;
|
.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(),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await?;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(message))
|
Ok(Json(message))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ mod message_unreact;
|
|||||||
mod permissions_set;
|
mod permissions_set;
|
||||||
mod permissions_set_default;
|
mod permissions_set_default;
|
||||||
mod voice_join;
|
mod voice_join;
|
||||||
|
mod webhook_create;
|
||||||
|
mod webhook_fetch_all;
|
||||||
|
|
||||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||||
openapi_get_routes_spec![
|
openapi_get_routes_spec![
|
||||||
@@ -49,6 +51,8 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
|||||||
permissions_set_default::req,
|
permissions_set_default::req,
|
||||||
message_react::react_message,
|
message_react::react_message,
|
||||||
message_unreact::unreact_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>>(),
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ mod safety;
|
|||||||
mod servers;
|
mod servers;
|
||||||
mod sync;
|
mod sync;
|
||||||
mod users;
|
mod users;
|
||||||
|
mod webhooks;
|
||||||
|
|
||||||
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
|
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
|
||||||
let settings = OpenApiSettings::default();
|
let settings = OpenApiSettings::default();
|
||||||
@@ -37,6 +38,7 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
|
|||||||
"/onboard" => onboard::routes(),
|
"/onboard" => onboard::routes(),
|
||||||
"/push" => push::routes(),
|
"/push" => push::routes(),
|
||||||
"/sync" => sync::routes(),
|
"/sync" => sync::routes(),
|
||||||
|
"/webhooks" => webhooks::routes()
|
||||||
};
|
};
|
||||||
|
|
||||||
rocket
|
rocket
|
||||||
@@ -86,7 +88,8 @@ fn custom_openapi_spec() -> OpenApi {
|
|||||||
"Messaging",
|
"Messaging",
|
||||||
"Interactions",
|
"Interactions",
|
||||||
"Groups",
|
"Groups",
|
||||||
"Voice"
|
"Voice",
|
||||||
|
"Webhooks",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -158,7 +161,12 @@ fn custom_openapi_spec() -> OpenApi {
|
|||||||
servers: vec![
|
servers: vec![
|
||||||
Server {
|
Server {
|
||||||
url: "https://api.revolt.chat".to_owned(),
|
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()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Server {
|
Server {
|
||||||
@@ -288,6 +296,13 @@ fn custom_openapi_spec() -> OpenApi {
|
|||||||
),
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
Tag {
|
||||||
|
name: "Webhooks".to_owned(),
|
||||||
|
description: Some(
|
||||||
|
"Send messages from 3rd party services".to_owned(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
],
|
],
|
||||||
..Default::default()
|
..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)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ mod report_content;
|
|||||||
|
|
||||||
mod fetch_snapshots;
|
mod fetch_snapshots;
|
||||||
|
|
||||||
|
mod create_strike;
|
||||||
mod delete_strike;
|
mod delete_strike;
|
||||||
mod edit_strike;
|
mod edit_strike;
|
||||||
mod fetch_strikes;
|
mod fetch_strikes;
|
||||||
@@ -22,6 +23,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
|||||||
// Snapshots
|
// Snapshots
|
||||||
fetch_snapshots::fetch_snapshots,
|
fetch_snapshots::fetch_snapshots,
|
||||||
// Strikes
|
// Strikes
|
||||||
|
create_strike::create_strike,
|
||||||
fetch_strikes::fetch_strikes,
|
fetch_strikes::fetch_strikes,
|
||||||
edit_strike::edit_strike,
|
edit_strike::edit_strike,
|
||||||
delete_strike::delete_strike
|
delete_strike::delete_strike
|
||||||
|
|||||||
@@ -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,34 @@
|
|||||||
|
use revolt_database::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: String,
|
||||||
|
) -> Result<EmptyResponse> {
|
||||||
|
let webhook = db
|
||||||
|
.fetch_webhook(&webhook_id)
|
||||||
|
.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::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: String,
|
||||||
|
token: String,
|
||||||
|
) -> Result<EmptyResponse> {
|
||||||
|
let webhook = db.fetch_webhook(&webhook_id).await?;
|
||||||
|
webhook.assert_token(&token)?;
|
||||||
|
webhook.delete(db).await.map(|_| EmptyResponse)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use revolt_database::{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: String,
|
||||||
|
user: User,
|
||||||
|
data: Json<DataEditWebhook>,
|
||||||
|
) -> Result<Json<Webhook>> {
|
||||||
|
let data = data.into_inner();
|
||||||
|
data.validate()
|
||||||
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
|
let mut webhook = db
|
||||||
|
.fetch_webhook(&webhook_id)
|
||||||
|
.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,56 @@
|
|||||||
|
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: String,
|
||||||
|
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 = db.fetch_webhook(&webhook_id).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,51 @@
|
|||||||
|
use revolt_database::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: String,
|
||||||
|
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 = db
|
||||||
|
.fetch_webhook(&webhook_id)
|
||||||
|
.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,30 @@
|
|||||||
|
use revolt_database::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: String,
|
||||||
|
user: User,
|
||||||
|
) -> Result<Json<ResponseWebhook>> {
|
||||||
|
let webhook = db
|
||||||
|
.fetch_webhook(&webhook_id)
|
||||||
|
.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::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: String,
|
||||||
|
token: String,
|
||||||
|
) -> Result<Json<Webhook>> {
|
||||||
|
let webhook = db.fetch_webhook(&webhook_id).await?;
|
||||||
|
webhook.assert_token(&token)?;
|
||||||
|
Ok(Json(webhook.into()))
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-quark"
|
name = "revolt-quark"
|
||||||
version = "0.5.20"
|
version = "0.6.0-rc.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ default = [ "test" ]
|
|||||||
# Serialisation
|
# Serialisation
|
||||||
revolt_optional_struct = "0.2.0"
|
revolt_optional_struct = "0.2.0"
|
||||||
serde = { version = "1", features = ["derive"] }
|
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"] }
|
iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] }
|
||||||
|
|
||||||
# Formats
|
# Formats
|
||||||
@@ -93,3 +93,4 @@ sentry = "0.25.0"
|
|||||||
# Core
|
# Core
|
||||||
revolt-result = { path = "../core/result", features = [ "serde", "schemas" ] }
|
revolt-result = { path = "../core/result", features = [ "serde", "schemas" ] }
|
||||||
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }
|
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }
|
||||||
|
revolt-models = { path = "../core/models" }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use authifier::AuthifierEvent;
|
use authifier::AuthifierEvent;
|
||||||
|
use revolt_models::v0::{FieldsWebhook, PartialWebhook, Webhook};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::models::channel::{FieldsChannel, PartialChannel};
|
use crate::models::channel::{FieldsChannel, PartialChannel};
|
||||||
@@ -214,6 +215,19 @@ pub enum EventV1 {
|
|||||||
/// Delete emoji
|
/// Delete emoji
|
||||||
EmojiDelete { id: String },
|
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
|
/// New report
|
||||||
ReportCreate(Report),
|
ReportCreate(Report),
|
||||||
|
|
||||||
|
|||||||
@@ -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 crate::{AbstractAttachment, AbstractChannel, Error, OverrideField, Result};
|
||||||
|
|
||||||
use super::super::DummyDb;
|
use super::super::DummyDb;
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
events::client::EventV1,
|
events::client::EventV1,
|
||||||
models::{
|
models::{
|
||||||
channel::{FieldsChannel, PartialChannel},
|
channel::{FieldsChannel, PartialChannel},
|
||||||
message::SystemMessage,
|
message::{DataMessageSend, Message, Reply, SystemMessage, RE_MENTION},
|
||||||
Channel,
|
Channel,
|
||||||
},
|
},
|
||||||
tasks::ack::AckEvent,
|
tasks::{ack::AckEvent, process_embeds},
|
||||||
Database, Error, OverrideField, Result,
|
types::push::MessageAuthor,
|
||||||
|
variables::delta::{MAX_ATTACHMENT_COUNT, MAX_REPLY_COUNT},
|
||||||
|
web::idempotency::IdempotencyKey,
|
||||||
|
Database, Error, OverrideField, Ref, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@@ -394,4 +401,149 @@ impl Channel {
|
|||||||
_ => Err(Error::InvalidOperation),
|
_ => 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)?;
|
||||||
|
|
||||||
|
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.is_empty())
|
||||||
|
&& (data.embeds.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>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process included embeds.
|
||||||
|
let mut embeds = vec![];
|
||||||
|
for sendable_embed in data.embeds {
|
||||||
|
embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !embeds.is_empty() {
|
||||||
|
message.embeds.replace(embeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add attachments to message.
|
||||||
|
let mut attachments = vec![];
|
||||||
|
if data.attachments.len() > *MAX_ATTACHMENT_COUNT {
|
||||||
|
return Err(Error::TooManyAttachments {
|
||||||
|
max: *MAX_ATTACHMENT_COUNT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for attachment_id in data.attachments {
|
||||||
|
attachments.push(
|
||||||
|
db.find_and_use_attachment(&attachment_id, "attachments", "message", &message_id)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !attachments.is_empty() {
|
||||||
|
message.attachments.replace(attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
tasks::ack::AckEvent,
|
||||||
types::{
|
types::{
|
||||||
january::{Embed, Text},
|
january::{Embed, Text},
|
||||||
push::PushNotification,
|
push::{MessageAuthor, PushNotification},
|
||||||
},
|
},
|
||||||
Database, Error, Permission, Result,
|
Database, Error, Permission, Result,
|
||||||
};
|
};
|
||||||
@@ -66,7 +66,7 @@ impl Message {
|
|||||||
&mut self,
|
&mut self,
|
||||||
db: &Database,
|
db: &Database,
|
||||||
channel: &Channel,
|
channel: &Channel,
|
||||||
sender: Option<&User>,
|
sender: Option<MessageAuthor<'_>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.create_no_web_push(db, channel.id(), channel.is_direct_dm())
|
self.create_no_web_push(db, channel.id(), channel.is_direct_dm())
|
||||||
.await?;
|
.await?;
|
||||||
@@ -170,20 +170,15 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Validate the sum of content of a message is under threshold
|
/// Validate the sum of content of a message is under threshold
|
||||||
pub fn validate_sum(
|
pub fn validate_sum(content: &Option<String>, embeds: &Vec<SendableEmbed>) -> Result<()> {
|
||||||
content: &Option<String>,
|
|
||||||
embeds: &Option<Vec<SendableEmbed>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut running_total = 0;
|
let mut running_total = 0;
|
||||||
if let Some(content) = content {
|
if let Some(content) = content {
|
||||||
running_total += content.len();
|
running_total += content.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(embeds) = embeds {
|
for embed in embeds {
|
||||||
for embed in embeds {
|
if let Some(desc) = &embed.description {
|
||||||
if let Some(desc) = &embed.description {
|
running_total += desc.len();
|
||||||
running_total += desc.len();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +266,10 @@ impl Message {
|
|||||||
// Write to database
|
// Write to database
|
||||||
db.clear_reaction(&self.id, emoji).await
|
db.clear_reaction(&self.id, emoji).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_webhook(&self) -> bool {
|
||||||
|
self.webhook.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IntoUsers {
|
pub trait IntoUsers {
|
||||||
@@ -279,7 +278,11 @@ pub trait IntoUsers {
|
|||||||
|
|
||||||
impl IntoUsers for Message {
|
impl IntoUsers for Message {
|
||||||
fn get_user_ids(&self) -> Vec<String> {
|
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 {
|
if let Some(msg) = &self.system {
|
||||||
match msg {
|
match msg {
|
||||||
|
|||||||
@@ -37,9 +37,24 @@ impl MongoDb {
|
|||||||
operation: "delete_many",
|
operation: "delete_many",
|
||||||
with: "channel_unreads",
|
with: "channel_unreads",
|
||||||
})
|
})
|
||||||
.map(|_| ())
|
.map(|_| ())?;
|
||||||
|
|
||||||
// update many attachments with parent id
|
// update many attachments with parent id
|
||||||
|
|
||||||
|
// Delete all webhooks on this channel.
|
||||||
|
self.col::<Document>("webhooks")
|
||||||
|
.delete_many(
|
||||||
|
doc! {
|
||||||
|
"channel": &id
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "delete_many",
|
||||||
|
with: "webhooks",
|
||||||
|
})
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use crate::util::regex::RE_COLOUR;
|
|||||||
|
|
||||||
use indexmap::{IndexMap, IndexSet};
|
use indexmap::{IndexMap, IndexSet};
|
||||||
use iso8601_timestamp::Timestamp;
|
use iso8601_timestamp::Timestamp;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
use revolt_models::v0::MessageWebhook;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
@@ -18,6 +21,9 @@ pub fn if_false(t: &bool) -> bool {
|
|||||||
!t
|
!t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub static RE_MENTION: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
|
||||||
|
|
||||||
/// # Reply
|
/// # Reply
|
||||||
///
|
///
|
||||||
/// Representation of a message reply before it is sent.
|
/// Representation of a message reply before it is sent.
|
||||||
@@ -30,10 +36,11 @@ pub struct Reply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Representation of a text embed before it is sent.
|
/// Representation of a text embed before it is sent.
|
||||||
#[derive(Validate, Serialize, Deserialize, JsonSchema, Clone, Debug)]
|
#[derive(Validate, Serialize, Deserialize, JsonSchema, Clone, Debug, Default)]
|
||||||
pub struct SendableEmbed {
|
pub struct SendableEmbed {
|
||||||
#[validate(length(min = 1, max = 128))]
|
#[validate(length(min = 1, max = 128))]
|
||||||
pub icon_url: Option<String>,
|
pub icon_url: Option<String>,
|
||||||
|
#[validate(length(min = 1, max = 256))]
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
#[validate(length(min = 1, max = 100))]
|
#[validate(length(min = 1, max = 100))]
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
@@ -119,9 +126,11 @@ pub struct Message {
|
|||||||
pub nonce: Option<String>,
|
pub nonce: Option<String>,
|
||||||
/// Id of the channel this message was sent in
|
/// Id of the channel this message was sent in
|
||||||
pub channel: String,
|
pub channel: String,
|
||||||
/// Id of the user that sent this message
|
/// Id of the user or webhook that sent this message
|
||||||
pub author: String,
|
pub author: String,
|
||||||
|
/// The webhook that sent this message
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub webhook: Option<MessageWebhook>,
|
||||||
/// Message content
|
/// Message content
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub content: Option<String>,
|
pub content: Option<String>,
|
||||||
@@ -247,3 +256,32 @@ pub struct AppendMessage {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub embeds: Option<Vec<Embed>>,
|
pub embeds: Option<Vec<Embed>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct DataMessageSend {
|
||||||
|
/// Unique token to prevent duplicate message sending
|
||||||
|
///
|
||||||
|
/// **This is deprecated and replaced by `Idempotency-Key`!**
|
||||||
|
#[validate(length(min = 1, max = 64))]
|
||||||
|
pub nonce: Option<String>,
|
||||||
|
|
||||||
|
/// Message content to send
|
||||||
|
#[validate(length(min = 0, max = 2000))]
|
||||||
|
pub content: Option<String>,
|
||||||
|
/// Attachments to include in message
|
||||||
|
#[serde(default)]
|
||||||
|
pub attachments: Vec<String>,
|
||||||
|
/// Messages to reply to
|
||||||
|
pub replies: Option<Vec<Reply>>,
|
||||||
|
/// Embeds to include in message
|
||||||
|
///
|
||||||
|
/// Text embed content contributes to the content length cap
|
||||||
|
#[serde(default)]
|
||||||
|
#[validate(length(min = 1, max = 10))]
|
||||||
|
pub embeds: Vec<SendableEmbed>,
|
||||||
|
/// Masquerade to apply to this message
|
||||||
|
#[validate]
|
||||||
|
pub masquerade: Option<Masquerade>,
|
||||||
|
/// Information about how this message should be interacted with
|
||||||
|
pub interactions: Option<Interactions>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
|
use crate::models::{channel::{Channel, FieldsChannel, PartialChannel}};
|
||||||
use crate::{OverrideField, Result};
|
use crate::{OverrideField, Result};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use revolt_models::v0::Webhook;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::models::{Message, User};
|
use crate::models::{Message, User};
|
||||||
@@ -25,14 +26,42 @@ pub struct PushNotification {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum MessageAuthor<'a> {
|
||||||
|
User(&'a User),
|
||||||
|
Webhook(&'a Webhook),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MessageAuthor<'a> {
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
MessageAuthor::User(user) => &user.id,
|
||||||
|
MessageAuthor::Webhook(webhook) => &webhook.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn avatar(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
MessageAuthor::User(user) => user.avatar.as_ref().map(|file| file.id.as_str()),
|
||||||
|
MessageAuthor::Webhook(webhook) => webhook.avatar.as_ref().map(|file| file.id.as_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn username(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
MessageAuthor::User(user) => &user.username,
|
||||||
|
MessageAuthor::Webhook(webhook) => &webhook.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PushNotification {
|
impl PushNotification {
|
||||||
/// Create a new notification from a given message, author and channel ID
|
/// Create a new notification from a given message, author and channel ID
|
||||||
pub fn new(msg: Message, author: Option<&User>, channel_id: &str) -> Self {
|
pub fn new(msg: Message, author: Option<MessageAuthor<'_>>, channel_id: &str) -> Self {
|
||||||
let icon = if let Some(author) = author {
|
let icon = if let Some(author) = &author {
|
||||||
if let Some(avatar) = &author.avatar {
|
if let Some(avatar) = author.avatar() {
|
||||||
format!("{}/avatars/{}", &*AUTUMN_URL, avatar.id)
|
format!("{}/avatars/{}", &*AUTUMN_URL, avatar)
|
||||||
} else {
|
} else {
|
||||||
format!("{}/users/{}/default_avatar", &*PUBLIC_URL, msg.author)
|
format!("{}/users/{}/default_avatar", &*PUBLIC_URL, author.id())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
format!("{}/assets/logo.png", &*APP_URL)
|
format!("{}/assets/logo.png", &*APP_URL)
|
||||||
@@ -59,7 +88,7 @@ impl PushNotification {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
author: author
|
author: author
|
||||||
.map(|x| x.username.to_string())
|
.map(|x| x.username().to_string())
|
||||||
.unwrap_or_else(|| "Revolt".to_string()),
|
.unwrap_or_else(|| "Revolt".to_string()),
|
||||||
icon,
|
icon,
|
||||||
image,
|
image,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
publish:
|
publish:
|
||||||
cargo publish --package revolt-result
|
cargo publish --package revolt-result
|
||||||
cargo publish --package revolt-permissions
|
cargo publish --package revolt-permissions
|
||||||
cargo publish --package revolt-database
|
|
||||||
cargo publish --package revolt-presence
|
|
||||||
cargo publish --package revolt-models
|
cargo publish --package revolt-models
|
||||||
|
cargo publish --package revolt-presence
|
||||||
|
cargo publish --package revolt-database
|
||||||
|
|||||||
Reference in New Issue
Block a user