feat: Working server/comment routes

This commit is contained in:
IAmTomahawkx
2025-07-22 03:27:05 -07:00
parent ff36d71ad5
commit bf4530563a
70 changed files with 1841 additions and 411 deletions

View File

@@ -6,10 +6,10 @@ use std::{
use futures::lock::Mutex;
use crate::{
AdminAuditItem, AdminCase, AdminCaseComment, AdminObjectNote, AdminStrike, AdminToken,
AdminUser, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite,
Member, MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan,
Snapshot, User, UserSettings, Webhook,
AdminAuditItem, AdminCase, AdminComment, AdminStrike, AdminToken, AdminUser, Bot, Channel,
ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member, MemberCompositeKey,
Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot, User, UserSettings,
Webhook,
};
database_derived!(
@@ -18,8 +18,7 @@ database_derived!(
pub struct ReferenceDb {
pub admin_audits: Arc<Mutex<BTreeMap<String, AdminAuditItem>>>,
pub admin_cases: Arc<Mutex<HashMap<String, AdminCase>>>,
pub admin_case_comments: Arc<Mutex<HashMap<String, AdminCaseComment>>>,
pub admin_object_notes: Arc<Mutex<HashMap<String, AdminObjectNote>>>,
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>>>,

View File

@@ -1,3 +1,6 @@
use iso8601_timestamp::Timestamp;
use revolt_models::v0::AdminAuditItemActions;
use crate::util::basic::transform_optional_string;
auto_derived! {
@@ -7,7 +10,7 @@ auto_derived! {
pub id: String,
/// The moderator who performed the action
pub mod_id: String,
/// The action performed (previously 'permission')
/// 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>,
@@ -15,13 +18,15 @@ auto_derived! {
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: &str,
action: AdminAuditItemActions,
case_id: Option<&str>,
target_id: Option<&str>,
context: Option<&str>,
@@ -34,6 +39,7 @@ impl AdminAuditItem {
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(),
}
}
}

View File

@@ -1,54 +0,0 @@
auto_derived_partial! {
pub struct AdminCaseComment {
/// The comment ID
#[serde(rename = "_id")]
pub id: String,
/// The ID of the case this comment is attached to
pub case_id: String,
/// The author ID
pub user_id: String,
/// When the comment was edited, if applicable, in iso8601
pub edited_at: Option<String>,
/// The content
pub content: String
},
"PartialAdminCaseComment"
}
impl AdminCaseComment {
pub fn new(case_id: &str, user_id: &str, content: &str) -> AdminCaseComment {
let id = ulid::Ulid::new().to_string();
AdminCaseComment {
id,
case_id: case_id.to_string(),
user_id: user_id.to_string(),
edited_at: None,
content: content.to_string(),
}
}
/// Edit the comment, updating the edited_at time as well
pub fn edit(&mut self, content: &str) {
self.content = content.to_string();
self.edited_at = Some(
iso8601_timestamp::Timestamp::now_utc()
.format_short()
.to_string(),
);
}
}
impl PartialAdminCaseComment {
pub fn new() -> PartialAdminCaseComment {
PartialAdminCaseComment::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(),
);
}
}

View File

@@ -1,19 +0,0 @@
mod mongodb;
mod reference;
use revolt_result::Result;
use crate::{models::admin_case_comments::AdminCaseComment, PartialAdminCaseComment};
#[async_trait]
pub trait AbstractAdminCaseComments: Sync + Send {
async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()>;
async fn admin_case_comment_update(
&self,
comment_id: &str,
partial: &PartialAdminCaseComment,
) -> Result<()>;
async fn admin_case_comment_fetch(&self, case_id: &str) -> Result<Vec<AdminCaseComment>>;
}

View File

@@ -1,36 +0,0 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::{AdminCaseComment, PartialAdminCaseComment};
use super::AbstractAdminCaseComments;
static COL: &str = "admin_case_comments";
#[async_trait]
impl AbstractAdminCaseComments for MongoDb {
async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()> {
query!(self, insert_one, COL, comment).map(|_| ())
}
async fn admin_case_comment_update(
&self,
comment_id: &str,
partial: &PartialAdminCaseComment,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
comment_id,
partial,
vec![],
None
)
.map(|_| ())
}
async fn admin_case_comment_fetch(&self, case_id: &str) -> Result<Vec<AdminCaseComment>> {
query!(self, find, COL, doc! {"case_id": case_id})
}
}

View File

@@ -1,49 +0,0 @@
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{AdminCaseComment, PartialAdminCaseComment};
use super::AbstractAdminCaseComments;
#[async_trait]
impl AbstractAdminCaseComments for ReferenceDb {
async fn admin_case_comment_insert(&self, comment: AdminCaseComment) -> Result<()> {
let mut admin_case_comments = self.admin_case_comments.lock().await;
if admin_case_comments.contains_key(&comment.id) {
Err(create_database_error!("insert", "admin_case_comments"))
} else {
admin_case_comments.insert(comment.id.to_string(), comment.clone());
Ok(())
}
}
async fn admin_case_comment_update(
&self,
comment_id: &str,
partial: &PartialAdminCaseComment,
) -> Result<()> {
let mut admin_case_comments = self.admin_case_comments.lock().await;
if let Some(comment) = admin_case_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_case_comment_fetch(&self, case_id: &str) -> Result<Vec<AdminCaseComment>> {
let admin_case_comments = self.admin_case_comments.lock().await;
Ok(admin_case_comments
.iter()
.filter_map(|(_, c)| {
if c.case_id == case_id {
Some(c.clone())
} else {
None
}
})
.collect())
}
}

View File

@@ -14,6 +14,8 @@ pub trait AbstractAdminCases: Sync + Send {
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(

View File

@@ -35,6 +35,11 @@ impl AbstractAdminCases for MongoDb {
.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,

View File

@@ -46,6 +46,15 @@ impl AbstractAdminCases for ReferenceDb {
}
}
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(

View File

@@ -0,0 +1,95 @@
use revolt_models::v0;
auto_derived_partial! {
pub struct AdminComment {
/// The comment ID
#[serde(rename = "_id")]
pub id: String,
/// The Object this comment is attached to
pub object_id: String,
/// The ID of the case this comment is attached to, if applicable
pub case_id: Option<String>,
/// The author ID
pub user_id: String,
/// When the comment was edited, if applicable, in iso8601
pub edited_at: Option<String>,
/// The content, if not a system event
pub content: Option<String>,
/// The system event that occurred, if applicable
pub system_message: Option<String>,
/// The system message target
pub system_message_target: Option<String>,
/// The system message raw context
pub system_message_context: Option<String>,
},
"PartialAdminComment"
}
impl AdminComment {
pub fn new(
object_id: &str,
user_id: &str,
content: &str,
case_id: Option<&str>,
) -> AdminComment {
let id = ulid::Ulid::new().to_string();
AdminComment {
id,
object_id: object_id.to_string(),
case_id: case_id.map(|c| c.to_string()),
user_id: user_id.to_string(),
edited_at: None,
content: Some(content.to_string()),
system_message: None,
system_message_context: None,
system_message_target: None,
}
}
pub fn new_system_message(
object_id: &str,
user_id: &str,
case_id: &str,
system_message_kind: v0::AdminAuditItemActions,
context: Option<&str>,
target: &str,
) -> AdminComment {
let id = ulid::Ulid::new().to_string();
AdminComment {
id,
object_id: object_id.to_string(),
case_id: Some(case_id.to_string()),
user_id: user_id.to_string(),
edited_at: None,
content: None,
system_message: Some(serde_json::to_string(&system_message_kind).unwrap()), // if this explodes i'll eat my hat.
system_message_context: context.map(|c| c.to_string()),
system_message_target: Some(target.to_string()),
}
}
/// Edit the comment, updating the edited_at time as well
pub fn edit(&mut self, content: &str) {
self.content = Some(content.to_string());
self.edited_at = Some(
iso8601_timestamp::Timestamp::now_utc()
.format_short()
.to_string(),
);
}
}
impl PartialAdminComment {
pub fn new() -> PartialAdminComment {
PartialAdminComment::default()
}
/// Edit the comment, updating the edited_at time as well
pub fn edit(&mut self, content: &str) {
self.content = Some(content.to_string());
self.edited_at = Some(
iso8601_timestamp::Timestamp::now_utc()
.format_short()
.to_string(),
);
}
}

View File

@@ -0,0 +1,28 @@
mod mongodb;
mod reference;
use revolt_result::Result;
use crate::{models::admin_comments::AdminComment, PartialAdminComment};
#[async_trait]
pub trait AbstractAdminComments: Sync + Send {
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()>;
async fn admin_comment_update(
&self,
comment_id: &str,
partial: &PartialAdminComment,
) -> Result<()>;
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment>;
/// Fetch all comments related to the case. This includes comments made on other objects.
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>>;
/// Fetch all comments on an object
async fn admin_comment_fetch_object_comments(
&self,
object_id: &str,
) -> Result<Vec<AdminComment>>;
}

View File

@@ -0,0 +1,47 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::{AdminComment, PartialAdminComment};
use super::AbstractAdminComments;
static COL: &str = "admin_comments";
#[async_trait]
impl AbstractAdminComments for MongoDb {
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> {
query!(self, insert_one, COL, comment).map(|_| ())
}
async fn admin_comment_update(
&self,
comment_id: &str,
partial: &PartialAdminComment,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
comment_id,
partial,
vec![],
None
)
.map(|_| ())
}
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment> {
query!(self, find_one, COL, doc! {"_id": id})?.ok_or_else(|| create_error!(NotFound))
}
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>> {
query!(self, find, COL, doc! {"case_id": case_id})
}
async fn admin_comment_fetch_object_comments(
&self,
object_id: &str,
) -> Result<Vec<AdminComment>> {
query!(self, find, COL, doc! {"object_id": object_id})
}
}

View File

@@ -0,0 +1,73 @@
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{AdminComment, PartialAdminComment};
use super::AbstractAdminComments;
#[async_trait]
impl AbstractAdminComments for ReferenceDb {
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> {
let mut admin_comments = self.admin_comments.lock().await;
if admin_comments.contains_key(&comment.id) {
Err(create_database_error!("insert", "admin_comments"))
} else {
admin_comments.insert(comment.id.to_string(), comment.clone());
Ok(())
}
}
async fn admin_comment_update(
&self,
comment_id: &str,
partial: &PartialAdminComment,
) -> Result<()> {
let mut admin_comments = self.admin_comments.lock().await;
if let Some(comment) = admin_comments.get_mut(comment_id) {
comment.apply_options(partial.clone());
comment.edited_at = Some(Timestamp::now_utc().format().to_string());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment> {
let admin_comments = self.admin_comments.lock().await;
admin_comments
.get(id)
.map_or(Err(create_error!(NotFound)), |ac| Ok(ac.clone()))
}
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>> {
let admin_comments = self.admin_comments.lock().await;
Ok(admin_comments
.iter()
.filter_map(|(_, c)| {
if c.case_id.as_ref().is_some_and(|c| c == case_id) {
Some(c.clone())
} else {
None
}
})
.collect())
}
async fn admin_comment_fetch_object_comments(
&self,
object_id: &str,
) -> Result<Vec<AdminComment>> {
let admin_comments = self.admin_comments.lock().await;
Ok(admin_comments
.iter()
.filter_map(|(_, c)| {
if c.object_id == object_id {
Some(c.clone())
} else {
None
}
})
.collect())
}
}

View File

@@ -28,6 +28,20 @@ impl AbstractMigrations for MongoDb {
init::create_database(self).await;
}
let config = revolt_config::config().await;
if config.features.admin_api_enabled {
let db = self.db();
let colls = db
.list_collection_names()
.await
.expect("Failed to fetch collection names.");
if !colls.iter().any(|x| x == "admin_audits") {
info!("You've enabled the admin api for the first time. Setting up database...");
init::create_admin_database(&db).await;
}
}
Ok(())
}
}

View File

@@ -8,6 +8,8 @@ pub async fn create_database(db: &MongoDb) {
info!("Creating database.");
let db = db.db();
let config = revolt_config::config().await;
db.create_collection("accounts")
.await
.expect("Failed to create accounts collection.");
@@ -263,5 +265,135 @@ 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;
}
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.");
}

View File

@@ -1,5 +0,0 @@
mod models;
mod ops;
pub use models::*;
pub use ops::*;

View File

@@ -1,35 +0,0 @@
auto_derived_partial! {
pub struct AdminObjectNote {
/// The ID of the note, which is the same as the ID of the object it's attached to
#[serde(rename = "_id")]
pub id: String,
/// When the note was edited, in iso8601
pub edited_at: String,
/// The last user to edit the note
pub last_edited_by_id: String,
/// The content of the note
pub content: String,
},
"PartialAdminObjectNote"
}
impl AdminObjectNote {
pub fn new(object_id: &str, user_id: &str, content: &str) -> AdminObjectNote {
AdminObjectNote {
id: object_id.to_string(),
edited_at: iso8601_timestamp::Timestamp::now_utc()
.format_short()
.to_string(),
last_edited_by_id: user_id.to_string(),
content: content.to_string(),
}
}
pub fn to_partial(&self) -> PartialAdminObjectNote {
PartialAdminObjectNote {
id: Some(self.id.clone()),
edited_at: Some(self.edited_at.clone()),
last_edited_by_id: Some(self.last_edited_by_id.clone()),
content: Some(self.content.clone()),
}
}
}

View File

@@ -1,12 +0,0 @@
mod mongodb;
mod reference;
use revolt_result::Result;
use crate::models::admin_notes::AdminObjectNote;
#[async_trait]
pub trait AbstractAdminNotes: Sync + Send {
async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()>;
async fn admin_note_fetch(&self, target_id: &str) -> Result<AdminObjectNote>;
}

View File

@@ -1,34 +0,0 @@
use revolt_result::Result;
use crate::AdminObjectNote;
use crate::MongoDb;
use super::AbstractAdminNotes;
static COL: &str = "admin_notes";
#[async_trait]
impl AbstractAdminNotes for MongoDb {
async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()> {
let resp: Result<()> = query!(self, insert_one, COL, note.clone()).map(|_| ());
if resp.is_err() {
query!(
self,
update_one_by_id,
COL,
note.id.as_str(),
note.to_partial(),
vec![],
None
)
.map(|_| ())
} else {
Ok(())
}
}
async fn admin_note_fetch(&self, target_id: &str) -> Result<AdminObjectNote> {
query!(self, find_one_by_id, COL, target_id)?
.ok_or_else(|| create_database_error!("find_one", COL))
}
}

View File

@@ -1,29 +0,0 @@
use revolt_result::Result;
use crate::AdminObjectNote;
use crate::ReferenceDb;
use super::AbstractAdminNotes;
#[async_trait]
impl AbstractAdminNotes for ReferenceDb {
async fn admin_note_update(&self, note: AdminObjectNote) -> Result<()> {
let mut admin_notes = self.admin_object_notes.lock().await;
if let Some(existing_note) = admin_notes.get_mut(&note.id) {
existing_note.apply_options(note.to_partial());
Ok(())
} else {
admin_notes.insert(note.id.clone(), note);
Ok(())
}
}
async fn admin_note_fetch(&self, target_id: &str) -> Result<AdminObjectNote> {
let admin_notes = self.admin_object_notes.lock().await;
if let Some(note) = admin_notes.get(target_id) {
Ok(note.clone())
} else {
Err(create_error!(NotFound))
}
}
}

View File

@@ -12,7 +12,14 @@ static COL: &str = "channel_invites";
impl AbstractChannelInvites for MongoDb {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
query!(self, insert_one, COL, &invite).map(|_| ())
self.insert_one(COL, &invite).await.or_else(|e| {
if e.to_string().contains("duplicate") {
return Err(create_error!(InviteExists));
} else {
return Err(create_database_error!("insert_one", COL));
}
})?;
Ok(())
}
/// Fetch an invite by the code

View File

@@ -1,8 +1,7 @@
mod admin_audits;
mod admin_case_comments;
mod admin_cases;
mod admin_comments;
mod admin_migrations;
mod admin_notes;
mod admin_strikes;
mod admin_tokens;
mod admin_users;
@@ -26,10 +25,9 @@ mod user_settings;
mod users;
pub use admin_audits::*;
pub use admin_case_comments::*;
pub use admin_cases::*;
pub use admin_comments::*;
pub use admin_migrations::*;
pub use admin_notes::*;
pub use admin_strikes::*;
pub use admin_tokens::*;
pub use admin_users::*;
@@ -58,9 +56,8 @@ pub trait AbstractDatabase:
Sync
+ Send
+ admin_audits::AbstractAdminAudits
+ admin_case_comments::AbstractAdminCaseComments
+ admin_cases::AbstractAdminCases
+ admin_notes::AbstractAdminNotes
+ admin_comments::AbstractAdminComments
+ admin_strikes::AbstractAdminStrikes
+ admin_users::AbstractAdminUsers
+ admin_tokens::AbstractAdminTokens

View File

@@ -81,6 +81,7 @@ impl Member {
server: &Server,
user: &User,
channels: Option<Vec<Channel>>,
create_alert: bool,
) -> Result<(Member, Vec<Channel>)> {
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
return Err(create_error!(Banned));
@@ -141,18 +142,20 @@ impl Member {
.private(user.id.clone())
.await;
if let Some(id) = server
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
if create_alert {
if let Some(id) = server
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.send_without_notifications(db, None, None, false, false, false)
.await
.ok();
}
.into_message(id.to_string())
.send_without_notifications(db, None, None, false, false, false)
.await
.ok();
}
Ok((member, channels))

View File

@@ -117,4 +117,18 @@ pub trait AbstractServerMembers: Sync + Send {
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> 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)>>;
}

View File

@@ -1,8 +1,10 @@
use std::collections::HashMap;
use futures::StreamExt;
use mongodb::options::ReadConcern;
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{AbstractUsers, FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{IntoDocumentPath, MongoDb};
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
@@ -238,6 +240,88 @@ impl AbstractServerMembers for MongoDb {
)
.map(|_| ())
}
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 {

View File

@@ -1,3 +1,6 @@
use std::collections::HashSet;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::ReferenceDb;
@@ -178,4 +181,91 @@ impl AbstractServerMembers for ReferenceDb {
Err(create_error!(NotFound))
}
}
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())
}
}

View File

@@ -1447,12 +1447,38 @@ impl From<crate::AdminToken> for AdminToken {
}
}
impl From<crate::AdminObjectNote> for AdminObjectNote {
fn from(value: crate::AdminObjectNote) -> Self {
AdminObjectNote {
edited_at: Timestamp::parse(&value.edited_at).unwrap(), // if it's invalid it shouldn't have made it to this point.
last_edited_by_id: value.last_edited_by_id,
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,
}
}
}

View File

@@ -2,7 +2,7 @@ use std::str::FromStr;
use revolt_result::Result;
#[cfg(feature = "rocket-impl")]
use rocket::request::FromParam;
use rocket::{form::FromFormField, request::FromParam};
#[cfg(feature = "rocket-impl")]
use schemars::{
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
@@ -133,3 +133,10 @@ impl JsonSchema for Reference {
})
}
}
#[cfg(feature = "rocket-impl")]
impl<'r> FromFormField<'r> for Reference {
fn from_value(field: rocket::form::ValueField<'r>) -> rocket::form::Result<'r, Self> {
Ok(Reference::from_unchecked(field.value.into()))
}
}

View File

@@ -1,6 +1,6 @@
use iso8601_timestamp::Timestamp;
use crate::v0::{Report, User};
use crate::v0::{self, Report, User};
auto_derived! {
#[derive(Default)]
@@ -28,7 +28,7 @@ auto_derived! {
#[cfg_attr(feature="serde", serde(rename="mod"))]
pub mod_id: String,
/// The action performed (previously 'permission')
pub action: String,
pub action: AdminAuditItemActions,
/// The relevant case ID, if applicable
#[cfg_attr(feature="serde", serde(rename="case"))]
pub case_id: String,
@@ -37,14 +37,19 @@ auto_derived! {
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 AdminCaseCommentCreate {
/// The ID of the case this comment is attached to
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: String,
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
@@ -52,25 +57,39 @@ auto_derived! {
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct AdminCaseCommentEdit {
pub struct AdminCommentEdit {
/// The new content
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
pub content: String
}
pub struct AdminCaseComment {
pub struct AdminComment {
/// The comment ID
pub id: String,
/// The ID of the case this comment is attached to
/// 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: String,
/// The user who posted the comment
#[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
pub content: String
/// 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)]
@@ -119,23 +138,6 @@ auto_derived! {
pub reports: Vec<Report>
}
pub struct AdminObjectNote {
/// When the note was edited, in iso8601
pub edited_at: Timestamp,
/// The last user to edit the note
#[cfg_attr(feature="serde", serde(rename="last_edited_by"))]
pub last_edited_by_id: String,
/// The content of the note
pub content: String,
}
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct AdminObjectNoteEdit {
#[cfg_attr(feature = "validator", validate(length(max = 2000)))]
pub content: String
}
pub struct AdminStrike {
/// The strike ID
pub id: String,
@@ -259,9 +261,11 @@ auto_derived! {
pub permissions: Option<u64>,
}
#[repr(u64)]
// Flags
#[repr(u32)]
pub enum AdminUserPermissionFlags {
ObjectNotes = 0,
Comments = 0,
ManageAdminUsers = 1,
CreateTokens = 2,
@@ -285,4 +289,88 @@ auto_derived! {
ManageNotes = 15,
Discover = 16,
}
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
}
// 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,
}
}
}
impl std::fmt::Display for AdminAuditItemActions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}

View File

@@ -127,4 +127,9 @@ auto_derived!(
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsMember>>,
}
pub struct MemberWithUserResponse {
pub user: crate::v0::User,
pub member: Member,
}
);

View File

@@ -34,8 +34,10 @@ 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::InviteExists => StatusCode::BAD_REQUEST,
ErrorType::UnknownServer => StatusCode::NOT_FOUND,
ErrorType::InvalidRole => StatusCode::NOT_FOUND,

View File

@@ -101,11 +101,13 @@ pub enum ErrorType {
NotInGroup,
AlreadyPinned,
NotPinned,
InviteExists,
// ? Server related errors
UnknownServer,
InvalidRole,
Banned,
NotAMember,
TooManyServers {
max: usize,
},

View File

@@ -40,9 +40,11 @@ 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::InvalidFlagValue => Status::BadRequest,
ErrorType::InviteExists => Status::BadRequest,
ErrorType::UnknownServer => Status::NotFound,
ErrorType::InvalidRole => Status::NotFound,

View File

@@ -0,0 +1,44 @@
use revolt_database::{AdminAuthorization, AdminComment, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Create a comment on an object or case. Requires Comments permission.
#[openapi(tag = "Admin")]
#[post("/comments", data = "<body>")]
pub async fn admin_comment_create(
db: &State<Database>,
auth: AdminAuthorization,
body: Json<v0::AdminCommentCreate>,
) -> Result<Json<v0::AdminComment>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::Comments) {
return Err(create_error!(MissingPermission {
permission: "Comments".to_string()
}));
}
let comment = AdminComment::new(
&body.object_id,
&user.id,
&body.content,
body.case_id.as_deref(),
);
db.admin_comment_insert(comment.clone()).await?;
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::CommentCreate,
None,
Some(&body.object_id),
None,
)
.await?;
Ok(Json(comment.into()))
}

View File

@@ -0,0 +1,56 @@
use iso8601_timestamp::Timestamp;
use revolt_database::{
util::reference::Reference, AdminAuthorization, Database, PartialAdminComment,
};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Create a comment on an object or case. Requires Comments permission.
#[openapi(tag = "Admin")]
#[patch("/comments/<id>", data = "<body>")]
pub async fn admin_comment_edit(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
body: Json<v0::AdminCommentEdit>,
) -> Result<EmptyResponse> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::Comments) {
return Err(create_error!(MissingPermission {
permission: "Comments".to_string()
}));
}
let existing = db.admin_comment_fetch(&id.id).await?;
if existing.user_id != user.id {
return Err(create_error!(MissingPermission {
permission: "CannotEditOthersComment".to_string()
}));
}
let partial = PartialAdminComment {
content: Some(body.content.clone()),
edited_at: Some(Timestamp::now_utc().format_short().to_string()),
..Default::default()
};
db.admin_comment_update(&id.id, &partial).await?;
let context = format!("comment id {:}, object id {:}", &id.id, &existing.object_id);
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::CommentEdit,
None,
Some(&existing.object_id),
Some(&context),
)
.await?;
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,43 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Fetch all comments related to a case. id should be the 7 character short code. Requires Comments permission.
#[openapi(tag = "Admin")]
#[get("/comments/case/<id>")]
pub async fn admin_comment_fetch_case(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
) -> Result<Json<Vec<v0::AdminComment>>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::Comments) {
return Err(create_error!(MissingPermission {
permission: "Comments".to_string()
}));
}
let comments: Vec<v0::AdminComment> = db
.admin_comment_fetch_object_comments(&id.id)
.await?
.iter()
.map(|f| f.clone().into())
.collect();
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::CommentFetchForObject,
Some(&id.id),
None,
None,
)
.await?;
Ok(Json(comments))
}

