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

@@ -10,6 +10,11 @@ pub struct ProjectType {
pub name: String,
}
pub struct SideType {
pub id: SideTypeId,
pub name: String,
}
pub struct Loader {
pub id: LoaderId,
pub loader: String,
@@ -46,23 +51,7 @@ pub struct DonationPlatform {
pub name: String,
}
pub struct CategoryBuilder<'a> {
pub name: Option<&'a str>,
pub project_type: Option<&'a ProjectTypeId>,
pub icon: Option<&'a str>,
pub header: Option<&'a str>,
}
impl Category {
pub fn builder() -> CategoryBuilder<'static> {
CategoryBuilder {
name: None,
project_type: None,
icon: None,
header: None,
}
}
pub async fn get_id<'a, E>(
name: &str,
exec: E,
@@ -105,26 +94,6 @@ impl Category {
Ok(result.map(|r| CategoryId(r.id)))
}
pub async fn get_name<'a, E>(
id: CategoryId,
exec: E,
) -> Result<String, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT category FROM categories
WHERE id = $1
",
id as CategoryId
)
.fetch_one(exec)
.await?;
Ok(result.category)
}
pub async fn list<'a, E>(exec: E) -> Result<Vec<Category>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
@@ -152,118 +121,9 @@ impl Category {
Ok(result)
}
pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
DELETE FROM categories
WHERE category = $1
",
name
)
.execute(exec)
.await?;
if result.rows_affected() == 0 {
// Nothing was deleted
Ok(None)
} else {
Ok(Some(()))
}
}
}
impl<'a> CategoryBuilder<'a> {
/// The name of the category. Must be ASCII alphanumeric or `-`/`_`
pub fn name(
self,
name: &'a str,
) -> Result<CategoryBuilder<'a>, DatabaseError> {
Ok(Self {
name: Some(name),
..self
})
}
pub fn header(
self,
header: &'a str,
) -> Result<CategoryBuilder<'a>, DatabaseError> {
Ok(Self {
header: Some(header),
..self
})
}
pub fn project_type(
self,
project_type: &'a ProjectTypeId,
) -> Result<CategoryBuilder<'a>, DatabaseError> {
Ok(Self {
project_type: Some(project_type),
..self
})
}
pub fn icon(
self,
icon: &'a str,
) -> Result<CategoryBuilder<'a>, DatabaseError> {
Ok(Self {
icon: Some(icon),
..self
})
}
pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<CategoryId, DatabaseError>
where
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{
let id = *self.project_type.ok_or_else(|| {
DatabaseError::Other("No project type specified.".to_string())
})?;
let result = sqlx::query!(
"
INSERT INTO categories (category, project_type, icon, header)
VALUES ($1, $2, $3, $4)
RETURNING id
",
self.name,
id as ProjectTypeId,
self.icon,
self.header
)
.fetch_one(exec)
.await?;
Ok(CategoryId(result.id))
}
}
pub struct LoaderBuilder<'a> {
pub name: Option<&'a str>,
pub icon: Option<&'a str>,
pub supported_project_types: Option<&'a [ProjectTypeId]>,
}
impl Loader {
pub fn builder() -> LoaderBuilder<'static> {
LoaderBuilder {
name: None,
icon: None,
supported_project_types: None,
}
}
pub async fn get_id<'a, E>(
name: &str,
exec: E,
@@ -284,26 +144,6 @@ impl Loader {
Ok(result.map(|r| LoaderId(r.id)))
}
pub async fn get_name<'a, E>(
id: LoaderId,
exec: E,
) -> Result<String, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT loader FROM loaders
WHERE id = $1
",
id as LoaderId
)
.fetch_one(exec)
.await?;
Ok(result.loader)
}
pub async fn list<'a, E>(exec: E) -> Result<Vec<Loader>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
@@ -337,110 +177,6 @@ impl Loader {
Ok(result)
}
// TODO: remove loaders with projects using them
pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
DELETE FROM loaders
WHERE loader = $1
",
name
)
.execute(exec)
.await?;
if result.rows_affected() == 0 {
// Nothing was deleted
Ok(None)
} else {
Ok(Some(()))
}
}
}
impl<'a> LoaderBuilder<'a> {
/// The name of the loader. Must be ASCII alphanumeric or `-`/`_`
pub fn name(
self,
name: &'a str,
) -> Result<LoaderBuilder<'a>, DatabaseError> {
Ok(Self {
name: Some(name),
..self
})
}
pub fn icon(
self,
icon: &'a str,
) -> Result<LoaderBuilder<'a>, DatabaseError> {
Ok(Self {
icon: Some(icon),
..self
})
}
pub fn supported_project_types(
self,
supported_project_types: &'a [ProjectTypeId],
) -> Result<LoaderBuilder<'a>, DatabaseError> {
Ok(Self {
supported_project_types: Some(supported_project_types),
..self
})
}
pub async fn insert(
self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<LoaderId, super::DatabaseError> {
let result = sqlx::query!(
"
INSERT INTO loaders (loader, icon)
VALUES ($1, $2)
ON CONFLICT (loader) DO NOTHING
RETURNING id
",
self.name,
self.icon
)
.fetch_one(&mut *transaction)
.await?;
if let Some(project_types) = self.supported_project_types {
sqlx::query!(
"
DELETE FROM loaders_project_types
WHERE joining_loader_id = $1
",
result.id
)
.execute(&mut *transaction)
.await?;
for project_type in project_types {
sqlx::query!(
"
INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)
VALUES ($1, $2)
",
result.id,
project_type.0,
)
.execute(&mut *transaction)
.await?;
}
}
Ok(LoaderId(result.id))
}
}
#[derive(Default)]
@@ -475,26 +211,6 @@ impl GameVersion {
Ok(result.map(|r| GameVersionId(r.id)))
}
pub async fn get_name<'a, E>(
id: GameVersionId,
exec: E,
) -> Result<String, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT version FROM game_versions
WHERE id = $1
",
id as GameVersionId
)
.fetch_one(exec)
.await?;
Ok(result.version)
}
pub async fn list<'a, E>(exec: E) -> Result<Vec<GameVersion>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
@@ -595,31 +311,6 @@ impl GameVersion {
Ok(result)
}
pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
DELETE FROM game_versions
WHERE version = $1
",
name
)
.execute(exec)
.await?;
if result.rows_affected() == 0 {
// Nothing was deleted
Ok(None)
} else {
Ok(Some(()))
}
}
}
impl<'a> GameVersionBuilder<'a> {
@@ -681,17 +372,7 @@ impl<'a> GameVersionBuilder<'a> {
}
}
#[derive(Default)]
pub struct DonationPlatformBuilder<'a> {
pub short: Option<&'a str>,
pub name: Option<&'a str>,
}
impl DonationPlatform {
pub fn builder() -> DonationPlatformBuilder<'static> {
DonationPlatformBuilder::default()
}
pub async fn get_id<'a, E>(
id: &str,
exec: E,
@@ -712,30 +393,6 @@ impl DonationPlatform {
Ok(result.map(|r| DonationPlatformId(r.id)))
}
pub async fn get<'a, E>(
id: DonationPlatformId,
exec: E,
) -> Result<DonationPlatform, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT short, name FROM donation_platforms
WHERE id = $1
",
id as DonationPlatformId
)
.fetch_one(exec)
.await?;
Ok(DonationPlatform {
id,
short: result.short,
name: result.name,
})
}
pub async fn list<'a, E>(
exec: E,
) -> Result<Vec<DonationPlatform>, DatabaseError>
@@ -760,89 +417,9 @@ impl DonationPlatform {
Ok(result)
}
pub async fn remove<'a, E>(
short: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
DELETE FROM donation_platforms
WHERE short = $1
",
short
)
.execute(exec)
.await?;
if result.rows_affected() == 0 {
// Nothing was deleted
Ok(None)
} else {
Ok(Some(()))
}
}
}
impl<'a> DonationPlatformBuilder<'a> {
/// The donation platform short name. Spaces must be replaced with '_' for it to be valid
pub fn short(
self,
short: &'a str,
) -> Result<DonationPlatformBuilder<'a>, DatabaseError> {
Ok(Self {
short: Some(short),
..self
})
}
/// The donation platform long name
pub fn name(
self,
name: &'a str,
) -> Result<DonationPlatformBuilder<'a>, DatabaseError> {
Ok(Self {
name: Some(name),
..self
})
}
pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<DonationPlatformId, DatabaseError>
where
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
INSERT INTO donation_platforms (short, name)
VALUES ($1, $2)
ON CONFLICT (short) DO NOTHING
RETURNING id
",
self.short,
self.name,
)
.fetch_one(exec)
.await?;
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,
@@ -863,26 +440,6 @@ impl ReportType {
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>,
@@ -899,74 +456,9 @@ impl ReportType {
Ok(result)
}
pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
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> {
Ok(Self { name: Some(name) })
}
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))
}
}
pub struct ProjectTypeBuilder<'a> {
pub name: Option<&'a str>,
}
impl ProjectType {
pub fn builder() -> ProjectTypeBuilder<'static> {
ProjectTypeBuilder { name: None }
}
pub async fn get_id<'a, E>(
name: &str,
exec: E,
@@ -987,53 +479,6 @@ impl ProjectType {
Ok(result.map(|r| ProjectTypeId(r.id)))
}
pub async fn get_many_id<'a, E>(
names: &[String],
exec: E,
) -> Result<Vec<ProjectType>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let project_types = sqlx::query!(
"
SELECT id, name FROM project_types
WHERE name = ANY($1)
",
names
)
.fetch_many(exec)
.try_filter_map(|e| async {
Ok(e.right().map(|x| ProjectType {
id: ProjectTypeId(x.id),
name: x.name,
}))
})
.try_collect::<Vec<ProjectType>>()
.await?;
Ok(project_types)
}
pub async fn get_name<'a, E>(
id: ProjectTypeId,
exec: E,
) -> Result<String, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT name FROM project_types
WHERE id = $1
",
id as ProjectTypeId
)
.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>,
@@ -1050,62 +495,43 @@ impl ProjectType {
Ok(result)
}
}
// TODO: remove loaders with mods using them
pub async fn remove<'a, E>(
impl SideType {
pub async fn get_id<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
) -> Result<Option<SideTypeId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
DELETE FROM project_types
SELECT id FROM side_types
WHERE name = $1
",
name
)
.execute(exec)
.fetch_optional(exec)
.await?;
if result.rows_affected() == 0 {
// Nothing was deleted
Ok(None)
} else {
Ok(Some(()))
}
}
}
impl<'a> ProjectTypeBuilder<'a> {
/// The name of the project type. Must be ASCII alphanumeric or `-`/`_`
pub fn name(
self,
name: &'a str,
) -> Result<ProjectTypeBuilder<'a>, DatabaseError> {
Ok(Self { name: Some(name) })
Ok(result.map(|r| SideTypeId(r.id)))
}
pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<ProjectTypeId, DatabaseError>
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
where
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
INSERT INTO project_types (name)
VALUES ($1)
ON CONFLICT (name) DO NOTHING
RETURNING id
",
self.name
SELECT name FROM side_types
"
)
.fetch_one(exec)
.fetch_many(exec)
.try_filter_map(|e| async { Ok(e.right().map(|c| c.name)) })
.try_collect::<Vec<String>>()
.await?;
Ok(ProjectTypeId(result.id))
Ok(result)
}
}

