mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-09 04:26:53 +00:00
Compare commits
48 Commits
feat/audit
...
feat/admin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cebf6e35b | ||
|
|
db6cd1a014 | ||
|
|
35a11f7916 | ||
|
|
feb936a278 | ||
|
|
f10aaa5bbe | ||
|
|
0e8f220dfb | ||
|
|
cf4b9ba2ca | ||
|
|
08fb35c3f9 | ||
|
|
a22378c35c | ||
|
|
164be6a2da | ||
|
|
9217c6e49b | ||
|
|
23ba7d8fd8 | ||
|
|
6ece5cde3b | ||
|
|
7c2098c611 | ||
|
|
9fb344a433 | ||
|
|
948fb97e89 | ||
|
|
05bceb6d4a | ||
|
|
e48d92c635 | ||
|
|
3f4df765eb | ||
|
|
a3d390c085 | ||
|
|
db4b26251f | ||
|
|
93a00ffbf8 | ||
|
|
2d8b7481bc | ||
|
|
8bd5542e71 | ||
|
|
533cc5835a | ||
|
|
80b749d6fc | ||
|
|
74ce10162c | ||
|
|
9a67cb139c | ||
|
|
5e98a01624 | ||
|
|
9fe7de709a | ||
|
|
5b9a909923 | ||
|
|
913946cfa1 | ||
|
|
01e0c30596 | ||
|
|
1bb12b416a | ||
|
|
5e577358ac | ||
|
|
6e78f0b9c0 | ||
|
|
26faa18d6e | ||
|
|
a152d825a2 | ||
|
|
85ad261cc3 | ||
|
|
f857ca6926 | ||
|
|
73ce052e1a | ||
|
|
4fd71c98d6 | ||
|
|
96f046bc76 | ||
|
|
6ecfd14b07 | ||
|
|
bf4530563a | ||
|
|
ff36d71ad5 | ||
|
|
f8eb324543 | ||
|
|
03db5b1ddf |
2
.github/workflows/docker.yaml
vendored
2
.github/workflows/docker.yaml
vendored
@@ -12,7 +12,7 @@ permissions:
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
group: docker-test-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/rust.yaml
vendored
2
.github/workflows/rust.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
group: rust-build-test-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
|
||||
@@ -7,7 +7,7 @@ gh = "2.25.0"
|
||||
rust = "1.92.0"
|
||||
"cargo:cargo-nextest" = "0.9.122"
|
||||
|
||||
"github:git-town/git-town" = "22.4.0"
|
||||
"github:git-town/git-town" = "22.7.1"
|
||||
|
||||
[settings]
|
||||
experimental = true
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -7330,6 +7330,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha1 0.10.6",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"totp-lite",
|
||||
"ulid",
|
||||
"unicode-segmentation",
|
||||
|
||||
@@ -28,6 +28,7 @@ futures-locks = "0.7.1"
|
||||
async-lock = "2.8.0"
|
||||
async-recursion = "1.0.4"
|
||||
tokio-util = { version = "0.7.18" }
|
||||
tokio-stream = "0.1.18"
|
||||
|
||||
# Error Handling
|
||||
anyhow = "1.0.100"
|
||||
|
||||
@@ -70,6 +70,9 @@ trust_cloudflare = false
|
||||
easypwned = ""
|
||||
# Tenor API Key
|
||||
tenor_key = ""
|
||||
# admin api machine keys
|
||||
# admin_keys = ["key_1", "key_2"]
|
||||
admin_keys = []
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
@@ -88,10 +91,6 @@ max_concurrent_connections = 50
|
||||
# How long to ring devices for when calling in dms/groups, in seconds
|
||||
call_ring_duration = 30
|
||||
|
||||
[api.audit_logs]
|
||||
# How long audit log entries last before being removed, in seconds
|
||||
expires_after = 2592000 # 30d
|
||||
|
||||
[api.livekit.nodes]
|
||||
|
||||
[api.users]
|
||||
@@ -223,11 +222,14 @@ default_bucket = "revolt-uploads"
|
||||
[features]
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
admin_api_enabled = false
|
||||
# Enable push notifications for mass pings (everyone, online, roles)
|
||||
# When false this will still ping in-client but will not send notifications from pushd
|
||||
mass_mentions_send_notifications = true
|
||||
# Can role/everyone pings be used at all
|
||||
mass_mentions_enabled = true
|
||||
# Show the admin api in the OpenAPI spec
|
||||
admin_api_show_spec = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ pub struct ApiSecurity {
|
||||
pub trust_cloudflare: bool,
|
||||
pub easypwned: String,
|
||||
pub tenor_key: String,
|
||||
pub admin_keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -260,12 +261,6 @@ pub struct ApiUsers {
|
||||
pub min_username_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiAuditLogs {
|
||||
/// How long audit log entries last before being removed, in seconds
|
||||
pub expires_after: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Api {
|
||||
pub registration: ApiRegistration,
|
||||
@@ -274,7 +269,6 @@ pub struct Api {
|
||||
pub workers: ApiWorkers,
|
||||
pub livekit: ApiLiveKit,
|
||||
pub users: ApiUsers,
|
||||
pub audit_logs: ApiAuditLogs,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -446,6 +440,8 @@ pub struct Features {
|
||||
pub webhooks_enabled: bool,
|
||||
pub mass_mentions_send_notifications: bool,
|
||||
pub mass_mentions_enabled: bool,
|
||||
pub admin_api_enabled: bool,
|
||||
pub admin_api_show_spec: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub advanced: FeaturesAdvanced,
|
||||
|
||||
@@ -82,6 +82,7 @@ async-recursion = { workspace = true }
|
||||
|
||||
# Async
|
||||
tokio = { workspace = true, optional = true }
|
||||
tokio-stream = { workspace = true }
|
||||
|
||||
# Axum Impl
|
||||
axum = { workspace = true, optional = true }
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use self::mongodb::*;
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Account, AccountInvite, AuditLogEntry, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji,
|
||||
File, FileHash, Invite, MFATicket, Member, MemberCompositeKey, Message, PolicyChange,
|
||||
RatelimitEvent, Report, Server, ServerBan, Session, Snapshot, User, UserSettings, Webhook,
|
||||
AdminAuditItem, AdminCase, AdminComment, AdminStrike, AdminToken, AdminUser, Bot, Channel,
|
||||
ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member, MemberCompositeKey,
|
||||
Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot, User, UserSettings,
|
||||
Webhook, Account, AccountInvite, Session, MFATicket
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ReferenceDb {
|
||||
pub audit_logs: Arc<Mutex<HashMap<String, AuditLogEntry>>>,
|
||||
pub admin_audits: Arc<Mutex<BTreeMap<String, AdminAuditItem>>>,
|
||||
pub admin_cases: Arc<Mutex<HashMap<String, AdminCase>>>,
|
||||
pub admin_comments: Arc<Mutex<HashMap<String, AdminComment>>>,
|
||||
pub admin_strikes: Arc<Mutex<HashMap<String, AdminStrike>>>,
|
||||
pub admin_tokens: Arc<Mutex<HashMap<String, AdminToken>>>,
|
||||
pub admin_users: Arc<Mutex<HashMap<String, AdminUser>>>,
|
||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
|
||||
|
||||
@@ -281,6 +281,16 @@ pub enum EventV1 {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Channel wiped
|
||||
///
|
||||
/// Clients should remove all associated data:
|
||||
/// - Messages
|
||||
/// - Unreads
|
||||
/// - Voice States
|
||||
ChannelWipe {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// User joins a group
|
||||
ChannelGroupJoin {
|
||||
id: String,
|
||||
|
||||
@@ -77,78 +77,6 @@ macro_rules! auto_derived_partial {
|
||||
};
|
||||
}
|
||||
|
||||
/// Internal macro for `generate_diff!`, you should not need to use this yourself.
|
||||
macro_rules! generate_field_diff {
|
||||
(optional, $remove:ident, $fieldsmember:path, $self:ident, $before:ident, $partial:ident, $field:ident) => {
|
||||
if $partial.$field.is_some() || $remove.contains(&$fieldsmember) {
|
||||
$before.$field = $self.$field.clone();
|
||||
};
|
||||
};
|
||||
|
||||
(optional, default, $remove:ident, $fieldsmember:path, $self:ident, $before:ident, $partial:ident, $field:ident) => {
|
||||
if $partial.$field.is_some() || $remove.contains(&$fieldsmember) {
|
||||
$before.$field = Some($self.$field.clone());
|
||||
};
|
||||
};
|
||||
|
||||
($self:ident, $before:ident, $partial:ident, $field:ident) => {
|
||||
if $partial.$field.is_some() {
|
||||
$before.$field = Some($self.$field.clone());
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a partial model containing the data which has changed in an update
|
||||
///
|
||||
/// ## Usage:
|
||||
/// `before` is the "output" containing what the model had before being updated,
|
||||
/// this will corraspond to `partial` which is what the data is being changed too.
|
||||
///
|
||||
/// ```rs
|
||||
/// let mut before = PartialModel::default();
|
||||
///
|
||||
/// generate_diff!(
|
||||
/// self, // database model
|
||||
/// before, // mutable empty partial corrasponding to the current model
|
||||
/// partial, // partial containing what is being updated
|
||||
/// remove, // slice of fields being removed
|
||||
/// (
|
||||
/// name, // regular non-nullable non-removable field
|
||||
/// (FieldsEnum::Nickname) nickname, // optional removable field
|
||||
/// ((default) FieldsEnum::Roles) roles, // optional removable field with custom default
|
||||
/// )
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// See `Member::generate_diff` `Server::generate_diff` `Role::generate_diff` for full examples
|
||||
macro_rules! generate_diff {
|
||||
(
|
||||
$self:ident,
|
||||
$before:ident,
|
||||
$partial:ident,
|
||||
$remove:ident,
|
||||
(
|
||||
$(
|
||||
$(
|
||||
$(@$optional:tt)? (
|
||||
$($(@$default:tt)? (default))?
|
||||
$fieldsmember:path
|
||||
)
|
||||
)?
|
||||
$field: ident
|
||||
),*
|
||||
$(,)?
|
||||
)
|
||||
) => {
|
||||
$(
|
||||
generate_field_diff!(
|
||||
$( $($optional)? optional, $($($default)? default,)? $remove, $fieldsmember,)?
|
||||
$self, $before, $partial, $field
|
||||
);
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
mod drivers;
|
||||
pub use drivers::*;
|
||||
|
||||
@@ -187,6 +115,7 @@ pub use amqp::amqp::AMQP;
|
||||
#[cfg(feature = "voice")]
|
||||
pub mod voice;
|
||||
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
|
||||
5
crates/core/database/src/models/admin_audits/mod.rs
Normal file
5
crates/core/database/src/models/admin_audits/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
45
crates/core/database/src/models/admin_audits/models.rs
Normal file
45
crates/core/database/src/models/admin_audits/models.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_models::v0::AdminAuditItemActions;
|
||||
|
||||
use crate::util::basic::transform_optional_string;
|
||||
|
||||
auto_derived! {
|
||||
pub struct AdminAuditItem {
|
||||
/// The audit item ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The moderator who performed the action
|
||||
pub mod_id: String,
|
||||
/// The action performed (previously 'permission'). Should be one of v0::AdminAuditActionAction
|
||||
pub action: String,
|
||||
/// The relevant case ID, if applicable
|
||||
pub case_id: Option<String>,
|
||||
/// The object the action was taken against, if applicable
|
||||
pub target_id: Option<String>,
|
||||
/// The context of the action, if applicable (eg. search phrases)
|
||||
pub context: Option<String>,
|
||||
/// When the action occurred, in iso8601.
|
||||
pub timestamp: String
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminAuditItem {
|
||||
pub fn new(
|
||||
mod_id: &str,
|
||||
action: AdminAuditItemActions,
|
||||
case_id: Option<&str>,
|
||||
target_id: Option<&str>,
|
||||
context: Option<&str>,
|
||||
) -> AdminAuditItem {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminAuditItem {
|
||||
id,
|
||||
mod_id: mod_id.to_string(),
|
||||
action: action.to_string(),
|
||||
case_id: transform_optional_string(case_id),
|
||||
target_id: transform_optional_string(target_id),
|
||||
context: transform_optional_string(context),
|
||||
timestamp: Timestamp::now_utc().format_short().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
17
crates/core/database/src/models/admin_audits/ops.rs
Normal file
17
crates/core/database/src/models/admin_audits/ops.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_audits::AdminAuditItem;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminAudits: Sync + Send {
|
||||
async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()>;
|
||||
|
||||
async fn admin_audit_fetch(
|
||||
&self,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminAuditItem>>;
|
||||
}
|
||||
48
crates/core/database/src/models/admin_audits/ops/mongodb.rs
Normal file
48
crates/core/database/src/models/admin_audits/ops/mongodb.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use tokio_stream::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AdminAuditItem;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractAdminAudits;
|
||||
|
||||
static COL: &str = "admin_audits";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminAudits for MongoDb {
|
||||
async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()> {
|
||||
query!(self, insert_one, COL, audit).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_audit_fetch(
|
||||
&self,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminAuditItem>> {
|
||||
if let Some(before) = before_id {
|
||||
Ok(self
|
||||
.col::<AdminAuditItem>(COL)
|
||||
.find(doc! {
|
||||
"_id": { "$lt": before}
|
||||
})
|
||||
.sort(doc! {"_id": -1})
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|f| f.ok())
|
||||
.collect()
|
||||
.await)
|
||||
} else {
|
||||
Ok(self
|
||||
.col::<AdminAuditItem>(COL)
|
||||
.find(doc! {})
|
||||
.sort(doc! {"_id": -1})
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|f| f.ok())
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AdminAuditItem;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractAdminAudits;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminAudits for ReferenceDb {
|
||||
async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()> {
|
||||
let mut admin_audits = self.admin_audits.lock().await;
|
||||
if admin_audits.contains_key(&audit.id) {
|
||||
Err(create_database_error!("insert", "admin_audits"))
|
||||
} else {
|
||||
admin_audits.insert(audit.id.to_string(), audit.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_audit_fetch(
|
||||
&self,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminAuditItem>> {
|
||||
let admin_audits = self.admin_audits.lock().await;
|
||||
if let Some(before) = before_id {
|
||||
Ok(admin_audits
|
||||
.iter()
|
||||
.rev()
|
||||
.skip_while(|(id, _)| id.as_str() <= before)
|
||||
.by_ref()
|
||||
.take(limit as usize)
|
||||
.map(|(_, item)| item.clone())
|
||||
.collect())
|
||||
} else {
|
||||
Ok(admin_audits
|
||||
.iter()
|
||||
.rev()
|
||||
.by_ref()
|
||||
.take(limit as usize)
|
||||
.map(|(_, item)| item.clone())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/admin_cases/mod.rs
Normal file
5
crates/core/database/src/models/admin_cases/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
53
crates/core/database/src/models/admin_cases/models.rs
Normal file
53
crates/core/database/src/models/admin_cases/models.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
auto_derived_partial! {
|
||||
pub struct AdminCase {
|
||||
/// The case ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The case Short ID
|
||||
pub short_id: String,
|
||||
|
||||
/// The owner of the case
|
||||
pub owner_id: String,
|
||||
/// The title of the case
|
||||
pub title: String,
|
||||
/// The status of the case (open/closed)
|
||||
pub status: String,
|
||||
/// When the case was closed, in iso8601
|
||||
pub closed_at: Option<String>,
|
||||
/// The tags for the case
|
||||
pub tags: Vec<String>,
|
||||
},
|
||||
"PartialAdminCase"
|
||||
}
|
||||
|
||||
impl AdminCase {
|
||||
pub fn new(owner_id: &str, title: &str) -> AdminCase {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
let short_id = id.clone().split_off(id.len() - 7);
|
||||
|
||||
AdminCase {
|
||||
id,
|
||||
short_id,
|
||||
owner_id: owner_id.to_string(),
|
||||
title: title.to_string(),
|
||||
status: "Open".to_string(),
|
||||
closed_at: None,
|
||||
tags: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_tags(&self, other: &[String]) -> Vec<String> {
|
||||
let mut resp: Vec<String> = vec![];
|
||||
// Shitty combining chain; itll only ever be like 5 items
|
||||
resp.extend(self.tags.clone());
|
||||
resp.extend(other.iter().filter_map(|p| {
|
||||
if !self.tags.contains(p) {
|
||||
Some(p.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
30
crates/core/database/src/models/admin_cases/ops.rs
Normal file
30
crates/core/database/src/models/admin_cases/ops.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_cases::{AdminCase, PartialAdminCase};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminCases: Sync + Send {
|
||||
async fn admin_case_create(&self, case: AdminCase) -> Result<()>;
|
||||
|
||||
async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()>;
|
||||
|
||||
async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()>;
|
||||
|
||||
async fn admin_case_fetch(&self, case_id: &str) -> Result<AdminCase>;
|
||||
|
||||
async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result<AdminCase>;
|
||||
|
||||
/// title is fuzzy, the rest of the arguments are direct matches
|
||||
/// before_id and limit are for paginating
|
||||
async fn admin_case_search(
|
||||
&self,
|
||||
title: Option<&str>,
|
||||
status: Option<&str>,
|
||||
owner_id: Option<&str>,
|
||||
tags: Option<Vec<String>>,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminCase>>;
|
||||
}
|
||||
85
crates/core/database/src/models/admin_cases/ops/mongodb.rs
Normal file
85
crates/core/database/src/models/admin_cases/ops/mongodb.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use tokio_stream::StreamExt;
|
||||
use bson::Document;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminCase, PartialAdminCase};
|
||||
|
||||
use super::AbstractAdminCases;
|
||||
|
||||
static COL: &str = "admin_cases";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminCases for MongoDb {
|
||||
async fn admin_case_create(&self, case: AdminCase) -> Result<()> {
|
||||
query!(self, insert_one, COL, case).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()> {
|
||||
self.col::<Document>("safety_reports")
|
||||
.update_one(
|
||||
doc! {"_id": { "$regex": format!("{}$", report_id)}},
|
||||
doc! {"case_id": case_id},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("safety_reports", "update_one"))
|
||||
}
|
||||
|
||||
async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, case_id, partial, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_case_fetch(&self, case_id: &str) -> Result<AdminCase> {
|
||||
query!(self, find_one_by_id, COL, case_id)?
|
||||
.ok_or_else(|| create_database_error!("find_one", COL))
|
||||
}
|
||||
|
||||
async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result<AdminCase> {
|
||||
query!(self, find_one, COL, doc! {"short_id": short_id})?
|
||||
.ok_or_else(|| create_database_error!("find_one", COL))
|
||||
}
|
||||
|
||||
/// title is fuzzy, the rest of the arguments are direct matches
|
||||
async fn admin_case_search(
|
||||
&self,
|
||||
title: Option<&str>,
|
||||
status: Option<&str>,
|
||||
owner_id: Option<&str>,
|
||||
tags: Option<Vec<String>>,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminCase>> {
|
||||
let mut query = Document::new();
|
||||
|
||||
if let Some(title) = title {
|
||||
query.insert("$text", doc! {"$search": title});
|
||||
}
|
||||
|
||||
if let Some(status) = status {
|
||||
query.insert("status", status);
|
||||
}
|
||||
|
||||
if let Some(owner) = owner_id {
|
||||
query.insert("owner_id", owner);
|
||||
}
|
||||
|
||||
if let Some(tags) = tags {
|
||||
query.insert("tags", doc! {"$elemMatch": {"$in": tags}});
|
||||
}
|
||||
|
||||
if let Some(before) = before_id {
|
||||
query.insert("_id", doc! {"$lt": before});
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.col::<AdminCase>(COL)
|
||||
.find(query)
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|f| f.ok())
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
}
|
||||
112
crates/core/database/src/models/admin_cases/ops/reference.rs
Normal file
112
crates/core/database/src/models/admin_cases/ops/reference.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminCase, PartialAdminCase};
|
||||
|
||||
use super::AbstractAdminCases;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminCases for ReferenceDb {
|
||||
async fn admin_case_create(&self, case: AdminCase) -> Result<()> {
|
||||
let mut admin_cases = self.admin_cases.lock().await;
|
||||
if admin_cases.contains_key(&case.id) {
|
||||
Err(create_database_error!("insert", "admin_cases"))
|
||||
} else {
|
||||
admin_cases.insert(case.id.to_string(), case.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()> {
|
||||
let mut reports = self.safety_reports.lock().await;
|
||||
if let Some(report) = reports.get_mut(report_id) {
|
||||
report.case_id = Some(case_id.to_string());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()> {
|
||||
let mut admin_cases = self.admin_cases.lock().await;
|
||||
if let Some(case) = admin_cases.get_mut(case_id) {
|
||||
case.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_case_fetch(&self, case_id: &str) -> Result<AdminCase> {
|
||||
let admin_cases = self.admin_cases.lock().await;
|
||||
if let Some(case) = admin_cases.get(case_id) {
|
||||
Ok(case.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result<AdminCase> {
|
||||
let admin_cases = self.admin_cases.lock().await;
|
||||
if let Some((_, case)) = admin_cases.iter().find(|(_, c)| c.short_id == short_id) {
|
||||
Ok(case.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// title is fuzzy, the rest of the arguments are direct matches
|
||||
/// before_id and limit are for paginating
|
||||
async fn admin_case_search(
|
||||
&self,
|
||||
title: Option<&str>,
|
||||
status: Option<&str>,
|
||||
owner_id: Option<&str>,
|
||||
tags: Option<Vec<String>>,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminCase>> {
|
||||
let admin_cases = self.admin_cases.lock().await;
|
||||
Ok(admin_cases
|
||||
.iter()
|
||||
.filter(|(_, case)| {
|
||||
if let Some(title) = title {
|
||||
case.title.to_lowercase().contains(title)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.filter(|(_, case)| {
|
||||
if let Some(status) = status {
|
||||
case.status == status
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.filter(|(_, case)| {
|
||||
if let Some(owner) = owner_id {
|
||||
case.owner_id == owner
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.filter(|(_, case)| {
|
||||
if let Some(tags) = &tags {
|
||||
tags.iter().filter(|p| case.tags.contains(p)).count() > 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.skip_while(|(id, _)| {
|
||||
if let Some(before) = before_id {
|
||||
id.as_str() <= before
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.by_ref()
|
||||
.take(limit as usize)
|
||||
.map(|(_, case)| case.clone())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/admin_comments/mod.rs
Normal file
5
crates/core/database/src/models/admin_comments/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
95
crates/core/database/src/models/admin_comments/models.rs
Normal file
95
crates/core/database/src/models/admin_comments/models.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use revolt_models::v0;
|
||||
|
||||
auto_derived_partial! {
|
||||
pub struct AdminComment {
|
||||
/// The comment ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The Object this comment is attached to
|
||||
pub object_id: String,
|
||||
/// The ID of the case this comment is attached to, if applicable
|
||||
pub case_id: Option<String>,
|
||||
/// The author ID
|
||||
pub user_id: String,
|
||||
/// When the comment was edited, if applicable, in iso8601
|
||||
pub edited_at: Option<String>,
|
||||
/// The content, if not a system event
|
||||
pub content: Option<String>,
|
||||
/// The system event that occurred, if applicable
|
||||
pub system_message: Option<String>,
|
||||
/// The system message target
|
||||
pub system_message_target: Option<String>,
|
||||
/// The system message raw context
|
||||
pub system_message_context: Option<String>,
|
||||
},
|
||||
"PartialAdminComment"
|
||||
}
|
||||
|
||||
impl AdminComment {
|
||||
pub fn new(
|
||||
object_id: &str,
|
||||
user_id: &str,
|
||||
content: &str,
|
||||
case_id: Option<&str>,
|
||||
) -> AdminComment {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminComment {
|
||||
id,
|
||||
object_id: object_id.to_string(),
|
||||
case_id: case_id.map(|c| c.to_string()),
|
||||
user_id: user_id.to_string(),
|
||||
edited_at: None,
|
||||
content: Some(content.to_string()),
|
||||
system_message: None,
|
||||
system_message_context: None,
|
||||
system_message_target: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_system_message(
|
||||
object_id: &str,
|
||||
user_id: &str,
|
||||
case_id: &str,
|
||||
system_message_kind: v0::AdminAuditItemActions,
|
||||
context: Option<&str>,
|
||||
target: &str,
|
||||
) -> AdminComment {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminComment {
|
||||
id,
|
||||
object_id: object_id.to_string(),
|
||||
case_id: Some(case_id.to_string()),
|
||||
user_id: user_id.to_string(),
|
||||
edited_at: None,
|
||||
content: None,
|
||||
system_message: Some(serde_json::to_string(&system_message_kind).unwrap()), // if this explodes i'll eat my hat.
|
||||
system_message_context: context.map(|c| c.to_string()),
|
||||
system_message_target: Some(target.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit the comment, updating the edited_at time as well
|
||||
pub fn edit(&mut self, content: &str) {
|
||||
self.content = Some(content.to_string());
|
||||
self.edited_at = Some(
|
||||
iso8601_timestamp::Timestamp::now_utc()
|
||||
.format_short()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialAdminComment {
|
||||
pub fn new() -> PartialAdminComment {
|
||||
PartialAdminComment::default()
|
||||
}
|
||||
/// Edit the comment, updating the edited_at time as well
|
||||
pub fn edit(&mut self, content: &str) {
|
||||
self.content = Some(content.to_string());
|
||||
self.edited_at = Some(
|
||||
iso8601_timestamp::Timestamp::now_utc()
|
||||
.format_short()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
crates/core/database/src/models/admin_comments/ops.rs
Normal file
28
crates/core/database/src/models/admin_comments/ops.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{models::admin_comments::AdminComment, PartialAdminComment};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminComments: Sync + Send {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()>;
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment>;
|
||||
|
||||
/// Fetch all comments related to the case. This includes comments made on other objects.
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>>;
|
||||
|
||||
/// Fetch all comments on an object
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>>;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminComment, PartialAdminComment};
|
||||
|
||||
use super::AbstractAdminComments;
|
||||
|
||||
static COL: &str = "admin_comments";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminComments for MongoDb {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> {
|
||||
query!(self, insert_one, COL, comment).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
comment_id,
|
||||
partial,
|
||||
vec![],
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment> {
|
||||
query!(self, find_one, COL, doc! {"_id": id})?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>> {
|
||||
query!(self, find, COL, doc! {"case_id": case_id})
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>> {
|
||||
query!(self, find, COL, doc! {"object_id": object_id})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminComment, PartialAdminComment};
|
||||
|
||||
use super::AbstractAdminComments;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminComments for ReferenceDb {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> {
|
||||
let mut admin_comments = self.admin_comments.lock().await;
|
||||
if admin_comments.contains_key(&comment.id) {
|
||||
Err(create_database_error!("insert", "admin_comments"))
|
||||
} else {
|
||||
admin_comments.insert(comment.id.to_string(), comment.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()> {
|
||||
let mut admin_comments = self.admin_comments.lock().await;
|
||||
if let Some(comment) = admin_comments.get_mut(comment_id) {
|
||||
comment.apply_options(partial.clone());
|
||||
comment.edited_at = Some(Timestamp::now_utc().format().to_string());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
admin_comments
|
||||
.get(id)
|
||||
.map_or(Err(create_error!(NotFound)), |ac| Ok(ac.clone()))
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
Ok(admin_comments
|
||||
.iter()
|
||||
.filter_map(|(_, c)| {
|
||||
if c.case_id.as_ref().is_some_and(|c| c == case_id) {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
Ok(admin_comments
|
||||
.iter()
|
||||
.filter_map(|(_, c)| {
|
||||
if c.object_id == object_id {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,20 @@ impl AbstractMigrations for MongoDb {
|
||||
init::create_database(self).await;
|
||||
}
|
||||
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
if config.features.admin_api_enabled {
|
||||
let db = self.db();
|
||||
let colls = db
|
||||
.list_collection_names()
|
||||
.await
|
||||
.expect("Failed to fetch collection names.");
|
||||
if !colls.iter().any(|x| x == "admin_audits") {
|
||||
info!("You've enabled the admin api for the first time. Setting up database...");
|
||||
init::create_admin_database(&db).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ pub async fn create_database(db: &MongoDb) {
|
||||
info!("Creating database.");
|
||||
let db = db.db();
|
||||
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
db.create_collection("accounts")
|
||||
.await
|
||||
.expect("Failed to create accounts collection.");
|
||||
@@ -98,9 +100,6 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create pubsub collection.");
|
||||
|
||||
db.create_collection("audit_logs")
|
||||
.await
|
||||
.expect("Failed to create audit_logs collection");
|
||||
db.create_collection("sessions")
|
||||
.await
|
||||
.expect("Failed to create sessions collection.");
|
||||
@@ -278,57 +277,10 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "audit_logs",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"expires_at": 1_i32,
|
||||
},
|
||||
"name": "expires_at_ttl",
|
||||
// We set the expire after to 0 because we store when it expires instead of when the document was inserted,
|
||||
// this is because mongo cant read the timestamp from the ulid so we need to do this workaround.
|
||||
// relevant docs: https://www.mongodb.com/docs/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time
|
||||
"expireAfterSeconds": 0
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"server": 1_i32,
|
||||
"user": 1_i32,
|
||||
"action.type": 1_i32,
|
||||
},
|
||||
"name": "audit_log_filters",
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create audit_logs index");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "audit_logs",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"expires_at": 1_i32,
|
||||
},
|
||||
"name": "expires_at_ttl",
|
||||
// We set the expire after to 0 because we store when it expires instead of when the document was inserted,
|
||||
// this is because mongo cant read the timestamp from the ulid so we need to do this workaround.
|
||||
// relevant docs: https://www.mongodb.com/docs/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time
|
||||
"expireAfterSeconds": 0
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"server": 1_i32,
|
||||
"user": 1_i32,
|
||||
"action.type": 1_i32,
|
||||
},
|
||||
"name": "audit_log_filters",
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create audit_logs index");
|
||||
// Only create these tables if the admin api is enabled
|
||||
if config.features.admin_api_enabled {
|
||||
create_admin_database(&db).await;
|
||||
}
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "accounts",
|
||||
@@ -416,3 +368,128 @@ pub async fn create_database(db: &MongoDb) {
|
||||
|
||||
info!("Created database.");
|
||||
}
|
||||
|
||||
pub async fn create_admin_database(db: &mongodb::Database) {
|
||||
db.create_collection("admin_audits")
|
||||
.await
|
||||
.expect("Failed to create admin_audits collection.");
|
||||
|
||||
db.create_collection("admin_comments")
|
||||
.await
|
||||
.expect("Failed to create admin_comments collection.");
|
||||
|
||||
db.create_collection("admin_cases")
|
||||
.await
|
||||
.expect("Failed to create admin_cases collection.");
|
||||
|
||||
db.create_collection("admin_strikes")
|
||||
.await
|
||||
.expect("Failed to create admin_strikes collection.");
|
||||
|
||||
db.create_collection("admin_tokens")
|
||||
.await
|
||||
.expect("Failed to create admin_tokens collection.");
|
||||
|
||||
db.create_collection("admin_users")
|
||||
.await
|
||||
.expect("Failed to create admin_users collection.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "admin_comments",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"case_id": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "case_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"object_id": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "object_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create admin_comments index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "admin_cases",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"id": 1_i32,
|
||||
},
|
||||
"unique": true,
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"short_id": 1_i32,
|
||||
},
|
||||
"unique": true,
|
||||
"name": "short_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create admin_cases index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "admin_users",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"email": 1_i32,
|
||||
},
|
||||
"unique": true,
|
||||
"name": "case_id"
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create admin_comments index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "admin_audits",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"mod_id": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "mod_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"target_id": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "target_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create admin_audits index.");
|
||||
|
||||
// indexes on regular data that are only needed for admin queries
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "server_members",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"joined_at": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "joined_at"
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create server_members::joined_at index.");
|
||||
|
||||
info!("Created admin database.");
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 52; // MUST BE +1 to last migration
|
||||
pub const LATEST_REVISION: i32 = 51; // MUST BE +1 to last migration
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -1490,39 +1490,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if revision >= 51 {
|
||||
info!("Running migration [revision 51 / 28-11-2025]: Add audit logs collection");
|
||||
|
||||
db.db()
|
||||
.create_collection("audit_logs")
|
||||
.await
|
||||
.expect("Failed to create audit_logs collection");
|
||||
|
||||
db.db()
|
||||
.run_command(doc! {
|
||||
"createIndexes": "audit_logs",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"expires_at": 1_i32,
|
||||
},
|
||||
"name": "expires_at_ttl",
|
||||
"expireAfterSeconds": 0
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"server": 1_i32,
|
||||
"user": 1_i32,
|
||||
"action.type": 1_i32,
|
||||
},
|
||||
"name": "audit_log_filters",
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create audit_logs index");
|
||||
};
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
LATEST_REVISION.max(revision)
|
||||
}
|
||||
|
||||
5
crates/core/database/src/models/admin_strikes/mod.rs
Normal file
5
crates/core/database/src/models/admin_strikes/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
47
crates/core/database/src/models/admin_strikes/models.rs
Normal file
47
crates/core/database/src/models/admin_strikes/models.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::util::basic::transform_optional_string;
|
||||
|
||||
auto_derived_partial! {
|
||||
pub struct AdminStrike {
|
||||
/// The strike ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The object receiving the strike (user/server)
|
||||
pub target_id: String,
|
||||
/// The moderator who gave the strike
|
||||
pub mod_id: String,
|
||||
/// The case the strike was made under
|
||||
pub case_id: Option<String>,
|
||||
/// Action associated with the strike (eg. suspension/ban)
|
||||
pub associated_action: Option<String>,
|
||||
/// Has the strike been removed
|
||||
pub overruled: bool,
|
||||
/// The user-facing reason for the strike
|
||||
pub reason: String,
|
||||
/// Internal context for the strike
|
||||
pub mod_context: Option<String>,
|
||||
},
|
||||
"PartialAdminStrike"
|
||||
}
|
||||
|
||||
impl AdminStrike {
|
||||
pub fn new(
|
||||
target_id: &str,
|
||||
mod_id: &str,
|
||||
case_id: Option<&str>,
|
||||
associated_action: Option<&str>,
|
||||
reason: &str,
|
||||
mod_context: Option<&str>,
|
||||
) -> AdminStrike {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminStrike {
|
||||
id,
|
||||
target_id: target_id.to_string(),
|
||||
mod_id: mod_id.to_string(),
|
||||
case_id: transform_optional_string(case_id),
|
||||
associated_action: transform_optional_string(associated_action),
|
||||
overruled: false,
|
||||
reason: reason.to_string(),
|
||||
mod_context: transform_optional_string(mod_context),
|
||||
}
|
||||
}
|
||||
}
|
||||
17
crates/core/database/src/models/admin_strikes/ops.rs
Normal file
17
crates/core/database/src/models/admin_strikes/ops.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_strikes::models::{AdminStrike, PartialAdminStrike};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminStrikes: Sync + Send {
|
||||
async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()>;
|
||||
|
||||
async fn admin_strike_update(&self, strike_id: &str, partial: PartialAdminStrike)
|
||||
-> Result<()>;
|
||||
|
||||
async fn admin_strike_get(&self, strike_id: &str) -> Result<AdminStrike>;
|
||||
|
||||
async fn admin_strike_get_user(&self, user_id: &str) -> Result<Vec<AdminStrike>>;
|
||||
}
|
||||
41
crates/core/database/src/models/admin_strikes/ops/mongodb.rs
Normal file
41
crates/core/database/src/models/admin_strikes/ops/mongodb.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminStrike, PartialAdminStrike};
|
||||
|
||||
use super::AbstractAdminStrikes;
|
||||
|
||||
static COL: &str = "admin_strikes";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminStrikes for MongoDb {
|
||||
async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()> {
|
||||
query!(self, insert_one, COL, strike).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_strike_update(
|
||||
&self,
|
||||
strike_id: &str,
|
||||
partial: PartialAdminStrike,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
strike_id,
|
||||
partial,
|
||||
vec![],
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_strike_get(&self, strike_id: &str) -> Result<AdminStrike> {
|
||||
query!(self, find_one_by_id, COL, strike_id)?
|
||||
.ok_or_else(|| create_database_error!("find_one", COL))?
|
||||
}
|
||||
|
||||
async fn admin_strike_get_user(&self, user_id: &str) -> Result<Vec<AdminStrike>> {
|
||||
query!(self, find, COL, doc! {"target_id": user_id})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminStrike, PartialAdminStrike};
|
||||
|
||||
use super::AbstractAdminStrikes;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminStrikes for ReferenceDb {
|
||||
async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()> {
|
||||
let mut admin_strikes = self.admin_strikes.lock().await;
|
||||
if admin_strikes.contains_key(&strike.id) {
|
||||
Err(create_database_error!("insert", "admin_strikes"))
|
||||
} else {
|
||||
admin_strikes.insert(strike.id.to_string(), strike.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_strike_update(
|
||||
&self,
|
||||
strike_id: &str,
|
||||
partial: PartialAdminStrike,
|
||||
) -> Result<()> {
|
||||
let mut admin_strikes = self.admin_strikes.lock().await;
|
||||
if let Some(strike) = admin_strikes.get_mut(strike_id) {
|
||||
strike.apply_options(partial);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_strike_get(&self, strike_id: &str) -> Result<AdminStrike> {
|
||||
let admin_strikes = self.admin_strikes.lock().await;
|
||||
if let Some(strike) = admin_strikes.get(strike_id) {
|
||||
Ok(strike.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_strike_get_user(&self, user_id: &str) -> Result<Vec<AdminStrike>> {
|
||||
let admin_strikes = self.admin_strikes.lock().await;
|
||||
Ok(admin_strikes
|
||||
.iter()
|
||||
.filter(|(_, strike)| strike.target_id == user_id)
|
||||
.map(|(_, strike)| strike.clone())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
12
crates/core/database/src/models/admin_tokens/mod.rs
Normal file
12
crates/core/database/src/models/admin_tokens/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod rocket;
|
||||
mod schema;
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::rocket::*;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::schema::*;
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
71
crates/core/database/src/models/admin_tokens/models.rs
Normal file
71
crates/core/database/src/models/admin_tokens/models.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AdminUser, Database};
|
||||
|
||||
auto_derived! {
|
||||
pub struct AdminToken {
|
||||
/// The token ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The user this token is attached to
|
||||
pub user_id: String,
|
||||
/// The token itself
|
||||
pub token: String,
|
||||
/// The expiry timestamp for this token, in iso6801
|
||||
pub expiry: String
|
||||
}
|
||||
|
||||
/// This struct is used to validate machine tokens when doing machine to machine communication.
|
||||
pub struct AdminMachineToken {
|
||||
/// Placeholder field.
|
||||
pub on_behalf_of: AdminUser
|
||||
}
|
||||
|
||||
pub enum AdminAuthorization {
|
||||
AdminUser(AdminUser),
|
||||
AdminMachine(AdminMachineToken),
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminToken {
|
||||
pub fn new(user_id: &str, expiry: Timestamp) -> AdminToken {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
let token = nanoid::nanoid!(64);
|
||||
AdminToken {
|
||||
id,
|
||||
user_id: user_id.to_string(),
|
||||
token,
|
||||
expiry: expiry.format_short().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminMachineToken {
|
||||
pub async fn new_from_email(email: &str, db: &Database) -> Result<AdminMachineToken> {
|
||||
println!("{}", email);
|
||||
if email == "example@example.com" {
|
||||
// This is basically just a workaround to make the first user.
|
||||
let user_count = db.admin_user_count().await?;
|
||||
println!("{}", user_count);
|
||||
if user_count == 0 {
|
||||
return Ok(AdminMachineToken {
|
||||
on_behalf_of: AdminUser {
|
||||
id: "00".to_string(),
|
||||
platform_user_id: "".to_string(),
|
||||
email: "example@example.com".to_string(),
|
||||
active: true,
|
||||
permissions: u64::MAX,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
let user = db.admin_user_fetch_email(email).await?;
|
||||
Ok(AdminMachineToken { on_behalf_of: user })
|
||||
}
|
||||
|
||||
pub async fn new_from_id(user_id: &str, db: &Database) -> Result<AdminMachineToken> {
|
||||
let user = db.admin_user_fetch(user_id).await?;
|
||||
Ok(AdminMachineToken { on_behalf_of: user })
|
||||
}
|
||||
}
|
||||
16
crates/core/database/src/models/admin_tokens/ops.rs
Normal file
16
crates/core/database/src/models/admin_tokens/ops.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_tokens::AdminToken;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminTokens: Sync + Send {
|
||||
async fn admin_token_create(&self, token: AdminToken) -> Result<()>;
|
||||
|
||||
async fn admin_token_revoke(&self, token_id: &str) -> Result<()>;
|
||||
|
||||
async fn admin_token_authenticate(&self, token: &str) -> Result<AdminToken>;
|
||||
|
||||
async fn admin_token_fetch(&self, id: &str) -> Result<AdminToken>;
|
||||
}
|
||||
34
crates/core/database/src/models/admin_tokens/ops/mongodb.rs
Normal file
34
crates/core/database/src/models/admin_tokens/ops/mongodb.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AdminToken;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractAdminTokens;
|
||||
|
||||
static COL: &str = "admin_tokens";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminTokens for MongoDb {
|
||||
async fn admin_token_create(&self, token: AdminToken) -> Result<()> {
|
||||
query!(self, insert_one, COL, token).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_token_revoke(&self, token_id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, token_id).map(|k| {
|
||||
if k.deleted_count > 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
})?
|
||||
}
|
||||
|
||||
async fn admin_token_authenticate(&self, token: &str) -> Result<AdminToken> {
|
||||
query!(self, find_one, COL, doc! {"token": token})?
|
||||
.ok_or_else(|| create_error!(InvalidCredentials))
|
||||
}
|
||||
|
||||
async fn admin_token_fetch(&self, id: &str) -> Result<AdminToken> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AdminToken;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractAdminTokens;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminTokens for ReferenceDb {
|
||||
async fn admin_token_create(&self, token: AdminToken) -> Result<()> {
|
||||
let mut admin_tokens = self.admin_tokens.lock().await;
|
||||
if admin_tokens.contains_key(&token.id) {
|
||||
Err(create_database_error!("insert", "admin_tokens"))
|
||||
} else {
|
||||
admin_tokens.insert(token.id.to_string(), token.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_token_revoke(&self, token_id: &str) -> Result<()> {
|
||||
let mut admin_tokens = self.admin_tokens.lock().await;
|
||||
if admin_tokens.remove(token_id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_token_authenticate(&self, token: &str) -> Result<AdminToken> {
|
||||
let admin_tokens = self.admin_tokens.lock().await;
|
||||
let result = admin_tokens
|
||||
.iter()
|
||||
.filter_map(|(_, t)| {
|
||||
if t.token == token {
|
||||
Some(t.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next();
|
||||
|
||||
Ok(result.ok_or_else(|| create_error!(NotFound))?)
|
||||
}
|
||||
|
||||
async fn admin_token_fetch(&self, id: &str) -> Result<AdminToken> {
|
||||
let admin_tokens = self.admin_tokens.lock().await;
|
||||
let result = admin_tokens.iter().find(|tok| tok.0 == id);
|
||||
|
||||
Ok(result
|
||||
.map(|t| t.1.clone())
|
||||
.ok_or_else(|| create_error!(NotFound))?)
|
||||
}
|
||||
}
|
||||
146
crates/core/database/src/models/admin_tokens/rocket.rs
Normal file
146
crates/core/database/src/models/admin_tokens/rocket.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::{capture_internal_error, report_error};
|
||||
use revolt_result::{Error, Result};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, FromRequest, Outcome, Request};
|
||||
|
||||
use crate::{AdminAuthorization, AdminMachineToken, AdminUser, Database};
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminMachineToken {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let user: &Option<AdminMachineToken> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
if let Some(on_behalf_of) = request
|
||||
.headers()
|
||||
.get("x-admin-on-behalf-of")
|
||||
.next()
|
||||
.map(|x| x.to_string())
|
||||
{
|
||||
if let Some(token) = request
|
||||
.headers()
|
||||
.get("x-admin-machine")
|
||||
.next()
|
||||
.map(|x| x.to_string())
|
||||
{
|
||||
let config = revolt_config::config().await;
|
||||
let token = token.to_string();
|
||||
if config.api.security.admin_keys.contains(&token) {
|
||||
let resp: Result<AdminMachineToken> = if on_behalf_of.contains("@") {
|
||||
AdminMachineToken::new_from_email(&on_behalf_of, db).await
|
||||
} else {
|
||||
AdminMachineToken::new_from_id(&on_behalf_of, db).await
|
||||
};
|
||||
if let Ok(resp) = resp {
|
||||
return Some(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(user) = user {
|
||||
Outcome::Success(user.clone())
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(InvalidSession)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminAuthorization {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let machine: &Option<AdminMachineToken> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
if let Some(token) = request
|
||||
.headers()
|
||||
.get("x-admin-machine")
|
||||
.next()
|
||||
.map(|x| x.to_string())
|
||||
{
|
||||
if let Some(on_behalf_of) = request
|
||||
.headers()
|
||||
.get("x-admin-on-behalf-of")
|
||||
.next()
|
||||
.map(|x| x.to_string())
|
||||
{
|
||||
let config = revolt_config::config().await;
|
||||
let token = token.to_string();
|
||||
if config.api.security.admin_keys.contains(&token) {
|
||||
let resp: Result<AdminMachineToken> = if on_behalf_of.contains("@") {
|
||||
AdminMachineToken::new_from_email(&on_behalf_of, db).await
|
||||
} else {
|
||||
AdminMachineToken::new_from_id(&on_behalf_of, db).await
|
||||
};
|
||||
if let Ok(resp) = resp {
|
||||
return Some(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(machine) = machine {
|
||||
if !machine.on_behalf_of.active {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(LockedOut)))
|
||||
} else {
|
||||
Outcome::Success(AdminAuthorization::AdminMachine(machine.clone()))
|
||||
}
|
||||
} else {
|
||||
let user: &Option<AdminUser> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
let token = request
|
||||
.headers()
|
||||
.get("x-admin-user")
|
||||
.next()
|
||||
.map(|x| x.to_string());
|
||||
|
||||
if let Some(token) = token {
|
||||
if let Ok(admin_token) = db.admin_token_authenticate(&token).await {
|
||||
if let Some(expiry) = Timestamp::parse(&admin_token.expiry) {
|
||||
if expiry < Timestamp::now_utc() {
|
||||
if let Err(e) = db.admin_token_revoke(&admin_token.id).await {
|
||||
capture_internal_error!(e);
|
||||
}
|
||||
return None;
|
||||
} else if let Ok(user) =
|
||||
db.admin_user_fetch(&admin_token.user_id).await
|
||||
{
|
||||
if !user.active {
|
||||
return None;
|
||||
}
|
||||
return Some(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(user) = user {
|
||||
if !user.active {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(LockedOut)))
|
||||
} else {
|
||||
Outcome::Success(AdminAuthorization::AdminUser(user.clone()))
|
||||
}
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(InvalidSession)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
crates/core/database/src/models/admin_tokens/schema.rs
Normal file
59
crates/core/database/src/models/admin_tokens/schema.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
|
||||
use revolt_rocket_okapi::{
|
||||
gen::OpenApiGenerator,
|
||||
request::{OpenApiFromRequest, RequestHeaderInput},
|
||||
};
|
||||
|
||||
use crate::{AdminAuthorization, AdminMachineToken};
|
||||
|
||||
impl OpenApiFromRequest<'_> for AdminAuthorization {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
_name: String,
|
||||
_required: bool,
|
||||
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
||||
let mut requirements = schemars::Map::new();
|
||||
requirements.insert("Admin Token".to_owned(), vec![]);
|
||||
|
||||
Ok(RequestHeaderInput::Security(
|
||||
"Admin Token".to_owned(),
|
||||
SecurityScheme {
|
||||
data: SecuritySchemeData::ApiKey {
|
||||
name: "x-admin-user".to_owned(),
|
||||
location: "header".to_owned(),
|
||||
},
|
||||
description: Some("Used to authenticate as an admin user.
|
||||
Can instead use an x-admin-machine token with an x-on-behalf-of containing the userid or email of
|
||||
the user the machine is performing the action for.".to_owned()),
|
||||
extensions: schemars::Map::new(),
|
||||
},
|
||||
requirements,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenApiFromRequest<'_> for AdminMachineToken {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
_name: String,
|
||||
_required: bool,
|
||||
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
||||
let mut requirements = schemars::Map::new();
|
||||
requirements.insert("Admin Machine Token".to_owned(), vec![]);
|
||||
|
||||
Ok(RequestHeaderInput::Security(
|
||||
"Admin Machine Token".to_owned(),
|
||||
SecurityScheme {
|
||||
data: SecuritySchemeData::ApiKey {
|
||||
name: "x-admin-machine".to_owned(),
|
||||
location: "header".to_owned(),
|
||||
},
|
||||
description: Some("Used by machines to authenticate on behalf of a user.
|
||||
Machines are trusted devices that authenticate as other users via providing the x-on-behalf-of header
|
||||
with an admin user's ID or email.".to_owned()),
|
||||
extensions: schemars::Map::new(),
|
||||
},
|
||||
requirements,
|
||||
))
|
||||
}
|
||||
}
|
||||
9
crates/core/database/src/models/admin_users/mod.rs
Normal file
9
crates/core/database/src/models/admin_users/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod rocket;
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::rocket::*;
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
69
crates/core/database/src/models/admin_users/models.rs
Normal file
69
crates/core/database/src/models/admin_users/models.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
auto_derived_partial! {
|
||||
pub struct AdminUser {
|
||||
/// The ID of the user
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The user's revolt ID.
|
||||
pub platform_user_id: String,
|
||||
/// The user's email
|
||||
pub email: String,
|
||||
/// Whether the user is active or not (ie. can they use the api)
|
||||
pub active: bool,
|
||||
/// The permissions of the user
|
||||
pub permissions: u64,
|
||||
},
|
||||
"PartialAdminUser"
|
||||
}
|
||||
|
||||
impl AdminUser {
|
||||
pub fn new(email: &str, platform_user_id: &str, permissions: u64) -> AdminUser {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminUser {
|
||||
id,
|
||||
platform_user_id: platform_user_id.to_string(),
|
||||
email: email.to_string(),
|
||||
active: true,
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_id(id: &str, db: &Database) -> Result<AdminUser> {
|
||||
return db.admin_user_fetch(id).await;
|
||||
}
|
||||
|
||||
pub async fn find_by_email(email: &str, db: &Database) -> Result<AdminUser> {
|
||||
return db.admin_user_fetch_email(email).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AdminUserPermissionFlagsValue(pub u64);
|
||||
|
||||
impl AdminUserPermissionFlagsValue {
|
||||
pub fn has(&self, flag: revolt_models::v0::AdminUserPermissionFlags) -> bool {
|
||||
self.has_value(flag as u64)
|
||||
}
|
||||
pub fn has_value(&self, bit: u64) -> bool {
|
||||
let mask = 1 << bit;
|
||||
self.0 & mask == mask
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&mut self,
|
||||
flag: revolt_models::v0::AdminUserPermissionFlags,
|
||||
toggle: bool,
|
||||
) -> &mut Self {
|
||||
self.set_value(flag as u64, toggle)
|
||||
}
|
||||
pub fn set_value(&mut self, bit: u64, toggle: bool) -> &mut Self {
|
||||
if toggle {
|
||||
self.0 |= 1 << bit;
|
||||
} else {
|
||||
self.0 &= !(1 << bit);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
20
crates/core/database/src/models/admin_users/ops.rs
Normal file
20
crates/core/database/src/models/admin_users/ops.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_users::models::{AdminUser, PartialAdminUser};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminUsers: Sync + Send {
|
||||
async fn admin_user_insert(&self, user: AdminUser) -> Result<()>;
|
||||
|
||||
async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()>;
|
||||
|
||||
async fn admin_user_fetch(&self, user_id: &str) -> Result<AdminUser>;
|
||||
|
||||
async fn admin_user_fetch_email(&self, email: &str) -> Result<AdminUser>;
|
||||
|
||||
async fn admin_user_list(&self) -> Result<Vec<AdminUser>>;
|
||||
|
||||
async fn admin_user_count(&self) -> Result<u16>;
|
||||
}
|
||||
35
crates/core/database/src/models/admin_users/ops/mongodb.rs
Normal file
35
crates/core/database/src/models/admin_users/ops/mongodb.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminUser, PartialAdminUser};
|
||||
|
||||
use super::AbstractAdminUsers;
|
||||
|
||||
static COL: &str = "admin_users";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminUsers for MongoDb {
|
||||
async fn admin_user_insert(&self, user: AdminUser) -> Result<()> {
|
||||
query!(self, insert_one, COL, user).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, user_id, partial, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_user_fetch(&self, user_id: &str) -> Result<AdminUser> {
|
||||
query!(self, find_one_by_id, COL, user_id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
async fn admin_user_fetch_email(&self, email: &str) -> Result<AdminUser> {
|
||||
query!(self, find_one, COL, doc! {"email": email})?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
async fn admin_user_list(&self) -> Result<Vec<AdminUser>> {
|
||||
query!(self, find, COL, doc! {})
|
||||
}
|
||||
|
||||
async fn admin_user_count(&self) -> Result<u16> {
|
||||
query!(self, count_documents, COL, doc! {}).map(|resp| resp as u16)
|
||||
}
|
||||
}
|
||||
56
crates/core/database/src/models/admin_users/ops/reference.rs
Normal file
56
crates/core/database/src/models/admin_users/ops/reference.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminUser, PartialAdminUser};
|
||||
|
||||
use super::AbstractAdminUsers;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminUsers for ReferenceDb {
|
||||
async fn admin_user_insert(&self, user: AdminUser) -> Result<()> {
|
||||
let mut admin_users = self.admin_users.lock().await;
|
||||
if admin_users.contains_key(&user.id) {
|
||||
Err(create_database_error!("insert", "admin_users"))
|
||||
} else {
|
||||
admin_users.insert(user.id.to_string(), user.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()> {
|
||||
let mut admin_users = self.admin_users.lock().await;
|
||||
if let Some(existing_user) = admin_users.get_mut(user_id) {
|
||||
existing_user.apply_options(partial);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_user_fetch(&self, user_id: &str) -> Result<AdminUser> {
|
||||
let admin_users = self.admin_users.lock().await;
|
||||
if let Some(user) = admin_users.get(user_id) {
|
||||
Ok(user.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_user_fetch_email(&self, email: &str) -> Result<AdminUser> {
|
||||
let admin_users = self.admin_users.lock().await;
|
||||
if let Some((_, user)) = admin_users.iter().filter(|(_, p)| p.email == email).next() {
|
||||
Ok(user.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_user_list(&self) -> Result<Vec<AdminUser>> {
|
||||
let admin_users = self.admin_users.lock().await;
|
||||
Ok(admin_users.iter().map(|(_, u)| u).cloned().collect())
|
||||
}
|
||||
|
||||
async fn admin_user_count(&self) -> Result<u16> {
|
||||
Ok(self.admin_users.lock().await.len() as u16)
|
||||
}
|
||||
}
|
||||
43
crates/core/database/src/models/admin_users/rocket.rs
Normal file
43
crates/core/database/src/models/admin_users/rocket.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, FromRequest, Outcome, Request};
|
||||
use revolt_result::Error;
|
||||
use crate::{AdminUser, Database};
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminUser {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let user: &Option<AdminUser> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
let token = request
|
||||
.headers()
|
||||
.get("x-admin-user")
|
||||
.next()
|
||||
.map(|x| x.to_string());
|
||||
|
||||
if let Some(token) = token {
|
||||
if let Ok(admin_token) = db.admin_token_authenticate(&token).await {
|
||||
if let Ok(user) = db.admin_user_fetch(&admin_token.user_id).await {
|
||||
return Some(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(user) = user {
|
||||
if !user.active {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(LockedOut)))
|
||||
} else {
|
||||
Outcome::Success(user.clone())
|
||||
}
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(InvalidCredentials)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -1,275 +0,0 @@
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::config;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{Database, PartialChannel, PartialMember, PartialRole, PartialServer, User, PartialEmoji};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
auto_derived!(
|
||||
/// Audit log entry
|
||||
pub struct AuditLogEntry {
|
||||
/// Unique ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// When the audit log entry gets auto-deleted
|
||||
///
|
||||
/// This is only stored in the database and not given to users.
|
||||
pub expires_at: Timestamp,
|
||||
|
||||
/// The server the entry happened in
|
||||
pub server: String,
|
||||
/// User provided reason
|
||||
pub reason: Option<String>,
|
||||
/// User who ran the action
|
||||
pub user: String,
|
||||
/// User this action is targetting
|
||||
pub target: Option<String>,
|
||||
/// The action ran
|
||||
pub action: AuditLogEntryAction,
|
||||
}
|
||||
|
||||
/// Indivual audit log action
|
||||
#[serde(tag = "type")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum AuditLogEntryAction {
|
||||
MessageDelete {
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
MessageBulkDelete {
|
||||
channel: String,
|
||||
count: usize,
|
||||
},
|
||||
MessagePin {
|
||||
message: String,
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
MessageUnpin {
|
||||
message: String,
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
BanCreate {
|
||||
user: String,
|
||||
},
|
||||
BanDelete {
|
||||
user: String,
|
||||
},
|
||||
ChannelCreate {
|
||||
channel: String,
|
||||
name: String,
|
||||
},
|
||||
ChannelEdit {
|
||||
channel: String,
|
||||
before: PartialChannel,
|
||||
after: PartialChannel,
|
||||
},
|
||||
ChannelRolePermissionsEdit {
|
||||
channel: String,
|
||||
role: String,
|
||||
permissions: OverrideField,
|
||||
},
|
||||
ChannelDelete {
|
||||
channel: String,
|
||||
name: String,
|
||||
},
|
||||
MemberEdit {
|
||||
user: String,
|
||||
before: PartialMember,
|
||||
after: PartialMember,
|
||||
},
|
||||
MemberKick {
|
||||
user: String,
|
||||
},
|
||||
ServerEdit {
|
||||
before: PartialServer,
|
||||
after: PartialServer,
|
||||
},
|
||||
RoleEdit {
|
||||
role: String,
|
||||
before: PartialRole,
|
||||
after: PartialRole,
|
||||
},
|
||||
RoleCreate {
|
||||
role: String,
|
||||
name: String,
|
||||
},
|
||||
RoleDelete {
|
||||
role: String,
|
||||
name: String,
|
||||
},
|
||||
RolesReorder {
|
||||
before: Vec<String>,
|
||||
after: Vec<String>,
|
||||
},
|
||||
InviteCreate {
|
||||
invite: String,
|
||||
channel: String,
|
||||
},
|
||||
InviteDelete {
|
||||
invite: String,
|
||||
channel: String,
|
||||
},
|
||||
WebhookCreate {
|
||||
webhook: String,
|
||||
name: String,
|
||||
channel: String,
|
||||
},
|
||||
WebhookDelete {
|
||||
webhook: String,
|
||||
name: String,
|
||||
channel: String,
|
||||
},
|
||||
EmojiCreate {
|
||||
emoji: String,
|
||||
name: String,
|
||||
},
|
||||
EmojiUpdate {
|
||||
emoji: String,
|
||||
before: PartialEmoji,
|
||||
after: PartialEmoji,
|
||||
},
|
||||
EmojiDelete {
|
||||
emoji: String,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Audit Log Query
|
||||
pub struct AuditLogQuery {
|
||||
/// Filter by who ran the action
|
||||
pub user: Option<String>,
|
||||
/// Filter by who the action is targetting
|
||||
pub target: Option<String>,
|
||||
/// Filter by the action type
|
||||
pub r#type: Option<Vec<String>>,
|
||||
/// Entries before a certain entry id
|
||||
pub before: Option<String>,
|
||||
/// Entries after a certain entry id
|
||||
pub after: Option<String>,
|
||||
/// Maximum number of entries to fetch
|
||||
pub limit: i64,
|
||||
}
|
||||
);
|
||||
|
||||
impl AuditLogEntryAction {
|
||||
// TODO: migrate this to a rabbitmq queue to avoid spawning lots of tasks
|
||||
/// Generates an `AuditLogEntry` for the current action and inserts it into the database
|
||||
pub async fn insert<R: Into<Option<String>>>(
|
||||
self,
|
||||
db: &Database,
|
||||
server: String,
|
||||
reason: R,
|
||||
user: String,
|
||||
target: Option<String>,
|
||||
) -> AuditLogEntry {
|
||||
let config = config().await;
|
||||
|
||||
let id = Ulid::new();
|
||||
let expires_at = id
|
||||
.datetime()
|
||||
.checked_add(Duration::from_secs(config.api.audit_logs.expires_after))
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
let entry = AuditLogEntry {
|
||||
id: id.to_string(),
|
||||
expires_at,
|
||||
server,
|
||||
reason: reason.into(),
|
||||
user,
|
||||
target,
|
||||
action: self,
|
||||
};
|
||||
|
||||
// running the insert inside a task can cause race conditions in the test so for now just dont use a task for tests for now
|
||||
// this will need to be redone for when we migrate to using rabbitmq here anyway.
|
||||
#[cfg(not(test))]
|
||||
tokio::task::spawn({
|
||||
let db = db.clone();
|
||||
let entry = entry.clone();
|
||||
|
||||
async move { revolt_config::report_internal_error!(db.insert_audit_log_entry(&entry).await) }
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
db.insert_audit_log_entry(&entry).await.unwrap();
|
||||
|
||||
entry
|
||||
}
|
||||
}
|
||||
|
||||
impl AuditLogEntry {
|
||||
/// Fetches the corrasponding users and members for each audit log entry
|
||||
pub async fn with_users(
|
||||
db: &Database,
|
||||
server_id: &str,
|
||||
user: &User,
|
||||
entries: &[Self],
|
||||
) -> Result<(Vec<v0::User>, Vec<v0::Member>)> {
|
||||
let mut user_ids = HashSet::new();
|
||||
|
||||
for entry in entries {
|
||||
user_ids.insert(entry.user.clone());
|
||||
|
||||
match &entry.action {
|
||||
AuditLogEntryAction::MessageDelete { author, .. } => {
|
||||
user_ids.insert(author.clone());
|
||||
}
|
||||
AuditLogEntryAction::BanCreate { user } => {
|
||||
user_ids.insert(user.clone());
|
||||
}
|
||||
AuditLogEntryAction::BanDelete { user } => {
|
||||
user_ids.insert(user.clone());
|
||||
}
|
||||
AuditLogEntryAction::ChannelCreate { .. } => {}
|
||||
AuditLogEntryAction::MemberEdit { user, .. } => {
|
||||
user_ids.insert(user.clone());
|
||||
}
|
||||
AuditLogEntryAction::MemberKick { user } => {
|
||||
user_ids.insert(user.clone());
|
||||
}
|
||||
AuditLogEntryAction::MessagePin { author, .. } => {
|
||||
user_ids.insert(author.clone());
|
||||
}
|
||||
AuditLogEntryAction::MessageUnpin { author, .. } => {
|
||||
user_ids.insert(author.clone());
|
||||
}
|
||||
AuditLogEntryAction::ServerEdit { .. } => {}
|
||||
AuditLogEntryAction::RoleEdit { .. } => {}
|
||||
AuditLogEntryAction::RoleCreate { .. } => {}
|
||||
AuditLogEntryAction::RoleDelete { .. } => {}
|
||||
AuditLogEntryAction::RolesReorder { .. } => {}
|
||||
AuditLogEntryAction::MessageBulkDelete { .. } => {}
|
||||
AuditLogEntryAction::ChannelEdit { .. } => {}
|
||||
AuditLogEntryAction::ChannelRolePermissionsEdit { .. } => {}
|
||||
AuditLogEntryAction::ChannelDelete { .. } => {}
|
||||
AuditLogEntryAction::InviteCreate { .. } => {}
|
||||
AuditLogEntryAction::InviteDelete { .. } => {}
|
||||
AuditLogEntryAction::WebhookCreate { .. } => {}
|
||||
AuditLogEntryAction::WebhookDelete { .. } => {}
|
||||
AuditLogEntryAction::EmojiCreate { .. } => {}
|
||||
AuditLogEntryAction::EmojiUpdate { .. } => {}
|
||||
AuditLogEntryAction::EmojiDelete { .. } => {}
|
||||
};
|
||||
}
|
||||
|
||||
let user_ids = user_ids.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let users = User::fetch_many_ids_as_mutuals(db, user, &user_ids).await?;
|
||||
let members = db
|
||||
.fetch_members(server_id, &user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok((users, members))
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AuditLogEntry, AuditLogQuery};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAuditLogs: Sync + Send {
|
||||
/// Inserts an entry into the server's audit log
|
||||
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()>;
|
||||
|
||||
/// Fetches a server's audit logs using the provided query options
|
||||
async fn get_server_audit_logs(
|
||||
&self,
|
||||
server: &str,
|
||||
query: AuditLogQuery,
|
||||
) -> Result<Vec<AuditLogEntry>>;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use mongodb::options::FindOptions;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AuditLogEntry, AuditLogQuery, MongoDb};
|
||||
|
||||
use super::AbstractAuditLogs;
|
||||
|
||||
static COL: &str = "audit_logs";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAuditLogs for MongoDb {
|
||||
/// Inserts an entry into the server's audit log
|
||||
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()> {
|
||||
query!(self, insert_one, COL, entry).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetches a server's audit logs using the provided query options
|
||||
async fn get_server_audit_logs(
|
||||
&self,
|
||||
server: &str,
|
||||
query: AuditLogQuery,
|
||||
) -> Result<Vec<AuditLogEntry>> {
|
||||
let mut filter = doc! {
|
||||
"server": server
|
||||
};
|
||||
|
||||
if let Some(user) = query.user {
|
||||
filter.insert("user", user);
|
||||
};
|
||||
|
||||
if let Some(target) = query.target {
|
||||
filter.insert("target", target);
|
||||
}
|
||||
|
||||
if let Some(types) = query.r#type {
|
||||
filter.insert("action.type", doc! { "$in": types });
|
||||
};
|
||||
|
||||
if let Some(doc) = match (query.before, query.after) {
|
||||
(Some(before), Some(after)) => Some(doc! {
|
||||
"$lt": before,
|
||||
"$gt": after
|
||||
}),
|
||||
(Some(before), _) => Some(doc! {
|
||||
"$lt": before
|
||||
}),
|
||||
(_, Some(after)) => Some(doc! {
|
||||
"$gt": after
|
||||
}),
|
||||
_ => None,
|
||||
} {
|
||||
filter.insert("_id", doc);
|
||||
};
|
||||
|
||||
self.find_with_options(
|
||||
COL,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.limit(query.limit)
|
||||
.sort(doc! { "_id": -1 })
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AuditLogEntry, AuditLogQuery, ReferenceDb};
|
||||
|
||||
use super::AbstractAuditLogs;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAuditLogs for ReferenceDb {
|
||||
/// Inserts an entry into the server's audit log
|
||||
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()> {
|
||||
self.audit_logs
|
||||
.lock()
|
||||
.await
|
||||
.insert(entry.id.clone(), entry.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetches a server's audit logs using the provided query options
|
||||
async fn get_server_audit_logs(
|
||||
&self,
|
||||
server: &str,
|
||||
query: AuditLogQuery,
|
||||
) -> Result<Vec<AuditLogEntry>> {
|
||||
let lock = self.audit_logs.lock().await;
|
||||
|
||||
let mut logs = lock
|
||||
.values()
|
||||
.filter(|entry| {
|
||||
if entry.server != server {
|
||||
return false;
|
||||
};
|
||||
|
||||
if let Some(user) = &query.user {
|
||||
if &entry.user != user {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if query.target.is_some() && entry.target != query.target {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(before) = &query.before {
|
||||
if &entry.id > before {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
if let Some(after) = &query.after {
|
||||
if &entry.id < after {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
if let Some(action_types) = &query.r#type {
|
||||
let entry_type = serde_json::to_value(entry.action.clone())
|
||||
.unwrap()
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("type")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
if !action_types.contains(&entry_type) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
logs.sort_by(|a, b| b.id.cmp(&a.id));
|
||||
logs.truncate(query.limit as usize);
|
||||
Ok(logs)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,14 @@ static COL: &str = "channel_invites";
|
||||
impl AbstractChannelInvites for MongoDb {
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
||||
query!(self, insert_one, COL, &invite).map(|_| ())
|
||||
self.insert_one(COL, &invite).await.or_else(|e| {
|
||||
if e.to_string().contains("duplicate") {
|
||||
return Err(create_error!(InviteExists));
|
||||
} else {
|
||||
return Err(create_database_error!("insert_one", COL));
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch an invite by the code
|
||||
|
||||
@@ -648,122 +648,6 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialChannel containing the data which has changed in an update
|
||||
pub fn generate_diff(
|
||||
&self,
|
||||
partial: &PartialChannel,
|
||||
remove: &[FieldsChannel],
|
||||
) -> PartialChannel {
|
||||
let mut before = PartialChannel::default();
|
||||
|
||||
match self {
|
||||
Channel::SavedMessages { .. } => {}
|
||||
Channel::DirectMessage {
|
||||
active,
|
||||
last_message_id,
|
||||
..
|
||||
} => {
|
||||
if partial.active.is_some() {
|
||||
before.active = Some(*active);
|
||||
};
|
||||
|
||||
if partial.last_message_id.is_some() {
|
||||
before.last_message_id = last_message_id.clone()
|
||||
};
|
||||
}
|
||||
Channel::Group {
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
icon,
|
||||
last_message_id,
|
||||
permissions,
|
||||
nsfw,
|
||||
..
|
||||
} => {
|
||||
if partial.name.is_some() {
|
||||
before.name = Some(name.clone());
|
||||
};
|
||||
|
||||
if partial.owner.is_some() {
|
||||
before.owner = Some(owner.clone());
|
||||
};
|
||||
|
||||
if partial.description.is_some() || remove.contains(&FieldsChannel::Description) {
|
||||
before.description = description.clone();
|
||||
};
|
||||
|
||||
if partial.icon.is_some() || remove.contains(&FieldsChannel::Icon) {
|
||||
before.icon = icon.clone();
|
||||
};
|
||||
|
||||
if partial.last_message_id.is_some() {
|
||||
before.last_message_id = last_message_id.clone()
|
||||
};
|
||||
|
||||
if partial.permissions.is_some() {
|
||||
before.permissions = *permissions;
|
||||
};
|
||||
|
||||
if partial.nsfw.is_some() {
|
||||
before.nsfw = Some(*nsfw);
|
||||
};
|
||||
}
|
||||
Channel::TextChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
last_message_id,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
voice,
|
||||
slowmode,
|
||||
..
|
||||
} => {
|
||||
if partial.name.is_some() {
|
||||
before.name = Some(name.clone());
|
||||
};
|
||||
|
||||
if partial.description.is_some() || remove.contains(&FieldsChannel::Description) {
|
||||
before.description = description.clone();
|
||||
};
|
||||
|
||||
if partial.icon.is_some() || remove.contains(&FieldsChannel::Icon) {
|
||||
before.icon = icon.clone();
|
||||
};
|
||||
|
||||
if partial.last_message_id.is_some() {
|
||||
before.last_message_id = last_message_id.clone()
|
||||
};
|
||||
|
||||
if partial.default_permissions.is_some()
|
||||
|| remove.contains(&FieldsChannel::DefaultPermissions)
|
||||
{
|
||||
before.default_permissions = *default_permissions;
|
||||
};
|
||||
|
||||
if partial.role_permissions.is_some() {
|
||||
before.role_permissions = Some(role_permissions.clone());
|
||||
};
|
||||
|
||||
if partial.nsfw.is_some() {
|
||||
before.nsfw = Some(*nsfw);
|
||||
};
|
||||
|
||||
if partial.voice.is_some() || remove.contains(&FieldsChannel::Voice) {
|
||||
before.voice = voice.clone();
|
||||
};
|
||||
|
||||
if partial.slowmode.is_some() {
|
||||
before.slowmode = *slowmode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
before
|
||||
}
|
||||
|
||||
/// Acknowledge a message
|
||||
pub async fn ack(&self, user: &str, message: &str, amqp: &AMQP) -> Result<()> {
|
||||
EventV1::ChannelAck {
|
||||
@@ -883,6 +767,13 @@ impl Channel {
|
||||
// - channels list / categories list on server
|
||||
db.delete_channel(self).await
|
||||
}
|
||||
|
||||
/// Wipe a channel's messages
|
||||
pub async fn wipe(&self, db: &Database) -> Result<()> {
|
||||
let id = self.id().to_string();
|
||||
EventV1::ChannelWipe { id: id.clone() }.p(id).await;
|
||||
db.wipe_channel(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
|
||||
@@ -54,5 +54,6 @@ pub trait AbstractChannels: Sync + Send {
|
||||
async fn remove_user_from_groups(&self, channel_ids: Vec<String>, user_id: &str) -> Result<()>;
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
|
||||
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>; // Wipe a channel's messages
|
||||
async fn wipe_channel(&self, channel: &Channel) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -304,6 +304,23 @@ impl AbstractChannels for MongoDb {
|
||||
// Delete the channel itself
|
||||
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
|
||||
}
|
||||
|
||||
// Wipe a channel's messages
|
||||
async fn wipe_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let id = channel.id().to_string();
|
||||
|
||||
// Delete invites and unreads.
|
||||
self.delete_associated_channel_objects(Bson::String(id.to_string()))
|
||||
.await?;
|
||||
|
||||
// Delete messages.
|
||||
self.delete_bulk_messages(doc! {
|
||||
"channel": &id
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
|
||||
@@ -182,4 +182,10 @@ impl AbstractChannels for ReferenceDb {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn wipe_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
messages.retain(|_, message| message.channel != channel.id());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
.collect()
|
||||
});
|
||||
|
||||
auto_derived_partial!(
|
||||
auto_derived!(
|
||||
/// Emoji
|
||||
pub struct Emoji {
|
||||
/// Unique Id
|
||||
@@ -34,17 +34,20 @@ auto_derived_partial!(
|
||||
/// Whether the emoji is marked as nsfw
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub nsfw: bool,
|
||||
},
|
||||
"PartialEmoji"
|
||||
);
|
||||
}
|
||||
|
||||
auto_derived!(
|
||||
/// Parent Id of the emoji
|
||||
#[serde(tag = "type")]
|
||||
pub enum EmojiParent {
|
||||
Server { id: String },
|
||||
Detached,
|
||||
}
|
||||
|
||||
/// Partial representation of an emoji
|
||||
pub struct PartialEmoji {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
@@ -69,14 +72,14 @@ impl Emoji {
|
||||
}
|
||||
|
||||
/// Delete an emoji
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
EventV1::EmojiDelete {
|
||||
id: self.id.to_string(),
|
||||
}
|
||||
.p(self.parent().to_string())
|
||||
.await;
|
||||
|
||||
db.detach_emoji(self).await
|
||||
db.detach_emoji(&self).await
|
||||
}
|
||||
|
||||
/// Update an emoji
|
||||
@@ -109,18 +112,4 @@ impl Emoji {
|
||||
Ok(PERMISSIBLE_EMOJIS.contains(&sanitized_emoji))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialEmoji containing the data which has changed in an update
|
||||
pub fn generate_diff(&self, partial: &PartialEmoji) -> PartialEmoji {
|
||||
let mut before = PartialEmoji::default();
|
||||
|
||||
generate_diff!(
|
||||
self, before, partial, remove,
|
||||
(
|
||||
name,
|
||||
)
|
||||
);
|
||||
|
||||
before
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,13 +998,11 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Delete a message
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
let file_ids = self
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
let file_ids: Vec<String> = self
|
||||
.attachments
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|file| file.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
if !file_ids.is_empty() {
|
||||
db.mark_attachments_as_deleted(&file_ids).await?;
|
||||
@@ -1013,10 +1011,10 @@ impl Message {
|
||||
db.delete_message(&self.id).await?;
|
||||
|
||||
EventV1::MessageDelete {
|
||||
id: self.id.clone(),
|
||||
id: self.id,
|
||||
channel: self.channel.clone(),
|
||||
}
|
||||
.p(self.channel.clone())
|
||||
.p(self.channel)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
mod admin_audits;
|
||||
mod admin_cases;
|
||||
mod admin_comments;
|
||||
mod admin_migrations;
|
||||
mod audit_logs;
|
||||
mod admin_strikes;
|
||||
mod admin_tokens;
|
||||
mod admin_users;
|
||||
mod bots;
|
||||
mod channel_invites;
|
||||
mod channel_unreads;
|
||||
@@ -23,8 +28,13 @@ mod account_invites;
|
||||
mod sessions;
|
||||
mod mfa_tickets;
|
||||
|
||||
pub use admin_audits::*;
|
||||
pub use admin_cases::*;
|
||||
pub use admin_comments::*;
|
||||
pub use admin_migrations::*;
|
||||
pub use audit_logs::*;
|
||||
pub use admin_strikes::*;
|
||||
pub use admin_tokens::*;
|
||||
pub use admin_users::*;
|
||||
pub use bots::*;
|
||||
pub use channel_invites::*;
|
||||
pub use channel_unreads::*;
|
||||
@@ -56,8 +66,13 @@ use crate::MongoDb;
|
||||
pub trait AbstractDatabase:
|
||||
Sync
|
||||
+ Send
|
||||
+ admin_audits::AbstractAdminAudits
|
||||
+ admin_cases::AbstractAdminCases
|
||||
+ admin_comments::AbstractAdminComments
|
||||
+ admin_strikes::AbstractAdminStrikes
|
||||
+ admin_users::AbstractAdminUsers
|
||||
+ admin_tokens::AbstractAdminTokens
|
||||
+ admin_migrations::AbstractMigrations
|
||||
+ audit_logs::AbstractAuditLogs
|
||||
+ bots::AbstractBots
|
||||
+ channels::AbstractChannels
|
||||
+ channel_invites::AbstractChannelInvites
|
||||
|
||||
@@ -18,5 +18,7 @@ auto_derived!(
|
||||
/// Additional notes included on the report
|
||||
#[serde(default)]
|
||||
pub notes: String,
|
||||
/// The case this report is assigned to
|
||||
pub case_id: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -110,6 +110,7 @@ impl Member {
|
||||
server: &Server,
|
||||
user: &User,
|
||||
channels: Option<Vec<Channel>>,
|
||||
create_alert: bool,
|
||||
) -> Result<(Member, Vec<Channel>)> {
|
||||
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
|
||||
return Err(create_error!(Banned));
|
||||
@@ -188,18 +189,20 @@ impl Member {
|
||||
.private(user.id.clone())
|
||||
.await;
|
||||
|
||||
if let Some(id) = server
|
||||
.system_messages
|
||||
.as_ref()
|
||||
.and_then(|x| x.user_joined.as_ref())
|
||||
{
|
||||
SystemMessage::UserJoined {
|
||||
id: user.id.clone(),
|
||||
if create_alert {
|
||||
if let Some(id) = server
|
||||
.system_messages
|
||||
.as_ref()
|
||||
.and_then(|x| x.user_joined.as_ref())
|
||||
{
|
||||
SystemMessage::UserJoined {
|
||||
id: user.id.clone(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send_without_notifications(db, None, None, false, false, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send_without_notifications(db, None, None, false, false, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok((member, channels))
|
||||
@@ -245,26 +248,6 @@ impl Member {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialMember containing the data which has changed in an update
|
||||
pub fn generate_diff(&self, partial: &PartialMember, remove: &[FieldsMember]) -> PartialMember {
|
||||
let mut before = PartialMember::default();
|
||||
|
||||
generate_diff!(
|
||||
self, before, partial, remove,
|
||||
(
|
||||
(FieldsMember::Nickname) nickname,
|
||||
(FieldsMember::Avatar) avatar,
|
||||
(FieldsMember::Timeout) timeout,
|
||||
(FieldsMember::Pronouns) pronouns,
|
||||
((default) FieldsMember::Roles) roles,
|
||||
((default) FieldsMember::CanPublish) can_publish,
|
||||
((default) FieldsMember::CanReceive) can_receive,
|
||||
)
|
||||
);
|
||||
|
||||
before
|
||||
}
|
||||
|
||||
/// Get this user's current ranking
|
||||
pub fn get_ranking(&self, server: &Server) -> i64 {
|
||||
let mut value = i64::MAX;
|
||||
@@ -290,7 +273,7 @@ impl Member {
|
||||
|
||||
/// Remove member from server
|
||||
pub async fn remove(
|
||||
&self,
|
||||
self,
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
intention: RemovalIntention,
|
||||
@@ -317,9 +300,9 @@ impl Member {
|
||||
})
|
||||
{
|
||||
match intention {
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user.clone() },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user.clone() },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user.clone() },
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user },
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
// TODO: support notifications here in the future?
|
||||
@@ -369,8 +352,10 @@ mod tests {
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
Member::create(&db, &server, &owner, None).await.unwrap();
|
||||
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
|
||||
Member::create(&db, &server, &owner, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut kickable_member = Member::create(&db, &server, &kickable_user, None, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
@@ -394,7 +379,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let kickable_member = Member::create(&db, &server, &kickable_user, None)
|
||||
let kickable_member = Member::create(&db, &server, &kickable_user, None, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
@@ -132,4 +132,18 @@ pub trait AbstractServerMembers: Sync + Send {
|
||||
|
||||
/// Removes a user from every server they are in
|
||||
async fn clear_memberships(&self, user_id: &str) -> Result<()>;
|
||||
|
||||
/// Fetch all participants of a server.
|
||||
async fn fetch_server_participants(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> Result<Vec<(crate::User, Option<Member>)>>;
|
||||
|
||||
/// Fetch all members of a server
|
||||
async fn fetch_server_members(
|
||||
&self,
|
||||
server_id: &str,
|
||||
page_size: u8,
|
||||
after: Option<usize>,
|
||||
) -> Result<Vec<(crate::User, Member)>>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use bson::Document;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use futures::StreamExt;
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use mongodb::options::ReadConcern;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
use crate::{AbstractUsers, FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
|
||||
@@ -337,6 +339,88 @@ impl AbstractServerMembers for MongoDb {
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("delete_many", COL))
|
||||
}
|
||||
|
||||
async fn fetch_server_participants(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> Result<Vec<(crate::User, Option<Member>)>> {
|
||||
let server: crate::Server = query!(self, find_one, "servers", doc! {"_id": server_id})?
|
||||
.ok_or_else(|| create_database_error!("find", "servers"))?;
|
||||
let ids: Vec<String> = self
|
||||
.db()
|
||||
.collection::<crate::Message>("messages")
|
||||
.distinct("author", doc! {"channel": {"$in": server.channels}})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("distinct", "messages"))?
|
||||
.iter()
|
||||
.map(|b| bson::from_bson::<String>(b.clone()).unwrap()) // json encoded string for some logic-defying reason
|
||||
.collect();
|
||||
|
||||
println!("{:?}", &ids);
|
||||
|
||||
let users = self.fetch_users(&ids).await?;
|
||||
let members = self
|
||||
.fetch_members(server_id, &ids)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|f| (f.id.user.clone(), f.clone()))
|
||||
.collect::<HashMap<String, Member>>();
|
||||
|
||||
Ok(users
|
||||
.iter()
|
||||
.map(|u| (u.clone(), members.get(&u.id).cloned()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn fetch_server_members(
|
||||
&self,
|
||||
server_id: &str,
|
||||
page_size: u8,
|
||||
after: Option<usize>,
|
||||
) -> Result<Vec<(crate::User, Member)>> {
|
||||
let mut members_stream = if let Some(after) = after {
|
||||
self.col::<crate::Member>(COL)
|
||||
.find(doc! {"_id.server": server_id, "joined_at": {"$gt": after as i64}})
|
||||
.sort(doc! {"joined_at": -1})
|
||||
.limit(page_size as i64)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
} else {
|
||||
self.col::<crate::Member>(COL)
|
||||
.find(doc! {"_id.server": server_id})
|
||||
.sort(doc! {"joined_at": -1})
|
||||
.limit(page_size as i64)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
};
|
||||
|
||||
let mut members = vec![];
|
||||
if members_stream.advance().await.is_ok_and(|f| f) {
|
||||
loop {
|
||||
if let Ok(x) = members_stream.deserialize_current() {
|
||||
members.push(x);
|
||||
if let Ok(r) = members_stream.advance().await {
|
||||
if !r {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let user_ids: Vec<String> = members.iter().map(|m| m.id.user.clone()).collect();
|
||||
let mut set = HashMap::new();
|
||||
self.fetch_users(&user_ids).await?.iter().for_each(|f| {
|
||||
set.insert(f.id.clone(), f.clone());
|
||||
});
|
||||
|
||||
return Ok(members
|
||||
.iter()
|
||||
.map(|f| (set.remove(&f.id.user).unwrap(), f.clone()))
|
||||
.collect());
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsMember {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
@@ -209,4 +212,91 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_server_participants(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> Result<Vec<(crate::User, Option<Member>)>> {
|
||||
let servers = self.servers.lock().await;
|
||||
let server = servers.get(server_id).ok_or(create_error!(NotFound))?;
|
||||
|
||||
let mut hash = HashSet::new();
|
||||
server.channels.iter().for_each(|c| {
|
||||
hash.insert(c.clone());
|
||||
});
|
||||
|
||||
drop(servers);
|
||||
|
||||
let messages = self.messages.lock().await;
|
||||
let userids = messages
|
||||
.iter()
|
||||
.filter(|(_, message)| hash.contains(&message.channel))
|
||||
.map(|(_, message)| message.author.clone())
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
drop(messages);
|
||||
|
||||
let users = self.users.lock().await;
|
||||
let members = self.server_members.lock().await;
|
||||
|
||||
let resp_users: Vec<(crate::User, Option<crate::Member>)> = users
|
||||
.iter()
|
||||
.filter(|(uid, _)| userids.contains(*uid))
|
||||
.map(|(uid, user)| {
|
||||
let key = MemberCompositeKey {
|
||||
server: server_id.to_string(),
|
||||
user: uid.clone(),
|
||||
};
|
||||
// fuck it we ball
|
||||
(user.clone(), members.get(&key).map(|f| f.clone()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(resp_users)
|
||||
}
|
||||
|
||||
/// Fetch all members of a server
|
||||
async fn fetch_server_members(
|
||||
&self,
|
||||
server_id: &str,
|
||||
page_size: u8,
|
||||
after: Option<usize>,
|
||||
) -> Result<Vec<(crate::User, Member)>> {
|
||||
let members = self.server_members.lock().await;
|
||||
let users = self.users.lock().await;
|
||||
|
||||
let mut server_members: Vec<&Member> = members
|
||||
.iter()
|
||||
.filter(|(_, f)| f.id.server == server_id)
|
||||
.map(|(_, m)| m)
|
||||
.collect();
|
||||
|
||||
server_members.sort_by(|a, b| a.joined_at.cmp(&b.joined_at));
|
||||
let iterator = server_members.iter();
|
||||
|
||||
let mut resp: Vec<&&Member> = if let Some(after) = after {
|
||||
iterator
|
||||
.skip_while(|f| {
|
||||
f.joined_at
|
||||
.duration_since(Timestamp::UNIX_EPOCH)
|
||||
.whole_milliseconds()
|
||||
<= after as i128
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
iterator.collect()
|
||||
};
|
||||
|
||||
resp.truncate(page_size as usize);
|
||||
|
||||
Ok(resp
|
||||
.iter()
|
||||
.map(|f| {
|
||||
(
|
||||
users.get(&f.id.user).unwrap().clone(),
|
||||
f.clone().clone().clone(),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,31 +235,6 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialServer containing the data which has changed in an update
|
||||
pub fn generate_diff(&self, partial: &PartialServer, remove: &[FieldsServer]) -> PartialServer {
|
||||
let mut before = PartialServer::default();
|
||||
|
||||
generate_diff!(
|
||||
self, before, partial, remove,
|
||||
(
|
||||
owner,
|
||||
name,
|
||||
(FieldsServer::Description) description,
|
||||
(FieldsServer::Categories) categories,
|
||||
(FieldsServer::SystemMessages) system_messages,
|
||||
roles,
|
||||
default_permissions,
|
||||
(FieldsServer::Icon) icon,
|
||||
(FieldsServer::Banner) banner,
|
||||
nsfw,
|
||||
analytics,
|
||||
discoverable,
|
||||
)
|
||||
);
|
||||
|
||||
before
|
||||
}
|
||||
|
||||
/// Ordered roles list
|
||||
pub fn ordered_roles(&self) -> Vec<(String, Role)> {
|
||||
let mut ordered_roles = self.roles.clone().into_iter().collect::<Vec<_>>();
|
||||
@@ -402,27 +377,8 @@ impl Role {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a PartialRole containing the data which has changed in an update
|
||||
pub fn generate_diff(&self, partial: &PartialRole, remove: &[FieldsRole]) -> PartialRole {
|
||||
let mut before = PartialRole::default();
|
||||
|
||||
generate_diff!(
|
||||
self, before, partial, remove,
|
||||
(
|
||||
name,
|
||||
permissions,
|
||||
(FieldsRole::Colour) colour,
|
||||
hoist,
|
||||
rank,
|
||||
(FieldsRole::Icon) icon,
|
||||
)
|
||||
);
|
||||
|
||||
before
|
||||
}
|
||||
|
||||
/// Delete a role
|
||||
pub async fn delete(&self, db: &Database, server_id: &str) -> Result<()> {
|
||||
pub async fn delete(self, db: &Database, server_id: &str) -> Result<()> {
|
||||
EventV1::ServerRoleDelete {
|
||||
id: server_id.to_string(),
|
||||
role_id: self.id.clone(),
|
||||
|
||||
@@ -259,13 +259,6 @@ impl MongoDb {
|
||||
})
|
||||
.await?;
|
||||
|
||||
self.col::<Document>("audit_logs")
|
||||
.delete_many(doc! {
|
||||
"server": &server_id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "audit_logs"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,6 +886,30 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserFlagsValue(pub u32);
|
||||
|
||||
impl UserFlagsValue {
|
||||
pub fn has(&self, flag: UserFlags) -> bool {
|
||||
self.has_value(flag as u32)
|
||||
}
|
||||
pub fn has_value(&self, bit: u32) -> bool {
|
||||
let mask = 1 << bit;
|
||||
self.0 & mask == mask
|
||||
}
|
||||
|
||||
pub fn set(&mut self, flag: UserFlags, toggle: bool) -> &mut Self {
|
||||
self.set_value(flag as u32, toggle)
|
||||
}
|
||||
pub fn set_value(&mut self, bit: u32, toggle: bool) -> &mut Self {
|
||||
if toggle {
|
||||
self.0 |= 1 << bit;
|
||||
} else {
|
||||
self.0 &= !(1 << bit);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::User;
|
||||
|
||||
3
crates/core/database/src/util/basic.rs
Normal file
3
crates/core/database/src/util/basic.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub fn transform_optional_string(s: Option<&str>) -> Option<String> {
|
||||
s.map(|f| f.to_string())
|
||||
}
|
||||
@@ -612,6 +612,7 @@ impl From<crate::Report> for Report {
|
||||
additional_context: value.additional_context,
|
||||
status: value.status,
|
||||
notes: value.notes,
|
||||
case_id: value.case_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1421,14 +1422,6 @@ impl From<FieldsMessage> for crate::FieldsMessage {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::VoiceInformation> for VoiceInformation {
|
||||
fn from(value: crate::VoiceInformation) -> Self {
|
||||
VoiceInformation {
|
||||
max_users: value.max_users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VoiceInformation> for crate::VoiceInformation {
|
||||
fn from(value: VoiceInformation) -> Self {
|
||||
crate::VoiceInformation {
|
||||
@@ -1437,151 +1430,10 @@ impl From<VoiceInformation> for crate::VoiceInformation {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::AuditLogEntryAction> for AuditLogEntryAction {
|
||||
fn from(value: crate::AuditLogEntryAction) -> Self {
|
||||
match value {
|
||||
crate::AuditLogEntryAction::MessageDelete { author, channel } => {
|
||||
AuditLogEntryAction::MessageDelete { author, channel }
|
||||
}
|
||||
crate::AuditLogEntryAction::BanCreate { user } => {
|
||||
AuditLogEntryAction::BanCreate { user }
|
||||
}
|
||||
crate::AuditLogEntryAction::BanDelete { user } => {
|
||||
AuditLogEntryAction::BanDelete { user }
|
||||
}
|
||||
crate::AuditLogEntryAction::ChannelCreate { channel, name } => {
|
||||
AuditLogEntryAction::ChannelCreate { channel, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::MemberEdit {
|
||||
user,
|
||||
before,
|
||||
after,
|
||||
} => AuditLogEntryAction::MemberEdit {
|
||||
user,
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::MemberKick { user } => {
|
||||
AuditLogEntryAction::MemberKick { user }
|
||||
}
|
||||
crate::AuditLogEntryAction::ServerEdit { before, after } => {
|
||||
AuditLogEntryAction::ServerEdit {
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
}
|
||||
}
|
||||
crate::AuditLogEntryAction::RoleEdit {
|
||||
role,
|
||||
before,
|
||||
after,
|
||||
} => AuditLogEntryAction::RoleEdit {
|
||||
role,
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::RoleCreate { role, name } => {
|
||||
AuditLogEntryAction::RoleCreate { role, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::RoleDelete { role, name } => {
|
||||
AuditLogEntryAction::RoleDelete { role, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::RolesReorder { before, after } => {
|
||||
AuditLogEntryAction::RolesReorder { before, after }
|
||||
}
|
||||
crate::AuditLogEntryAction::MessageBulkDelete { channel, count } => {
|
||||
AuditLogEntryAction::MessageBulkDelete { channel, count }
|
||||
}
|
||||
crate::AuditLogEntryAction::ChannelEdit {
|
||||
channel,
|
||||
before,
|
||||
after,
|
||||
} => AuditLogEntryAction::ChannelEdit {
|
||||
channel,
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::ChannelRolePermissionsEdit {
|
||||
channel,
|
||||
role,
|
||||
permissions,
|
||||
} => AuditLogEntryAction::ChannelRolePermissionsEdit {
|
||||
channel,
|
||||
role,
|
||||
permissions: permissions.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::ChannelDelete { channel, name } => {
|
||||
AuditLogEntryAction::ChannelDelete { channel, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::InviteDelete { invite, channel } => {
|
||||
AuditLogEntryAction::InviteDelete { invite, channel }
|
||||
}
|
||||
crate::AuditLogEntryAction::WebhookCreate {
|
||||
webhook,
|
||||
name,
|
||||
channel,
|
||||
} => AuditLogEntryAction::WebhookCreate {
|
||||
webhook,
|
||||
name,
|
||||
channel,
|
||||
},
|
||||
crate::AuditLogEntryAction::WebhookDelete {
|
||||
webhook,
|
||||
name,
|
||||
channel,
|
||||
} => AuditLogEntryAction::WebhookDelete {
|
||||
webhook,
|
||||
name,
|
||||
channel,
|
||||
},
|
||||
crate::AuditLogEntryAction::EmojiCreate { emoji, name } => {
|
||||
AuditLogEntryAction::EmojiCreate { emoji, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::EmojiUpdate {
|
||||
emoji,
|
||||
before,
|
||||
after,
|
||||
} => AuditLogEntryAction::EmojiUpdate {
|
||||
emoji,
|
||||
before: before.into(),
|
||||
after: after.into(),
|
||||
},
|
||||
crate::AuditLogEntryAction::EmojiDelete { emoji, name } => {
|
||||
AuditLogEntryAction::EmojiDelete { emoji, name }
|
||||
}
|
||||
crate::AuditLogEntryAction::MessagePin {
|
||||
message,
|
||||
author,
|
||||
channel,
|
||||
} => AuditLogEntryAction::MessagePin {
|
||||
message,
|
||||
author,
|
||||
channel,
|
||||
},
|
||||
crate::AuditLogEntryAction::MessageUnpin {
|
||||
message,
|
||||
author,
|
||||
channel,
|
||||
} => AuditLogEntryAction::MessageUnpin {
|
||||
message,
|
||||
author,
|
||||
channel,
|
||||
},
|
||||
crate::AuditLogEntryAction::InviteCreate { invite, channel } => {
|
||||
AuditLogEntryAction::InviteCreate { invite, channel }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::AuditLogEntry> for AuditLogEntry {
|
||||
fn from(value: crate::AuditLogEntry) -> Self {
|
||||
AuditLogEntry {
|
||||
id: value.id,
|
||||
server: value.server,
|
||||
reason: value.reason,
|
||||
user: value.user,
|
||||
target: value.target,
|
||||
action: value.action.into(),
|
||||
impl From<crate::VoiceInformation> for VoiceInformation {
|
||||
fn from(value: crate::VoiceInformation) -> Self {
|
||||
VoiceInformation {
|
||||
max_users: value.max_users,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1675,8 +1527,97 @@ impl From<WebPushSubscription> for crate::WebPushSubscription {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialEmoji> for PartialEmoji {
|
||||
fn from(value: crate::PartialEmoji) -> Self {
|
||||
PartialEmoji { name: value.name }
|
||||
impl From<crate::AdminUser> for AdminUser {
|
||||
fn from(value: crate::AdminUser) -> Self {
|
||||
AdminUser {
|
||||
id: value.id,
|
||||
platform_user_id: value.platform_user_id,
|
||||
email: value.email,
|
||||
active: value.active,
|
||||
permissions: value.permissions,
|
||||
revolt_user: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdminUser> for crate::AdminUser {
|
||||
fn from(value: AdminUser) -> Self {
|
||||
crate::AdminUser {
|
||||
id: value.id,
|
||||
platform_user_id: value.platform_user_id,
|
||||
email: value.email,
|
||||
active: value.active,
|
||||
permissions: value.permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdminUserEdit> for crate::PartialAdminUser {
|
||||
fn from(value: AdminUserEdit) -> Self {
|
||||
crate::PartialAdminUser {
|
||||
id: None,
|
||||
platform_user_id: value.platform_user_id,
|
||||
email: value.email,
|
||||
active: value.active,
|
||||
permissions: value.permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdminToken> for crate::AdminToken {
|
||||
fn from(value: AdminToken) -> Self {
|
||||
crate::AdminToken {
|
||||
id: value.id,
|
||||
user_id: value.user_id,
|
||||
token: value.token,
|
||||
expiry: value.expiry.format_short().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::AdminToken> for AdminToken {
|
||||
fn from(value: crate::AdminToken) -> Self {
|
||||
AdminToken {
|
||||
id: value.id,
|
||||
user_id: value.user_id,
|
||||
token: value.token,
|
||||
expiry: Timestamp::parse(&value.expiry).unwrap(), // if it's invalid it shouldn't have made it to this point.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::AdminComment> for AdminComment {
|
||||
fn from(value: crate::AdminComment) -> Self {
|
||||
AdminComment {
|
||||
id: value.id,
|
||||
case_id: value.case_id,
|
||||
object_id: value.object_id,
|
||||
user_id: value.user_id,
|
||||
edited_at: value.edited_at.map(|f| Timestamp::parse(&f).unwrap()),
|
||||
content: value.content,
|
||||
system_message: value
|
||||
.system_message
|
||||
.map(|f| serde_json::from_str(&f).unwrap()),
|
||||
system_message_target: value.system_message_target,
|
||||
system_message_context: value.system_message_context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdminComment> for crate::AdminComment {
|
||||
fn from(value: AdminComment) -> Self {
|
||||
crate::AdminComment {
|
||||
id: value.id,
|
||||
object_id: value.object_id,
|
||||
case_id: value.case_id,
|
||||
user_id: value.user_id,
|
||||
edited_at: value.edited_at.map(|f| f.format_short().to_string()),
|
||||
content: value.content,
|
||||
system_message: value
|
||||
.system_message
|
||||
.map(|f| serde_json::to_string(&f).unwrap()), // i'll eat my hat if this fails
|
||||
system_message_target: value.system_message_target,
|
||||
system_message_context: value.system_message_context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod acker;
|
||||
pub mod basic;
|
||||
pub mod bridge;
|
||||
pub mod bulk_permissions;
|
||||
pub mod captcha;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::str::FromStr;
|
||||
|
||||
use revolt_result::Result;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use rocket::request::FromParam;
|
||||
use rocket::{form::FromFormField, request::FromParam};
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use schemars::{
|
||||
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
|
||||
@@ -10,7 +10,8 @@ use schemars::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
|
||||
AdminToken, Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User,
|
||||
Webhook,
|
||||
};
|
||||
|
||||
/// Reference to some object in the database
|
||||
@@ -25,6 +26,11 @@ impl<'a> Reference<'a> {
|
||||
Reference { id }
|
||||
}
|
||||
|
||||
/// Fetch Admin Token from Ref
|
||||
pub async fn as_admin_token(&self, db: &Database) -> Result<AdminToken> {
|
||||
db.admin_token_fetch(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch ban from Ref
|
||||
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
|
||||
db.fetch_ban(server, self.id).await
|
||||
@@ -126,3 +132,10 @@ impl<'a> JsonSchema for Reference<'a> {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
impl<'r> FromFormField<'r> for Reference<'r> {
|
||||
fn from_value(field: rocket::form::ValueField<'r>) -> rocket::form::Result<'r, Self> {
|
||||
Ok(Reference::from_unchecked(field.value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
399
crates/core/models/src/v0/admin.rs
Normal file
399
crates/core/models/src/v0/admin.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
use crate::v0::{self, Report, User};
|
||||
|
||||
auto_derived! {
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminAuditItemCreate {
|
||||
/// The ID of the mod who performed the action
|
||||
#[cfg_attr(feature="serde", serde(rename="mod"))]
|
||||
pub mod_id: String,
|
||||
/// The action performed (previously 'permission')
|
||||
pub action: String,
|
||||
/// The relevant case ID, if applicable
|
||||
#[cfg_attr(feature="serde", serde(rename="case"))]
|
||||
pub case_id: String,
|
||||
/// The id of the target (any object) the action was taken against, if applicable
|
||||
#[cfg_attr(feature="serde", serde(rename="target"))]
|
||||
pub target_id: Option<String>,
|
||||
/// The context attached to the action, if applicable (eg. search phrases)
|
||||
pub context: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AdminAuditItem {
|
||||
/// The audit item ID
|
||||
pub id: String,
|
||||
/// The ID of the mod who performed the action
|
||||
#[cfg_attr(feature="serde", serde(rename="mod"))]
|
||||
pub mod_id: String,
|
||||
/// The action performed (previously 'permission')
|
||||
pub action: AdminAuditItemActions,
|
||||
/// The relevant case ID, if applicable
|
||||
#[cfg_attr(feature="serde", serde(rename="case"))]
|
||||
pub case_id: String,
|
||||
/// The id of the target (any object) the action was taken against, if applicable
|
||||
#[cfg_attr(feature="serde", serde(rename="target"))]
|
||||
pub target_id: Option<String>,
|
||||
/// The context attached to the action, if applicable (eg. search phrases)
|
||||
pub context: Option<String>,
|
||||
/// The time the action occurred at.
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminCommentCreate {
|
||||
/// The ID of the case this comment is attached to, if applicable. This should be the 7 character short code.
|
||||
#[cfg_attr(feature="serde", serde(rename="case"))]
|
||||
pub case_id: Option<String>,
|
||||
/// The ID of the object this comment is attached to. Use full case ULID when creating a case comment.
|
||||
#[cfg_attr(feature="serde", serde(rename="object"))]
|
||||
pub object_id: String,
|
||||
/// The content
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
|
||||
pub content: String
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminCommentEdit {
|
||||
/// The new content
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
|
||||
pub content: String
|
||||
}
|
||||
|
||||
pub struct AdminComment {
|
||||
/// The comment ID
|
||||
pub id: String,
|
||||
/// The ID of the case this comment is attached to, if applicable. This should be the 7 character short code
|
||||
#[cfg_attr(feature="serde", serde(rename="case"))]
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))]
|
||||
pub case_id: Option<String>,
|
||||
/// The object this comment is attached to. If this is referencing a case this is the full case ID
|
||||
#[cfg_attr(feature="serde", serde(rename="object"))]
|
||||
pub object_id: String,
|
||||
/// The user who posted the comment/action
|
||||
#[cfg_attr(feature="serde", serde(rename="user"))]
|
||||
pub user_id: String,
|
||||
/// When the comment was edited, if applicable, in iso8601
|
||||
pub edited_at: Option<Timestamp>,
|
||||
/// The content, if this is not a system event
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))]
|
||||
pub content: Option<String>,
|
||||
/// The system event that happened, if this is not a comment (only applicable for cases)
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))]
|
||||
pub system_message: Option<AdminAuditItemActions>,
|
||||
/// The system message target
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))]
|
||||
pub system_message_target: Option<String>,
|
||||
/// The system message raw context
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))]
|
||||
pub system_message_context: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminCaseCreate {
|
||||
/// The owner of the case. Defaults to the creator
|
||||
#[cfg_attr(feature="serde", serde(rename="owner"))]
|
||||
pub owner: Option<String>,
|
||||
/// The title of the case. If not provided, a default will be generated from the report(s) assigned
|
||||
pub title: Option<String>,
|
||||
/// The report IDs to initially attach to this case (and generate a default title from)
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub initial_reports: Vec<String>
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AdminCaseEdit {
|
||||
/// The new owner of the case.
|
||||
#[cfg_attr(feature="serde", serde(rename="owner"))]
|
||||
pub owner_id: Option<String>,
|
||||
/// The new title of the case.
|
||||
pub title: Option<String>,
|
||||
/// Report IDs to add to the case.
|
||||
pub add_reports: Option<Vec<String>>,
|
||||
/// Report IDs to remove from the case.
|
||||
pub remove_reports: Option<Vec<String>>
|
||||
}
|
||||
|
||||
pub struct AdminCase {
|
||||
/// The case ID
|
||||
pub id: String,
|
||||
/// The case Short ID
|
||||
pub short_id: String,
|
||||
/// The owner of the case
|
||||
#[cfg_attr(feature="serde", serde(rename="owner"))]
|
||||
pub owner_id: String,
|
||||
/// The title of the case
|
||||
pub title: String,
|
||||
/// The status of the case (open/closed)
|
||||
pub status: String,
|
||||
/// When the case was closed
|
||||
pub closed_at: Option<Timestamp>,
|
||||
/// The tags for the case
|
||||
pub tags: Vec<String>,
|
||||
/// The reports assigned to this case
|
||||
pub reports: Vec<Report>
|
||||
}
|
||||
|
||||
pub struct AdminStrike {
|
||||
/// The strike ID
|
||||
pub id: String,
|
||||
/// The object receiving the strike (user/server)
|
||||
#[cfg_attr(feature="serde", serde(rename="target"))]
|
||||
pub target_id: String,
|
||||
/// The moderator who gave the strike
|
||||
#[cfg_attr(feature="serde", serde(rename="mod"))]
|
||||
pub mod_id: String,
|
||||
/// The case the strike was made under
|
||||
#[cfg_attr(feature="serde", serde(rename="case"))]
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))]
|
||||
pub case_id: Option<String>,
|
||||
/// Action associated with the strike (eg. suspension/ban)
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))]
|
||||
pub associated_action: Option<String>,
|
||||
/// Has the strike been removed
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="crate::if_false"))]
|
||||
pub overruled: bool,
|
||||
/// The user-facing reason for the strike
|
||||
pub reason: String,
|
||||
/// Internal context for the strike
|
||||
#[cfg_attr(feature="serde", serde(skip_serializing_if="Option::is_none"))]
|
||||
pub mod_context: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminStrikeCreate {
|
||||
/// The object receiving the strike (user/server)
|
||||
#[cfg_attr(feature="serde", serde(rename="target"))]
|
||||
pub target_id: String,
|
||||
/// The case the strike was made under
|
||||
#[cfg_attr(feature="serde", serde(rename="case"))]
|
||||
pub case_id: Option<String>,
|
||||
/// Action associated with the strike (eg. suspension/ban)
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 25)))]
|
||||
pub associated_action: Option<String>,
|
||||
/// The user-facing reason for the strike
|
||||
#[cfg_attr(feature = "validator", validate(length(max = 2000)))]
|
||||
pub reason: String,
|
||||
/// Internal context for the strike
|
||||
#[cfg_attr(feature = "validator", validate(length(max = 2000)))]
|
||||
pub mod_context: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminStrikeEdit {
|
||||
/// The case the strike was made under
|
||||
#[cfg_attr(feature="serde", serde(rename="case"))]
|
||||
pub case_id: Option<String>,
|
||||
/// Action associated with the strike (eg. suspension/ban)
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 25)))]
|
||||
pub associated_action: Option<String>,
|
||||
/// The user-facing reason for the strike
|
||||
#[cfg_attr(feature = "validator", validate(length(max = 2000)))]
|
||||
pub reason: Option<String>,
|
||||
/// Internal context for the strike
|
||||
#[cfg_attr(feature = "validator", validate(length(max = 2000)))]
|
||||
pub mod_context: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AdminToken {
|
||||
/// The token ID
|
||||
pub id: String,
|
||||
/// The user this token is attached to
|
||||
#[cfg_attr(feature = "serde", serde(rename="user"))]
|
||||
pub user_id: String,
|
||||
/// The token itself
|
||||
pub token: String,
|
||||
/// The expiry timestamp for this token, in iso6801
|
||||
pub expiry: Timestamp
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminTokenCreate {
|
||||
/// The expiry timestamp for this token, in iso6801. Max 30 days from current time.
|
||||
pub expiry: Timestamp,
|
||||
}
|
||||
|
||||
pub struct AdminUser {
|
||||
/// The ID of the user
|
||||
pub id: String,
|
||||
/// The user's revolt ID.
|
||||
pub platform_user_id: String,
|
||||
/// The user's email
|
||||
pub email: String,
|
||||
/// Whether the user is active or not (ie. can they use the api)
|
||||
pub active: bool,
|
||||
/// The permissions of the user
|
||||
pub permissions: u64,
|
||||
/// The Revolt user attached to this user. Use this for data like names and avatars.
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub revolt_user: Option<User>
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminUserCreate {
|
||||
/// The user's revolt ID.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 10, max = 20)))]
|
||||
pub platform_user_id: String,
|
||||
/// The user's email
|
||||
#[cfg_attr(feature = "validator", validate(email))]
|
||||
pub email: String,
|
||||
/// Whether the user is active or not (ie. can they use the api)
|
||||
pub active: bool,
|
||||
/// The permissions of the user
|
||||
pub permissions: u64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct AdminUserEdit {
|
||||
/// The user's revolt ID.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 10, max = 20)))]
|
||||
pub platform_user_id: Option<String>,
|
||||
/// The user's email
|
||||
#[cfg_attr(feature = "validator", validate(email))]
|
||||
pub email: Option<String>,
|
||||
/// Whether the user is active or not (ie. can they use the api)
|
||||
pub active: Option<bool>,
|
||||
/// The permissions of the user
|
||||
pub permissions: Option<u64>,
|
||||
}
|
||||
|
||||
// Flags
|
||||
|
||||
#[repr(u32)]
|
||||
pub enum AdminUserPermissionFlags {
|
||||
Comments = 0,
|
||||
|
||||
ManageAdminUsers = 1,
|
||||
CreateTokens = 2,
|
||||
|
||||
ViewUsers = 3,
|
||||
ManageUsers = 4,
|
||||
ManageUsersSenstiveInfo = 5,
|
||||
|
||||
ViewServers = 6,
|
||||
ManageServers = 7,
|
||||
ViewDMChannels = 8,
|
||||
ManageDMChannels = 9,
|
||||
|
||||
ViewCases = 10,
|
||||
ManageOwnCases = 11,
|
||||
ManageOtherCases = 12,
|
||||
|
||||
ViewReports = 13,
|
||||
|
||||
Search = 14,
|
||||
ManageNotes = 15,
|
||||
Discover = 16,
|
||||
|
||||
ManageAccounts = 17,
|
||||
ManageChannels = 18,
|
||||
}
|
||||
|
||||
pub enum AdminAuditItemActions {
|
||||
// Meta
|
||||
CreateAdminUser,
|
||||
EditAdminUser,
|
||||
CreateToken,
|
||||
RevokeToken,
|
||||
|
||||
// Comments
|
||||
CommentCreate,
|
||||
CommentEdit,
|
||||
CommentFetchForObject,
|
||||
|
||||
// Servers
|
||||
ServerFetch,
|
||||
ServerFetchParticipants,
|
||||
ServerFetchMembers,
|
||||
ServerAddMember,
|
||||
ServerChangeOwner,
|
||||
ServerCreateInvite,
|
||||
ServerDeleteInvite,
|
||||
ServerDeleteAllInvites,
|
||||
ServerDelete,
|
||||
ServerEdit,
|
||||
ServerRemoveMember,
|
||||
ServerSetFlags,
|
||||
ServerInstanceBanAllMembers,
|
||||
ServerBanMember,
|
||||
ServerUnbanMember,
|
||||
|
||||
// Accounts
|
||||
DeleteAccount,
|
||||
DisableAccount,
|
||||
EmailChange,
|
||||
EmailVerify,
|
||||
EnableAccount,
|
||||
|
||||
// Channels
|
||||
DeleteChannel,
|
||||
EditChannel,
|
||||
WipeChannel
|
||||
}
|
||||
|
||||
// Joiner payloads
|
||||
pub struct AdminServerResponse {
|
||||
pub server: v0::Server,
|
||||
pub owner: v0::User,
|
||||
pub comments: Vec<v0::AdminComment>,
|
||||
}
|
||||
|
||||
pub struct AdminServerParticipantsResponse {
|
||||
pub users: Vec<v0::User>,
|
||||
pub members: Vec<v0::Member>,
|
||||
pub sort_strategy: String
|
||||
}
|
||||
|
||||
pub struct AdminMemberWithUserAndOffsetResponse {
|
||||
pub after: Option<usize>,
|
||||
pub users: Vec<v0::MemberWithUserResponse>
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminAuditItemActions {
|
||||
/// Does this action create a case comment?
|
||||
pub fn makes_comment(&self) -> bool {
|
||||
match self {
|
||||
AdminAuditItemActions::CreateAdminUser => false,
|
||||
AdminAuditItemActions::EditAdminUser => false,
|
||||
AdminAuditItemActions::CreateToken => false,
|
||||
AdminAuditItemActions::RevokeToken => false,
|
||||
AdminAuditItemActions::CommentCreate => false,
|
||||
AdminAuditItemActions::CommentEdit => false,
|
||||
AdminAuditItemActions::CommentFetchForObject => false,
|
||||
AdminAuditItemActions::ServerFetch => false,
|
||||
AdminAuditItemActions::ServerFetchParticipants => false,
|
||||
AdminAuditItemActions::ServerAddMember => true,
|
||||
AdminAuditItemActions::ServerChangeOwner => true,
|
||||
AdminAuditItemActions::ServerCreateInvite => true,
|
||||
AdminAuditItemActions::ServerDeleteInvite => true,
|
||||
AdminAuditItemActions::ServerDeleteAllInvites => true,
|
||||
AdminAuditItemActions::ServerDelete => true,
|
||||
AdminAuditItemActions::ServerEdit => true,
|
||||
AdminAuditItemActions::ServerRemoveMember => true,
|
||||
AdminAuditItemActions::ServerSetFlags => true,
|
||||
AdminAuditItemActions::ServerInstanceBanAllMembers => true,
|
||||
AdminAuditItemActions::ServerBanMember => true,
|
||||
AdminAuditItemActions::ServerUnbanMember => true,
|
||||
AdminAuditItemActions::ServerFetchMembers => false,
|
||||
AdminAuditItemActions::DeleteAccount => true,
|
||||
AdminAuditItemActions::DisableAccount => true,
|
||||
AdminAuditItemActions::EmailChange => true,
|
||||
AdminAuditItemActions::EmailVerify => true,
|
||||
AdminAuditItemActions::EnableAccount => true,
|
||||
AdminAuditItemActions::DeleteChannel => true,
|
||||
AdminAuditItemActions::EditChannel => true,
|
||||
AdminAuditItemActions::WipeChannel => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AdminAuditItemActions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
use crate::v0::{Member, PartialChannel, PartialEmoji, PartialMember, PartialRole, PartialServer, User};
|
||||
use revolt_permissions::Override;
|
||||
|
||||
auto_derived!(
|
||||
/// Audit log entry
|
||||
pub struct AuditLogEntry {
|
||||
/// Unique ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// The server the entry happened in
|
||||
pub server: String,
|
||||
/// User provided reason
|
||||
pub reason: Option<String>,
|
||||
/// User who ran the action
|
||||
pub user: String,
|
||||
/// User this action is targetting
|
||||
pub target: Option<String>,
|
||||
/// The action ran
|
||||
pub action: AuditLogEntryAction,
|
||||
}
|
||||
|
||||
/// Indivual action stored on the audit log
|
||||
#[serde(tag = "type")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum AuditLogEntryAction {
|
||||
MessageDelete {
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
MessageBulkDelete {
|
||||
channel: String,
|
||||
count: usize,
|
||||
},
|
||||
MessagePin {
|
||||
message: String,
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
MessageUnpin {
|
||||
message: String,
|
||||
author: String,
|
||||
channel: String,
|
||||
},
|
||||
BanCreate {
|
||||
user: String,
|
||||
},
|
||||
BanDelete {
|
||||
user: String,
|
||||
},
|
||||
ChannelCreate {
|
||||
channel: String,
|
||||
name: String,
|
||||
},
|
||||
ChannelEdit {
|
||||
channel: String,
|
||||
before: PartialChannel,
|
||||
after: PartialChannel,
|
||||
},
|
||||
ChannelRolePermissionsEdit {
|
||||
channel: String,
|
||||
role: String,
|
||||
permissions: Override,
|
||||
},
|
||||
ChannelDelete {
|
||||
channel: String,
|
||||
name: String,
|
||||
},
|
||||
MemberEdit {
|
||||
user: String,
|
||||
before: PartialMember,
|
||||
after: PartialMember,
|
||||
},
|
||||
MemberKick {
|
||||
user: String,
|
||||
},
|
||||
ServerEdit {
|
||||
before: PartialServer,
|
||||
after: PartialServer,
|
||||
},
|
||||
RoleEdit {
|
||||
role: String,
|
||||
before: PartialRole,
|
||||
after: PartialRole,
|
||||
},
|
||||
RoleCreate {
|
||||
role: String,
|
||||
name: String,
|
||||
},
|
||||
RoleDelete {
|
||||
role: String,
|
||||
name: String,
|
||||
},
|
||||
RolesReorder {
|
||||
before: Vec<String>,
|
||||
after: Vec<String>,
|
||||
},
|
||||
InviteCreate {
|
||||
invite: String,
|
||||
channel: String,
|
||||
},
|
||||
InviteDelete {
|
||||
invite: String,
|
||||
channel: String,
|
||||
},
|
||||
WebhookCreate {
|
||||
webhook: String,
|
||||
name: String,
|
||||
channel: String,
|
||||
},
|
||||
WebhookDelete {
|
||||
webhook: String,
|
||||
name: String,
|
||||
channel: String,
|
||||
},
|
||||
EmojiCreate {
|
||||
emoji: String,
|
||||
name: String,
|
||||
},
|
||||
EmojiUpdate {
|
||||
emoji: String,
|
||||
before: PartialEmoji,
|
||||
after: PartialEmoji,
|
||||
},
|
||||
EmojiDelete {
|
||||
emoji: String,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Audit log query filters
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
#[cfg_attr(feature = "rocket", derive(rocket::FromForm))]
|
||||
pub struct OptionsAuditLogQuery {
|
||||
/// Filter by who ran the action
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub user: Option<String>,
|
||||
/// Filter by who the action is targetting
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub target: Option<String>,
|
||||
/// Filter by the action type
|
||||
pub r#type: Option<Vec<String>>,
|
||||
/// Entries before a certain entry id
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub before: Option<String>,
|
||||
/// Entries after a certain entry id
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub after: Option<String>,
|
||||
/// Maximum number of entries to fetch
|
||||
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
/// Response containing the audit log entries and the users involved
|
||||
pub struct AuditLogQueryResponse {
|
||||
/// List of audit logs
|
||||
pub audit_logs: Vec<AuditLogEntry>,
|
||||
/// List of users
|
||||
pub users: Vec<User>,
|
||||
/// List of members
|
||||
pub members: Vec<Member>,
|
||||
}
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
mod audit_logs;
|
||||
mod admin;
|
||||
mod bots;
|
||||
mod channel_invites;
|
||||
mod channel_unreads;
|
||||
@@ -19,7 +19,7 @@ mod accounts;
|
||||
mod mfa_tickets;
|
||||
mod sessions;
|
||||
|
||||
pub use audit_logs::*;
|
||||
pub use admin::*;
|
||||
pub use bots::*;
|
||||
pub use channel_invites::*;
|
||||
pub use channel_unreads::*;
|
||||
|
||||
@@ -8,6 +8,8 @@ auto_derived!(
|
||||
pub id: String,
|
||||
/// Id of the user creating this report
|
||||
pub author_id: String,
|
||||
/// The case this report is attached to
|
||||
pub case_id: Option<String>,
|
||||
/// Reported content
|
||||
pub content: ReportedContent,
|
||||
/// Additional report context
|
||||
|
||||
@@ -159,4 +159,9 @@ auto_derived!(
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Vec<FieldsMember>,
|
||||
}
|
||||
|
||||
pub struct MemberWithUserResponse {
|
||||
pub user: crate::v0::User,
|
||||
pub member: Member,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -205,6 +205,8 @@ auto_derived!(
|
||||
Banned = 4,
|
||||
/// User was marked as spam and removed from platform
|
||||
Spam = 8,
|
||||
/// User is a global moderator
|
||||
GlobalModerator = 16,
|
||||
}
|
||||
|
||||
/// New user profile data
|
||||
|
||||
@@ -100,11 +100,8 @@ pub enum ChannelPermission {
|
||||
/// Mention roles
|
||||
MentionRoles = 1 << 38,
|
||||
|
||||
/// Access server audit logs
|
||||
ViewAuditLogs = 1 << 40,
|
||||
|
||||
// * Misc. permissions
|
||||
// % Bits 41 to 52: free area
|
||||
// % Bits 39 to 52: free area
|
||||
// % Bits 53 to 64: do not use
|
||||
|
||||
// * Grant all permissions
|
||||
|
||||
@@ -39,9 +39,11 @@ impl IntoResponse for Error {
|
||||
ErrorType::GroupTooLarge { .. } => StatusCode::FORBIDDEN,
|
||||
ErrorType::AlreadyInGroup => StatusCode::CONFLICT,
|
||||
ErrorType::NotInGroup => StatusCode::NOT_FOUND,
|
||||
ErrorType::NotAMember => StatusCode::BAD_REQUEST,
|
||||
ErrorType::AlreadyPinned => StatusCode::BAD_REQUEST,
|
||||
ErrorType::NotPinned => StatusCode::BAD_REQUEST,
|
||||
ErrorType::InSlowmode { retry_after: _ } => StatusCode::TOO_MANY_REQUESTS,
|
||||
ErrorType::InviteExists => StatusCode::BAD_REQUEST,
|
||||
|
||||
ErrorType::CantCreateServers => StatusCode::FORBIDDEN,
|
||||
ErrorType::UnknownServer => StatusCode::NOT_FOUND,
|
||||
@@ -90,7 +92,6 @@ impl IntoResponse for Error {
|
||||
ErrorType::UnknownNode => StatusCode::BAD_REQUEST,
|
||||
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
|
||||
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
|
||||
ErrorType::HeaderTooLarge => StatusCode::BAD_REQUEST,
|
||||
|
||||
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
|
||||
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
@@ -121,6 +122,10 @@ impl IntoResponse for Error {
|
||||
ErrorType::DisallowedMFAMethod => StatusCode::BAD_REQUEST,
|
||||
ErrorType::OperationFailed => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ErrorType::IncorrectData { .. } => StatusCode::BAD_REQUEST,
|
||||
|
||||
ErrorType::ImATeaPot => StatusCode::IM_A_TEAPOT,
|
||||
|
||||
ErrorType::PrivilegedAccount => StatusCode::FORBIDDEN,
|
||||
};
|
||||
|
||||
(status, Json(&self)).into_response()
|
||||
|
||||
@@ -106,12 +106,14 @@ pub enum ErrorType {
|
||||
InSlowmode {
|
||||
retry_after: u64,
|
||||
},
|
||||
InviteExists,
|
||||
|
||||
// ? Server related errors
|
||||
CantCreateServers,
|
||||
UnknownServer,
|
||||
InvalidRole,
|
||||
Banned,
|
||||
NotAMember,
|
||||
TooManyServers {
|
||||
max: usize,
|
||||
},
|
||||
@@ -164,7 +166,6 @@ pub enum ErrorType {
|
||||
FailedValidation {
|
||||
error: String,
|
||||
},
|
||||
HeaderTooLarge,
|
||||
OperationFailed,
|
||||
IncorrectData {
|
||||
with: String,
|
||||
@@ -212,6 +213,12 @@ pub enum ErrorType {
|
||||
|
||||
TotpAlreadyEnabled,
|
||||
DisallowedMFAMethod,
|
||||
|
||||
// ? Admin API relate errors
|
||||
PrivilegedAccount,
|
||||
|
||||
// :)
|
||||
ImATeaPot,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
||||
@@ -41,10 +41,12 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::GroupTooLarge { .. } => Status::Forbidden,
|
||||
ErrorType::AlreadyInGroup => Status::Conflict,
|
||||
ErrorType::NotInGroup => Status::NotFound,
|
||||
ErrorType::NotAMember => Status::BadRequest,
|
||||
ErrorType::AlreadyPinned => Status::BadRequest,
|
||||
ErrorType::NotPinned => Status::BadRequest,
|
||||
ErrorType::InSlowmode { retry_after: _ } => Status::TooManyRequests,
|
||||
ErrorType::InvalidFlagValue => Status::BadRequest,
|
||||
ErrorType::InviteExists => Status::BadRequest,
|
||||
|
||||
ErrorType::CantCreateServers => Status::Forbidden,
|
||||
ErrorType::UnknownServer => Status::NotFound,
|
||||
@@ -91,7 +93,6 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::NotConnected => Status::BadRequest,
|
||||
ErrorType::UnknownNode => Status::BadRequest,
|
||||
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
|
||||
ErrorType::HeaderTooLarge => Status::BadRequest,
|
||||
|
||||
ErrorType::ProxyError => Status::BadRequest,
|
||||
ErrorType::FileTooSmall => Status::UnprocessableEntity,
|
||||
@@ -126,6 +127,10 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::DisallowedMFAMethod => Status::BadRequest,
|
||||
ErrorType::OperationFailed => Status::InternalServerError,
|
||||
ErrorType::IncorrectData { .. } => Status::BadRequest,
|
||||
|
||||
ErrorType::ImATeaPot => Status::ImATeapot,
|
||||
|
||||
ErrorType::PrivilegedAccount => Status::Forbidden,
|
||||
};
|
||||
|
||||
// Serialize the error data structure into JSON.
|
||||
|
||||
56
crates/delta/src/routes/admin/accounts/account_delete.rs
Normal file
56
crates/delta/src/routes/admin/accounts/account_delete.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::routes::admin::util::{
|
||||
create_audit_action, flatten_authorized_user, user_has_permission,
|
||||
};
|
||||
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// Delete an account. Requires ManageAccounts permissions
|
||||
#[openapi(tag = "Admin")]
|
||||
#[delete("/accounts/<id>?<case>")]
|
||||
pub async fn admin_account_delete(
|
||||
db: &State<Database>,
|
||||
auth: AdminAuthorization,
|
||||
id: Reference<'_>,
|
||||
case: Option<&str>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let user = flatten_authorized_user(&auth);
|
||||
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: "ManageAccounts".to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
let target = id.as_user(db).await?;
|
||||
|
||||
if target.privileged {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
|
||||
let admin = db.admin_user_fetch(&target.id).await.ok();
|
||||
|
||||
if let Some(admin) = admin {
|
||||
if user_has_permission(&admin, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
}
|
||||
|
||||
db.fetch_account(&target.id)
|
||||
.await?
|
||||
.mark_deleted(db)
|
||||
.await?;
|
||||
|
||||
create_audit_action(
|
||||
db,
|
||||
&user.id,
|
||||
v0::AdminAuditItemActions::DeleteAccount,
|
||||
case,
|
||||
Some(id.id),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
57
crates/delta/src/routes/admin/accounts/account_disable.rs
Normal file
57
crates/delta/src/routes/admin/accounts/account_disable.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use crate::routes::admin::util::{
|
||||
create_audit_action, flatten_authorized_user, user_has_permission,
|
||||
};
|
||||
use revolt_database::util::reference::Reference;
|
||||
use revolt_database::{AdminAuthorization, Database};
|
||||
use revolt_models::v0::{self};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// Disable an account. Requires ManageAccounts permissions
|
||||
#[openapi(tag = "Admin")]
|
||||
#[post("/accounts/disable/<id>?<case>")]
|
||||
pub async fn admin_account_disable(
|
||||
db: &State<Database>,
|
||||
auth: AdminAuthorization,
|
||||
id: Reference<'_>,
|
||||
case: Option<&str>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let user = flatten_authorized_user(&auth);
|
||||
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: "ManageAccounts".to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
let target = id.as_user(db).await?;
|
||||
|
||||
if target.privileged {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
|
||||
let admin = db.admin_user_fetch(&target.id).await.ok();
|
||||
|
||||
if let Some(admin) = admin {
|
||||
if user_has_permission(&admin, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
}
|
||||
|
||||
db.fetch_account(&target.id).await?
|
||||
.disable(db)
|
||||
.await?;
|
||||
|
||||
create_audit_action(
|
||||
db,
|
||||
&user.id,
|
||||
v0::AdminAuditItemActions::DisableAccount,
|
||||
case,
|
||||
Some(id.id),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use crate::routes::admin::util::{
|
||||
create_audit_action, flatten_authorized_user, user_has_permission,
|
||||
};
|
||||
use revolt_database::util::reference::Reference;
|
||||
use revolt_database::{AdminAuthorization, Database};
|
||||
use revolt_models::v0::{self};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
use revolt_database::util::email::{normalise_email, validate_email};
|
||||
|
||||
/// Change the email of an account. Requires ManageAccounts permissions
|
||||
#[openapi(tag = "Admin")]
|
||||
#[patch("/accounts/email/<target>?<case>", data = "<data>")]
|
||||
pub async fn admin_account_email_change(
|
||||
db: &State<Database>,
|
||||
auth: AdminAuthorization,
|
||||
target: Reference<'_>,
|
||||
case: Option<&str>,
|
||||
data: Json<v0::DataChangeEmail>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let data = data.into_inner();
|
||||
|
||||
validate_email(&data.email)?;
|
||||
|
||||
let user = flatten_authorized_user(&auth);
|
||||
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: "ManageAccounts".to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
let target = target.as_user(db).await?;
|
||||
|
||||
if target.privileged {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
|
||||
let admin = db.admin_user_fetch(&target.id).await.ok();
|
||||
|
||||
if let Some(admin) = admin {
|
||||
if user_has_permission(&admin, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
}
|
||||
|
||||
let mut account = db.fetch_account(&target.id).await?;
|
||||
|
||||
account.email_normalised = normalise_email(data.email.clone());
|
||||
account.email = data.email;
|
||||
account.save(db).await?;
|
||||
|
||||
|
||||
create_audit_action(
|
||||
db,
|
||||
&user.id,
|
||||
v0::AdminAuditItemActions::EmailChange,
|
||||
case,
|
||||
Some(&target.id),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use crate::routes::admin::util::{
|
||||
create_audit_action, flatten_authorized_user, user_has_permission,
|
||||
};
|
||||
use revolt_database::{util::reference::Reference, AdminAuthorization, Database, EmailVerification, MFATicket};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
use revolt_database::util::email::normalise_email;
|
||||
|
||||
/// Verify the email of an account. Requires ManageAccounts permissions
|
||||
#[openapi(tag = "Admin")]
|
||||
#[put("/accounts/email/<target>?<case>")]
|
||||
pub async fn admin_account_email_verify(
|
||||
db: &State<Database>,
|
||||
auth: AdminAuthorization,
|
||||
target: Reference<'_>,
|
||||
case: Option<&str>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let user = flatten_authorized_user(&auth);
|
||||
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: "ManageAccounts".to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
let target = target.as_user(db).await?;
|
||||
|
||||
if target.privileged {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
|
||||
let admin = db.admin_user_fetch(&target.id).await.ok();
|
||||
|
||||
if let Some(admin) = admin {
|
||||
if user_has_permission(&admin, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
}
|
||||
|
||||
let mut account = db.fetch_account(&target.id).await?;
|
||||
|
||||
// Update account email
|
||||
if let EmailVerification::Moving { new_email, .. } = &account.verification {
|
||||
account.email = new_email.clone();
|
||||
account.email_normalised = normalise_email(new_email.clone());
|
||||
} else {
|
||||
let mut ticket = MFATicket::new(account.id.to_string(), false);
|
||||
ticket.authorised = true;
|
||||
ticket.save(db).await?;
|
||||
};
|
||||
|
||||
// Mark as verified
|
||||
account.verification = EmailVerification::Verified;
|
||||
|
||||
// Save to database
|
||||
account.save(db).await?;
|
||||
|
||||
create_audit_action(
|
||||
db,
|
||||
&user.id,
|
||||
v0::AdminAuditItemActions::DeleteAccount,
|
||||
case,
|
||||
Some(&target.id),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
57
crates/delta/src/routes/admin/accounts/account_enable.rs
Normal file
57
crates/delta/src/routes/admin/accounts/account_enable.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use crate::routes::admin::util::{
|
||||
create_audit_action, flatten_authorized_user, user_has_permission,
|
||||
};
|
||||
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// Enable an account. Requires ManageAccounts permissions
|
||||
#[openapi(tag = "Admin")]
|
||||
#[post("/accounts/enable/<target>?<case>")]
|
||||
pub async fn admin_account_enable(
|
||||
db: &State<Database>,
|
||||
auth: AdminAuthorization,
|
||||
target: Reference<'_>,
|
||||
case: Option<&str>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let user = flatten_authorized_user(&auth);
|
||||
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: "ManageAccounts".to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
let target = target.as_user(db).await?;
|
||||
|
||||
if target.privileged {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
|
||||
let admin = db.admin_user_fetch(&target.id).await.ok();
|
||||
|
||||
if let Some(admin) = admin {
|
||||
if user_has_permission(&admin, v0::AdminUserPermissionFlags::ManageAccounts) {
|
||||
return Err(create_error!(PrivilegedAccount));
|
||||
}
|
||||
}
|
||||
|
||||
let mut account = db.fetch_account(&target.id).await?;
|
||||
|
||||
account.disabled = false;
|
||||
|
||||
account.save(db).await?;
|
||||
|
||||
create_audit_action(
|
||||
db,
|
||||
&user.id,
|
||||
v0::AdminAuditItemActions::EnableAccount,
|
||||
case,
|
||||
Some(&target.id),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// This should allow resetting of all factors or individual ones.
|
||||
7
crates/delta/src/routes/admin/accounts/mod.rs
Normal file
7
crates/delta/src/routes/admin/accounts/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod account_delete;
|
||||
pub mod account_disable;
|
||||
pub mod account_email_change;
|
||||
pub mod account_email_verify;
|
||||
pub mod account_enable;
|
||||
pub mod account_mfa_reset;
|
||||
pub mod account_reset_lockout;
|
||||
@@ -0,0 +1,2 @@
|
||||
// this one is sort of just an idea. hold off on it for now.
|
||||
// Allow the mod to add an action to a case (same as the audit log actions), in case it got missed by the automated system.
|
||||
@@ -0,0 +1,4 @@
|
||||
// add a collaborator (this concept does not replace case ownership,
|
||||
// but will allow it to show up in both the owner and any collaborators personalized open case list)
|
||||
// users without sensitive permissions may only be added as collaborators to sensitive cases by administrators/moderation leads
|
||||
// users who do not have administrator/moderation lead perms may only add a collaborator to a case they own or are also a collaborator on.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// assign reports or other cases to the specified case by ULID
|
||||
// this should take an array of {type: report | case, id: ULID}
|
||||
// recursion of cases should be allowed
|
||||
// reports are allowed to be assigned to multiple cases
|
||||
// if a report comes from a queue that is marked as sensitive, this case should also be marked sensitive.
|
||||
@@ -0,0 +1,3 @@
|
||||
// Assign a new owner to a case.
|
||||
// Only administrators/moderation leads should be allowed to assign a case that does not belong to them.
|
||||
// only administrators/moderation leads may assign someone a sensitive case if the assignee does not have sensitive permissions.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user