View File

@@ -0,0 +1,43 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Fetch comments on an object. Requires Comments permission.
#[openapi(tag = "Admin")]
#[get("/comments/object/<id>")]
pub async fn admin_comment_fetch_object(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
) -> Result<Json<Vec<v0::AdminComment>>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::Comments) {
return Err(create_error!(MissingPermission {
permission: "Comments".to_string()
}));
}
let comments: Vec<v0::AdminComment> = db
.admin_comment_fetch_object_comments(&id.id)
.await?
.iter()
.map(|f| f.clone().into())
.collect();
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::CommentFetchForObject,
None,
Some(&id.id),
None,
)
.await?;
Ok(Json(comments))
}

View File

@@ -0,0 +1,4 @@
pub mod comment_create;
pub mod comment_edit;
pub mod comment_fetch_case;
pub mod comment_fetch_object;

View File

@@ -18,7 +18,9 @@ pub async fn admin_create_token(
&auth.on_behalf_of,
v0::AdminUserPermissionFlags::CreateTokens,
) {
return Err(create_error!(NotFound));
return Err(create_error!(MissingPermission {
permission: "CreateTokens".to_string()
}));
}
if body.expiry > Timestamp::now_utc() + Duration::days(30) {

View File

@@ -15,7 +15,9 @@ pub async fn admin_create_user(
) -> Result<Json<v0::AdminUser>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAdminUsers) {
return Err(create_error!(NotFound));
return Err(create_error!(MissingPermission {
permission: "ManageAdminUsers".to_string()
}));
}
// TODO: technically there's a privilege escalation here since anyone with the manageAdminUsers permission can assign whatever permissions they want.