View File

@@ -106,7 +106,22 @@ generate_ids!(
NotificationId
);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Type)]
generate_ids!(
pub generate_thread_id,
ThreadId,
8,
"SELECT EXISTS(SELECT 1 FROM threads WHERE id=$1)",
ThreadId
);
generate_ids!(
pub generate_thread_message_id,
ThreadMessageId,
8,
"SELECT EXISTS(SELECT 1 FROM threads_messages WHERE id=$1)",
ThreadMessageId
);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Type, Deserialize)]
#[sqlx(transparent)]
pub struct UserId(pub i64);
@@ -169,6 +184,13 @@ pub struct NotificationId(pub i64);
#[sqlx(transparent)]
pub struct NotificationActionId(pub i32);
#[derive(Copy, Clone, Debug, Type, Deserialize)]
#[sqlx(transparent)]
pub struct ThreadId(pub i64);
#[derive(Copy, Clone, Debug, Type, Deserialize)]
#[sqlx(transparent)]
pub struct ThreadMessageId(pub i64);
use crate::models::ids;
impl From<ids::ProjectId> for ProjectId {
@@ -231,3 +253,23 @@ impl From<NotificationId> for ids::NotificationId {
ids::NotificationId(id.0 as u64)
}
}
impl From<ids::ThreadId> for ThreadId {
fn from(id: ids::ThreadId) -> Self {
ThreadId(id.0 as i64)
}
}
impl From<ThreadId> for ids::ThreadId {
fn from(id: ThreadId) -> Self {
ids::ThreadId(id.0 as u64)
}
}
impl From<ids::ThreadMessageId> for ThreadMessageId {
fn from(id: ids::ThreadMessageId) -> Self {
ThreadMessageId(id.0 as i64)
}
}
impl From<ThreadMessageId> for ids::ThreadMessageId {
fn from(id: ThreadMessageId) -> Self {
ids::ThreadMessageId(id.0 as u64)
}
}

