You've already forked AstralRinth
forked from didirus/AstralRinth
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:
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 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 struct CategoryBuilder<'a> {
|
||||||
pub name: Option<&'a str>,
|
pub name: Option<&'a str>,
|
||||||
}
|
}
|
||||||
@@ -453,3 +465,293 @@ impl<'a> GameVersionBuilder<'a> {
|
|||||||
Ok(GameVersionId(result.id))
|
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)]
|
#[derive(Copy, Clone, Debug, Type)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
pub struct StatusId(pub i32);
|
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)]
|
#[derive(Copy, Clone, Debug, Type)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
|
|||||||
@@ -79,3 +79,44 @@ impl ids::StatusId {
|
|||||||
Ok(result.map(|r| ids::StatusId(r.id)))
|
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::*;
|
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 struct ModBuilder {
|
||||||
pub mod_id: ModId,
|
pub mod_id: ModId,
|
||||||
pub team_id: TeamId,
|
pub team_id: TeamId,
|
||||||
@@ -10,9 +41,16 @@ pub struct ModBuilder {
|
|||||||
pub issues_url: Option<String>,
|
pub issues_url: Option<String>,
|
||||||
pub source_url: Option<String>,
|
pub source_url: Option<String>,
|
||||||
pub wiki_url: Option<String>,
|
pub wiki_url: Option<String>,
|
||||||
|
pub license_url: Option<String>,
|
||||||
|
pub discord_url: Option<String>,
|
||||||
pub categories: Vec<CategoryId>,
|
pub categories: Vec<CategoryId>,
|
||||||
pub initial_versions: Vec<super::version_item::VersionBuilder>,
|
pub initial_versions: Vec<super::version_item::VersionBuilder>,
|
||||||
pub status: StatusId,
|
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 {
|
impl ModBuilder {
|
||||||
@@ -34,6 +72,12 @@ impl ModBuilder {
|
|||||||
issues_url: self.issues_url,
|
issues_url: self.issues_url,
|
||||||
source_url: self.source_url,
|
source_url: self.source_url,
|
||||||
wiki_url: self.wiki_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?;
|
mod_struct.insert(&mut *transaction).await?;
|
||||||
|
|
||||||
@@ -42,6 +86,11 @@ impl ModBuilder {
|
|||||||
version.insert(&mut *transaction).await?;
|
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 {
|
for category in self.categories {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -73,6 +122,12 @@ pub struct Mod {
|
|||||||
pub issues_url: Option<String>,
|
pub issues_url: Option<String>,
|
||||||
pub source_url: Option<String>,
|
pub source_url: Option<String>,
|
||||||
pub wiki_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 {
|
impl Mod {
|
||||||
@@ -85,12 +140,16 @@ impl Mod {
|
|||||||
INSERT INTO mods (
|
INSERT INTO mods (
|
||||||
id, team_id, title, description, body_url,
|
id, team_id, title, description, body_url,
|
||||||
published, downloads, icon_url, issues_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 (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6, $7, $8, $9,
|
$6, $7, $8, $9,
|
||||||
$10, $11, $12
|
$10, $11, $12, $13,
|
||||||
|
$14, $15, $16, $17,
|
||||||
|
$18
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as ModId,
|
self.id as ModId,
|
||||||
@@ -104,7 +163,13 @@ impl Mod {
|
|||||||
self.issues_url.as_ref(),
|
self.issues_url.as_ref(),
|
||||||
self.source_url.as_ref(),
|
self.source_url.as_ref(),
|
||||||
self.wiki_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)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -121,8 +186,8 @@ impl Mod {
|
|||||||
SELECT title, description, downloads,
|
SELECT title, description, downloads,
|
||||||
icon_url, body_url, published,
|
icon_url, body_url, published,
|
||||||
updated, status,
|
updated, status,
|
||||||
issues_url, source_url, wiki_url,
|
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||||
team_id
|
team_id, client_side, server_side, license, slug
|
||||||
FROM mods
|
FROM mods
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
@@ -145,7 +210,13 @@ impl Mod {
|
|||||||
issues_url: row.issues_url,
|
issues_url: row.issues_url,
|
||||||
source_url: row.source_url,
|
source_url: row.source_url,
|
||||||
wiki_url: row.wiki_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),
|
status: StatusId(row.status),
|
||||||
|
server_side: SideTypeId(row.server_side),
|
||||||
|
license: LicenseId(row.license),
|
||||||
|
slug: row.slug,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -164,8 +235,8 @@ impl Mod {
|
|||||||
SELECT id, title, description, downloads,
|
SELECT id, title, description, downloads,
|
||||||
icon_url, body_url, published,
|
icon_url, body_url, published,
|
||||||
updated, status,
|
updated, status,
|
||||||
issues_url, source_url, wiki_url,
|
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||||
team_id
|
team_id, client_side, server_side, license, slug
|
||||||
FROM mods
|
FROM mods
|
||||||
WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))
|
WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||||
",
|
",
|
||||||
@@ -186,7 +257,13 @@ impl Mod {
|
|||||||
issues_url: m.issues_url,
|
issues_url: m.issues_url,
|
||||||
source_url: m.source_url,
|
source_url: m.source_url,
|
||||||
wiki_url: m.wiki_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),
|
status: StatusId(m.status),
|
||||||
|
server_side: SideTypeId(m.server_side),
|
||||||
|
license: LicenseId(m.license),
|
||||||
|
slug: m.slug,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<Mod>>()
|
.try_collect::<Vec<Mod>>()
|
||||||
@@ -227,6 +304,16 @@ impl Mod {
|
|||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM mods_donations
|
||||||
|
WHERE joining_mod_id = $1
|
||||||
|
",
|
||||||
|
id as ModId,
|
||||||
|
)
|
||||||
|
.execute(exec)
|
||||||
|
.await?;
|
||||||
|
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
let versions: Vec<VersionId> = sqlx::query!(
|
let versions: Vec<VersionId> = sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -277,6 +364,30 @@ impl Mod {
|
|||||||
Ok(Some(()))
|
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>(
|
pub async fn get_full<'a, 'b, E>(
|
||||||
id: ModId,
|
id: ModId,
|
||||||
executor: E,
|
executor: E,
|
||||||
@@ -312,6 +423,24 @@ impl Mod {
|
|||||||
.try_collect::<Vec<VersionId>>()
|
.try_collect::<Vec<VersionId>>()
|
||||||
.await?;
|
.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!(
|
let status = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT status FROM statuses
|
SELECT status FROM statuses
|
||||||
@@ -323,11 +452,48 @@ impl Mod {
|
|||||||
.await?
|
.await?
|
||||||
.status;
|
.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 {
|
Ok(Some(QueryMod {
|
||||||
inner,
|
inner,
|
||||||
categories,
|
categories,
|
||||||
versions,
|
versions,
|
||||||
|
donation_urls: donations,
|
||||||
status: crate::models::mods::ModStatus::from_str(&status),
|
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 {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -351,5 +517,10 @@ pub struct QueryMod {
|
|||||||
|
|
||||||
pub categories: Vec<String>,
|
pub categories: Vec<String>,
|
||||||
pub versions: Vec<VersionId>,
|
pub versions: Vec<VersionId>,
|
||||||
|
pub donation_urls: Vec<DonationUrl>,
|
||||||
pub status: crate::models::mods::ModStatus,
|
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,
|
name: m.member_name,
|
||||||
role: m.role,
|
role: m.role,
|
||||||
permissions: Permissions::from_bits(m.permissions as u64)
|
permissions: Permissions::from_bits(m.permissions as u64)
|
||||||
.ok_or_else(|| super::DatabaseError::BitflagError)?,
|
.ok_or(super::DatabaseError::BitflagError)?,
|
||||||
accepted: m.accepted,
|
accepted: m.accepted,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
@@ -297,7 +297,7 @@ impl TeamMember {
|
|||||||
name: m.member_name,
|
name: m.member_name,
|
||||||
role: m.role,
|
role: m.role,
|
||||||
permissions: Permissions::from_bits(m.permissions as u64)
|
permissions: Permissions::from_bits(m.permissions as u64)
|
||||||
.ok_or_else(|| super::DatabaseError::BitflagError)?,
|
.ok_or(super::DatabaseError::BitflagError)?,
|
||||||
accepted: m.accepted,
|
accepted: m.accepted,
|
||||||
}))
|
}))
|
||||||
} else {
|
} 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>
|
pub async fn get_many<'a, E>(user_ids: Vec<UserId>, exec: E) -> Result<Vec<User>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ pub struct VersionBuilder {
|
|||||||
pub game_versions: Vec<GameVersionId>,
|
pub game_versions: Vec<GameVersionId>,
|
||||||
pub loaders: Vec<LoaderId>,
|
pub loaders: Vec<LoaderId>,
|
||||||
pub release_channel: ChannelId,
|
pub release_channel: ChannelId,
|
||||||
|
pub featured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct VersionFileBuilder {
|
pub struct VersionFileBuilder {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub hashes: Vec<HashBuilder>,
|
pub hashes: Vec<HashBuilder>,
|
||||||
|
pub primary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VersionFileBuilder {
|
impl VersionFileBuilder {
|
||||||
@@ -81,6 +83,7 @@ impl VersionBuilder {
|
|||||||
downloads: 0,
|
downloads: 0,
|
||||||
release_channel: self.release_channel,
|
release_channel: self.release_channel,
|
||||||
accepted: false,
|
accepted: false,
|
||||||
|
featured: self.featured,
|
||||||
};
|
};
|
||||||
|
|
||||||
version.insert(&mut *transaction).await?;
|
version.insert(&mut *transaction).await?;
|
||||||
@@ -154,6 +157,7 @@ pub struct Version {
|
|||||||
pub downloads: i32,
|
pub downloads: i32,
|
||||||
pub release_channel: ChannelId,
|
pub release_channel: ChannelId,
|
||||||
pub accepted: bool,
|
pub accepted: bool,
|
||||||
|
pub featured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Version {
|
impl Version {
|
||||||
@@ -166,13 +170,13 @@ impl Version {
|
|||||||
INSERT INTO versions (
|
INSERT INTO versions (
|
||||||
id, mod_id, author_id, name, version_number,
|
id, mod_id, author_id, name, version_number,
|
||||||
changelog_url, date_published,
|
changelog_url, date_published,
|
||||||
downloads, release_channel, accepted
|
downloads, release_channel, accepted, featured
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6, $7,
|
$6, $7,
|
||||||
$8, $9,
|
$8, $9,
|
||||||
$10
|
$10, $11
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as VersionId,
|
self.id as VersionId,
|
||||||
@@ -184,7 +188,8 @@ impl Version {
|
|||||||
self.date_published,
|
self.date_published,
|
||||||
self.downloads,
|
self.downloads,
|
||||||
self.release_channel as ChannelId,
|
self.release_channel as ChannelId,
|
||||||
self.accepted
|
self.accepted,
|
||||||
|
self.featured
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -234,7 +239,7 @@ impl Version {
|
|||||||
|
|
||||||
let files = sqlx::query!(
|
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
|
WHERE files.version_id = $1
|
||||||
",
|
",
|
||||||
id as VersionId,
|
id as VersionId,
|
||||||
@@ -246,6 +251,7 @@ impl Version {
|
|||||||
version_id: id,
|
version_id: id,
|
||||||
url: c.url,
|
url: c.url,
|
||||||
filename: c.filename,
|
filename: c.filename,
|
||||||
|
primary: c.is_primary,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<VersionFile>>()
|
.try_collect::<Vec<VersionFile>>()
|
||||||
@@ -367,7 +373,7 @@ impl Version {
|
|||||||
"
|
"
|
||||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||||
v.changelog_url, v.date_published, v.downloads,
|
v.changelog_url, v.date_published, v.downloads,
|
||||||
v.release_channel, v.accepted
|
v.release_channel, v.accepted, v.featured
|
||||||
FROM versions v
|
FROM versions v
|
||||||
WHERE v.id = $1
|
WHERE v.id = $1
|
||||||
",
|
",
|
||||||
@@ -388,6 +394,7 @@ impl Version {
|
|||||||
downloads: row.downloads,
|
downloads: row.downloads,
|
||||||
release_channel: ChannelId(row.release_channel),
|
release_channel: ChannelId(row.release_channel),
|
||||||
accepted: row.accepted,
|
accepted: row.accepted,
|
||||||
|
featured: row.featured,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -408,7 +415,7 @@ impl Version {
|
|||||||
"
|
"
|
||||||
SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,
|
SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,
|
||||||
v.changelog_url, v.date_published, v.downloads,
|
v.changelog_url, v.date_published, v.downloads,
|
||||||
v.release_channel, accepted
|
v.release_channel, v.accepted, v.featured
|
||||||
FROM versions v
|
FROM versions v
|
||||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||||
",
|
",
|
||||||
@@ -427,6 +434,7 @@ impl Version {
|
|||||||
downloads: v.downloads,
|
downloads: v.downloads,
|
||||||
release_channel: ChannelId(v.release_channel),
|
release_channel: ChannelId(v.release_channel),
|
||||||
accepted: v.accepted,
|
accepted: v.accepted,
|
||||||
|
featured: v.featured,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<Version>>()
|
.try_collect::<Vec<Version>>()
|
||||||
@@ -446,7 +454,7 @@ impl Version {
|
|||||||
"
|
"
|
||||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||||
v.changelog_url, v.date_published, v.downloads,
|
v.changelog_url, v.date_published, v.downloads,
|
||||||
release_channels.channel, v.accepted
|
release_channels.channel, v.accepted, v.featured
|
||||||
FROM versions v
|
FROM versions v
|
||||||
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
||||||
WHERE v.id = $1
|
WHERE v.id = $1
|
||||||
@@ -487,7 +495,7 @@ impl Version {
|
|||||||
|
|
||||||
let mut files = sqlx::query!(
|
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
|
WHERE files.version_id = $1
|
||||||
",
|
",
|
||||||
id as VersionId,
|
id as VersionId,
|
||||||
@@ -499,6 +507,7 @@ impl Version {
|
|||||||
url: c.url,
|
url: c.url,
|
||||||
filename: c.filename,
|
filename: c.filename,
|
||||||
hashes: std::collections::HashMap::new(),
|
hashes: std::collections::HashMap::new(),
|
||||||
|
primary: c.is_primary,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<QueryFile>>()
|
.try_collect::<Vec<QueryFile>>()
|
||||||
@@ -535,6 +544,7 @@ impl Version {
|
|||||||
loaders,
|
loaders,
|
||||||
game_versions,
|
game_versions,
|
||||||
accepted: row.accepted,
|
accepted: row.accepted,
|
||||||
|
featured: row.featured,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -564,6 +574,7 @@ pub struct VersionFile {
|
|||||||
pub version_id: VersionId,
|
pub version_id: VersionId,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
|
pub primary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileHash {
|
pub struct FileHash {
|
||||||
@@ -587,6 +598,7 @@ pub struct QueryVersion {
|
|||||||
pub game_versions: Vec<String>,
|
pub game_versions: Vec<String>,
|
||||||
pub loaders: Vec<String>,
|
pub loaders: Vec<String>,
|
||||||
pub accepted: bool,
|
pub accepted: bool,
|
||||||
|
pub featured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct QueryFile {
|
pub struct QueryFile {
|
||||||
@@ -594,4 +606,5 @@ pub struct QueryFile {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub hashes: std::collections::HashMap<String, Vec<u8>>,
|
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,
|
allow_missing_vars: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Pepper {
|
||||||
|
pub pepper: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
dotenv::dotenv().ok();
|
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 indexing_queue = Arc::new(search::indexing::queue::CreationQueue::new());
|
||||||
|
|
||||||
let queue_ref = indexing_queue.clone();
|
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);
|
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")
|
let allowed_origins = dotenv::var("CORS_ORIGINS")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| serde_json::from_str::<Vec<String>>(&s).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(file_host.clone())
|
||||||
.data(indexing_queue.clone())
|
.data(indexing_queue.clone())
|
||||||
.data(search_config.clone())
|
.data(search_config.clone())
|
||||||
|
.data(ip_salt.clone())
|
||||||
.service(routes::index_get)
|
.service(routes::index_get)
|
||||||
.service(
|
.service(
|
||||||
web::scope("/api/v1/")
|
web::scope("/api/v1/")
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ pub struct VersionId(pub u64);
|
|||||||
pub struct Mod {
|
pub struct Mod {
|
||||||
/// The ID of the mod, encoded as a base62 string.
|
/// The ID of the mod, encoded as a base62 string.
|
||||||
pub id: ModId,
|
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.
|
/// The team of people that has ownership of this mod.
|
||||||
pub team: TeamId,
|
pub team: TeamId,
|
||||||
/// The title or name of the mod.
|
/// The title or name of the mod.
|
||||||
@@ -35,6 +37,13 @@ pub struct Mod {
|
|||||||
pub updated: DateTime<Utc>,
|
pub updated: DateTime<Utc>,
|
||||||
/// The status of the mod
|
/// The status of the mod
|
||||||
pub status: ModStatus,
|
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.
|
/// The total number of downloads the mod has had.
|
||||||
pub downloads: u32,
|
pub downloads: u32,
|
||||||
@@ -42,7 +51,7 @@ pub struct Mod {
|
|||||||
pub categories: Vec<String>,
|
pub categories: Vec<String>,
|
||||||
/// A list of ids for versions of the mod.
|
/// A list of ids for versions of the mod.
|
||||||
pub versions: Vec<VersionId>,
|
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>,
|
pub icon_url: Option<String>,
|
||||||
/// An optional link to where to submit bugs or issues with the mod.
|
/// An optional link to where to submit bugs or issues with the mod.
|
||||||
pub issues_url: Option<String>,
|
pub issues_url: Option<String>,
|
||||||
@@ -50,6 +59,60 @@ pub struct Mod {
|
|||||||
pub source_url: Option<String>,
|
pub source_url: Option<String>,
|
||||||
/// An optional link to the mod's wiki page or other relevant information.
|
/// An optional link to the mod's wiki page or other relevant information.
|
||||||
pub wiki_url: Option<String>,
|
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.
|
/// 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 {
|
impl std::fmt::Display for ModStatus {
|
||||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self {
|
write!(fmt, "{}", self.as_str())
|
||||||
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"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,10 +172,7 @@ impl ModStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_searchable(&self) -> bool {
|
pub fn is_searchable(&self) -> bool {
|
||||||
match self {
|
matches!(self, ModStatus::Approved)
|
||||||
ModStatus::Approved => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +185,8 @@ pub struct Version {
|
|||||||
pub mod_id: ModId,
|
pub mod_id: ModId,
|
||||||
/// The ID of the author who published this version
|
/// The ID of the author who published this version
|
||||||
pub author_id: UserId,
|
pub author_id: UserId,
|
||||||
|
/// Whether the version is featured or not
|
||||||
|
pub featured: bool,
|
||||||
|
|
||||||
/// The name of this version
|
/// The name of this version
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -166,6 +221,8 @@ pub struct VersionFile {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
/// The filename of the file.
|
/// The filename of the file.
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
|
/// Whether the file is the primary file of a version
|
||||||
|
pub primary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
|||||||
|
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("mod")
|
web::scope("mod")
|
||||||
|
.service(mods::mod_slug_get)
|
||||||
.service(mods::mod_get)
|
.service(mods::mod_get)
|
||||||
.service(mods::mod_delete)
|
.service(mods::mod_delete)
|
||||||
.service(mods::mod_edit)
|
.service(mods::mod_edit)
|
||||||
|
.service(mods::mod_icon_edit)
|
||||||
.service(web::scope("{mod_id}").service(versions::version_list)),
|
.service(web::scope("{mod_id}").service(versions::version_list)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,7 +48,8 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
|||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("version_file")
|
web::scope("version_file")
|
||||||
.service(versions::delete_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(users::users_get);
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("user")
|
web::scope("user")
|
||||||
|
.service(users::user_username_get)
|
||||||
.service(users::user_get)
|
.service(users::user_get)
|
||||||
.service(users::mods_list)
|
.service(users::mods_list)
|
||||||
.service(users::user_delete)
|
.service(users::user_delete)
|
||||||
|
.service(users::user_edit)
|
||||||
|
.service(users::user_icon_edit)
|
||||||
.service(users::teams),
|
.service(users::teams),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -84,6 +90,8 @@ pub fn moderation_config(cfg: &mut web::ServiceConfig) {
|
|||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum ApiError {
|
pub enum ApiError {
|
||||||
|
#[error("Environment Error")]
|
||||||
|
EnvError(#[from] dotenv::Error),
|
||||||
#[error("Error while uploading file")]
|
#[error("Error while uploading file")]
|
||||||
FileHostingError(#[from] FileHostingError),
|
FileHostingError(#[from] FileHostingError),
|
||||||
#[error("Internal server error")]
|
#[error("Internal server error")]
|
||||||
@@ -103,6 +111,7 @@ pub enum ApiError {
|
|||||||
impl actix_web::ResponseError for ApiError {
|
impl actix_web::ResponseError for ApiError {
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||||
match self {
|
match self {
|
||||||
|
ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||||
ApiError::CustomAuthenticationError(..) => 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(
|
actix_web::web::HttpResponse::build(self.status_code()).json(
|
||||||
crate::models::error::ApiError {
|
crate::models::error::ApiError {
|
||||||
error: match self {
|
error: match self {
|
||||||
|
ApiError::EnvError(..) => "environment_error",
|
||||||
ApiError::DatabaseError(..) => "database_error",
|
ApiError::DatabaseError(..) => "database_error",
|
||||||
ApiError::AuthenticationError(..) => "unauthorized",
|
ApiError::AuthenticationError(..) => "unauthorized",
|
||||||
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::auth::{get_user_from_headers, AuthenticationError};
|
|||||||
use crate::database::models;
|
use crate::database::models;
|
||||||
use crate::file_hosting::{FileHost, FileHostingError};
|
use crate::file_hosting::{FileHost, FileHostingError};
|
||||||
use crate::models::error::ApiError;
|
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::models::users::UserId;
|
||||||
use crate::routes::version_creation::InitialVersionData;
|
use crate::routes::version_creation::InitialVersionData;
|
||||||
use crate::search::indexing::{queue::CreationQueue, IndexingError};
|
use crate::search::indexing::{queue::CreationQueue, IndexingError};
|
||||||
@@ -99,6 +99,8 @@ impl actix_web::ResponseError for CreateError {
|
|||||||
struct ModCreateData {
|
struct ModCreateData {
|
||||||
/// The title or name of the mod.
|
/// The title or name of the mod.
|
||||||
pub mod_name: String,
|
pub mod_name: String,
|
||||||
|
/// The slug of a mod, used for vanity URLs
|
||||||
|
pub mod_slug: Option<String>,
|
||||||
/// A short description of the mod.
|
/// A short description of the mod.
|
||||||
pub mod_description: String,
|
pub mod_description: String,
|
||||||
/// A long description of the mod, in markdown.
|
/// A long description of the mod, in markdown.
|
||||||
@@ -113,8 +115,20 @@ struct ModCreateData {
|
|||||||
pub source_url: Option<String>,
|
pub source_url: Option<String>,
|
||||||
/// An optional link to the mod's wiki page or other relevant information.
|
/// An optional link to the mod's wiki page or other relevant information.
|
||||||
pub wiki_url: Option<String>,
|
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.
|
/// An optional boolean. If true, the mod will be created as a draft.
|
||||||
pub is_draft: Option<bool>,
|
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 {
|
pub struct UploadedFile {
|
||||||
@@ -461,7 +475,53 @@ async fn mod_create_inner(
|
|||||||
|
|
||||||
let status_id = models::StatusId::get_id(&status, &mut *transaction)
|
let status_id = models::StatusId::get_id(&status, &mut *transaction)
|
||||||
.await?
|
.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 {
|
let mod_builder = models::mod_item::ModBuilder {
|
||||||
mod_id: mod_id.into(),
|
mod_id: mod_id.into(),
|
||||||
@@ -474,15 +534,23 @@ async fn mod_create_inner(
|
|||||||
source_url: mod_create_data.source_url,
|
source_url: mod_create_data.source_url,
|
||||||
wiki_url: mod_create_data.wiki_url,
|
wiki_url: mod_create_data.wiki_url,
|
||||||
|
|
||||||
|
license_url: mod_create_data.license_url,
|
||||||
|
discord_url: mod_create_data.discord_url,
|
||||||
categories,
|
categories,
|
||||||
initial_versions: versions,
|
initial_versions: versions,
|
||||||
status: status_id,
|
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 now = chrono::Utc::now();
|
||||||
|
|
||||||
let response = crate::models::mods::Mod {
|
let response = crate::models::mods::Mod {
|
||||||
id: mod_id,
|
id: mod_id,
|
||||||
|
slug: mod_builder.slug.clone(),
|
||||||
team: team_id.into(),
|
team: team_id.into(),
|
||||||
title: mod_builder.title.clone(),
|
title: mod_builder.title.clone(),
|
||||||
description: mod_builder.description.clone(),
|
description: mod_builder.description.clone(),
|
||||||
@@ -490,6 +558,13 @@ async fn mod_create_inner(
|
|||||||
published: now,
|
published: now,
|
||||||
updated: now,
|
updated: now,
|
||||||
status,
|
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,
|
downloads: 0,
|
||||||
categories: mod_create_data.categories,
|
categories: mod_create_data.categories,
|
||||||
versions: mod_builder
|
versions: mod_builder
|
||||||
@@ -501,6 +576,8 @@ async fn mod_create_inner(
|
|||||||
issues_url: mod_builder.issues_url.clone(),
|
issues_url: mod_builder.issues_url.clone(),
|
||||||
source_url: mod_builder.source_url.clone(),
|
source_url: mod_builder.source_url.clone(),
|
||||||
wiki_url: mod_builder.wiki_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?;
|
let _mod_id = mod_builder.insert(&mut *transaction).await?;
|
||||||
@@ -598,6 +675,7 @@ async fn create_initial_version(
|
|||||||
game_versions,
|
game_versions,
|
||||||
loaders,
|
loaders,
|
||||||
release_channel,
|
release_channel,
|
||||||
|
featured: version_data.featured,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(version)
|
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 {
|
let content_type = match &*extension {
|
||||||
"bmp" => "image/bmp",
|
"bmp" => "image/bmp",
|
||||||
"gif" => "image/gif",
|
"gif" => "image/gif",
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ use super::ApiError;
|
|||||||
use crate::auth::check_is_moderator_from_headers;
|
use crate::auth::check_is_moderator_from_headers;
|
||||||
use crate::database;
|
use crate::database;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::mods::{ModStatus, VersionType};
|
use crate::models::mods::{ModStatus, VersionType, ModId};
|
||||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||||
use serde::Deserialize;
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use sqlx::types::chrono::{DateTime, Utc};
|
||||||
|
use crate::models::teams::TeamId;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ResultCount {
|
pub struct ResultCount {
|
||||||
@@ -17,6 +19,42 @@ fn default_count() -> i16 {
|
|||||||
100
|
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")]
|
#[get("mods")]
|
||||||
pub async fn mods(
|
pub async fn mods(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
@@ -41,15 +79,14 @@ pub async fn mods(
|
|||||||
)
|
)
|
||||||
.fetch_many(&**pool)
|
.fetch_many(&**pool)
|
||||||
.try_filter_map(|e| async {
|
.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(),
|
id: database::models::ids::ModId(m.id).into(),
|
||||||
|
slug: m.slug,
|
||||||
team: database::models::ids::TeamId(m.team_id).into(),
|
team: database::models::ids::TeamId(m.team_id).into(),
|
||||||
title: m.title,
|
title: m.title,
|
||||||
description: m.description,
|
description: m.description,
|
||||||
body_url: m.body_url,
|
body_url: m.body_url,
|
||||||
published: m.published,
|
published: m.published,
|
||||||
categories: vec![],
|
|
||||||
versions: vec![],
|
|
||||||
icon_url: m.icon_url,
|
icon_url: m.icon_url,
|
||||||
issues_url: m.issues_url,
|
issues_url: m.issues_url,
|
||||||
source_url: m.source_url,
|
source_url: m.source_url,
|
||||||
@@ -57,9 +94,10 @@ pub async fn mods(
|
|||||||
updated: m.updated,
|
updated: m.updated,
|
||||||
downloads: m.downloads as u32,
|
downloads: m.downloads as u32,
|
||||||
wiki_url: m.wiki_url,
|
wiki_url: m.wiki_url,
|
||||||
|
discord_url: m.discord_url,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<models::mods::Mod>>()
|
.try_collect::<Vec<ModerationMod>>()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
@@ -92,6 +130,7 @@ pub async fn versions(
|
|||||||
id: database::models::ids::VersionId(m.id).into(),
|
id: database::models::ids::VersionId(m.id).into(),
|
||||||
mod_id: database::models::ids::ModId(m.mod_id).into(),
|
mod_id: database::models::ids::ModId(m.mod_id).into(),
|
||||||
author_id: database::models::ids::UserId(m.author_id).into(),
|
author_id: database::models::ids::UserId(m.author_id).into(),
|
||||||
|
featured: m.featured,
|
||||||
name: m.name,
|
name: m.name,
|
||||||
version_number: m.version_number,
|
version_number: m.version_number,
|
||||||
changelog_url: m.changelog_url,
|
changelog_url: m.changelog_url,
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ use crate::auth::get_user_from_headers;
|
|||||||
use crate::database;
|
use crate::database;
|
||||||
use crate::file_hosting::FileHost;
|
use crate::file_hosting::FileHost;
|
||||||
use crate::models;
|
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::models::teams::Permissions;
|
||||||
use crate::search::{search_for_mod, SearchConfig, SearchError};
|
use crate::search::{search_for_mod, SearchConfig, SearchError};
|
||||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||||
|
use futures::StreamExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -80,6 +81,53 @@ pub async fn mods_get(
|
|||||||
Ok(HttpResponse::Ok().json(mods))
|
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}")]
|
#[get("{id}")]
|
||||||
pub async fn mod_get(
|
pub async fn mod_get(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
@@ -132,6 +180,7 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
|||||||
|
|
||||||
models::mods::Mod {
|
models::mods::Mod {
|
||||||
id: m.id.into(),
|
id: m.id.into(),
|
||||||
|
slug: m.slug,
|
||||||
team: m.team_id.into(),
|
team: m.team_id.into(),
|
||||||
title: m.title,
|
title: m.title,
|
||||||
description: m.description,
|
description: m.description,
|
||||||
@@ -139,6 +188,13 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
|||||||
published: m.published,
|
published: m.published,
|
||||||
updated: m.updated,
|
updated: m.updated,
|
||||||
status: data.status,
|
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,
|
downloads: m.downloads as u32,
|
||||||
categories: data.categories,
|
categories: data.categories,
|
||||||
versions: data.versions.into_iter().map(|v| v.into()).collect(),
|
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,
|
issues_url: m.issues_url,
|
||||||
source_url: m.source_url,
|
source_url: m.source_url,
|
||||||
wiki_url: m.wiki_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"
|
with = "::serde_with::rust::double_option"
|
||||||
)]
|
)]
|
||||||
pub wiki_url: Option<Option<String>>,
|
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}")]
|
#[patch("{id}")]
|
||||||
@@ -270,12 +350,10 @@ pub async fn mod_edit(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if status == &ModStatus::Rejected || status == &ModStatus::Approved {
|
if (status == &ModStatus::Rejected || status == &ModStatus::Approved) && !user.role.is_mod() {
|
||||||
if !user.role.is_mod() {
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
return Err(ApiError::CustomAuthenticationError(
|
"You don't have permission to set this status".to_string(),
|
||||||
"You don't have permission to set this status".to_string(),
|
));
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_id = database::models::StatusId::get_id(&status, &mut *transaction)
|
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()))?;
|
.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 let Some(body) = &new_mod.body {
|
||||||
if !perms.contains(Permissions::EDIT_BODY) {
|
if !perms.contains(Permissions::EDIT_BODY) {
|
||||||
return Err(ApiError::CustomAuthenticationError(
|
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}")]
|
#[delete("{id}")]
|
||||||
pub async fn mod_delete(
|
pub async fn mod_delete(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use super::ApiError;
|
use super::ApiError;
|
||||||
use crate::auth::check_is_admin_from_headers;
|
use crate::auth::check_is_admin_from_headers;
|
||||||
use crate::database::models;
|
use crate::database::models;
|
||||||
|
use crate::database::models::categories::{DonationPlatform, License};
|
||||||
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
|
||||||
use models::categories::{Category, GameVersion, Loader};
|
use models::categories::{Category, GameVersion, Loader};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -16,7 +17,13 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(loader_delete)
|
.service(loader_delete)
|
||||||
.service(game_version_list)
|
.service(game_version_list)
|
||||||
.service(game_version_create)
|
.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>,
|
pool: web::Data<PgPool>,
|
||||||
category: web::Path<(String,)>,
|
category: web::Path<(String,)>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_admin_from_headers(
|
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||||
req.headers(),
|
|
||||||
&mut *pool
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let name = category.into_inner().0;
|
let name = category.into_inner().0;
|
||||||
|
|
||||||
@@ -56,14 +56,7 @@ pub async fn category_delete(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
category: web::Path<(String,)>,
|
category: web::Path<(String,)>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_admin_from_headers(
|
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||||
req.headers(),
|
|
||||||
&mut *pool
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let name = category.into_inner().0;
|
let name = category.into_inner().0;
|
||||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||||
@@ -94,14 +87,7 @@ pub async fn loader_create(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
loader: web::Path<(String,)>,
|
loader: web::Path<(String,)>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_admin_from_headers(
|
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||||
req.headers(),
|
|
||||||
&mut *pool
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let name = loader.into_inner().0;
|
let name = loader.into_inner().0;
|
||||||
|
|
||||||
@@ -116,14 +102,7 @@ pub async fn loader_delete(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
loader: web::Path<(String,)>,
|
loader: web::Path<(String,)>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_admin_from_headers(
|
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||||
req.headers(),
|
|
||||||
&mut *pool
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let name = loader.into_inner().0;
|
let name = loader.into_inner().0;
|
||||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
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,)>,
|
game_version: web::Path<(String,)>,
|
||||||
version_data: web::Json<GameVersionData>,
|
version_data: web::Json<GameVersionData>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_admin_from_headers(
|
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||||
req.headers(),
|
|
||||||
&mut *pool
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let name = game_version.into_inner().0;
|
let name = game_version.into_inner().0;
|
||||||
|
|
||||||
@@ -209,14 +181,7 @@ pub async fn game_version_delete(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
game_version: web::Path<(String,)>,
|
game_version: web::Path<(String,)>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_admin_from_headers(
|
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||||
req.headers(),
|
|
||||||
&mut *pool
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let name = game_version.into_inner().0;
|
let name = game_version.into_inner().0;
|
||||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
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(""))
|
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::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||||
use crate::database::models::{TeamMember, User};
|
use crate::database::models::{TeamMember, User};
|
||||||
|
use crate::file_hosting::FileHost;
|
||||||
use crate::models::users::{Role, UserId};
|
use crate::models::users::{Role, UserId};
|
||||||
use crate::routes::ApiError;
|
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 serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[get("user")]
|
#[get("user")]
|
||||||
pub async fn user_auth_get(
|
pub async fn user_auth_get(
|
||||||
@@ -44,22 +47,30 @@ pub async fn users_get(
|
|||||||
|
|
||||||
let users: Vec<crate::models::users::User> = users_data
|
let users: Vec<crate::models::users::User> = users_data
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|data| crate::models::users::User {
|
.map(convert_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),
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(users))
|
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}")]
|
#[get("{id}")]
|
||||||
pub async fn user_get(
|
pub async fn user_get(
|
||||||
info: web::Path<(UserId,)>,
|
info: web::Path<(UserId,)>,
|
||||||
@@ -71,23 +82,27 @@ pub async fn user_get(
|
|||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
if let Some(data) = user_data {
|
if let Some(data) = user_data {
|
||||||
let response = crate::models::users::User {
|
let response = convert_user(data);
|
||||||
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),
|
|
||||||
};
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
Ok(HttpResponse::Ok().json(response))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
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")]
|
#[get("{user_id}/mods")]
|
||||||
pub async fn mods_list(
|
pub async fn mods_list(
|
||||||
info: web::Path<(UserId,)>,
|
info: web::Path<(UserId,)>,
|
||||||
@@ -161,6 +176,236 @@ pub async fn teams(
|
|||||||
Ok(HttpResponse::Ok().json(team_members))
|
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
|
// TODO: Make this actually do stuff
|
||||||
#[delete("{id}")]
|
#[delete("{id}")]
|
||||||
pub async fn user_delete(
|
pub async fn user_delete(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub struct InitialVersionData {
|
|||||||
pub game_versions: Vec<GameVersion>,
|
pub game_versions: Vec<GameVersion>,
|
||||||
pub release_channel: VersionType,
|
pub release_channel: VersionType,
|
||||||
pub loaders: Vec<ModLoader>,
|
pub loaders: Vec<ModLoader>,
|
||||||
|
pub featured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
@@ -265,6 +266,7 @@ async fn version_create_inner(
|
|||||||
game_versions,
|
game_versions,
|
||||||
loaders,
|
loaders,
|
||||||
release_channel,
|
release_channel,
|
||||||
|
featured: version_create_data.featured,
|
||||||
});
|
});
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -298,6 +300,7 @@ async fn version_create_inner(
|
|||||||
id: builder.version_id.into(),
|
id: builder.version_id.into(),
|
||||||
mod_id: builder.mod_id.into(),
|
mod_id: builder.mod_id.into(),
|
||||||
author_id: user.id,
|
author_id: user.id,
|
||||||
|
featured: builder.featured,
|
||||||
name: builder.name.clone(),
|
name: builder.name.clone(),
|
||||||
version_number: builder.version_number.clone(),
|
version_number: builder.version_number.clone(),
|
||||||
changelog_url: builder.changelog_url.clone(),
|
changelog_url: builder.changelog_url.clone(),
|
||||||
@@ -324,6 +327,7 @@ async fn version_create_inner(
|
|||||||
.collect(),
|
.collect(),
|
||||||
url: file.url.clone(),
|
url: file.url.clone(),
|
||||||
filename: file.filename.clone(),
|
filename: file.filename.clone(),
|
||||||
|
primary: file.primary,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
dependencies: version_data.dependencies,
|
dependencies: version_data.dependencies,
|
||||||
@@ -528,6 +532,7 @@ pub async fn upload_file(
|
|||||||
// bytes, but this is the string version.
|
// bytes, but this is the string version.
|
||||||
hash: upload_data.content_sha1.into_bytes(),
|
hash: upload_data.content_sha1.into_bytes(),
|
||||||
}],
|
}],
|
||||||
|
primary: uploaded_files.len() == 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::ApiError;
|
use super::ApiError;
|
||||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
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::file_hosting::FileHost;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::teams::Permissions;
|
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(),
|
mod_id: data.mod_id.into(),
|
||||||
author_id: data.author_id.into(),
|
author_id: data.author_id.into(),
|
||||||
|
|
||||||
|
featured: data.featured,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
version_number: data.version_number,
|
version_number: data.version_number,
|
||||||
changelog_url: data.changelog_url,
|
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()?)))
|
.map(|(k, v)| Some((k, String::from_utf8(v).ok()?)))
|
||||||
.collect::<Option<_>>()
|
.collect::<Option<_>>()
|
||||||
.unwrap_or_else(Default::default),
|
.unwrap_or_else(Default::default),
|
||||||
|
primary: f.primary,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -204,6 +206,8 @@ pub struct EditVersion {
|
|||||||
pub game_versions: Option<Vec<models::mods::GameVersion>>,
|
pub game_versions: Option<Vec<models::mods::GameVersion>>,
|
||||||
pub loaders: Option<Vec<models::mods::ModLoader>>,
|
pub loaders: Option<Vec<models::mods::ModLoader>>,
|
||||||
pub accepted: Option<bool>,
|
pub accepted: Option<bool>,
|
||||||
|
pub featured: Option<bool>,
|
||||||
|
pub primary_file: Option<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("{id}")]
|
#[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 {
|
if let Some(body) = &new_version.changelog {
|
||||||
let mod_id: models::mods::ModId = version_item.mod_id.into();
|
let mod_id: models::mods::ModId = version_item.mod_id.into();
|
||||||
let body_path = format!(
|
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}
|
// under /api/v1/version_file/{hash}
|
||||||
#[delete("{version_id}")]
|
#[delete("{version_id}")]
|
||||||
pub async fn delete_file(
|
pub async fn delete_file(
|
||||||
|
|||||||
Reference in New Issue
Block a user