View File

@@ -17,7 +17,9 @@ pub async fn admin_edit_user(
) -> Result<EmptyResponse> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageAdminUsers) {
return Err(create_error!(NotFound));
return Err(create_error!(MissingPermission {
permission: "ManageAdminUsers".to_string()
}));
}
// TODO: technically there's a privilege escalation here since anyone with the manageAdminUsers permission can assign whatever permissions they want.

View File

@@ -19,7 +19,9 @@ pub async fn admin_revoke_token(
&auth.on_behalf_of,
v0::AdminUserPermissionFlags::CreateTokens,
) {
return Err(create_error!(NotFound));
return Err(create_error!(MissingPermission {
permission: "CreateTokens".to_string()
}));
}
let token = token.as_admin_token(db).await?;

View File

@@ -3,24 +3,38 @@ use rocket::Route;
mod accounts;
mod cases;
mod comments;
mod meta;
mod reports;
mod roles;
mod search;
mod servers;
mod users;
mod object_edit_note;
mod object_get_note;
mod util;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
object_edit_note::admin_object_edit_note,
object_get_note::admin_object_get_note,
meta::create_user::admin_create_user,
meta::edit_user::admin_edit_user,
meta::fetch_users::admin_fetch_users,
meta::create_token::admin_create_token,
meta::revoke_token::admin_revoke_token
meta::revoke_token::admin_revoke_token,
comments::comment_create::admin_comment_create,
comments::comment_edit::admin_comment_edit,
comments::comment_fetch_case::admin_comment_fetch_case,
comments::comment_fetch_object::admin_comment_fetch_object,
servers::fetch::server_get::admin_server_get,
servers::fetch::server_get_participants::admin_server_get_participants,
servers::fetch::server_get_all_members::admin_server_get_members,
servers::actions::server_add_members::admin_server_add_member,
servers::actions::server_ban_members::admin_server_ban_member,
servers::actions::server_unban_members::admin_server_unban_member,
servers::actions::server_change_owner::admin_server_change_owner,
servers::actions::server_create_invite::admin_server_create_invite,
servers::actions::server_delete_invites::admin_server_delete_invites,
servers::actions::server_delete::admin_server_delete,
servers::actions::server_edit::admin_server_edit,
servers::actions::server_remove_members::admin_server_remove_members
]
}

