Compare commits

..
Author SHA1 Message Date
stoat-release[bot] ae41fd6e6a chore(deps): update rust crate anyhow to v1.0.103 2026-07-07 11:32:26 +00:00
Zomatree 0b53db9921 fix: voice system messages and call notifs by fetching participant list (#846)
Signed-off-by: Zomatree <me@zomatree.live>
2026-07-06 22:37:27 -07:00
Zomatree 21daf3aec6 fix: allow removing channel slowmode (#836)
Signed-off-by: Zomatree <me@zomatree.live>
2026-07-02 19:23:52 -07:00
Tom 59f6e012f8 feat: replace tenor with gifbox (#844)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-07-02 19:20:18 -07:00
183 changed files with 210 additions and 4681 deletions
Generated
+2 -3
View File
@@ -185,9 +185,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "arbitrary"
@@ -7330,7 +7330,6 @@ dependencies = [
"serde_json",
"sha1 0.10.6",
"tokio",
"tokio-stream",
"totp-lite",
"ulid",
"unicode-segmentation",
-1
View File
@@ -28,7 +28,6 @@ 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"
-6
View File
@@ -70,9 +70,6 @@ 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
@@ -222,14 +219,11 @@ 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]
-3
View File
@@ -228,7 +228,6 @@ pub struct ApiSecurity {
pub trust_cloudflare: bool,
pub easypwned: String,
pub tenor_key: String,
pub admin_keys: Vec<String>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -440,8 +439,6 @@ 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,
-1
View File
@@ -82,7 +82,6 @@ async-recursion = { workspace = true }
# Async
tokio = { workspace = true, optional = true }
tokio-stream = { workspace = true }
# Axum Impl
axum = { workspace = true, optional = true }
+1 -1
View File
@@ -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::*;
+4 -14
View File
@@ -1,27 +1,17 @@
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
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
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 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>>>,
-10
View File
@@ -281,16 +281,6 @@ 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,
@@ -1,5 +0,0 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;
@@ -1,45 +0,0 @@
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(),
}
}
}
@@ -1,17 +0,0 @@
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>>;
}
@@ -1,48 +0,0 @@
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)
}
}
}
@@ -1,45 +0,0 @@
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())
}
}
}
@@ -1,5 +0,0 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;
@@ -1,53 +0,0 @@
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
}
}
@@ -1,30 +0,0 @@
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>>;
}
@@ -1,85 +0,0 @@
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)
}
}
@@ -1,112 +0,0 @@
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())
}
}
@@ -1,5 +0,0 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;
@@ -1,95 +0,0 @@
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(),
);
}
}
@@ -1,28 +0,0 @@
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>>;
}
@@ -1,47 +0,0 @@
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})
}
}
@@ -1,73 +0,0 @@
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,20 +28,6 @@ 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,8 +8,6 @@ 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.");
@@ -277,11 +275,6 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create ratelimit_events 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",
"indexes": [
@@ -368,128 +361,3 @@ 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.");
}
@@ -1,5 +0,0 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;
@@ -1,47 +0,0 @@
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),
}
}
}
@@ -1,17 +0,0 @@
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>>;
}
@@ -1,41 +0,0 @@
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})
}
}
@@ -1,51 +0,0 @@
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())
}
}
@@ -1,12 +0,0 @@
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::*;
@@ -1,71 +0,0 @@
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 })
}
}
@@ -1,16 +0,0 @@
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>;
}
@@ -1,34 +0,0 @@
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))
}
}
@@ -1,53 +0,0 @@
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))?)
}
}
@@ -1,146 +0,0 @@
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)))
}
}
}
}
@@ -1,59 +0,0 @@
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,
))
}
}
@@ -1,9 +0,0 @@
mod models;
mod ops;
#[cfg(feature = "rocket-impl")]
mod rocket;
#[cfg(feature = "rocket-impl")]
pub use self::rocket::*;
pub use models::*;
pub use ops::*;
@@ -1,69 +0,0 @@
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
}
}
@@ -1,20 +0,0 @@
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>;
}
@@ -1,35 +0,0 @@
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)
}
}
@@ -1,56 +0,0 @@
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)
}
}
@@ -1,43 +0,0 @@
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)))
}
}
}
@@ -12,14 +12,7 @@ static COL: &str = "channel_invites";
impl AbstractChannelInvites for MongoDb {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
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(())
query!(self, insert_one, COL, &invite).map(|_| ())
}
/// Fetch an invite by the code
@@ -161,6 +161,7 @@ auto_derived!(
Icon,
DefaultPermissions,
Voice,
Slowmode,
}
);
@@ -554,6 +555,12 @@ impl Channel {
}
_ => {}
},
FieldsChannel::Slowmode => match self {
Self::TextChannel { slowmode, .. } => {
slowmode.take();
}
_ => {}
}
}
}
@@ -767,13 +774,6 @@ 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")]
@@ -784,6 +784,7 @@ impl IntoDocumentPath for FieldsChannel {
FieldsChannel::Icon => "icon",
FieldsChannel::DefaultPermissions => "default_permissions",
FieldsChannel::Voice => "voice",
FieldsChannel::Slowmode => "slowmode",
})
}
}
@@ -54,6 +54,5 @@ 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<()>; // Wipe a channel's messages
async fn wipe_channel(&self, channel: &Channel) -> Result<()>;
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
}
@@ -304,23 +304,6 @@ 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,10 +182,4 @@ 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(())
}
}
-18
View File
@@ -1,10 +1,4 @@
mod admin_audits;
mod admin_cases;
mod admin_comments;
mod admin_migrations;
mod admin_strikes;
mod admin_tokens;
mod admin_users;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -28,13 +22,7 @@ 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 admin_strikes::*;
pub use admin_tokens::*;
pub use admin_users::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
@@ -66,12 +54,6 @@ 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
+ bots::AbstractBots
+ channels::AbstractChannels
@@ -18,7 +18,5 @@ 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,7 +110,6 @@ 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));
@@ -189,20 +188,18 @@ impl Member {
.private(user.id.clone())
.await;
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();
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();
}
Ok((member, channels))
@@ -352,10 +349,8 @@ mod tests {
.unwrap()
.0;
Member::create(&db, &server, &owner, None, false)
.await
.unwrap();
let mut kickable_member = Member::create(&db, &server, &kickable_user, None, false)
Member::create(&db, &server, &owner, None).await.unwrap();
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
@@ -379,7 +374,7 @@ mod tests {
.await
.unwrap();
let kickable_member = Member::create(&db, &server, &kickable_user, None, false)
let kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
@@ -132,18 +132,4 @@ 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,12 +1,10 @@
use bson::Document;
use std::collections::HashMap;
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use mongodb::options::ReadConcern;
use revolt_result::Result;
use crate::{AbstractUsers, FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{IntoDocumentPath, MongoDb};
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
@@ -339,88 +337,6 @@ 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,6 +1,3 @@
use std::collections::HashSet;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::ReferenceDb;
@@ -212,91 +209,4 @@ 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())
}
}
@@ -886,30 +886,6 @@ 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
View File
@@ -1,3 +0,0 @@
pub fn transform_optional_string(s: Option<&str>) -> Option<String> {
s.map(|f| f.to_string())
}
+2 -96
View File
@@ -319,6 +319,7 @@ impl From<FieldsChannel> for crate::FieldsChannel {
FieldsChannel::Icon => crate::FieldsChannel::Icon,
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
FieldsChannel::Voice => crate::FieldsChannel::Voice,
FieldsChannel::Slowmode => crate::FieldsChannel::Slowmode,
}
}
}
@@ -330,6 +331,7 @@ impl From<crate::FieldsChannel> for FieldsChannel {
crate::FieldsChannel::Icon => FieldsChannel::Icon,
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
crate::FieldsChannel::Voice => FieldsChannel::Voice,
crate::FieldsChannel::Slowmode => FieldsChannel::Slowmode,
}
}
}
@@ -612,7 +614,6 @@ impl From<crate::Report> for Report {
additional_context: value.additional_context,
status: value.status,
notes: value.notes,
case_id: value.case_id,
}
}
}
@@ -1526,98 +1527,3 @@ impl From<WebPushSubscription> for crate::WebPushSubscription {
}
}
}
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
View File
@@ -1,5 +1,4 @@
pub mod acker;
pub mod basic;
pub mod bridge;
pub mod bulk_permissions;
pub mod captcha;
+2 -15
View File
@@ -2,7 +2,7 @@ use std::str::FromStr;
use revolt_result::Result;
#[cfg(feature = "rocket-impl")]
use rocket::{form::FromFormField, request::FromParam};
use rocket::request::FromParam;
#[cfg(feature = "rocket-impl")]
use schemars::{
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
@@ -10,8 +10,7 @@ use schemars::{
};
use crate::{
AdminToken, Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User,
Webhook,
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
};
/// Reference to some object in the database
@@ -26,11 +25,6 @@ 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
@@ -132,10 +126,3 @@ 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()))
}
}
@@ -159,4 +159,17 @@ impl VoiceClient {
.await
.to_internal_error()
}
pub async fn get_room_participants(
&self,
node: &str,
channel_id: &str,
) -> Result<Vec<ParticipantInfo>> {
let room = self.get_node(node)?;
room.client
.list_participants(channel_id)
.await
.to_internal_error()
}
}
-399
View File
@@ -1,399 +0,0 @@
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
View File
@@ -164,6 +164,7 @@ auto_derived!(
Icon,
DefaultPermissions,
Voice,
Slowmode,
}
/// New webhook information
-2
View File
@@ -1,4 +1,3 @@
mod admin;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -19,7 +18,6 @@ mod accounts;
mod mfa_tickets;
mod sessions;
pub use admin::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
@@ -8,8 +8,6 @@ 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,9 +159,4 @@ auto_derived!(
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsMember>,
}
pub struct MemberWithUserResponse {
pub user: crate::v0::User,
pub member: Member,
}
);
-2
View File
@@ -205,8 +205,6 @@ 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
-6
View File
@@ -39,11 +39,9 @@ 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,
@@ -122,10 +120,6 @@ 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()
-8
View File
@@ -106,14 +106,12 @@ pub enum ErrorType {
InSlowmode {
retry_after: u64,
},
InviteExists,
// ? Server related errors
CantCreateServers,
UnknownServer,
InvalidRole,
Banned,
NotAMember,
TooManyServers {
max: usize,
},
@@ -213,12 +211,6 @@ pub enum ErrorType {
TotpAlreadyEnabled,
DisallowedMFAMethod,
// ? Admin API relate errors
PrivilegedAccount,
// :)
ImATeaPot,
}
#[macro_export]
-6
View File
@@ -41,12 +41,10 @@ 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,
@@ -127,10 +125,6 @@ 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.
+91 -92
View File
@@ -1,19 +1,15 @@
use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver};
use livekit_protocol::TrackType;
use revolt_database::{
events::client::EventV1,
iso8601_timestamp::{Duration, Timestamp},
util::reference::Reference,
voice::{
create_voice_state, delete_channel_voice_state, delete_voice_state,
get_user_moved_from_voice, get_user_moved_to_voice, update_voice_state_tracks,
RoomMetadata, UserVoiceChannel, VoiceClient,
},
Database, AMQP,
AMQP, Database, PartialMessage, SystemMessage, events::client::EventV1, iso8601_timestamp::{Duration, Timestamp}, util::reference::Reference, voice::{
RoomMetadata, UserVoiceChannel, VoiceClient, create_voice_state, delete_channel_voice_state, delete_voice_state, get_call_notification_recipients, get_user_moved_from_voice, get_user_moved_to_voice, get_voice_channel_members, set_channel_call_started_system_message, take_channel_call_started_system_message, update_voice_state_tracks
}
};
use revolt_models::v0;
use revolt_result::{Result, ToRevoltError};
use rocket::{post, State};
use rocket_empty::EmptyResponse;
use ulid::Ulid;
use crate::guard::AuthHeader;
@@ -21,12 +17,12 @@ use crate::guard::AuthHeader;
pub async fn ingress(
db: &State<Database>,
voice_client: &State<VoiceClient>,
_amqp: &State<AMQP>,
amqp: &State<AMQP>,
node: &str,
auth_header: AuthHeader<'_>,
body: &str,
) -> Result<EmptyResponse> {
log::debug!("received event: {body:?}");
log::debug!("received event: {body}");
let config = revolt_config::config().await;
@@ -63,16 +59,18 @@ pub async fn ingress(
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let channel = UserVoiceChannel {
let voice_channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
let joined_at = Timestamp::UNIX_EPOCH
.checked_add(Duration::seconds(event.created_at))
.unwrap();
let voice_state = create_voice_state(&channel, user_id, joined_at).await?;
let voice_state = create_voice_state(&voice_channel, user_id, joined_at).await?;
// Only publish one event when a user is moved from one channel to another.
if let Some(moved_from) = get_user_moved_to_voice(channel_id, user_id).await? {
@@ -93,63 +91,66 @@ pub async fn ingress(
.await;
};
// TODO: fix `num_participants` being incorrect sometimes see (#457)
// First user who joined - send call started system message.
// if event.room.as_ref().unwrap().num_participants == 1 {
// let user = Reference::from_unchecked(user_id).as_user(db).await?;
let participants = voice_client.get_room_participants(node, channel_id).await?;
// let message_id =
// Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap())
// .to_string();
if participants.len() == 1 {
let user = Reference::from_unchecked(user_id).as_user(db).await?;
let message_id = Ulid::from_datetime(
Timestamp::UNIX_EPOCH
.checked_add(Duration::seconds(event.created_at))
.unwrap()
.into(),
)
.to_string();
// let mut call_started_message = SystemMessage::CallStarted {
// by: user_id.to_string(),
// finished_at: None,
// }
// .into_message(channel.id().to_string());
let mut call_started_message = SystemMessage::CallStarted {
by: user_id.to_string(),
finished_at: None,
}
.into_message(channel_id.clone());
// call_started_message.id = message_id;
call_started_message.id = message_id;
// set_channel_call_started_system_message(channel.id(), &call_started_message.id)
// .await?;
set_channel_call_started_system_message(channel_id, &call_started_message.id)
.await?;
// call_started_message
// .send(
// db,
// Some(amqp),
// v0::MessageAuthor::System {
// username: &user.username,
// avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
// },
// None,
// None,
// &channel,
// false,
// )
// .await?;
call_started_message
.send(
db,
Some(amqp),
v0::MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
&channel,
false,
)
.await?;
// let recipients = get_call_notification_recipients(&channel_id, &user_id).await?;
// let now = joined_at.format_short().to_string();
let recipients = get_call_notification_recipients(channel_id, user_id).await?;
let now = joined_at.format_short().to_string();
// if let Err(e) = amqp
// .dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients)
// .await
// {
// revolt_config::capture_error(&e);
// }
// }
if let Err(e) = amqp
.dm_call_updated(&user.id, channel_id, Some(&now), false, recipients)
.await
{
revolt_config::capture_error(&e);
}
}
}
// User left a channel
"participant_left" => {
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let channel = UserVoiceChannel {
let voice_channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
delete_voice_state(&channel, user_id).await?;
delete_voice_state(&voice_channel, user_id).await?;
// Dont send leave event when a user is moved
if get_user_moved_from_voice(channel_id, user_id)
@@ -164,49 +165,47 @@ pub async fn ingress(
.await;
};
// See above for why this is commented out
// // Update CallStarted system message if everyone has left with the end time
// let members = get_voice_channel_members(channel_id).await?;
let members = get_voice_channel_members(&voice_channel).await?;
// if members.is_none_or(|m| m.is_empty()) {
// // The channel is empty so send out an "end" message for ringing
// if let Err(e) = amqp
// .dm_call_updated(user_id, channel_id, None, true, None)
// .await
// {
// revolt_config::capture_internal_error!(&e);
// }
if members.is_none_or(|m| m.is_empty()) {
// The channel is empty so send out an "end" message for ringing
if let Err(e) = amqp
.dm_call_updated(user_id, channel_id, None, true, None)
.await
{
revolt_config::capture_internal_error!(&e);
}
// if let Some(system_message_id) =
// take_channel_call_started_system_message(channel_id).await?
// {
// // Could have been deleted
// if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
// .as_message(db)
// .await
// {
// if let Some(SystemMessage::CallStarted { finished_at, .. }) =
// &mut message.system
// {
// *finished_at = Some(Timestamp::now_utc());
if let Some(system_message_id) =
take_channel_call_started_system_message(channel_id).await?
{
// Could have been deleted
if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
.as_message(db)
.await
{
if let Some(SystemMessage::CallStarted { finished_at, .. }) =
&mut message.system
{
*finished_at = Some(Timestamp::now_utc());
// message
// .update(
// db,
// PartialMessage {
// system: message.system.clone(),
// ..Default::default()
// },
// Vec::new(),
// )
// .await?;
// } else {
// log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
// }
// };
// };
// }
message
.update(
db,
PartialMessage {
system: message.system.clone(),
..Default::default()
},
Vec::new(),
)
.await?;
} else {
log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
}
};
};
}
}
// Audio/video track was started/stopped/unmuted/muted
"track_published" | "track_unpublished" | "track_unmuted" | "track_muted" => {
@@ -1,56 +0,0 @@
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)
}
@@ -1,57 +0,0 @@
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)
}
@@ -1,66 +0,0 @@
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)
}
@@ -1,70 +0,0 @@
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)
}
@@ -1,57 +0,0 @@
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)
}
@@ -1 +0,0 @@
// This should allow resetting of all factors or individual ones.
@@ -1,7 +0,0 @@
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;
@@ -1,2 +0,0 @@
// 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.
@@ -1,4 +0,0 @@
// 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.
@@ -1,5 +0,0 @@
// 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.
@@ -1,3 +0,0 @@
// 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.
@@ -1,3 +0,0 @@
// see add_collaborator
// The same rules apply, but collaborators may not remove other collaborators unless they are an admin/moderation lead.
// Owners may assign whoever (observing sensitive case rules).
@@ -1,2 +0,0 @@
// mark a case as sensitive or not.
// cases with reports that contain
@@ -1 +0,0 @@
//close/reopen a case
@@ -1,6 +0,0 @@
mod case_add_comment;
mod case_create;
mod case_edit_tags;
mod case_rename;
mod case_send_notification;
mod case_state;
@@ -1 +0,0 @@
// Should return the case model, but not all the models associated with evidence linked to the case.
@@ -1,16 +0,0 @@
// This should return all cases (paginated similar to case_get_open) with the following filters:
// ```js
// {
// state: OPEN | CLOSED,
// owner: ULID,
// collaborator: ULID, // cases this user collaborated on
// before: Timestamp,
// after: Timestamp,
// tags: [tags],
// sensitive: bool // cases that involve children (eg cp, underage, etc),
// ... whatever else you can think of.
// }
// ```
// specifying owners/collaborators should be limited to admins/moderation leads
// the sensitive tag should be limited to some sort of "sensitive topics" permission
// users without the sensitive permission should still have sensitive cases returned, but the evidence ids should be removed.
@@ -1,2 +0,0 @@
// get data associated with the evidence of a case (other case|server|user/members|etc)
// This should return nested case models, but not sub-nested models as that could lead to recursion problems.
@@ -1,6 +0,0 @@
// get open cases. This should be paginated, and the pagination offset is determined by the last received case ID from the previous request.
// Eg. GET /cases?after=OTHER_CASE_ULID&count=50
// It should also have a flag to filter by only the users cases & collaborated cases
// EG. GET /cases?owned=true&collaborated=true
// There should also be a flag to disallow sensitive cases, which should default to allowed if not specified.
// users without the sensitive permission should still have sensitive cases returned, but the evidence ids should be removed unless they are the owner/collaborator (assignable by admins/moderation leads)

Some files were not shown because too many files have changed in this diff Show More