You've already forked AstralRinth
forked from didirus/AstralRinth
Reports (#165)
* Reports WIP * Finish reports * Clippy fixes Co-authored-by: Geometrically <geometrically@pop-os.localdomain>
This commit is contained in:
@@ -17,6 +17,11 @@ pub struct Category {
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
pub struct ReportType {
|
||||
pub id: ReportTypeId,
|
||||
pub report_type: String,
|
||||
}
|
||||
|
||||
pub struct License {
|
||||
pub id: LicenseId,
|
||||
pub short: String,
|
||||
@@ -354,22 +359,61 @@ impl GameVersion {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn list_type<'a, E>(version_type: &str, exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
pub async fn list_filter<'a, E>(
|
||||
version_type_option: Option<&str>,
|
||||
major_option: Option<bool>,
|
||||
exec: E,
|
||||
) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
WHERE type = $1
|
||||
ORDER BY created DESC
|
||||
",
|
||||
version_type
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
let result;
|
||||
|
||||
if let Some(version_type) = version_type_option {
|
||||
if let Some(major) = major_option {
|
||||
result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
WHERE major = $1 AND type = $2
|
||||
ORDER BY created DESC
|
||||
",
|
||||
major,
|
||||
version_type
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
} else {
|
||||
result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
WHERE type = $1
|
||||
ORDER BY created DESC
|
||||
",
|
||||
version_type
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
}
|
||||
} else if let Some(major) = major_option {
|
||||
result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
WHERE major = $1
|
||||
ORDER BY created DESC
|
||||
",
|
||||
major
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
} else {
|
||||
result = Vec::new();
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -755,3 +799,129 @@ impl<'a> DonationPlatformBuilder<'a> {
|
||||
Ok(DonationPlatformId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReportTypeBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl ReportType {
|
||||
pub fn builder() -> ReportTypeBuilder<'static> {
|
||||
ReportTypeBuilder { name: None }
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ReportTypeId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM report_types
|
||||
WHERE name = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| ReportTypeId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_name<'a, E>(id: ReportTypeId, exec: E) -> Result<String, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT name FROM report_types
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ReportTypeId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.name)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT name FROM report_types
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.name)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// TODO: remove loaders with mods using them
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM report_types
|
||||
WHERE name = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ReportTypeBuilder<'a> {
|
||||
/// The name of the report type. Must be ASCII alphanumeric or `-`/`_`
|
||||
pub fn name(self, name: &'a str) -> Result<ReportTypeBuilder<'a>, DatabaseError> {
|
||||
if name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
Ok(Self { name: Some(name) })
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<ReportTypeId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO report_types (name)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (name) DO NOTHING
|
||||
RETURNING id
|
||||
",
|
||||
self.name
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(ReportTypeId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,13 @@ generate_ids!(
|
||||
"SELECT EXISTS(SELECT 1 FROM users WHERE id=$1)",
|
||||
UserId
|
||||
);
|
||||
generate_ids!(
|
||||
pub generate_report_id,
|
||||
ReportId,
|
||||
8,
|
||||
"SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)",
|
||||
ReportId
|
||||
);
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Type)]
|
||||
#[sqlx(transparent)]
|
||||
@@ -130,6 +137,13 @@ pub struct LoaderId(pub i32);
|
||||
#[sqlx(transparent)]
|
||||
pub struct CategoryId(pub i32);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct ReportId(pub i64);
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct ReportTypeId(pub i32);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct FileId(pub i64);
|
||||
@@ -180,3 +194,13 @@ impl From<VersionId> for ids::VersionId {
|
||||
ids::VersionId(id.0 as u64)
|
||||
}
|
||||
}
|
||||
impl From<ids::ReportId> for ReportId {
|
||||
fn from(id: ids::ReportId) -> Self {
|
||||
ReportId(id.0 as i64)
|
||||
}
|
||||
}
|
||||
impl From<ReportId> for ids::ReportId {
|
||||
fn from(id: ReportId) -> Self {
|
||||
ids::ReportId(id.0 as u64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use thiserror::Error;
|
||||
pub mod categories;
|
||||
pub mod ids;
|
||||
pub mod mod_item;
|
||||
pub mod report_item;
|
||||
pub mod team_item;
|
||||
pub mod user_item;
|
||||
pub mod version_item;
|
||||
|
||||
@@ -300,6 +300,16 @@ impl Mod {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM reports
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods_categories
|
||||
@@ -453,13 +463,13 @@ impl Mod {
|
||||
categories: m
|
||||
.categories
|
||||
.unwrap_or_default()
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
versions: m
|
||||
.versions
|
||||
.unwrap_or_default()
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map(|x| VersionId(x.parse().unwrap_or_default()))
|
||||
.collect(),
|
||||
donation_urls: vec![],
|
||||
@@ -531,8 +541,8 @@ impl Mod {
|
||||
slug: m.slug.clone(),
|
||||
body: m.body.clone(),
|
||||
},
|
||||
categories: m.categories.unwrap_or_default().split(",").map(|x| x.to_string()).collect(),
|
||||
versions: m.versions.unwrap_or_default().split(",").map(|x| VersionId(x.parse().unwrap_or_default())).collect(),
|
||||
categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(),
|
||||
versions: m.versions.unwrap_or_default().split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect(),
|
||||
donation_urls: vec![],
|
||||
status: crate::models::mods::ModStatus::from_str(&m.status_name),
|
||||
license_id: m.short,
|
||||
|
||||
153
src/database/models/report_item.rs
Normal file
153
src/database/models/report_item.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use super::ids::*;
|
||||
|
||||
pub struct Report {
|
||||
pub id: ReportId,
|
||||
pub report_type_id: ReportTypeId,
|
||||
pub mod_id: Option<ModId>,
|
||||
pub version_id: Option<VersionId>,
|
||||
pub user_id: Option<UserId>,
|
||||
pub body: String,
|
||||
pub reporter: UserId,
|
||||
pub created: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
pub struct QueryReport {
|
||||
pub id: ReportId,
|
||||
pub report_type: String,
|
||||
pub mod_id: Option<ModId>,
|
||||
pub version_id: Option<VersionId>,
|
||||
pub user_id: Option<UserId>,
|
||||
pub body: String,
|
||||
pub reporter: UserId,
|
||||
pub created: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub async fn insert(
|
||||
&self,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), sqlx::error::Error> {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO reports (
|
||||
id, report_type_id, mod_id, version_id, user_id,
|
||||
body, reporter
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7
|
||||
)
|
||||
",
|
||||
self.id as ReportId,
|
||||
self.report_type_id as ReportTypeId,
|
||||
self.mod_id.map(|x| x.0 as i64),
|
||||
self.version_id.map(|x| x.0 as i64),
|
||||
self.user_id.map(|x| x.0 as i64),
|
||||
self.body,
|
||||
self.reporter as UserId
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a, E>(id: ReportId, exec: E) -> Result<Option<QueryReport>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created
|
||||
FROM reports r
|
||||
INNER JOIN report_types rt ON rt.id = r.report_type_id
|
||||
WHERE r.id = $1
|
||||
",
|
||||
id as ReportId,
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
Ok(Some(QueryReport {
|
||||
id,
|
||||
report_type: row.name,
|
||||
mod_id: row.mod_id.map(ModId),
|
||||
version_id: row.version_id.map(VersionId),
|
||||
user_id: row.user_id.map(UserId),
|
||||
body: row.body,
|
||||
reporter: UserId(row.reporter),
|
||||
created: row.created,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_many<'a, E>(
|
||||
version_ids: Vec<ReportId>,
|
||||
exec: E,
|
||||
) -> Result<Vec<QueryReport>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let version_ids_parsed: Vec<i64> = version_ids.into_iter().map(|x| x.0).collect();
|
||||
let versions = sqlx::query!(
|
||||
"
|
||||
SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created
|
||||
FROM reports r
|
||||
INNER JOIN report_types rt ON rt.id = r.report_type_id
|
||||
WHERE r.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
",
|
||||
&version_ids_parsed
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|row| QueryReport {
|
||||
id: ReportId(row.id),
|
||||
report_type: row.name,
|
||||
mod_id: row.mod_id.map(ModId),
|
||||
version_id: row.version_id.map(VersionId),
|
||||
user_id: row.user_id.map(UserId),
|
||||
body: row.body,
|
||||
reporter: UserId(row.reporter),
|
||||
created: row.created,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<QueryReport>>()
|
||||
.await?;
|
||||
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
pub async fn remove_full<'a, E>(id: ReportId, exec: E) -> Result<Option<()>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1)
|
||||
",
|
||||
id as ReportId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
if !result.exists.unwrap_or(false) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM reports WHERE id = $1
|
||||
",
|
||||
id as ReportId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
@@ -239,6 +239,16 @@ impl User {
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM reports
|
||||
WHERE user_id = $1
|
||||
",
|
||||
id as UserId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM team_members
|
||||
|
||||
@@ -217,6 +217,16 @@ impl Version {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM reports
|
||||
WHERE version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM game_versions_versions gvv
|
||||
@@ -569,13 +579,13 @@ impl Version {
|
||||
game_versions: v
|
||||
.game_versions
|
||||
.unwrap_or_default()
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
loaders: v
|
||||
.loaders
|
||||
.unwrap_or_default()
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
featured: v.featured,
|
||||
@@ -683,8 +693,8 @@ impl Version {
|
||||
downloads: v.downloads,
|
||||
release_channel: v.release_channel,
|
||||
files,
|
||||
game_versions: v.game_versions.unwrap_or_default().split(",").map(|x| x.to_string()).collect(),
|
||||
loaders: v.loaders.unwrap_or_default().split(",").map(|x| x.to_string()).collect(),
|
||||
game_versions: v.game_versions.unwrap_or_default().split(',').map(|x| x.to_string()).collect(),
|
||||
loaders: v.loaders.unwrap_or_default().split(',').map(|x| x.to_string()).collect(),
|
||||
featured: v.featured,
|
||||
dependencies,
|
||||
}
|
||||
@@ -714,6 +724,7 @@ pub struct FileHash {
|
||||
pub hash: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct QueryVersion {
|
||||
pub id: VersionId,
|
||||
pub mod_id: ModId,
|
||||
@@ -733,6 +744,7 @@ pub struct QueryVersion {
|
||||
pub dependencies: Vec<(VersionId, String)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct QueryFile {
|
||||
pub id: FileId,
|
||||
pub url: String,
|
||||
|
||||
Reference in New Issue
Block a user