View File

@@ -1,28 +0,0 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, AdminObjectNote, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{flatten_authorized_user, user_has_permission};
/// Edit an object note. Requires ObjectNotes permission.
#[openapi(tag = "Admin")]
#[post("/notes/<object>", data = "<body>")]
pub async fn admin_object_edit_note(
db: &State<Database>,
auth: AdminAuthorization,
object: Reference,
body: Json<v0::AdminObjectNoteEdit>,
) -> Result<EmptyResponse> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ObjectNotes) {
return Err(create_error!(NotFound));
}
// Unfortunately due to how our system works, we can't limit users to editing objects they have permissions for (eg users, servers, etc.).
// Hence it being tied to its own permission
db.admin_note_update(AdminObjectNote::new(&object.id, &user.id, &body.content))
.await?;
Ok(EmptyResponse)
}

View File

@@ -1,24 +0,0 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{flatten_authorized_user, user_has_permission};
/// Edit an object note. Requires ObjectNotes permission.
#[openapi(tag = "Admin")]
#[get("/notes/<object>")]
pub async fn admin_object_get_note(
db: &State<Database>,
auth: AdminAuthorization,
object: Reference,
) -> Result<Json<v0::AdminObjectNote>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ObjectNotes) {
return Err(create_error!(NotFound));
}
// Unfortunately due to how our system works, we can't limit users to editing objects they have permissions for (eg users, servers, etc.).
// Hence it being tied to its own permission
Ok(Json(db.admin_note_fetch(&object.id).await?.into()))
}

