mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-06 19:25:59 +00:00
Compare commits
22 Commits
v0.8.8
...
feat/unive
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d3373c3f | ||
|
|
a392b347a4 | ||
|
|
a186732ca5 | ||
|
|
e635bc23ec | ||
|
|
3cb7da95e3 | ||
|
|
dea0f675dd | ||
|
|
480f210ce8 | ||
|
|
caa8607468 | ||
|
|
32e6600272 | ||
|
|
5335124306 | ||
|
|
1561481eb4 | ||
|
|
8f9015a6ff | ||
|
|
ed22b3a5ce | ||
|
|
65fbd36624 | ||
|
|
050ba16d4a | ||
|
|
65bc6c8fc6 | ||
|
|
6ad3da5f35 | ||
|
|
947eb15771 | ||
|
|
f4ee35fb09 | ||
|
|
6048587d34 | ||
|
|
80cf8fc4e8 | ||
|
|
4f54227495 |
983
Cargo.lock
generated
983
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ services:
|
||||
- minio
|
||||
entrypoint: >
|
||||
/bin/sh -c "while ! /usr/bin/mc ready minio; do
|
||||
/usr/bin/mc config host add minio http://minio:9000 minioautumn minioautumn;
|
||||
/usr/bin/mc alias set minio http://minio:9000 minioautumn minioautumn;
|
||||
echo 'Waiting minio...' && sleep 1;
|
||||
done; /usr/bin/mc mb minio/revolt-uploads; exit 0;"
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ description = "Revolt Backend: Configuration"
|
||||
[features]
|
||||
anyhow = ["dep:sentry-anyhow"]
|
||||
report-macros = ["revolt-result"]
|
||||
sentry = ["dep:sentry"]
|
||||
test = ["async-std"]
|
||||
default = ["test", "anyhow"]
|
||||
default = ["test", "sentry"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
@@ -32,7 +33,7 @@ log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
# Sentry
|
||||
sentry = "0.31.5"
|
||||
sentry = { version = "0.31.5", optional = true }
|
||||
sentry-anyhow = { version = "0.38.1", optional = true }
|
||||
|
||||
# Core
|
||||
|
||||
@@ -6,10 +6,12 @@ use futures_locks::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
pub use sentry::{capture_error, capture_message, Level};
|
||||
#[cfg(feature = "anyhow")]
|
||||
pub use sentry_anyhow::capture_anyhow;
|
||||
|
||||
#[cfg(feature = "report-macros")]
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! report_error {
|
||||
( $expr: expr, $error: ident $( $tt:tt )? ) => {
|
||||
@@ -24,7 +26,7 @@ macro_rules! report_error {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "report-macros")]
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! capture_internal_error {
|
||||
( $expr: expr ) => {
|
||||
@@ -35,7 +37,7 @@ macro_rules! capture_internal_error {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "report-macros")]
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! report_internal_error {
|
||||
( $expr: expr ) => {
|
||||
@@ -419,6 +421,7 @@ pub async fn config() -> Settings {
|
||||
}
|
||||
|
||||
/// Configure logging and common Rust variables
|
||||
#[cfg(feature = "sentry")]
|
||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
@@ -444,6 +447,7 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
#[macro_export]
|
||||
macro_rules! configure {
|
||||
($application: ident) => {
|
||||
|
||||
@@ -10,12 +10,12 @@ description = "Revolt Backend: Database Implementation"
|
||||
|
||||
[features]
|
||||
# Databases
|
||||
mongodb = ["dep:mongodb", "bson"]
|
||||
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
|
||||
|
||||
# ... Other
|
||||
tasks = ["isahc", "linkify", "url-escape"]
|
||||
async-std-runtime = ["async-std"]
|
||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
||||
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
|
||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
|
||||
axum-impl = ["axum"]
|
||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||
|
||||
@@ -91,13 +91,8 @@ rocket = { version = "0.5.1", default-features = false, features = [
|
||||
revolt_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.10.0", optional = true }
|
||||
|
||||
# Notifications
|
||||
fcm_v1 = "0.3.0"
|
||||
web-push = "0.10.0"
|
||||
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0.15", features = ["rocket_impl"] }
|
||||
authifier = { version = "1.0.15" }
|
||||
|
||||
# RabbitMQ
|
||||
amqprs = { version = "1.7.0" }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
@@ -13,6 +14,7 @@ use authifier::Authifier;
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use self::mongodb::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
@@ -25,8 +27,10 @@ pub enum DatabaseInfo {
|
||||
/// Use the mock database
|
||||
Reference,
|
||||
/// Connect to MongoDB
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDb { uri: String, database_name: String },
|
||||
/// Use existing MongoDB connection
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDbFromClient(::mongodb::Client, String),
|
||||
}
|
||||
|
||||
@@ -36,6 +40,7 @@ pub enum Database {
|
||||
/// Mock database
|
||||
Reference(ReferenceDb),
|
||||
/// MongoDB database
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDb(MongoDb),
|
||||
}
|
||||
|
||||
@@ -45,7 +50,7 @@ impl DatabaseInfo {
|
||||
pub async fn connect(self) -> Result<Database, String> {
|
||||
let config = config().await;
|
||||
|
||||
Ok(match self {
|
||||
match self {
|
||||
DatabaseInfo::Auto => {
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
DatabaseInfo::Test(format!(
|
||||
@@ -53,16 +58,20 @@ impl DatabaseInfo {
|
||||
rand::thread_rng().gen_range(1_000_000..10_000_000)
|
||||
))
|
||||
.connect()
|
||||
.await?
|
||||
.await
|
||||
} else if !config.database.mongodb.is_empty() {
|
||||
DatabaseInfo::MongoDb {
|
||||
#[cfg(feature = "mongodb")]
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name: "revolt".to_string(),
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
.await;
|
||||
|
||||
#[cfg(not(feature = "mongodb"))]
|
||||
return Err("MongoDB not enabled.".to_string())
|
||||
} else {
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
DatabaseInfo::Reference.connect().await
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Test(database_name) => {
|
||||
@@ -70,30 +79,36 @@ impl DatabaseInfo {
|
||||
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
|
||||
.as_str()
|
||||
{
|
||||
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
|
||||
"REFERENCE" => DatabaseInfo::Reference.connect().await,
|
||||
"MONGODB" => {
|
||||
DatabaseInfo::MongoDb {
|
||||
#[cfg(feature = "mongodb")]
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name,
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
.await;
|
||||
|
||||
#[cfg(not(feature = "mongodb"))]
|
||||
return Err("MongoDB not enabled.".to_string())
|
||||
}
|
||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Reference => Database::Reference(Default::default()),
|
||||
DatabaseInfo::Reference => Ok(Database::Reference(Default::default())),
|
||||
#[cfg(feature = "mongodb")]
|
||||
DatabaseInfo::MongoDb { uri, database_name } => {
|
||||
let client = ::mongodb::Client::with_uri_str(uri)
|
||||
.await
|
||||
.map_err(|_| "Failed to init db connection.".to_string())?;
|
||||
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
Ok(Database::MongoDb(MongoDb(client, database_name)))
|
||||
}
|
||||
#[cfg(feature = "mongodb")]
|
||||
DatabaseInfo::MongoDbFromClient(client, database_name) => {
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
Ok(Database::MongoDb(MongoDb(client, database_name)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,12 +234,16 @@ impl Database {
|
||||
Authifier {
|
||||
database: match self {
|
||||
Database::Reference(_) => Default::default(),
|
||||
#[cfg(feature = "mongodb")]
|
||||
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: auth_config,
|
||||
#[cfg(feature = "tasks")]
|
||||
event_channel: Some(crate::tasks::authifier_relay::sender()),
|
||||
#[cfg(not(feature = "tasks"))]
|
||||
event_channel: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
database_derived!(
|
||||
#[cfg(feature = "mongodb")]
|
||||
/// MongoDB implementation
|
||||
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
||||
);
|
||||
|
||||
@@ -144,7 +144,13 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// User joins server
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
ServerMemberJoin {
|
||||
id: String,
|
||||
// Deprecated: use member.id.user
|
||||
#[deprecated = "Use member.id.user instead"]
|
||||
user: String,
|
||||
member: Member,
|
||||
},
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use serde::Deserialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use super::client::Ping;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClientMessage {
|
||||
Authenticate { token: String },
|
||||
|
||||
@@ -25,6 +25,9 @@ pub use mongodb;
|
||||
#[macro_use]
|
||||
extern crate bson;
|
||||
|
||||
#[cfg(not(feature = "async-std-runtime"))]
|
||||
compile_error!("async-std-runtime feature must be enabled.");
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! query {
|
||||
@@ -103,6 +106,7 @@ pub mod util;
|
||||
pub use models::*;
|
||||
|
||||
pub mod events;
|
||||
#[cfg(feature = "tasks")]
|
||||
pub mod tasks;
|
||||
|
||||
mod amqp;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::ChannelUnread;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -8,10 +8,13 @@ use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
|
||||
events::client::EventV1, Database, File, PartialServer,
|
||||
Server, SystemMessage, User, AMQP,
|
||||
};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
use crate::IntoDocumentPath;
|
||||
|
||||
auto_derived!(
|
||||
#[serde(tag = "channel_type")]
|
||||
pub enum Channel {
|
||||
@@ -646,10 +649,11 @@ impl Channel {
|
||||
.private(user.to_string())
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "tasks")]
|
||||
crate::tasks::ack::queue_ack(
|
||||
self.id().to_string(),
|
||||
user.to_string(),
|
||||
AckEvent::AckMessage {
|
||||
crate::tasks::ack::AckEvent::AckMessage {
|
||||
id: message.to_string(),
|
||||
},
|
||||
)
|
||||
@@ -766,6 +770,7 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
impl IntoDocumentPath for FieldsChannel {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::FileHash;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::File;
|
||||
|
||||
use super::FileUsedFor;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1,
|
||||
tasks::{self, ack::AckEvent},
|
||||
util::{
|
||||
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
|
||||
permissions::DatabasePermissionQuery,
|
||||
@@ -22,6 +21,9 @@ use crate::{
|
||||
Channel, Database, Emoji, File, User, AMQP,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tasks")]
|
||||
use crate::tasks::{self, ack::AckEvent};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Message
|
||||
pub struct Message {
|
||||
@@ -616,9 +618,11 @@ impl Message {
|
||||
.await;
|
||||
|
||||
// Update last_message_id
|
||||
#[cfg(feature = "tasks")]
|
||||
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
|
||||
|
||||
// Add mentions for affected users
|
||||
#[cfg(feature = "tasks")]
|
||||
if !mentions_elsewhere {
|
||||
if let Some(mentions) = &self.mentions {
|
||||
tasks::ack::queue_message(
|
||||
@@ -637,6 +641,7 @@ impl Message {
|
||||
}
|
||||
|
||||
// Generate embeds
|
||||
#[cfg(feature = "tasks")]
|
||||
if generate_embeds {
|
||||
if let Some(content) = &self.content {
|
||||
tasks::process_embeds::queue(
|
||||
@@ -673,10 +678,12 @@ impl Message {
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
if !self.has_suppressed_notifications()
|
||||
&& (self.mentions.is_some() || self.contains_mass_push_mention())
|
||||
{
|
||||
// send Push notifications
|
||||
#[cfg(feature = "tasks")]
|
||||
tasks::ack::queue_message(
|
||||
self.channel.to_string(),
|
||||
AckEvent::ProcessMessage {
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -38,7 +38,10 @@ pub use servers::*;
|
||||
pub use user_settings::*;
|
||||
pub use users::*;
|
||||
|
||||
use crate::{Database, MongoDb, ReferenceDb};
|
||||
use crate::{Database, ReferenceDb};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
use crate::MongoDb;
|
||||
|
||||
pub trait AbstractDatabase:
|
||||
Sync
|
||||
@@ -66,6 +69,8 @@ pub trait AbstractDatabase:
|
||||
}
|
||||
|
||||
impl AbstractDatabase for ReferenceDb {}
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
impl AbstractDatabase for MongoDb {}
|
||||
|
||||
impl std::ops::Deref for Database {
|
||||
@@ -74,6 +79,7 @@ impl std::ops::Deref for Database {
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match &self {
|
||||
Database::Reference(dummy) => dummy,
|
||||
#[cfg(feature = "mongodb")]
|
||||
Database::MongoDb(mongo) => mongo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::PolicyChange;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::Report;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::Snapshot;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{MemberCompositeKey, ServerBan};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ auto_derived_partial!(
|
||||
/// Timestamp this member is timed out until
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeout: Option<Timestamp>,
|
||||
// This value only exists in the database, not the models.
|
||||
// If it is not-None, the database layer should return None to member fetching queries.
|
||||
// pub pending_deletion_at: Option<Timestamp>
|
||||
},
|
||||
"PartialMember"
|
||||
);
|
||||
@@ -50,6 +53,7 @@ auto_derived!(
|
||||
Avatar,
|
||||
Roles,
|
||||
Timeout,
|
||||
JoinedAt,
|
||||
}
|
||||
|
||||
/// Member removal intention
|
||||
@@ -90,7 +94,7 @@ impl Member {
|
||||
return Err(create_error!(AlreadyInServer));
|
||||
}
|
||||
|
||||
let member = Member {
|
||||
let mut member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
@@ -98,7 +102,9 @@ impl Member {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_member(&member).await?;
|
||||
if let Some(updated) = db.insert_or_merge_member(&member).await? {
|
||||
member = updated;
|
||||
}
|
||||
|
||||
let should_fetch = channels.is_none();
|
||||
let mut channels = channels.unwrap_or_default();
|
||||
@@ -124,6 +130,7 @@ impl Member {
|
||||
EventV1::ServerMemberJoin {
|
||||
id: server.id.clone(),
|
||||
user: user.id.clone(),
|
||||
member: member.clone().into(),
|
||||
}
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
@@ -186,6 +193,7 @@ impl Member {
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsMember) {
|
||||
match field {
|
||||
FieldsMember::JoinedAt => (),
|
||||
FieldsMember::Avatar => self.avatar = None,
|
||||
FieldsMember::Nickname => self.nickname = None,
|
||||
FieldsMember::Roles => self.roles.clear(),
|
||||
@@ -224,7 +232,7 @@ impl Member {
|
||||
intention: RemovalIntention,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
db.delete_member(&self.id).await?;
|
||||
db.soft_delete_member(&self.id).await?;
|
||||
|
||||
EventV1::ServerMemberLeave {
|
||||
id: self.id.server.to_string(),
|
||||
@@ -260,3 +268,74 @@ impl Member {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use iso8601_timestamp::{Duration, Timestamp};
|
||||
use revolt_models::v0::DataCreateServer;
|
||||
|
||||
use crate::{Member, PartialMember, RemovalIntention, Server, User};
|
||||
|
||||
#[async_std::test]
|
||||
async fn muted_member_rejoin() {
|
||||
database_test!(|db| async move {
|
||||
match db {
|
||||
crate::Database::Reference(_) => return,
|
||||
crate::Database::MongoDb(_) => (),
|
||||
}
|
||||
let owner = User::create(&db, "Server Owner".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let kickable_user = User::create(&db, "Member".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let server = Server::create(
|
||||
&db,
|
||||
DataCreateServer {
|
||||
name: "Server".to_string(),
|
||||
description: None,
|
||||
nsfw: None,
|
||||
},
|
||||
&owner,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
Member::create(&db, &server, &owner, None).await.unwrap();
|
||||
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
kickable_member
|
||||
.update(
|
||||
&db,
|
||||
PartialMember {
|
||||
timeout: Some(Timestamp::now_utc() + Duration::minutes(5)),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(kickable_member.in_timeout());
|
||||
|
||||
kickable_member
|
||||
.remove(&db, &server, RemovalIntention::Kick, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let kickable_member = Member::create(&db, &server, &kickable_user, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
assert!(kickable_member.in_timeout())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
use ::mongodb::SessionCursor;
|
||||
#[cfg(feature = "mongodb")]
|
||||
use ::mongodb::{ClientSession, SessionCursor};
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ChunkedServerMembersGenerator {
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDb {
|
||||
session: ::mongodb::ClientSession,
|
||||
session: ClientSession,
|
||||
cursor: Option<SessionCursor<Member>>,
|
||||
},
|
||||
|
||||
@@ -22,7 +26,7 @@ pub enum ChunkedServerMembersGenerator {
|
||||
|
||||
impl ChunkedServerMembersGenerator {
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
|
||||
pub fn new_mongo(session: ClientSession, cursor: SessionCursor<Member>) -> Self {
|
||||
ChunkedServerMembersGenerator::MongoDb {
|
||||
session,
|
||||
cursor: Some(cursor),
|
||||
@@ -69,13 +73,13 @@ impl ChunkedServerMembersGenerator {
|
||||
#[async_trait]
|
||||
pub trait AbstractServerMembers: Sync + Send {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()>;
|
||||
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>>;
|
||||
|
||||
/// Fetch a server member by their id
|
||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
|
||||
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch all members in a server as an iterator
|
||||
async fn fetch_all_members_chunked(
|
||||
@@ -96,10 +100,10 @@ pub trait AbstractServerMembers: Sync + Send {
|
||||
) -> Result<ChunkedServerMembersGenerator>;
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>;
|
||||
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch member count of a server
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
|
||||
@@ -115,6 +119,14 @@ pub trait AbstractServerMembers: Sync + Send {
|
||||
remove: Vec<FieldsMember>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
/// Marks a user as no longer a member of a server, while retaining the database value.
|
||||
/// This is used to keep information such as timeouts in place, but will remove information such as join date and applied roles.
|
||||
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
|
||||
/// Forcibly delete a server member by their id.
|
||||
/// This will cancel any pending timeouts or other longer term actions, and they will not be reapplied on rejoin.
|
||||
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
|
||||
/// Fetch all members who have been marked for deletion.
|
||||
async fn remove_dangling_members(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use bson::Document;
|
||||
use futures::StreamExt;
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use mongodb::options::ReadConcern;
|
||||
use revolt_result::Result;
|
||||
|
||||
@@ -11,9 +13,42 @@ static COL: &str = "server_members";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractServerMembers for MongoDb {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
||||
query!(self, insert_one, COL, &member).map(|_| ())
|
||||
/// Insert a new server member (or use the existing member if one is found)
|
||||
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
|
||||
let existing: Result<Option<Document>> = query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.server": &member.id.server,
|
||||
"_id.user": &member.id.user,
|
||||
"pending_deletion_at": {"$exists": true}
|
||||
}
|
||||
);
|
||||
// Update the existing record if it exist, otherwise make a new record
|
||||
if existing.is_ok_and(|x| x.is_some()) {
|
||||
self.col::<Member>(COL)
|
||||
.find_one_and_update(
|
||||
doc! {
|
||||
"_id.server": &member.id.server,
|
||||
"_id.user": &member.id.user,
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"joined_at": member.joined_at.duration_since(Timestamp::UNIX_EPOCH).whole_seconds(),
|
||||
},
|
||||
"$unset": {
|
||||
"pending_deletion_at": ""
|
||||
}
|
||||
},
|
||||
)
|
||||
.return_document(mongodb::options::ReturnDocument::After)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
} else {
|
||||
query!(self, insert_one, COL, &member).map(|_| ())?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a server member by their id
|
||||
@@ -24,18 +59,20 @@ impl AbstractServerMembers for MongoDb {
|
||||
COL,
|
||||
doc! {
|
||||
"_id.server": server_id,
|
||||
"_id.user": user_id
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.server": server_id
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
@@ -139,11 +176,12 @@ impl AbstractServerMembers for MongoDb {
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.user": user_id
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
@@ -159,11 +197,12 @@ impl AbstractServerMembers for MongoDb {
|
||||
}
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
||||
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false},
|
||||
"_id.user": {
|
||||
"$in": ids
|
||||
}
|
||||
@@ -185,7 +224,8 @@ impl AbstractServerMembers for MongoDb {
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
|
||||
self.col::<Member>(COL)
|
||||
.count_documents(doc! {
|
||||
"_id.server": server_id
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map(|c| c as usize)
|
||||
@@ -196,7 +236,8 @@ impl AbstractServerMembers for MongoDb {
|
||||
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
|
||||
self.col::<Member>(COL)
|
||||
.count_documents(doc! {
|
||||
"_id.user": user_id
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map(|c| c as usize)
|
||||
@@ -225,8 +266,42 @@ impl AbstractServerMembers for MongoDb {
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Marks a member for deletion.
|
||||
/// This will remove the record if the user has no pending actions (eg. timeout),
|
||||
/// otherwise will slate the record for deletion by revolt_crond once the actions expire.
|
||||
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
let member = self.fetch_member(&id.server, &id.user).await;
|
||||
if let Ok(member) = member {
|
||||
if member.in_timeout() {
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
doc! {
|
||||
"_id.server": &id.server,
|
||||
"_id.user": &id.user,
|
||||
},
|
||||
doc! {
|
||||
"$set": {"pending_deletion_at": format!("{}", member.timeout.unwrap().format())},
|
||||
"$unset": {
|
||||
"joined_at": "",
|
||||
"avatar": "",
|
||||
"nickname": "",
|
||||
"roles": ""
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
} else {
|
||||
self.force_delete_member(id).await
|
||||
}
|
||||
} else {
|
||||
Err(create_database_error!("fetch_member", COL))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
delete_one,
|
||||
@@ -238,11 +313,25 @@ impl AbstractServerMembers for MongoDb {
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn remove_dangling_members(&self) -> Result<()> {
|
||||
let now = Timestamp::now_utc();
|
||||
let date = bson::to_bson(&now).expect("Failed to serialize timestamp");
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(doc! {
|
||||
"pending_deletion_at": {"$lt": date}
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("count_documents", COL))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsMember {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsMember::JoinedAt => "joined_at",
|
||||
FieldsMember::Avatar => "avatar",
|
||||
FieldsMember::Nickname => "nickname",
|
||||
FieldsMember::Roles => "roles",
|
||||
|
||||
@@ -8,13 +8,13 @@ use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
|
||||
#[async_trait]
|
||||
impl AbstractServerMembers for ReferenceDb {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
||||
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
if server_members.contains_key(&member.id) {
|
||||
Err(create_database_error!("insert", "member"))
|
||||
} else {
|
||||
server_members.insert(member.id.clone(), member.clone());
|
||||
Ok(())
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
@@ -105,7 +105,7 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
@@ -115,7 +115,7 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
||||
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(ids
|
||||
.iter()
|
||||
@@ -169,8 +169,26 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Soft delete a member
|
||||
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
|
||||
let member = server_members.get_mut(id);
|
||||
if let Some(member) = member {
|
||||
if member.in_timeout() {
|
||||
panic!("Soft deletion is not implemented.")
|
||||
} else if server_members.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
if server_members.remove(id).is_some() {
|
||||
Ok(())
|
||||
@@ -178,4 +196,8 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_dangling_members(&self) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::UserSettings;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||
use axum::{
|
||||
extract::{FromRef, FromRequestParts},
|
||||
http::request::Parts,
|
||||
};
|
||||
|
||||
use revolt_result::{create_error, Error, Result};
|
||||
|
||||
use crate::{Database, User};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FromRequestParts<Database> for User {
|
||||
impl<S: Send + Sync> FromRequestParts<S> for User
|
||||
where
|
||||
Database: FromRef<S>,
|
||||
{
|
||||
type Rejection = Error;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> {
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> {
|
||||
let db = Database::from_ref(state);
|
||||
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
|
||||
let bot = db.fetch_bot_by_token(bot_token).await?;
|
||||
db.fetch_user(&bot.id).await
|
||||
|
||||
@@ -4,6 +4,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -212,16 +212,34 @@ impl AbstractUsers for MongoDb {
|
||||
partial: &PartialUser,
|
||||
remove: Vec<FieldsUser>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
if remove.contains(&FieldsUser::StatusText) && partial.status.is_some() {
|
||||
// stupid-ass workaround to fix mongo conflicting the same item
|
||||
let _: Result<()> = query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
PartialUser {
|
||||
..Default::default()
|
||||
},
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ());
|
||||
|
||||
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
|
||||
} else {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
/// Set relationship with another user
|
||||
|
||||
@@ -707,6 +707,7 @@ impl From<crate::FieldsMember> for FieldsMember {
|
||||
crate::FieldsMember::Nickname => FieldsMember::Nickname,
|
||||
crate::FieldsMember::Roles => FieldsMember::Roles,
|
||||
crate::FieldsMember::Timeout => FieldsMember::Timeout,
|
||||
crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +719,7 @@ impl From<FieldsMember> for crate::FieldsMember {
|
||||
FieldsMember::Nickname => crate::FieldsMember::Nickname,
|
||||
FieldsMember::Roles => crate::FieldsMember::Roles,
|
||||
FieldsMember::Timeout => crate::FieldsMember::Timeout,
|
||||
FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,41 +14,40 @@ use crate::{
|
||||
};
|
||||
|
||||
/// Reference to some object in the database
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Reference {
|
||||
pub struct Reference<'a> {
|
||||
/// Id of object
|
||||
pub id: String,
|
||||
pub id: &'a str,
|
||||
}
|
||||
|
||||
impl Reference {
|
||||
impl<'a> Reference<'a> {
|
||||
/// Create a Ref from an unchecked string
|
||||
pub fn from_unchecked(id: String) -> Reference {
|
||||
pub fn from_unchecked(id: &'a str) -> Reference<'a> {
|
||||
Reference { id }
|
||||
}
|
||||
|
||||
/// Fetch ban from Ref
|
||||
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
|
||||
db.fetch_ban(server, &self.id).await
|
||||
db.fetch_ban(server, self.id).await
|
||||
}
|
||||
|
||||
/// Fetch bot from Ref
|
||||
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
|
||||
db.fetch_bot(&self.id).await
|
||||
db.fetch_bot(self.id).await
|
||||
}
|
||||
|
||||
/// Fetch emoji from Ref
|
||||
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
|
||||
db.fetch_emoji(&self.id).await
|
||||
db.fetch_emoji(self.id).await
|
||||
}
|
||||
|
||||
/// Fetch channel from Ref
|
||||
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
|
||||
db.fetch_channel(&self.id).await
|
||||
db.fetch_channel(self.id).await
|
||||
}
|
||||
|
||||
/// Fetch invite from Ref or create invite to server if discoverable
|
||||
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
||||
if ulid::Ulid::from_str(&self.id).is_ok() {
|
||||
if ulid::Ulid::from_str(self.id).is_ok() {
|
||||
let server = self.as_server(db).await?;
|
||||
if !server.discoverable {
|
||||
return Err(create_error!(NotFound));
|
||||
@@ -65,18 +64,18 @@ impl Reference {
|
||||
.ok_or(create_error!(NotFound))?,
|
||||
})
|
||||
} else {
|
||||
db.fetch_invite(&self.id).await
|
||||
db.fetch_invite(self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch message from Ref
|
||||
pub async fn as_message(&self, db: &Database) -> Result<Message> {
|
||||
db.fetch_message(&self.id).await
|
||||
db.fetch_message(self.id).await
|
||||
}
|
||||
|
||||
/// Fetch message from Ref and validate channel
|
||||
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
|
||||
let msg = db.fetch_message(&self.id).await?;
|
||||
let msg = db.fetch_message(self.id).await?;
|
||||
if msg.channel != channel {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
@@ -86,36 +85,36 @@ impl Reference {
|
||||
|
||||
/// Fetch member from Ref
|
||||
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
|
||||
db.fetch_member(server, &self.id).await
|
||||
db.fetch_member(server, self.id).await
|
||||
}
|
||||
|
||||
/// Fetch server from Ref
|
||||
pub async fn as_server(&self, db: &Database) -> Result<Server> {
|
||||
db.fetch_server(&self.id).await
|
||||
db.fetch_server(self.id).await
|
||||
}
|
||||
|
||||
/// Fetch user from Ref
|
||||
pub async fn as_user(&self, db: &Database) -> Result<User> {
|
||||
db.fetch_user(&self.id).await
|
||||
db.fetch_user(self.id).await
|
||||
}
|
||||
|
||||
/// Fetch webhook from Ref
|
||||
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
|
||||
db.fetch_webhook(&self.id).await
|
||||
db.fetch_webhook(self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
impl<'r> FromParam<'r> for Reference {
|
||||
impl<'r> FromParam<'r> for Reference<'r> {
|
||||
type Error = &'r str;
|
||||
|
||||
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||
Ok(Reference::from_unchecked(param.into()))
|
||||
Ok(Reference::from_unchecked(param))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
impl JsonSchema for Reference {
|
||||
impl<'a> JsonSchema for Reference<'a> {
|
||||
fn schema_name() -> String {
|
||||
"Id".to_string()
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String>
|
||||
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
|
||||
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
|
||||
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
|
||||
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
|
||||
LoadedFixture::ServerMember(member) => {
|
||||
db.insert_or_merge_member(&member).await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,9 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
|
||||
.decrypt_in_place(nonce, b"", &mut buf)
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
// Remove the authentication tag bytes that were added during encryption
|
||||
buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES);
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
|
||||
@@ -132,8 +132,8 @@ auto_derived!(
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
|
||||
pub interactions_url: Option<String>,
|
||||
/// Fields to remove from bot object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsBot>>,
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Vec<FieldsBot>,
|
||||
}
|
||||
|
||||
/// Where we are inviting a bot to
|
||||
|
||||
@@ -207,7 +207,7 @@ auto_derived!(
|
||||
|
||||
/// Fields to remove from channel
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Option<Vec<FieldsChannel>>,
|
||||
pub remove: Vec<FieldsChannel>,
|
||||
}
|
||||
|
||||
/// Create new group
|
||||
|
||||
@@ -215,7 +215,7 @@ auto_derived!(
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct SendableEmbed {
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
|
||||
pub icon_url: Option<String>,
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
|
||||
pub url: Option<String>,
|
||||
|
||||
@@ -77,6 +77,7 @@ auto_derived!(
|
||||
Avatar,
|
||||
Roles,
|
||||
Timeout,
|
||||
JoinedAt,
|
||||
}
|
||||
|
||||
/// Member removal intention
|
||||
@@ -124,7 +125,7 @@ auto_derived!(
|
||||
/// Timestamp this member is timed out until
|
||||
pub timeout: Option<Timestamp>,
|
||||
/// Fields to remove from channel object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsMember>>,
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Vec<FieldsMember>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -175,6 +175,8 @@ auto_derived!(
|
||||
/// Ranking position
|
||||
///
|
||||
/// Smaller values take priority.
|
||||
///
|
||||
/// **Removed** - no effect, use the edit server role positions route
|
||||
pub rank: Option<i64>,
|
||||
}
|
||||
|
||||
@@ -247,8 +249,8 @@ auto_derived!(
|
||||
pub analytics: Option<bool>,
|
||||
|
||||
/// Fields to remove from server object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsServer>>,
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Vec<FieldsServer>,
|
||||
}
|
||||
|
||||
/// New role information
|
||||
@@ -270,8 +272,8 @@ auto_derived!(
|
||||
/// **Removed** - no effect, use the edit server role positions route
|
||||
pub rank: Option<i64>,
|
||||
/// Fields to remove from role object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsRole>>,
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Vec<FieldsRole>,
|
||||
}
|
||||
|
||||
/// New role permissions
|
||||
|
||||
@@ -245,8 +245,8 @@ auto_derived!(
|
||||
pub flags: Option<i32>,
|
||||
|
||||
/// Fields to remove from user object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsUser>>,
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Vec<FieldsUser>,
|
||||
}
|
||||
|
||||
/// User flag reponse
|
||||
@@ -255,12 +255,14 @@ auto_derived!(
|
||||
pub flags: i32,
|
||||
}
|
||||
|
||||
/// Mutual friends and servers response
|
||||
/// Mutual friends, servers, groups and DMs response
|
||||
pub struct MutualResponse {
|
||||
/// Array of mutual user IDs that both users are friends with
|
||||
pub users: Vec<String>,
|
||||
/// Array of mutual server IDs that both users are in
|
||||
pub servers: Vec<String>,
|
||||
/// Array of mutual group and dm IDs that both users are in
|
||||
pub channels: Vec<String>,
|
||||
}
|
||||
|
||||
/// Bot information for if the user is a bot
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
name = "revolt-parser"
|
||||
version = "0.8.8"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Message Parser"
|
||||
|
||||
[dependencies]
|
||||
|
||||
9
crates/core/parser/LICENSE
Normal file
9
crates/core/parser/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -4,15 +4,18 @@ use logos::Logos;
|
||||
|
||||
#[derive(Debug, Clone, Logos, PartialEq)]
|
||||
#[logos(skip "\n")]
|
||||
pub enum MessageToken {
|
||||
#[logos(subpattern id="[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")]
|
||||
pub enum MessageToken<'a> {
|
||||
#[token("\\")]
|
||||
Escape,
|
||||
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())]
|
||||
#[regex("```[^`\n]*", |_| 3)]
|
||||
#[regex("``", |_| 2)]
|
||||
#[regex("`", |_| 1)]
|
||||
CodeblockMarker(usize),
|
||||
#[regex("<@[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
|
||||
UserMention(String),
|
||||
#[regex("<%[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
|
||||
RoleMention(String),
|
||||
#[regex("<@(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1])]
|
||||
UserMention(&'a str),
|
||||
#[regex("<%(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1],)]
|
||||
RoleMention(&'a str),
|
||||
#[token("@everyone")]
|
||||
MentionEveryone,
|
||||
#[token("@online")]
|
||||
@@ -27,13 +30,13 @@ pub struct MessageResults {
|
||||
pub mentions_online: bool
|
||||
}
|
||||
|
||||
struct MessageParserIterator<I> {
|
||||
struct MessageParserIterator<'a, I> {
|
||||
inner: I,
|
||||
temp: VecDeque<MessageToken>
|
||||
temp: VecDeque<MessageToken<'a>>
|
||||
}
|
||||
|
||||
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> {
|
||||
type Item = MessageToken;
|
||||
impl<'a, I: Iterator<Item = MessageToken<'a>>> Iterator for MessageParserIterator<'a, I> {
|
||||
type Item = MessageToken<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.temp.is_empty() {
|
||||
@@ -80,8 +83,8 @@ pub fn parse_message(text: &str) -> MessageResults {
|
||||
match token {
|
||||
MessageToken::Escape => {}
|
||||
MessageToken::CodeblockMarker(_) => {},
|
||||
MessageToken::UserMention(id) => { results.user_mentions.insert(id); },
|
||||
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); },
|
||||
MessageToken::UserMention(id) => { results.user_mentions.insert(id.to_string()); },
|
||||
MessageToken::RoleMention(id) => { results.role_mentions.insert(id.to_string()); },
|
||||
MessageToken::MentionEveryone => results.mentions_everyone = true,
|
||||
MessageToken::MentionOnline => results.mentions_online = true,
|
||||
};
|
||||
@@ -106,7 +109,7 @@ mod tests {
|
||||
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(output.len(), 1);
|
||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -114,7 +117,7 @@ mod tests {
|
||||
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(output.len(), 1);
|
||||
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -138,8 +141,8 @@ mod tests {
|
||||
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(output.len(), 4);
|
||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
assert_eq!(output[2], MessageToken::MentionEveryone);
|
||||
assert_eq!(output[3], MessageToken::MentionOnline);
|
||||
}
|
||||
@@ -149,8 +152,8 @@ mod tests {
|
||||
let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(output.len(), 4);
|
||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
assert_eq!(output[2], MessageToken::MentionEveryone);
|
||||
assert_eq!(output[3], MessageToken::MentionOnline);
|
||||
}
|
||||
@@ -170,15 +173,14 @@ mod tests {
|
||||
|
||||
assert_eq!(output.len(), 5);
|
||||
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
|
||||
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
assert_eq!(output[3], MessageToken::MentionEveryone);
|
||||
assert_eq!(output[4], MessageToken::MentionOnline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inline_codeblock_no_mentions() {
|
||||
|
||||
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(output.len(), 2);
|
||||
@@ -192,12 +194,21 @@ mod tests {
|
||||
|
||||
assert_eq!(output.len(), 5);
|
||||
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
|
||||
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
||||
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||
assert_eq!(output[3], MessageToken::MentionEveryone);
|
||||
assert_eq!(output[4], MessageToken::MentionOnline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codeblock_with_language_no_mentions() {
|
||||
let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online```").collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(output.len(), 2);
|
||||
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
|
||||
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_double_inline_codeblock() {
|
||||
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();
|
||||
|
||||
@@ -8,10 +8,18 @@ pub use server::*;
|
||||
pub use user::*;
|
||||
|
||||
/// Holds a permission value to manipulate.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct PermissionValue(u64);
|
||||
|
||||
impl PermissionValue {
|
||||
pub fn from_raw(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub fn into_raw(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Apply a given override to this value
|
||||
pub fn apply(&mut self, v: Override) {
|
||||
self.allow(v.allow);
|
||||
|
||||
26
crates/core/ratelimits/Cargo.toml
Normal file
26
crates/core/ratelimits/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "revolt-ratelimits"
|
||||
version = "0.8.8"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
rocket = ["dep:rocket", "dep:revolt_rocket_okapi", "revolt-database/rocket-impl"]
|
||||
axum = ["dep:axum", "revolt-database/axum-impl"]
|
||||
|
||||
default = ["rocket", "axum"]
|
||||
|
||||
[dependencies]
|
||||
revolt-database = { version = "0.8.8", path = "../database"}
|
||||
revolt-result = { version = "0.8.8", path = "../result" }
|
||||
revolt-config = { version = "0.8.8", path = "../config" }
|
||||
|
||||
rocket = { version = "0.5.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.10.0", optional = true }
|
||||
|
||||
axum = { version = "0.7.5", optional = true, features = ["macros"] }
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
authifier = { version = "1.0.15" }
|
||||
dashmap = "5.2.0"
|
||||
async-trait = "0.1.81"
|
||||
log = "0.4"
|
||||
194
crates/core/ratelimits/src/axum.rs
Normal file
194
crates/core/ratelimits/src/axum.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
Json, RequestPartsExt, Router,
|
||||
body::Body,
|
||||
extract::{ConnectInfo, FromRef, FromRequestParts, State},
|
||||
http::{HeaderValue, Request, StatusCode, request::Parts},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
};
|
||||
use revolt_database::{Database, User};
|
||||
use revolt_config::config;
|
||||
|
||||
use crate::ratelimiter::{RatelimitInformation, Ratelimiter, RequestKind};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct AxumRequestKind;
|
||||
|
||||
impl RequestKind for AxumRequestKind {
|
||||
type R<'a> = Parts;
|
||||
}
|
||||
|
||||
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<AxumRequestKind>;
|
||||
|
||||
fn to_ip(parts: &Parts) -> String {
|
||||
parts
|
||||
.extensions
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|info| info.ip().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn to_real_ip(parts: &Parts) -> String {
|
||||
if config().await.api.security.trust_cloudflare {
|
||||
parts
|
||||
.headers
|
||||
.get("CF-Connecting-IP")
|
||||
.map(|x| x.to_str().unwrap().to_string())
|
||||
.unwrap_or_else(|| to_ip(parts))
|
||||
} else {
|
||||
to_ip(parts)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S: Send + Sync> FromRequestParts<S> for Ratelimiter
|
||||
where
|
||||
Database: FromRef<S>,
|
||||
RatelimitStorage: FromRef<S>,
|
||||
{
|
||||
type Rejection = Json<Ratelimiter>;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
if parts
|
||||
.extensions
|
||||
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
|
||||
.is_none()
|
||||
{
|
||||
let storage = RatelimitStorage::from_ref(state);
|
||||
|
||||
let identifier = if let Ok(user) = parts.extract_with_state::<User, _>(state).await {
|
||||
user.id
|
||||
} else {
|
||||
to_real_ip(parts).await
|
||||
};
|
||||
|
||||
let (bucket, resource) = storage.resolver.resolve_bucket(parts);
|
||||
let limit = storage.resolver.resolve_bucket_limit(bucket);
|
||||
|
||||
let ratelimiter =
|
||||
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource));
|
||||
|
||||
parts.extensions.insert(ratelimiter.map_err(Json));
|
||||
};
|
||||
|
||||
*parts
|
||||
.extensions
|
||||
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S: Send + Sync> FromRequestParts<S> for RatelimitInformation
|
||||
where
|
||||
Database: FromRef<S>,
|
||||
RatelimitStorage: FromRef<S>,
|
||||
{
|
||||
type Rejection = Json<RatelimitInformation>;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
if parts
|
||||
.extensions
|
||||
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
|
||||
.is_none()
|
||||
{
|
||||
let ratelimiter = parts.extract_with_state::<Ratelimiter, S>(state).await;
|
||||
|
||||
parts.extensions.insert(ratelimiter);
|
||||
};
|
||||
|
||||
let ratelimiter = *parts
|
||||
.extensions
|
||||
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
|
||||
.unwrap();
|
||||
|
||||
match ratelimiter {
|
||||
Ok(ratelimter) => Ok(RatelimitInformation::Success(ratelimter)),
|
||||
Err(ratelimiter) => Err(Json(RatelimitInformation::Failure {
|
||||
retry_after: ratelimiter.reset,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ratelimit_middleware(
|
||||
State(database): State<Database>,
|
||||
State(ratelimit_storage): State<RatelimitStorage>,
|
||||
request: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
#[derive(axum::extract::FromRef)]
|
||||
struct TempState {
|
||||
database: Database,
|
||||
ratelimit_storage: RatelimitStorage,
|
||||
}
|
||||
|
||||
let state = TempState {
|
||||
database,
|
||||
ratelimit_storage,
|
||||
};
|
||||
|
||||
let (mut parts, body) = request.into_parts();
|
||||
|
||||
let res = Ratelimiter::from_request_parts(&mut parts, &state).await;
|
||||
|
||||
let (Ok(ratelimiter) | Err(Json(ratelimiter))) = &res;
|
||||
|
||||
let mut response = if res.is_ok() {
|
||||
let request = Request::from_parts(parts, body);
|
||||
|
||||
next.run(request).await
|
||||
} else {
|
||||
let ratelimit_info = RatelimitInformation::from_request_parts(&mut parts, &state).await;
|
||||
|
||||
ratelimit_info.map(Json).into_response()
|
||||
};
|
||||
|
||||
let Ratelimiter {
|
||||
key,
|
||||
limit,
|
||||
remaining,
|
||||
reset,
|
||||
} = ratelimiter;
|
||||
|
||||
let headers = response.headers_mut();
|
||||
|
||||
headers.insert(
|
||||
"X-RateLimit-Limit",
|
||||
HeaderValue::from_str(&limit.to_string()).unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
"X-RateLimit-Bucket",
|
||||
HeaderValue::from_str(&key.to_string()).unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
"X-RateLimit-Remaining",
|
||||
HeaderValue::from_str(&remaining.to_string()).unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
"X-RateLimit-Reset-After",
|
||||
HeaderValue::from_str(&reset.to_string()).unwrap(),
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
*response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
|
||||
};
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
async fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
|
||||
Json(info)
|
||||
}
|
||||
|
||||
pub fn routes<S: Clone + Send + Sync + 'static>() -> Router<S>
|
||||
where
|
||||
Database: FromRef<S>,
|
||||
RatelimitStorage: FromRef<S>,
|
||||
{
|
||||
Router::new().route("/ratelimit", get(ratelimit_info))
|
||||
}
|
||||
7
crates/core/ratelimits/src/lib.rs
Normal file
7
crates/core/ratelimits/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod ratelimiter;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub mod rocket;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum;
|
||||
145
crates/core/ratelimits/src/ratelimiter.rs
Normal file
145
crates/core/ratelimits/src/ratelimiter.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
use std::ops::Add;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use dashmap::DashMap;
|
||||
|
||||
pub trait RequestKind {
|
||||
type R<'a>;
|
||||
}
|
||||
|
||||
pub trait RatelimitResolver<R>: Send + Sync {
|
||||
fn resolve_bucket<'a>(&self, request: &'a R) -> (&'a str, Option<&'a str>);
|
||||
fn resolve_bucket_limit(&self, bucket: &str) -> u32;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RatelimitStorage<K: RequestKind> {
|
||||
pub resolver: Arc<dyn for<'a> RatelimitResolver<K::R<'a>>>,
|
||||
pub map: Arc<DashMap<u64, Entry>>,
|
||||
}
|
||||
|
||||
impl<K: RequestKind> RatelimitStorage<K> {
|
||||
pub fn new<R: for<'a> RatelimitResolver<K::R<'a>> + 'static>(resolver: R) -> Self {
|
||||
Self {
|
||||
resolver: Arc::new(resolver),
|
||||
map: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ratelimit Bucket
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Entry {
|
||||
used: u32,
|
||||
reset: u128,
|
||||
}
|
||||
|
||||
/// Get the current time from Unix Epoch as a Duration
|
||||
fn now() -> Duration {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards...")
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
/// Find bucket by its key
|
||||
pub fn from(map: &DashMap<u64, Entry>, key: u64) -> Entry {
|
||||
map.get(&key).map(|x| *x).unwrap_or_else(|| Entry {
|
||||
used: 0,
|
||||
reset: now().add(Duration::from_secs(10)).as_millis(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Deduct one unit from the bucket and save
|
||||
pub fn deduct(&mut self) {
|
||||
let current_time = now().as_millis();
|
||||
if current_time > self.reset {
|
||||
self.used = 1;
|
||||
self.reset = now().add(Duration::from_secs(10)).as_millis();
|
||||
} else {
|
||||
self.used += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Save information
|
||||
pub fn save(self, map: &DashMap<u64, Entry>, key: u64) {
|
||||
map.insert(key, self);
|
||||
}
|
||||
|
||||
/// Get remaining units in the bucket
|
||||
pub fn get_remaining(&self, limit: u32) -> u32 {
|
||||
if now().as_millis() > self.reset {
|
||||
limit
|
||||
} else {
|
||||
limit - self.used
|
||||
}
|
||||
}
|
||||
|
||||
/// Get how long bucket has until reset
|
||||
pub fn left_until_reset(&self) -> u128 {
|
||||
let current_time = now().as_millis();
|
||||
self.reset.saturating_sub(current_time)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ratelimit Guard
|
||||
#[derive(Serialize, Clone, Copy, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Ratelimiter {
|
||||
pub key: u64,
|
||||
pub limit: u32,
|
||||
pub remaining: u32,
|
||||
pub reset: u128,
|
||||
}
|
||||
|
||||
impl Ratelimiter {
|
||||
/// Generate guard from identifier and target bucket
|
||||
pub fn from(
|
||||
map: &DashMap<u64, Entry>,
|
||||
identifier: &str,
|
||||
limit: u32,
|
||||
(bucket, resource): (&str, Option<&str>),
|
||||
) -> Result<Ratelimiter, Ratelimiter> {
|
||||
let mut key = DefaultHasher::new();
|
||||
key.write(identifier.as_bytes());
|
||||
key.write(bucket.as_bytes());
|
||||
|
||||
if let Some(id) = resource {
|
||||
key.write(id.as_bytes());
|
||||
}
|
||||
|
||||
let key = key.finish();
|
||||
let mut entry = Entry::from(map, key);
|
||||
|
||||
let remaining = entry.get_remaining(limit);
|
||||
let reset = entry.left_until_reset();
|
||||
let mut ratelimiter = Ratelimiter {
|
||||
key,
|
||||
limit,
|
||||
remaining,
|
||||
reset,
|
||||
};
|
||||
if remaining == 0 {
|
||||
return Err(ratelimiter);
|
||||
}
|
||||
|
||||
entry.deduct();
|
||||
entry.save(map, key);
|
||||
ratelimiter.remaining -= 1;
|
||||
ratelimiter.reset = entry.left_until_reset();
|
||||
|
||||
Ok(ratelimiter)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RatelimitInformation {
|
||||
Success(Ratelimiter),
|
||||
Failure { retry_after: u128 },
|
||||
}
|
||||
163
crates/core/ratelimits/src/rocket.rs
Normal file
163
crates/core/ratelimits/src/rocket.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use async_trait::async_trait;
|
||||
use log::info;
|
||||
use rocket::fairing::{Fairing, Info, Kind};
|
||||
use rocket::http::uri::Origin;
|
||||
use rocket::http::{Method, Status};
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Data, Request, Response, State};
|
||||
use revolt_config::config;
|
||||
|
||||
use revolt_rocket_okapi::r#gen::OpenApiGenerator;
|
||||
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
|
||||
|
||||
use authifier::models::Session;
|
||||
|
||||
use crate::ratelimiter::RequestKind;
|
||||
use crate::ratelimiter::{RatelimitInformation, Ratelimiter};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RocketRequestKind;
|
||||
|
||||
impl RequestKind for RocketRequestKind {
|
||||
type R<'a> = Request<'a>;
|
||||
}
|
||||
|
||||
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<RocketRequestKind>;
|
||||
|
||||
/// Find the remote IP of the client
|
||||
fn to_ip(request: &'_ rocket::Request<'_>) -> String {
|
||||
request
|
||||
.remote()
|
||||
.map(|x| x.ip().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Find the actual IP of the client
|
||||
async fn to_real_ip(request: &'_ rocket::Request<'_>) -> String {
|
||||
if config().await.api.security.trust_cloudflare {
|
||||
request
|
||||
.headers()
|
||||
.get_one("CF-Connecting-IP")
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| to_ip(request))
|
||||
} else {
|
||||
to_ip(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for Ratelimiter {
|
||||
type Error = Ratelimiter;
|
||||
|
||||
async fn from_request<'a>(request: &'r rocket::Request<'a>) -> Outcome<Self, Self::Error> {
|
||||
let ratelimiter = request
|
||||
.local_cache_async(async {
|
||||
use rocket::outcome::Outcome;
|
||||
|
||||
let storage = request.guard::<&State<RatelimitStorage>>().await.unwrap();
|
||||
|
||||
let identifier = if let Outcome::Success(session) = request.guard::<Session>().await
|
||||
{
|
||||
session.id
|
||||
} else {
|
||||
to_real_ip(request).await
|
||||
};
|
||||
|
||||
let (bucket, resource) = storage.resolver.resolve_bucket(request);
|
||||
let limit = storage.resolver.resolve_bucket_limit(bucket);
|
||||
|
||||
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource))
|
||||
})
|
||||
.await;
|
||||
|
||||
match ratelimiter {
|
||||
Ok(ratelimiter) => Outcome::Success(*ratelimiter),
|
||||
Err(ratelimiter) => Outcome::Error((Status::TooManyRequests, *ratelimiter)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenApiFromRequest<'_> for Ratelimiter {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
_name: String,
|
||||
_required: bool,
|
||||
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
||||
Ok(RequestHeaderInput::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach ratelimiter to the Rocket application
|
||||
pub struct RatelimitFairing;
|
||||
|
||||
#[async_trait]
|
||||
impl Fairing for RatelimitFairing {
|
||||
fn info(&self) -> Info {
|
||||
Info {
|
||||
name: "Ratelimiter",
|
||||
kind: Kind::Request | Kind::Response,
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
|
||||
use rocket::outcome::Outcome;
|
||||
if let Outcome::Error(_) = request.guard::<Ratelimiter>().await {
|
||||
info!(
|
||||
"User rate-limited on route {}! (IP = {:?})",
|
||||
request.uri(),
|
||||
to_real_ip(request).await
|
||||
);
|
||||
|
||||
request.set_method(Method::Get);
|
||||
request.set_uri(Origin::parse("/ratelimit").unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
|
||||
let guard = request.guard::<Ratelimiter>().await;
|
||||
let (Outcome::Success(ratelimiter) | Outcome::Error((_, ratelimiter))) = guard else {
|
||||
unreachable!()
|
||||
};
|
||||
let Ratelimiter {
|
||||
key,
|
||||
limit,
|
||||
remaining,
|
||||
reset,
|
||||
} = ratelimiter;
|
||||
|
||||
response.set_raw_header("X-RateLimit-Limit", limit.to_string());
|
||||
response.set_raw_header("X-RateLimit-Bucket", key.to_string());
|
||||
response.set_raw_header("X-RateLimit-Remaining", remaining.to_string());
|
||||
response.set_raw_header("X-RateLimit-Reset-After", reset.to_string());
|
||||
|
||||
if guard.is_error() {
|
||||
response.set_status(Status::TooManyRequests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for RatelimitInformation {
|
||||
type Error = u128;
|
||||
|
||||
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let info = match request.guard::<Ratelimiter>().await {
|
||||
Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter),
|
||||
Outcome::Error((_, ratelimiter)) => RatelimitInformation::Failure {
|
||||
retry_after: ratelimiter.reset,
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Outcome::Success(info)
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::get("/ratelimit")]
|
||||
fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
|
||||
Json(info)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<rocket::Route> {
|
||||
rocket::routes![ratelimit_info]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use revolt_config::configure;
|
||||
use revolt_database::DatabaseInfo;
|
||||
use revolt_result::Result;
|
||||
use tasks::{file_deletion, prune_dangling_files};
|
||||
use tasks::{file_deletion, prune_dangling_files, prune_members};
|
||||
use tokio::try_join;
|
||||
|
||||
pub mod tasks;
|
||||
@@ -13,7 +13,8 @@ async fn main() -> Result<()> {
|
||||
let db = DatabaseInfo::Auto.connect().await.expect("database");
|
||||
try_join!(
|
||||
file_deletion::task(db.clone()),
|
||||
prune_dangling_files::task(db)
|
||||
prune_dangling_files::task(db.clone()),
|
||||
prune_members::task(db.clone())
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod file_deletion;
|
||||
pub mod prune_dangling_files;
|
||||
pub mod prune_members;
|
||||
|
||||
18
crates/daemons/crond/src/tasks/prune_members.rs
Normal file
18
crates/daemons/crond/src/tasks/prune_members.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use log::warn;
|
||||
use revolt_database::Database;
|
||||
use revolt_result::Result;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub async fn task(db: Database) -> Result<()> {
|
||||
loop {
|
||||
let success = db.remove_dangling_members().await;
|
||||
if let Err(s) = success {
|
||||
revolt_config::capture_error(&s);
|
||||
warn!("Failed to prune dangling members: {:?}", &s);
|
||||
}
|
||||
|
||||
sleep(Duration::from_secs(90)).await;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ license = "AGPL-3.0-or-later"
|
||||
revolt-result = { version = "0.8.8", path = "../../core/result" }
|
||||
revolt-config = { version = "0.8.8", path = "../../core/config", features = [
|
||||
"report-macros",
|
||||
"anyhow"
|
||||
] }
|
||||
revolt-database = { version = "0.8.8", path = "../../core/database" }
|
||||
revolt-models = { version = "0.8.8", path = "../../core/models", features = [
|
||||
|
||||
@@ -81,6 +81,7 @@ revolt-models = { path = "../core/models", features = [
|
||||
revolt-presence = { path = "../core/presence" }
|
||||
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
|
||||
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
|
||||
revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] }
|
||||
|
||||
[build-dependencies]
|
||||
vergen = "7.5.0"
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod util;
|
||||
use revolt_config::config;
|
||||
use revolt_database::events::client::EventV1;
|
||||
use revolt_database::AMQP;
|
||||
use revolt_ratelimits::rocket as ratelimiter;
|
||||
use rocket::{Build, Rocket};
|
||||
use rocket_cors::{AllowedOrigins, CorsOptions};
|
||||
use rocket_prometheus::PrometheusMetrics;
|
||||
@@ -122,18 +123,22 @@ pub async fn web() -> Rocket<Build> {
|
||||
let rocket = rocket::build();
|
||||
let prometheus = PrometheusMetrics::new();
|
||||
|
||||
// Ratelimits
|
||||
let ratelimits = ratelimiter::RatelimitStorage::new(util::ratelimits::DeltaRatelimits);
|
||||
|
||||
routes::mount(config, rocket)
|
||||
.attach(prometheus.clone())
|
||||
.mount("/metrics", prometheus)
|
||||
.mount("/", rocket_cors::catch_all_options_routes())
|
||||
.mount("/", util::ratelimiter::routes())
|
||||
.mount("/", ratelimiter::routes())
|
||||
.mount("/swagger/", swagger)
|
||||
.mount("/0.8/swagger/", swagger_0_8)
|
||||
.manage(authifier)
|
||||
.manage(db)
|
||||
.manage(amqp)
|
||||
.manage(cors.clone())
|
||||
.attach(util::ratelimiter::RatelimitFairing)
|
||||
.manage(ratelimits)
|
||||
.attach(ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
.configure(rocket::Config {
|
||||
limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
|
||||
|
||||
@@ -11,7 +11,7 @@ use rocket_empty::EmptyResponse;
|
||||
pub async fn delete_bot(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let bot = target.as_bot(db).await?;
|
||||
if bot.owner != user.id {
|
||||
|
||||
@@ -14,7 +14,7 @@ use validator::Validate;
|
||||
pub async fn edit_bot(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
data: Json<DataEditBot>,
|
||||
) -> Result<Json<v0::BotWithUserResponse>> {
|
||||
let data = data.into_inner();
|
||||
@@ -37,7 +37,7 @@ pub async fn edit_bot(
|
||||
if data.public.is_none()
|
||||
&& data.analytics.is_none()
|
||||
&& data.interactions_url.is_none()
|
||||
&& data.remove.is_none()
|
||||
&& data.remove.is_empty()
|
||||
{
|
||||
return Ok(Json(v0::BotWithUserResponse {
|
||||
bot: bot.into(),
|
||||
@@ -64,7 +64,6 @@ pub async fn edit_bot(
|
||||
db,
|
||||
partial,
|
||||
remove
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.collect(),
|
||||
@@ -100,7 +99,7 @@ mod test {
|
||||
.body(
|
||||
json!(v0::DataEditBot {
|
||||
public: Some(true),
|
||||
remove: Some(vec![FieldsBot::Token]),
|
||||
remove: vec![FieldsBot::Token],
|
||||
..Default::default()
|
||||
})
|
||||
.to_string(),
|
||||
|
||||
@@ -11,7 +11,7 @@ use rocket::{serde::json::Json, State};
|
||||
pub async fn fetch_bot(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
bot: Reference,
|
||||
bot: Reference<'_>,
|
||||
) -> Result<Json<FetchBotResponse>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
|
||||
@@ -13,9 +13,9 @@ use rocket::State;
|
||||
pub async fn fetch_public_bot(
|
||||
db: &State<Database>,
|
||||
user: Option<User>,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<PublicBot>> {
|
||||
let bot = db.fetch_bot(&target.id).await?;
|
||||
let bot = db.fetch_bot(target.id).await?;
|
||||
if !bot.public && user.is_none_or(|x| x.id != bot.owner) {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn invite_bot(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
dest: Json<v0::InviteBotDestination>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
@@ -168,8 +168,8 @@ mod test {
|
||||
.await;
|
||||
|
||||
match event {
|
||||
EventV1::ServerMemberJoin { user, .. } => {
|
||||
assert_eq!(bot.id, user);
|
||||
EventV1::ServerMemberJoin { member, .. } => {
|
||||
assert_eq!(bot.id, member.id.user);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ use rocket_empty::EmptyResponse;
|
||||
pub async fn ack(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
message: Reference,
|
||||
target: Reference<'_>,
|
||||
message: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
@@ -29,7 +29,7 @@ pub async fn ack(
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||
|
||||
channel
|
||||
.ack(&user.id, &message.id)
|
||||
.ack(&user.id, message.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub async fn delete(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
options: v0::OptionsChannelDelete,
|
||||
) -> Result<EmptyResponse> {
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
|
||||
@@ -17,7 +17,7 @@ pub async fn edit(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::DataEditChannel>,
|
||||
) -> Result<Json<v0::Channel>> {
|
||||
let data = data.into_inner();
|
||||
@@ -38,7 +38,7 @@ pub async fn edit(
|
||||
&& data.icon.is_none()
|
||||
&& data.nsfw.is_none()
|
||||
&& data.owner.is_none()
|
||||
&& data.remove.is_none()
|
||||
&& data.remove.is_empty()
|
||||
{
|
||||
return Ok(Json(channel.into()));
|
||||
}
|
||||
@@ -112,23 +112,21 @@ pub async fn edit(
|
||||
nsfw,
|
||||
..
|
||||
} => {
|
||||
if let Some(fields) = &data.remove {
|
||||
if fields.contains(&v0::FieldsChannel::Icon) {
|
||||
if let Some(icon) = &icon {
|
||||
db.mark_attachment_as_deleted(&icon.id).await?;
|
||||
}
|
||||
if data.remove.contains(&v0::FieldsChannel::Icon) {
|
||||
if let Some(icon) = &icon {
|
||||
db.mark_attachment_as_deleted(&icon.id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
for field in fields {
|
||||
match field {
|
||||
v0::FieldsChannel::Description => {
|
||||
description.take();
|
||||
}
|
||||
v0::FieldsChannel::Icon => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
for field in &data.remove {
|
||||
match field {
|
||||
v0::FieldsChannel::Description => {
|
||||
description.take();
|
||||
}
|
||||
v0::FieldsChannel::Icon => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,11 +212,7 @@ pub async fn edit(
|
||||
.update(
|
||||
db,
|
||||
partial,
|
||||
data.remove
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect(),
|
||||
data.remove.into_iter().map(|f| f.into()).collect(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use rocket::{serde::json::Json, State};
|
||||
pub async fn fetch(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<v0::Channel>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ pub async fn add_member(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
group_id: Reference,
|
||||
member_id: Reference,
|
||||
group_id: Reference<'_>,
|
||||
member_id: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
|
||||
@@ -14,8 +14,8 @@ pub async fn remove_member(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
member: Reference,
|
||||
target: Reference<'_>,
|
||||
member: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
|
||||
@@ -18,7 +18,7 @@ use rocket::{serde::json::Json, State};
|
||||
pub async fn create_invite(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<v0::Invite>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
|
||||
@@ -17,7 +17,7 @@ use rocket::{serde::json::Json, State};
|
||||
pub async fn fetch_members(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<Vec<v0::User>>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
|
||||
@@ -22,7 +22,7 @@ use validator::Validate;
|
||||
pub async fn bulk_delete_messages(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
options: Json<v0::OptionsBulkDelete>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let options = options.into_inner();
|
||||
@@ -51,7 +51,7 @@ pub async fn bulk_delete_messages(
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
|
||||
Message::bulk_delete(db, &target.id, options.ids)
|
||||
Message::bulk_delete(db, target.id, options.ids)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ use rocket_empty::EmptyResponse;
|
||||
pub async fn clear_reactions(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
|
||||
@@ -15,10 +15,10 @@ use rocket_empty::EmptyResponse;
|
||||
pub async fn delete(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let message = msg.as_message_in_channel(db, &target.id).await?;
|
||||
let message = msg.as_message_in_channel(db, target.id).await?;
|
||||
|
||||
if message.author != user.id {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
@@ -18,8 +18,8 @@ use validator::Validate;
|
||||
pub async fn edit(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
edit: Json<v0::DataEditMessage>,
|
||||
) -> Result<Json<v0::Message>> {
|
||||
let edit = edit.into_inner();
|
||||
|
||||
@@ -15,8 +15,8 @@ use rocket::{serde::json::Json, State};
|
||||
pub async fn fetch(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
) -> Result<Json<v0::Message>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, Database, PartialMessage, SystemMessage, User, AMQP};
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, PartialMessage, SystemMessage, User, AMQP,
|
||||
};
|
||||
use revolt_models::v0::MessageAuthor;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
@@ -14,8 +17,8 @@ pub async fn message_pin(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
@@ -169,7 +172,7 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
|
||||
let updated_message = Reference::from_unchecked(message.id)
|
||||
let updated_message = Reference::from_unchecked(&message.id)
|
||||
.as_message(&harness.db)
|
||||
.await
|
||||
.expect("Failed to find updated message");
|
||||
|
||||
@@ -16,7 +16,7 @@ use validator::Validate;
|
||||
pub async fn query(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
options: v0::OptionsQueryMessages,
|
||||
) -> Result<Json<v0::BulkMessageResponse>> {
|
||||
options.validate().map_err(|error| {
|
||||
|
||||
@@ -15,9 +15,9 @@ use rocket_empty::EmptyResponse;
|
||||
pub async fn react_message(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
emoji: Reference,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
emoji: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
@@ -30,7 +30,7 @@ pub async fn react_message(
|
||||
|
||||
// Add the reaction
|
||||
message
|
||||
.add_reaction(db, &user, &emoji.id)
|
||||
.add_reaction(db, &user, emoji.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use validator::Validate;
|
||||
pub async fn search(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
options: Json<v0::DataMessageSearch>,
|
||||
) -> Result<Json<v0::BulkMessageResponse>> {
|
||||
if user.bot.is_some() {
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn message_send(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::DataMessageSend>,
|
||||
idempotency: IdempotencyKey,
|
||||
) -> Result<Json<v0::Message>> {
|
||||
@@ -202,7 +202,7 @@ mod test {
|
||||
Member::create(&harness.db, &server, &user, Some(channels.clone()))
|
||||
.await
|
||||
.expect("Failed to create member");
|
||||
let member = Reference::from_unchecked(user.id.clone())
|
||||
let member = Reference::from_unchecked(&user.id)
|
||||
.as_member(&harness.db, &server.id)
|
||||
.await
|
||||
.expect("Failed to get member");
|
||||
@@ -242,7 +242,7 @@ mod test {
|
||||
Member::create(&harness.db, &server, &second_user, Some(channels.clone()))
|
||||
.await
|
||||
.expect("Failed to create second member");
|
||||
let mut second_member = Reference::from_unchecked(second_user.id.clone())
|
||||
let mut second_member = Reference::from_unchecked(&second_user.id)
|
||||
.as_member(&harness.db, &server.id)
|
||||
.await
|
||||
.expect("Failed to get second member");
|
||||
|
||||
@@ -14,8 +14,8 @@ pub async fn message_unpin(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
@@ -91,7 +91,7 @@ mod test {
|
||||
Member::create(&harness.db, &server, &user, Some(channels.clone()))
|
||||
.await
|
||||
.expect("Failed to create member");
|
||||
let member = Reference::from_unchecked(user.id.clone())
|
||||
let member = Reference::from_unchecked(&user.id)
|
||||
.as_member(&harness.db, &server.id)
|
||||
.await
|
||||
.expect("Failed to get member");
|
||||
@@ -174,7 +174,7 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
|
||||
let updated_message = Reference::from_unchecked(message.id)
|
||||
let updated_message = Reference::from_unchecked(&message.id)
|
||||
.as_message(&harness.db)
|
||||
.await
|
||||
.expect("Failed to find updated message");
|
||||
|
||||
@@ -18,9 +18,9 @@ use rocket_empty::EmptyResponse;
|
||||
pub async fn unreact_message(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
emoji: Reference,
|
||||
target: Reference<'_>,
|
||||
msg: Reference<'_>,
|
||||
emoji: Reference<'_>,
|
||||
options: v0::OptionsUnreact,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
@@ -41,14 +41,14 @@ pub async fn unreact_message(
|
||||
// Check if we should wipe all of this reaction
|
||||
if remove_all {
|
||||
return message
|
||||
.clear_reaction(db, &emoji.id)
|
||||
.clear_reaction(db, emoji.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse);
|
||||
}
|
||||
|
||||
// Remove the reaction
|
||||
message
|
||||
.remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), &emoji.id)
|
||||
.remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), emoji.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
group_remove_member::remove_member,
|
||||
voice_join::call,
|
||||
permissions_set::set_role_permissions,
|
||||
permissions_set_default::set_default_permissions,
|
||||
permissions_set_default::set_default_channel_permissions,
|
||||
message_react::react_message,
|
||||
message_unreact::unreact_message,
|
||||
message_clear_reactions::clear_reactions,
|
||||
|
||||
@@ -17,7 +17,7 @@ use rocket::{serde::json::Json, State};
|
||||
pub async fn set_role_permissions(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
role_id: String,
|
||||
data: Json<v0::DataSetRolePermissions>,
|
||||
) -> Result<Json<v0::Channel>> {
|
||||
|
||||
@@ -14,10 +14,10 @@ use rocket::{serde::json::Json, State};
|
||||
/// Channel must be a `Group`, `TextChannel` or `VoiceChannel`.
|
||||
#[openapi(tag = "Channel Permissions")]
|
||||
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
|
||||
pub async fn set_default_permissions(
|
||||
pub async fn set_default_channel_permissions(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::DataDefaultChannelPermissions>,
|
||||
) -> Result<Json<v0::Channel>> {
|
||||
let data = data.into_inner();
|
||||
|
||||
@@ -16,7 +16,7 @@ use rocket::{serde::json::Json, State};
|
||||
pub async fn call(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<v0::LegacyCreateVoiceUserResponse>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
|
||||
@@ -19,7 +19,7 @@ use validator::Validate;
|
||||
pub async fn create_webhook(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::CreateWebhookBody>,
|
||||
) -> Result<Json<v0::Webhook>> {
|
||||
let data = data.into_inner();
|
||||
|
||||
@@ -15,7 +15,7 @@ use rocket::{serde::json::Json, State};
|
||||
pub async fn fetch_webhooks(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
channel_id: Reference,
|
||||
channel_id: Reference<'_>,
|
||||
) -> Result<Json<Vec<Webhook>>> {
|
||||
let channel = channel_id.as_channel(db).await?;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use rocket_empty::EmptyResponse;
|
||||
pub async fn delete_emoji(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
emoji_id: Reference,
|
||||
emoji_id: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
// Fetch the emoji
|
||||
let emoji = emoji_id.as_emoji(db).await?;
|
||||
@@ -26,7 +26,7 @@ pub async fn delete_emoji(
|
||||
// Otherwise, validate we have permission to delete from parent
|
||||
match &emoji.parent {
|
||||
EmojiParent::Server { id } => {
|
||||
let server = db.fetch_server(id).await?;
|
||||
let server = db.fetch_server(id.as_str()).await?;
|
||||
|
||||
// Check for permission
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
|
||||
@@ -9,7 +9,7 @@ use rocket::{serde::json::Json, State};
|
||||
/// Fetch an emoji by its id.
|
||||
#[openapi(tag = "Emojis")]
|
||||
#[get("/emoji/<emoji_id>")]
|
||||
pub async fn fetch_emoji(db: &State<Database>, emoji_id: Reference) -> Result<Json<v0::Emoji>> {
|
||||
pub async fn fetch_emoji(db: &State<Database>, emoji_id: Reference<'_>) -> Result<Json<v0::Emoji>> {
|
||||
emoji_id
|
||||
.as_emoji(db)
|
||||
.await
|
||||
|
||||
@@ -12,7 +12,7 @@ use rocket_empty::EmptyResponse;
|
||||
/// Delete an invite by its id.
|
||||
#[openapi(tag = "Invites")]
|
||||
#[delete("/<target>")]
|
||||
pub async fn delete(db: &State<Database>, user: User, target: Reference) -> Result<EmptyResponse> {
|
||||
pub async fn delete(db: &State<Database>, user: User, target: Reference<'_>) -> Result<EmptyResponse> {
|
||||
let invite = target.as_invite(db).await?;
|
||||
|
||||
if user.id == invite.creator() {
|
||||
|
||||
@@ -8,7 +8,7 @@ use rocket::{serde::json::Json, State};
|
||||
/// Fetch an invite by its id.
|
||||
#[openapi(tag = "Invites")]
|
||||
#[get("/<target>")]
|
||||
pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::InviteResponse>> {
|
||||
pub async fn fetch(db: &State<Database>, target: Reference<'_>) -> Result<Json<v0::InviteResponse>> {
|
||||
Ok(Json(match target.as_invite(db).await? {
|
||||
Invite::Server {
|
||||
channel, creator, ..
|
||||
@@ -34,7 +34,7 @@ pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::I
|
||||
let server = db.fetch_server(&server).await?;
|
||||
|
||||
v0::InviteResponse::Server {
|
||||
code: target.id,
|
||||
code: target.id.to_string(),
|
||||
member_count: db.fetch_member_count(&server.id).await? as i64,
|
||||
server_id: server.id,
|
||||
server_name: server.name,
|
||||
@@ -64,7 +64,7 @@ pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::I
|
||||
description,
|
||||
..
|
||||
} => v0::InviteResponse::Group {
|
||||
code: target.id,
|
||||
code: target.id.to_string(),
|
||||
channel_id: id,
|
||||
channel_name: name,
|
||||
channel_description: description,
|
||||
|
||||
@@ -12,7 +12,7 @@ pub async fn join(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<v0::InviteJoinResponse>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
|
||||
@@ -17,8 +17,8 @@ use validator::Validate;
|
||||
pub async fn ban(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
server: Reference,
|
||||
target: Reference,
|
||||
server: Reference<'_>,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::DataBanCreate>,
|
||||
) -> Result<Json<v0::ServerBan>> {
|
||||
let data = data.into_inner();
|
||||
@@ -56,7 +56,7 @@ pub async fn ban(
|
||||
.await?;
|
||||
}
|
||||
|
||||
ServerBan::create(db, &server, &target.id, data.reason)
|
||||
ServerBan::create(db, &server, target.id, data.reason)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.map(Json)
|
||||
|
||||
@@ -17,7 +17,7 @@ use rocket::State;
|
||||
pub async fn list(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<v0::BanListResult>> {
|
||||
let server = target.as_server(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user