feat: Working server/comment routes

This commit is contained in:
IAmTomahawkx
2025-07-22 03:27:05 -07:00
committed by Angelo
parent 4fd71c98d6
commit 73ce052e1a
70 changed files with 1846 additions and 414 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::*;
@@ -61,9 +59,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

@@ -85,6 +85,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));
@@ -148,18 +149,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))
@@ -305,8 +308,10 @@ mod tests {
.unwrap()
.0;
Member::create(&db, &server, &owner, None).await.unwrap();
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
Member::create(&db, &server, &owner, None, false)
.await
.unwrap();
let mut kickable_member = Member::create(&db, &server, &kickable_user, None, false)
.await
.unwrap()
.0;
@@ -330,7 +335,7 @@ mod tests {
.await
.unwrap();
let kickable_member = Member::create(&db, &server, &kickable_user, None)
let kickable_member = Member::create(&db, &server, &kickable_user, None, false)
.await
.unwrap()
.0;

View File

@@ -129,4 +129,18 @@ pub trait AbstractServerMembers: Sync + Send {
/// Fetch all members who have been marked for deletion.
async fn remove_dangling_members(&self) -> 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,10 +1,12 @@
use bson::Document;
use std::collections::HashMap;
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use mongodb::options::ReadConcern;
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{AbstractUsers, FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{IntoDocumentPath, MongoDb};
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
@@ -326,6 +328,88 @@ impl AbstractServerMembers for MongoDb {
.map(|_| ())
.map_err(|_| create_database_error!("count_documents", 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 {

View File

@@ -1,3 +1,6 @@
use std::collections::HashSet;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::ReferenceDb;
@@ -200,4 +203,91 @@ impl AbstractServerMembers for ReferenceDb {
async fn remove_dangling_members(&self) -> Result<()> {
todo!()
}
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

@@ -1448,12 +1448,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},
@@ -132,3 +132,10 @@ impl<'a> JsonSchema for Reference<'a> {
})
}
}
#[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()))
}
}