View File

@@ -0,0 +1,9 @@
pub mod server_add_members;
pub mod server_ban_members;
pub mod server_change_owner;
pub mod server_create_invite;
pub mod server_delete;
pub mod server_delete_invites;
pub mod server_edit;
pub mod server_remove_members;
pub mod server_unban_members;

View File

@@ -0,0 +1,48 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database, Member};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// # Add server member
///
/// Add a user to a server, optionally disabling notifications
#[openapi(tag = "Admin")]
#[post("/servers/<id>/members?<case>&<user_id>&<suppress_alerts>")]
pub async fn admin_server_add_member(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
user_id: Reference,
suppress_alerts: bool,
) -> Result<Json<v0::MemberWithUserResponse>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let server = id.as_server(db).await?;
let user = user_id.as_user(db).await?;
let (member, _) = Member::create(db, &server, &user, None, !suppress_alerts).await?;
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerAddMember,
case,
Some(&id.id),
None,
)
.await?;
Ok(Json(v0::MemberWithUserResponse {
user: user.into_self(false).await,
member: member.into(),
}))
}

View File

@@ -0,0 +1,83 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database, ServerBan};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// # Ban a server member
///
/// Ban a member who is not the owner
#[openapi(tag = "Admin")]
#[post(
"/servers/<id>/ban?<case>&<user_id>&<suppress_alerts>",
data = "<body>"
)]
pub async fn admin_server_ban_member(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
user_id: Reference,
suppress_alerts: bool,
body: Json<v0::DataBanCreate>,
) -> Result<Json<v0::ServerBan>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let body = body.into_inner();
body.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let server = id.as_server(db).await?;
let target = user_id.as_user(db).await?;
if target.id == server.owner {
return Err(create_error!(InvalidOperation));
}
let resp = if let Ok(member) = user_id.as_member(db, &id.id).await {
member
.remove(
db,
&server,
revolt_database::RemovalIntention::Ban,
suppress_alerts,
)
.await?;
Ok(ServerBan::create(db, &server, &target.id, body.reason.clone()).await?)
} else {
Err(create_error!(NotAMember))
}?;
let context = format!(
"user id: {:}, server id: {:}, reason: {:}",
&user_id.id,
&id.id,
body.reason
.clone()
.unwrap_or_else(|| "None Provided".to_string())
);
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerBanMember,
case,
Some(&id.id),
Some(&context),
)
.await?;
Ok(Json(resp.into()))
}