View File

@@ -1,7 +1,3 @@
#![allow(dead_code)]
// TODO: remove attr once routes are created
use chrono::{DateTime, Utc};
use thiserror::Error;
pub mod categories;
@@ -10,6 +6,7 @@ pub mod notification_item;
pub mod project_item;
pub mod report_item;
pub mod team_item;
pub mod thread_item;
pub mod user_item;
pub mod version_item;
@@ -17,6 +14,7 @@ pub use ids::*;
pub use project_item::Project;
pub use team_item::Team;
pub use team_item::TeamMember;
pub use thread_item::{Thread, ThreadMessage};
pub use user_item::User;
pub use version_item::Version;
@@ -28,82 +26,6 @@ pub enum DatabaseError {
RandomId,
#[error("A database request failed")]
Other(String),
}
impl ids::SideTypeId {
pub async fn get_id<'a, E>(
side: &crate::models::projects::SideType,
exec: E,
) -> Result<Option<Self>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id FROM side_types
WHERE name = $1
",
side.as_str()
)
.fetch_optional(exec)
.await?;
Ok(result.map(|r| ids::SideTypeId(r.id)))
}
}
impl ids::DonationPlatformId {
pub async fn get_id<'a, E>(
id: &str,
exec: E,
) -> Result<Option<Self>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id FROM donation_platforms
WHERE short = $1
",
id
)
.fetch_optional(exec)
.await?;
Ok(result.map(|r| ids::DonationPlatformId(r.id)))
}
}
impl ids::ProjectTypeId {
pub async fn get_id<'a, E>(
project_type: String,
exec: E,
) -> Result<Option<Self>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id FROM project_types
WHERE name = $1
",
project_type
)
.fetch_optional(exec)
.await?;
Ok(result.map(|r| ProjectTypeId(r.id)))
}
}
pub fn convert_postgres_date(input: &str) -> DateTime<Utc> {
let mut result = DateTime::parse_from_str(input, "%Y-%m-%d %T.%f%#z");
if result.is_err() {
result = DateTime::parse_from_str(input, "%Y-%m-%d %T%#z")
}
result
.map(|x| x.with_timezone(&Utc))
.unwrap_or_else(|_| Utc::now())
#[error("Error while parsing JSON: {0}")]
Json(#[from] serde_json::Error),
}

View File

@@ -101,6 +101,7 @@ pub struct ProjectBuilder {
pub donation_urls: Vec<DonationUrl>,
pub gallery_items: Vec<GalleryItem>,
pub color: Option<u32>,
pub thread_id: ThreadId,
}
impl ProjectBuilder {
@@ -146,6 +147,7 @@ impl ProjectBuilder {
color: self.color,
loaders: vec![],
game_versions: vec![],
thread_id: Some(self.thread_id),
};
project_struct.insert(&mut *transaction).await?;
@@ -230,6 +232,7 @@ pub struct Project {
pub color: Option<u32>,
pub loaders: Vec<String>,
pub game_versions: Vec<String>,
pub thread_id: Option<ThreadId>,
}
impl Project {
@@ -244,14 +247,14 @@ impl Project {
published, downloads, icon_url, issues_url,
source_url, wiki_url, status, requested_status, discord_url,
client_side, server_side, license_url, license,
slug, project_type, color
slug, project_type, color, thread_id
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9,
$10, $11, $12, $13, $14,
$15, $16, $17, $18,
LOWER($19), $20, $21
LOWER($19), $20, $21, $22
)
",
self.id as ProjectId,
@@ -274,7 +277,8 @@ impl Project {
&self.license,
self.slug.as_ref(),
self.project_type as ProjectTypeId,
self.color.map(|x| x as i32)
self.color.map(|x| x as i32),
self.thread_id.map(|x| x.0),
)
.execute(&mut *transaction)
.await?;
@@ -313,7 +317,7 @@ impl Project {
issues_url, source_url, wiki_url, discord_url, license_url,
team_id, client_side, server_side, license, slug,
moderation_message, moderation_message_body, flame_anvil_project,
flame_anvil_user, webhook_sent, color, loaders, game_versions
flame_anvil_user, webhook_sent, color, loaders, game_versions, thread_id
FROM mods
WHERE id = ANY($1)
",
@@ -359,6 +363,7 @@ impl Project {
loaders: m.loaders,
game_versions: m.game_versions,
queued: m.queued,
thread_id: m.thread_id.map(ThreadId),
}))
})
.try_collect::<Vec<Project>>()
@@ -386,6 +391,26 @@ impl Project {
return Ok(None);
};
let thread_id = sqlx::query!(
"
SELECT thread_id FROM mods
WHERE id = $1
",
id as ProjectId
)
.fetch_optional(&mut *transaction)
.await?;
if let Some(thread_id) = thread_id {
if let Some(id) = thread_id.thread_id {
crate::database::models::Thread::remove_full(
ThreadId(id),
transaction,
)
.await?;
}
}
sqlx::query!(
"
DELETE FROM mod_follows
@@ -654,7 +679,7 @@ impl Project {
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.flame_anvil_project flame_anvil_project, m.flame_anvil_user flame_anvil_user, m.webhook_sent, m.color,
m.loaders loaders, m.game_versions game_versions,
m.loaders loaders, m.game_versions game_versions, m.thread_id thread_id,
ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,
ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,
JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,
@@ -720,6 +745,7 @@ impl Project {
loaders: m.loaders,
game_versions: m.game_versions,
queued: m.queued,
thread_id: m.thread_id.map(ThreadId),
},
project_type: m.project_type_name,
categories: m.categories.unwrap_or_default(),

View File

@@ -10,6 +10,8 @@ pub struct Report {
pub body: String,
pub reporter: UserId,
pub created: DateTime<Utc>,
pub closed: bool,
pub thread_id: ThreadId,
}
pub struct QueryReport {
@@ -21,6 +23,8 @@ pub struct QueryReport {
pub body: String,
pub reporter: UserId,
pub created: DateTime<Utc>,
pub closed: bool,
pub thread_id: Option<ThreadId>,
}
impl Report {
@@ -32,11 +36,11 @@ impl Report {
"
INSERT INTO reports (
id, report_type_id, mod_id, version_id, user_id,
body, reporter
body, reporter, thread_id
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7
$6, $7, $8
)
",
self.id as ReportId,
@@ -45,7 +49,8 @@ impl Report {
self.version_id.map(|x| x.0 as i64),
self.user_id.map(|x| x.0 as i64),
self.body,
self.reporter as UserId
self.reporter as UserId,
self.thread_id as ThreadId,
)
.execute(&mut *transaction)
.await?;
@@ -78,7 +83,7 @@ impl Report {
report_ids.iter().map(|x| x.0).collect();
let reports = sqlx::query!(
"
SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created
SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, r.thread_id, r.closed
FROM reports r
INNER JOIN report_types rt ON rt.id = r.report_type_id
WHERE r.id = ANY($1)
@@ -97,6 +102,8 @@ impl Report {
body: x.body,
reporter: UserId(x.reporter),
created: x.created,
closed: x.closed,
thread_id: x.thread_id.map(ThreadId),
}))
})
.try_collect::<Vec<QueryReport>>()
@@ -105,33 +112,50 @@ impl Report {
Ok(reports)
}
pub async fn remove_full<'a, E>(
pub async fn remove_full(
id: ReportId,
exec: E,
) -> Result<Option<()>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> {
let result = sqlx::query!(
"
SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1)
",
id as ReportId
)
.fetch_one(exec)
.fetch_one(&mut *transaction)
.await?;
if !result.exists.unwrap_or(false) {
return Ok(None);
}
let thread_id = sqlx::query!(
"
SELECT thread_id FROM reports
WHERE id = $1
",
id as ReportId
)
.fetch_optional(&mut *transaction)
.await?;
if let Some(thread_id) = thread_id {
if let Some(id) = thread_id.thread_id {
crate::database::models::Thread::remove_full(
ThreadId(id),
transaction,
)
.await?;
}
}
sqlx::query!(
"
DELETE FROM reports WHERE id = $1
",
id as ReportId,
)
.execute(exec)
.execute(&mut *transaction)
.await?;
Ok(Some(()))

View File

@@ -104,53 +104,6 @@ pub struct QueryTeamMember {
}
impl TeamMember {
/// Lists the members of a team
pub async fn get_from_team<'a, 'b, E>(
id: TeamId,
executor: E,
) -> Result<Vec<TeamMember>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::stream::TryStreamExt;
let team_members = sqlx::query!(
"
SELECT id, user_id, role, permissions, accepted, payouts_split, ordering
FROM team_members
WHERE team_id = $1
ORDER BY ordering
",
id as TeamId,
)
.fetch_many(executor)
.try_filter_map(|e| async {
if let Some(m) = e.right() {
Ok(Some(Ok(TeamMember {
id: TeamMemberId(m.id),
team_id: id,
user_id: UserId(m.user_id),
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
ordering: m.ordering,
})))
} else {
Ok(None)
}
})
.try_collect::<Vec<Result<TeamMember, super::DatabaseError>>>()
.await?;
let team_members = team_members
.into_iter()
.collect::<Result<Vec<TeamMember>, super::DatabaseError>>()?;
Ok(team_members)
}
// Lists the full members of a team
pub async fn get_from_team_full<'a, 'b, E>(
id: TeamId,
@@ -232,100 +185,6 @@ impl TeamMember {
Ok(team_members)
}
/// Lists the team members for a user. Does not list pending requests.
pub async fn get_from_user_public<'a, 'b, E>(
id: UserId,
executor: E,
) -> Result<Vec<TeamMember>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::stream::TryStreamExt;
let team_members = sqlx::query!(
"
SELECT id, team_id, role, permissions, accepted, payouts_split, ordering
FROM team_members
WHERE (user_id = $1 AND accepted = TRUE)
ORDER BY ordering
",
id as UserId,
)
.fetch_many(executor)
.try_filter_map(|e| async {
if let Some(m) = e.right() {
Ok(Some(Ok(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id: id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
ordering: m.ordering,
})))
} else {
Ok(None)
}
})
.try_collect::<Vec<Result<TeamMember, super::DatabaseError>>>()
.await?;
let team_members = team_members
.into_iter()
.collect::<Result<Vec<TeamMember>, super::DatabaseError>>()?;
Ok(team_members)
}
/// Lists the team members for a user. Includes pending requests.
pub async fn get_from_user_private<'a, 'b, E>(
id: UserId,
executor: E,
) -> Result<Vec<TeamMember>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::stream::TryStreamExt;
let team_members = sqlx::query!(
"
SELECT id, team_id, role, permissions, accepted, payouts_split, ordering
FROM team_members
WHERE user_id = $1
ORDER BY ordering
",
id as UserId,
)
.fetch_many(executor)
.try_filter_map(|e| async {
if let Some(m) = e.right() {
Ok(Some(Ok(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id: id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
ordering: m.ordering,
})))
} else {
Ok(None)
}
})
.try_collect::<Vec<Result<TeamMember, super::DatabaseError>>>()
.await?;
let team_members = team_members
.into_iter()
.collect::<Result<Vec<TeamMember>, super::DatabaseError>>()?;
Ok(team_members)
}
/// Gets a team member from a user id and team id. Does not return pending members.
pub async fn get_from_user_id<'a, 'b, E>(
id: TeamId,

View File

@@ -0,0 +1,267 @@
use super::ids::*;
use crate::database::models::DatabaseError;
use crate::models::threads::{MessageBody, ThreadType};
use chrono::{DateTime, Utc};
use serde::Deserialize;
pub struct ThreadBuilder {
pub type_: ThreadType,
pub members: Vec<UserId>,
}
pub struct Thread {
pub id: ThreadId,
pub type_: ThreadType,
pub messages: Vec<ThreadMessage>,
pub members: Vec<UserId>,
}
pub struct ThreadMessageBuilder {
pub author_id: Option<UserId>,
pub body: MessageBody,
pub thread_id: ThreadId,
pub show_in_mod_inbox: Option<bool>,
}
#[derive(Deserialize)]
pub struct ThreadMessage {
pub id: ThreadMessageId,
pub thread_id: ThreadId,
pub author_id: Option<UserId>,
pub body: MessageBody,
pub created: DateTime<Utc>,
pub show_in_mod_inbox: bool,
}
impl ThreadMessageBuilder {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ThreadMessageId, DatabaseError> {
let thread_message_id =
generate_thread_message_id(&mut *transaction).await?;
sqlx::query!(
"
INSERT INTO threads_messages (
id, author_id, body, thread_id, show_in_mod_inbox
)
VALUES (
$1, $2, $3, $4, $5
)
",
thread_message_id as ThreadMessageId,
self.author_id.map(|x| x.0),
serde_json::value::to_value(self.body.clone())?,
self.thread_id as ThreadId,
self.show_in_mod_inbox,
)
.execute(&mut *transaction)
.await?;
Ok(thread_message_id)
}
}
impl ThreadBuilder {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ThreadId, DatabaseError> {
let thread_id = generate_thread_id(&mut *transaction).await?;
sqlx::query!(
"
INSERT INTO threads (
id, thread_type
)
VALUES (
$1, $2
)
",
thread_id as ThreadId,
self.type_.as_str(),
)
.execute(&mut *transaction)
.await?;
for member in &self.members {
sqlx::query!(
"
INSERT INTO threads_members (
thread_id, user_id
)
VALUES (
$1, $2
)
",
thread_id as ThreadId,
*member as UserId,
)
.execute(&mut *transaction)
.await?;
}
Ok(thread_id)
}
}
impl Thread {
pub async fn get<'a, E>(
id: ThreadId,
exec: E,
) -> Result<Option<Thread>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
Self::get_many(&[id], exec)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
thread_ids: &[ThreadId],
exec: E,
) -> Result<Vec<Thread>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let thread_ids_parsed: Vec<i64> =
thread_ids.iter().map(|x| x.0).collect();
let threads = sqlx::query!(
"
SELECT t.id, t.thread_type,
ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members,
JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created)) filter (where tmsg.id is not null) messages
FROM threads t
LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id
LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id
WHERE t.id = ANY($1)
GROUP BY t.id
",
&thread_ids_parsed
)
.fetch_many(exec)
.try_filter_map(|e| async {
Ok(e.right().map(|x| Thread {
id: ThreadId(x.id),
type_: ThreadType::from_str(&x.thread_type),
messages: serde_json::from_value(
x.messages.unwrap_or_default(),
)
.ok()
.unwrap_or_default(),
members: x.members.unwrap_or_default().into_iter().map(UserId).collect(),
}))
})
.try_collect::<Vec<Thread>>()
.await?;
Ok(threads)
}
pub async fn remove_full(
id: ThreadId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> {
sqlx::query!(
"
DELETE FROM threads_messages
WHERE thread_id = $1
",
id as ThreadId,
)
.execute(&mut *transaction)
.await?;
sqlx::query!(
"
DELETE FROM threads_members
WHERE thread_id = $1
",
id as ThreadId
)
.execute(&mut *transaction)
.await?;
sqlx::query!(
"
DELETE FROM threads
WHERE id = $1
",
id as ThreadId,
)
.execute(&mut *transaction)
.await?;
Ok(Some(()))
}
}
impl ThreadMessage {
pub async fn get<'a, E>(
id: ThreadMessageId,
exec: E,
) -> Result<Option<ThreadMessage>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
Self::get_many(&[id], exec)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
message_ids: &[ThreadMessageId],
exec: E,
) -> Result<Vec<ThreadMessage>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let message_ids_parsed: Vec<i64> =
message_ids.iter().map(|x| x.0).collect();
let messages = sqlx::query!(
"
SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created, tm.show_in_mod_inbox
FROM threads_messages tm
WHERE tm.id = ANY($1)
",
&message_ids_parsed
)
.fetch_many(exec)
.try_filter_map(|e| async {
Ok(e.right().map(|x| ThreadMessage {
id: ThreadMessageId(x.id),
thread_id: ThreadId(x.thread_id),
author_id: x.author_id.map(UserId),
body: serde_json::from_value(x.body)
.unwrap_or(MessageBody::Deleted),
created: x.created,
show_in_mod_inbox: x.show_in_mod_inbox,
}))
})
.try_collect::<Vec<ThreadMessage>>()
.await?;
Ok(messages)
}
pub async fn remove_full(
id: ThreadMessageId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> {
sqlx::query!(
"
DELETE FROM threads_messages
WHERE id = $1
",
id as ThreadMessageId,
)
.execute(&mut *transaction)
.await?;
Ok(Some(()))
}
}

