Add report + moderation messaging (#567)

* Add report + moderation messaging

* Add system messages

* address review comments

* Remove ds store

* Update messaging

* run prep

---------

Co-authored-by: Geometrically <geometrically@Jais-MacBook-Pro.local>
This commit is contained in:
Geometrically
2023-04-12 17:59:43 -07:00
committed by GitHub
parent 7605df1bd9
commit 8f61e9876f
26 changed files with 2005 additions and 2180 deletions

View File

@@ -9,6 +9,7 @@ mod reports;
mod statistics;
mod tags;
mod teams;
mod threads;
mod users;
mod version_creation;
mod version_file;
@@ -30,6 +31,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
.configure(statistics::config)
.configure(tags::config)
.configure(teams::config)
.configure(threads::config)
.configure(users::config)
.configure(version_file::config)
.configure(versions::config),

View File

@@ -1,11 +1,13 @@
use super::version_creation::InitialVersionData;
use crate::database::models;
use crate::database::models::thread_item::ThreadBuilder;
use crate::file_hosting::{FileHost, FileHostingError};
use crate::models::error::ApiError;
use crate::models::projects::{
DonationLink, License, ProjectId, ProjectStatus, SideType, VersionId,
VersionStatus,
};
use crate::models::threads::ThreadType;
use crate::models::users::UserId;
use crate::search::indexing::IndexingError;
use crate::util::auth::{get_user_from_headers, AuthenticationError};
@@ -464,8 +466,8 @@ async fn project_create_inner(
project_create_data = create_data;
}
let project_type_id = models::ProjectTypeId::get_id(
project_create_data.project_type.clone(),
let project_type_id = models::categories::ProjectType::get_id(
project_create_data.project_type.as_str(),
&mut *transaction,
)
.await?
@@ -698,8 +700,8 @@ async fn project_create_inner(
)));
}
let client_side_id = models::SideTypeId::get_id(
&project_create_data.client_side,
let client_side_id = models::categories::SideType::get_id(
project_create_data.client_side.as_str(),
&mut *transaction,
)
.await?
@@ -709,8 +711,8 @@ async fn project_create_inner(
)
})?;
let server_side_id = models::SideTypeId::get_id(
&project_create_data.server_side,
let server_side_id = models::categories::SideType::get_id(
project_create_data.server_side.as_str(),
&mut *transaction,
)
.await?
@@ -733,7 +735,7 @@ async fn project_create_inner(
if let Some(urls) = &project_create_data.donation_urls {
for url in urls {
let platform_id = models::DonationPlatformId::get_id(
let platform_id = models::categories::DonationPlatform::get_id(
&url.id,
&mut *transaction,
)
@@ -754,6 +756,13 @@ async fn project_create_inner(
}
}
let thread_id = ThreadBuilder {
type_: ThreadType::Project,
members: vec![],
}
.insert(&mut *transaction)
.await?;
let project_builder = models::project_item::ProjectBuilder {
project_id: project_id.into(),
project_type_id,
@@ -790,6 +799,7 @@ async fn project_create_inner(
})
.collect(),
color: icon_data.and_then(|x| x.1),
thread_id,
};
let now = Utc::now();
@@ -838,6 +848,7 @@ async fn project_create_inner(
flame_anvil_project: None,
flame_anvil_user: None,
color: project_builder.color,
thread_id: Some(project_builder.thread_id.into()),
};
let _project_id = project_builder.insert(&mut *transaction).await?;

View File

@@ -1,5 +1,6 @@
use crate::database;
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::thread_item::ThreadMessageBuilder;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::ids::base62_impl::parse_base62;
@@ -7,6 +8,7 @@ use crate::models::projects::{
DonationLink, Project, ProjectId, ProjectStatus, SearchRequest, SideType,
};
use crate::models::teams::Permissions;
use crate::models::threads::MessageBody;
use crate::routes::ApiError;
use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::{
@@ -45,10 +47,12 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.service(project_unfollow)
.service(project_schedule)
.service(super::teams::team_members_get_project)
.service(web::scope("{project_id}")
.service(super::versions::version_list)
.service(super::versions::version_project_get)
.service(dependency_list)),
.service(
web::scope("{project_id}")
.service(super::versions::version_list)
.service(super::versions::version_project_get)
.service(dependency_list),
),
);
}
@@ -160,7 +164,7 @@ pub async fn project_get_check(
) -> Result<HttpResponse, ApiError> {
let slug = info.into_inner().0;
let id_option = models::ids::base62_impl::parse_base62(&slug).ok();
let id_option = parse_base62(&slug).ok();
let id = if let Some(id) = id_option {
let id = sqlx::query!(
@@ -315,8 +319,7 @@ pub async fn dependency_list(
}
}
/// A project returned from the API
#[derive(Serialize, Deserialize, Validate)]
#[derive(Deserialize, Validate)]
pub struct EditProject {
#[validate(
length(min = 3, max = 64),
@@ -634,6 +637,20 @@ pub async fn project_edit(
.await?;
}
if let Some(thread) = project_item.inner.thread_id {
ThreadMessageBuilder {
author_id: Some(user.id.into()),
body: MessageBody::StatusChange {
new_status: *status,
old_status: project_item.inner.status,
},
thread_id: thread,
show_in_mod_inbox: None,
}
.insert(&mut transaction)
.await?;
}
sqlx::query!(
"
UPDATE mods
@@ -916,7 +933,7 @@ pub async fn project_edit(
// We are able to unwrap here because the slug is always set
if !slug.eq(&project_item.inner.slug.unwrap_or_default()) {
let results = sqlx::query!(
"
"
SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1))
",
slug
@@ -953,12 +970,13 @@ pub async fn project_edit(
));
}
let side_type_id = database::models::SideTypeId::get_id(
new_side,
&mut *transaction,
)
.await?
.expect("No database entry found for side type");
let side_type_id =
database::models::categories::SideType::get_id(
new_side.as_str(),
&mut *transaction,
)
.await?
.expect("No database entry found for side type");
sqlx::query!(
"
@@ -981,12 +999,13 @@ pub async fn project_edit(
));
}
let side_type_id = database::models::SideTypeId::get_id(
new_side,
&mut *transaction,
)
.await?
.expect("No database entry found for side type");
let side_type_id =
database::models::categories::SideType::get_id(
new_side.as_str(),
&mut *transaction,
)
.await?
.expect("No database entry found for side type");
sqlx::query!(
"
@@ -1054,7 +1073,7 @@ pub async fn project_edit(
for donation in donations {
let platform_id =
database::models::DonationPlatformId::get_id(
database::models::categories::DonationPlatform::get_id(
&donation.id,
&mut *transaction,
)

View File

@@ -1,21 +1,28 @@
use crate::database::models::thread_item::{
ThreadBuilder, ThreadMessageBuilder,
};
use crate::models::ids::{
base62_impl::parse_base62, ProjectId, UserId, VersionId,
};
use crate::models::reports::{ItemType, Report};
use crate::models::threads::{MessageBody, ThreadType};
use crate::routes::ApiError;
use crate::util::auth::{
check_is_moderator_from_headers, get_user_from_headers,
};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::StreamExt;
use serde::Deserialize;
use sqlx::PgPool;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(reports);
cfg.service(report_create);
cfg.service(delete_report);
cfg.service(report_edit);
cfg.service(report_delete);
cfg.service(report_get);
}
#[derive(Deserialize)]
@@ -60,6 +67,14 @@ pub async fn report_create(
new_report.report_type
))
})?;
let thread_id = ThreadBuilder {
type_: ThreadType::Report,
members: vec![],
}
.insert(&mut transaction)
.await?;
let mut report = crate::database::models::report_item::Report {
id,
report_type_id: report_type,
@@ -69,6 +84,8 @@ pub async fn report_create(
body: new_report.body.clone(),
reporter: current_user.id.into(),
created: Utc::now(),
closed: false,
thread_id,
};
match new_report.item_type {
@@ -150,44 +167,72 @@ pub async fn report_create(
reporter: current_user.id,
body: new_report.body.clone(),
created: Utc::now(),
closed: false,
thread_id: Some(report.thread_id.into()),
}))
}
#[derive(Deserialize)]
pub struct ResultCount {
pub struct ReportsRequestOptions {
#[serde(default = "default_count")]
count: i16,
#[serde(default = "default_all")]
all: bool,
}
fn default_count() -> i16 {
100
}
fn default_all() -> bool {
true
}
#[get("report")]
pub async fn reports(
req: HttpRequest,
pool: web::Data<PgPool>,
count: web::Query<ResultCount>,
count: web::Query<ReportsRequestOptions>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
use futures::stream::TryStreamExt;
let report_ids = sqlx::query!(
"
SELECT id FROM reports
ORDER BY created ASC
LIMIT $1;
",
count.count as i64
)
.fetch_many(&**pool)
.try_filter_map(|e| async {
Ok(e.right()
.map(|m| crate::database::models::ids::ReportId(m.id)))
})
.try_collect::<Vec<crate::database::models::ids::ReportId>>()
.await?;
let report_ids = if user.role.is_mod() && count.all {
sqlx::query!(
"
SELECT id FROM reports
WHERE closed = FALSE
ORDER BY created ASC
LIMIT $1;
",
count.count as i64
)
.fetch_many(&**pool)
.try_filter_map(|e| async {
Ok(e.right()
.map(|m| crate::database::models::ids::ReportId(m.id)))
})
.try_collect::<Vec<crate::database::models::ids::ReportId>>()
.await?
} else {
sqlx::query!(
"
SELECT id FROM reports
WHERE closed = FALSE AND reporter = $1
ORDER BY created ASC
LIMIT $2;
",
user.id.0 as i64,
count.count as i64
)
.fetch_many(&**pool)
.try_filter_map(|e| async {
Ok(e.right()
.map(|m| crate::database::models::ids::ReportId(m.id)))
})
.try_collect::<Vec<crate::database::models::ids::ReportId>>()
.await?
};
let query_reports = crate::database::models::report_item::Report::get_many(
&report_ids,
@@ -198,47 +243,130 @@ pub async fn reports(
let mut reports = Vec::new();
for x in query_reports {
let mut item_id = "".to_string();
let mut item_type = ItemType::Unknown;
if let Some(project_id) = x.project_id {
item_id = serde_json::to_string::<ProjectId>(&project_id.into())?;
item_type = ItemType::Project;
} else if let Some(version_id) = x.version_id {
item_id = serde_json::to_string::<VersionId>(&version_id.into())?;
item_type = ItemType::Version;
} else if let Some(user_id) = x.user_id {
item_id = serde_json::to_string::<UserId>(&user_id.into())?;
item_type = ItemType::User;
}
reports.push(Report {
id: x.id.into(),
report_type: x.report_type,
item_id,
item_type,
reporter: x.reporter.into(),
body: x.body,
created: x.created,
})
reports.push(to_report(x)?);
}
Ok(HttpResponse::Ok().json(reports))
}
#[get("report/{id}")]
pub async fn report_get(
req: HttpRequest,
pool: web::Data<PgPool>,
info: web::Path<(crate::models::reports::ReportId,)>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0.into();
let report =
crate::database::models::report_item::Report::get(id, &**pool).await?;
if let Some(report) = report {
if !user.role.is_mod() && report.user_id != Some(user.id.into()) {
return Ok(HttpResponse::NotFound().body(""));
}
Ok(HttpResponse::Ok().json(to_report(report)?))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[derive(Deserialize, Validate)]
pub struct EditReport {
#[validate(length(max = 65536))]
pub body: Option<String>,
pub closed: Option<bool>,
}
#[patch("report/{id}")]
pub async fn report_edit(
req: HttpRequest,
pool: web::Data<PgPool>,
info: web::Path<(crate::models::reports::ReportId,)>,
edit_report: web::Json<EditReport>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0.into();
let report =
crate::database::models::report_item::Report::get(id, &**pool).await?;
if let Some(report) = report {
if !user.role.is_mod() && report.user_id != Some(user.id.into()) {
return Ok(HttpResponse::NotFound().body(""));
}
let mut transaction = pool.begin().await?;
if let Some(edit_body) = &edit_report.body {
sqlx::query!(
"
UPDATE reports
SET body = $1
WHERE (id = $2)
",
edit_body,
id as crate::database::models::ids::ReportId,
)
.execute(&mut *transaction)
.await?;
}
if let Some(edit_closed) = edit_report.closed {
if report.closed && !edit_closed && !user.role.is_mod() {
return Err(ApiError::InvalidInput(
"You cannot reopen a report!".to_string(),
));
}
if let Some(thread) = report.thread_id {
ThreadMessageBuilder {
author_id: Some(user.id.into()),
body: MessageBody::ThreadClosure,
thread_id: thread,
show_in_mod_inbox: None,
}
.insert(&mut transaction)
.await?;
}
sqlx::query!(
"
UPDATE reports
SET closed = $1
WHERE (id = $2)
",
edit_closed,
id as crate::database::models::ids::ReportId,
)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[delete("report/{id}")]
pub async fn delete_report(
pub async fn report_delete(
req: HttpRequest,
pool: web::Data<PgPool>,
info: web::Path<(crate::models::reports::ReportId,)>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
let mut transaction = pool.begin().await?;
let result = crate::database::models::report_item::Report::remove_full(
info.into_inner().0.into(),
&**pool,
&mut transaction,
)
.await?;
transaction.commit().await?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
@@ -246,3 +374,33 @@ pub async fn delete_report(
Ok(HttpResponse::NotFound().body(""))
}
}
fn to_report(
x: crate::database::models::report_item::QueryReport,
) -> Result<Report, ApiError> {
let mut item_id = "".to_string();
let mut item_type = ItemType::Unknown;
if let Some(project_id) = x.project_id {
item_id = serde_json::to_string::<ProjectId>(&project_id.into())?;
item_type = ItemType::Project;
} else if let Some(version_id) = x.version_id {
item_id = serde_json::to_string::<VersionId>(&version_id.into())?;
item_type = ItemType::Version;
} else if let Some(user_id) = x.user_id {
item_id = serde_json::to_string::<UserId>(&user_id.into())?;
item_type = ItemType::User;
}
Ok(Report {
id: x.id.into(),
report_type: x.report_type,
item_id,
item_type,
reporter: x.reporter.into(),
body: x.body,
created: x.created,
closed: x.closed,
thread_id: x.thread_id.map(|x| x.into()),
})
}

View File

@@ -1,10 +1,9 @@
use super::ApiError;
use crate::database::models;
use crate::database::models::categories::{
DonationPlatform, ProjectType, ReportType,
DonationPlatform, ProjectType, ReportType, SideType,
};
use crate::util::auth::check_is_admin_from_headers;
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
use actix_web::{get, web, HttpResponse};
use chrono::{DateTime, Utc};
use models::categories::{Category, GameVersion, Loader};
use sqlx::PgPool;
@@ -13,22 +12,14 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("tag")
.service(category_list)
.service(category_create)
.service(category_delete)
.service(loader_list)
.service(loader_create)
.service(loader_delete)
.service(game_version_list)
.service(game_version_create)
.service(game_version_delete)
.service(license_list)
.service(license_text)
.service(donation_platform_create)
.service(donation_platform_list)
.service(donation_platform_delete)
.service(report_type_create)
.service(report_type_delete)
.service(report_type_list),
.service(report_type_list)
.service(project_type_list)
.service(side_type_list),
);
}
@@ -60,62 +51,6 @@ pub async fn category_list(
Ok(HttpResponse::Ok().json(results))
}
#[put("category")]
pub async fn category_create(
req: HttpRequest,
pool: web::Data<PgPool>,
new_category: web::Json<CategoryData>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let project_type = crate::database::models::ProjectTypeId::get_id(
new_category.project_type.clone(),
&**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"Specified project type does not exist!".to_string(),
)
})?;
let _id = Category::builder()
.name(&new_category.name)?
.project_type(&project_type)?
.icon(&new_category.icon)?
.header(&new_category.header)?
.insert(&**pool)
.await?;
Ok(HttpResponse::NoContent().body(""))
}
#[delete("category/{name}")]
pub async fn category_delete(
req: HttpRequest,
pool: web::Data<PgPool>,
category: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = category.into_inner().0;
let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = Category::remove(&name, &mut transaction).await?;
transaction
.commit()
.await
.map_err(models::DatabaseError::from)?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct LoaderData {
icon: String,
@@ -142,62 +77,6 @@ pub async fn loader_list(
Ok(HttpResponse::Ok().json(results))
}
#[put("loader")]
pub async fn loader_create(
req: HttpRequest,
pool: web::Data<PgPool>,
new_loader: web::Json<LoaderData>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let mut transaction = pool.begin().await?;
let project_types = ProjectType::get_many_id(
&new_loader.supported_project_types,
&mut *transaction,
)
.await?;
let _id = Loader::builder()
.name(&new_loader.name)?
.icon(&new_loader.icon)?
.supported_project_types(
&project_types.into_iter().map(|x| x.id).collect::<Vec<_>>(),
)?
.insert(&mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}
#[delete("loader/{name}")]
pub async fn loader_delete(
req: HttpRequest,
pool: web::Data<PgPool>,
loader: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = loader.into_inner().0;
let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = Loader::remove(&name, &mut transaction).await?;
transaction
.commit()
.await
.map_err(models::DatabaseError::from)?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[derive(serde::Serialize)]
pub struct GameVersionQueryData {
pub version: String,
@@ -238,66 +117,6 @@ pub async fn game_version_list(
Ok(HttpResponse::Ok().json(results))
}
#[derive(serde::Deserialize)]
pub struct GameVersionData {
#[serde(rename = "type")]
type_: String,
date: Option<DateTime<Utc>>,
}
#[put("game_version/{name}")]
pub async fn game_version_create(
req: HttpRequest,
pool: web::Data<PgPool>,
game_version: web::Path<(String,)>,
version_data: web::Json<GameVersionData>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = game_version.into_inner().0;
// The version type currently isn't limited, but it should be one of:
// "release", "snapshot", "alpha", "beta", "other"
let mut builder = GameVersion::builder()
.version(&name)?
.version_type(&version_data.type_)?;
if let Some(date) = &version_data.date {
builder = builder.created(date);
}
let _id = builder.insert(&**pool).await?;
Ok(HttpResponse::NoContent().body(""))
}
#[delete("game_version/{name}")]
pub async fn game_version_delete(
req: HttpRequest,
pool: web::Data<PgPool>,
game_version: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = game_version.into_inner().0;
let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = GameVersion::remove(&name, &mut transaction).await?;
transaction
.commit()
.await
.map_err(models::DatabaseError::from)?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[derive(serde::Serialize)]
pub struct License {
short: String,
@@ -372,57 +191,6 @@ pub async fn donation_platform_list(
Ok(HttpResponse::Ok().json(results))
}
#[derive(serde::Deserialize)]
pub struct DonationPlatformData {
name: String,
}
#[put("donation_platform/{name}")]
pub async fn donation_platform_create(
req: HttpRequest,
pool: web::Data<PgPool>,
license: web::Path<(String,)>,
license_data: web::Json<DonationPlatformData>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let short = license.into_inner().0;
let _id = DonationPlatform::builder()
.short(&short)?
.name(&license_data.name)?
.insert(&**pool)
.await?;
Ok(HttpResponse::NoContent().body(""))
}
#[delete("donation_platform/{name}")]
pub async fn donation_platform_delete(
req: HttpRequest,
pool: web::Data<PgPool>,
loader: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = loader.into_inner().0;
let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = DonationPlatform::remove(&name, &mut transaction).await?;
transaction
.commit()
.await
.map_err(models::DatabaseError::from)?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[get("report_type")]
pub async fn report_type_list(
pool: web::Data<PgPool>,
@@ -431,43 +199,18 @@ pub async fn report_type_list(
Ok(HttpResponse::Ok().json(results))
}
#[put("report_type/{name}")]
pub async fn report_type_create(
req: HttpRequest,
#[get("project_type")]
pub async fn project_type_list(
pool: web::Data<PgPool>,
loader: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = loader.into_inner().0;
let _id = ReportType::builder().name(&name)?.insert(&**pool).await?;
Ok(HttpResponse::NoContent().body(""))
let results = ProjectType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results))
}
#[delete("report_type/{name}")]
pub async fn report_type_delete(
req: HttpRequest,
#[get("side_type")]
pub async fn side_type_list(
pool: web::Data<PgPool>,
report_type: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = report_type.into_inner().0;
let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = ReportType::remove(&name, &mut transaction).await?;
transaction
.commit()
.await
.map_err(models::DatabaseError::from)?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
let results = SideType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results))
}

278
src/routes/v2/threads.rs Normal file
View File

@@ -0,0 +1,278 @@
use crate::database;
use crate::database::models::thread_item::ThreadMessageBuilder;
use crate::models::ids::ThreadMessageId;
use crate::models::projects::ProjectStatus;
use crate::models::threads::{
MessageBody, Thread, ThreadId, ThreadMessage, ThreadType,
};
use crate::models::users::User;
use crate::routes::ApiError;
use crate::util::auth::{
check_is_moderator_from_headers, get_user_from_headers,
};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("thread")
.service(moderation_inbox)
.service(thread_get)
.service(thread_send_message),
);
cfg.service(web::scope("message").service(message_delete));
}
pub async fn is_authorized_thread(
thread: &database::models::Thread,
user: &User,
pool: &PgPool,
) -> Result<bool, ApiError> {
if user.role.is_mod() {
return Ok(true);
}
let user_id: database::models::UserId = user.id.into();
Ok(match thread.type_ {
ThreadType::Report => {
let report_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)",
thread.id as database::models::ids::ThreadId,
user_id as database::models::ids::UserId,
)
.fetch_one(pool)
.await?
.exists;
report_exists.unwrap_or(false)
}
ThreadType::Project => {
let project_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 WHERE thread_id = $1)",
thread.id as database::models::ids::ThreadId,
user_id as database::models::ids::UserId,
)
.fetch_one(pool)
.await?
.exists;
project_exists.unwrap_or(false)
}
ThreadType::DirectMessage => thread.members.contains(&user_id),
})
}
#[get("{id}")]
pub async fn thread_get(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0.into();
let thread_data = database::models::Thread::get(string, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
if let Some(data) = thread_data {
if is_authorized_thread(&data, &user, &pool).await? {
let users: Vec<User> = database::models::User::get_many(
&data
.messages
.iter()
.filter_map(|x| x.author_id)
.collect::<Vec<_>>(),
&**pool,
)
.await?
.into_iter()
.map(From::from)
.collect();
let thread_type = data.type_;
return Ok(HttpResponse::Ok().json(Thread {
id: data.id.into(),
type_: thread_type,
messages: data
.messages
.into_iter()
.map(|x| ThreadMessage {
id: x.id.into(),
author_id: if thread_type == ThreadType::Report
&& users
.iter()
.find(|y| x.author_id == Some(y.id.into()))
.map(|x| x.role.is_mod())
.unwrap_or(false)
{
None
} else {
x.author_id.map(|x| x.into())
},
body: x.body,
created: x.created,
})
.collect(),
members: users,
}));
}
}
Ok(HttpResponse::NotFound().body(""))
}
#[derive(Deserialize)]
pub struct NewThreadMessage {
pub body: MessageBody,
}
#[post("{id}")]
pub async fn thread_send_message(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
pool: web::Data<PgPool>,
new_message: web::Json<NewThreadMessage>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
if let MessageBody::Text { body } = &new_message.body {
if body.len() > 65536 {
return Err(ApiError::InvalidInput(
"Input body is too long!".to_string(),
));
}
}
let string: database::models::ThreadId = info.into_inner().0.into();
let result = database::models::Thread::get(string, &**pool).await?;
if let Some(thread) = result {
if !is_authorized_thread(&thread, &user, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
let mod_notif = if thread.type_ == ThreadType::Project {
let status = sqlx::query!(
"SELECT m.status FROM mods m WHERE thread_id = $1",
thread.id as database::models::ids::ThreadId,
)
.fetch_one(&**pool)
.await?;
let status = ProjectStatus::from_str(&status.status);
status == ProjectStatus::Processing && !user.role.is_mod()
} else {
false
};
let mut transaction = pool.begin().await?;
ThreadMessageBuilder {
author_id: Some(user.id.into()),
body: new_message.body.clone(),
thread_id: thread.id,
show_in_mod_inbox: Some(mod_notif),
}
.insert(&mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[get("inbox")]
pub async fn moderation_inbox(
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
let messages = sqlx::query!(
"
SELECT tm.id, tm.thread_id, tm.author_id, tm.body, tm.created, m.id project_id FROM threads_messages tm
INNER JOIN mods m ON m.thread_id = tm.thread_id
WHERE tm.show_in_mod_inbox = TRUE
"
)
.fetch_all(&**pool)
.await?
.into_iter()
.map(|x| serde_json::json! ({
"message": ThreadMessage {
id: ThreadMessageId(x.id as u64),
author_id: x.author_id.map(|x| crate::models::users::UserId(x as u64)),
body: serde_json::from_value(x.body).unwrap_or(MessageBody::Deleted),
created: x.created
},
"project_id": crate::models::projects::ProjectId(x.project_id as u64),
}))
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(messages))
}
#[post("{id}/read")]
pub async fn read_message(
req: HttpRequest,
info: web::Path<(ThreadMessageId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE threads_messages
SET show_in_mod_inbox = FALSE
WHERE id = $1
",
id.0 as i64,
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}
#[delete("{id}")]
pub async fn message_delete(
req: HttpRequest,
info: web::Path<(ThreadMessageId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let result = database::models::ThreadMessage::get(
info.into_inner().0.into(),
&**pool,
)
.await?;
if let Some(thread) = result {
if !user.role.is_mod() && thread.author_id != Some(user.id.into()) {
return Err(ApiError::CustomAuthentication(
"You cannot delete this message!".to_string(),
));
}
let mut transaction = pool.begin().await?;
database::models::ThreadMessage::remove_full(
thread.id,
&mut transaction,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}

View File

@@ -203,11 +203,8 @@ pub async fn user_edit(
ApiError::Validation(validation_errors_to_string(err, None))
})?;
let id_option = crate::database::models::User::get_id_from_username_or_id(
&info.into_inner().0,
&**pool,
)
.await?;
let id_option =
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
let user_id: UserId = id.into();
@@ -217,10 +214,7 @@ pub async fn user_edit(
if let Some(username) = &new_user.username {
let existing_user_id_option =
crate::database::models::User::get_id_from_username_or_id(
username, &**pool,
)
.await?;
User::get_id_from_username_or_id(username, &**pool).await?;
if existing_user_id_option
.map(UserId::from)
@@ -754,6 +748,8 @@ pub async fn user_payouts_request(
data: web::Json<PayoutData>,
payouts_queue: web::Data<Arc<Mutex<PayoutsQueue>>>,
) -> Result<HttpResponse, ApiError> {
let mut payouts_queue = payouts_queue.lock().await;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option =
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
@@ -775,8 +771,6 @@ pub async fn user_payouts_request(
return if data.amount < payouts_data.balance {
let mut transaction = pool.begin().await?;
let mut payouts_queue = payouts_queue.lock().await;
let leftover = payouts_queue
.send_payout(PayoutItem {
amount: PayoutAmount {

View File

@@ -1,7 +1,7 @@
use super::ApiError;
use crate::database::models::{version_item::QueryVersion, DatabaseError};
use crate::models::ids::VersionId;
use crate::models::projects::{GameVersion, Loader, Version};
use crate::models::projects::{GameVersion, Loader, Project, Version};
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers;
use crate::util::routes::ok_or_not_found;
@@ -404,6 +404,65 @@ pub async fn get_versions_from_hashes(
Ok(HttpResponse::Ok().json(response?))
}
#[post("project")]
pub async fn get_projects_from_hashes(
pool: web::Data<PgPool>,
file_data: web::Json<FileHashes>,
) -> Result<HttpResponse, ApiError> {
let hashes_parsed: Vec<Vec<u8>> = file_data
.hashes
.iter()
.map(|x| x.to_lowercase().as_bytes().to_vec())
.collect();
let result = sqlx::query!(
"
SELECT h.hash hash, h.algorithm algorithm, m.id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)
",
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
hashes_parsed.as_slice(),
file_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
)
.fetch_all(&**pool)
.await?;
let project_ids = result
.iter()
.map(|x| database::models::ProjectId(x.project_id))
.collect::<Vec<_>>();
let versions_data =
database::models::Project::get_many_full(&project_ids, &**pool).await?;
let response: Result<HashMap<String, Project>, ApiError> = result
.into_iter()
.filter_map(|row| {
versions_data
.clone()
.into_iter()
.find(|x| x.inner.id.0 == row.project_id)
.map(|v| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok((
parsed_hash,
crate::models::projects::Project::from(v),
))
} else {
Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}",
row.project_id
))))
}
})
})
.collect();
Ok(HttpResponse::Ok().json(response?))
}
#[post("download")]
pub async fn download_files(
pool: web::Data<PgPool>,