View File

@@ -0,0 +1,55 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database, PartialServer};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// # Change Server Owner
///
/// Change the ownership of a server.
#[openapi(tag = "Admin")]
#[post("/servers/<id>/owner?<case>&<user_id>")]
pub async fn admin_server_change_owner(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
user_id: Reference,
) -> Result<EmptyResponse> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let server = id.as_server(db).await?;
let target = user_id.as_user(db).await?;
if target.id == server.owner {
return Err(create_error!(InvalidOperation));
}
let partial = PartialServer {
owner: Some(target.id),
..Default::default()
};
db.update_server(&server.id, &partial, vec![]).await?;
let context = format!("user id: {:}, server id: {:}", &user_id.id, &id.id);
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerChangeOwner,
case,
Some(&id.id),
Some(&context),
)
.await?;
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,66 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database, Invite};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// # Create Server Invite
///
/// Create an invite to a server. Optionally include a slug to create a vanity URL.
#[openapi(tag = "Admin")]
#[post("/servers/<id>/invites?<case>&<slug>&<channel_id>")]
pub async fn admin_server_create_invite(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
channel_id: Reference,
case: Option<&str>,
slug: Option<&str>,
) -> Result<Json<v0::Invite>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let server = id.as_server(db).await?;
let channel = channel_id.as_channel(db).await?;
let creator = db.fetch_user(&server.owner).await?;
let invite: Invite;
if let Some(slug) = slug {
invite = Invite::Server {
code: slug.to_string(),
creator: creator.id.clone(),
server: server.id.clone(),
channel: channel.id().to_string(),
};
#[allow(clippy::disallowed_methods)]
db.insert_invite(&invite).await?;
} else {
invite = Invite::create_channel_invite(db, &creator, &channel).await?;
}
let context = format!(
"server id: {:}, invite slug: {:}, channel: {:}",
&id.id,
invite.code(),
channel.id()
);
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerCreateInvite,
case,
Some(&id.id),
Some(&context),
)
.await?;
Ok(Json(invite.into()))
}

View File

@@ -0,0 +1,41 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Get a list of admin users. Any active user may use this endpoint.
/// Typically the client should cache this data.
#[openapi(tag = "Admin")]
#[delete("/servers/<id>?<case>")]
pub async fn admin_server_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::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
db.delete_server(&id.id).await?;
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerDelete,
case,
Some(&id.id),
None,
)
.await?;
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,62 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Delete Server Invite
///
/// Delete a server invite, or all of a server's invites.
#[openapi(tag = "Admin")]
#[delete("/servers/<id>/invites?<case>&<slug>")]
pub async fn admin_server_delete_invites(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
slug: Option<Reference>,
) -> Result<EmptyResponse> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
if let Some(slug) = slug {
let invite = slug.as_invite(db).await?;
db.delete_invite(&slug.id).await?;
let context = format!("server id: {:}, invite slug: {:}", &id.id, invite.code());
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerDeleteInvite,
case,
Some(&id.id),
Some(&context),
)
.await?;
} else {
let invites = db.fetch_invites_for_server(&id.id).await?;
for invite in invites {
db.delete_invite(invite.code()).await?;
}
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerDeleteAllInvites,
case,
Some(&id.id),
None,
)
.await?;
}
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,52 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// # Edit server
///
/// Edit any attributes of a server.
#[openapi(tag = "Admin")]
#[patch("/servers/<id>?<case>", data = "<body>")]
pub async fn admin_server_edit(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
body: Json<v0::DataEditServer>,
) -> Result<Json<v0::Server>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let body = body.into_inner();
body.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut server = id.as_server(db).await?;
let user = db.fetch_user(&server.owner).await?;
crate::routes::servers::server_edit::edit_data(body, db, &mut server, &user).await?;
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerEdit,
case,
Some(&id.id),
None,
)
.await?;
Ok(Json(server.into()))
}