View File

@@ -451,6 +451,28 @@ impl User {
.execute(&mut *transaction)
.await?;
sqlx::query!(
r#"
UPDATE threads_messages
SET body = '{"type": "deleted"}', author_id = $2
WHERE author_id = $1
"#,
id as UserId,
deleted_user as UserId,
)
.execute(&mut *transaction)
.await?;
sqlx::query!(
"
DELETE FROM threads_members
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut *transaction)
.await?;
sqlx::query!(
"
DELETE FROM users

View File

@@ -568,66 +568,6 @@ impl Version {
Ok(map)
}
pub async fn get<'a, 'b, E>(
id: VersionId,
executor: E,
) -> Result<Option<Self>, sqlx::error::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
Self::get_many(&[id], executor)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
version_ids: &[VersionId],
exec: E,
) -> Result<Vec<Version>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let version_ids_parsed: Vec<i64> =
version_ids.iter().map(|x| x.0).collect();
let versions = sqlx::query!(
"
SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,
v.changelog, v.date_published, v.downloads,
v.version_type, v.featured, v.status, v.requested_status
FROM versions v
WHERE v.id = ANY($1)
ORDER BY v.date_published ASC
",
&version_ids_parsed
)
.fetch_many(exec)
.try_filter_map(|e| async {
Ok(e.right().map(|v| Version {
id: VersionId(v.id),
project_id: ProjectId(v.mod_id),
author_id: UserId(v.author_id),
name: v.name,
version_number: v.version_number,
changelog: v.changelog,
changelog_url: None,
date_published: v.date_published,
downloads: v.downloads,
featured: v.featured,
version_type: v.version_type,
status: VersionStatus::from_str(&v.status),
requested_status: v
.requested_status
.map(|x| VersionStatus::from_str(&x)),
}))
})
.try_collect::<Vec<Version>>()
.await?;
Ok(versions)
}
pub async fn get_full<'a, 'b, E>(
id: VersionId,
executor: E,