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:
parent
7605df1bd9
commit
8f61e9876f
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
|
||||
# Created by https://www.gitignore.io/api/rust,clion
|
||||
# Edit at https://www.gitignore.io/?templates=rust,clion
|
||||
|
||||
@ -107,3 +106,4 @@ fabric.properties
|
||||
|
||||
# End of https://www.gitignore.io/api/rust,clion
|
||||
|
||||
.DS_Store
|
||||
32
migrations/20230324202117_messaging.sql
Normal file
32
migrations/20230324202117_messaging.sql
Normal file
@ -0,0 +1,32 @@
|
||||
-- Add migration script here
|
||||
|
||||
-- Add route for users to see their own reports
|
||||
|
||||
CREATE TABLE threads (
|
||||
id bigint PRIMARY KEY,
|
||||
-- can be either "report", "project", or "direct_message". direct message is unused for now
|
||||
thread_type VARCHAR(64) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE threads_messages (
|
||||
id bigint PRIMARY KEY,
|
||||
thread_id bigint REFERENCES threads ON UPDATE CASCADE NOT NULL,
|
||||
-- If this is null, it's a system message
|
||||
author_id bigint REFERENCES users ON UPDATE CASCADE NULL,
|
||||
body jsonb NOT NULL,
|
||||
created timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
show_in_mod_inbox BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE threads_members (
|
||||
thread_id bigint REFERENCES threads ON UPDATE CASCADE NOT NULL,
|
||||
user_id bigint REFERENCES users ON UPDATE CASCADE NOT NULL,
|
||||
PRIMARY KEY (thread_id, user_id)
|
||||
);
|
||||
|
||||
ALTER TABLE reports
|
||||
ADD COLUMN closed boolean NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE reports
|
||||
ADD COLUMN thread_id bigint references threads ON UPDATE CASCADE;
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN thread_id bigint references threads ON UPDATE CASCADE;
|
||||
1749
sqlx-data.json
1749
sqlx-data.json
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(()))
|
||||
|
||||
@ -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,
|
||||
|
||||
267
src/database/models/thread_item.rs
Normal file
267
src/database/models/thread_item.rs
Normal 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(()))
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -4,6 +4,8 @@ pub use super::notifications::NotificationId;
|
||||
pub use super::projects::{ProjectId, VersionId};
|
||||
pub use super::reports::ReportId;
|
||||
pub use super::teams::TeamId;
|
||||
pub use super::threads::ThreadId;
|
||||
pub use super::threads::ThreadMessageId;
|
||||
pub use super::users::UserId;
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
@ -109,6 +111,8 @@ base62_id_impl!(VersionId, VersionId);
|
||||
base62_id_impl!(TeamId, TeamId);
|
||||
base62_id_impl!(ReportId, ReportId);
|
||||
base62_id_impl!(NotificationId, NotificationId);
|
||||
base62_id_impl!(ThreadId, ThreadId);
|
||||
base62_id_impl!(ThreadMessageId, ThreadMessageId);
|
||||
|
||||
pub mod base62_impl {
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
|
||||
@ -5,4 +5,5 @@ pub mod pack;
|
||||
pub mod projects;
|
||||
pub mod reports;
|
||||
pub mod teams;
|
||||
pub mod threads;
|
||||
pub mod users;
|
||||
|
||||
@ -3,6 +3,7 @@ use super::teams::TeamId;
|
||||
use super::users::UserId;
|
||||
use crate::database::models::project_item::QueryProject;
|
||||
use crate::database::models::version_item::QueryVersion;
|
||||
use crate::models::threads::ThreadId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
@ -56,6 +57,7 @@ pub struct Project {
|
||||
/// The requested status of this projct
|
||||
pub requested_status: Option<ProjectStatus>,
|
||||
|
||||
/// DEPRECATED: moved to threads system
|
||||
/// The rejection data of the project
|
||||
pub moderator_message: Option<ModeratorMessage>,
|
||||
|
||||
@ -107,6 +109,9 @@ pub struct Project {
|
||||
|
||||
/// The color of the project (picked from icon)
|
||||
pub color: Option<u32>,
|
||||
|
||||
/// The thread of the moderation messages of the project
|
||||
pub thread_id: Option<ThreadId>,
|
||||
}
|
||||
|
||||
impl From<QueryProject> for Project {
|
||||
@ -195,6 +200,7 @@ impl From<QueryProject> for Project {
|
||||
flame_anvil_project: m.flame_anvil_project,
|
||||
flame_anvil_user: m.flame_anvil_user.map(|x| x.into()),
|
||||
color: m.color,
|
||||
thread_id: m.thread_id.map(|x| x.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use super::ids::Base62Id;
|
||||
use crate::models::ids::UserId;
|
||||
use crate::models::ids::{ThreadId, UserId};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -17,6 +17,8 @@ pub struct Report {
|
||||
pub reporter: UserId,
|
||||
pub body: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub closed: bool,
|
||||
pub thread_id: Option<ThreadId>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
|
||||
80
src/models/threads.rs
Normal file
80
src/models/threads.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use super::ids::Base62Id;
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::models::users::{User, UserId};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ThreadId(pub u64);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ThreadMessageId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Thread {
|
||||
pub id: ThreadId,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: ThreadType,
|
||||
pub messages: Vec<ThreadMessage>,
|
||||
pub members: Vec<User>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ThreadMessage {
|
||||
pub id: ThreadMessageId,
|
||||
pub author_id: Option<UserId>,
|
||||
pub body: MessageBody,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum MessageBody {
|
||||
Text {
|
||||
body: String,
|
||||
},
|
||||
StatusChange {
|
||||
new_status: ProjectStatus,
|
||||
old_status: ProjectStatus,
|
||||
},
|
||||
ThreadClosure,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ThreadType {
|
||||
Report,
|
||||
Project,
|
||||
DirectMessage,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ThreadType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadType {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ThreadType::Report => "report",
|
||||
ThreadType::Project => "project",
|
||||
ThreadType::DirectMessage => "direct_message",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(string: &str) -> ThreadType {
|
||||
match string {
|
||||
"report" => ThreadType::Report,
|
||||
"project" => ThreadType::Project,
|
||||
"direct_message" => ThreadType::DirectMessage,
|
||||
_ => ThreadType::DirectMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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()),
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
278
src/routes/v2/threads.rs
Normal 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(""))
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -117,21 +117,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_is_admin_from_headers<'a, 'b, E>(
|
||||
headers: &HeaderMap,
|
||||
executor: E,
|
||||
) -> Result<User, AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let user = get_user_from_headers(headers, executor).await?;
|
||||
|
||||
match user.role {
|
||||
Role::Admin => Ok(user),
|
||||
_ => Err(AuthenticationError::InvalidCredentials),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_authorized(
|
||||
project_data: &Project,
|
||||
user_option: &Option<User>,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user