View File

@@ -0,0 +1,61 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// # Ban a server member
///
/// Ban a member who is not the owner
#[openapi(tag = "Admin")]
#[post("/servers/<id>/remove?<case>&<user_id>&<suppress_alerts>")]
pub async fn admin_server_remove_members(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
user_id: Vec<Reference>,
suppress_alerts: bool,
) -> Result<EmptyResponse> {
let admin_user = flatten_authorized_user(&auth);
if !user_has_permission(admin_user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let server = id.as_server(db).await?;
for uid in user_id {
let target = uid.as_member(db, &id.id).await?;
if target.id.user == server.owner {
return Err(create_error!(InvalidOperation));
}
target
.remove(
db,
&server,
revolt_database::RemovalIntention::Kick,
suppress_alerts,
)
.await?;
let context = format!("user id: {:}, server id: {:}", &uid.id, &id.id);
create_audit_action(
db,
&admin_user.id,
v0::AdminAuditItemActions::ServerRemoveMember,
case,
Some(&id.id),
Some(&context),
)
.await?;
}
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,60 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Get a list of admin users. Any active user may use this endpoint.
/// Typically the client should cache this data.
#[openapi(tag = "Admin")]
#[post(
"/servers/<id>/unban?<case>&<user_id>&<suppress_alerts>",
data = "<body>"
)]
pub async fn admin_server_unban_member(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
user_id: Reference,
suppress_alerts: bool,
body: Json<v0::DataBanCreate>,
) -> Result<EmptyResponse> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let ban = db
.fetch_ban(&id.id, &user_id.id)
.await
.map_err(|_| create_error!(NotFound))?;
db.delete_ban(&ban.id).await?;
let context = format!(
"user id: {:}, server id: {:}, reason: {:}",
&user_id.id,
&id.id,
body.reason
.clone()
.unwrap_or_else(|| "None Provided".to_string())
);
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerUnbanMember,
case,
Some(&id.id),
Some(&context),
)
.await?;
Ok(EmptyResponse)
}

View File

@@ -0,0 +1,3 @@
pub mod server_get;
pub mod server_get_all_members;
pub mod server_get_participants;

View File

@@ -0,0 +1,51 @@
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Get a list of admin users. Any active user may use this endpoint.
/// Typically the client should cache this data.
#[openapi(tag = "Admin")]
#[get("/servers/<id>?<case>")]
pub async fn admin_server_get(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
) -> Result<Json<v0::AdminServerResponse>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let server = id.as_server(db).await?;
let owner = db.fetch_user(&server.owner).await?.into_self(false).await;
let comments: Vec<v0::AdminComment> = db
.admin_comment_fetch_object_comments(&server.id)
.await?
.iter()
.map(|f| f.clone().into())
.collect();
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerFetch,
case,
Some(&id.id),
None,
)
.await?;
Ok(Json(v0::AdminServerResponse {
server: server.into(),
owner,
comments,
}))
}

View File

@@ -0,0 +1,67 @@
use futures::future::join_all;
use iso8601_timestamp::Timestamp;
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Get a list of admin users. Any active user may use this endpoint.
/// Typically the client should cache this data.
#[openapi(tag = "Admin")]
#[get("/servers/<id>/members?<case>&<after>")]
pub async fn admin_server_get_members(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
after: Option<usize>,
) -> Result<Json<v0::AdminMemberWithUserAndOffsetResponse>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let server = id.as_server(db).await?;
let members = db.fetch_server_members(&server.id, 100, after).await?;
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerFetchMembers,
case,
Some(&id.id),
None,
)
.await?;
let members_and_users = join_all(members.iter().map(|(u, m)| async move {
let user: v0::User = u.clone().into_self(false).await;
let member: v0::Member = m.clone().into();
v0::MemberWithUserResponse { user, member }
}))
.await;
let mut resp = v0::AdminMemberWithUserAndOffsetResponse {
after: None,
users: vec![],
};
if let Some(last) = members_and_users.last() {
resp.after = Some(
last.member
.joined_at
.duration_since(Timestamp::UNIX_EPOCH)
.whole_milliseconds() as usize,
);
}
resp.users = members_and_users;
Ok(Json(resp))
}

View File

@@ -0,0 +1,51 @@
use futures::future::join_all;
use revolt_database::{util::reference::Reference, AdminAuthorization, Database};
use revolt_models::v0::{self};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::routes::admin::util::{
create_audit_action, flatten_authorized_user, user_has_permission,
};
/// Get a list of admin users. Any active user may use this endpoint.
/// Typically the client should cache this data.
#[openapi(tag = "Admin")]
#[get("/servers/<id>/participants?<case>")]
pub async fn admin_server_get_participants(
db: &State<Database>,
auth: AdminAuthorization,
id: Reference,
case: Option<&str>,
) -> Result<Json<Vec<(v0::User, Option<v0::Member>)>>> {
let user = flatten_authorized_user(&auth);
if !user_has_permission(user, v0::AdminUserPermissionFlags::ManageServers) {
return Err(create_error!(MissingPermission {
permission: "ManageServers".to_string()
}));
}
let server = id.as_server(db).await?;
let participants = db.fetch_server_participants(&server.id).await?;
create_audit_action(
db,
&user.id,
v0::AdminAuditItemActions::ServerFetchParticipants,
case,
Some(&id.id),
None,
)
.await?;
println!("{:?}", &participants);
let resp = join_all(participants.iter().map(|(u, m)| async move {
let user: v0::User = u.clone().into_self(false).await;
let member: Option<v0::Member> = m.clone().map(|mu| mu.into());
(user, member)
}))
.await;
Ok(Json(resp))
}

View File

@@ -1,2 +1,2 @@
mod actions;
mod fetch;
pub mod actions;
pub mod fetch;

View File

