More mod info (#104)
* More mod info * Downloading mods * Run prepare * User editing + icon editing * Finish * Some fixes * Fix clippy errors
This commit is contained in:
parent
92e1847c59
commit
1da5357df6
63
migrations/20201122043349_more-mod-data.sql
Normal file
63
migrations/20201122043349_more-mod-data.sql
Normal file
@ -0,0 +1,63 @@
|
||||
CREATE TABLE donation_platforms (
|
||||
id serial PRIMARY KEY,
|
||||
short varchar(100) UNIQUE NOT NULL,
|
||||
name varchar(500) UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO donation_platforms (short, name) VALUES ('patreon', 'Patreon');
|
||||
INSERT INTO donation_platforms (short, name) VALUES ('bmac', 'Buy Me a Coffee');
|
||||
INSERT INTO donation_platforms (short, name) VALUES ('paypal', 'PayPal');
|
||||
INSERT INTO donation_platforms (short, name) VALUES ('github', 'GitHub Sponsors');
|
||||
INSERT INTO donation_platforms (short, name) VALUES ('ko-fi', 'Ko-fi');
|
||||
INSERT INTO donation_platforms (short, name) VALUES ('other', 'Other');
|
||||
|
||||
CREATE TABLE mods_donations (
|
||||
joining_mod_id bigint REFERENCES mods ON UPDATE CASCADE NOT NULL,
|
||||
joining_platform_id int REFERENCES donation_platforms ON UPDATE CASCADE NOT NULL,
|
||||
url varchar(2048) NOT NULL,
|
||||
PRIMARY KEY (joining_mod_id, joining_platform_id)
|
||||
);
|
||||
|
||||
CREATE TABLE side_types (
|
||||
id serial PRIMARY KEY,
|
||||
name varchar(64) UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO side_types (name) VALUES ('required');
|
||||
INSERT INTO side_types (name) VALUES ('no-functionality');
|
||||
INSERT INTO side_types (name) VALUES ('unsupported');
|
||||
INSERT INTO side_types (name) VALUES ('unknown');
|
||||
|
||||
CREATE TABLE licenses (
|
||||
id serial PRIMARY KEY,
|
||||
short varchar(60) UNIQUE NOT NULL,
|
||||
name varchar(1000) UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO licenses (short, name) VALUES ('custom', 'Custom License');
|
||||
|
||||
ALTER TABLE versions
|
||||
ADD COLUMN featured BOOLEAN NOT NULL default FALSE;
|
||||
ALTER TABLE files
|
||||
ADD COLUMN is_primary BOOLEAN NOT NULL default FALSE;
|
||||
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN license integer REFERENCES licenses NOT NULL default 1;
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN license_url varchar(1000) NULL;
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN client_side integer REFERENCES side_types NOT NULL default 4;
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN server_side integer REFERENCES side_types NOT NULL default 4;
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN discord_url varchar(255) NULL;
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN slug varchar(255) NULL UNIQUE;
|
||||
|
||||
CREATE TABLE downloads (
|
||||
id serial PRIMARY KEY,
|
||||
version_id bigint REFERENCES versions ON UPDATE CASCADE NOT NULL,
|
||||
date timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
-- A SHA1 hash of the downloader IP address
|
||||
identifier varchar(40) NOT NULL
|
||||
);
|
||||
1907
sqlx-data.json
1907
sqlx-data.json
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,18 @@ pub struct Category {
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
pub struct License {
|
||||
pub id: LicenseId,
|
||||
pub short: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct DonationPlatform {
|
||||
pub id: DonationPlatformId,
|
||||
pub short: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct CategoryBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
@ -453,3 +465,293 @@ impl<'a> GameVersionBuilder<'a> {
|
||||
Ok(GameVersionId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LicenseBuilder<'a> {
|
||||
pub short: Option<&'a str>,
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl License {
|
||||
pub fn builder() -> LicenseBuilder<'static> {
|
||||
LicenseBuilder::default()
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(id: &str, exec: E) -> Result<Option<LicenseId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM licenses
|
||||
WHERE short = $1
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| LicenseId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get<'a, E>(id: LicenseId, exec: E) -> Result<License, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT short, name FROM licenses
|
||||
WHERE id = $1
|
||||
",
|
||||
id as LicenseId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(License {
|
||||
id,
|
||||
short: result.short,
|
||||
name: result.name,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<License>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id, short, name FROM licenses
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|c| License {
|
||||
id: LicenseId(c.id),
|
||||
short: c.short,
|
||||
name: c.name,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<License>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn remove<'a, E>(short: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM licenses
|
||||
WHERE short = $1
|
||||
",
|
||||
short
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LicenseBuilder<'a> {
|
||||
/// The license's short name/abbreviation. Spaces must be replaced with '_' for it to be valid
|
||||
pub fn short(self, short: &'a str) -> Result<LicenseBuilder<'a>, DatabaseError> {
|
||||
if short
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
|
||||
{
|
||||
Ok(Self {
|
||||
short: Some(short),
|
||||
..self
|
||||
})
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(short.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The license's long name
|
||||
pub fn name(self, name: &'a str) -> Result<LicenseBuilder<'a>, DatabaseError> {
|
||||
Ok(Self {
|
||||
name: Some(name),
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<LicenseId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO licenses (short, name)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (short) DO NOTHING
|
||||
RETURNING id
|
||||
",
|
||||
self.short,
|
||||
self.name,
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(LicenseId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
) -> Result<Option<DonationPlatformId>, 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| 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>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id, short, name FROM donation_platforms
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|c| DonationPlatform {
|
||||
id: DonationPlatformId(c.id),
|
||||
short: c.short,
|
||||
name: c.name,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<DonationPlatform>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn remove<'a, E>(short: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
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> {
|
||||
if short
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
|
||||
{
|
||||
Ok(Self {
|
||||
short: Some(short),
|
||||
..self
|
||||
})
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(short.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,6 +104,15 @@ pub struct ModId(pub i64);
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct StatusId(pub i32);
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct SideTypeId(pub i32);
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct LicenseId(pub i32);
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct DonationPlatformId(pub i32);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
|
||||
@ -79,3 +79,44 @@ impl ids::StatusId {
|
||||
Ok(result.map(|r| ids::StatusId(r.id)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ids::SideTypeId {
|
||||
pub async fn get_id<'a, E>(
|
||||
side: &crate::models::mods::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)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,36 @@
|
||||
use super::ids::*;
|
||||
|
||||
pub struct DonationUrl {
|
||||
pub mod_id: ModId,
|
||||
pub platform_id: DonationPlatformId,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl DonationUrl {
|
||||
pub async fn insert(
|
||||
&self,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), sqlx::error::Error> {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO mods_donations (
|
||||
joining_mod_id, joining_platform_id, url
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3
|
||||
)
|
||||
",
|
||||
self.mod_id as ModId,
|
||||
self.platform_id as DonationPlatformId,
|
||||
self.url,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModBuilder {
|
||||
pub mod_id: ModId,
|
||||
pub team_id: TeamId,
|
||||
@ -10,9 +41,16 @@ pub struct ModBuilder {
|
||||
pub issues_url: Option<String>,
|
||||
pub source_url: Option<String>,
|
||||
pub wiki_url: Option<String>,
|
||||
pub license_url: Option<String>,
|
||||
pub discord_url: Option<String>,
|
||||
pub categories: Vec<CategoryId>,
|
||||
pub initial_versions: Vec<super::version_item::VersionBuilder>,
|
||||
pub status: StatusId,
|
||||
pub client_side: SideTypeId,
|
||||
pub server_side: SideTypeId,
|
||||
pub license: LicenseId,
|
||||
pub slug: Option<String>,
|
||||
pub donation_urls: Vec<DonationUrl>,
|
||||
}
|
||||
|
||||
impl ModBuilder {
|
||||
@ -34,6 +72,12 @@ impl ModBuilder {
|
||||
issues_url: self.issues_url,
|
||||
source_url: self.source_url,
|
||||
wiki_url: self.wiki_url,
|
||||
license_url: self.license_url,
|
||||
discord_url: self.discord_url,
|
||||
client_side: self.client_side,
|
||||
server_side: self.server_side,
|
||||
license: self.license,
|
||||
slug: self.slug,
|
||||
};
|
||||
mod_struct.insert(&mut *transaction).await?;
|
||||
|
||||
@ -42,6 +86,11 @@ impl ModBuilder {
|
||||
version.insert(&mut *transaction).await?;
|
||||
}
|
||||
|
||||
for mut donation in self.donation_urls {
|
||||
donation.mod_id = self.mod_id;
|
||||
donation.insert(&mut *transaction).await?;
|
||||
}
|
||||
|
||||
for category in self.categories {
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -73,6 +122,12 @@ pub struct Mod {
|
||||
pub issues_url: Option<String>,
|
||||
pub source_url: Option<String>,
|
||||
pub wiki_url: Option<String>,
|
||||
pub license_url: Option<String>,
|
||||
pub discord_url: Option<String>,
|
||||
pub client_side: SideTypeId,
|
||||
pub server_side: SideTypeId,
|
||||
pub license: LicenseId,
|
||||
pub slug: Option<String>,
|
||||
}
|
||||
|
||||
impl Mod {
|
||||
@ -85,12 +140,16 @@ impl Mod {
|
||||
INSERT INTO mods (
|
||||
id, team_id, title, description, body_url,
|
||||
published, downloads, icon_url, issues_url,
|
||||
source_url, wiki_url, status
|
||||
source_url, wiki_url, status, discord_url,
|
||||
client_side, server_side, license_url, license,
|
||||
slug
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, $9,
|
||||
$10, $11, $12
|
||||
$10, $11, $12, $13,
|
||||
$14, $15, $16, $17,
|
||||
$18
|
||||
)
|
||||
",
|
||||
self.id as ModId,
|
||||
@ -104,7 +163,13 @@ impl Mod {
|
||||
self.issues_url.as_ref(),
|
||||
self.source_url.as_ref(),
|
||||
self.wiki_url.as_ref(),
|
||||
self.status.0
|
||||
self.status.0,
|
||||
self.discord_url.as_ref(),
|
||||
self.client_side as SideTypeId,
|
||||
self.server_side as SideTypeId,
|
||||
self.license_url.as_ref(),
|
||||
self.license as LicenseId,
|
||||
self.slug.as_ref()
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@ -121,8 +186,8 @@ impl Mod {
|
||||
SELECT title, description, downloads,
|
||||
icon_url, body_url, published,
|
||||
updated, status,
|
||||
issues_url, source_url, wiki_url,
|
||||
team_id
|
||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||
team_id, client_side, server_side, license, slug
|
||||
FROM mods
|
||||
WHERE id = $1
|
||||
",
|
||||
@ -145,7 +210,13 @@ impl Mod {
|
||||
issues_url: row.issues_url,
|
||||
source_url: row.source_url,
|
||||
wiki_url: row.wiki_url,
|
||||
license_url: row.license_url,
|
||||
discord_url: row.discord_url,
|
||||
client_side: SideTypeId(row.client_side),
|
||||
status: StatusId(row.status),
|
||||
server_side: SideTypeId(row.server_side),
|
||||
license: LicenseId(row.license),
|
||||
slug: row.slug,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
@ -164,8 +235,8 @@ impl Mod {
|
||||
SELECT id, title, description, downloads,
|
||||
icon_url, body_url, published,
|
||||
updated, status,
|
||||
issues_url, source_url, wiki_url,
|
||||
team_id
|
||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||
team_id, client_side, server_side, license, slug
|
||||
FROM mods
|
||||
WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
",
|
||||
@ -186,7 +257,13 @@ impl Mod {
|
||||
issues_url: m.issues_url,
|
||||
source_url: m.source_url,
|
||||
wiki_url: m.wiki_url,
|
||||
license_url: m.license_url,
|
||||
discord_url: m.discord_url,
|
||||
client_side: SideTypeId(m.client_side),
|
||||
status: StatusId(m.status),
|
||||
server_side: SideTypeId(m.server_side),
|
||||
license: LicenseId(m.license),
|
||||
slug: m.slug,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<Mod>>()
|
||||
@ -227,6 +304,16 @@ impl Mod {
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods_donations
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
let versions: Vec<VersionId> = sqlx::query!(
|
||||
"
|
||||
@ -277,6 +364,30 @@ impl Mod {
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn get_full_from_slug<'a, 'b, E>(
|
||||
slug: String,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryMod>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let id = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM mods
|
||||
WHERE slug = $1
|
||||
",
|
||||
slug
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(mod_id) = id {
|
||||
Mod::get_full(ModId(mod_id.id), executor).await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_full<'a, 'b, E>(
|
||||
id: ModId,
|
||||
executor: E,
|
||||
@ -312,6 +423,24 @@ impl Mod {
|
||||
.try_collect::<Vec<VersionId>>()
|
||||
.await?;
|
||||
|
||||
let donations: Vec<DonationUrl> = sqlx::query!(
|
||||
"
|
||||
SELECT joining_platform_id, url FROM mods_donations
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|c| DonationUrl {
|
||||
mod_id: id,
|
||||
platform_id: DonationPlatformId(c.joining_platform_id),
|
||||
url: c.url,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<DonationUrl>>()
|
||||
.await?;
|
||||
|
||||
let status = sqlx::query!(
|
||||
"
|
||||
SELECT status FROM statuses
|
||||
@ -323,11 +452,48 @@ impl Mod {
|
||||
.await?
|
||||
.status;
|
||||
|
||||
let client_side = sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
inner.client_side.0,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await?
|
||||
.name;
|
||||
|
||||
let server_side = sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
inner.server_side.0,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await?
|
||||
.name;
|
||||
|
||||
let license = sqlx::query!(
|
||||
"
|
||||
SELECT short, name FROM licenses
|
||||
WHERE id = $1
|
||||
",
|
||||
inner.license.0,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(Some(QueryMod {
|
||||
inner,
|
||||
categories,
|
||||
versions,
|
||||
donation_urls: donations,
|
||||
status: crate::models::mods::ModStatus::from_str(&status),
|
||||
license_id: license.short,
|
||||
license_name: license.name,
|
||||
client_side: crate::models::mods::SideType::from_str(&client_side),
|
||||
server_side: crate::models::mods::SideType::from_str(&server_side)
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
@ -351,5 +517,10 @@ pub struct QueryMod {
|
||||
|
||||
pub categories: Vec<String>,
|
||||
pub versions: Vec<VersionId>,
|
||||
pub donation_urls: Vec<DonationUrl>,
|
||||
pub status: crate::models::mods::ModStatus,
|
||||
pub license_id: String,
|
||||
pub license_name: String,
|
||||
pub client_side: crate::models::mods::SideType,
|
||||
pub server_side: crate::models::mods::SideType,
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ impl TeamMember {
|
||||
name: m.member_name,
|
||||
role: m.role,
|
||||
permissions: Permissions::from_bits(m.permissions as u64)
|
||||
.ok_or_else(|| super::DatabaseError::BitflagError)?,
|
||||
.ok_or(super::DatabaseError::BitflagError)?,
|
||||
accepted: m.accepted,
|
||||
}))
|
||||
} else {
|
||||
@ -297,7 +297,7 @@ impl TeamMember {
|
||||
name: m.member_name,
|
||||
role: m.role,
|
||||
permissions: Permissions::from_bits(m.permissions as u64)
|
||||
.ok_or_else(|| super::DatabaseError::BitflagError)?,
|
||||
.ok_or(super::DatabaseError::BitflagError)?,
|
||||
accepted: m.accepted,
|
||||
}))
|
||||
} else {
|
||||
|
||||
@ -113,6 +113,43 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_from_username<'a, 'b, E>(
|
||||
username: String,
|
||||
executor: E,
|
||||
) -> Result<Option<Self>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT u.id, u.github_id, u.name, u.email,
|
||||
u.avatar_url, u.bio,
|
||||
u.created, u.role
|
||||
FROM users u
|
||||
WHERE u.username = $1
|
||||
",
|
||||
username
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
Ok(Some(User {
|
||||
id: UserId(row.id),
|
||||
github_id: row.github_id,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
avatar_url: row.avatar_url,
|
||||
username,
|
||||
bio: row.bio,
|
||||
created: row.created,
|
||||
role: row.role,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_many<'a, E>(user_ids: Vec<UserId>, exec: E) -> Result<Vec<User>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
|
||||
@ -13,12 +13,14 @@ pub struct VersionBuilder {
|
||||
pub game_versions: Vec<GameVersionId>,
|
||||
pub loaders: Vec<LoaderId>,
|
||||
pub release_channel: ChannelId,
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
pub struct VersionFileBuilder {
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub hashes: Vec<HashBuilder>,
|
||||
pub primary: bool,
|
||||
}
|
||||
|
||||
impl VersionFileBuilder {
|
||||
@ -81,6 +83,7 @@ impl VersionBuilder {
|
||||
downloads: 0,
|
||||
release_channel: self.release_channel,
|
||||
accepted: false,
|
||||
featured: self.featured,
|
||||
};
|
||||
|
||||
version.insert(&mut *transaction).await?;
|
||||
@ -154,6 +157,7 @@ pub struct Version {
|
||||
pub downloads: i32,
|
||||
pub release_channel: ChannelId,
|
||||
pub accepted: bool,
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
@ -166,13 +170,13 @@ impl Version {
|
||||
INSERT INTO versions (
|
||||
id, mod_id, author_id, name, version_number,
|
||||
changelog_url, date_published,
|
||||
downloads, release_channel, accepted
|
||||
downloads, release_channel, accepted, featured
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7,
|
||||
$8, $9,
|
||||
$10
|
||||
$10, $11
|
||||
)
|
||||
",
|
||||
self.id as VersionId,
|
||||
@ -184,7 +188,8 @@ impl Version {
|
||||
self.date_published,
|
||||
self.downloads,
|
||||
self.release_channel as ChannelId,
|
||||
self.accepted
|
||||
self.accepted,
|
||||
self.featured
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@ -234,7 +239,7 @@ impl Version {
|
||||
|
||||
let files = sqlx::query!(
|
||||
"
|
||||
SELECT files.id, files.url, files.filename FROM files
|
||||
SELECT files.id, files.url, files.filename, files.is_primary FROM files
|
||||
WHERE files.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
@ -246,6 +251,7 @@ impl Version {
|
||||
version_id: id,
|
||||
url: c.url,
|
||||
filename: c.filename,
|
||||
primary: c.is_primary,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<VersionFile>>()
|
||||
@ -367,7 +373,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
v.release_channel, v.accepted
|
||||
v.release_channel, v.accepted, v.featured
|
||||
FROM versions v
|
||||
WHERE v.id = $1
|
||||
",
|
||||
@ -388,6 +394,7 @@ impl Version {
|
||||
downloads: row.downloads,
|
||||
release_channel: ChannelId(row.release_channel),
|
||||
accepted: row.accepted,
|
||||
featured: row.featured,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
@ -408,7 +415,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
v.release_channel, accepted
|
||||
v.release_channel, v.accepted, v.featured
|
||||
FROM versions v
|
||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
",
|
||||
@ -427,6 +434,7 @@ impl Version {
|
||||
downloads: v.downloads,
|
||||
release_channel: ChannelId(v.release_channel),
|
||||
accepted: v.accepted,
|
||||
featured: v.featured,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<Version>>()
|
||||
@ -446,7 +454,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
release_channels.channel, v.accepted
|
||||
release_channels.channel, v.accepted, v.featured
|
||||
FROM versions v
|
||||
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
||||
WHERE v.id = $1
|
||||
@ -487,7 +495,7 @@ impl Version {
|
||||
|
||||
let mut files = sqlx::query!(
|
||||
"
|
||||
SELECT files.id, files.url, files.filename FROM files
|
||||
SELECT files.id, files.url, files.filename, files.is_primary FROM files
|
||||
WHERE files.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
@ -499,6 +507,7 @@ impl Version {
|
||||
url: c.url,
|
||||
filename: c.filename,
|
||||
hashes: std::collections::HashMap::new(),
|
||||
primary: c.is_primary,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<QueryFile>>()
|
||||
@ -535,6 +544,7 @@ impl Version {
|
||||
loaders,
|
||||
game_versions,
|
||||
accepted: row.accepted,
|
||||
featured: row.featured,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
@ -564,6 +574,7 @@ pub struct VersionFile {
|
||||
pub version_id: VersionId,
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub primary: bool,
|
||||
}
|
||||
|
||||
pub struct FileHash {
|
||||
@ -587,6 +598,7 @@ pub struct QueryVersion {
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
pub accepted: bool,
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
pub struct QueryFile {
|
||||
@ -594,4 +606,5 @@ pub struct QueryFile {
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub hashes: std::collections::HashMap<String, Vec<u8>>,
|
||||
pub primary: bool,
|
||||
}
|
||||
|
||||
48
src/main.rs
48
src/main.rs
@ -36,6 +36,11 @@ struct Config {
|
||||
allow_missing_vars: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Pepper {
|
||||
pub pepper: String,
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
@ -155,6 +160,44 @@ async fn main() -> std::io::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
let pool_ref = pool.clone();
|
||||
scheduler.run(std::time::Duration::from_secs(15 * 60), move || {
|
||||
let pool_ref = pool_ref.clone();
|
||||
// Use sqlx to delete records more than an hour old
|
||||
info!("Deleting old records from temporary tables");
|
||||
|
||||
async move {
|
||||
let downloads_result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM downloads
|
||||
WHERE date < (CURRENT_DATE - INTERVAL '30 minutes ago')
|
||||
"
|
||||
)
|
||||
.execute(&pool_ref)
|
||||
.await;
|
||||
|
||||
if let Err(e) = downloads_result {
|
||||
warn!("Deleting old records from temporary table downloads failed: {:?}", e);
|
||||
}
|
||||
|
||||
let states_result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM states
|
||||
WHERE expires < CURRENT_DATE
|
||||
"
|
||||
)
|
||||
.execute(&pool_ref)
|
||||
.await;
|
||||
|
||||
if let Err(e) = states_result {
|
||||
warn!("Deleting old records from temporary table states failed: {:?}", e);
|
||||
}
|
||||
|
||||
info!("Finished deleting old records from temporary tables");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let indexing_queue = Arc::new(search::indexing::queue::CreationQueue::new());
|
||||
|
||||
let queue_ref = indexing_queue.clone();
|
||||
@ -216,6 +259,10 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
scheduler::schedule_versions(&mut scheduler, pool.clone(), skip_initial);
|
||||
|
||||
let ip_salt = Pepper {
|
||||
pepper: crate::models::ids::Base62Id(crate::models::ids::random_base62(11)).to_string()
|
||||
};
|
||||
|
||||
let allowed_origins = dotenv::var("CORS_ORIGINS")
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<Vec<String>>(&s).ok())
|
||||
@ -247,6 +294,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.data(file_host.clone())
|
||||
.data(indexing_queue.clone())
|
||||
.data(search_config.clone())
|
||||
.data(ip_salt.clone())
|
||||
.service(routes::index_get)
|
||||
.service(
|
||||
web::scope("/api/v1/")
|
||||
|
||||
@ -21,6 +21,8 @@ pub struct VersionId(pub u64);
|
||||
pub struct Mod {
|
||||
/// The ID of the mod, encoded as a base62 string.
|
||||
pub id: ModId,
|
||||
/// The slug of a mod, used for vanity URLs
|
||||
pub slug: Option<String>,
|
||||
/// The team of people that has ownership of this mod.
|
||||
pub team: TeamId,
|
||||
/// The title or name of the mod.
|
||||
@ -35,6 +37,13 @@ pub struct Mod {
|
||||
pub updated: DateTime<Utc>,
|
||||
/// The status of the mod
|
||||
pub status: ModStatus,
|
||||
/// The license of this mod
|
||||
pub license: License,
|
||||
|
||||
/// The support range for the client mod
|
||||
pub client_side: SideType,
|
||||
/// The support range for the server mod
|
||||
pub server_side: SideType,
|
||||
|
||||
/// The total number of downloads the mod has had.
|
||||
pub downloads: u32,
|
||||
@ -42,7 +51,7 @@ pub struct Mod {
|
||||
pub categories: Vec<String>,
|
||||
/// A list of ids for versions of the mod.
|
||||
pub versions: Vec<VersionId>,
|
||||
///The URL of the icon of the mod
|
||||
/// The URL of the icon of the mod
|
||||
pub icon_url: Option<String>,
|
||||
/// An optional link to where to submit bugs or issues with the mod.
|
||||
pub issues_url: Option<String>,
|
||||
@ -50,6 +59,60 @@ pub struct Mod {
|
||||
pub source_url: Option<String>,
|
||||
/// An optional link to the mod's wiki page or other relevant information.
|
||||
pub wiki_url: Option<String>,
|
||||
/// An optional link to the mod's discord
|
||||
pub discord_url: Option<String>,
|
||||
/// An optional list of all donation links the mod has
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SideType {
|
||||
Required,
|
||||
NoFunctionality,
|
||||
Unsupported,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SideType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl SideType {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SideType::Required => "required",
|
||||
SideType::NoFunctionality => "no-functionality",
|
||||
SideType::Unsupported => "unsupported",
|
||||
SideType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(string: &str) -> SideType {
|
||||
match string {
|
||||
"required" => SideType::Required,
|
||||
"no-functionality" => SideType::NoFunctionality,
|
||||
"unsupported" => SideType::Unsupported,
|
||||
_ => SideType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct License {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DonationLink {
|
||||
pub id: String,
|
||||
pub platform: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// A status decides the visbility of a mod in search, URLs, and the whole site itself.
|
||||
@ -71,14 +134,7 @@ pub enum ModStatus {
|
||||
|
||||
impl std::fmt::Display for ModStatus {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
ModStatus::Approved => write!(fmt, "approved"),
|
||||
ModStatus::Rejected => write!(fmt, "rejected"),
|
||||
ModStatus::Draft => write!(fmt, "draft"),
|
||||
ModStatus::Unlisted => write!(fmt, "unlisted"),
|
||||
ModStatus::Processing => write!(fmt, "processing"),
|
||||
ModStatus::Unknown => write!(fmt, "unknown"),
|
||||
}
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,10 +172,7 @@ impl ModStatus {
|
||||
}
|
||||
|
||||
pub fn is_searchable(&self) -> bool {
|
||||
match self {
|
||||
ModStatus::Approved => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, ModStatus::Approved)
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,6 +185,8 @@ pub struct Version {
|
||||
pub mod_id: ModId,
|
||||
/// The ID of the author who published this version
|
||||
pub author_id: UserId,
|
||||
/// Whether the version is featured or not
|
||||
pub featured: bool,
|
||||
|
||||
/// The name of this version
|
||||
pub name: String,
|
||||
@ -166,6 +221,8 @@ pub struct VersionFile {
|
||||
pub url: String,
|
||||
/// The filename of the file.
|
||||
pub filename: String,
|
||||
/// Whether the file is the primary file of a version
|
||||
pub primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
|
||||
@ -26,9 +26,11 @@ pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
||||
|
||||
cfg.service(
|
||||
web::scope("mod")
|
||||
.service(mods::mod_slug_get)
|
||||
.service(mods::mod_get)
|
||||
.service(mods::mod_delete)
|
||||
.service(mods::mod_edit)
|
||||
.service(mods::mod_icon_edit)
|
||||
.service(web::scope("{mod_id}").service(versions::version_list)),
|
||||
);
|
||||
}
|
||||
@ -46,7 +48,8 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("version_file")
|
||||
.service(versions::delete_file)
|
||||
.service(versions::get_version_from_hash),
|
||||
.service(versions::get_version_from_hash)
|
||||
.service(versions::download_version),
|
||||
);
|
||||
}
|
||||
|
||||
@ -56,9 +59,12 @@ pub fn users_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(users::users_get);
|
||||
cfg.service(
|
||||
web::scope("user")
|
||||
.service(users::user_username_get)
|
||||
.service(users::user_get)
|
||||
.service(users::mods_list)
|
||||
.service(users::user_delete)
|
||||
.service(users::user_edit)
|
||||
.service(users::user_icon_edit)
|
||||
.service(users::teams),
|
||||
);
|
||||
}
|
||||
@ -84,6 +90,8 @@ pub fn moderation_config(cfg: &mut web::ServiceConfig) {
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ApiError {
|
||||
#[error("Environment Error")]
|
||||
EnvError(#[from] dotenv::Error),
|
||||
#[error("Error while uploading file")]
|
||||
FileHostingError(#[from] FileHostingError),
|
||||
#[error("Internal server error")]
|
||||
@ -103,6 +111,7 @@ pub enum ApiError {
|
||||
impl actix_web::ResponseError for ApiError {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
match self {
|
||||
ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||
@ -117,6 +126,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
actix_web::web::HttpResponse::build(self.status_code()).json(
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
ApiError::EnvError(..) => "environment_error",
|
||||
ApiError::DatabaseError(..) => "database_error",
|
||||
ApiError::AuthenticationError(..) => "unauthorized",
|
||||
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::auth::{get_user_from_headers, AuthenticationError};
|
||||
use crate::database::models;
|
||||
use crate::file_hosting::{FileHost, FileHostingError};
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::mods::{ModId, ModStatus, VersionId};
|
||||
use crate::models::mods::{DonationLink, License, ModId, ModStatus, SideType, VersionId};
|
||||
use crate::models::users::UserId;
|
||||
use crate::routes::version_creation::InitialVersionData;
|
||||
use crate::search::indexing::{queue::CreationQueue, IndexingError};
|
||||
@ -99,6 +99,8 @@ impl actix_web::ResponseError for CreateError {
|
||||
struct ModCreateData {
|
||||
/// The title or name of the mod.
|
||||
pub mod_name: String,
|
||||
/// The slug of a mod, used for vanity URLs
|
||||
pub mod_slug: Option<String>,
|
||||
/// A short description of the mod.
|
||||
pub mod_description: String,
|
||||
/// A long description of the mod, in markdown.
|
||||
@ -113,8 +115,20 @@ struct ModCreateData {
|
||||
pub source_url: Option<String>,
|
||||
/// An optional link to the mod's wiki page or other relevant information.
|
||||
pub wiki_url: Option<String>,
|
||||
/// An optional link to the mod's license page
|
||||
pub license_url: Option<String>,
|
||||
/// An optional link to the mod's discord.
|
||||
pub discord_url: Option<String>,
|
||||
/// An optional boolean. If true, the mod will be created as a draft.
|
||||
pub is_draft: Option<bool>,
|
||||
/// The support range for the client mod
|
||||
pub client_side: SideType,
|
||||
/// The support range for the server mod
|
||||
pub server_side: SideType,
|
||||
/// The license id that the mod follows
|
||||
pub license_id: String,
|
||||
/// An optional list of all donation links the mod has
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
}
|
||||
|
||||
pub struct UploadedFile {
|
||||
@ -461,7 +475,53 @@ async fn mod_create_inner(
|
||||
|
||||
let status_id = models::StatusId::get_id(&status, &mut *transaction)
|
||||
.await?
|
||||
.expect("No database entry found for status");
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!("Status {} does not exist.", status.clone()))
|
||||
})?;
|
||||
let client_side_id =
|
||||
models::SideTypeId::get_id(&mod_create_data.client_side, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
"Client side type specified does not exist.".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let server_side_id =
|
||||
models::SideTypeId::get_id(&mod_create_data.server_side, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
"Server side type specified does not exist.".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let license_id =
|
||||
models::categories::License::get_id(&mod_create_data.license_id, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput("License specified does not exist.".to_string())
|
||||
})?;
|
||||
let mut donation_urls = vec![];
|
||||
|
||||
if let Some(urls) = &mod_create_data.donation_urls {
|
||||
for url in urls {
|
||||
let platform_id = models::DonationPlatformId::get_id(&url.id, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Donation platform {} does not exist.",
|
||||
url.id.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
donation_urls.push(models::mod_item::DonationUrl {
|
||||
mod_id: mod_id.into(),
|
||||
platform_id,
|
||||
url: url.url.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let mod_builder = models::mod_item::ModBuilder {
|
||||
mod_id: mod_id.into(),
|
||||
@ -474,15 +534,23 @@ async fn mod_create_inner(
|
||||
source_url: mod_create_data.source_url,
|
||||
wiki_url: mod_create_data.wiki_url,
|
||||
|
||||
license_url: mod_create_data.license_url,
|
||||
discord_url: mod_create_data.discord_url,
|
||||
categories,
|
||||
initial_versions: versions,
|
||||
status: status_id,
|
||||
client_side: client_side_id,
|
||||
server_side: server_side_id,
|
||||
license: license_id,
|
||||
slug: mod_create_data.mod_slug,
|
||||
donation_urls,
|
||||
};
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let response = crate::models::mods::Mod {
|
||||
id: mod_id,
|
||||
slug: mod_builder.slug.clone(),
|
||||
team: team_id.into(),
|
||||
title: mod_builder.title.clone(),
|
||||
description: mod_builder.description.clone(),
|
||||
@ -490,6 +558,13 @@ async fn mod_create_inner(
|
||||
published: now,
|
||||
updated: now,
|
||||
status,
|
||||
license: License {
|
||||
id: mod_create_data.license_id.clone(),
|
||||
name: "".to_string(),
|
||||
url: mod_builder.license_url.clone(),
|
||||
},
|
||||
client_side: mod_create_data.client_side,
|
||||
server_side: mod_create_data.server_side,
|
||||
downloads: 0,
|
||||
categories: mod_create_data.categories,
|
||||
versions: mod_builder
|
||||
@ -501,6 +576,8 @@ async fn mod_create_inner(
|
||||
issues_url: mod_builder.issues_url.clone(),
|
||||
source_url: mod_builder.source_url.clone(),
|
||||
wiki_url: mod_builder.wiki_url.clone(),
|
||||
discord_url: mod_builder.discord_url.clone(),
|
||||
donation_urls: mod_create_data.donation_urls.clone(),
|
||||
};
|
||||
|
||||
let _mod_id = mod_builder.insert(&mut *transaction).await?;
|
||||
@ -598,6 +675,7 @@ async fn create_initial_version(
|
||||
game_versions,
|
||||
loaders,
|
||||
release_channel,
|
||||
featured: version_data.featured,
|
||||
};
|
||||
|
||||
Ok(version)
|
||||
@ -642,7 +720,7 @@ async fn process_icon_upload(
|
||||
}
|
||||
}
|
||||
|
||||
fn get_image_content_type(extension: &str) -> Option<&'static str> {
|
||||
pub fn get_image_content_type(extension: &str) -> Option<&'static str> {
|
||||
let content_type = match &*extension {
|
||||
"bmp" => "image/bmp",
|
||||
"gif" => "image/gif",
|
||||
|
||||
@ -2,10 +2,12 @@ use super::ApiError;
|
||||
use crate::auth::check_is_moderator_from_headers;
|
||||
use crate::database;
|
||||
use crate::models;
|
||||
use crate::models::mods::{ModStatus, VersionType};
|
||||
use crate::models::mods::{ModStatus, VersionType, ModId};
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||
use serde::Deserialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::PgPool;
|
||||
use sqlx::types::chrono::{DateTime, Utc};
|
||||
use crate::models::teams::TeamId;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResultCount {
|
||||
@ -17,6 +19,42 @@ fn default_count() -> i16 {
|
||||
100
|
||||
}
|
||||
|
||||
/// A mod returned from the API moderation routes
|
||||
#[derive(Serialize)]
|
||||
pub struct ModerationMod {
|
||||
/// The ID of the mod, encoded as a base62 string.
|
||||
pub id: ModId,
|
||||
/// The slug of a mod, used for vanity URLs
|
||||
pub slug: Option<String>,
|
||||
/// The team of people that has ownership of this mod.
|
||||
pub team: TeamId,
|
||||
/// The title or name of the mod.
|
||||
pub title: String,
|
||||
/// A short description of the mod.
|
||||
pub description: String,
|
||||
/// The link to the long description of the mod.
|
||||
pub body_url: String,
|
||||
/// The date at which the mod was first published.
|
||||
pub published: DateTime<Utc>,
|
||||
/// The date at which the mod was first published.
|
||||
pub updated: DateTime<Utc>,
|
||||
/// The status of the mod
|
||||
pub status: ModStatus,
|
||||
|
||||
/// The total number of downloads the mod has had.
|
||||
pub downloads: u32,
|
||||
/// The URL of the icon of the mod
|
||||
pub icon_url: Option<String>,
|
||||
/// An optional link to where to submit bugs or issues with the mod.
|
||||
pub issues_url: Option<String>,
|
||||
/// An optional link to the source code for the mod.
|
||||
pub source_url: Option<String>,
|
||||
/// An optional link to the mod's wiki page or other relevant information.
|
||||
pub wiki_url: Option<String>,
|
||||
/// An optional link to the mod's discord
|
||||
pub discord_url: Option<String>,
|
||||
}
|
||||
|
||||
#[get("mods")]
|
||||
pub async fn mods(
|
||||
req: HttpRequest,
|
||||
@ -41,15 +79,14 @@ pub async fn mods(
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| models::mods::Mod {
|
||||
Ok(e.right().map(|m| ModerationMod {
|
||||
id: database::models::ids::ModId(m.id).into(),
|
||||
slug: m.slug,
|
||||
team: database::models::ids::TeamId(m.team_id).into(),
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
body_url: m.body_url,
|
||||
published: m.published,
|
||||
categories: vec![],
|
||||
versions: vec![],
|
||||
icon_url: m.icon_url,
|
||||
issues_url: m.issues_url,
|
||||
source_url: m.source_url,
|
||||
@ -57,9 +94,10 @@ pub async fn mods(
|
||||
updated: m.updated,
|
||||
downloads: m.downloads as u32,
|
||||
wiki_url: m.wiki_url,
|
||||
discord_url: m.discord_url,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<models::mods::Mod>>()
|
||||
.try_collect::<Vec<ModerationMod>>()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
@ -92,6 +130,7 @@ pub async fn versions(
|
||||
id: database::models::ids::VersionId(m.id).into(),
|
||||
mod_id: database::models::ids::ModId(m.mod_id).into(),
|
||||
author_id: database::models::ids::UserId(m.author_id).into(),
|
||||
featured: m.featured,
|
||||
name: m.name,
|
||||
version_number: m.version_number,
|
||||
changelog_url: m.changelog_url,
|
||||
|
||||
@ -3,10 +3,11 @@ use crate::auth::get_user_from_headers;
|
||||
use crate::database;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models;
|
||||
use crate::models::mods::{ModStatus, SearchRequest};
|
||||
use crate::models::mods::{DonationLink, License, ModStatus, SearchRequest, SideType};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::search::{search_for_mod, SearchConfig, SearchError};
|
||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
@ -80,6 +81,53 @@ pub async fn mods_get(
|
||||
Ok(HttpResponse::Ok().json(mods))
|
||||
}
|
||||
|
||||
#[get("@{id}")]
|
||||
pub async fn mod_slug_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let mod_data = database::models::Mod::get_full_from_slug(id, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
if let Some(data) = mod_data {
|
||||
let mut authorized = !data.status.is_hidden();
|
||||
|
||||
if let Some(user) = user_option {
|
||||
if !authorized {
|
||||
if user.role.is_mod() {
|
||||
authorized = true;
|
||||
} else {
|
||||
let user_id: database::models::ids::UserId = user.id.into();
|
||||
|
||||
let mod_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM team_members WHERE id = $1 AND user_id = $2)",
|
||||
data.inner.team_id as database::models::ids::TeamId,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists;
|
||||
|
||||
authorized = mod_exists.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if authorized {
|
||||
return Ok(HttpResponse::Ok().json(convert_mod(data)));
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{id}")]
|
||||
pub async fn mod_get(
|
||||
req: HttpRequest,
|
||||
@ -132,6 +180,7 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
||||
|
||||
models::mods::Mod {
|
||||
id: m.id.into(),
|
||||
slug: m.slug,
|
||||
team: m.team_id.into(),
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
@ -139,6 +188,13 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
||||
published: m.published,
|
||||
updated: m.updated,
|
||||
status: data.status,
|
||||
license: License {
|
||||
id: data.license_id,
|
||||
name: data.license_name,
|
||||
url: m.license_url,
|
||||
},
|
||||
client_side: data.client_side,
|
||||
server_side: data.server_side,
|
||||
downloads: m.downloads as u32,
|
||||
categories: data.categories,
|
||||
versions: data.versions.into_iter().map(|v| v.into()).collect(),
|
||||
@ -146,6 +202,8 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
||||
issues_url: m.issues_url,
|
||||
source_url: m.source_url,
|
||||
wiki_url: m.wiki_url,
|
||||
discord_url: m.discord_url,
|
||||
donation_urls: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,6 +233,28 @@ pub struct EditMod {
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub wiki_url: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub license_url: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub discord_url: Option<Option<String>>,
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
pub license_id: Option<String>,
|
||||
pub client_side: Option<SideType>,
|
||||
pub server_side: Option<SideType>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub slug: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
@ -270,12 +350,10 @@ pub async fn mod_edit(
|
||||
));
|
||||
}
|
||||
|
||||
if status == &ModStatus::Rejected || status == &ModStatus::Approved {
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to set this status".to_string(),
|
||||
));
|
||||
}
|
||||
if (status == &ModStatus::Rejected || status == &ModStatus::Approved) && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to set this status".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let status_id = database::models::StatusId::get_id(&status, &mut *transaction)
|
||||
@ -421,6 +499,199 @@ pub async fn mod_edit(
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(license_url) = &new_mod.license_url {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the license URL of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET license_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
license_url.as_deref(),
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(discord_url) = &new_mod.discord_url {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the discord URL of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET discord_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
discord_url.as_deref(),
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(slug) = &new_mod.slug {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the slug of this mod!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET slug = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
slug.as_deref(),
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(new_side) = &new_mod.client_side {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the side type of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let side_type_id =
|
||||
database::models::SideTypeId::get_id(new_side, &mut *transaction)
|
||||
.await?
|
||||
.expect("No database entry found for side type");
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET client_side = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
side_type_id as database::models::SideTypeId,
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(new_side) = &new_mod.server_side {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the side type of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let side_type_id =
|
||||
database::models::SideTypeId::get_id(new_side, &mut *transaction)
|
||||
.await?
|
||||
.expect("No database entry found for side type");
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET server_side = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
side_type_id as database::models::SideTypeId,
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(license) = &new_mod.license_id {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the license of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let license_id =
|
||||
database::models::categories::License::get_id(license, &mut *transaction)
|
||||
.await?
|
||||
.expect("No database entry found for license");
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET license = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
license_id as database::models::LicenseId,
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(donations) = &new_mod.donation_urls {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the donation links of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods_donations
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
for donation in donations {
|
||||
let platform_id = database::models::DonationPlatformId::get_id(
|
||||
&donation.id,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError(format!(
|
||||
"Platform {} does not exist.",
|
||||
donation.id.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url)
|
||||
VALUES ($1, $2, $3)
|
||||
",
|
||||
id as database::models::ids::ModId,
|
||||
platform_id as database::models::ids::DonationPlatformId,
|
||||
donation.url
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(body) = &new_mod.body {
|
||||
if !perms.contains(Permissions::EDIT_BODY) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
@ -452,6 +723,99 @@ pub async fn mod_edit(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Extension {
|
||||
pub ext: String,
|
||||
}
|
||||
|
||||
#[patch("{id}/icon")]
|
||||
pub async fn mod_icon_edit(
|
||||
web::Query(ext): web::Query<Extension>,
|
||||
req: HttpRequest,
|
||||
info: web::Path<(models::ids::ModId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = super::mod_creation::get_image_content_type(&*ext.ext) {
|
||||
let cdn_url = dotenv::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0;
|
||||
|
||||
let mod_item = database::models::Mod::get(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
mod_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::DatabaseError)?
|
||||
.ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?;
|
||||
|
||||
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to edit this mod's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = mod_item.icon_url {
|
||||
let name = icon.split('/').next();
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut bytes = web::BytesMut::new();
|
||||
while let Some(item) = payload.next().await {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string())
|
||||
})?);
|
||||
}
|
||||
|
||||
if bytes.len() >= 262144 {
|
||||
return Err(ApiError::InvalidInputError(String::from(
|
||||
"Icons must be smaller than 256KiB",
|
||||
)));
|
||||
}
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/icon.{}", id, ext.ext),
|
||||
bytes.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mod_id: database::models::ids::ModId = id.into();
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET icon_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
mod_id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInputError(format!(
|
||||
"Invalid format for mod icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("{id}")]
|
||||
pub async fn mod_delete(
|
||||
req: HttpRequest,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::check_is_admin_from_headers;
|
||||
use crate::database::models;
|
||||
use crate::database::models::categories::{DonationPlatform, License};
|
||||
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
|
||||
use models::categories::{Category, GameVersion, Loader};
|
||||
use sqlx::PgPool;
|
||||
@ -16,7 +17,13 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.service(loader_delete)
|
||||
.service(game_version_list)
|
||||
.service(game_version_create)
|
||||
.service(game_version_delete),
|
||||
.service(game_version_delete)
|
||||
.service(license_create)
|
||||
.service(license_delete)
|
||||
.service(license_list)
|
||||
.service(donation_platform_create)
|
||||
.service(donation_platform_list)
|
||||
.service(donation_platform_delete),
|
||||
);
|
||||
}
|
||||
|
||||
@ -34,14 +41,7 @@ pub async fn category_create(
|
||||
pool: web::Data<PgPool>,
|
||||
category: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = category.into_inner().0;
|
||||
|
||||
@ -56,14 +56,7 @@ pub async fn category_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
category: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
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)?;
|
||||
@ -94,14 +87,7 @@ pub async fn loader_create(
|
||||
pool: web::Data<PgPool>,
|
||||
loader: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = loader.into_inner().0;
|
||||
|
||||
@ -116,14 +102,7 @@ pub async fn loader_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
loader: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
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)?;
|
||||
@ -176,14 +155,7 @@ pub async fn game_version_create(
|
||||
game_version: web::Path<(String,)>,
|
||||
version_data: web::Json<GameVersionData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = game_version.into_inner().0;
|
||||
|
||||
@ -209,14 +181,7 @@ pub async fn game_version_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
game_version: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
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)?;
|
||||
@ -234,3 +199,141 @@ pub async fn game_version_delete(
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct LicenseQueryData {
|
||||
short: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("license")]
|
||||
pub async fn license_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results: Vec<LicenseQueryData> = License::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| LicenseQueryData {
|
||||
short: x.short,
|
||||
name: x.name,
|
||||
})
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct LicenseData {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[put("license/{name}")]
|
||||
pub async fn license_create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
license: web::Path<(String,)>,
|
||||
license_data: web::Json<LicenseData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let short = license.into_inner().0;
|
||||
|
||||
let _id = License::builder()
|
||||
.short(&short)?
|
||||
.name(&license_data.name)?
|
||||
.insert(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[delete("license/{name}")]
|
||||
pub async fn license_delete(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
license: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = license.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
|
||||
let result = License::remove(&name, &mut transaction).await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct DonationPlatformQueryData {
|
||||
short: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("donation_platform")]
|
||||
pub async fn donation_platform_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results: Vec<DonationPlatformQueryData> = DonationPlatform::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| DonationPlatformQueryData {
|
||||
short: x.short,
|
||||
name: x.name,
|
||||
})
|
||||
.collect();
|
||||
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::Ok().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::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use crate::database::models::{TeamMember, User};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::users::{Role, UserId};
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
|
||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[get("user")]
|
||||
pub async fn user_auth_get(
|
||||
@ -44,22 +47,30 @@ pub async fn users_get(
|
||||
|
||||
let users: Vec<crate::models::users::User> = users_data
|
||||
.into_iter()
|
||||
.map(|data| crate::models::users::User {
|
||||
id: data.id.into(),
|
||||
github_id: data.github_id.map(|i| i as u64),
|
||||
username: data.username,
|
||||
name: data.name,
|
||||
email: None,
|
||||
avatar_url: data.avatar_url,
|
||||
bio: data.bio,
|
||||
created: data.created,
|
||||
role: Role::from_string(&*data.role),
|
||||
})
|
||||
.map(convert_user)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(users))
|
||||
}
|
||||
|
||||
#[get("@{id}")]
|
||||
pub async fn user_username_get(
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let user_data = User::get_from_username(id, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(data) = user_data {
|
||||
let response = convert_user(data);
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{id}")]
|
||||
pub async fn user_get(
|
||||
info: web::Path<(UserId,)>,
|
||||
@ -71,23 +82,27 @@ pub async fn user_get(
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(data) = user_data {
|
||||
let response = crate::models::users::User {
|
||||
id: data.id.into(),
|
||||
github_id: data.github_id.map(|i| i as u64),
|
||||
username: data.username,
|
||||
name: data.name,
|
||||
email: None,
|
||||
avatar_url: data.avatar_url,
|
||||
bio: data.bio,
|
||||
created: data.created,
|
||||
role: Role::from_string(&*data.role),
|
||||
};
|
||||
let response = convert_user(data);
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_user(data: crate::database::models::user_item::User) -> crate::models::users::User {
|
||||
crate::models::users::User {
|
||||
id: data.id.into(),
|
||||
github_id: data.github_id.map(|i| i as u64),
|
||||
username: data.username,
|
||||
name: data.name,
|
||||
email: None,
|
||||
avatar_url: data.avatar_url,
|
||||
bio: data.bio,
|
||||
created: data.created,
|
||||
role: Role::from_string(&*data.role),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{user_id}/mods")]
|
||||
pub async fn mods_list(
|
||||
info: web::Path<(UserId,)>,
|
||||
@ -161,6 +176,236 @@ pub async fn teams(
|
||||
Ok(HttpResponse::Ok().json(team_members))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditUser {
|
||||
pub username: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub name: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub email: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub bio: Option<Option<String>>,
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
pub async fn user_edit(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
new_user: web::Json<EditUser>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let user_id = info.into_inner().0;
|
||||
let id: crate::database::models::ids::UserId = user_id.into();
|
||||
|
||||
if user.id == user_id || user.role.is_mod() {
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(username) = &new_user.username {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET username = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
username,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(name) = &new_user.name {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET name = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
name.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(bio) = &new_user.bio {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET bio = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
bio.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(email) = &new_user.email {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET email = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
email.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(role) = &new_user.role {
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the role of this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let role = Role::from_string(role).to_string();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET role = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
role,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to edit this user!".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Extension {
|
||||
pub ext: String,
|
||||
}
|
||||
|
||||
#[patch("{id}/icon")]
|
||||
pub async fn user_icon_edit(
|
||||
web::Query(ext): web::Query<Extension>,
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = super::mod_creation::get_image_content_type(&*ext.ext) {
|
||||
let cdn_url = dotenv::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0;
|
||||
|
||||
if user.id != id && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to edit this user's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut icon_url = user.avatar_url;
|
||||
|
||||
if user.id != id {
|
||||
let new_user = User::get(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(new) = new_user {
|
||||
icon_url = new.avatar_url;
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = icon_url {
|
||||
if icon.starts_with(&cdn_url) {
|
||||
let name = icon.split('/').next();
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut bytes = web::BytesMut::new();
|
||||
while let Some(item) = payload.next().await {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string())
|
||||
})?);
|
||||
}
|
||||
|
||||
if bytes.len() >= 262144 {
|
||||
return Err(ApiError::InvalidInputError(String::from(
|
||||
"Icons must be smaller than 256KiB",
|
||||
)));
|
||||
}
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("user/{}/icon.{}", id, ext.ext),
|
||||
bytes.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mod_id: crate::database::models::ids::UserId = id.into();
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET avatar_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
mod_id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInputError(format!(
|
||||
"Invalid format for user icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this actually do stuff
|
||||
#[delete("{id}")]
|
||||
pub async fn user_delete(
|
||||
|
||||
@ -24,6 +24,7 @@ pub struct InitialVersionData {
|
||||
pub game_versions: Vec<GameVersion>,
|
||||
pub release_channel: VersionType,
|
||||
pub loaders: Vec<ModLoader>,
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@ -265,6 +266,7 @@ async fn version_create_inner(
|
||||
game_versions,
|
||||
loaders,
|
||||
release_channel,
|
||||
featured: version_create_data.featured,
|
||||
});
|
||||
|
||||
continue;
|
||||
@ -298,6 +300,7 @@ async fn version_create_inner(
|
||||
id: builder.version_id.into(),
|
||||
mod_id: builder.mod_id.into(),
|
||||
author_id: user.id,
|
||||
featured: builder.featured,
|
||||
name: builder.name.clone(),
|
||||
version_number: builder.version_number.clone(),
|
||||
changelog_url: builder.changelog_url.clone(),
|
||||
@ -324,6 +327,7 @@ async fn version_create_inner(
|
||||
.collect(),
|
||||
url: file.url.clone(),
|
||||
filename: file.filename.clone(),
|
||||
primary: file.primary,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
dependencies: version_data.dependencies,
|
||||
@ -528,6 +532,7 @@ pub async fn upload_file(
|
||||
// bytes, but this is the string version.
|
||||
hash: upload_data.content_sha1.into_bytes(),
|
||||
}],
|
||||
primary: uploaded_files.len() == 1,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use crate::database;
|
||||
use crate::{database, Pepper};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models;
|
||||
use crate::models::teams::Permissions;
|
||||
@ -151,6 +151,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
||||
mod_id: data.mod_id.into(),
|
||||
author_id: data.author_id.into(),
|
||||
|
||||
featured: data.featured,
|
||||
name: data.name,
|
||||
version_number: data.version_number,
|
||||
changelog_url: data.changelog_url,
|
||||
@ -178,6 +179,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
||||
.map(|(k, v)| Some((k, String::from_utf8(v).ok()?)))
|
||||
.collect::<Option<_>>()
|
||||
.unwrap_or_else(Default::default),
|
||||
primary: f.primary,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@ -204,6 +206,8 @@ pub struct EditVersion {
|
||||
pub game_versions: Option<Vec<models::mods::GameVersion>>,
|
||||
pub loaders: Option<Vec<models::mods::ModLoader>>,
|
||||
pub accepted: Option<bool>,
|
||||
pub featured: Option<bool>,
|
||||
pub primary_file: Option<(String, String)>,
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
@ -388,6 +392,65 @@ pub async fn version_edit(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(featured) = &new_version.featured {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
SET featured = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
featured,
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(primary_file) = &new_version.primary_file {
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM files
|
||||
INNER JOIN hashes ON hash = $1 AND algorithm = $2
|
||||
",
|
||||
primary_file.1.as_bytes(),
|
||||
primary_file.0
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError(format!(
|
||||
"Specified file with hash {} does not exist.",
|
||||
primary_file.1.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE files
|
||||
SET is_primary = FALSE
|
||||
WHERE (version_id = $1)
|
||||
",
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE files
|
||||
SET is_primary = TRUE
|
||||
WHERE (id = $1)
|
||||
",
|
||||
result.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(body) = &new_version.changelog {
|
||||
let mod_id: models::mods::ModId = version_item.mod_id.into();
|
||||
let body_path = format!(
|
||||
@ -518,6 +581,102 @@ pub async fn get_version_from_hash(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DownloadRedirect {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}/download
|
||||
#[get("{version_id}/download")]
|
||||
pub async fn download_version(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
pepper: web::Data<Pepper>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hash = info.into_inner().0;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.url url, f.id id, f.version_id version_id, v.mod_id mod_id FROM files f
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
INNER JOIN hashes ON hash = $1 AND algorithm = $2
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(id) = result {
|
||||
let real_ip = req.connection_info();
|
||||
let ip_option = real_ip.realip_remote_addr();
|
||||
|
||||
if let Some(ip) = ip_option {
|
||||
let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest();
|
||||
|
||||
let download_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)",
|
||||
id.version_id,
|
||||
hash,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists.unwrap_or(false);
|
||||
|
||||
if !download_exists {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO downloads (
|
||||
version_id, identifier
|
||||
)
|
||||
VALUES (
|
||||
$1, $2
|
||||
)
|
||||
",
|
||||
id.version_id,
|
||||
hash
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
id.version_id,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
id.mod_id,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.header("Location", &*id.url)
|
||||
.json(DownloadRedirect { url: id.url }))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}
|
||||
#[delete("{version_id}")]
|
||||
pub async fn delete_file(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user