mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-14 21:47:02 +00:00
feat: Working server/comment routes
This commit is contained in:
@@ -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>>>,
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
95
crates/core/database/src/models/admin_comments/models.rs
Normal file
95
crates/core/database/src/models/admin_comments/models.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use revolt_models::v0;
|
||||
|
||||
auto_derived_partial! {
|
||||
pub struct AdminComment {
|
||||
/// The comment ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The Object this comment is attached to
|
||||
pub object_id: String,
|
||||
/// The ID of the case this comment is attached to, if applicable
|
||||
pub case_id: Option<String>,
|
||||
/// The author ID
|
||||
pub user_id: String,
|
||||
/// When the comment was edited, if applicable, in iso8601
|
||||
pub edited_at: Option<String>,
|
||||
/// The content, if not a system event
|
||||
pub content: Option<String>,
|
||||
/// The system event that occurred, if applicable
|
||||
pub system_message: Option<String>,
|
||||
/// The system message target
|
||||
pub system_message_target: Option<String>,
|
||||
/// The system message raw context
|
||||
pub system_message_context: Option<String>,
|
||||
},
|
||||
"PartialAdminComment"
|
||||
}
|
||||
|
||||
impl AdminComment {
|
||||
pub fn new(
|
||||
object_id: &str,
|
||||
user_id: &str,
|
||||
content: &str,
|
||||
case_id: Option<&str>,
|
||||
) -> AdminComment {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminComment {
|
||||
id,
|
||||
object_id: object_id.to_string(),
|
||||
case_id: case_id.map(|c| c.to_string()),
|
||||
user_id: user_id.to_string(),
|
||||
edited_at: None,
|
||||
content: Some(content.to_string()),
|
||||
system_message: None,
|
||||
system_message_context: None,
|
||||
system_message_target: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_system_message(
|
||||
object_id: &str,
|
||||
user_id: &str,
|
||||
case_id: &str,
|
||||
system_message_kind: v0::AdminAuditItemActions,
|
||||
context: Option<&str>,
|
||||
target: &str,
|
||||
) -> AdminComment {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminComment {
|
||||
id,
|
||||
object_id: object_id.to_string(),
|
||||
case_id: Some(case_id.to_string()),
|
||||
user_id: user_id.to_string(),
|
||||
edited_at: None,
|
||||
content: None,
|
||||
system_message: Some(serde_json::to_string(&system_message_kind).unwrap()), // if this explodes i'll eat my hat.
|
||||
system_message_context: context.map(|c| c.to_string()),
|
||||
system_message_target: Some(target.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit the comment, updating the edited_at time as well
|
||||
pub fn edit(&mut self, content: &str) {
|
||||
self.content = Some(content.to_string());
|
||||
self.edited_at = Some(
|
||||
iso8601_timestamp::Timestamp::now_utc()
|
||||
.format_short()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialAdminComment {
|
||||
pub fn new() -> PartialAdminComment {
|
||||
PartialAdminComment::default()
|
||||
}
|
||||
/// Edit the comment, updating the edited_at time as well
|
||||
pub fn edit(&mut self, content: &str) {
|
||||
self.content = Some(content.to_string());
|
||||
self.edited_at = Some(
|
||||
iso8601_timestamp::Timestamp::now_utc()
|
||||
.format_short()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
crates/core/database/src/models/admin_comments/ops.rs
Normal file
28
crates/core/database/src/models/admin_comments/ops.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{models::admin_comments::AdminComment, PartialAdminComment};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminComments: Sync + Send {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()>;
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment>;
|
||||
|
||||
/// Fetch all comments related to the case. This includes comments made on other objects.
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>>;
|
||||
|
||||
/// Fetch all comments on an object
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>>;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminComment, PartialAdminComment};
|
||||
|
||||
use super::AbstractAdminComments;
|
||||
|
||||
static COL: &str = "admin_comments";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminComments for MongoDb {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> {
|
||||
query!(self, insert_one, COL, comment).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
comment_id,
|
||||
partial,
|
||||
vec![],
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment> {
|
||||
query!(self, find_one, COL, doc! {"_id": id})?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>> {
|
||||
query!(self, find, COL, doc! {"case_id": case_id})
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>> {
|
||||
query!(self, find, COL, doc! {"object_id": object_id})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminComment, PartialAdminComment};
|
||||
|
||||
use super::AbstractAdminComments;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminComments for ReferenceDb {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> {
|
||||
let mut admin_comments = self.admin_comments.lock().await;
|
||||
if admin_comments.contains_key(&comment.id) {
|
||||
Err(create_database_error!("insert", "admin_comments"))
|
||||
} else {
|
||||
admin_comments.insert(comment.id.to_string(), comment.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()> {
|
||||
let mut admin_comments = self.admin_comments.lock().await;
|
||||
if let Some(comment) = admin_comments.get_mut(comment_id) {
|
||||
comment.apply_options(partial.clone());
|
||||
comment.edited_at = Some(Timestamp::now_utc().format().to_string());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
admin_comments
|
||||
.get(id)
|
||||
.map_or(Err(create_error!(NotFound)), |ac| Ok(ac.clone()))
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
Ok(admin_comments
|
||||
.iter()
|
||||
.filter_map(|(_, c)| {
|
||||
if c.case_id.as_ref().is_some_and(|c| c == case_id) {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
Ok(admin_comments
|
||||
.iter()
|
||||
.filter_map(|(_, c)| {
|
||||
if c.object_id == object_id {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,20 @@ impl AbstractMigrations for MongoDb {
|
||||
init::create_database(self).await;
|
||||
}
|
||||
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
if config.features.admin_api_enabled {
|
||||
let db = self.db();
|
||||
let colls = db
|
||||
.list_collection_names()
|
||||
.await
|
||||
.expect("Failed to fetch collection names.");
|
||||
if !colls.iter().any(|x| x == "admin_audits") {
|
||||
info!("You've enabled the admin api for the first time. Setting up database...");
|
||||
init::create_admin_database(&db).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ pub async fn create_database(db: &MongoDb) {
|
||||
info!("Creating database.");
|
||||
let db = db.db();
|
||||
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
db.create_collection("accounts")
|
||||
.await
|
||||
.expect("Failed to create accounts collection.");
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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(¬e.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)>>;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user