@@ -1,5 +1,9 @@
use revolt_database::{AdminAuthorization, AdminUser, AdminUserPermissionFlagsValue};
use revolt_models::v0::AdminUserPermissionFlags;
use revolt_database::{
AdminAuditItem, AdminAuthorization, AdminComment, AdminUser, AdminUserPermissionFlagsValue,
Database,
};
use revolt_models::v0::{AdminAuditItemActions, AdminUserPermissionFlags};
use revolt_result::Result;
pub fn user_has_permission(user: &AdminUser, permission: AdminUserPermissionFlags) -> bool {
let flags = AdminUserPermissionFlagsValue(user.permissions);
@@ -7,9 +11,40 @@ pub fn user_has_permission(user: &AdminUser, permission: AdminUserPermissionFlag
flags.has(permission)
}
pub fn flatten_authorized_user<'a>(auth: &'a AdminAuthorization) -> &'a AdminUser {
pub fn flatten_authorized_user(auth: &AdminAuthorization) -> &AdminUser {
match auth {
AdminAuthorization::AdminUser(admin_user) => &admin_user,
AdminAuthorization::AdminUser(admin_user) => admin_user,
AdminAuthorization::AdminMachine(admin_machine_token) => &admin_machine_token.on_behalf_of,
}
}
/// Creates audit logs, and if applicable, adds a comment to the referenced case.
pub async fn create_audit_action(
db: &Database,
mod_id: &str,
action: AdminAuditItemActions,
short_case_id: Option<&str>,
target_id: Option<&str>,
context: Option<&str>,
) -> Result<()> {
let audit = AdminAuditItem::new(mod_id, action.clone(), short_case_id, target_id, context);
db.admin_audit_insert(audit).await?;
if action.makes_comment() {
if let Some(short_case_id) = short_case_id {
if let Some(target_id) = target_id {
let case = db.admin_case_fetch_from_shorthand(short_case_id).await?;
db.admin_comment_insert(AdminComment::new_system_message(
&case.id,
mod_id,
short_case_id,
action,
context,
target_id,
))
.await?;
}
}
}
Ok(())
}

View File

@@ -43,7 +43,7 @@ pub async fn invite_bot(
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?;
Member::create(db, &server, &bot_user, None)
Member::create(db, &server, &bot_user, None, true)
.await
.map(|_| EmptyResponse)
}

View File

@@ -24,7 +24,7 @@ pub async fn join(
match &invite {
Invite::Server { server, .. } => {
let server = db.fetch_server(server).await?;
let (_, channels) = Member::create(db, &server, &user, None).await?;
let (_, channels) = Member::create(db, &server, &user, None, true).await?;
Ok(Json(InviteJoinResponse::Server {
channels: channels.into_iter().map(|c| c.into()).collect(),

View File

@@ -21,7 +21,7 @@ mod roles_fetch;
mod server_ack;
mod server_create;
mod server_delete;
mod server_edit;
pub mod server_edit;
mod server_fetch;
pub fn routes() -> (Vec<Route>, OpenApi) {

View File

@@ -30,7 +30,7 @@ pub async fn create_server(
user.can_acquire_server(db).await?;
let (server, channels) = Server::create(db, data, &user, true).await?;
let (_, channels) = Member::create(db, &server, &user, Some(channels)).await?;
let (_, channels) = Member::create(db, &server, &user, Some(channels), true).await?;
Ok(Json(v0::CreateServerLegacyResponse {
server: server.into(),

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, File, PartialServer, User,
Database, File, PartialServer, Server, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -69,6 +69,16 @@ pub async fn edit(
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
}
edit_data(data, db, &mut server, &user).await?;
Ok(Json(server.into()))
}
pub async fn edit_data(
data: v0::DataEditServer,
db: &Database,
server: &mut Server,
user: &User,
) -> Result<()> {
let v0::DataEditServer {
name,
description,
@@ -158,5 +168,5 @@ pub async fn edit(
)
.await?;
Ok(Json(server.into()))
Ok(())
}

View File

@@ -167,7 +167,7 @@ impl TestHarness {
server: &Server,
channels: Vec<Channel>,
) -> (Channel, Member, Message) {
let (member, channels) = Member::create(&self.db, server, user, Some(channels))
let (member, channels) = Member::create(&self.db, server, user, Some(channels), true)
.await
.expect("Failed to create member");
let channel = &channels[0];

View File

@@ -0,0 +1,52 @@
# requires httpx off pypi
import httpx
import math
from os import getenv
import asyncio
URL = getenv("ZAMMAD_URL")
TOKEN = getenv("ZAMMAD_TOKEN")
EMAIL_FILE = getenv("EMAILS_FILE")
PAYLOAD_TEMPLATE = getenv("PAYLOAD_FILE")
assert URL and TOKEN and EMAIL_FILE and PAYLOAD_TEMPLATE, ValueError("Expected env variables EMAIL_FILE, PAYLOAD_FILE, ZAMMAD_URL and ZAMMAD_TOKEN.")
URL = URL.strip("/") + "/api/v1/tickets"
async def worker(payload: dict, emails: list[str]) -> None:
async with httpx.AsyncClient(headers={"Authorization": f"Token token={TOKEN}"}) as client:
assert URL # redundant but makes pyright happy
for email in emails:
payload["customer_id"] = f"guess:{email}"
payload["article"]["to"] = email
print(email)
resp = await client.post(URL, json=payload)
if resp.status_code > 300:
raise RuntimeError(f"Failed to create ticket: {resp.status_code}\n{resp.read()}")
async def main():
assert EMAIL_FILE and PAYLOAD_TEMPLATE # pyright happy time
with open(EMAIL_FILE) as f:
emails = [x.strip() for x in f.read().split("\n")]
with open(PAYLOAD_TEMPLATE) as f:
p = {}
exec(f.read(), p)
payload = p["PAYLOAD"]
# WORKER_COUNT = min(max(round(len(emails) / 10), 1), 10)
# EMAILS_PER = math.ceil(len(emails) / WORKER_COUNT)
# print(WORKER_COUNT, EMAILS_PER)
# tasks = []
# for worker_id in range(WORKER_COUNT):
# worker_emails = emails[EMAILS_PER * worker_id:EMAILS_PER * worker_id + 1]
# tasks.append(asyncio.create_task(worker(payload, worker_emails)))
# await asyncio.gather(*tasks)
await worker(payload, emails)
asyncio.run(main())