forked from didirus/AstralRinth
Project Types, Code Cleanup, and Rename Mods -> Projects (#192)
* Initial work for modpacks and project types * Code cleanup, fix some issues * Username route getting, remove pointless tests * Base validator types + fixes * Fix strange IML generation * Multiple hash requests for version files * Fix docker build (hopefully) * Legacy routes * Finish validator architecture * Update rust version in dockerfile * Added caching and fixed typo (#203) * Added caching and fixed typo * Fixed clippy error * Removed log for cache * Add final validators, fix how loaders are handled and add icons to tags * Fix search module * Fix parts of legacy API not working Co-authored-by: Redblueflame <contact@redblueflame.com>
This commit is contained in:
2
.env
2
.env
@@ -28,7 +28,7 @@ LOCAL_INDEX_INTERVAL=3600
|
||||
# 30 minutes
|
||||
VERSION_INDEX_INTERVAL=1800
|
||||
|
||||
GITHUB_CLIENT_ID=3acffb2e808d16d4b226
|
||||
GITHUB_CLIENT_ID=none
|
||||
GITHUB_CLIENT_SECRET=none
|
||||
|
||||
RATE_LIMIT_IGNORE_IPS='[]'
|
||||
73
.idea/labrinth.iml
generated
73
.idea/labrinth.iml
generated
@@ -3,6 +3,79 @@
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/ahash-7eac216debe9f38a/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/async-trait-1f1149946021f68f/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/backtrace-a81dd5b1d549b1fe/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/bitflags-f9113967e336fcbe/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/brotli-sys-cb7557720acb5147/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/bzip2-sys-eaaf2f410b5f53aa/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/const_fn-083a7d629a9dd60f/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/cookie-abe2724f87f58612/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crc-312e8aeb69db3079/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crc32fast-098f9153d31ba9cd/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-utils-5bfd8a6166e7f6db/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-utils-ab308bfacfd03956/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/curl-6ddc5714a8986856/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/curl-sys-8ee7f7f4784d489b/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/encoding_rs-b7969e2119dfc51e/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/failure_derive-711d9019bf1d05c7/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/futures-channel-e6098f658bcb2591/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/futures-core-438eabbe001915aa/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/futures-macro-9cc56fcce7840df6/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/futures-task-ee55e29f27db9b98/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/futures-util-a1175a80ced777ce/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/generic-array-5440520ddf822692/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/getrandom-950b65ede84eb5e1/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/getrandom-de6c07f96ef855eb/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/hashbrown-ae09dc04d2f34e91/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/httparse-9599bdafe9a55039/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/indexmap-89e57c5173b349d9/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/isahc-86aa24d4580a977e/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/labrinth-3e3d312c8607dd17/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/lexical-core-8bf0f1a2d1b43ece/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/libc-d707e48de33d48d9/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/libnghttp2-sys-a51e5a3d7e3464ce/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/libz-sys-d2384f20b2f9b583/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/log-7c8712f109ca5294/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/maybe-uninit-19304d5387c9a95a/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/memchr-44de47fae7f7702f/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/mime_guess-ef6330e686a1be7f/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/miniz_oxide-c3cab35945b7a047/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/native-tls-3fea547460520a21/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/nom-43b66ebbb07372cf/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/nom-82a5ac3f69d853fd/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/num-integer-882a92c01312881d/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/num-traits-9247bc1ce3fdb48a/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/openssl-901f3ac4ff42e527/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/openssl-sys-139c8d838997f47a/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/pin-project-internal-01048751181af249/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/proc-macro-error-attr-a6a60d53cc2b710b/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/proc-macro-error-c8210b250a97a5c4/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/proc-macro-hack-d5e47daad3405e02/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/proc-macro-nested-a3ffff2430ef2c60/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/proc-macro2-9d6e979cb1b79e98/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/radium-5e75940e19c92849/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/ring-0c62041ed1124a76/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/rustversion-07fd960262c655bb/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/ryu-ec3b0518e33f72cd/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/sentry-contexts-061540b96a2797f0/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/serde-6f6f4c19e9ef78e0/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/serde_derive-9db413d6fd81e485/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/serde_json-e891f6d1195c8922/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/standback-619b4e48d02f2a1b/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/syn-df093b2222f7c0e4/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/time-2568f1074dc4b3c3/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/tokio-e723dcf9898f8aa3/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/typenum-913dc171f8a5b7c3/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/unicase-58734f758c305d57/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/v_escape-cbf9195349484203/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/v_htmlescape-f705d5a87b606c7e/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/value-bag-9890a878f3f6e4ee/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/whoami-9edf4b12be590553/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/const_fn-b8ca4a53a9eb8acd/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/labrinth-175a4aa21ea52414/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/proc-macro2-7094aab0eee55116/out" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/sentry-contexts-ca4afd3cc95c36c4/out" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
1147
Cargo.lock
generated
1147
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "labrinth"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
#Team members, please add your emails and usernames
|
||||
authors = ["geometrically <jai.a@tuta.io>", "Redblueflame <contact@redblueflame.com>", "Aeledfyr <aeledfyr@gmail.com>", "Charalampos Fanoulis <yo@fanoulis.dev>", "AppleTheGolden <scotsbox@protonmail.com>"]
|
||||
edition = "2018"
|
||||
@@ -12,11 +12,11 @@ path = "src/main.rs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "3.1.0"
|
||||
actix-rt = "1.1.1"
|
||||
actix-files = "0.4.0"
|
||||
actix-web = "3.3.2"
|
||||
actix-rt = "1.1.0"
|
||||
actix-files = "0.5.0"
|
||||
actix-multipart = "0.3.0"
|
||||
actix-cors = "0.4.1"
|
||||
actix-cors = "0.5.4"
|
||||
actix-ratelimit = "0.3.0"
|
||||
|
||||
meilisearch-sdk = "0.6.0"
|
||||
@@ -35,6 +35,10 @@ base64 = "0.13.0"
|
||||
sha1 = { version = "0.6.0", features = ["std"] }
|
||||
sha2 = "0.9.2"
|
||||
bitflags = "1.2.1"
|
||||
zip = "0.5.12"
|
||||
|
||||
validator = { version = "0.13", features = ["derive"] }
|
||||
regex = "1.5.4"
|
||||
|
||||
gumdrop = "0.8.0"
|
||||
dotenv = "0.15"
|
||||
@@ -52,3 +56,5 @@ sqlx = { version = "0.4.2", features = ["runtime-actix-rustls", "postgres", "chr
|
||||
|
||||
sentry = { version = "0.22.0", features = ["log"] }
|
||||
sentry-actix = "0.22.0"
|
||||
|
||||
cached = "0.23.0"
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.45.1 as build
|
||||
FROM rust:1.52.1 as build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/labrinth
|
||||
|
||||
31
migrations/20210509010206_project_types.sql
Normal file
31
migrations/20210509010206_project_types.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
ALTER TABLE users ADD CONSTRAINT username_unique UNIQUE (username);
|
||||
|
||||
CREATE TABLE project_types (
|
||||
id serial PRIMARY KEY,
|
||||
name varchar(64) UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO project_types (name) VALUES ('mod');
|
||||
INSERT INTO project_types (name) VALUES ('modpack');
|
||||
|
||||
CREATE TABLE loaders_project_types (
|
||||
joining_loader_id int REFERENCES loaders ON UPDATE CASCADE NOT NULL,
|
||||
joining_project_type_id int REFERENCES project_types ON UPDATE CASCADE NOT NULL,
|
||||
PRIMARY KEY (joining_loader_id, joining_project_type_id)
|
||||
);
|
||||
|
||||
ALTER TABLE mods
|
||||
ADD COLUMN project_type integer REFERENCES project_types NOT NULL default 1;
|
||||
|
||||
ALTER TABLE categories
|
||||
ADD COLUMN project_type integer REFERENCES project_types NOT NULL default 1,
|
||||
ADD COLUMN icon varchar(20000) NOT NULL default '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
|
||||
|
||||
ALTER TABLE loaders
|
||||
ADD COLUMN icon varchar(20000) NOT NULL default '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
|
||||
|
||||
ALTER TABLE mods
|
||||
ALTER COLUMN project_type DROP DEFAULT;
|
||||
|
||||
ALTER TABLE categories
|
||||
ALTER COLUMN project_type DROP DEFAULT;
|
||||
3180
sqlx-data.json
3180
sqlx-data.json
File diff suppressed because it is too large
Load Diff
44
src/database/cache/mod.rs
vendored
Normal file
44
src/database/cache/mod.rs
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
//pub mod project_cache;
|
||||
//pub mod project_query_cache;
|
||||
#[macro_export]
|
||||
macro_rules! generate_cache {
|
||||
($name:ident,$id:ty, $val:ty, $cache_name:ident, $mod_name:ident, $getter_name:ident, $setter_name:ident) => {
|
||||
pub mod $mod_name {
|
||||
use cached::async_mutex::Mutex;
|
||||
use cached::{Cached, SizedCache};
|
||||
use lazy_static::lazy_static;
|
||||
lazy_static! {
|
||||
static ref $cache_name: Mutex<SizedCache<$id, $val>> =
|
||||
Mutex::new(SizedCache::with_size(400));
|
||||
}
|
||||
|
||||
pub async fn $getter_name<'a>(id: $id) -> Option<$val> {
|
||||
let mut cache = $cache_name.lock().await;
|
||||
Cached::cache_get(&mut *cache, &id).map(|e| e.clone())
|
||||
}
|
||||
pub async fn $setter_name<'a>(id: $id, val: &$val) {
|
||||
let mut cache = $cache_name.lock().await;
|
||||
Cached::cache_set(&mut *cache, id, val.clone());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
generate_cache!(
|
||||
project,
|
||||
String,
|
||||
crate::database::Project,
|
||||
PROJECT_CACHE,
|
||||
project_cache,
|
||||
get_cache_project,
|
||||
set_cache_project
|
||||
);
|
||||
generate_cache!(
|
||||
query_project,
|
||||
String,
|
||||
crate::database::models::project_item::QueryProject,
|
||||
QUERY_PROJECT_CACHE,
|
||||
query_project_cache,
|
||||
get_cache_query_project,
|
||||
set_cache_query_project
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
mod cache;
|
||||
pub mod models;
|
||||
mod postgres_database;
|
||||
|
||||
pub use models::Mod;
|
||||
pub use models::Project;
|
||||
pub use models::Version;
|
||||
pub use postgres_database::check_for_migrations;
|
||||
pub use postgres_database::connect;
|
||||
|
||||
@@ -2,19 +2,30 @@ use super::ids::*;
|
||||
use super::DatabaseError;
|
||||
use futures::TryStreamExt;
|
||||
|
||||
pub struct ProjectType {
|
||||
pub id: ProjectTypeId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct Loader {
|
||||
pub id: LoaderId,
|
||||
pub loader: String,
|
||||
pub icon: String,
|
||||
pub supported_project_types: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct GameVersion {
|
||||
pub id: GameVersionId,
|
||||
pub version: String,
|
||||
pub version_type: String,
|
||||
pub date: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
pub struct Category {
|
||||
pub id: CategoryId,
|
||||
pub category: String,
|
||||
pub project_type: String,
|
||||
pub icon: String,
|
||||
}
|
||||
|
||||
pub struct ReportType {
|
||||
@@ -36,11 +47,17 @@ pub struct DonationPlatform {
|
||||
|
||||
pub struct CategoryBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
pub project_type: Option<&'a ProjectTypeId>,
|
||||
pub icon: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub fn builder() -> CategoryBuilder<'static> {
|
||||
CategoryBuilder { name: None }
|
||||
CategoryBuilder {
|
||||
name: None,
|
||||
project_type: None,
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<CategoryId>, DatabaseError>
|
||||
@@ -59,7 +76,36 @@ impl Category {
|
||||
SELECT id FROM categories
|
||||
WHERE category = $1
|
||||
",
|
||||
name
|
||||
name,
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| CategoryId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_id_project<'a, E>(
|
||||
name: &str,
|
||||
project_type: ProjectTypeId,
|
||||
exec: E,
|
||||
) -> Result<Option<CategoryId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM categories
|
||||
WHERE category = $1 AND project_type = $2
|
||||
",
|
||||
name,
|
||||
project_type as ProjectTypeId
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
@@ -84,18 +130,27 @@ impl Category {
|
||||
Ok(result.category)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<Category>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT category FROM categories
|
||||
SELECT c.id id, c.category category, c.icon icon, pt.name project_type
|
||||
FROM categories c
|
||||
INNER JOIN project_types pt ON c.project_type = pt.id
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.category)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|c| Category {
|
||||
id: CategoryId(c.id),
|
||||
category: c.category,
|
||||
project_type: c.project_type,
|
||||
icon: c.icon,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<Category>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -133,24 +188,49 @@ impl<'a> CategoryBuilder<'a> {
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
Ok(Self { name: Some(name) })
|
||||
Ok(Self {
|
||||
name: Some(name),
|
||||
..self
|
||||
})
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_type(
|
||||
self,
|
||||
project_type: &'a ProjectTypeId,
|
||||
) -> Result<CategoryBuilder<'a>, DatabaseError> {
|
||||
Ok(Self {
|
||||
project_type: Some(project_type),
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
pub fn icon(self, icon: &'a str) -> Result<CategoryBuilder<'a>, DatabaseError> {
|
||||
Ok(Self {
|
||||
icon: Some(icon),
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<CategoryId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let id = *self
|
||||
.project_type
|
||||
.ok_or_else(|| DatabaseError::Other("No project type specified.".to_string()))?;
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO categories (category)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (category) DO NOTHING
|
||||
INSERT INTO categories (category, project_type, icon)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (category, project_type, icon) DO NOTHING
|
||||
RETURNING id
|
||||
",
|
||||
self.name
|
||||
self.name,
|
||||
id as ProjectTypeId,
|
||||
self.icon
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
@@ -161,11 +241,17 @@ impl<'a> CategoryBuilder<'a> {
|
||||
|
||||
pub struct LoaderBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
pub icon: Option<&'a str>,
|
||||
pub supported_project_types: Option<&'a [ProjectTypeId]>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn builder() -> LoaderBuilder<'static> {
|
||||
LoaderBuilder { name: None }
|
||||
LoaderBuilder {
|
||||
name: None,
|
||||
icon: None,
|
||||
supported_project_types: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<LoaderId>, DatabaseError>
|
||||
@@ -209,24 +295,41 @@ impl Loader {
|
||||
Ok(result.loader)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<Loader>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT loader FROM loaders
|
||||
SELECT l.id id, l.loader loader, l.icon icon,
|
||||
STRING_AGG(DISTINCT pt.name, ',') project_types
|
||||
FROM loaders l
|
||||
LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id
|
||||
LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id
|
||||
GROUP BY l.id;
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.loader)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|x| Loader {
|
||||
id: LoaderId(x.id),
|
||||
loader: x.loader,
|
||||
icon: x.icon,
|
||||
supported_project_types: x
|
||||
.project_types
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// TODO: remove loaders with mods using them
|
||||
// TODO: remove loaders with projects using them
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
@@ -259,28 +362,74 @@ impl<'a> LoaderBuilder<'a> {
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
Ok(Self { name: Some(name) })
|
||||
Ok(Self {
|
||||
name: Some(name),
|
||||
..self
|
||||
})
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<LoaderId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
pub fn icon(self, icon: &'a str) -> Result<LoaderBuilder<'a>, DatabaseError> {
|
||||
Ok(Self {
|
||||
icon: Some(icon),
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
pub fn supported_project_types(
|
||||
self,
|
||||
supported_project_types: &'a [ProjectTypeId],
|
||||
) -> Result<LoaderBuilder<'a>, DatabaseError> {
|
||||
Ok(Self {
|
||||
supported_project_types: Some(supported_project_types),
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn insert(
|
||||
self,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<LoaderId, super::DatabaseError> {
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO loaders (loader)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (loader) DO NOTHING
|
||||
INSERT INTO loaders (loader, icon)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (loader, icon) DO NOTHING
|
||||
RETURNING id
|
||||
",
|
||||
self.name
|
||||
self.name,
|
||||
self.icon
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if let Some(project_types) = self.supported_project_types {
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM loaders_project_types
|
||||
WHERE joining_loader_id = $1
|
||||
",
|
||||
result.id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
for project_type in project_types {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)
|
||||
VALUES ($1, $2)
|
||||
",
|
||||
result.id,
|
||||
project_type.0,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LoaderId(result.id))
|
||||
}
|
||||
}
|
||||
@@ -341,19 +490,24 @@ impl GameVersion {
|
||||
Ok(result.version)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<GameVersion>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv
|
||||
ORDER BY created DESC
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||
id: GameVersionId(c.id),
|
||||
version: c.version_,
|
||||
version_type: c.type_,
|
||||
date: c.created
|
||||
})) })
|
||||
.try_collect::<Vec<GameVersion>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -363,7 +517,7 @@ impl GameVersion {
|
||||
version_type_option: Option<&str>,
|
||||
major_option: Option<bool>,
|
||||
exec: E,
|
||||
) -> Result<Vec<String>, DatabaseError>
|
||||
) -> Result<Vec<GameVersion>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@@ -373,7 +527,7 @@ impl GameVersion {
|
||||
if let Some(major) = major_option {
|
||||
result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv
|
||||
WHERE major = $1 AND type = $2
|
||||
ORDER BY created DESC
|
||||
",
|
||||
@@ -381,35 +535,50 @@ impl GameVersion {
|
||||
version_type
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||
id: GameVersionId(c.id),
|
||||
version: c.version_,
|
||||
version_type: c.type_,
|
||||
date: c.created
|
||||
})) })
|
||||
.try_collect::<Vec<GameVersion>>()
|
||||
.await?;
|
||||
} else {
|
||||
result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv
|
||||
WHERE type = $1
|
||||
ORDER BY created DESC
|
||||
",
|
||||
version_type
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||
id: GameVersionId(c.id),
|
||||
version: c.version_,
|
||||
version_type: c.type_,
|
||||
date: c.created
|
||||
})) })
|
||||
.try_collect::<Vec<GameVersion>>()
|
||||
.await?;
|
||||
}
|
||||
} else if let Some(major) = major_option {
|
||||
result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv
|
||||
WHERE major = $1
|
||||
ORDER BY created DESC
|
||||
",
|
||||
major
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||
id: GameVersionId(c.id),
|
||||
version: c.version_,
|
||||
version_type: c.type_,
|
||||
date: c.created
|
||||
})) })
|
||||
.try_collect::<Vec<GameVersion>>()
|
||||
.await?;
|
||||
} else {
|
||||
result = Vec::new();
|
||||
@@ -867,7 +1036,6 @@ impl ReportType {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// TODO: remove loaders with mods using them
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
@@ -925,3 +1093,156 @@ impl<'a> ReportTypeBuilder<'a> {
|
||||
Ok(ReportTypeId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectTypeBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl ProjectType {
|
||||
pub fn builder() -> ProjectTypeBuilder<'static> {
|
||||
ProjectTypeBuilder { name: None }
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ProjectTypeId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM project_types
|
||||
WHERE name = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| ProjectTypeId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_many_id<'a, E>(
|
||||
names: &Vec<String>,
|
||||
exec: E,
|
||||
) -> Result<Vec<ProjectType>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let project_types = sqlx::query!(
|
||||
"
|
||||
SELECT id, name FROM project_types
|
||||
WHERE name IN (SELECT * FROM UNNEST($1::varchar[]))
|
||||
",
|
||||
names
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|x| ProjectType {
|
||||
id: ProjectTypeId(x.id),
|
||||
name: x.name,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<ProjectType>>()
|
||||
.await?;
|
||||
|
||||
Ok(project_types)
|
||||
}
|
||||
|
||||
pub async fn get_name<'a, E>(id: ProjectTypeId, exec: E) -> Result<String, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT name FROM project_types
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ProjectTypeId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.name)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT name FROM project_types
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.name)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// TODO: remove loaders with mods using them
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM project_types
|
||||
WHERE name = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ProjectTypeBuilder<'a> {
|
||||
/// The name of the project type. Must be ASCII alphanumeric or `-`/`_`
|
||||
pub fn name(self, name: &'a str) -> Result<ProjectTypeBuilder<'a>, DatabaseError> {
|
||||
if name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
Ok(Self { name: Some(name) })
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<ProjectTypeId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO project_types (name)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (name) DO NOTHING
|
||||
RETURNING id
|
||||
",
|
||||
self.name
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(ProjectTypeId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ macro_rules! generate_ids {
|
||||
}
|
||||
|
||||
generate_ids!(
|
||||
pub generate_mod_id,
|
||||
ModId,
|
||||
pub generate_project_id,
|
||||
ProjectId,
|
||||
8,
|
||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
||||
ModId
|
||||
ProjectId
|
||||
);
|
||||
generate_ids!(
|
||||
pub generate_version_id,
|
||||
@@ -115,7 +115,11 @@ pub struct TeamMemberId(pub i64);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct ModId(pub i64);
|
||||
pub struct ProjectId(pub i64);
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct ProjectTypeId(pub i32);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct StatusId(pub i32);
|
||||
@@ -169,14 +173,14 @@ pub struct NotificationActionId(pub i32);
|
||||
|
||||
use crate::models::ids;
|
||||
|
||||
impl From<ids::ModId> for ModId {
|
||||
fn from(id: ids::ModId) -> Self {
|
||||
ModId(id.0 as i64)
|
||||
impl From<ids::ProjectId> for ProjectId {
|
||||
fn from(id: ids::ProjectId) -> Self {
|
||||
ProjectId(id.0 as i64)
|
||||
}
|
||||
}
|
||||
impl From<ModId> for ids::ModId {
|
||||
fn from(id: ModId) -> Self {
|
||||
ids::ModId(id.0 as u64)
|
||||
impl From<ProjectId> for ids::ProjectId {
|
||||
fn from(id: ProjectId) -> Self {
|
||||
ids::ProjectId(id.0 as u64)
|
||||
}
|
||||
}
|
||||
impl From<ids::UserId> for UserId {
|
||||
|
||||
@@ -5,15 +5,15 @@ use thiserror::Error;
|
||||
|
||||
pub mod categories;
|
||||
pub mod ids;
|
||||
pub mod mod_item;
|
||||
pub mod notification_item;
|
||||
pub mod project_item;
|
||||
pub mod report_item;
|
||||
pub mod team_item;
|
||||
pub mod user_item;
|
||||
pub mod version_item;
|
||||
|
||||
pub use ids::*;
|
||||
pub use mod_item::Mod;
|
||||
pub use project_item::Project;
|
||||
pub use team_item::Team;
|
||||
pub use team_item::TeamMember;
|
||||
pub use user_item::User;
|
||||
@@ -62,7 +62,7 @@ impl ids::ChannelId {
|
||||
|
||||
impl ids::StatusId {
|
||||
pub async fn get_id<'a, E>(
|
||||
status: &crate::models::mods::ModStatus,
|
||||
status: &crate::models::projects::ProjectStatus,
|
||||
exec: E,
|
||||
) -> Result<Option<Self>, DatabaseError>
|
||||
where
|
||||
@@ -84,7 +84,7 @@ impl ids::StatusId {
|
||||
|
||||
impl ids::SideTypeId {
|
||||
pub async fn get_id<'a, E>(
|
||||
side: &crate::models::mods::SideType,
|
||||
side: &crate::models::projects::SideType,
|
||||
exec: E,
|
||||
) -> Result<Option<Self>, DatabaseError>
|
||||
where
|
||||
@@ -122,3 +122,22 @@ impl ids::DonationPlatformId {
|
||||
Ok(result.map(|r| ids::DonationPlatformId(r.id)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ids::ProjectTypeId {
|
||||
pub async fn get_id<'a, E>(project_type: String, exec: E) -> Result<Option<Self>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM project_types
|
||||
WHERE name = $1
|
||||
",
|
||||
project_type
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| ids::ProjectTypeId(r.id)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,8 @@ impl Notification {
|
||||
FROM notifications n
|
||||
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
|
||||
WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
GROUP BY n.id, n.user_id;
|
||||
GROUP BY n.id, n.user_id
|
||||
ORDER BY n.created DESC;
|
||||
",
|
||||
¬ification_ids_parsed
|
||||
)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use super::ids::*;
|
||||
|
||||
use crate::database::cache::project_cache::{get_cache_project, set_cache_project};
|
||||
use crate::database::cache::query_project_cache::{
|
||||
get_cache_query_project, set_cache_query_project,
|
||||
};
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DonationUrl {
|
||||
pub mod_id: ModId,
|
||||
pub project_id: ProjectId,
|
||||
pub platform_id: DonationPlatformId,
|
||||
pub platform_short: String,
|
||||
pub platform_name: String,
|
||||
@@ -22,7 +26,7 @@ impl DonationUrl {
|
||||
$1, $2, $3
|
||||
)
|
||||
",
|
||||
self.mod_id as ModId,
|
||||
self.project_id as ProjectId,
|
||||
self.platform_id as DonationPlatformId,
|
||||
self.url,
|
||||
)
|
||||
@@ -33,8 +37,9 @@ impl DonationUrl {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModBuilder {
|
||||
pub mod_id: ModId,
|
||||
pub struct ProjectBuilder {
|
||||
pub project_id: ProjectId,
|
||||
pub project_type_id: ProjectTypeId,
|
||||
pub team_id: TeamId,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
@@ -55,13 +60,14 @@ pub struct ModBuilder {
|
||||
pub donation_urls: Vec<DonationUrl>,
|
||||
}
|
||||
|
||||
impl ModBuilder {
|
||||
impl ProjectBuilder {
|
||||
pub async fn insert(
|
||||
self,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<ModId, super::DatabaseError> {
|
||||
let mod_struct = Mod {
|
||||
id: self.mod_id,
|
||||
) -> Result<ProjectId, super::DatabaseError> {
|
||||
let project_struct = Project {
|
||||
id: self.project_id,
|
||||
project_type: self.project_type_id,
|
||||
team_id: self.team_id,
|
||||
title: self.title,
|
||||
description: self.description,
|
||||
@@ -83,15 +89,15 @@ impl ModBuilder {
|
||||
license: self.license,
|
||||
slug: self.slug,
|
||||
};
|
||||
mod_struct.insert(&mut *transaction).await?;
|
||||
project_struct.insert(&mut *transaction).await?;
|
||||
|
||||
for mut version in self.initial_versions {
|
||||
version.mod_id = self.mod_id;
|
||||
version.project_id = self.project_id;
|
||||
version.insert(&mut *transaction).await?;
|
||||
}
|
||||
|
||||
for mut donation in self.donation_urls {
|
||||
donation.mod_id = self.mod_id;
|
||||
donation.project_id = self.project_id;
|
||||
donation.insert(&mut *transaction).await?;
|
||||
}
|
||||
|
||||
@@ -101,19 +107,20 @@ impl ModBuilder {
|
||||
INSERT INTO mods_categories (joining_mod_id, joining_category_id)
|
||||
VALUES ($1, $2)
|
||||
",
|
||||
self.mod_id as ModId,
|
||||
self.project_id as ProjectId,
|
||||
category as CategoryId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(self.mod_id)
|
||||
Ok(self.project_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Mod {
|
||||
pub id: ModId,
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Project {
|
||||
pub id: ProjectId,
|
||||
pub project_type: ProjectTypeId,
|
||||
pub team_id: TeamId,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
@@ -136,7 +143,7 @@ pub struct Mod {
|
||||
pub slug: Option<String>,
|
||||
}
|
||||
|
||||
impl Mod {
|
||||
impl Project {
|
||||
pub async fn insert(
|
||||
&self,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
@@ -148,17 +155,17 @@ impl Mod {
|
||||
published, downloads, icon_url, issues_url,
|
||||
source_url, wiki_url, status, discord_url,
|
||||
client_side, server_side, license_url, license,
|
||||
slug
|
||||
slug, project_type
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, $9,
|
||||
$10, $11, $12, $13,
|
||||
$14, $15, $16, $17,
|
||||
LOWER($18)
|
||||
LOWER($18), $19
|
||||
)
|
||||
",
|
||||
self.id as ModId,
|
||||
self.id as ProjectId,
|
||||
self.team_id as TeamId,
|
||||
&self.title,
|
||||
&self.description,
|
||||
@@ -175,7 +182,8 @@ impl Mod {
|
||||
self.server_side as SideTypeId,
|
||||
self.license_url.as_ref(),
|
||||
self.license as LicenseId,
|
||||
self.slug.as_ref()
|
||||
self.slug.as_ref(),
|
||||
self.project_type as ProjectTypeId
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@@ -183,13 +191,16 @@ impl Mod {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a, 'b, E>(id: ModId, executor: E) -> Result<Option<Self>, sqlx::error::Error>
|
||||
pub async fn get<'a, 'b, E>(
|
||||
id: ProjectId,
|
||||
executor: E,
|
||||
) -> Result<Option<Self>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT title, description, downloads, follows,
|
||||
SELECT project_type, title, description, downloads, follows,
|
||||
icon_url, body, body_url, published,
|
||||
updated, status,
|
||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||
@@ -197,14 +208,15 @@ impl Mod {
|
||||
FROM mods
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
Ok(Some(Mod {
|
||||
Ok(Some(Project {
|
||||
id,
|
||||
project_type: ProjectTypeId(row.project_type),
|
||||
team_id: TeamId(row.team_id),
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
@@ -231,16 +243,19 @@ impl Mod {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_many<'a, E>(mod_ids: Vec<ModId>, exec: E) -> Result<Vec<Mod>, sqlx::Error>
|
||||
pub async fn get_many<'a, E>(
|
||||
project_ids: Vec<ProjectId>,
|
||||
exec: E,
|
||||
) -> Result<Vec<Project>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let mod_ids_parsed: Vec<i64> = mod_ids.into_iter().map(|x| x.0).collect();
|
||||
let mods = sqlx::query!(
|
||||
let project_ids_parsed: Vec<i64> = project_ids.into_iter().map(|x| x.0).collect();
|
||||
let projects = sqlx::query!(
|
||||
"
|
||||
SELECT id, title, description, downloads, follows,
|
||||
SELECT id, project_type, title, description, downloads, follows,
|
||||
icon_url, body, body_url, published,
|
||||
updated, status,
|
||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||
@@ -248,12 +263,13 @@ impl Mod {
|
||||
FROM mods
|
||||
WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
",
|
||||
&mod_ids_parsed
|
||||
&project_ids_parsed
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| Mod {
|
||||
id: ModId(m.id),
|
||||
Ok(e.right().map(|m| Project {
|
||||
id: ProjectId(m.id),
|
||||
project_type: ProjectTypeId(m.project_type),
|
||||
team_id: TeamId(m.team_id),
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
@@ -276,14 +292,14 @@ impl Mod {
|
||||
follows: m.follows,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<Mod>>()
|
||||
.try_collect::<Vec<Project>>()
|
||||
.await?;
|
||||
|
||||
Ok(mods)
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
pub async fn remove_full<'a, 'b, E>(
|
||||
id: ModId,
|
||||
id: ProjectId,
|
||||
exec: E,
|
||||
) -> Result<Option<()>, sqlx::error::Error>
|
||||
where
|
||||
@@ -293,7 +309,7 @@ impl Mod {
|
||||
"
|
||||
SELECT team_id FROM mods WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
@@ -309,7 +325,7 @@ impl Mod {
|
||||
DELETE FROM mod_follows
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId
|
||||
id as ProjectId
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -319,7 +335,7 @@ impl Mod {
|
||||
DELETE FROM mod_follows
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -329,7 +345,7 @@ impl Mod {
|
||||
DELETE FROM reports
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -339,7 +355,7 @@ impl Mod {
|
||||
DELETE FROM mods_categories
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -349,7 +365,7 @@ impl Mod {
|
||||
DELETE FROM mods_donations
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -360,7 +376,7 @@ impl Mod {
|
||||
SELECT id FROM versions
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) })
|
||||
@@ -376,7 +392,7 @@ impl Mod {
|
||||
DELETE FROM mods
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -407,7 +423,7 @@ impl Mod {
|
||||
pub async fn get_full_from_slug<'a, 'b, E>(
|
||||
slug: &str,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryMod>, sqlx::error::Error>
|
||||
) -> Result<Option<QueryProject>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
@@ -421,49 +437,154 @@ impl Mod {
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(mod_id) = id {
|
||||
Mod::get_full(ModId(mod_id.id), executor).await
|
||||
if let Some(project_id) = id {
|
||||
Project::get_full(ProjectId(project_id.id), executor).await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_full<'a, 'b, E>(
|
||||
id: ModId,
|
||||
pub async fn get_from_slug<'a, 'b, E>(
|
||||
slug: &str,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryMod>, sqlx::error::Error>
|
||||
) -> Result<Option<Project>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let id = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM mods
|
||||
WHERE LOWER(slug) = LOWER($1)
|
||||
",
|
||||
slug
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(project_id) = id {
|
||||
Project::get(ProjectId(project_id.id), executor).await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_from_slug_or_project_id<'a, 'b, E>(
|
||||
slug_or_project_id: String,
|
||||
executor: E,
|
||||
) -> Result<Option<Project>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
// Check in the cache
|
||||
let cached = get_cache_project(slug_or_project_id.clone()).await;
|
||||
if let Some(data) = cached {
|
||||
return Ok(Some(data));
|
||||
}
|
||||
let id_option =
|
||||
crate::models::ids::base62_impl::parse_base62(&*slug_or_project_id.clone()).ok();
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let mut project = Project::get(ProjectId(id as i64), executor).await?;
|
||||
|
||||
if project.is_none() {
|
||||
project = Project::get_from_slug(&slug_or_project_id, executor).await?;
|
||||
}
|
||||
// Cache the response
|
||||
if let Some(data) = project {
|
||||
set_cache_project(slug_or_project_id.clone(), &data).await;
|
||||
Ok(Some(data))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
let project = Project::get_from_slug(&slug_or_project_id, executor).await?;
|
||||
// Capture the data, and try to cache it
|
||||
if let Some(data) = project {
|
||||
set_cache_project(slug_or_project_id.clone(), &data).await;
|
||||
Ok(Some(data))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_full_from_slug_or_project_id<'a, 'b, E>(
|
||||
slug_or_project_id: String,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryProject>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
// Query cache
|
||||
let cached = get_cache_query_project(slug_or_project_id.clone()).await;
|
||||
if let Some(data) = cached {
|
||||
return Ok(Some(data));
|
||||
}
|
||||
let id_option =
|
||||
crate::models::ids::base62_impl::parse_base62(&*slug_or_project_id.clone()).ok();
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let mut project = Project::get_full(ProjectId(id as i64), executor).await?;
|
||||
|
||||
if project.is_none() {
|
||||
project = Project::get_full_from_slug(&slug_or_project_id, executor).await?;
|
||||
}
|
||||
// Save the variable
|
||||
if let Some(data) = project {
|
||||
set_cache_query_project(slug_or_project_id.clone(), &data).await;
|
||||
Ok(Some(data))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
let project = Project::get_full_from_slug(&slug_or_project_id, executor).await?;
|
||||
if let Some(data) = project {
|
||||
set_cache_query_project(slug_or_project_id.clone(), &data).await;
|
||||
Ok(Some(data))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_full<'a, 'b, E>(
|
||||
id: ProjectId,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryProject>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,
|
||||
SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,
|
||||
m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,
|
||||
m.updated updated, m.status status,
|
||||
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
|
||||
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,
|
||||
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name,
|
||||
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
|
||||
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions
|
||||
FROM mods m
|
||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
||||
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
|
||||
LEFT OUTER JOIN versions v ON v.mod_id = m.id
|
||||
INNER JOIN project_types pt ON pt.id = m.project_type
|
||||
INNER JOIN statuses s ON s.id = m.status
|
||||
INNER JOIN side_types cs ON m.client_side = cs.id
|
||||
INNER JOIN side_types ss ON m.server_side = ss.id
|
||||
INNER JOIN licenses l ON m.license = l.id
|
||||
WHERE m.id = $1
|
||||
GROUP BY m.id, s.id, cs.id, ss.id, l.id;
|
||||
GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(m) = result {
|
||||
Ok(Some(QueryMod {
|
||||
inner: Mod {
|
||||
id: ModId(m.id),
|
||||
Ok(Some(QueryProject {
|
||||
inner: Project {
|
||||
id: ProjectId(m.id),
|
||||
project_type: ProjectTypeId(m.project_type),
|
||||
team_id: TeamId(m.team_id),
|
||||
title: m.title.clone(),
|
||||
description: m.description.clone(),
|
||||
@@ -485,6 +606,7 @@ impl Mod {
|
||||
body: m.body.clone(),
|
||||
follows: m.follows,
|
||||
},
|
||||
project_type: m.project_type_name,
|
||||
categories: m
|
||||
.categories
|
||||
.unwrap_or_default()
|
||||
@@ -498,11 +620,11 @@ impl Mod {
|
||||
.map(|x| VersionId(x.parse().unwrap_or_default()))
|
||||
.collect(),
|
||||
donation_urls: vec![],
|
||||
status: crate::models::mods::ModStatus::from_str(&m.status_name),
|
||||
status: crate::models::projects::ProjectStatus::from_str(&m.status_name),
|
||||
license_id: m.short,
|
||||
license_name: m.license_name,
|
||||
client_side: crate::models::mods::SideType::from_str(&m.client_side_type),
|
||||
server_side: crate::models::mods::SideType::from_str(&m.server_side_type),
|
||||
client_side: crate::models::projects::SideType::from_str(&m.client_side_type),
|
||||
server_side: crate::models::projects::SideType::from_str(&m.server_side_type),
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -510,42 +632,44 @@ impl Mod {
|
||||
}
|
||||
|
||||
pub async fn get_many_full<'a, E>(
|
||||
mod_ids: Vec<ModId>,
|
||||
project_ids: Vec<ProjectId>,
|
||||
exec: E,
|
||||
) -> Result<Vec<QueryMod>, sqlx::Error>
|
||||
) -> Result<Vec<QueryProject>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mod_ids_parsed: Vec<i64> = mod_ids.into_iter().map(|x| x.0).collect();
|
||||
let project_ids_parsed: Vec<i64> = project_ids.into_iter().map(|x| x.0).collect();
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,
|
||||
SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,
|
||||
m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,
|
||||
m.updated updated, m.status status,
|
||||
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
|
||||
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,
|
||||
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name,
|
||||
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
|
||||
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions
|
||||
FROM mods m
|
||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
||||
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
|
||||
LEFT OUTER JOIN versions v ON v.mod_id = m.id
|
||||
INNER JOIN project_types pt ON pt.id = m.project_type
|
||||
INNER JOIN statuses s ON s.id = m.status
|
||||
INNER JOIN side_types cs ON m.client_side = cs.id
|
||||
INNER JOIN side_types ss ON m.server_side = ss.id
|
||||
INNER JOIN licenses l ON m.license = l.id
|
||||
WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
GROUP BY m.id, s.id, cs.id, ss.id, l.id;
|
||||
GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;
|
||||
",
|
||||
&mod_ids_parsed
|
||||
&project_ids_parsed
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| QueryMod {
|
||||
inner: Mod {
|
||||
id: ModId(m.id),
|
||||
Ok(e.right().map(|m| QueryProject {
|
||||
inner: Project {
|
||||
id: ProjectId(m.id),
|
||||
project_type: ProjectTypeId(m.project_type),
|
||||
team_id: TeamId(m.team_id),
|
||||
title: m.title.clone(),
|
||||
description: m.description.clone(),
|
||||
@@ -567,30 +691,31 @@ impl Mod {
|
||||
body: m.body.clone(),
|
||||
follows: m.follows
|
||||
},
|
||||
project_type: m.project_type_name,
|
||||
categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(),
|
||||
versions: m.versions.unwrap_or_default().split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect(),
|
||||
donation_urls: vec![],
|
||||
status: crate::models::mods::ModStatus::from_str(&m.status_name),
|
||||
status: crate::models::projects::ProjectStatus::from_str(&m.status_name),
|
||||
license_id: m.short,
|
||||
license_name: m.license_name,
|
||||
client_side: crate::models::mods::SideType::from_str(&m.client_side_type),
|
||||
server_side: crate::models::mods::SideType::from_str(&m.server_side_type),
|
||||
client_side: crate::models::projects::SideType::from_str(&m.client_side_type),
|
||||
server_side: crate::models::projects::SideType::from_str(&m.server_side_type),
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<QueryMod>>()
|
||||
.try_collect::<Vec<QueryProject>>()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueryMod {
|
||||
pub inner: Mod,
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct QueryProject {
|
||||
pub inner: Project,
|
||||
pub project_type: String,
|
||||
pub categories: Vec<String>,
|
||||
pub versions: Vec<VersionId>,
|
||||
pub donation_urls: Vec<DonationUrl>,
|
||||
pub status: crate::models::mods::ModStatus,
|
||||
pub status: crate::models::projects::ProjectStatus,
|
||||
pub license_id: String,
|
||||
pub license_name: String,
|
||||
pub client_side: crate::models::mods::SideType,
|
||||
pub server_side: crate::models::mods::SideType,
|
||||
pub client_side: crate::models::projects::SideType,
|
||||
pub server_side: crate::models::projects::SideType,
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use super::ids::*;
|
||||
pub struct Report {
|
||||
pub id: ReportId,
|
||||
pub report_type_id: ReportTypeId,
|
||||
pub mod_id: Option<ModId>,
|
||||
pub project_id: Option<ProjectId>,
|
||||
pub version_id: Option<VersionId>,
|
||||
pub user_id: Option<UserId>,
|
||||
pub body: String,
|
||||
@@ -14,7 +14,7 @@ pub struct Report {
|
||||
pub struct QueryReport {
|
||||
pub id: ReportId,
|
||||
pub report_type: String,
|
||||
pub mod_id: Option<ModId>,
|
||||
pub project_id: Option<ProjectId>,
|
||||
pub version_id: Option<VersionId>,
|
||||
pub user_id: Option<UserId>,
|
||||
pub body: String,
|
||||
@@ -40,7 +40,7 @@ impl Report {
|
||||
",
|
||||
self.id as ReportId,
|
||||
self.report_type_id as ReportTypeId,
|
||||
self.mod_id.map(|x| x.0 as i64),
|
||||
self.project_id.map(|x| x.0 as i64),
|
||||
self.version_id.map(|x| x.0 as i64),
|
||||
self.user_id.map(|x| x.0 as i64),
|
||||
self.body,
|
||||
@@ -72,7 +72,7 @@ impl Report {
|
||||
Ok(Some(QueryReport {
|
||||
id,
|
||||
report_type: row.name,
|
||||
mod_id: row.mod_id.map(ModId),
|
||||
project_id: row.mod_id.map(ProjectId),
|
||||
version_id: row.version_id.map(VersionId),
|
||||
user_id: row.user_id.map(UserId),
|
||||
body: row.body,
|
||||
@@ -108,7 +108,7 @@ impl Report {
|
||||
Ok(e.right().map(|row| QueryReport {
|
||||
id: ReportId(row.id),
|
||||
report_type: row.name,
|
||||
mod_id: row.mod_id.map(ModId),
|
||||
project_id: row.mod_id.map(ProjectId),
|
||||
version_id: row.version_id.map(VersionId),
|
||||
user_id: row.user_id.map(UserId),
|
||||
body: row.body,
|
||||
|
||||
@@ -61,7 +61,7 @@ impl TeamBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// A team of users who control a mod
|
||||
/// A team of users who control a project
|
||||
pub struct Team {
|
||||
/// The id of the team
|
||||
pub id: TeamId,
|
||||
@@ -412,8 +412,8 @@ impl TeamMember {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_from_user_id_mod<'a, 'b, E>(
|
||||
id: ModId,
|
||||
pub async fn get_from_user_id_project<'a, 'b, E>(
|
||||
id: ProjectId,
|
||||
user_id: UserId,
|
||||
executor: E,
|
||||
) -> Result<Option<Self>, super::DatabaseError>
|
||||
@@ -426,7 +426,7 @@ impl TeamMember {
|
||||
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE
|
||||
WHERE m.id = $1
|
||||
",
|
||||
id as ModId,
|
||||
id as ProjectId,
|
||||
user_id as UserId
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::ids::{ModId, UserId};
|
||||
use super::ids::{ProjectId, UserId};
|
||||
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
@@ -24,7 +24,7 @@ impl User {
|
||||
avatar_url, bio, created
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, LOWER($3), $4, $5,
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8
|
||||
)
|
||||
",
|
||||
@@ -186,17 +186,17 @@ impl User {
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub async fn get_mods<'a, E>(
|
||||
pub async fn get_projects<'a, E>(
|
||||
user_id: UserId,
|
||||
status: &str,
|
||||
exec: E,
|
||||
) -> Result<Vec<ModId>, sqlx::Error>
|
||||
) -> Result<Vec<ProjectId>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let mods = sqlx::query!(
|
||||
let projects = sqlx::query!(
|
||||
"
|
||||
SELECT m.id FROM mods m
|
||||
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
||||
@@ -206,23 +206,23 @@ impl User {
|
||||
status,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) })
|
||||
.try_collect::<Vec<ModId>>()
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) })
|
||||
.try_collect::<Vec<ProjectId>>()
|
||||
.await?;
|
||||
|
||||
Ok(mods)
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
pub async fn get_mods_private<'a, E>(
|
||||
pub async fn get_projects_private<'a, E>(
|
||||
user_id: UserId,
|
||||
exec: E,
|
||||
) -> Result<Vec<ModId>, sqlx::Error>
|
||||
) -> Result<Vec<ProjectId>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let mods = sqlx::query!(
|
||||
let projects = sqlx::query!(
|
||||
"
|
||||
SELECT m.id FROM mods m
|
||||
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
||||
@@ -231,11 +231,11 @@ impl User {
|
||||
user_id as UserId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) })
|
||||
.try_collect::<Vec<ModId>>()
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) })
|
||||
.try_collect::<Vec<ProjectId>>()
|
||||
.await?;
|
||||
|
||||
Ok(mods)
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
pub async fn remove<'a, 'b, E>(id: UserId, exec: E) -> Result<Option<()>, sqlx::error::Error>
|
||||
@@ -353,7 +353,7 @@ impl User {
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
use futures::TryStreamExt;
|
||||
let mods: Vec<ModId> = sqlx::query!(
|
||||
let projects: Vec<ProjectId> = sqlx::query!(
|
||||
"
|
||||
SELECT m.id FROM mods m
|
||||
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
||||
@@ -363,12 +363,12 @@ impl User {
|
||||
crate::models::teams::OWNER_ROLE
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) })
|
||||
.try_collect::<Vec<ModId>>()
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) })
|
||||
.try_collect::<Vec<ProjectId>>()
|
||||
.await?;
|
||||
|
||||
for mod_id in mods {
|
||||
let _result = super::mod_item::Mod::remove_full(mod_id, exec).await?;
|
||||
for project_id in projects {
|
||||
let _result = super::project_item::Project::remove_full(project_id, exec).await?;
|
||||
}
|
||||
|
||||
let notifications: Vec<i64> = sqlx::query!(
|
||||
@@ -439,4 +439,56 @@ impl User {
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn get_id_from_username_or_id<'a, 'b, E>(
|
||||
username_or_id: String,
|
||||
executor: E,
|
||||
) -> Result<Option<UserId>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let id_option = crate::models::ids::base62_impl::parse_base62(&*username_or_id).ok();
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let id = UserId(id as i64);
|
||||
|
||||
let mut user_id = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM users
|
||||
WHERE id = $1
|
||||
",
|
||||
id as UserId
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?
|
||||
.map(|x| UserId(x.id));
|
||||
|
||||
if user_id.is_none() {
|
||||
user_id = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM users
|
||||
WHERE LOWER(username) = LOWER($1)
|
||||
",
|
||||
username_or_id
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?
|
||||
.map(|x| UserId(x.id));
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
} else {
|
||||
let id = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM users
|
||||
WHERE LOWER(username) = LOWER($1)
|
||||
",
|
||||
username_or_id
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(id.map(|x| UserId(x.id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
|
||||
pub struct VersionBuilder {
|
||||
pub version_id: VersionId,
|
||||
pub mod_id: ModId,
|
||||
pub project_id: ProjectId,
|
||||
pub author_id: UserId,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
@@ -75,7 +75,7 @@ impl VersionBuilder {
|
||||
) -> Result<VersionId, DatabaseError> {
|
||||
let version = Version {
|
||||
id: self.version_id,
|
||||
mod_id: self.mod_id,
|
||||
project_id: self.project_id,
|
||||
author_id: self.author_id,
|
||||
name: self.name,
|
||||
version_number: self.version_number,
|
||||
@@ -95,7 +95,7 @@ impl VersionBuilder {
|
||||
SET updated = NOW()
|
||||
WHERE id = $1
|
||||
",
|
||||
self.mod_id as ModId,
|
||||
self.project_id as ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@@ -150,7 +150,7 @@ impl VersionBuilder {
|
||||
|
||||
pub struct Version {
|
||||
pub id: VersionId,
|
||||
pub mod_id: ModId,
|
||||
pub project_id: ProjectId,
|
||||
pub author_id: UserId,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
@@ -182,7 +182,7 @@ impl Version {
|
||||
)
|
||||
",
|
||||
self.id as VersionId,
|
||||
self.mod_id as ModId,
|
||||
self.project_id as ProjectId,
|
||||
self.author_id as UserId,
|
||||
&self.name,
|
||||
&self.version_number,
|
||||
@@ -359,8 +359,8 @@ impl Version {
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
pub async fn get_mod_versions<'a, E>(
|
||||
mod_id: ModId,
|
||||
pub async fn get_project_versions<'a, E>(
|
||||
project_id: ProjectId,
|
||||
game_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<String>>,
|
||||
exec: E,
|
||||
@@ -382,7 +382,7 @@ impl Version {
|
||||
) AS version
|
||||
ORDER BY version.date_published ASC
|
||||
",
|
||||
mod_id as ModId,
|
||||
project_id as ProjectId,
|
||||
&game_versions.unwrap_or_default(),
|
||||
&loaders.unwrap_or_default(),
|
||||
)
|
||||
@@ -417,7 +417,7 @@ impl Version {
|
||||
if let Some(row) = result {
|
||||
Ok(Some(Version {
|
||||
id,
|
||||
mod_id: ModId(row.mod_id),
|
||||
project_id: ProjectId(row.mod_id),
|
||||
author_id: UserId(row.author_id),
|
||||
name: row.name,
|
||||
version_number: row.version_number,
|
||||
@@ -450,6 +450,7 @@ impl Version {
|
||||
v.release_channel, v.featured
|
||||
FROM versions v
|
||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
ORDER BY v.date_published ASC
|
||||
",
|
||||
&version_ids_parsed
|
||||
)
|
||||
@@ -457,7 +458,7 @@ impl Version {
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|v| Version {
|
||||
id: VersionId(v.id),
|
||||
mod_id: ModId(v.mod_id),
|
||||
project_id: ProjectId(v.mod_id),
|
||||
author_id: UserId(v.author_id),
|
||||
name: v.name,
|
||||
version_number: v.version_number,
|
||||
@@ -480,7 +481,7 @@ impl Version {
|
||||
executor: E,
|
||||
) -> Result<Option<QueryVersion>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
@@ -566,7 +567,7 @@ impl Version {
|
||||
|
||||
Ok(Some(QueryVersion {
|
||||
id: VersionId(v.id),
|
||||
mod_id: ModId(v.mod_id),
|
||||
project_id: ProjectId(v.mod_id),
|
||||
author_id: UserId(v.author_id),
|
||||
name: v.version_name,
|
||||
version_number: v.version_number,
|
||||
@@ -625,7 +626,8 @@ impl Version {
|
||||
LEFT OUTER JOIN hashes h on f.id = h.file_id
|
||||
LEFT OUTER JOIN dependencies d on v.id = d.dependent_id
|
||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
GROUP BY v.id, rc.id;
|
||||
GROUP BY v.id, rc.id
|
||||
ORDER BY v.date_published ASC;
|
||||
",
|
||||
&version_ids_parsed
|
||||
)
|
||||
@@ -683,7 +685,7 @@ impl Version {
|
||||
|
||||
QueryVersion {
|
||||
id: VersionId(v.id),
|
||||
mod_id: ModId(v.mod_id),
|
||||
project_id: ProjectId(v.mod_id),
|
||||
author_id: UserId(v.author_id),
|
||||
name: v.version_name,
|
||||
version_number: v.version_number,
|
||||
@@ -727,7 +729,7 @@ pub struct FileHash {
|
||||
#[derive(Clone)]
|
||||
pub struct QueryVersion {
|
||||
pub id: VersionId,
|
||||
pub mod_id: ModId,
|
||||
pub project_id: ProjectId,
|
||||
pub author_id: UserId,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
|
||||
@@ -79,61 +79,3 @@ impl FileHost for BackblazeHost {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use authorization::*;
|
||||
use delete::*;
|
||||
use upload::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_authorization() {
|
||||
println!("{}", dotenv::var("BACKBLAZE_BUCKET_ID").unwrap());
|
||||
let authorization_data = authorize_account(
|
||||
&dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
|
||||
&dotenv::var("BACKBLAZE_KEY").unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
get_upload_url(
|
||||
&authorization_data,
|
||||
&dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_file_management() {
|
||||
let authorization_data = authorize_account(
|
||||
&dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
|
||||
&dotenv::var("BACKBLAZE_KEY").unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let upload_url_data = get_upload_url(
|
||||
&authorization_data,
|
||||
&dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let upload_data = upload_file(
|
||||
&upload_url_data,
|
||||
"text/plain",
|
||||
"test.txt",
|
||||
"test file".to_string().into_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
delete_file_version(
|
||||
&authorization_data,
|
||||
&upload_data.file_id,
|
||||
&upload_data.file_name,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -76,32 +76,3 @@ impl FileHost for S3Host {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::file_hosting::s3_host::S3Host;
|
||||
use crate::file_hosting::FileHost;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_file_management() {
|
||||
let s3_host = S3Host::new(
|
||||
&*dotenv::var("S3_BUCKET_NAME").unwrap(),
|
||||
&*dotenv::var("S3_REGION").unwrap(),
|
||||
&*dotenv::var("S3_URL").unwrap(),
|
||||
&*dotenv::var("S3_ACCESS_TOKEN").unwrap(),
|
||||
&*dotenv::var("S3_SECRET").unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
s3_host
|
||||
.upload_file(
|
||||
"text/plain",
|
||||
"test.txt",
|
||||
"test file".to_string().into_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
s3_host.delete_file_version("", "test.txt").await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
39
src/main.rs
39
src/main.rs
@@ -7,7 +7,7 @@ use env_logger::Env;
|
||||
use gumdrop::Options;
|
||||
use log::{error, info, warn};
|
||||
use rand::Rng;
|
||||
use search::indexing::index_mods;
|
||||
use search::indexing::index_projects;
|
||||
use search::indexing::IndexingSettings;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -18,6 +18,7 @@ mod models;
|
||||
mod routes;
|
||||
mod scheduler;
|
||||
mod search;
|
||||
mod validate;
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
struct Config {
|
||||
@@ -158,13 +159,10 @@ async fn main() -> std::io::Result<()> {
|
||||
return;
|
||||
}
|
||||
info!("Indexing local database");
|
||||
let settings = IndexingSettings {
|
||||
index_local: true,
|
||||
index_external: false,
|
||||
};
|
||||
let result = index_mods(pool_ref, settings, &thread_search_config).await;
|
||||
let settings = IndexingSettings { index_local: true };
|
||||
let result = index_projects(pool_ref, settings, &thread_search_config).await;
|
||||
if let Err(e) = result {
|
||||
warn!("Local mod indexing failed: {:?}", e);
|
||||
warn!("Local project indexing failed: {:?}", e);
|
||||
}
|
||||
info!("Done indexing local database");
|
||||
}
|
||||
@@ -229,12 +227,12 @@ async fn main() -> std::io::Result<()> {
|
||||
if local_skip {
|
||||
return;
|
||||
}
|
||||
info!("Indexing created mod queue");
|
||||
info!("Indexing created project queue");
|
||||
let result = search::indexing::queue::index_queue(&*queue, &thread_search_config).await;
|
||||
if let Err(e) = result {
|
||||
warn!("Indexing created mods failed: {:?}", e);
|
||||
warn!("Indexing created projects failed: {:?}", e);
|
||||
}
|
||||
info!("Done indexing created mod queue");
|
||||
info!("Done indexing created project queue");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -252,13 +250,12 @@ async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(
|
||||
Cors::new()
|
||||
Cors::default()
|
||||
.allowed_methods(vec!["GET", "POST", "DELETE", "PATCH", "PUT"])
|
||||
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
||||
.allowed_header(http::header::CONTENT_TYPE)
|
||||
.send_wildcard()
|
||||
.max_age(3600)
|
||||
.finish(),
|
||||
.allow_any_origin()
|
||||
.max_age(3600),
|
||||
)
|
||||
.wrap(
|
||||
// This is a hacky workaround to allowing the frontend server-side renderer to have
|
||||
@@ -297,19 +294,9 @@ async fn main() -> std::io::Result<()> {
|
||||
.data(indexing_queue.clone())
|
||||
.data(search_config.clone())
|
||||
.data(ip_salt.clone())
|
||||
.configure(routes::v1_config)
|
||||
.configure(routes::v2_config)
|
||||
.service(routes::index_get)
|
||||
.service(
|
||||
web::scope("/api/v1/")
|
||||
.configure(routes::auth_config)
|
||||
.configure(routes::tags_config)
|
||||
.configure(routes::mods_config)
|
||||
.configure(routes::versions_config)
|
||||
.configure(routes::teams_config)
|
||||
.configure(routes::users_config)
|
||||
.configure(routes::moderation_config)
|
||||
.configure(routes::reports_config)
|
||||
.configure(routes::notifications_config),
|
||||
)
|
||||
.service(web::scope("/maven/").configure(routes::maven_config))
|
||||
.default_service(web::get().to(routes::not_found))
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub use super::mods::{ModId, VersionId};
|
||||
pub use super::notifications::NotificationId;
|
||||
pub use super::projects::{ProjectId, VersionId};
|
||||
pub use super::reports::ReportId;
|
||||
pub use super::teams::TeamId;
|
||||
pub use super::users::UserId;
|
||||
@@ -105,7 +105,7 @@ macro_rules! base62_id_impl {
|
||||
impl_base62_display!($struct);
|
||||
}
|
||||
}
|
||||
base62_id_impl!(ModId, ModId);
|
||||
base62_id_impl!(ProjectId, ProjectId);
|
||||
base62_id_impl!(UserId, UserId);
|
||||
base62_id_impl!(VersionId, VersionId);
|
||||
base62_id_impl!(TeamId, TeamId);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod error;
|
||||
pub mod ids;
|
||||
pub mod mods;
|
||||
pub mod notifications;
|
||||
pub mod projects;
|
||||
pub mod reports;
|
||||
pub mod teams;
|
||||
pub mod users;
|
||||
|
||||
@@ -3,74 +3,77 @@ use super::teams::TeamId;
|
||||
use super::users::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// The ID of a specific mod, encoded as base62 for usage in the API
|
||||
/// The ID of a specific project, encoded as base62 for usage in the API
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ModId(pub u64);
|
||||
pub struct ProjectId(pub u64);
|
||||
|
||||
/// The ID of a specific version of a mod
|
||||
/// The ID of a specific version of a project
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct VersionId(pub u64);
|
||||
|
||||
/// A mod returned from the API
|
||||
/// A project returned from the API
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Mod {
|
||||
/// The ID of the mod, encoded as a base62 string.
|
||||
pub id: ModId,
|
||||
/// The slug of a mod, used for vanity URLs
|
||||
pub struct Project {
|
||||
/// The ID of the project, encoded as a base62 string.
|
||||
pub id: ProjectId,
|
||||
/// The slug of a project, used for vanity URLs
|
||||
pub slug: Option<String>,
|
||||
/// The team of people that has ownership of this mod.
|
||||
/// The project type of the project
|
||||
pub project_type: String,
|
||||
/// The team of people that has ownership of this project.
|
||||
pub team: TeamId,
|
||||
/// The title or name of the mod.
|
||||
/// The title or name of the project.
|
||||
pub title: String,
|
||||
/// A short description of the mod.
|
||||
/// A short description of the project.
|
||||
pub description: String,
|
||||
/// A long form description of the mod.
|
||||
/// A long form description of the project.
|
||||
pub body: String,
|
||||
/// The link to the long description of the mod. (Deprecated), being replaced by `body`
|
||||
/// The link to the long description of the project. (Deprecated), being replaced by `body`
|
||||
pub body_url: Option<String>,
|
||||
/// The date at which the mod was first published.
|
||||
/// The date at which the project was first published.
|
||||
pub published: DateTime<Utc>,
|
||||
/// The date at which the mod was first published.
|
||||
/// The date at which the project was first published.
|
||||
pub updated: DateTime<Utc>,
|
||||
/// The status of the mod
|
||||
pub status: ModStatus,
|
||||
/// The license of this mod
|
||||
/// The status of the project
|
||||
pub status: ProjectStatus,
|
||||
/// The license of this project
|
||||
pub license: License,
|
||||
|
||||
/// The support range for the client mod
|
||||
/// The support range for the client project*
|
||||
pub client_side: SideType,
|
||||
/// The support range for the server mod
|
||||
/// The support range for the server project
|
||||
pub server_side: SideType,
|
||||
|
||||
/// The total number of downloads the mod has had.
|
||||
/// The total number of downloads the project has had.
|
||||
pub downloads: u32,
|
||||
/// The total number of followers this mod has accumulated
|
||||
/// The total number of followers this project has accumulated
|
||||
pub followers: u32,
|
||||
|
||||
/// A list of the categories that the mod is in.
|
||||
/// A list of the categories that the project is in.
|
||||
pub categories: Vec<String>,
|
||||
/// A list of ids for versions of the mod.
|
||||
/// A list of ids for versions of the project.
|
||||
pub versions: Vec<VersionId>,
|
||||
/// The URL of the icon of the mod
|
||||
/// The URL of the icon of the project
|
||||
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 project.
|
||||
pub issues_url: Option<String>,
|
||||
/// An optional link to the source code for the mod.
|
||||
/// An optional link to the source code for the project.
|
||||
pub source_url: Option<String>,
|
||||
/// An optional link to the mod's wiki page or other relevant information.
|
||||
/// An optional link to the project's wiki page or other relevant information.
|
||||
pub wiki_url: Option<String>,
|
||||
/// An optional link to the mod's discord
|
||||
/// An optional link to the project's discord
|
||||
pub discord_url: Option<String>,
|
||||
/// An optional list of all donation links the mod has
|
||||
/// An optional list of all donation links the project has
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SideType {
|
||||
Required,
|
||||
@@ -113,22 +116,23 @@ pub struct License {
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||
pub struct DonationLink {
|
||||
pub id: String,
|
||||
pub platform: String,
|
||||
#[validate(url)]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// A status decides the visbility of a mod in search, URLs, and the whole site itself.
|
||||
/// Approved - Mod is displayed on search, and accessible by URL
|
||||
/// Rejected - Mod is not displayed on search, and not accessible by URL (Temporary state, mod can reapply)
|
||||
/// Draft - Mod is not displayed on search, and not accessible by URL
|
||||
/// Unlisted - Mod is not displayed on search, but accessible by URL
|
||||
/// Processing - Mod is not displayed on search, and not accessible by URL (Temporary state, mod under review)
|
||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
/// A status decides the visbility of a project in search, URLs, and the whole site itself.
|
||||
/// Approved - Project is displayed on search, and accessible by URL
|
||||
/// Rejected - Project is not displayed on search, and not accessible by URL (Temporary state, project can reapply)
|
||||
/// Draft - Project is not displayed on search, and not accessible by URL
|
||||
/// Unlisted - Project is not displayed on search, but accessible by URL
|
||||
/// Processing - Project is not displayed on search, and not accessible by URL (Temporary state, project under review)
|
||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ModStatus {
|
||||
pub enum ProjectStatus {
|
||||
Approved,
|
||||
Rejected,
|
||||
Draft,
|
||||
@@ -137,57 +141,57 @@ pub enum ModStatus {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModStatus {
|
||||
impl std::fmt::Display for ProjectStatus {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ModStatus {
|
||||
pub fn from_str(string: &str) -> ModStatus {
|
||||
impl ProjectStatus {
|
||||
pub fn from_str(string: &str) -> ProjectStatus {
|
||||
match string {
|
||||
"processing" => ModStatus::Processing,
|
||||
"rejected" => ModStatus::Rejected,
|
||||
"approved" => ModStatus::Approved,
|
||||
"draft" => ModStatus::Draft,
|
||||
"unlisted" => ModStatus::Unlisted,
|
||||
_ => ModStatus::Unknown,
|
||||
"processing" => ProjectStatus::Processing,
|
||||
"rejected" => ProjectStatus::Rejected,
|
||||
"approved" => ProjectStatus::Approved,
|
||||
"draft" => ProjectStatus::Draft,
|
||||
"unlisted" => ProjectStatus::Unlisted,
|
||||
_ => ProjectStatus::Unknown,
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ModStatus::Approved => "approved",
|
||||
ModStatus::Rejected => "rejected",
|
||||
ModStatus::Draft => "draft",
|
||||
ModStatus::Unlisted => "unlisted",
|
||||
ModStatus::Processing => "processing",
|
||||
ModStatus::Unknown => "unknown",
|
||||
ProjectStatus::Approved => "approved",
|
||||
ProjectStatus::Rejected => "rejected",
|
||||
ProjectStatus::Draft => "draft",
|
||||
ProjectStatus::Unlisted => "unlisted",
|
||||
ProjectStatus::Processing => "processing",
|
||||
ProjectStatus::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
match self {
|
||||
ModStatus::Approved => false,
|
||||
ModStatus::Rejected => true,
|
||||
ModStatus::Draft => true,
|
||||
ModStatus::Unlisted => false,
|
||||
ModStatus::Processing => true,
|
||||
ModStatus::Unknown => true,
|
||||
ProjectStatus::Approved => false,
|
||||
ProjectStatus::Rejected => true,
|
||||
ProjectStatus::Draft => true,
|
||||
ProjectStatus::Unlisted => false,
|
||||
ProjectStatus::Processing => true,
|
||||
ProjectStatus::Unknown => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_searchable(&self) -> bool {
|
||||
matches!(self, ModStatus::Approved)
|
||||
matches!(self, ProjectStatus::Approved)
|
||||
}
|
||||
}
|
||||
|
||||
/// A specific version of a mod
|
||||
/// A specific version of a project
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Version {
|
||||
/// The ID of the version, encoded as a base62 string.
|
||||
pub id: VersionId,
|
||||
/// The ID of the mod this version is for.
|
||||
pub mod_id: ModId,
|
||||
/// The ID of the project this version is for.
|
||||
pub project_id: ProjectId,
|
||||
/// The ID of the author who published this version
|
||||
pub author_id: UserId,
|
||||
/// Whether the version is featured or not
|
||||
@@ -197,9 +201,9 @@ pub struct Version {
|
||||
pub name: String,
|
||||
/// The version number. Ideally will follow semantic versioning
|
||||
pub version_number: String,
|
||||
/// The changelog for this version of the mod.
|
||||
/// The changelog for this version of the project.
|
||||
pub changelog: String,
|
||||
/// A link to the changelog for this version of the mod. (Deprecated), being replaced by `changelog`
|
||||
/// A link to the changelog for this version of the project. (Deprecated), being replaced by `changelog`
|
||||
pub changelog_url: Option<String>,
|
||||
/// The date that this version was published.
|
||||
pub date_published: DateTime<Utc>,
|
||||
@@ -210,15 +214,15 @@ pub struct Version {
|
||||
|
||||
/// A list of files available for download for this version.
|
||||
pub files: Vec<VersionFile>,
|
||||
/// A list of mods that this version depends on.
|
||||
/// A list of projects that this version depends on.
|
||||
pub dependencies: Vec<Dependency>,
|
||||
/// A list of versions of Minecraft that this version of the mod supports.
|
||||
/// A list of versions of Minecraft that this version of the project supports.
|
||||
pub game_versions: Vec<GameVersion>,
|
||||
/// The loaders that this version works on
|
||||
pub loaders: Vec<ModLoader>,
|
||||
pub loaders: Vec<Loader>,
|
||||
}
|
||||
|
||||
/// A single mod file, with a url for the file and the file's hash
|
||||
/// A single project file, with a url for the file and the file's hash
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VersionFile {
|
||||
/// A map of hashes of the file. The key is the hashing algorithm
|
||||
@@ -310,14 +314,14 @@ impl DependencyType {
|
||||
}
|
||||
|
||||
/// A specific version of Minecraft
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(transparent)]
|
||||
pub struct GameVersion(pub String);
|
||||
|
||||
/// A mod loader
|
||||
/// A project loader
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(transparent)]
|
||||
pub struct ModLoader(pub String);
|
||||
pub struct Loader(pub String);
|
||||
|
||||
// These fields must always succeed parsing; deserialize errors aren't
|
||||
// processed correctly (don't return JSON errors)
|
||||
@@ -22,7 +22,7 @@ pub struct Report {
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ItemType {
|
||||
Mod,
|
||||
Project,
|
||||
Version,
|
||||
User,
|
||||
Unknown,
|
||||
@@ -31,7 +31,7 @@ pub enum ItemType {
|
||||
impl ItemType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ItemType::Mod => "mod",
|
||||
ItemType::Project => "project",
|
||||
ItemType::Version => "version",
|
||||
ItemType::User => "user",
|
||||
ItemType::Unknown => "unknown",
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct TeamId(pub u64);
|
||||
pub const OWNER_ROLE: &str = "Owner";
|
||||
|
||||
// TODO: permissions, role names, etc
|
||||
/// A team of users who control a mod
|
||||
/// A team of users who control a project
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Team {
|
||||
/// The id of the team
|
||||
@@ -31,7 +31,7 @@ bitflags::bitflags! {
|
||||
const MANAGE_INVITES = 1 << 4;
|
||||
const REMOVE_MEMBER = 1 << 5;
|
||||
const EDIT_MEMBER = 1 << 6;
|
||||
const DELETE_MOD = 1 << 7;
|
||||
const DELETE_PROJECT = 1 << 7;
|
||||
const ALL = 0b11111111;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ pub async fn index_get() -> HttpResponse {
|
||||
});
|
||||
|
||||
HttpResponse::Ok().json(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database;
|
||||
use crate::models::mods::ModId;
|
||||
use crate::models::projects::ProjectId;
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
@@ -55,22 +55,13 @@ pub async fn maven_metadata(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
let id_option: Option<ModId> = serde_json::from_str(&*format!("\"{}\"", string)).ok();
|
||||
|
||||
let mod_data = if let Some(id) = id_option {
|
||||
match database::models::Mod::get_full(id.into(), &**pool).await {
|
||||
Ok(Some(data)) => Ok(Some(data)),
|
||||
Ok(None) => database::models::Mod::get_full_from_slug(&string, &**pool).await,
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
database::models::Mod::get_full_from_slug(&string, &**pool).await
|
||||
}
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let project_data =
|
||||
database::models::Project::get_full_from_slug_or_project_id(string, &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let data = if let Some(data) = mod_data {
|
||||
let data = if let Some(data) = project_data {
|
||||
data
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
@@ -85,17 +76,16 @@ pub async fn maven_metadata(
|
||||
} else {
|
||||
let user_id: database::models::ids::UserId = user.id.into();
|
||||
|
||||
let mod_exists = sqlx::query!(
|
||||
let project_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_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()))?
|
||||
.await?
|
||||
.exists;
|
||||
|
||||
authorized = mod_exists.unwrap_or(false);
|
||||
authorized = project_exists.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,15 +100,16 @@ pub async fn maven_metadata(
|
||||
LEFT JOIN release_channels ON release_channels.id = versions.release_channel
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
data.inner.id as database::models::ids::ModId
|
||||
data.inner.id as database::models::ids::ProjectId
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
let project_id: ProjectId = data.inner.id.into();
|
||||
|
||||
let respdata = Metadata {
|
||||
group_id: "maven.modrinth".to_string(),
|
||||
artifact_id: string,
|
||||
artifact_id: format!("{}", project_id),
|
||||
versioning: Versioning {
|
||||
latest: version_names
|
||||
.last()
|
||||
@@ -141,7 +132,7 @@ pub async fn maven_metadata(
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/xml")
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(|e| ApiError::XmlError(e))?))
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?))
|
||||
}
|
||||
|
||||
#[get("maven/modrinth/{id}/{versionnum}/{file}")]
|
||||
@@ -150,22 +141,21 @@ pub async fn version_file(
|
||||
web::Path((string, vnum, file)): web::Path<(String, String, String)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id_option: Option<ModId> = serde_json::from_str(&*format!("\"{}\"", string)).ok();
|
||||
let id_option: Option<ProjectId> = serde_json::from_str(&*format!("\"{}\"", string)).ok();
|
||||
|
||||
let mod_data = if let Some(id) = id_option {
|
||||
match database::models::Mod::get_full(id.into(), &**pool).await {
|
||||
let project_data = if let Some(id) = id_option {
|
||||
match database::models::Project::get_full(id.into(), &**pool).await {
|
||||
Ok(Some(data)) => Ok(Some(data)),
|
||||
Ok(None) => database::models::Mod::get_full_from_slug(&string, &**pool).await,
|
||||
Ok(None) => database::models::Project::get_full_from_slug(&string, &**pool).await,
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
database::models::Mod::get_full_from_slug(&string, &**pool).await
|
||||
}
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
database::models::Project::get_full_from_slug(&string, &**pool).await
|
||||
}?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let data = if let Some(data) = mod_data {
|
||||
let data = if let Some(data) = project_data {
|
||||
data
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
@@ -180,17 +170,16 @@ pub async fn version_file(
|
||||
} else {
|
||||
let user_id: database::models::ids::UserId = user.id.into();
|
||||
|
||||
let mod_exists = sqlx::query!(
|
||||
let project_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_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()))?
|
||||
.await?
|
||||
.exists;
|
||||
|
||||
authorized = mod_exists.unwrap_or(false);
|
||||
authorized = project_exists.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,12 +190,11 @@ pub async fn version_file(
|
||||
|
||||
let vid = if let Some(vid) = sqlx::query!(
|
||||
"SELECT id FROM versions WHERE mod_id = $1 AND version_number = $2",
|
||||
data.inner.id as database::models::ids::ModId,
|
||||
data.inner.id as database::models::ids::ProjectId,
|
||||
vnum
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.await?
|
||||
{
|
||||
vid
|
||||
} else {
|
||||
@@ -215,8 +203,7 @@ pub async fn version_file(
|
||||
|
||||
let version = if let Some(version) =
|
||||
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.await?
|
||||
{
|
||||
version
|
||||
} else {
|
||||
@@ -238,19 +225,17 @@ pub async fn version_file(
|
||||
};
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("text/xml")
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(|e| ApiError::XmlError(e))?));
|
||||
} else {
|
||||
if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) {
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?));
|
||||
} else if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) {
|
||||
return Ok(HttpResponse::TemporaryRedirect()
|
||||
.header("Location", &*selected_file.url)
|
||||
.body(""));
|
||||
} else if file == format!("{}-{}.jar", &string, &version.version_number) {
|
||||
if let Some(selected_file) = version.files.iter().last() {
|
||||
return Ok(HttpResponse::TemporaryRedirect()
|
||||
.header("Location", &*selected_file.url)
|
||||
.body(""));
|
||||
} else if file == format!("{}-{}.jar", &string, &version.version_number) {
|
||||
if let Some(selected_file) = version.files.iter().last() {
|
||||
return Ok(HttpResponse::TemporaryRedirect()
|
||||
.header("Location", &*selected_file.url)
|
||||
.body(""));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
use actix_web::web;
|
||||
|
||||
mod v1;
|
||||
pub use v1::v1_config;
|
||||
|
||||
mod auth;
|
||||
mod index;
|
||||
mod maven;
|
||||
mod mod_creation;
|
||||
mod moderation;
|
||||
mod mods;
|
||||
mod not_found;
|
||||
mod notifications;
|
||||
mod project_creation;
|
||||
mod projects;
|
||||
mod reports;
|
||||
mod tags;
|
||||
mod teams;
|
||||
mod users;
|
||||
mod version_creation;
|
||||
mod version_file;
|
||||
mod versions;
|
||||
|
||||
pub use auth::config as auth_config;
|
||||
@@ -22,21 +26,35 @@ pub use self::index::index_get;
|
||||
pub use self::not_found::not_found;
|
||||
use crate::file_hosting::FileHostingError;
|
||||
|
||||
pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(mods::mod_search);
|
||||
cfg.service(mods::mods_get);
|
||||
cfg.service(mod_creation::mod_create);
|
||||
pub fn v2_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/v2/")
|
||||
.configure(auth_config)
|
||||
.configure(tags_config)
|
||||
.configure(projects_config)
|
||||
.configure(versions_config)
|
||||
.configure(teams_config)
|
||||
.configure(users_config)
|
||||
.configure(moderation_config)
|
||||
.configure(reports_config)
|
||||
.configure(notifications_config),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn projects_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(projects::project_search);
|
||||
cfg.service(projects::projects_get);
|
||||
cfg.service(project_creation::project_create);
|
||||
|
||||
cfg.service(
|
||||
web::scope("mod")
|
||||
.service(mods::mod_slug_get)
|
||||
.service(mods::mod_get)
|
||||
.service(mods::mod_delete)
|
||||
.service(mods::mod_edit)
|
||||
.service(mods::mod_icon_edit)
|
||||
.service(mods::mod_follow)
|
||||
.service(mods::mod_unfollow)
|
||||
.service(web::scope("{mod_id}").service(versions::version_list)),
|
||||
web::scope("project")
|
||||
.service(projects::project_get)
|
||||
.service(projects::project_delete)
|
||||
.service(projects::project_edit)
|
||||
.service(projects::project_icon_edit)
|
||||
.service(projects::project_follow)
|
||||
.service(projects::project_unfollow)
|
||||
.service(web::scope("{project_id}").service(versions::version_list)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,9 +75,17 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
cfg.service(
|
||||
web::scope("version_file")
|
||||
.service(versions::delete_file)
|
||||
.service(versions::get_version_from_hash)
|
||||
.service(versions::download_version),
|
||||
.service(version_file::delete_file)
|
||||
.service(version_file::get_version_from_hash)
|
||||
.service(version_file::download_version)
|
||||
.service(version_file::get_update_from_hash),
|
||||
);
|
||||
|
||||
cfg.service(
|
||||
web::scope("version_files")
|
||||
.service(version_file::get_versions_from_hashes)
|
||||
.service(version_file::download_files)
|
||||
.service(version_file::update_files),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,9 +95,8 @@ pub fn users_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(users::users_get);
|
||||
cfg.service(
|
||||
web::scope("user")
|
||||
.service(users::user_username_get)
|
||||
.service(users::user_get)
|
||||
.service(users::mods_list)
|
||||
.service(users::projects_list)
|
||||
.service(users::user_delete)
|
||||
.service(users::user_edit)
|
||||
.service(users::user_icon_edit)
|
||||
@@ -102,7 +127,7 @@ pub fn notifications_config(cfg: &mut web::ServiceConfig) {
|
||||
}
|
||||
|
||||
pub fn moderation_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::scope("moderation").service(moderation::mods));
|
||||
cfg.service(web::scope("moderation").service(moderation::get_projects));
|
||||
}
|
||||
|
||||
pub fn reports_config(cfg: &mut web::ServiceConfig) {
|
||||
@@ -117,8 +142,10 @@ pub enum ApiError {
|
||||
EnvError(#[from] dotenv::Error),
|
||||
#[error("Error while uploading file")]
|
||||
FileHostingError(#[from] FileHostingError),
|
||||
#[error("Internal server error: {0}")]
|
||||
#[error("Database Error: {0}")]
|
||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||
#[error("Database Error: {0}")]
|
||||
SqlxDatabaseError(#[from] sqlx::Error),
|
||||
#[error("Internal server error: {0}")]
|
||||
XmlError(String),
|
||||
#[error("Deserialization error: {0}")]
|
||||
@@ -129,6 +156,8 @@ pub enum ApiError {
|
||||
CustomAuthenticationError(String),
|
||||
#[error("Invalid Input: {0}")]
|
||||
InvalidInputError(String),
|
||||
#[error("Error while validating input: {0}")]
|
||||
ValidationError(#[from] validator::ValidationErrors),
|
||||
#[error("Search Error: {0}")]
|
||||
SearchError(#[from] meilisearch_sdk::errors::Error),
|
||||
#[error("Indexing Error: {0}")]
|
||||
@@ -140,6 +169,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
match self {
|
||||
ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::SqlxDatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||
ApiError::XmlError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -148,6 +178,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::IndexingError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::FileHostingError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::InvalidInputError(..) => actix_web::http::StatusCode::BAD_REQUEST,
|
||||
ApiError::ValidationError(..) => actix_web::http::StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +187,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
ApiError::EnvError(..) => "environment_error",
|
||||
ApiError::SqlxDatabaseError(..) => "database_error",
|
||||
ApiError::DatabaseError(..) => "database_error",
|
||||
ApiError::AuthenticationError(..) => "unauthorized",
|
||||
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
||||
@@ -165,6 +197,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::IndexingError(..) => "indexing_error",
|
||||
ApiError::FileHostingError(..) => "file_hosting_error",
|
||||
ApiError::InvalidInputError(..) => "invalid_input",
|
||||
ApiError::ValidationError(..) => "invalid_input",
|
||||
},
|
||||
description: &self.to_string(),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::check_is_moderator_from_headers;
|
||||
use crate::database;
|
||||
use crate::models::mods::{Mod, ModStatus};
|
||||
use crate::models::projects::{Project, ProjectStatus};
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
@@ -9,15 +9,15 @@ use sqlx::PgPool;
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResultCount {
|
||||
#[serde(default = "default_count")]
|
||||
count: i16,
|
||||
pub count: i16,
|
||||
}
|
||||
|
||||
fn default_count() -> i16 {
|
||||
100
|
||||
}
|
||||
|
||||
#[get("mods")]
|
||||
pub async fn mods(
|
||||
#[get("projects")]
|
||||
pub async fn get_projects(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
count: web::Query<ResultCount>,
|
||||
@@ -26,7 +26,7 @@ pub async fn mods(
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let mod_ids = sqlx::query!(
|
||||
let project_ids = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM mods
|
||||
WHERE status = (
|
||||
@@ -35,21 +35,19 @@ pub async fn mods(
|
||||
ORDER BY updated ASC
|
||||
LIMIT $2;
|
||||
",
|
||||
ModStatus::Processing.as_str(),
|
||||
ProjectStatus::Processing.as_str(),
|
||||
count.count as i64
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ids::ModId(m.id))) })
|
||||
.try_collect::<Vec<database::models::ModId>>()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ProjectId(m.id))) })
|
||||
.try_collect::<Vec<database::models::ProjectId>>()
|
||||
.await?;
|
||||
|
||||
let mods: Vec<Mod> = database::models::mod_item::Mod::get_many_full(mod_ids, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
let projects: Vec<Project> = database::Project::get_many_full(project_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(super::mods::convert_mod)
|
||||
.map(super::projects::convert_project)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(mods))
|
||||
Ok(HttpResponse::Ok().json(projects))
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@ pub async fn notifications_get(
|
||||
|
||||
let notifications_data =
|
||||
database::models::notification_item::Notification::get_many(notification_ids, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
let mut notifications: Vec<Notification> = Vec::new();
|
||||
|
||||
@@ -52,9 +51,7 @@ pub async fn notification_get(
|
||||
let id = info.into_inner().0;
|
||||
|
||||
let notification_data =
|
||||
database::models::notification_item::Notification::get(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
|
||||
|
||||
if let Some(data) = notification_data {
|
||||
if user.id == data.user_id.into() || user.role.is_mod() {
|
||||
@@ -100,17 +97,13 @@ pub async fn notification_delete(
|
||||
let id = info.into_inner().0;
|
||||
|
||||
let notification_data =
|
||||
database::models::notification_item::Notification::get(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
|
||||
|
||||
if let Some(data) = notification_data {
|
||||
if data.user_id == user.id.into() || user.role.is_mod() {
|
||||
database::models::notification_item::Notification::remove(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
database::models::notification_item::Notification::remove(id.into(), &**pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthenticationError(
|
||||
"You are not authorized to delete this notification!".to_string(),
|
||||
|
||||
@@ -2,7 +2,9 @@ use crate::auth::{get_user_from_headers, AuthenticationError};
|
||||
use crate::database::models;
|
||||
use crate::file_hosting::{FileHost, FileHostingError};
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::mods::{DonationLink, License, ModId, ModStatus, SideType, VersionId};
|
||||
use crate::models::projects::{
|
||||
DonationLink, License, ProjectId, ProjectStatus, SideType, VersionId,
|
||||
};
|
||||
use crate::models::users::UserId;
|
||||
use crate::routes::version_creation::InitialVersionData;
|
||||
use crate::search::indexing::{queue::CreationQueue, IndexingError};
|
||||
@@ -11,16 +13,19 @@ use actix_web::http::StatusCode;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use futures::stream::StreamExt;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CreateError {
|
||||
#[error("Environment Error")]
|
||||
EnvError(#[from] dotenv::Error),
|
||||
#[error("An unknown database error occured")]
|
||||
#[error("An unknown database error occurred")]
|
||||
SqlxDatabaseError(#[from] sqlx::Error),
|
||||
#[error("Database Error: {0}")]
|
||||
DatabaseError(#[from] models::DatabaseError),
|
||||
@@ -30,11 +35,15 @@ pub enum CreateError {
|
||||
MultipartError(actix_multipart::MultipartError),
|
||||
#[error("Error while parsing JSON: {0}")]
|
||||
SerDeError(#[from] serde_json::Error),
|
||||
#[error("Error while validating input: {0}")]
|
||||
ValidationError(#[from] validator::ValidationErrors),
|
||||
#[error("Error while uploading file")]
|
||||
FileHostingError(#[from] FileHostingError),
|
||||
#[error("Error while validating uploaded file: {0}")]
|
||||
FileValidationError(#[from] crate::validate::ValidationError),
|
||||
#[error("{}", .0)]
|
||||
MissingValueError(String),
|
||||
#[error("Invalid format for mod icon: {0}")]
|
||||
#[error("Invalid format for project icon: {0}")]
|
||||
InvalidIconFormat(String),
|
||||
#[error("Error with multipart data: {0}")]
|
||||
InvalidInput(String),
|
||||
@@ -46,7 +55,7 @@ pub enum CreateError {
|
||||
InvalidCategory(String),
|
||||
#[error("Invalid file type for version file: {0}")]
|
||||
InvalidFileType(String),
|
||||
#[error("Slug collides with other mod's id!")]
|
||||
#[error("Slug collides with other project's id!")]
|
||||
SlugCollision,
|
||||
#[error("Authentication Error: {0}")]
|
||||
Unauthorized(#[from] AuthenticationError),
|
||||
@@ -74,6 +83,8 @@ impl actix_web::ResponseError for CreateError {
|
||||
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
|
||||
CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED,
|
||||
CreateError::SlugCollision => StatusCode::BAD_REQUEST,
|
||||
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,46 +108,81 @@ impl actix_web::ResponseError for CreateError {
|
||||
CreateError::Unauthorized(..) => "unauthorized",
|
||||
CreateError::CustomAuthenticationError(..) => "unauthorized",
|
||||
CreateError::SlugCollision => "invalid_input",
|
||||
CreateError::ValidationError(..) => "invalid_input",
|
||||
CreateError::FileValidationError(..) => "invalid_input",
|
||||
},
|
||||
description: &self.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct ModCreateData {
|
||||
/// The title or name of the mod.
|
||||
pub mod_name: String,
|
||||
/// The slug of a mod, used for vanity URLs
|
||||
pub mod_slug: String,
|
||||
/// A short description of the mod.
|
||||
pub mod_description: String,
|
||||
/// A long description of the mod, in markdown.
|
||||
pub mod_body: String,
|
||||
/// A list of initial versions to upload with the created mod
|
||||
pub initial_versions: Vec<InitialVersionData>,
|
||||
/// A list of the categories that the mod is in.
|
||||
pub categories: Vec<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 license page
|
||||
pub license_url: Option<String>,
|
||||
/// An optional link to the mod's discord.
|
||||
pub discord_url: Option<String>,
|
||||
/// An optional boolean. If true, the mod will be created as a draft.
|
||||
pub is_draft: Option<bool>,
|
||||
/// The support range for the client mod
|
||||
lazy_static! {
|
||||
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap();
|
||||
}
|
||||
|
||||
fn default_project_type() -> String {
|
||||
"mod".to_string()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||
struct ProjectCreateData {
|
||||
#[validate(length(min = 3, max = 256))]
|
||||
#[serde(alias = "mod_name")]
|
||||
/// The title or name of the project.
|
||||
pub title: String,
|
||||
#[validate(length(min = 1, max = 64))]
|
||||
#[serde(default = "default_project_type")]
|
||||
/// The project type of this mod
|
||||
pub project_type: String,
|
||||
#[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")]
|
||||
#[serde(alias = "mod_slug")]
|
||||
/// The slug of a project, used for vanity URLs
|
||||
pub slug: String,
|
||||
#[validate(length(min = 3, max = 2048))]
|
||||
#[serde(alias = "mod_description")]
|
||||
/// A short description of the project.
|
||||
pub description: String,
|
||||
#[validate(length(max = 65536))]
|
||||
#[serde(alias = "mod_body")]
|
||||
/// A long description of the project, in markdown.
|
||||
pub body: String,
|
||||
|
||||
/// The support range for the client project
|
||||
pub client_side: SideType,
|
||||
/// The support range for the server mod
|
||||
/// The support range for the server project
|
||||
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
|
||||
|
||||
#[validate(length(max = 64))]
|
||||
/// A list of initial versions to upload with the created project
|
||||
pub initial_versions: Vec<InitialVersionData>,
|
||||
#[validate(length(max = 3))]
|
||||
/// A list of the categories that the project is in.
|
||||
pub categories: Vec<String>,
|
||||
|
||||
#[validate(url, length(max = 2048))]
|
||||
/// An optional link to where to submit bugs or issues with the project.
|
||||
pub issues_url: Option<String>,
|
||||
#[validate(url, length(max = 2048))]
|
||||
/// An optional link to the source code for the project.
|
||||
pub source_url: Option<String>,
|
||||
#[validate(url, length(max = 2048))]
|
||||
/// An optional link to the project's wiki page or other relevant information.
|
||||
pub wiki_url: Option<String>,
|
||||
#[validate(url, length(max = 2048))]
|
||||
/// An optional link to the project's license page
|
||||
pub license_url: Option<String>,
|
||||
#[validate(url, length(max = 2048))]
|
||||
/// An optional link to the project's discord.
|
||||
pub discord_url: Option<String>,
|
||||
/// An optional list of all donation links the project has\
|
||||
#[validate]
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
|
||||
/// An optional boolean. If true, the project will be created as a draft.
|
||||
pub is_draft: Option<bool>,
|
||||
|
||||
/// The license id that the project follows
|
||||
pub license_id: String,
|
||||
}
|
||||
|
||||
pub struct UploadedFile {
|
||||
@@ -156,8 +202,8 @@ pub async fn undo_uploads(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("mod")]
|
||||
pub async fn mod_create(
|
||||
#[post("project")]
|
||||
pub async fn project_create(
|
||||
req: HttpRequest,
|
||||
payload: Multipart,
|
||||
client: Data<PgPool>,
|
||||
@@ -167,7 +213,7 @@ pub async fn mod_create(
|
||||
let mut transaction = client.begin().await?;
|
||||
let mut uploaded_files = Vec::new();
|
||||
|
||||
let result = mod_create_inner(
|
||||
let result = project_create_inner(
|
||||
req,
|
||||
payload,
|
||||
&mut transaction,
|
||||
@@ -196,7 +242,7 @@ pub async fn mod_create(
|
||||
|
||||
/*
|
||||
|
||||
Mod Creation Steps:
|
||||
Project Creation Steps:
|
||||
Get logged in user
|
||||
Must match the author in the version creation
|
||||
|
||||
@@ -206,12 +252,12 @@ Get logged in user
|
||||
- Create versions
|
||||
- Some shared logic with version creation
|
||||
- Create list of VersionBuilders
|
||||
- Create ModBuilder
|
||||
- Create ProjectBuilder
|
||||
|
||||
2. Upload
|
||||
- Icon: check file format & size
|
||||
- Upload to backblaze & record URL
|
||||
- Mod files
|
||||
- Project files
|
||||
- Check for matching version
|
||||
- File size limits?
|
||||
- Check file type
|
||||
@@ -221,10 +267,10 @@ Get logged in user
|
||||
|
||||
3. Creation
|
||||
- Database stuff
|
||||
- Add mod data to indexing queue
|
||||
- Add project data to indexing queue
|
||||
*/
|
||||
|
||||
async fn mod_create_inner(
|
||||
pub async fn project_create_inner(
|
||||
req: HttpRequest,
|
||||
mut payload: Multipart,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
@@ -238,15 +284,17 @@ async fn mod_create_inner(
|
||||
// The currently logged in user
|
||||
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
|
||||
let mod_id: ModId = models::generate_mod_id(transaction).await?.into();
|
||||
let project_id: ProjectId = models::generate_project_id(transaction).await?.into();
|
||||
|
||||
let mod_create_data;
|
||||
let project_create_data;
|
||||
let mut versions;
|
||||
let mut versions_map = std::collections::HashMap::new();
|
||||
|
||||
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
|
||||
{
|
||||
// The first multipart field must be named "data" and contain a
|
||||
// JSON `ModCreateData` object.
|
||||
// JSON `ProjectCreateData` object.
|
||||
|
||||
let mut field = payload
|
||||
.next()
|
||||
@@ -275,75 +323,20 @@ async fn mod_create_inner(
|
||||
while let Some(chunk) = field.next().await {
|
||||
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
|
||||
}
|
||||
let create_data: ModCreateData = serde_json::from_slice(&data)?;
|
||||
let create_data: ProjectCreateData = serde_json::from_slice(&data)?;
|
||||
|
||||
{
|
||||
// Verify the lengths of various fields in the mod create data
|
||||
/*
|
||||
# ModCreateData
|
||||
mod_name: 3..=256
|
||||
mod_description: 3..=2048,
|
||||
mod_body: max of 64KiB?,
|
||||
categories: Vec<String>, 1..=256
|
||||
issues_url: 0..=2048, (Validate url?)
|
||||
source_url: 0..=2048,
|
||||
wiki_url: 0..=2048,
|
||||
create_data.validate()?;
|
||||
|
||||
initial_versions: Vec<InitialVersionData>,
|
||||
team_members: Vec<TeamMember>,
|
||||
let slug_project_id_option: Option<ProjectId> =
|
||||
serde_json::from_str(&*format!("\"{}\"", create_data.slug)).ok();
|
||||
|
||||
# TeamMember:
|
||||
name: 3..=64
|
||||
role: 3..=64
|
||||
*/
|
||||
|
||||
check_length(3..=256, "mod name", &create_data.mod_name)?;
|
||||
check_length(3..=2048, "mod description", &create_data.mod_description)?;
|
||||
check_length(3..=64, "mod slug", &create_data.mod_slug)?;
|
||||
check_length(..65536, "mod body", &create_data.mod_body)?;
|
||||
|
||||
if create_data.categories.len() > 3 {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"The maximum number of categories for a mod is four.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
create_data
|
||||
.categories
|
||||
.iter()
|
||||
.try_for_each(|f| check_length(1..=256, "category", f))?;
|
||||
|
||||
if let Some(url) = &create_data.issues_url {
|
||||
check_length(..=2048, "url", url)?;
|
||||
}
|
||||
if let Some(url) = &create_data.wiki_url {
|
||||
check_length(..=2048, "url", url)?;
|
||||
}
|
||||
if let Some(url) = &create_data.source_url {
|
||||
check_length(..=2048, "url", url)?;
|
||||
}
|
||||
if let Some(url) = &create_data.discord_url {
|
||||
check_length(..=2048, "url", url)?;
|
||||
}
|
||||
if let Some(url) = &create_data.license_url {
|
||||
check_length(..=2048, "url", url)?;
|
||||
}
|
||||
|
||||
create_data
|
||||
.initial_versions
|
||||
.iter()
|
||||
.try_for_each(|v| super::version_creation::check_version(v))?;
|
||||
}
|
||||
|
||||
let slug_modid_option: Option<ModId> =
|
||||
serde_json::from_str(&*format!("\"{}\"", create_data.mod_slug)).ok();
|
||||
if let Some(slug_modid) = slug_modid_option {
|
||||
let slug_modid: models::ids::ModId = slug_modid.into();
|
||||
if let Some(slug_project_id) = slug_project_id_option {
|
||||
let slug_project_id: models::ids::ProjectId = slug_project_id.into();
|
||||
let results = sqlx::query!(
|
||||
"
|
||||
SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)
|
||||
",
|
||||
slug_modid as models::ids::ModId
|
||||
slug_project_id as models::ids::ProjectId
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await
|
||||
@@ -366,13 +359,31 @@ async fn mod_create_inner(
|
||||
)));
|
||||
}
|
||||
}
|
||||
versions
|
||||
.push(create_initial_version(data, mod_id, current_user.id, transaction).await?);
|
||||
versions.push(
|
||||
create_initial_version(
|
||||
data,
|
||||
project_id,
|
||||
current_user.id,
|
||||
&all_game_versions,
|
||||
transaction,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
mod_create_data = create_data;
|
||||
project_create_data = create_data;
|
||||
}
|
||||
|
||||
let project_type_id =
|
||||
models::ProjectTypeId::get_id(project_create_data.project_type.clone(), &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Project Type {} does not exist.",
|
||||
project_create_data.project_type.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut icon_url = None;
|
||||
|
||||
while let Some(item) = payload.next().await {
|
||||
@@ -391,14 +402,14 @@ async fn mod_create_inner(
|
||||
if name == "icon" {
|
||||
if icon_url.is_some() {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"Mods can only have one icon",
|
||||
"Projects can only have one icon",
|
||||
)));
|
||||
}
|
||||
// Upload the icon to the cdn
|
||||
icon_url = Some(
|
||||
process_icon_upload(
|
||||
uploaded_files,
|
||||
mod_id,
|
||||
project_id,
|
||||
file_extension,
|
||||
file_host,
|
||||
field,
|
||||
@@ -420,27 +431,33 @@ async fn mod_create_inner(
|
||||
|
||||
// `index` is always valid for these lists
|
||||
let created_version = versions.get_mut(index).unwrap();
|
||||
let version_data = mod_create_data.initial_versions.get(index).unwrap();
|
||||
let version_data = project_create_data.initial_versions.get(index).unwrap();
|
||||
|
||||
// Upload the new jar file
|
||||
let file_builder = super::version_creation::upload_file(
|
||||
super::version_creation::upload_file(
|
||||
&mut field,
|
||||
file_host,
|
||||
uploaded_files,
|
||||
&mut created_version.files,
|
||||
&cdn_url,
|
||||
&content_disposition,
|
||||
mod_id,
|
||||
project_id,
|
||||
&version_data.version_number,
|
||||
&*project_create_data.project_type,
|
||||
version_data.loaders.clone(),
|
||||
version_data.game_versions.clone(),
|
||||
&all_game_versions,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add the newly uploaded file to the existing or new version
|
||||
created_version.files.push(file_builder);
|
||||
}
|
||||
|
||||
{
|
||||
// Check to make sure that all specified files were uploaded
|
||||
for (version_data, builder) in mod_create_data.initial_versions.iter().zip(versions.iter())
|
||||
for (version_data, builder) in project_create_data
|
||||
.initial_versions
|
||||
.iter()
|
||||
.zip(versions.iter())
|
||||
{
|
||||
if version_data.file_parts.len() != builder.files.len() {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
@@ -450,11 +467,15 @@ async fn mod_create_inner(
|
||||
}
|
||||
|
||||
// Convert the list of category names to actual categories
|
||||
let mut categories = Vec::with_capacity(mod_create_data.categories.len());
|
||||
for category in &mod_create_data.categories {
|
||||
let id = models::categories::Category::get_id(&category, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| CreateError::InvalidCategory(category.clone()))?;
|
||||
let mut categories = Vec::with_capacity(project_create_data.categories.len());
|
||||
for category in &project_create_data.categories {
|
||||
let id = models::categories::Category::get_id_project(
|
||||
&category,
|
||||
project_type_id,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| CreateError::InvalidCategory(category.clone()))?;
|
||||
categories.push(id);
|
||||
}
|
||||
|
||||
@@ -470,10 +491,10 @@ async fn mod_create_inner(
|
||||
let team_id = team.insert(&mut *transaction).await?;
|
||||
|
||||
let status;
|
||||
if mod_create_data.is_draft.unwrap_or(false) {
|
||||
status = ModStatus::Draft;
|
||||
if project_create_data.is_draft.unwrap_or(false) {
|
||||
status = ProjectStatus::Draft;
|
||||
} else {
|
||||
status = ModStatus::Processing;
|
||||
status = ProjectStatus::Processing;
|
||||
}
|
||||
|
||||
let status_id = models::StatusId::get_id(&status, &mut *transaction)
|
||||
@@ -482,7 +503,7 @@ async fn mod_create_inner(
|
||||
CreateError::InvalidInput(format!("Status {} does not exist.", status.clone()))
|
||||
})?;
|
||||
let client_side_id =
|
||||
models::SideTypeId::get_id(&mod_create_data.client_side, &mut *transaction)
|
||||
models::SideTypeId::get_id(&project_create_data.client_side, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
@@ -491,7 +512,7 @@ async fn mod_create_inner(
|
||||
})?;
|
||||
|
||||
let server_side_id =
|
||||
models::SideTypeId::get_id(&mod_create_data.server_side, &mut *transaction)
|
||||
models::SideTypeId::get_id(&project_create_data.server_side, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
@@ -500,14 +521,14 @@ async fn mod_create_inner(
|
||||
})?;
|
||||
|
||||
let license_id =
|
||||
models::categories::License::get_id(&mod_create_data.license_id, &mut *transaction)
|
||||
models::categories::License::get_id(&project_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 {
|
||||
if let Some(urls) = &project_create_data.donation_urls {
|
||||
for url in urls {
|
||||
let platform_id = models::DonationPlatformId::get_id(&url.id, &mut *transaction)
|
||||
.await?
|
||||
@@ -518,8 +539,8 @@ async fn mod_create_inner(
|
||||
))
|
||||
})?;
|
||||
|
||||
donation_urls.push(models::mod_item::DonationUrl {
|
||||
mod_id: mod_id.into(),
|
||||
donation_urls.push(models::project_item::DonationUrl {
|
||||
project_id: project_id.into(),
|
||||
platform_id,
|
||||
platform_short: "".to_string(),
|
||||
platform_name: "".to_string(),
|
||||
@@ -528,72 +549,76 @@ async fn mod_create_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let mod_builder = models::mod_item::ModBuilder {
|
||||
mod_id: mod_id.into(),
|
||||
let project_builder = models::project_item::ProjectBuilder {
|
||||
project_id: project_id.into(),
|
||||
project_type_id,
|
||||
team_id,
|
||||
title: mod_create_data.mod_name,
|
||||
description: mod_create_data.mod_description,
|
||||
body: mod_create_data.mod_body,
|
||||
title: project_create_data.title,
|
||||
description: project_create_data.description,
|
||||
body: project_create_data.body,
|
||||
icon_url,
|
||||
issues_url: mod_create_data.issues_url,
|
||||
source_url: mod_create_data.source_url,
|
||||
wiki_url: mod_create_data.wiki_url,
|
||||
issues_url: project_create_data.issues_url,
|
||||
source_url: project_create_data.source_url,
|
||||
wiki_url: project_create_data.wiki_url,
|
||||
|
||||
license_url: mod_create_data.license_url,
|
||||
discord_url: mod_create_data.discord_url,
|
||||
license_url: project_create_data.license_url,
|
||||
discord_url: project_create_data.discord_url,
|
||||
categories,
|
||||
initial_versions: versions,
|
||||
status: status_id,
|
||||
client_side: client_side_id,
|
||||
server_side: server_side_id,
|
||||
license: license_id,
|
||||
slug: Some(mod_create_data.mod_slug),
|
||||
slug: Some(project_create_data.slug),
|
||||
donation_urls,
|
||||
};
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let response = crate::models::mods::Mod {
|
||||
id: mod_id,
|
||||
slug: mod_builder.slug.clone(),
|
||||
let response = crate::models::projects::Project {
|
||||
id: project_id,
|
||||
slug: project_builder.slug.clone(),
|
||||
project_type: project_create_data.project_type.clone(),
|
||||
team: team_id.into(),
|
||||
title: mod_builder.title.clone(),
|
||||
description: mod_builder.description.clone(),
|
||||
body: mod_builder.body.clone(),
|
||||
title: project_builder.title.clone(),
|
||||
description: project_builder.description.clone(),
|
||||
body: project_builder.body.clone(),
|
||||
body_url: None,
|
||||
published: now,
|
||||
updated: now,
|
||||
status: status.clone(),
|
||||
license: License {
|
||||
id: mod_create_data.license_id.clone(),
|
||||
id: project_create_data.license_id.clone(),
|
||||
name: "".to_string(),
|
||||
url: mod_builder.license_url.clone(),
|
||||
url: project_builder.license_url.clone(),
|
||||
},
|
||||
client_side: mod_create_data.client_side,
|
||||
server_side: mod_create_data.server_side,
|
||||
client_side: project_create_data.client_side,
|
||||
server_side: project_create_data.server_side,
|
||||
downloads: 0,
|
||||
followers: 0,
|
||||
categories: mod_create_data.categories,
|
||||
versions: mod_builder
|
||||
categories: project_create_data.categories,
|
||||
versions: project_builder
|
||||
.initial_versions
|
||||
.iter()
|
||||
.map(|v| v.version_id.into())
|
||||
.collect::<Vec<_>>(),
|
||||
icon_url: mod_builder.icon_url.clone(),
|
||||
issues_url: mod_builder.issues_url.clone(),
|
||||
source_url: mod_builder.source_url.clone(),
|
||||
wiki_url: mod_builder.wiki_url.clone(),
|
||||
discord_url: mod_builder.discord_url.clone(),
|
||||
donation_urls: mod_create_data.donation_urls.clone(),
|
||||
icon_url: project_builder.icon_url.clone(),
|
||||
issues_url: project_builder.issues_url.clone(),
|
||||
source_url: project_builder.source_url.clone(),
|
||||
wiki_url: project_builder.wiki_url.clone(),
|
||||
discord_url: project_builder.discord_url.clone(),
|
||||
donation_urls: project_create_data.donation_urls.clone(),
|
||||
};
|
||||
|
||||
let _mod_id = mod_builder.insert(&mut *transaction).await?;
|
||||
let _project_id = project_builder.insert(&mut *transaction).await?;
|
||||
|
||||
if status.is_searchable() {
|
||||
let index_mod =
|
||||
crate::search::indexing::local_import::query_one(mod_id.into(), &mut *transaction)
|
||||
.await?;
|
||||
indexing_queue.add(index_mod);
|
||||
let index_project = crate::search::indexing::local_import::query_one(
|
||||
project_id.into(),
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?;
|
||||
indexing_queue.add(index_project);
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
@@ -602,18 +627,18 @@ async fn mod_create_inner(
|
||||
|
||||
async fn create_initial_version(
|
||||
version_data: &InitialVersionData,
|
||||
mod_id: ModId,
|
||||
project_id: ProjectId,
|
||||
author: UserId,
|
||||
all_game_versions: &[models::categories::GameVersion],
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<models::version_item::VersionBuilder, CreateError> {
|
||||
if version_data.mod_id.is_some() {
|
||||
if version_data.project_id.is_some() {
|
||||
return Err(CreateError::InvalidInput(String::from(
|
||||
"Found mod id in initial version for new mod",
|
||||
"Found project id in initial version for new project",
|
||||
)));
|
||||
}
|
||||
|
||||
check_length(3..=256, "version name", &version_data.version_title)?;
|
||||
check_length(1..=32, "version number", &version_data.version_number)?;
|
||||
version_data.validate()?;
|
||||
|
||||
// Randomly generate a new id to be used for the version
|
||||
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
|
||||
@@ -623,13 +648,17 @@ async fn create_initial_version(
|
||||
.await?
|
||||
.expect("Release Channel not found in database");
|
||||
|
||||
let mut game_versions = Vec::with_capacity(version_data.game_versions.len());
|
||||
for v in &version_data.game_versions {
|
||||
let id = models::categories::GameVersion::get_id(&v.0, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| CreateError::InvalidGameVersion(v.0.clone()))?;
|
||||
game_versions.push(id);
|
||||
}
|
||||
let game_versions = version_data
|
||||
.game_versions
|
||||
.iter()
|
||||
.map(|x| {
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.ok_or_else(|| CreateError::InvalidGameVersion(x.0.clone()))
|
||||
.map(|y| y.id)
|
||||
})
|
||||
.collect::<Result<Vec<models::GameVersionId>, CreateError>>()?;
|
||||
|
||||
let mut loaders = Vec::with_capacity(version_data.loaders.len());
|
||||
for l in &version_data.loaders {
|
||||
@@ -647,7 +676,7 @@ async fn create_initial_version(
|
||||
|
||||
let version = models::version_item::VersionBuilder {
|
||||
version_id: version_id.into(),
|
||||
mod_id: mod_id.into(),
|
||||
project_id: project_id.into(),
|
||||
author_id: author.into(),
|
||||
name: version_data.version_title.clone(),
|
||||
version_number: version_data.version_number.clone(),
|
||||
@@ -668,7 +697,7 @@ async fn create_initial_version(
|
||||
|
||||
async fn process_icon_upload(
|
||||
uploaded_files: &mut Vec<UploadedFile>,
|
||||
mod_id: ModId,
|
||||
project_id: ProjectId,
|
||||
file_extension: &str,
|
||||
file_host: &dyn FileHost,
|
||||
mut field: actix_multipart::Field,
|
||||
@@ -689,7 +718,7 @@ async fn process_icon_upload(
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/icon.{}", mod_id, file_extension),
|
||||
&format!("data/{}/icon.{}", project_id, file_extension),
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
@@ -723,31 +752,3 @@ pub fn get_image_content_type(extension: &str) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_length(
|
||||
range: impl std::ops::RangeBounds<usize> + std::fmt::Debug,
|
||||
field_name: &str,
|
||||
field: &str,
|
||||
) -> Result<(), CreateError> {
|
||||
use std::ops::Bound;
|
||||
|
||||
let length = field.len();
|
||||
if !range.contains(&length) {
|
||||
let bounds = match (range.start_bound(), range.end_bound()) {
|
||||
(Bound::Included(a), Bound::Included(b)) => format!("between {} and {} bytes", a, b),
|
||||
(Bound::Included(a), Bound::Excluded(b)) => {
|
||||
format!("between {} and {} bytes", a, b - 1)
|
||||
}
|
||||
(Bound::Included(a), Bound::Unbounded) => format!("more than {} bytes", a),
|
||||
(Bound::Unbounded, Bound::Included(b)) => format!("less than or equal to {} bytes", b),
|
||||
(Bound::Unbounded, Bound::Excluded(b)) => format!("less than {} bytes", b),
|
||||
_ => format!("{:?}", range),
|
||||
};
|
||||
Err(CreateError::InvalidInput(format!(
|
||||
"The {} must be {}; got {}.",
|
||||
field_name, bounds, length
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use crate::models::ids::{ModId, UserId, VersionId};
|
||||
use crate::models::ids::{ProjectId, UserId, VersionId};
|
||||
use crate::models::reports::{ItemType, Report};
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
|
||||
@@ -21,10 +21,7 @@ pub async fn report_create(
|
||||
pool: web::Data<PgPool>,
|
||||
mut body: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
|
||||
@@ -48,7 +45,7 @@ pub async fn report_create(
|
||||
let mut report = crate::database::models::report_item::Report {
|
||||
id,
|
||||
report_type_id: report_type,
|
||||
mod_id: None,
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
user_id: None,
|
||||
body: new_report.body.clone(),
|
||||
@@ -57,9 +54,10 @@ pub async fn report_create(
|
||||
};
|
||||
|
||||
match new_report.item_type {
|
||||
ItemType::Mod => {
|
||||
report.mod_id =
|
||||
Some(serde_json::from_str::<ModId>(&*format!("\"{}\"", new_report.item_id))?.into())
|
||||
ItemType::Project => {
|
||||
report.project_id = Some(
|
||||
serde_json::from_str::<ProjectId>(&*format!("\"{}\"", new_report.item_id))?.into(),
|
||||
)
|
||||
}
|
||||
ItemType::Version => {
|
||||
report.version_id = Some(
|
||||
@@ -79,14 +77,8 @@ pub async fn report_create(
|
||||
}
|
||||
}
|
||||
|
||||
report
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
report.insert(&mut transaction).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(Report {
|
||||
id: id.into(),
|
||||
@@ -133,12 +125,10 @@ pub async fn reports(
|
||||
.map(|m| crate::database::models::ids::ReportId(m.id)))
|
||||
})
|
||||
.try_collect::<Vec<crate::database::models::ids::ReportId>>()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
let query_reports = crate::database::models::report_item::Report::get_many(report_ids, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let query_reports =
|
||||
crate::database::models::report_item::Report::get_many(report_ids, &**pool).await?;
|
||||
|
||||
let mut reports = Vec::new();
|
||||
|
||||
@@ -146,9 +136,9 @@ pub async fn reports(
|
||||
let mut item_id = "".to_string();
|
||||
let mut item_type = ItemType::Unknown;
|
||||
|
||||
if let Some(mod_id) = x.mod_id {
|
||||
item_id = serde_json::to_string::<ModId>(&mod_id.into())?;
|
||||
item_type = ItemType::Mod;
|
||||
if let Some(project_id) = x.project_id {
|
||||
item_id = serde_json::to_string::<ProjectId>(&project_id.into())?;
|
||||
item_type = ItemType::Project;
|
||||
} else if let Some(version_id) = x.version_id {
|
||||
item_id = serde_json::to_string::<VersionId>(&version_id.into())?;
|
||||
item_type = ItemType::Version;
|
||||
@@ -183,11 +173,10 @@ pub async fn delete_report(
|
||||
info.into_inner().0.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::check_is_admin_from_headers;
|
||||
use crate::database::models;
|
||||
use crate::database::models::categories::{DonationPlatform, License, ReportType};
|
||||
use crate::database::models::categories::{DonationPlatform, License, ProjectType, ReportType};
|
||||
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
|
||||
use models::categories::{Category, GameVersion, Loader};
|
||||
use sqlx::PgPool;
|
||||
@@ -30,27 +30,55 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct CategoryData {
|
||||
icon: String,
|
||||
name: String,
|
||||
project_type: String,
|
||||
}
|
||||
|
||||
// TODO: searching / filtering? Could be used to implement a live
|
||||
// searching category list
|
||||
#[get("category")]
|
||||
pub async fn category_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = Category::list(&**pool).await?;
|
||||
let results = Category::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| CategoryData {
|
||||
icon: x.icon,
|
||||
name: x.category,
|
||||
project_type: x.project_type,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[put("category/{name}")]
|
||||
#[put("category")]
|
||||
pub async fn category_create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
category: web::Path<(String,)>,
|
||||
new_category: web::Json<CategoryData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = category.into_inner().0;
|
||||
let project_type = crate::database::models::ProjectTypeId::get_id(
|
||||
(&new_category).project_type.clone(),
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError("Specified project type does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let _id = Category::builder().name(&name)?.insert(&**pool).await?;
|
||||
let _id = Category::builder()
|
||||
.name(&new_category.name)?
|
||||
.project_type(&project_type)?
|
||||
.icon(&new_category.icon)?
|
||||
.insert(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("category/{name}")]
|
||||
@@ -72,31 +100,56 @@ pub async fn category_delete(
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct LoaderData {
|
||||
icon: String,
|
||||
name: String,
|
||||
supported_project_types: Vec<String>,
|
||||
}
|
||||
|
||||
#[get("loader")]
|
||||
pub async fn loader_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = Loader::list(&**pool).await?;
|
||||
let results = Loader::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| LoaderData {
|
||||
icon: x.icon,
|
||||
name: x.loader,
|
||||
supported_project_types: x.supported_project_types,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[put("loader/{name}")]
|
||||
#[put("loader")]
|
||||
pub async fn loader_create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
loader: web::Path<(String,)>,
|
||||
new_loader: web::Json<LoaderData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = loader.into_inner().0;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let _id = Loader::builder().name(&name)?.insert(&**pool).await?;
|
||||
let project_types =
|
||||
ProjectType::get_many_id(&new_loader.supported_project_types, &mut *transaction).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
let _id = Loader::builder()
|
||||
.name(&new_loader.name)?
|
||||
.icon(&new_loader.icon)?
|
||||
.supported_project_types(&*project_types.into_iter().map(|x| x.id).collect::<Vec<_>>())?
|
||||
.insert(&mut transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("loader/{name}")]
|
||||
@@ -118,14 +171,21 @@ pub async fn loader_delete(
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct GameVersionQueryData {
|
||||
pub version: String,
|
||||
pub version_type: String,
|
||||
pub date: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct GameVersionQuery {
|
||||
#[serde(rename = "type")]
|
||||
type_: Option<String>,
|
||||
major: Option<bool>,
|
||||
@@ -134,16 +194,22 @@ pub struct GameVersionQueryData {
|
||||
#[get("game_version")]
|
||||
pub async fn game_version_list(
|
||||
pool: web::Data<PgPool>,
|
||||
query: web::Query<GameVersionQueryData>,
|
||||
query: web::Query<GameVersionQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if query.type_.is_some() || query.major.is_some() {
|
||||
let results =
|
||||
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
let results: Vec<GameVersionQueryData> = if query.type_.is_some() || query.major.is_some() {
|
||||
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?
|
||||
} else {
|
||||
let results = GameVersion::list(&**pool).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
GameVersion::list(&**pool).await?
|
||||
}
|
||||
.into_iter()
|
||||
.map(|x| GameVersionQueryData {
|
||||
version: x.version,
|
||||
version_type: x.version_type,
|
||||
date: x.date,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -177,7 +243,7 @@ pub async fn game_version_create(
|
||||
|
||||
let _id = builder.insert(&**pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("game_version/{name}")]
|
||||
@@ -199,7 +265,7 @@ pub async fn game_version_delete(
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
@@ -246,7 +312,7 @@ pub async fn license_create(
|
||||
.insert(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("license/{name}")]
|
||||
@@ -268,7 +334,7 @@ pub async fn license_delete(
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
@@ -315,7 +381,7 @@ pub async fn donation_platform_create(
|
||||
.insert(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("donation_platform/{name}")]
|
||||
@@ -337,7 +403,7 @@ pub async fn donation_platform_delete(
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
@@ -361,7 +427,7 @@ pub async fn report_type_create(
|
||||
|
||||
let _id = ReportType::builder().name(&name)?.insert(&**pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("report_type/{name}")]
|
||||
@@ -383,7 +449,7 @@ pub async fn report_type_delete(
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder};
|
||||
use crate::database::models::TeamMember;
|
||||
use crate::models::ids::ModId;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::models::teams::{Permissions, TeamId};
|
||||
use crate::models::users::UserId;
|
||||
use crate::routes::ApiError;
|
||||
@@ -76,10 +76,7 @@ pub async fn join_team(
|
||||
"You are already a member of this team".to_string(),
|
||||
));
|
||||
}
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
// Edit Team Member to set Accepted to True
|
||||
TeamMember::edit_team_member(
|
||||
@@ -92,17 +89,14 @@ pub async fn join_team(
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
transaction.commit().await?;
|
||||
} else {
|
||||
return Err(ApiError::InvalidInputError(
|
||||
"There is no pending request from this team".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
fn default_role() -> String {
|
||||
@@ -127,10 +121,7 @@ pub async fn add_team_member(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let team_id = info.into_inner().0.into();
|
||||
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let team_member =
|
||||
@@ -181,8 +172,7 @@ pub async fn add_team_member(
|
||||
}
|
||||
|
||||
crate::database::models::User::get(member.user_id, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::InvalidInputError("An invalid User ID specified".to_string()))?;
|
||||
|
||||
let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
|
||||
@@ -195,8 +185,7 @@ pub async fn add_team_member(
|
||||
accepted: false,
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
@@ -206,17 +195,16 @@ pub async fn add_team_member(
|
||||
team_id as crate::database::models::ids::TeamId
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
let team: TeamId = team_id.into();
|
||||
NotificationBuilder {
|
||||
title: "You have been invited to join a team!".to_string(),
|
||||
text: format!(
|
||||
"Team invite from {} to join the team for mod {}",
|
||||
"Team invite from {} to join the team for project {}",
|
||||
current_user.username, result.title
|
||||
),
|
||||
link: format!("mod/{}", ModId(result.id as u64)),
|
||||
link: format!("project/{}", ProjectId(result.id as u64)),
|
||||
actions: vec![
|
||||
NotificationActionBuilder {
|
||||
title: "Accept".to_string(),
|
||||
@@ -234,12 +222,9 @@ pub async fn add_team_member(
|
||||
.insert(new_member.user_id.into(), &mut transaction)
|
||||
.await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -262,10 +247,7 @@ pub async fn edit_team_member(
|
||||
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let team_member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?;
|
||||
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let member = match team_member {
|
||||
Some(m) => m,
|
||||
@@ -306,12 +288,9 @@ pub async fn edit_team_member(
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("{id}/members/{user_id}")]
|
||||
@@ -371,7 +350,7 @@ pub async fn remove_team_member(
|
||||
"You do not have permission to cancel a team invite".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::models::User;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::ids::ModId;
|
||||
use crate::models::mods::ModStatus;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::models::notifications::Notification;
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::models::users::{Role, UserId};
|
||||
use crate::routes::notifications::convert_notification;
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||
use futures::StreamExt;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
#[get("user")]
|
||||
pub async fn user_auth_get(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
Ok(HttpResponse::Ok().json(
|
||||
get_user_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(get_user_from_headers(req.headers(), &mut *pool.acquire().await?).await?))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -45,33 +40,13 @@ pub async fn users_get(
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
|
||||
let users_data = User::get_many(user_ids, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let users_data = User::get_many(user_ids, &**pool).await?;
|
||||
|
||||
let users: Vec<crate::models::users::User> = users_data.into_iter().map(convert_user).collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(users))
|
||||
}
|
||||
|
||||
#[get("@{id}")]
|
||||
pub async fn user_username_get(
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let user_data = User::get_from_username(id, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(data) = user_data {
|
||||
let response = convert_user(data);
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{id}")]
|
||||
pub async fn user_get(
|
||||
info: web::Path<(String,)>,
|
||||
@@ -83,19 +58,13 @@ pub async fn user_get(
|
||||
let mut user_data;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
user_data = User::get(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
user_data = User::get(id.into(), &**pool).await?;
|
||||
|
||||
if user_data.is_none() {
|
||||
user_data = User::get_from_username(string, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
user_data = User::get_from_username(string, &**pool).await?;
|
||||
}
|
||||
} else {
|
||||
user_data = User::get_from_username(string, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
user_data = User::get_from_username(string, &**pool).await?;
|
||||
}
|
||||
|
||||
if let Some(data) = user_data {
|
||||
@@ -120,48 +89,35 @@ fn convert_user(data: crate::database::models::user_item::User) -> crate::models
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{user_id}/mods")]
|
||||
pub async fn mods_list(
|
||||
#[get("{user_id}/projects")]
|
||||
pub async fn projects_list(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let id: crate::database::models::UserId = info.into_inner().0.into();
|
||||
let id_option =
|
||||
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
||||
.await?;
|
||||
|
||||
let user_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)",
|
||||
id as crate::database::models::UserId,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists;
|
||||
|
||||
if user_exists.unwrap_or(false) {
|
||||
if let Some(id) = id_option {
|
||||
let user_id: UserId = id.into();
|
||||
|
||||
let mod_data = if let Some(current_user) = user {
|
||||
let project_data = if let Some(current_user) = user {
|
||||
if current_user.role.is_mod() || current_user.id == user_id {
|
||||
User::get_mods_private(id, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
User::get_projects_private(id, &**pool).await?
|
||||
} else {
|
||||
User::get_mods(id, ModStatus::Approved.as_str(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await?
|
||||
}
|
||||
} else {
|
||||
User::get_mods(id, ModStatus::Approved.as_str(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await?
|
||||
};
|
||||
|
||||
let response = mod_data
|
||||
let response = project_data
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.collect::<Vec<crate::models::ids::ModId>>();
|
||||
.collect::<Vec<crate::models::ids::ProjectId>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
@@ -169,140 +125,147 @@ pub async fn mods_list(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
lazy_static! {
|
||||
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate)]
|
||||
pub struct EditUser {
|
||||
#[validate(length(min = 1, max = 255), regex = "RE_URL_SAFE")]
|
||||
pub username: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
#[validate(length(min = 1, max = 255), regex = "RE_URL_SAFE")]
|
||||
pub name: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
#[validate(email)]
|
||||
pub email: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
#[validate(length(max = 160))]
|
||||
pub bio: Option<Option<String>>,
|
||||
pub role: Option<String>,
|
||||
pub role: Option<Role>,
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
pub async fn user_edit(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
info: web::Path<(String,)>,
|
||||
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();
|
||||
new_user.validate()?;
|
||||
|
||||
if user.id == user_id || user.role.is_mod() {
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let id_option =
|
||||
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
||||
.await?;
|
||||
|
||||
if let Some(username) = &new_user.username {
|
||||
sqlx::query!(
|
||||
"
|
||||
if let Some(id) = id_option {
|
||||
let user_id: UserId = id.into();
|
||||
|
||||
if user.id == user_id || user.role.is_mod() {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
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()))?;
|
||||
}
|
||||
username,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(name) = &new_user.name {
|
||||
sqlx::query!(
|
||||
"
|
||||
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()))?;
|
||||
}
|
||||
name.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(bio) = &new_user.bio {
|
||||
sqlx::query!(
|
||||
"
|
||||
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()))?;
|
||||
}
|
||||
bio.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(email) = &new_user.email {
|
||||
sqlx::query!(
|
||||
"
|
||||
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(),
|
||||
));
|
||||
email.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let role = Role::from_string(role).to_string();
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
let role = 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()))?;
|
||||
}
|
||||
role,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
transaction.commit().await?;
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to edit this user!".to_string(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to edit this user!".to_string(),
|
||||
))
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,81 +278,87 @@ pub struct Extension {
|
||||
pub async fn user_icon_edit(
|
||||
web::Query(ext): web::Query<Extension>,
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
info: web::Path<(String,)>,
|
||||
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) {
|
||||
if let Some(content_type) = super::project_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 id_option =
|
||||
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
||||
.await?;
|
||||
|
||||
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(id) = id_option {
|
||||
if user.id != id.into() && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to edit this user's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = icon_url {
|
||||
if icon.starts_with(&cdn_url) {
|
||||
let name = icon.split('/').next();
|
||||
let mut icon_url = user.avatar_url;
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
let user_id: UserId = id.into();
|
||||
|
||||
if user.id != user_id {
|
||||
let new_user = User::get(id, &**pool).await?;
|
||||
|
||||
if let Some(new) = new_user {
|
||||
icon_url = new.avatar_url;
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 let Some(icon) = icon_url {
|
||||
if icon.starts_with(&cdn_url) {
|
||||
let name = icon.split('/').next();
|
||||
|
||||
if bytes.len() >= 262144 {
|
||||
return Err(ApiError::InvalidInputError(String::from(
|
||||
"Icons must be smaller than 256KiB",
|
||||
)));
|
||||
}
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("user/{}/icon.{}", id, ext.ext),
|
||||
bytes.to_vec(),
|
||||
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.{}", user_id, ext.ext),
|
||||
bytes.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET avatar_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.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(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
} else {
|
||||
Err(ApiError::InvalidInputError(format!(
|
||||
"Invalid format for user icon: {}",
|
||||
@@ -411,32 +380,34 @@ fn default_removal() -> String {
|
||||
#[delete("{id}")]
|
||||
pub async fn user_delete(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
removal_type: web::Query<RemovalType>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0;
|
||||
let id_option =
|
||||
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
||||
.await?;
|
||||
|
||||
if !user.role.is_mod() && user.id != id {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to delete this user!".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(id) = id_option {
|
||||
if !user.role.is_mod() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to delete this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let result;
|
||||
if &*removal_type.removal_type == "full" {
|
||||
result = crate::database::models::User::remove_full(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
} else {
|
||||
result = crate::database::models::User::remove(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
};
|
||||
let result;
|
||||
if &*removal_type.removal_type == "full" {
|
||||
result = crate::database::models::User::remove_full(id, &**pool).await?;
|
||||
} else {
|
||||
result = crate::database::models::User::remove(id, &**pool).await?;
|
||||
};
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
@@ -445,59 +416,68 @@ pub async fn user_delete(
|
||||
#[get("{id}/follows")]
|
||||
pub async fn user_follows(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0;
|
||||
let id_option =
|
||||
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
||||
.await?;
|
||||
|
||||
if !user.role.is_mod() && user.id != id {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to see the mods this user follows!".to_string(),
|
||||
));
|
||||
if let Some(id) = id_option {
|
||||
if !user.role.is_mod() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to see the projects this user follows!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let projects: Vec<ProjectId> = sqlx::query!(
|
||||
"
|
||||
SELECT mf.mod_id FROM mod_follows mf
|
||||
WHERE mf.follower_id = $1
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.mod_id as u64))) })
|
||||
.try_collect::<Vec<ProjectId>>()
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(projects))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let user_id: crate::database::models::UserId = id.into();
|
||||
let mods: Vec<ModId> = sqlx::query!(
|
||||
"
|
||||
SELECT mf.mod_id FROM mod_follows mf
|
||||
WHERE mf.follower_id = $1
|
||||
",
|
||||
user_id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.mod_id as u64))) })
|
||||
.try_collect::<Vec<ModId>>()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(mods))
|
||||
}
|
||||
|
||||
#[get("{id}/notifications")]
|
||||
pub async fn user_notifications(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0;
|
||||
let id_option =
|
||||
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
||||
.await?;
|
||||
|
||||
if !user.role.is_mod() && user.id != id {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to see the mods this user follows!".to_string(),
|
||||
));
|
||||
if let Some(id) = id_option {
|
||||
if !user.role.is_mod() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to see the notifications of this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let notifications: Vec<Notification> =
|
||||
crate::database::models::notification_item::Notification::get_many_user(id, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(convert_notification)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(notifications))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
|
||||
let notifications: Vec<Notification> =
|
||||
crate::database::models::notification_item::Notification::get_many_user(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.into_iter()
|
||||
.map(convert_notification)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(notifications))
|
||||
}
|
||||
|
||||
129
src/routes/v1/mod.rs
Normal file
129
src/routes/v1/mod.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use actix_web::web;
|
||||
|
||||
mod moderation;
|
||||
mod mods;
|
||||
mod reports;
|
||||
mod tags;
|
||||
mod users;
|
||||
mod versions;
|
||||
|
||||
pub fn v1_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/api/v1/")
|
||||
.configure(super::auth_config)
|
||||
.configure(tags_config)
|
||||
.configure(mods_config)
|
||||
.configure(versions_config)
|
||||
.configure(teams_config)
|
||||
.configure(users_config)
|
||||
.configure(moderation_config)
|
||||
.configure(reports_config)
|
||||
.configure(notifications_config),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn tags_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/tag/")
|
||||
.service(tags::category_list)
|
||||
.service(tags::category_create)
|
||||
.service(super::tags::category_delete)
|
||||
.service(tags::loader_list)
|
||||
.service(tags::loader_create)
|
||||
.service(super::tags::loader_delete)
|
||||
.service(super::tags::game_version_list)
|
||||
.service(super::tags::game_version_create)
|
||||
.service(super::tags::game_version_delete)
|
||||
.service(super::tags::license_create)
|
||||
.service(super::tags::license_delete)
|
||||
.service(super::tags::license_list)
|
||||
.service(super::tags::donation_platform_create)
|
||||
.service(super::tags::donation_platform_list)
|
||||
.service(super::tags::donation_platform_delete)
|
||||
.service(super::tags::report_type_create)
|
||||
.service(super::tags::report_type_delete)
|
||||
.service(super::tags::report_type_list),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(mods::mod_search);
|
||||
cfg.service(mods::mods_get);
|
||||
cfg.service(mods::mod_create);
|
||||
|
||||
cfg.service(
|
||||
web::scope("mod")
|
||||
.service(super::projects::project_get)
|
||||
.service(super::projects::project_delete)
|
||||
.service(super::projects::project_edit)
|
||||
.service(super::projects::project_icon_edit)
|
||||
.service(super::projects::project_follow)
|
||||
.service(super::projects::project_unfollow)
|
||||
.service(web::scope("{mod_id}").service(versions::version_list)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(versions::versions_get);
|
||||
cfg.service(super::version_creation::version_create);
|
||||
cfg.service(
|
||||
web::scope("version")
|
||||
.service(versions::version_get)
|
||||
.service(super::versions::version_delete)
|
||||
.service(super::version_creation::upload_file_to_version)
|
||||
.service(super::versions::version_edit),
|
||||
);
|
||||
cfg.service(
|
||||
web::scope("version_file")
|
||||
.service(versions::delete_file)
|
||||
.service(versions::get_version_from_hash)
|
||||
.service(versions::download_version),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn users_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(super::users::user_auth_get);
|
||||
|
||||
cfg.service(super::users::users_get);
|
||||
cfg.service(
|
||||
web::scope("user")
|
||||
.service(super::users::user_get)
|
||||
.service(users::mods_list)
|
||||
.service(super::users::user_delete)
|
||||
.service(super::users::user_edit)
|
||||
.service(super::users::user_icon_edit)
|
||||
.service(super::users::user_notifications)
|
||||
.service(super::users::user_follows),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn teams_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("team")
|
||||
.service(super::teams::team_members_get)
|
||||
.service(super::teams::edit_team_member)
|
||||
.service(super::teams::add_team_member)
|
||||
.service(super::teams::join_team)
|
||||
.service(super::teams::remove_team_member),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn notifications_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(super::notifications::notifications_get);
|
||||
|
||||
cfg.service(
|
||||
web::scope("notification")
|
||||
.service(super::notifications::notification_get)
|
||||
.service(super::notifications::notification_delete),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn moderation_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::scope("moderation").service(moderation::get_mods));
|
||||
}
|
||||
|
||||
pub fn reports_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(reports::reports);
|
||||
cfg.service(reports::report_create);
|
||||
cfg.service(super::reports::delete_report);
|
||||
}
|
||||
44
src/routes/v1/moderation.rs
Normal file
44
src/routes/v1/moderation.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::auth::check_is_moderator_from_headers;
|
||||
use crate::database;
|
||||
use crate::models::projects::{Project, ProjectStatus};
|
||||
use crate::routes::moderation::ResultCount;
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::web;
|
||||
use actix_web::{get, HttpRequest, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[get("mods")]
|
||||
pub async fn get_mods(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
count: web::Query<ResultCount>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_moderator_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let project_ids = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM mods
|
||||
WHERE status = (
|
||||
SELECT id FROM statuses WHERE status = $1
|
||||
)
|
||||
ORDER BY updated ASC
|
||||
LIMIT $2;
|
||||
",
|
||||
ProjectStatus::Processing.as_str(),
|
||||
count.count as i64
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ProjectId(m.id))) })
|
||||
.try_collect::<Vec<database::models::ProjectId>>()
|
||||
.await?;
|
||||
|
||||
let projects: Vec<Project> = database::Project::get_many_full(project_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(crate::routes::projects::convert_project)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(projects))
|
||||
}
|
||||
172
src/routes/v1/mods.rs
Normal file
172
src/routes/v1/mods.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::projects::SearchRequest;
|
||||
use crate::routes::project_creation::{project_create_inner, undo_uploads, CreateError};
|
||||
use crate::routes::projects::{convert_project, ProjectIds};
|
||||
use crate::routes::ApiError;
|
||||
use crate::search::indexing::queue::CreationQueue;
|
||||
use crate::search::{search_for_project, SearchConfig, SearchError};
|
||||
use crate::{database, models};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::web;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{get, post, HttpRequest, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ResultSearchMod {
|
||||
pub mod_id: String,
|
||||
pub slug: Option<String>,
|
||||
pub author: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub categories: Vec<String>,
|
||||
pub versions: Vec<String>,
|
||||
pub downloads: i32,
|
||||
pub follows: i32,
|
||||
pub page_url: String,
|
||||
pub icon_url: String,
|
||||
pub author_url: String,
|
||||
pub date_created: String,
|
||||
pub date_modified: String,
|
||||
pub latest_version: String,
|
||||
pub license: String,
|
||||
pub client_side: String,
|
||||
pub server_side: String,
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SearchResults {
|
||||
pub hits: Vec<ResultSearchMod>,
|
||||
pub offset: usize,
|
||||
pub limit: usize,
|
||||
pub total_hits: usize,
|
||||
}
|
||||
|
||||
#[get("mod")]
|
||||
pub async fn mod_search(
|
||||
web::Query(info): web::Query<SearchRequest>,
|
||||
config: web::Data<SearchConfig>,
|
||||
) -> Result<HttpResponse, SearchError> {
|
||||
let results = search_for_project(&info, &**config).await?;
|
||||
Ok(HttpResponse::Ok().json(SearchResults {
|
||||
hits: results
|
||||
.hits
|
||||
.into_iter()
|
||||
.map(|x| ResultSearchMod {
|
||||
mod_id: x.project_id.clone(),
|
||||
slug: x.slug,
|
||||
author: x.author.clone(),
|
||||
title: x.title,
|
||||
description: x.description,
|
||||
categories: x.categories,
|
||||
versions: x.versions,
|
||||
downloads: x.downloads,
|
||||
follows: x.follows,
|
||||
page_url: format!("https://modrinth.com/mod/{}", x.project_id),
|
||||
icon_url: x.icon_url,
|
||||
author_url: format!("https://modrinth.com/user/{}", x.author),
|
||||
date_created: x.date_created,
|
||||
date_modified: x.date_modified,
|
||||
latest_version: x.latest_version,
|
||||
license: x.license,
|
||||
client_side: x.client_side,
|
||||
server_side: x.server_side,
|
||||
host: "modrinth".to_string(),
|
||||
})
|
||||
.collect(),
|
||||
offset: results.offset,
|
||||
limit: results.limit,
|
||||
total_hits: results.total_hits,
|
||||
}))
|
||||
}
|
||||
|
||||
#[get("mods")]
|
||||
pub async fn mods_get(
|
||||
req: HttpRequest,
|
||||
ids: web::Query<ProjectIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let project_ids = serde_json::from_str::<Vec<models::ids::ProjectId>>(&*ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
|
||||
let projects_data = database::models::Project::get_many_full(project_ids, &**pool).await?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let mut projects = Vec::new();
|
||||
|
||||
for project_data in projects_data {
|
||||
let mut authorized = !project_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 project_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)",
|
||||
project_data.inner.team_id as database::models::ids::TeamId,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await?
|
||||
.exists;
|
||||
|
||||
authorized = project_exists.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if authorized {
|
||||
projects.push(convert_project(project_data));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(projects))
|
||||
}
|
||||
|
||||
#[post("mod")]
|
||||
pub async fn mod_create(
|
||||
req: HttpRequest,
|
||||
payload: Multipart,
|
||||
client: Data<PgPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
indexing_queue: Data<Arc<CreationQueue>>,
|
||||
) -> Result<HttpResponse, CreateError> {
|
||||
let mut transaction = client.begin().await?;
|
||||
let mut uploaded_files = Vec::new();
|
||||
|
||||
let result = project_create_inner(
|
||||
req,
|
||||
payload,
|
||||
&mut transaction,
|
||||
&***file_host,
|
||||
&mut uploaded_files,
|
||||
&***indexing_queue,
|
||||
)
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let undo_result = undo_uploads(&***file_host, &uploaded_files).await;
|
||||
let rollback_result = transaction.rollback().await;
|
||||
|
||||
if let Err(e) = undo_result {
|
||||
return Err(e);
|
||||
}
|
||||
if let Err(e) = rollback_result {
|
||||
return Err(e.into());
|
||||
}
|
||||
} else {
|
||||
transaction.commit().await?;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
195
src/routes/v1/reports.rs
Normal file
195
src/routes/v1/reports.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use crate::models::ids::ReportId;
|
||||
use crate::models::projects::{ProjectId, VersionId};
|
||||
use crate::models::users::UserId;
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::web;
|
||||
use actix_web::{get, post, HttpRequest, HttpResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Report {
|
||||
pub id: ReportId,
|
||||
pub report_type: String,
|
||||
pub item_id: String,
|
||||
pub item_type: ItemType,
|
||||
pub reporter: UserId,
|
||||
pub body: String,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ItemType {
|
||||
Mod,
|
||||
Version,
|
||||
User,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ItemType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ItemType::Mod => "mod",
|
||||
ItemType::Version => "version",
|
||||
ItemType::User => "user",
|
||||
ItemType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateReport {
|
||||
pub report_type: String,
|
||||
pub item_id: String,
|
||||
pub item_type: ItemType,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[post("report")]
|
||||
pub async fn report_create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
mut body: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
|
||||
let mut bytes = web::BytesMut::new();
|
||||
while let Some(item) = body.next().await {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInputError("Error while parsing request payload!".to_string())
|
||||
})?);
|
||||
}
|
||||
let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?;
|
||||
|
||||
let id = crate::database::models::generate_report_id(&mut transaction).await?;
|
||||
let report_type = crate::database::models::categories::ReportType::get_id(
|
||||
&*new_report.report_type,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError(format!("Invalid report type: {}", new_report.report_type))
|
||||
})?;
|
||||
let mut report = crate::database::models::report_item::Report {
|
||||
id,
|
||||
report_type_id: report_type,
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
user_id: None,
|
||||
body: new_report.body.clone(),
|
||||
reporter: current_user.id.into(),
|
||||
created: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
match new_report.item_type {
|
||||
ItemType::Mod => {
|
||||
report.project_id = Some(
|
||||
serde_json::from_str::<ProjectId>(&*format!("\"{}\"", new_report.item_id))?.into(),
|
||||
)
|
||||
}
|
||||
ItemType::Version => {
|
||||
report.version_id = Some(
|
||||
serde_json::from_str::<VersionId>(&*format!("\"{}\"", new_report.item_id))?.into(),
|
||||
)
|
||||
}
|
||||
ItemType::User => {
|
||||
report.user_id = Some(
|
||||
serde_json::from_str::<UserId>(&*format!("\"{}\"", new_report.item_id))?.into(),
|
||||
)
|
||||
}
|
||||
ItemType::Unknown => {
|
||||
return Err(ApiError::InvalidInputError(format!(
|
||||
"Invalid report item type: {}",
|
||||
new_report.item_type.as_str()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
report.insert(&mut transaction).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(Report {
|
||||
id: id.into(),
|
||||
report_type: new_report.report_type.clone(),
|
||||
item_id: new_report.item_id.clone(),
|
||||
item_type: new_report.item_type.clone(),
|
||||
reporter: current_user.id,
|
||||
body: new_report.body.clone(),
|
||||
created: chrono::Utc::now(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResultCount {
|
||||
#[serde(default = "default_count")]
|
||||
count: i16,
|
||||
}
|
||||
|
||||
fn default_count() -> i16 {
|
||||
100
|
||||
}
|
||||
|
||||
#[get("report")]
|
||||
pub async fn reports(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
count: web::Query<ResultCount>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_moderator_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let report_ids = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM reports
|
||||
ORDER BY created ASC
|
||||
LIMIT $1;
|
||||
",
|
||||
count.count as i64
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right()
|
||||
.map(|m| crate::database::models::ids::ReportId(m.id)))
|
||||
})
|
||||
.try_collect::<Vec<crate::database::models::ids::ReportId>>()
|
||||
.await?;
|
||||
|
||||
let query_reports =
|
||||
crate::database::models::report_item::Report::get_many(report_ids, &**pool).await?;
|
||||
|
||||
let mut reports = Vec::new();
|
||||
|
||||
for x in query_reports {
|
||||
let mut item_id = "".to_string();
|
||||
let mut item_type = ItemType::Unknown;
|
||||
|
||||
if let Some(project_id) = x.project_id {
|
||||
item_id = serde_json::to_string::<ProjectId>(&project_id.into())?;
|
||||
item_type = ItemType::Mod;
|
||||
} else if let Some(version_id) = x.version_id {
|
||||
item_id = serde_json::to_string::<VersionId>(&version_id.into())?;
|
||||
item_type = ItemType::Version;
|
||||
} else if let Some(user_id) = x.user_id {
|
||||
item_id = serde_json::to_string::<UserId>(&user_id.into())?;
|
||||
item_type = ItemType::User;
|
||||
}
|
||||
|
||||
reports.push(Report {
|
||||
id: x.id.into(),
|
||||
report_type: x.report_type,
|
||||
item_id,
|
||||
item_type,
|
||||
reporter: x.reporter.into(),
|
||||
body: x.body,
|
||||
created: x.created,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(reports))
|
||||
}
|
||||
81
src/routes/v1/tags.rs
Normal file
81
src/routes/v1/tags.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::auth::check_is_admin_from_headers;
|
||||
use crate::database::models::categories::{Category, Loader, ProjectType};
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::{get, put, web};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
|
||||
const DEFAULT_ICON: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>"#;
|
||||
|
||||
#[get("category")]
|
||||
pub async fn category_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = Category::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| &*x.project_type == "mod")
|
||||
.map(|x| x.project_type)
|
||||
.collect::<Vec<String>>();
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[put("category/{name}")]
|
||||
pub async fn category_create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
category: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = category.into_inner().0;
|
||||
|
||||
let project_type = crate::database::models::ProjectTypeId::get_id("mod".to_string(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError("Specified project type does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let _id = Category::builder()
|
||||
.name(&name)?
|
||||
.icon(DEFAULT_ICON)?
|
||||
.project_type(&project_type)?
|
||||
.insert(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[get("loader")]
|
||||
pub async fn loader_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = Loader::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| x.supported_project_types.contains(&"mod".to_string()))
|
||||
.map(|x| x.loader)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[put("loader/{name}")]
|
||||
pub async fn loader_create(
|
||||
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?;
|
||||
|
||||
let project_types =
|
||||
ProjectType::get_many_id(&vec!["mod".to_string()], &mut *transaction).await?;
|
||||
|
||||
let _id = Loader::builder()
|
||||
.name(&name)?
|
||||
.icon(DEFAULT_ICON)?
|
||||
.supported_project_types(&*project_types.into_iter().map(|x| x.id).collect::<Vec<_>>())?
|
||||
.insert(&mut transaction)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
44
src/routes/v1/users.rs
Normal file
44
src/routes/v1/users.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::models::User;
|
||||
use crate::models::ids::UserId;
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::web;
|
||||
use actix_web::{get, HttpRequest, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[get("{user_id}/mods")]
|
||||
pub async fn mods_list(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let id_option =
|
||||
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
||||
.await?;
|
||||
|
||||
if let Some(id) = id_option {
|
||||
let user_id: UserId = id.into();
|
||||
|
||||
let project_data = if let Some(current_user) = user {
|
||||
if current_user.role.is_mod() || current_user.id == user_id {
|
||||
User::get_projects_private(id, &**pool).await?
|
||||
} else {
|
||||
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await?
|
||||
}
|
||||
} else {
|
||||
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await?
|
||||
};
|
||||
|
||||
let response = project_data
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.collect::<Vec<crate::models::ids::ProjectId>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
416
src/routes/v1/versions.rs
Normal file
416
src/routes/v1/versions.rs
Normal file
@@ -0,0 +1,416 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::ids::{ProjectId, UserId, VersionId};
|
||||
use crate::models::projects::{Dependency, GameVersion, Loader, Version, VersionFile, VersionType};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::routes::versions::{convert_version, VersionIds, VersionListFilters};
|
||||
use crate::routes::ApiError;
|
||||
use crate::{database, models, Pepper};
|
||||
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::borrow::Borrow;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A specific version of a mod
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LegacyVersion {
|
||||
pub id: VersionId,
|
||||
pub mod_id: ProjectId,
|
||||
pub author_id: UserId,
|
||||
pub featured: bool,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub changelog: String,
|
||||
pub changelog_url: Option<String>,
|
||||
pub date_published: DateTime<Utc>,
|
||||
pub downloads: u32,
|
||||
pub version_type: VersionType,
|
||||
pub files: Vec<VersionFile>,
|
||||
pub dependencies: Vec<Dependency>,
|
||||
pub game_versions: Vec<GameVersion>,
|
||||
pub loaders: Vec<Loader>,
|
||||
}
|
||||
|
||||
fn convert_to_legacy(version: Version) -> LegacyVersion {
|
||||
LegacyVersion {
|
||||
id: version.id,
|
||||
mod_id: version.project_id,
|
||||
author_id: version.author_id,
|
||||
featured: version.featured,
|
||||
name: version.name,
|
||||
version_number: version.version_number,
|
||||
changelog: version.changelog,
|
||||
changelog_url: version.changelog_url,
|
||||
date_published: version.date_published,
|
||||
downloads: version.downloads,
|
||||
version_type: version.version_type,
|
||||
files: version.files,
|
||||
dependencies: version.dependencies,
|
||||
game_versions: version.game_versions,
|
||||
loaders: version.loaders,
|
||||
}
|
||||
}
|
||||
|
||||
#[get("version")]
|
||||
pub async fn version_list(
|
||||
info: web::Path<(String,)>,
|
||||
web::Query(filters): web::Query<VersionListFilters>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let result = database::models::Project::get_from_slug_or_project_id(string, &**pool).await?;
|
||||
|
||||
if let Some(project) = result {
|
||||
let id = project.id;
|
||||
|
||||
let version_ids = database::models::Version::get_project_versions(
|
||||
id,
|
||||
filters
|
||||
.game_versions
|
||||
.as_ref()
|
||||
.map(|x| serde_json::from_str(x).unwrap_or_default()),
|
||||
filters
|
||||
.loaders
|
||||
.as_ref()
|
||||
.map(|x| serde_json::from_str(x).unwrap_or_default()),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut versions = database::models::Version::get_many_full(version_ids, &**pool).await?;
|
||||
|
||||
let mut response = versions
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|version| {
|
||||
filters
|
||||
.featured
|
||||
.map(|featured| featured == version.featured)
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.map(convert_version)
|
||||
.map(convert_to_legacy)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));
|
||||
|
||||
// Attempt to populate versions with "auto featured" versions
|
||||
if response.is_empty() && !versions.is_empty() && filters.featured.unwrap_or(false) {
|
||||
let loaders = database::models::categories::Loader::list(&**pool).await?;
|
||||
let game_versions =
|
||||
database::models::categories::GameVersion::list_filter(None, Some(true), &**pool)
|
||||
.await?;
|
||||
|
||||
let mut joined_filters = Vec::new();
|
||||
for game_version in &game_versions {
|
||||
for loader in &loaders {
|
||||
joined_filters.push((game_version, loader))
|
||||
}
|
||||
}
|
||||
|
||||
joined_filters.into_iter().for_each(|filter| {
|
||||
versions
|
||||
.iter()
|
||||
.find(|version| {
|
||||
version.game_versions.contains(&filter.0.version)
|
||||
&& version.loaders.contains(&filter.1.loader)
|
||||
})
|
||||
.map(|version| {
|
||||
response.push(convert_to_legacy(convert_version(version.clone())))
|
||||
})
|
||||
.unwrap_or(());
|
||||
});
|
||||
|
||||
if response.is_empty() {
|
||||
versions
|
||||
.into_iter()
|
||||
.for_each(|version| response.push(convert_to_legacy(convert_version(version))));
|
||||
}
|
||||
}
|
||||
|
||||
response.sort_by(|a, b| b.date_published.cmp(&a.date_published));
|
||||
response.dedup_by(|a, b| a.id == b.id);
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("versions")]
|
||||
pub async fn versions_get(
|
||||
ids: web::Query<VersionIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let version_ids = serde_json::from_str::<Vec<models::ids::VersionId>>(&*ids.ids)?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
let versions_data = database::models::Version::get_many_full(version_ids, &**pool).await?;
|
||||
|
||||
let mut versions = Vec::new();
|
||||
|
||||
for version_data in versions_data {
|
||||
versions.push(convert_to_legacy(convert_version(version_data)));
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(versions))
|
||||
}
|
||||
|
||||
#[get("{version_id}")]
|
||||
pub async fn version_get(
|
||||
info: web::Path<(models::ids::VersionId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
|
||||
|
||||
if let Some(data) = version_data {
|
||||
Ok(HttpResponse::Ok().json(convert_to_legacy(convert_version(data))))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Algorithm {
|
||||
#[serde(default = "default_algorithm")]
|
||||
algorithm: String,
|
||||
}
|
||||
|
||||
fn default_algorithm() -> String {
|
||||
"sha1".into()
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}
|
||||
#[get("{version_id}")]
|
||||
pub async fn get_version_from_hash(
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hash = info.into_inner().0.to_lowercase();
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.version_id version_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await?;
|
||||
|
||||
if let Some(id) = result {
|
||||
let version_data = database::models::Version::get_full(
|
||||
database::models::VersionId(id.version_id),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(data) = version_data {
|
||||
Ok(HttpResponse::Ok().json(super::versions::convert_version(data)))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DownloadRedirect {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}/download
|
||||
#[allow(clippy::await_holding_refcell_ref)]
|
||||
#[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 hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
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.borrow().remote_addr();
|
||||
|
||||
if let Some(ip) = ip_option {
|
||||
let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest();
|
||||
|
||||
let download_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)",
|
||||
id.version_id,
|
||||
hash,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists.unwrap_or(false);
|
||||
|
||||
if !download_exists {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO downloads (
|
||||
version_id, identifier
|
||||
)
|
||||
VALUES (
|
||||
$1, $2
|
||||
)
|
||||
",
|
||||
id.version_id,
|
||||
hash
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
id.version_id,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
id.mod_id,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.header("Location", &*id.url)
|
||||
.json(DownloadRedirect { url: id.url }))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}
|
||||
#[delete("{version_id}")]
|
||||
pub async fn delete_file(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let hash = info.into_inner().0.to_lowercase();
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id project_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
?;
|
||||
|
||||
if let Some(row) = result {
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id_version(
|
||||
database::models::ids::VersionId(row.version_id),
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::DatabaseError)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to delete this file!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !team_member
|
||||
.permissions
|
||||
.contains(Permissions::DELETE_VERSION)
|
||||
{
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to delete this file!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM hashes
|
||||
WHERE file_id = $1
|
||||
",
|
||||
row.id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM files
|
||||
WHERE files.id = $1
|
||||
",
|
||||
row.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let project_id: models::projects::ProjectId =
|
||||
database::models::ids::ProjectId(row.project_id).into();
|
||||
file_host
|
||||
.delete_file_version(
|
||||
"",
|
||||
&format!(
|
||||
"data/{}/versions/{}/{}",
|
||||
project_id, row.version_number, row.filename
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,43 @@ use crate::database::models;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::mods::{
|
||||
Dependency, GameVersion, ModId, ModLoader, Version, VersionFile, VersionId, VersionType,
|
||||
use crate::models::projects::{
|
||||
Dependency, GameVersion, Loader, ProjectId, Version, VersionFile, VersionId, VersionType,
|
||||
};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::routes::mod_creation::{CreateError, UploadedFile};
|
||||
use crate::routes::project_creation::{CreateError, UploadedFile};
|
||||
use crate::validate::{validate_file, ValidationResult};
|
||||
use actix_multipart::{Field, Multipart};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{post, HttpRequest, HttpResponse};
|
||||
use futures::stream::StreamExt;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
lazy_static! {
|
||||
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_\-.]*$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||
pub struct InitialVersionData {
|
||||
pub mod_id: Option<ModId>,
|
||||
#[serde(alias = "mod_id")]
|
||||
pub project_id: Option<ProjectId>,
|
||||
#[validate(length(min = 1, max = 256))]
|
||||
pub file_parts: Vec<String>,
|
||||
#[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")]
|
||||
pub version_number: String,
|
||||
#[validate(length(min = 3, max = 256))]
|
||||
pub version_title: String,
|
||||
#[validate(length(max = 65536))]
|
||||
pub version_body: Option<String>,
|
||||
#[validate(length(min = 0, max = 256))]
|
||||
pub dependencies: Vec<Dependency>,
|
||||
pub game_versions: Vec<GameVersion>,
|
||||
pub release_channel: VersionType,
|
||||
pub loaders: Vec<ModLoader>,
|
||||
pub loaders: Vec<Loader>,
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
@@ -34,42 +48,6 @@ struct InitialFileData {
|
||||
// TODO: hashes?
|
||||
}
|
||||
|
||||
pub fn check_version(version: &InitialVersionData) -> Result<(), CreateError> {
|
||||
/*
|
||||
# InitialVersionData
|
||||
file_parts: Vec<String>, 1..=256
|
||||
version_number: 1..=64,
|
||||
version_title: 3..=256,
|
||||
version_body: max of 64KiB,
|
||||
game_versions: Vec<GameVersion>, 1..=256
|
||||
release_channel: VersionType,
|
||||
loaders: Vec<ModLoader>, 1..=256
|
||||
*/
|
||||
use super::mod_creation::check_length;
|
||||
|
||||
version
|
||||
.file_parts
|
||||
.iter()
|
||||
.try_for_each(|f| check_length(1..=256, "file part name", f))?;
|
||||
|
||||
check_length(1..=64, "version number", &version.version_number)?;
|
||||
check_length(3..=256, "version title", &version.version_title)?;
|
||||
if let Some(body) = &version.version_body {
|
||||
check_length(..65536, "version body", body)?;
|
||||
}
|
||||
|
||||
version
|
||||
.game_versions
|
||||
.iter()
|
||||
.try_for_each(|v| check_length(1..=256, "game version", &v.0))?;
|
||||
version
|
||||
.loaders
|
||||
.iter()
|
||||
.try_for_each(|l| check_length(1..=256, "loader name", &l.0))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// under `/api/v1/version`
|
||||
#[post("version")]
|
||||
pub async fn version_create(
|
||||
@@ -91,7 +69,8 @@ pub async fn version_create(
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let undo_result = super::mod_creation::undo_uploads(&***file_host, &uploaded_files).await;
|
||||
let undo_result =
|
||||
super::project_creation::undo_uploads(&***file_host, &uploaded_files).await;
|
||||
let rollback_result = transaction.rollback().await;
|
||||
|
||||
if let Err(e) = undo_result {
|
||||
@@ -119,6 +98,8 @@ async fn version_create_inner(
|
||||
let mut initial_version_data = None;
|
||||
let mut version_builder = None;
|
||||
|
||||
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
|
||||
let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
|
||||
while let Some(item) = payload.next().await {
|
||||
@@ -139,34 +120,36 @@ async fn version_create_inner(
|
||||
let version_create_data: InitialVersionData = serde_json::from_slice(&data)?;
|
||||
initial_version_data = Some(version_create_data);
|
||||
let version_create_data = initial_version_data.as_ref().unwrap();
|
||||
if version_create_data.mod_id.is_none() {
|
||||
return Err(CreateError::MissingValueError("Missing mod id".to_string()));
|
||||
if version_create_data.project_id.is_none() {
|
||||
return Err(CreateError::MissingValueError(
|
||||
"Missing project id".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
check_version(version_create_data)?;
|
||||
version_create_data.validate()?;
|
||||
|
||||
let mod_id: models::ModId = version_create_data.mod_id.unwrap().into();
|
||||
let project_id: models::ProjectId = version_create_data.project_id.unwrap().into();
|
||||
|
||||
// Ensure that the mod this version is being added to exists
|
||||
// Ensure that the project this version is being added to exists
|
||||
let results = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
||||
mod_id as models::ModId
|
||||
project_id as models::ProjectId
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if !results.exists.unwrap_or(false) {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"An invalid mod id was supplied".to_string(),
|
||||
"An invalid project id was supplied".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check whether there is already a version of this mod with the
|
||||
// Check whether there is already a version of this project with the
|
||||
// same version number
|
||||
let results = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM versions WHERE (version_number=$1) AND (mod_id=$2))",
|
||||
version_create_data.version_number,
|
||||
mod_id as models::ModId,
|
||||
project_id as models::ProjectId,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
@@ -178,15 +161,18 @@ async fn version_create_inner(
|
||||
}
|
||||
|
||||
// Check that the user creating this version is a team member
|
||||
// of the mod the version is being added to.
|
||||
let team_member =
|
||||
models::TeamMember::get_from_user_id_mod(mod_id, user.id.into(), &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::CustomAuthenticationError(
|
||||
"You don't have permission to upload this version!".to_string(),
|
||||
)
|
||||
})?;
|
||||
// of the project the version is being added to.
|
||||
let team_member = models::TeamMember::get_from_user_id_project(
|
||||
project_id,
|
||||
user.id.into(),
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::CustomAuthenticationError(
|
||||
"You don't have permission to upload this version!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !team_member
|
||||
.permissions
|
||||
@@ -206,13 +192,17 @@ async fn version_create_inner(
|
||||
.await?
|
||||
.expect("Release channel not found in database");
|
||||
|
||||
let mut game_versions = Vec::with_capacity(version_create_data.game_versions.len());
|
||||
for v in &version_create_data.game_versions {
|
||||
let id = models::categories::GameVersion::get_id(&v.0, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| CreateError::InvalidGameVersion(v.0.clone()))?;
|
||||
game_versions.push(id);
|
||||
}
|
||||
let game_versions = version_create_data
|
||||
.game_versions
|
||||
.iter()
|
||||
.map(|x| {
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.ok_or_else(|| CreateError::InvalidGameVersion(x.0.clone()))
|
||||
.map(|y| y.id)
|
||||
})
|
||||
.collect::<Result<Vec<models::GameVersionId>, CreateError>>()?;
|
||||
|
||||
let mut loaders = Vec::with_capacity(version_create_data.loaders.len());
|
||||
for l in &version_create_data.loaders {
|
||||
@@ -230,7 +220,7 @@ async fn version_create_inner(
|
||||
|
||||
version_builder = Some(VersionBuilder {
|
||||
version_id: version_id.into(),
|
||||
mod_id: version_create_data.mod_id.unwrap().into(),
|
||||
project_id,
|
||||
author_id: user.id.into(),
|
||||
name: version_create_data.version_title.clone(),
|
||||
version_number: version_create_data.version_number.clone(),
|
||||
@@ -253,19 +243,38 @@ async fn version_create_inner(
|
||||
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
|
||||
})?;
|
||||
|
||||
let file_builder = upload_file(
|
||||
let project_type = sqlx::query!(
|
||||
"
|
||||
SELECT name FROM project_types pt
|
||||
INNER JOIN mods ON mods.project_type = pt.id
|
||||
WHERE mods.id = $1
|
||||
",
|
||||
version.project_id as models::ProjectId,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?
|
||||
.name;
|
||||
|
||||
let version_data = initial_version_data
|
||||
.clone()
|
||||
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
|
||||
|
||||
upload_file(
|
||||
&mut field,
|
||||
file_host,
|
||||
uploaded_files,
|
||||
&mut version.files,
|
||||
&cdn_url,
|
||||
&content_disposition,
|
||||
version.mod_id.into(),
|
||||
version.project_id.into(),
|
||||
&version.version_number,
|
||||
&*project_type,
|
||||
version_data.loaders,
|
||||
version_data.game_versions,
|
||||
&all_game_versions,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add the newly uploaded file to the existing or new version
|
||||
version.files.push(file_builder);
|
||||
}
|
||||
|
||||
let version_data = initial_version_data
|
||||
@@ -278,7 +287,7 @@ async fn version_create_inner(
|
||||
SELECT m.title FROM mods m
|
||||
WHERE id = $1
|
||||
",
|
||||
builder.mod_id as crate::database::models::ids::ModId
|
||||
builder.project_id as crate::database::models::ids::ProjectId
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
@@ -290,7 +299,7 @@ async fn version_create_inner(
|
||||
SELECT follower_id FROM mod_follows
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
builder.mod_id as crate::database::models::ids::ModId
|
||||
builder.project_id as crate::database::models::ids::ProjectId
|
||||
)
|
||||
.fetch_many(&mut *transaction)
|
||||
.try_filter_map(|e| async {
|
||||
@@ -300,17 +309,17 @@ async fn version_create_inner(
|
||||
.try_collect::<Vec<crate::database::models::ids::UserId>>()
|
||||
.await?;
|
||||
|
||||
let mod_id: ModId = builder.mod_id.into();
|
||||
let project_id: ProjectId = builder.project_id.into();
|
||||
let version_id: VersionId = builder.version_id.into();
|
||||
|
||||
NotificationBuilder {
|
||||
title: "A mod you followed has been updated!".to_string(),
|
||||
title: "A project you followed has been updated!".to_string(),
|
||||
text: format!(
|
||||
"Mod {} has been updated to version {}",
|
||||
"Project {} has been updated to version {}",
|
||||
result.title,
|
||||
version_data.version_number.clone()
|
||||
),
|
||||
link: format!("mod/{}/version/{}", mod_id, version_id),
|
||||
link: format!("project/{}/version/{}", project_id, version_id),
|
||||
actions: vec![],
|
||||
}
|
||||
.insert_many(users, &mut *transaction)
|
||||
@@ -318,7 +327,7 @@ async fn version_create_inner(
|
||||
|
||||
let response = Version {
|
||||
id: builder.version_id.into(),
|
||||
mod_id: builder.mod_id.into(),
|
||||
project_id: builder.project_id.into(),
|
||||
author_id: user.id,
|
||||
featured: builder.featured,
|
||||
name: builder.name.clone(),
|
||||
@@ -388,7 +397,8 @@ pub async fn upload_file_to_version(
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let undo_result = super::mod_creation::undo_uploads(&***file_host, &uploaded_files).await;
|
||||
let undo_result =
|
||||
super::project_creation::undo_uploads(&***file_host, &uploaded_files).await;
|
||||
let rollback_result = transaction.rollback().await;
|
||||
|
||||
if let Err(e) = undo_result {
|
||||
@@ -419,16 +429,7 @@ async fn upload_file_to_version_inner(
|
||||
|
||||
let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT mod_id, version_number, author_id
|
||||
FROM versions
|
||||
WHERE id = $1
|
||||
",
|
||||
version_id as models::VersionId,
|
||||
)
|
||||
.fetch_optional(&mut *transaction)
|
||||
.await?;
|
||||
let result = models::Version::get_full(version_id, &mut *transaction).await?;
|
||||
|
||||
let version = match result {
|
||||
Some(v) => v,
|
||||
@@ -457,9 +458,23 @@ async fn upload_file_to_version_inner(
|
||||
));
|
||||
}
|
||||
|
||||
let mod_id = ModId(version.mod_id as u64);
|
||||
let project_id = ProjectId(version.project_id.0 as u64);
|
||||
let version_number = version.version_number;
|
||||
|
||||
let project_type = sqlx::query!(
|
||||
"
|
||||
SELECT name FROM project_types pt
|
||||
INNER JOIN mods ON mods.project_type = pt.id
|
||||
WHERE mods.id = $1
|
||||
",
|
||||
version.project_id as models::ProjectId,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?
|
||||
.name;
|
||||
|
||||
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
|
||||
|
||||
while let Some(item) = payload.next().await {
|
||||
let mut field: Field = item.map_err(CreateError::MultipartError)?;
|
||||
let content_disposition = field.content_disposition().ok_or_else(|| {
|
||||
@@ -485,19 +500,27 @@ async fn upload_file_to_version_inner(
|
||||
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
|
||||
})?;
|
||||
|
||||
let file_builder = upload_file(
|
||||
upload_file(
|
||||
&mut field,
|
||||
file_host,
|
||||
uploaded_files,
|
||||
&mut file_builders,
|
||||
&cdn_url,
|
||||
&content_disposition,
|
||||
mod_id,
|
||||
project_id,
|
||||
&version_number,
|
||||
&*project_type,
|
||||
version.loaders.clone().into_iter().map(Loader).collect(),
|
||||
version
|
||||
.game_versions
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(GameVersion)
|
||||
.collect(),
|
||||
&all_game_versions,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO: Malware scan + file validation
|
||||
file_builders.push(file_builder);
|
||||
}
|
||||
|
||||
if file_builders.is_empty() {
|
||||
@@ -514,19 +537,26 @@ async fn upload_file_to_version_inner(
|
||||
}
|
||||
|
||||
// This function is used for adding a file to a version, uploading the initial
|
||||
// files for a version, and for uploading the initial version files for a mod
|
||||
// files for a version, and for uploading the initial version files for a project
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn upload_file(
|
||||
field: &mut Field,
|
||||
file_host: &dyn FileHost,
|
||||
uploaded_files: &mut Vec<UploadedFile>,
|
||||
version_files: &mut Vec<models::version_item::VersionFileBuilder>,
|
||||
cdn_url: &str,
|
||||
content_disposition: &actix_web::http::header::ContentDisposition,
|
||||
mod_id: crate::models::ids::ModId,
|
||||
project_id: crate::models::ids::ProjectId,
|
||||
version_number: &str,
|
||||
) -> Result<models::version_item::VersionFileBuilder, CreateError> {
|
||||
project_type: &str,
|
||||
loaders: Vec<Loader>,
|
||||
game_versions: Vec<GameVersion>,
|
||||
all_game_versions: &[models::categories::GameVersion],
|
||||
ignore_primary: bool,
|
||||
) -> Result<(), CreateError> {
|
||||
let (file_name, file_extension) = get_name_ext(content_disposition)?;
|
||||
|
||||
let content_type = mod_file_type(file_extension)
|
||||
let content_type = project_file_type(file_extension)
|
||||
.ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?;
|
||||
|
||||
let mut data = Vec::new();
|
||||
@@ -534,20 +564,32 @@ pub async fn upload_file(
|
||||
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
|
||||
}
|
||||
|
||||
// Mod file size limit of 25MiB
|
||||
// Project file size limit of 25MiB
|
||||
const FILE_SIZE_CAP: usize = 25 * (2 << 30);
|
||||
|
||||
// TODO: override file size cap for authorized users or mods
|
||||
// TODO: override file size cap for authorized users or projects
|
||||
if data.len() >= FILE_SIZE_CAP {
|
||||
return Err(CreateError::InvalidInput(
|
||||
String::from("Mod file exceeds the maximum of 25MiB. Contact a moderator or admin to request permission to upload larger files.")
|
||||
String::from("Project file exceeds the maximum of 25MiB. Contact a moderator or admin to request permission to upload larger files.")
|
||||
));
|
||||
}
|
||||
|
||||
let validation_result = validate_file(
|
||||
data.as_slice(),
|
||||
file_extension,
|
||||
project_type,
|
||||
loaders,
|
||||
game_versions,
|
||||
all_game_versions,
|
||||
)?;
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/versions/{}/{}", mod_id, version_number, file_name),
|
||||
&format!(
|
||||
"data/{}/versions/{}/{}",
|
||||
project_id, version_number, file_name
|
||||
),
|
||||
data.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
@@ -558,7 +600,7 @@ pub async fn upload_file(
|
||||
});
|
||||
|
||||
// TODO: Malware scan + file validation
|
||||
Ok(models::version_item::VersionFileBuilder {
|
||||
version_files.push(models::version_item::VersionFileBuilder {
|
||||
filename: file_name.to_string(),
|
||||
url: format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
hashes: vec![
|
||||
@@ -575,12 +617,16 @@ pub async fn upload_file(
|
||||
hash: upload_data.content_sha512.into_bytes(),
|
||||
},
|
||||
],
|
||||
primary: uploaded_files.len() == 1,
|
||||
})
|
||||
primary: validation_result == ValidationResult::Pass
|
||||
&& version_files.iter().all(|x| !x.primary)
|
||||
&& !ignore_primary,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Currently we only support jar mods; this may change in the future (datapacks?)
|
||||
fn mod_file_type(ext: &str) -> Option<&str> {
|
||||
// Currently we only support jar projects; this may change in the future (datapacks?)
|
||||
fn project_file_type(ext: &str) -> Option<&str> {
|
||||
match ext {
|
||||
"jar" => Some("application/java-archive"),
|
||||
_ => None,
|
||||
|
||||
519
src/routes/version_file.rs
Normal file
519
src/routes/version_file.rs
Normal file
@@ -0,0 +1,519 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models;
|
||||
use crate::models::projects::{GameVersion, Loader};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::{database, Pepper};
|
||||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Algorithm {
|
||||
#[serde(default = "default_algorithm")]
|
||||
algorithm: String,
|
||||
}
|
||||
|
||||
fn default_algorithm() -> String {
|
||||
"sha1".into()
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}
|
||||
#[get("{version_id}")]
|
||||
pub async fn get_version_from_hash(
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hash = info.into_inner().0.to_lowercase();
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.version_id version_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await?;
|
||||
|
||||
if let Some(id) = result {
|
||||
let version_data = database::models::Version::get_full(
|
||||
database::models::VersionId(id.version_id),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(data) = version_data {
|
||||
Ok(HttpResponse::Ok().json(super::versions::convert_version(data)))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[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.to_lowercase();
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.url url, f.id id, f.version_id version_id, v.mod_id project_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if let Some(id) = result {
|
||||
download_version_inner(
|
||||
database::models::VersionId(id.version_id),
|
||||
database::models::ProjectId(id.project_id),
|
||||
&req,
|
||||
&mut transaction,
|
||||
&pepper,
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.header("Location", &*id.url)
|
||||
.json(DownloadRedirect { url: id.url }))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_version_inner(
|
||||
version_id: database::models::VersionId,
|
||||
project_id: database::models::ProjectId,
|
||||
req: &HttpRequest,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
pepper: &web::Data<Pepper>,
|
||||
) -> Result<(), ApiError> {
|
||||
let real_ip = req.connection_info();
|
||||
let ip_option = real_ip.borrow().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)",
|
||||
version_id as database::models::VersionId,
|
||||
hash,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await
|
||||
?
|
||||
.exists.unwrap_or(false);
|
||||
|
||||
if !download_exists {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO downloads (
|
||||
version_id, identifier
|
||||
)
|
||||
VALUES (
|
||||
$1, $2
|
||||
)
|
||||
",
|
||||
version_id as database::models::VersionId,
|
||||
hash
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
version_id as database::models::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
project_id as database::models::ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}
|
||||
#[delete("{version_id}")]
|
||||
pub async fn delete_file(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let hash = info.into_inner().0.to_lowercase();
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id project_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
?;
|
||||
|
||||
if let Some(row) = result {
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id_version(
|
||||
database::models::ids::VersionId(row.version_id),
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::DatabaseError)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to delete this file!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !team_member
|
||||
.permissions
|
||||
.contains(Permissions::DELETE_VERSION)
|
||||
{
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to delete this file!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM hashes
|
||||
WHERE file_id = $1
|
||||
",
|
||||
row.id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM files
|
||||
WHERE files.id = $1
|
||||
",
|
||||
row.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let project_id: models::projects::ProjectId =
|
||||
database::models::ids::ProjectId(row.project_id).into();
|
||||
file_host
|
||||
.delete_file_version(
|
||||
"",
|
||||
&format!(
|
||||
"data/{}/versions/{}/{}",
|
||||
project_id, row.version_number, row.filename
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateData {
|
||||
pub hash: (String, String),
|
||||
pub loaders: Vec<Loader>,
|
||||
pub game_versions: Vec<GameVersion>,
|
||||
}
|
||||
|
||||
#[post("{version_id}/update")]
|
||||
pub async fn get_update_from_hash(
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
update_data: web::Json<UpdateData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hash = info.into_inner().0.to_lowercase();
|
||||
|
||||
// get version_id from hash
|
||||
// get mod_id from hash
|
||||
// get latest version satisfying conditions - if not found
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT v.mod_id project_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await?;
|
||||
|
||||
if let Some(id) = result {
|
||||
let version_ids = database::models::Version::get_project_versions(
|
||||
database::models::ProjectId(id.project_id),
|
||||
Some(
|
||||
update_data
|
||||
.game_versions
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|x| x.0)
|
||||
.collect(),
|
||||
),
|
||||
Some(
|
||||
update_data
|
||||
.loaders
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|x| x.0)
|
||||
.collect(),
|
||||
),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(version_id) = version_ids.last() {
|
||||
let version_data = database::models::Version::get_full(*version_id, &**pool).await?;
|
||||
|
||||
if let Some(data) = version_data {
|
||||
Ok(HttpResponse::Ok().json(super::versions::convert_version(data)))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
// Requests above with multiple versions below
|
||||
#[derive(Deserialize)]
|
||||
pub struct FileHashes {
|
||||
pub algorithm: String,
|
||||
pub hashes: Vec<String>,
|
||||
}
|
||||
|
||||
// under /api/v2/version_files
|
||||
#[post("/")]
|
||||
pub async fn get_versions_from_hashes(
|
||||
pool: web::Data<PgPool>,
|
||||
file_data: web::Json<FileHashes>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hashes_parsed: Vec<Vec<u8>> = file_data
|
||||
.hashes
|
||||
.iter()
|
||||
.map(|x| x.as_bytes().to_vec())
|
||||
.collect();
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT h.hash hash, h.algorithm algorithm, f.version_id version_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
WHERE h.algorithm = $2 AND h.hash IN (SELECT * FROM UNNEST($1::bytea[]))
|
||||
",
|
||||
hashes_parsed.as_slice(),
|
||||
file_data.algorithm
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
|
||||
let versions_data = database::models::Version::get_many_full(
|
||||
result
|
||||
.iter()
|
||||
.map(|x| database::models::VersionId(x.version_id))
|
||||
.collect(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut response = HashMap::new();
|
||||
|
||||
for row in result {
|
||||
if let Some(version) = versions_data.iter().find(|x| x.id.0 == row.version_id) {
|
||||
response.insert(row.hash, super::versions::convert_version(version.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
}
|
||||
|
||||
#[post("download")]
|
||||
pub async fn download_files(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
file_data: web::Json<FileHashes>,
|
||||
pepper: web::Data<Pepper>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hashes_parsed: Vec<Vec<u8>> = file_data
|
||||
.hashes
|
||||
.iter()
|
||||
.map(|x| x.as_bytes().to_vec())
|
||||
.collect();
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash IN (SELECT * FROM UNNEST($1::bytea[]))
|
||||
",
|
||||
hashes_parsed.as_slice(),
|
||||
file_data.algorithm
|
||||
)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let mut response = HashMap::new();
|
||||
|
||||
for row in result {
|
||||
download_version_inner(
|
||||
database::models::VersionId(row.version_id),
|
||||
database::models::ProjectId(row.project_id),
|
||||
&req,
|
||||
&mut transaction,
|
||||
&pepper,
|
||||
)
|
||||
.await?;
|
||||
response.insert(row.hash, row.url);
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ManyUpdateData {
|
||||
pub algorithm: String,
|
||||
pub hashes: Vec<String>,
|
||||
pub loaders: Vec<Loader>,
|
||||
pub game_versions: Vec<GameVersion>,
|
||||
}
|
||||
|
||||
#[post("update")]
|
||||
pub async fn update_files(
|
||||
pool: web::Data<PgPool>,
|
||||
update_data: web::Json<ManyUpdateData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hashes_parsed: Vec<Vec<u8>> = update_data
|
||||
.hashes
|
||||
.iter()
|
||||
.map(|x| x.as_bytes().to_vec())
|
||||
.collect();
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash IN (SELECT * FROM UNNEST($1::bytea[]))
|
||||
",
|
||||
hashes_parsed.as_slice(),
|
||||
update_data.algorithm
|
||||
)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let mut version_ids = Vec::new();
|
||||
|
||||
for row in &result {
|
||||
let updated_versions = database::models::Version::get_project_versions(
|
||||
database::models::ProjectId(row.project_id),
|
||||
Some(
|
||||
update_data
|
||||
.game_versions
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|x| x.0.clone())
|
||||
.collect(),
|
||||
),
|
||||
Some(
|
||||
update_data
|
||||
.loaders
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|x| x.0.clone())
|
||||
.collect(),
|
||||
),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(latest_version) = updated_versions.last() {
|
||||
version_ids.push(*latest_version);
|
||||
}
|
||||
}
|
||||
|
||||
let versions = database::models::Version::get_many_full(version_ids, &**pool).await?;
|
||||
|
||||
let mut response = HashMap::new();
|
||||
|
||||
for row in &result {
|
||||
if let Some(version) = versions.iter().find(|x| x.id.0 == row.version_id) {
|
||||
response.insert(
|
||||
row.hash.clone(),
|
||||
super::versions::convert_version(version.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::database;
|
||||
use crate::models;
|
||||
use crate::models::mods::{Dependency, DependencyType};
|
||||
use crate::models::projects::{Dependency, DependencyType};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::{database, Pepper};
|
||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::borrow::Borrow;
|
||||
use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct VersionListFilters {
|
||||
@@ -20,23 +20,18 @@ pub struct VersionListFilters {
|
||||
|
||||
#[get("version")]
|
||||
pub async fn version_list(
|
||||
info: web::Path<(models::ids::ModId,)>,
|
||||
info: web::Path<(String,)>,
|
||||
web::Query(filters): web::Query<VersionListFilters>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0.into();
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let mod_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)",
|
||||
id as database::models::ModId,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists;
|
||||
let result = database::models::Project::get_from_slug_or_project_id(string, &**pool).await?;
|
||||
|
||||
if mod_exists.unwrap_or(false) {
|
||||
let version_ids = database::models::Version::get_mod_versions(
|
||||
if let Some(project) = result {
|
||||
let id = project.id;
|
||||
|
||||
let version_ids = database::models::Version::get_project_versions(
|
||||
id,
|
||||
filters
|
||||
.game_versions
|
||||
@@ -48,12 +43,9 @@ pub async fn version_list(
|
||||
.map(|x| serde_json::from_str(x).unwrap_or_default()),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
let mut versions = database::models::Version::get_many_full(version_ids, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let mut versions = database::models::Version::get_many_full(version_ids, &**pool).await?;
|
||||
|
||||
let mut response = versions
|
||||
.iter()
|
||||
@@ -87,8 +79,8 @@ pub async fn version_list(
|
||||
versions
|
||||
.iter()
|
||||
.find(|version| {
|
||||
version.game_versions.contains(&filter.0)
|
||||
&& version.loaders.contains(&filter.1)
|
||||
version.game_versions.contains(&filter.0.version)
|
||||
&& version.loaders.contains(&filter.1.loader)
|
||||
})
|
||||
.map(|version| response.push(convert_version(version.clone())))
|
||||
.unwrap_or(());
|
||||
@@ -124,9 +116,7 @@ pub async fn versions_get(
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
let versions_data = database::models::Version::get_many_full(version_ids, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let versions_data = database::models::Version::get_many_full(version_ids, &**pool).await?;
|
||||
|
||||
let mut versions = Vec::new();
|
||||
|
||||
@@ -143,9 +133,7 @@ pub async fn version_get(
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let version_data = database::models::Version::get_full(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
|
||||
|
||||
if let Some(data) = version_data {
|
||||
Ok(HttpResponse::Ok().json(convert_version(data)))
|
||||
@@ -154,12 +142,14 @@ pub async fn version_get(
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_version(data: database::models::version_item::QueryVersion) -> models::mods::Version {
|
||||
use models::mods::VersionType;
|
||||
pub fn convert_version(
|
||||
data: database::models::version_item::QueryVersion,
|
||||
) -> models::projects::Version {
|
||||
use models::projects::VersionType;
|
||||
|
||||
models::mods::Version {
|
||||
models::projects::Version {
|
||||
id: data.id.into(),
|
||||
mod_id: data.mod_id.into(),
|
||||
project_id: data.project_id.into(),
|
||||
author_id: data.author_id.into(),
|
||||
|
||||
featured: data.featured,
|
||||
@@ -180,7 +170,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
models::mods::VersionFile {
|
||||
models::projects::VersionFile {
|
||||
url: f.url,
|
||||
filename: f.filename,
|
||||
// FIXME: Hashes are currently stored as an ascii byte slice instead
|
||||
@@ -206,25 +196,33 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
||||
game_versions: data
|
||||
.game_versions
|
||||
.into_iter()
|
||||
.map(models::mods::GameVersion)
|
||||
.map(models::projects::GameVersion)
|
||||
.collect(),
|
||||
loaders: data
|
||||
.loaders
|
||||
.into_iter()
|
||||
.map(models::mods::ModLoader)
|
||||
.map(models::projects::Loader)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
lazy_static! {
|
||||
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate)]
|
||||
pub struct EditVersion {
|
||||
#[validate(length(min = 3, max = 256))]
|
||||
pub name: Option<String>,
|
||||
#[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")]
|
||||
pub version_number: Option<String>,
|
||||
#[validate(length(max = 65536))]
|
||||
pub changelog: Option<String>,
|
||||
pub version_type: Option<models::mods::VersionType>,
|
||||
pub version_type: Option<models::projects::VersionType>,
|
||||
#[validate(length(min = 1, max = 256))]
|
||||
pub dependencies: Option<Vec<Dependency>>,
|
||||
pub game_versions: Option<Vec<models::mods::GameVersion>>,
|
||||
pub loaders: Option<Vec<models::mods::ModLoader>>,
|
||||
pub game_versions: Option<Vec<models::projects::GameVersion>>,
|
||||
pub loaders: Option<Vec<models::projects::Loader>>,
|
||||
pub featured: Option<bool>,
|
||||
pub primary_file: Option<(String, String)>,
|
||||
}
|
||||
@@ -238,12 +236,12 @@ pub async fn version_edit(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
new_version.validate()?;
|
||||
|
||||
let version_id = info.into_inner().0;
|
||||
let id = version_id.into();
|
||||
|
||||
let result = database::models::Version::get_full(id, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let result = database::models::Version::get_full(id, &**pool).await?;
|
||||
|
||||
if let Some(version_item) = result {
|
||||
let team_member = database::models::TeamMember::get_from_user_id_version(
|
||||
@@ -269,18 +267,9 @@ pub async fn version_edit(
|
||||
));
|
||||
}
|
||||
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
if let Some(name) = &new_version.name {
|
||||
if name.len() > 256 || name.len() < 3 {
|
||||
return Err(ApiError::InvalidInputError(
|
||||
"The version name must be within 3-256 characters!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
@@ -291,17 +280,10 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(number) = &new_version.version_number {
|
||||
if number.len() > 64 || number.is_empty() {
|
||||
return Err(ApiError::InvalidInputError(
|
||||
"The version number must be within 1-64 characters!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
@@ -312,8 +294,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(version_type) = &new_version.version_type {
|
||||
@@ -338,8 +319,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(dependencies) = &new_version.dependencies {
|
||||
@@ -350,8 +330,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
for dependency in dependencies {
|
||||
let dependency_id: database::models::ids::VersionId =
|
||||
@@ -367,8 +346,7 @@ pub async fn version_edit(
|
||||
dependency.dependency_type.as_str()
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,8 +358,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
for game_version in game_versions {
|
||||
let game_version_id = database::models::categories::GameVersion::get_id(
|
||||
@@ -404,8 +381,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,8 +393,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
for loader in loaders {
|
||||
let loader_id =
|
||||
@@ -439,8 +414,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,8 +429,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(primary_file) = &new_version.primary_file {
|
||||
@@ -470,8 +443,7 @@ pub async fn version_edit(
|
||||
primary_file.0
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError(format!(
|
||||
"Specified file with hash {} does not exist.",
|
||||
@@ -488,8 +460,7 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -500,18 +471,10 @@ pub async fn version_edit(
|
||||
result.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(body) = &new_version.changelog {
|
||||
if body.len() > 65536 {
|
||||
return Err(ApiError::InvalidInputError(
|
||||
"The version changelog must be less than 65536 characters long!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
@@ -522,15 +485,11 @@ pub async fn version_edit(
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
transaction.commit().await?;
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have permission to edit this version!".to_string(),
|
||||
@@ -574,261 +533,10 @@ pub async fn version_delete(
|
||||
}
|
||||
}
|
||||
|
||||
let result = database::models::Version::remove_full(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let result = database::models::Version::remove_full(id.into(), &**pool).await?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Algorithm {
|
||||
#[serde(default = "default_algorithm")]
|
||||
algorithm: String,
|
||||
}
|
||||
|
||||
fn default_algorithm() -> String {
|
||||
"sha1".into()
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}
|
||||
#[get("{version_id}")]
|
||||
pub async fn get_version_from_hash(
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hash = info.into_inner().0;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.version_id version_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(id) = result {
|
||||
let version_data = database::models::Version::get_full(
|
||||
database::models::VersionId(id.version_id),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(data) = version_data {
|
||||
Ok(HttpResponse::Ok().json(convert_version(data)))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DownloadRedirect {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}/download
|
||||
#[allow(clippy::await_holding_refcell_ref)]
|
||||
#[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 hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
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.borrow().remote_addr();
|
||||
|
||||
if let Some(ip) = ip_option {
|
||||
let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest();
|
||||
|
||||
let download_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)",
|
||||
id.version_id,
|
||||
hash,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists.unwrap_or(false);
|
||||
|
||||
if !download_exists {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO downloads (
|
||||
version_id, identifier
|
||||
)
|
||||
VALUES (
|
||||
$1, $2
|
||||
)
|
||||
",
|
||||
id.version_id,
|
||||
hash
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
id.version_id,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
id.mod_id,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.header("Location", &*id.url)
|
||||
.json(DownloadRedirect { url: id.url }))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}
|
||||
#[delete("{version_id}")]
|
||||
pub async fn delete_file(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let hash = info.into_inner().0;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id mod_id FROM hashes h
|
||||
INNER JOIN files f ON h.file_id = f.id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
WHERE h.algorithm = $2 AND h.hash = $1
|
||||
",
|
||||
hash.as_bytes(),
|
||||
algorithm.algorithm
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(row) = result {
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id_version(
|
||||
database::models::ids::VersionId(row.version_id),
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::DatabaseError)?
|
||||
.ok_or_else(|| {
|
||||
ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to delete this file!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !team_member
|
||||
.permissions
|
||||
.contains(Permissions::DELETE_VERSION)
|
||||
{
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to delete this file!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM hashes
|
||||
WHERE file_id = $1
|
||||
",
|
||||
row.id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM files
|
||||
WHERE files.id = $1
|
||||
",
|
||||
row.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
let mod_id: models::mods::ModId = database::models::ids::ModId(row.mod_id).into();
|
||||
file_host
|
||||
.delete_file_version(
|
||||
"",
|
||||
&format!(
|
||||
"data/{}/versions/{}/{}",
|
||||
mod_id, row.version_number, row.filename
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
|
||||
@@ -2,32 +2,32 @@ use futures::{StreamExt, TryStreamExt};
|
||||
use log::info;
|
||||
|
||||
use super::IndexingError;
|
||||
use crate::models::mods::SideType;
|
||||
use crate::search::UploadSearchMod;
|
||||
use crate::models::projects::SideType;
|
||||
use crate::search::UploadSearchProject;
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::borrow::Cow;
|
||||
|
||||
// TODO: only loaders for recent versions? For mods that have moved from forge to fabric
|
||||
pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingError> {
|
||||
info!("Indexing local mods!");
|
||||
// TODO: only loaders for recent versions? For projects that have moved from forge to fabric
|
||||
pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, IndexingError> {
|
||||
info!("Indexing local projects!");
|
||||
|
||||
let mut docs_to_add: Vec<UploadSearchMod> = vec![];
|
||||
let mut docs_to_add: Vec<UploadSearchProject> = vec![];
|
||||
|
||||
let mut mods = sqlx::query!(
|
||||
let mut projects = sqlx::query!(
|
||||
"
|
||||
SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m
|
||||
"
|
||||
).fetch(&pool);
|
||||
|
||||
while let Some(result) = mods.next().await {
|
||||
if let Ok(mod_data) = result {
|
||||
let status = crate::models::mods::ModStatus::from_str(
|
||||
while let Some(result) = projects.next().await {
|
||||
if let Ok(project_data) = result {
|
||||
let status = crate::models::projects::ProjectStatus::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT status FROM statuses
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.status,
|
||||
SELECT status FROM statuses
|
||||
WHERE id = $1
|
||||
",
|
||||
project_data.status,
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
@@ -46,7 +46,7 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
WHERE versions.mod_id = $1
|
||||
ORDER BY gv.created ASC
|
||||
",
|
||||
mod_data.id
|
||||
project_data.id
|
||||
)
|
||||
.fetch_many(&pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
@@ -60,7 +60,7 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
INNER JOIN loaders ON loaders.id = lv.loader_id
|
||||
WHERE versions.mod_id = $1
|
||||
",
|
||||
mod_data.id
|
||||
project_data.id
|
||||
)
|
||||
.fetch_many(&pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.loader))) })
|
||||
@@ -74,7 +74,7 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
INNER JOIN categories c ON mc.joining_category_id=c.id
|
||||
WHERE mc.joining_mod_id = $1
|
||||
",
|
||||
mod_data.id
|
||||
project_data.id
|
||||
)
|
||||
.fetch_many(&pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.category))) })
|
||||
@@ -90,22 +90,21 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
WHERE tm.team_id = $2 AND tm.role = $1
|
||||
",
|
||||
crate::models::teams::OWNER_ROLE,
|
||||
mod_data.team_id,
|
||||
project_data.team_id,
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
|
||||
let mut icon_url = "".to_string();
|
||||
|
||||
if let Some(url) = mod_data.icon_url {
|
||||
if let Some(url) = project_data.icon_url {
|
||||
icon_url = url;
|
||||
}
|
||||
|
||||
let mod_id = crate::models::ids::ModId(mod_data.id as u64);
|
||||
let author_id = crate::models::ids::UserId(user.id as u64);
|
||||
let project_id = crate::models::ids::ProjectId(project_data.id as u64);
|
||||
|
||||
// TODO: is this correct? This just gets the latest version of
|
||||
// minecraft that this mod has a version that supports; it doesn't
|
||||
// minecraft that this project has a version that supports; it doesn't
|
||||
// take betas or other info into account.
|
||||
let latest_version = versions
|
||||
.last()
|
||||
@@ -116,10 +115,10 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
let client_side = SideType::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.client_side,
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
project_data.client_side,
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
@@ -129,10 +128,10 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
let server_side = SideType::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.server_side,
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
project_data.server_side,
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
@@ -140,33 +139,31 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
);
|
||||
|
||||
let license = crate::database::models::categories::License::get(
|
||||
crate::database::models::LicenseId(mod_data.license),
|
||||
crate::database::models::LicenseId(project_data.license),
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
docs_to_add.push(UploadSearchMod {
|
||||
mod_id: format!("local-{}", mod_id),
|
||||
title: mod_data.title,
|
||||
description: mod_data.description,
|
||||
docs_to_add.push(UploadSearchProject {
|
||||
project_id: format!("local-{}", project_id),
|
||||
title: project_data.title,
|
||||
description: project_data.description,
|
||||
categories,
|
||||
versions,
|
||||
follows: mod_data.follows,
|
||||
downloads: mod_data.downloads,
|
||||
page_url: format!("https://modrinth.com/mod/{}", mod_id),
|
||||
follows: project_data.follows,
|
||||
downloads: project_data.downloads,
|
||||
icon_url,
|
||||
author: user.username,
|
||||
author_url: format!("https://modrinth.com/user/{}", author_id),
|
||||
date_created: mod_data.published,
|
||||
created_timestamp: mod_data.published.timestamp(),
|
||||
date_modified: mod_data.updated,
|
||||
modified_timestamp: mod_data.updated.timestamp(),
|
||||
date_created: project_data.published,
|
||||
created_timestamp: project_data.published.timestamp(),
|
||||
date_modified: project_data.updated,
|
||||
modified_timestamp: project_data.updated.timestamp(),
|
||||
latest_version,
|
||||
license: license.short,
|
||||
client_side: client_side.to_string(),
|
||||
server_side: server_side.to_string(),
|
||||
host: Cow::Borrowed("modrinth"),
|
||||
slug: mod_data.slug,
|
||||
slug: project_data.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -175,10 +172,10 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
}
|
||||
|
||||
pub async fn query_one(
|
||||
id: crate::database::models::ModId,
|
||||
id: crate::database::models::ProjectId,
|
||||
exec: &mut sqlx::PgConnection,
|
||||
) -> Result<UploadSearchMod, IndexingError> {
|
||||
let mod_data = sqlx::query!(
|
||||
) -> Result<UploadSearchProject, IndexingError> {
|
||||
let project_data = sqlx::query!(
|
||||
"
|
||||
SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug, m.license, m.client_side, m.server_side
|
||||
FROM mods m
|
||||
@@ -195,7 +192,7 @@ pub async fn query_one(
|
||||
WHERE versions.mod_id = $1
|
||||
ORDER BY gv.created ASC
|
||||
",
|
||||
mod_data.id
|
||||
project_data.id
|
||||
)
|
||||
.fetch_many(&mut *exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
@@ -209,7 +206,7 @@ pub async fn query_one(
|
||||
INNER JOIN loaders ON loaders.id = lv.loader_id
|
||||
WHERE versions.mod_id = $1
|
||||
",
|
||||
mod_data.id
|
||||
project_data.id
|
||||
)
|
||||
.fetch_many(&mut *exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.loader))) })
|
||||
@@ -223,7 +220,7 @@ pub async fn query_one(
|
||||
INNER JOIN categories c ON mc.joining_category_id=c.id
|
||||
WHERE mc.joining_mod_id = $1
|
||||
",
|
||||
mod_data.id
|
||||
project_data.id
|
||||
)
|
||||
.fetch_many(&mut *exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.category))) })
|
||||
@@ -239,22 +236,21 @@ pub async fn query_one(
|
||||
WHERE tm.team_id = $2 AND tm.role = $1
|
||||
",
|
||||
crate::models::teams::OWNER_ROLE,
|
||||
mod_data.team_id,
|
||||
project_data.team_id,
|
||||
)
|
||||
.fetch_one(&mut *exec)
|
||||
.await?;
|
||||
|
||||
let mut icon_url = "".to_string();
|
||||
|
||||
if let Some(url) = mod_data.icon_url {
|
||||
if let Some(url) = project_data.icon_url {
|
||||
icon_url = url;
|
||||
}
|
||||
|
||||
let mod_id = crate::models::ids::ModId(mod_data.id as u64);
|
||||
let author_id = crate::models::ids::UserId(user.id as u64);
|
||||
let project_id = crate::models::ids::ProjectId(project_data.id as u64);
|
||||
|
||||
// TODO: is this correct? This just gets the latest version of
|
||||
// minecraft that this mod has a version that supports; it doesn't
|
||||
// minecraft that this project has a version that supports; it doesn't
|
||||
// take betas or other info into account.
|
||||
let latest_version = versions
|
||||
.last()
|
||||
@@ -265,10 +261,10 @@ pub async fn query_one(
|
||||
let client_side = SideType::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.client_side,
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
project_data.client_side,
|
||||
)
|
||||
.fetch_one(&mut *exec)
|
||||
.await?
|
||||
@@ -278,10 +274,10 @@ pub async fn query_one(
|
||||
let server_side = SideType::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.server_side,
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
project_data.server_side,
|
||||
)
|
||||
.fetch_one(&mut *exec)
|
||||
.await?
|
||||
@@ -289,32 +285,30 @@ pub async fn query_one(
|
||||
);
|
||||
|
||||
let license = crate::database::models::categories::License::get(
|
||||
crate::database::models::LicenseId(mod_data.license),
|
||||
crate::database::models::LicenseId(project_data.license),
|
||||
&mut *exec,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(UploadSearchMod {
|
||||
mod_id: format!("local-{}", mod_id),
|
||||
title: mod_data.title,
|
||||
description: mod_data.description,
|
||||
Ok(UploadSearchProject {
|
||||
project_id: format!("local-{}", project_id),
|
||||
title: project_data.title,
|
||||
description: project_data.description,
|
||||
categories,
|
||||
versions,
|
||||
follows: mod_data.follows,
|
||||
downloads: mod_data.downloads,
|
||||
page_url: format!("https://modrinth.com/mod/{}", mod_id),
|
||||
follows: project_data.follows,
|
||||
downloads: project_data.downloads,
|
||||
icon_url,
|
||||
author: user.username,
|
||||
author_url: format!("https://modrinth.com/user/{}", author_id),
|
||||
date_created: mod_data.published,
|
||||
created_timestamp: mod_data.published.timestamp(),
|
||||
date_modified: mod_data.updated,
|
||||
modified_timestamp: mod_data.updated.timestamp(),
|
||||
date_created: project_data.published,
|
||||
created_timestamp: project_data.published.timestamp(),
|
||||
date_modified: project_data.updated,
|
||||
modified_timestamp: project_data.updated.timestamp(),
|
||||
latest_version,
|
||||
license: license.short,
|
||||
client_side: client_side.to_string(),
|
||||
server_side: server_side.to_string(),
|
||||
host: Cow::Borrowed("modrinth"),
|
||||
slug: mod_data.slug,
|
||||
slug: project_data.slug,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pub mod local_import;
|
||||
pub mod queue;
|
||||
|
||||
use crate::search::{SearchConfig, UploadSearchMod};
|
||||
use crate::search::{SearchConfig, UploadSearchProject};
|
||||
use local_import::index_local;
|
||||
use meilisearch_sdk::client::Client;
|
||||
use meilisearch_sdk::indexes::Index;
|
||||
@@ -27,14 +27,13 @@ pub enum IndexingError {
|
||||
EnvError(#[from] dotenv::Error),
|
||||
}
|
||||
|
||||
// The chunk size for adding mods to the indexing database. If the request size
|
||||
// The chunk size for adding projects to the indexing database. If the request size
|
||||
// is too large (>10MiB) then the request fails with an error. This chunk size
|
||||
// assumes a max average size of 1KiB per mod to avoid this cap.
|
||||
// assumes a max average size of 1KiB per project to avoid this cap.
|
||||
const MEILISEARCH_CHUNK_SIZE: usize = 10000;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IndexingSettings {
|
||||
pub index_external: bool,
|
||||
pub index_local: bool,
|
||||
}
|
||||
|
||||
@@ -42,31 +41,24 @@ impl IndexingSettings {
|
||||
#[allow(dead_code)]
|
||||
pub fn from_env() -> Self {
|
||||
let index_local = true;
|
||||
let index_external = dotenv::var("INDEX_CURSEFORGE")
|
||||
.ok()
|
||||
.and_then(|b| b.parse::<bool>().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
Self {
|
||||
index_external,
|
||||
index_local,
|
||||
}
|
||||
Self { index_local }
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn index_mods(
|
||||
pub async fn index_projects(
|
||||
pool: PgPool,
|
||||
settings: IndexingSettings,
|
||||
config: &SearchConfig,
|
||||
) -> Result<(), IndexingError> {
|
||||
let mut docs_to_add: Vec<UploadSearchMod> = vec![];
|
||||
let mut docs_to_add: Vec<UploadSearchProject> = vec![];
|
||||
|
||||
if settings.index_local {
|
||||
docs_to_add.append(&mut index_local(pool.clone()).await?);
|
||||
}
|
||||
// Write Indices
|
||||
|
||||
add_mods(docs_to_add, config).await?;
|
||||
add_projects(docs_to_add, config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -74,12 +66,12 @@ pub async fn index_mods(
|
||||
pub async fn reset_indices(config: &SearchConfig) -> Result<(), IndexingError> {
|
||||
let client = Client::new(&*config.address, &*config.key);
|
||||
|
||||
client.delete_index("relevance_mods").await?;
|
||||
client.delete_index("downloads_mods").await?;
|
||||
client.delete_index("follows_mods").await?;
|
||||
client.delete_index("alphabetically_mods").await?;
|
||||
client.delete_index("updated_mods").await?;
|
||||
client.delete_index("newest_mods").await?;
|
||||
client.delete_index("relevance_projects").await?;
|
||||
client.delete_index("downloads_projects").await?;
|
||||
client.delete_index("follows_projects").await?;
|
||||
client.delete_index("updated_projects").await?;
|
||||
client.delete_index("newest_projects").await?;
|
||||
client.delete_index("alphabetically_projects").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -87,7 +79,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
||||
let client = Client::new(&*config.address, &*config.key);
|
||||
|
||||
// Relevance Index
|
||||
update_index(&client, "relevance_mods", {
|
||||
update_index(&client, "relevance_projects", {
|
||||
let mut relevance_rules = default_rules();
|
||||
relevance_rules.push_back("desc(downloads)".to_string());
|
||||
relevance_rules.into()
|
||||
@@ -95,7 +87,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
||||
.await?;
|
||||
|
||||
// Downloads Index
|
||||
update_index(&client, "downloads_mods", {
|
||||
update_index(&client, "downloads_projects", {
|
||||
let mut downloads_rules = default_rules();
|
||||
downloads_rules.push_front("desc(downloads)".to_string());
|
||||
downloads_rules.into()
|
||||
@@ -103,7 +95,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
||||
.await?;
|
||||
|
||||
// Follows Index
|
||||
update_index(&client, "follows_mods", {
|
||||
update_index(&client, "follows_projects", {
|
||||
let mut follows_rules = default_rules();
|
||||
follows_rules.push_front("desc(follows)".to_string());
|
||||
follows_rules.into()
|
||||
@@ -111,15 +103,15 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
||||
.await?;
|
||||
|
||||
// Alphabetically Index
|
||||
update_index(&client, "alphabetically_mods", {
|
||||
update_index(&client, "alphabetically_projects", {
|
||||
let mut alphabetically_rules = default_rules();
|
||||
alphabetically_rules.push_front("desc(title)".to_string());
|
||||
alphabetically_rules.into()
|
||||
})
|
||||
.await?;
|
||||
.await?;
|
||||
|
||||
// Updated Index
|
||||
update_index(&client, "updated_mods", {
|
||||
update_index(&client, "updated_projects", {
|
||||
let mut updated_rules = default_rules();
|
||||
updated_rules.push_front("desc(modified_timestamp)".to_string());
|
||||
updated_rules.into()
|
||||
@@ -127,7 +119,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
||||
.await?;
|
||||
|
||||
// Created Index
|
||||
update_index(&client, "newest_mods", {
|
||||
update_index(&client, "newest_projects", {
|
||||
let mut newest_rules = default_rules();
|
||||
newest_rules.push_front("desc(created_timestamp)".to_string());
|
||||
newest_rules.into()
|
||||
@@ -147,7 +139,7 @@ async fn update_index<'a>(
|
||||
Err(meilisearch_sdk::errors::Error::MeiliSearchError {
|
||||
error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound,
|
||||
..
|
||||
}) => client.create_index(name, Some("mod_id")).await?,
|
||||
}) => client.create_index(name, Some("project_id")).await?,
|
||||
Err(e) => {
|
||||
return Err(IndexingError::IndexDBError(e));
|
||||
}
|
||||
@@ -171,7 +163,7 @@ async fn create_index<'a>(
|
||||
..
|
||||
}) => {
|
||||
// Only create index and set settings if the index doesn't already exist
|
||||
let index = client.create_index(name, Some("mod_id")).await?;
|
||||
let index = client.create_index(name, Some("project_id")).await?;
|
||||
|
||||
index
|
||||
.set_settings(&default_settings().with_ranking_rules(rules()))
|
||||
@@ -186,72 +178,72 @@ async fn create_index<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_to_index(index: Index<'_>, mods: &[UploadSearchMod]) -> Result<(), IndexingError> {
|
||||
async fn add_to_index(index: Index<'_>, mods: &[UploadSearchProject]) -> Result<(), IndexingError> {
|
||||
for chunk in mods.chunks(MEILISEARCH_CHUNK_SIZE) {
|
||||
index.add_documents(chunk, Some("mod_id")).await?;
|
||||
index.add_documents(chunk, Some("project_id")).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_mods(
|
||||
mods: Vec<UploadSearchMod>,
|
||||
pub async fn add_projects(
|
||||
projects: Vec<UploadSearchProject>,
|
||||
config: &SearchConfig,
|
||||
) -> Result<(), IndexingError> {
|
||||
let client = Client::new(&*config.address, &*config.key);
|
||||
|
||||
// Relevance Index
|
||||
let relevance_index = create_index(&client, "relevance_mods", || {
|
||||
let relevance_index = create_index(&client, "relevance_projects", || {
|
||||
let mut relevance_rules = default_rules();
|
||||
relevance_rules.push_back("desc(downloads)".to_string());
|
||||
relevance_rules.into()
|
||||
})
|
||||
.await?;
|
||||
add_to_index(relevance_index, &mods).await?;
|
||||
add_to_index(relevance_index, &projects).await?;
|
||||
|
||||
// Downloads Index
|
||||
let downloads_index = create_index(&client, "downloads_mods", || {
|
||||
let downloads_index = create_index(&client, "downloads_projects", || {
|
||||
let mut downloads_rules = default_rules();
|
||||
downloads_rules.push_front("desc(downloads)".to_string());
|
||||
downloads_rules.into()
|
||||
})
|
||||
.await?;
|
||||
add_to_index(downloads_index, &mods).await?;
|
||||
add_to_index(downloads_index, &projects).await?;
|
||||
|
||||
// Follows Index
|
||||
let follows_index = create_index(&client, "follows_mods", || {
|
||||
let follows_index = create_index(&client, "follows_projects", || {
|
||||
let mut follows_rules = default_rules();
|
||||
follows_rules.push_front("desc(follows)".to_string());
|
||||
follows_rules.into()
|
||||
})
|
||||
.await?;
|
||||
add_to_index(follows_index, &mods).await?;
|
||||
add_to_index(follows_index, &projects).await?;
|
||||
|
||||
// Alphabetically Index
|
||||
let alphabetically_index = create_index(&client, "alphabetically_mods", || {
|
||||
let alphabetically_index = create_index(&client, "alphabetically_projects", || {
|
||||
let mut alphabetically_rules = default_rules();
|
||||
alphabetically_rules.push_front("desc(title)".to_string());
|
||||
alphabetically_rules.into()
|
||||
})
|
||||
.await?;
|
||||
add_to_index(alphabetically_index, &mods).await?;
|
||||
.await?;
|
||||
add_to_index(alphabetically_index, &projects).await?;
|
||||
|
||||
// Updated Index
|
||||
let updated_index = create_index(&client, "updated_mods", || {
|
||||
let updated_index = create_index(&client, "updated_projects", || {
|
||||
let mut updated_rules = default_rules();
|
||||
updated_rules.push_front("desc(modified_timestamp)".to_string());
|
||||
updated_rules.into()
|
||||
})
|
||||
.await?;
|
||||
add_to_index(updated_index, &mods).await?;
|
||||
add_to_index(updated_index, &projects).await?;
|
||||
|
||||
// Created Index
|
||||
let newest_index = create_index(&client, "newest_mods", || {
|
||||
let newest_index = create_index(&client, "newest_projects", || {
|
||||
let mut newest_rules = default_rules();
|
||||
newest_rules.push_front("desc(created_timestamp)".to_string());
|
||||
newest_rules.into()
|
||||
})
|
||||
.await?;
|
||||
add_to_index(newest_index, &mods).await?;
|
||||
add_to_index(newest_index, &projects).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -271,7 +263,7 @@ fn default_rules() -> VecDeque<String> {
|
||||
|
||||
fn default_settings() -> Settings {
|
||||
let displayed_attributes = vec![
|
||||
"mod_id".to_string(),
|
||||
"project_id".to_string(),
|
||||
"slug".to_string(),
|
||||
"author".to_string(),
|
||||
"title".to_string(),
|
||||
@@ -280,16 +272,13 @@ fn default_settings() -> Settings {
|
||||
"versions".to_string(),
|
||||
"downloads".to_string(),
|
||||
"follows".to_string(),
|
||||
"page_url".to_string(),
|
||||
"icon_url".to_string(),
|
||||
"author_url".to_string(),
|
||||
"date_created".to_string(),
|
||||
"date_modified".to_string(),
|
||||
"latest_version".to_string(),
|
||||
"license".to_string(),
|
||||
"client_side".to_string(),
|
||||
"server_side".to_string(),
|
||||
"host".to_string(),
|
||||
];
|
||||
|
||||
let searchable_attributes = vec![
|
||||
@@ -325,7 +314,7 @@ fn default_settings() -> Settings {
|
||||
// This isn't currenly used, but I wrote it and it works, so I'm
|
||||
// keeping this mess in case someone needs it in the future.
|
||||
#[allow(dead_code)]
|
||||
pub fn sort_mods(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
pub fn sort_projects(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
let cmp = a.contains('.').cmp(&b.contains('.'));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{add_mods, IndexingError, UploadSearchMod};
|
||||
use super::{add_projects, IndexingError, UploadSearchProject};
|
||||
use crate::search::SearchConfig;
|
||||
use std::sync::Mutex;
|
||||
|
||||
@@ -7,7 +7,7 @@ pub struct CreationQueue {
|
||||
// and I don't think this can deadlock. This queue requires fast
|
||||
// writes and then a single potentially slower read/write that
|
||||
// empties the queue.
|
||||
queue: Mutex<Vec<UploadSearchMod>>,
|
||||
queue: Mutex<Vec<UploadSearchProject>>,
|
||||
}
|
||||
|
||||
impl CreationQueue {
|
||||
@@ -17,11 +17,11 @@ impl CreationQueue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&self, search_mod: UploadSearchMod) {
|
||||
pub fn add(&self, search_project: UploadSearchProject) {
|
||||
// Can only panic if mutex is poisoned
|
||||
self.queue.lock().unwrap().push(search_mod);
|
||||
self.queue.lock().unwrap().push(search_project);
|
||||
}
|
||||
pub fn take(&self) -> Vec<UploadSearchMod> {
|
||||
pub fn take(&self) -> Vec<UploadSearchProject> {
|
||||
std::mem::replace(&mut *self.queue.lock().unwrap(), Vec::with_capacity(10))
|
||||
}
|
||||
}
|
||||
@@ -31,5 +31,5 @@ pub async fn index_queue(
|
||||
config: &SearchConfig,
|
||||
) -> Result<(), IndexingError> {
|
||||
let queue = queue.take();
|
||||
add_mods(queue, config).await
|
||||
add_projects(queue, config).await
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::mods::SearchRequest;
|
||||
use crate::models::projects::SearchRequest;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::HttpResponse;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -57,11 +57,11 @@ pub struct SearchConfig {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// A mod document used for uploading mods to meilisearch's indices.
|
||||
/// A project document used for uploading projects to meilisearch's indices.
|
||||
/// This contains some extra data that is not returned by search results.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UploadSearchMod {
|
||||
pub mod_id: String,
|
||||
pub struct UploadSearchProject {
|
||||
pub project_id: String,
|
||||
pub slug: Option<String>,
|
||||
pub author: String,
|
||||
pub title: String,
|
||||
@@ -70,17 +70,15 @@ pub struct UploadSearchMod {
|
||||
pub versions: Vec<String>,
|
||||
pub follows: i32,
|
||||
pub downloads: i32,
|
||||
pub page_url: String,
|
||||
pub icon_url: String,
|
||||
pub author_url: String,
|
||||
pub latest_version: Cow<'static, str>,
|
||||
pub license: String,
|
||||
pub client_side: String,
|
||||
pub server_side: String,
|
||||
|
||||
/// RFC 3339 formatted creation date of the mod
|
||||
/// RFC 3339 formatted creation date of the project
|
||||
pub date_created: DateTime<Utc>,
|
||||
/// Unix timestamp of the creation date of the mod
|
||||
/// Unix timestamp of the creation date of the project
|
||||
pub created_timestamp: i64,
|
||||
/// RFC 3339 formatted date/time of last major modification (update)
|
||||
pub date_modified: DateTime<Utc>,
|
||||
@@ -92,15 +90,15 @@ pub struct UploadSearchMod {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SearchResults {
|
||||
pub hits: Vec<ResultSearchMod>,
|
||||
pub hits: Vec<ResultSearchProject>,
|
||||
pub offset: usize,
|
||||
pub limit: usize,
|
||||
pub total_hits: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ResultSearchMod {
|
||||
pub mod_id: String,
|
||||
pub struct ResultSearchProject {
|
||||
pub project_id: String,
|
||||
pub slug: Option<String>,
|
||||
pub author: String,
|
||||
pub title: String,
|
||||
@@ -110,39 +108,34 @@ pub struct ResultSearchMod {
|
||||
pub versions: Vec<String>,
|
||||
pub downloads: i32,
|
||||
pub follows: i32,
|
||||
pub page_url: String,
|
||||
pub icon_url: String,
|
||||
pub author_url: String,
|
||||
/// RFC 3339 formatted creation date of the mod
|
||||
/// RFC 3339 formatted creation date of the project
|
||||
pub date_created: String,
|
||||
/// RFC 3339 formatted modification date of the mod
|
||||
/// RFC 3339 formatted modification date of the project
|
||||
pub date_modified: String,
|
||||
pub latest_version: String,
|
||||
pub license: String,
|
||||
pub client_side: String,
|
||||
pub server_side: String,
|
||||
|
||||
/// The host of the mod: Either `modrinth` or `curseforge`
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
impl Document for UploadSearchMod {
|
||||
impl Document for UploadSearchProject {
|
||||
type UIDType = String;
|
||||
|
||||
fn get_uid(&self) -> &Self::UIDType {
|
||||
&self.mod_id
|
||||
&self.project_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Document for ResultSearchMod {
|
||||
impl Document for ResultSearchProject {
|
||||
type UIDType = String;
|
||||
|
||||
fn get_uid(&self) -> &Self::UIDType {
|
||||
&self.mod_id
|
||||
&self.project_id
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search_for_mod(
|
||||
pub async fn search_for_project(
|
||||
info: &SearchRequest,
|
||||
config: &SearchConfig,
|
||||
) -> Result<SearchResults, SearchError> {
|
||||
@@ -160,12 +153,12 @@ pub async fn search_for_mod(
|
||||
let limit = info.limit.as_deref().unwrap_or("10").parse()?;
|
||||
|
||||
let index = match index {
|
||||
"relevance" => "relevance_mods",
|
||||
"downloads" => "downloads_mods",
|
||||
"follows" => "follows_mods",
|
||||
"alphabetically" => "alphabetically_mods",
|
||||
"updated" => "updated_mods",
|
||||
"newest" => "newest_mods",
|
||||
"relevance" => "relevance_projects",
|
||||
"downloads" => "downloads_projects",
|
||||
"follows" => "follows_projects",
|
||||
"updated" => "updated_projects",
|
||||
"newest" => "newest_projects",
|
||||
"alphabetically" => "alphabetically_projects",
|
||||
i => return Err(SearchError::InvalidIndex(i.to_string())),
|
||||
};
|
||||
|
||||
@@ -203,7 +196,7 @@ pub async fn search_for_mod(
|
||||
query.with_facet_filters(&why_must_you_do_this);
|
||||
}
|
||||
|
||||
let results = query.execute::<ResultSearchMod>().await?;
|
||||
let results = query.execute::<ResultSearchProject>().await?;
|
||||
|
||||
Ok(SearchResults {
|
||||
hits: results.hits.into_iter().map(|r| r.result).collect(),
|
||||
|
||||
48
src/validate/fabric.rs
Normal file
48
src/validate/fabric.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
|
||||
pub struct FabricValidator {}
|
||||
|
||||
impl super::Validator for FabricValidator {
|
||||
fn get_file_extensions<'a>(&self) -> &'a [&'a str] {
|
||||
&["jar", "zip"]
|
||||
}
|
||||
|
||||
fn get_project_types<'a>(&self) -> &'a [&'a str] {
|
||||
&["mod"]
|
||||
}
|
||||
|
||||
fn get_supported_loaders<'a>(&self) -> &'a [&'a str] {
|
||||
&["fabric"]
|
||||
}
|
||||
|
||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||
// Time since release of 18w49a, the first fabric version
|
||||
SupportedGameVersions::PastDate(DateTime::<Utc>::from_utc(
|
||||
NaiveDateTime::from_timestamp(1543969469, 0),
|
||||
Utc,
|
||||
))
|
||||
}
|
||||
|
||||
fn validate(
|
||||
&self,
|
||||
archive: &mut ZipArchive<Cursor<&[u8]>>,
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
archive.by_name("fabric.mod.json")?;
|
||||
|
||||
if !archive
|
||||
.file_names()
|
||||
.any(|name| name.ends_with("refmap.json") || name.ends_with(".class"))
|
||||
{
|
||||
return Ok(ValidationResult::Warning(
|
||||
"Fabric mod file is a source file!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
//TODO: Check if file is a dev JAR?
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
}
|
||||
}
|
||||
86
src/validate/forge.rs
Normal file
86
src/validate/forge.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
|
||||
pub struct ForgeValidator {}
|
||||
|
||||
impl super::Validator for ForgeValidator {
|
||||
fn get_file_extensions<'a>(&self) -> &'a [&'a str] {
|
||||
&["jar", "zip"]
|
||||
}
|
||||
|
||||
fn get_project_types<'a>(&self) -> &'a [&'a str] {
|
||||
&["mod"]
|
||||
}
|
||||
|
||||
fn get_supported_loaders<'a>(&self) -> &'a [&'a str] {
|
||||
&["forge"]
|
||||
}
|
||||
|
||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||
// Time since release of 1.13, the first forge version which uses the new TOML system
|
||||
SupportedGameVersions::PastDate(DateTime::<Utc>::from_utc(
|
||||
NaiveDateTime::from_timestamp(1540122067, 0),
|
||||
Utc,
|
||||
))
|
||||
}
|
||||
|
||||
fn validate(
|
||||
&self,
|
||||
archive: &mut ZipArchive<Cursor<&[u8]>>,
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
archive.by_name("META-INF/mods.toml")?;
|
||||
|
||||
if !archive.file_names().any(|name| name.ends_with(".class")) {
|
||||
return Ok(ValidationResult::Warning(
|
||||
"Forge mod file is a source file!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
//TODO: Check if file is a dev JAR?
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LegacyForgeValidator {}
|
||||
|
||||
impl super::Validator for LegacyForgeValidator {
|
||||
fn get_file_extensions<'a>(&self) -> &'a [&'a str] {
|
||||
&["jar", "zip"]
|
||||
}
|
||||
|
||||
fn get_project_types<'a>(&self) -> &'a [&'a str] {
|
||||
&["mod"]
|
||||
}
|
||||
|
||||
fn get_supported_loaders<'a>(&self) -> &'a [&'a str] {
|
||||
&["forge"]
|
||||
}
|
||||
|
||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||
// Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods
|
||||
SupportedGameVersions::Range(
|
||||
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(1366818300, 0), Utc),
|
||||
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(1505810340, 0), Utc),
|
||||
)
|
||||
}
|
||||
|
||||
fn validate(
|
||||
&self,
|
||||
archive: &mut ZipArchive<Cursor<&[u8]>>,
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
archive.by_name("mcmod.info")?;
|
||||
|
||||
if !archive.file_names().any(|name| name.ends_with(".class")) {
|
||||
return Ok(ValidationResult::Warning(
|
||||
"Forge mod file is a source file!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
//TODO: Check if file is a dev JAR?
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
}
|
||||
}
|
||||
117
src/validate/mod.rs
Normal file
117
src/validate/mod.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use crate::models::projects::{GameVersion, Loader};
|
||||
use crate::validate::fabric::FabricValidator;
|
||||
use crate::validate::forge::{ForgeValidator, LegacyForgeValidator};
|
||||
use crate::validate::pack::PackValidator;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
use thiserror::Error;
|
||||
use zip::ZipArchive;
|
||||
|
||||
mod fabric;
|
||||
mod forge;
|
||||
mod pack;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidationError {
|
||||
#[error("Unable to read Zip Archive: {0}")]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Error while validating JSON: {0}")]
|
||||
SerDeError(#[from] serde_json::Error),
|
||||
#[error("Invalid Input: {0}")]
|
||||
InvalidInputError(String),
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub enum ValidationResult {
|
||||
/// File should be marked as primary
|
||||
Pass,
|
||||
/// File should not be marked primary, the reason for which is inside the String
|
||||
Warning(String),
|
||||
}
|
||||
|
||||
pub enum SupportedGameVersions {
|
||||
All,
|
||||
PastDate(DateTime<Utc>),
|
||||
Range(DateTime<Utc>, DateTime<Utc>),
|
||||
Custom(Vec<GameVersion>),
|
||||
}
|
||||
|
||||
pub trait Validator: Sync {
|
||||
fn get_file_extensions<'a>(&self) -> &'a [&'a str];
|
||||
fn get_project_types<'a>(&self) -> &'a [&'a str];
|
||||
fn get_supported_loaders<'a>(&self) -> &'a [&'a str];
|
||||
fn get_supported_game_versions(&self) -> SupportedGameVersions;
|
||||
fn validate(
|
||||
&self,
|
||||
archive: &mut ZipArchive<Cursor<&[u8]>>,
|
||||
) -> Result<ValidationResult, ValidationError>;
|
||||
}
|
||||
|
||||
static VALIDATORS: [&dyn Validator; 4] = [
|
||||
&PackValidator {},
|
||||
&FabricValidator {},
|
||||
&ForgeValidator {},
|
||||
&LegacyForgeValidator {},
|
||||
];
|
||||
|
||||
/// The return value is whether this file should be marked as primary or not, based on the analysis of the file
|
||||
pub fn validate_file(
|
||||
data: &[u8],
|
||||
file_extension: &str,
|
||||
project_type: &str,
|
||||
loaders: Vec<Loader>,
|
||||
game_versions: Vec<GameVersion>,
|
||||
all_game_versions: &[crate::database::models::categories::GameVersion],
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
let reader = std::io::Cursor::new(data);
|
||||
let mut zip = zip::ZipArchive::new(reader)?;
|
||||
|
||||
for validator in &VALIDATORS {
|
||||
if validator.get_file_extensions().contains(&file_extension)
|
||||
&& validator.get_project_types().contains(&project_type)
|
||||
&& loaders
|
||||
.iter()
|
||||
.any(|x| validator.get_supported_loaders().contains(&&*x.0))
|
||||
&& game_version_supported(
|
||||
&game_versions,
|
||||
all_game_versions,
|
||||
validator.get_supported_game_versions(),
|
||||
)
|
||||
{
|
||||
return validator.validate(&mut zip);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
}
|
||||
|
||||
fn game_version_supported(
|
||||
game_versions: &[GameVersion],
|
||||
all_game_versions: &[crate::database::models::categories::GameVersion],
|
||||
supported_game_versions: SupportedGameVersions,
|
||||
) -> bool {
|
||||
match supported_game_versions {
|
||||
SupportedGameVersions::All => true,
|
||||
SupportedGameVersions::PastDate(date) => game_versions.iter().any(|x| {
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.map(|x| x.date > date)
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
SupportedGameVersions::Range(before, after) => game_versions.iter().any(|x| {
|
||||
all_game_versions
|
||||
.iter()
|
||||
.find(|y| y.version == x.0)
|
||||
.map(|x| x.date > before && x.date < after)
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
SupportedGameVersions::Custom(versions) => {
|
||||
versions.iter().any(|x| game_versions.contains(x))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//todo: fabric/forge validators for 1.8+ respectively
|
||||
97
src/validate/pack.rs
Normal file
97
src/validate/pack.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use crate::models::projects::SideType;
|
||||
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Cursor, Read};
|
||||
use zip::ZipArchive;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFormat {
|
||||
pub game: String,
|
||||
pub format_version: i32,
|
||||
pub version_id: String,
|
||||
pub name: String,
|
||||
pub summary: Option<String>,
|
||||
pub dependencies: std::collections::HashMap<PackDependency, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFile {
|
||||
pub path: String,
|
||||
pub hashes: std::collections::HashMap<String, String>,
|
||||
pub env: Environment,
|
||||
pub downloads: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Environment {
|
||||
pub client: SideType,
|
||||
pub server: SideType,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PackDependency {
|
||||
Forge,
|
||||
FabricLoader,
|
||||
Minecraft,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PackDependency {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl PackDependency {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PackDependency::Forge => "forge",
|
||||
PackDependency::FabricLoader => "fabric-loader",
|
||||
PackDependency::Minecraft => "minecraft",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PackValidator {}
|
||||
|
||||
impl super::Validator for PackValidator {
|
||||
fn get_file_extensions<'a>(&self) -> &'a [&'a str] {
|
||||
&["zip"]
|
||||
}
|
||||
|
||||
fn get_project_types<'a>(&self) -> &'a [&'a str] {
|
||||
&["modpack"]
|
||||
}
|
||||
|
||||
fn get_supported_loaders<'a>(&self) -> &'a [&'a str] {
|
||||
&["forge", "fabric"]
|
||||
}
|
||||
|
||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||
SupportedGameVersions::All
|
||||
}
|
||||
|
||||
fn validate(
|
||||
&self,
|
||||
archive: &mut ZipArchive<Cursor<&[u8]>>,
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
let mut file = archive.by_name("index.json")?;
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let pack: PackFormat = serde_json::from_str(&*contents)?;
|
||||
|
||||
if pack.game != *"minecraft" {
|
||||
return Err(ValidationError::InvalidInputError(format!(
|
||||
"Game {0} does not exist!",
|
||||
pack.game
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user