You've already forked AstralRinth
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
|
# 30 minutes
|
||||||
VERSION_INDEX_INTERVAL=1800
|
VERSION_INDEX_INTERVAL=1800
|
||||||
|
|
||||||
GITHUB_CLIENT_ID=3acffb2e808d16d4b226
|
GITHUB_CLIENT_ID=none
|
||||||
GITHUB_CLIENT_SECRET=none
|
GITHUB_CLIENT_SECRET=none
|
||||||
|
|
||||||
RATE_LIMIT_IGNORE_IPS='[]'
|
RATE_LIMIT_IGNORE_IPS='[]'
|
||||||
73
.idea/labrinth.iml
generated
73
.idea/labrinth.iml
generated
@@ -3,6 +3,79 @@
|
|||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
<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" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<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]
|
[package]
|
||||||
name = "labrinth"
|
name = "labrinth"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
#Team members, please add your emails and usernames
|
#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>"]
|
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"
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "3.1.0"
|
actix-web = "3.3.2"
|
||||||
actix-rt = "1.1.1"
|
actix-rt = "1.1.0"
|
||||||
actix-files = "0.4.0"
|
actix-files = "0.5.0"
|
||||||
actix-multipart = "0.3.0"
|
actix-multipart = "0.3.0"
|
||||||
actix-cors = "0.4.1"
|
actix-cors = "0.5.4"
|
||||||
actix-ratelimit = "0.3.0"
|
actix-ratelimit = "0.3.0"
|
||||||
|
|
||||||
meilisearch-sdk = "0.6.0"
|
meilisearch-sdk = "0.6.0"
|
||||||
@@ -35,6 +35,10 @@ base64 = "0.13.0"
|
|||||||
sha1 = { version = "0.6.0", features = ["std"] }
|
sha1 = { version = "0.6.0", features = ["std"] }
|
||||||
sha2 = "0.9.2"
|
sha2 = "0.9.2"
|
||||||
bitflags = "1.2.1"
|
bitflags = "1.2.1"
|
||||||
|
zip = "0.5.12"
|
||||||
|
|
||||||
|
validator = { version = "0.13", features = ["derive"] }
|
||||||
|
regex = "1.5.4"
|
||||||
|
|
||||||
gumdrop = "0.8.0"
|
gumdrop = "0.8.0"
|
||||||
dotenv = "0.15"
|
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 = { version = "0.22.0", features = ["log"] }
|
||||||
sentry-actix = "0.22.0"
|
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
|
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
|
||||||
WORKDIR /usr/src/labrinth
|
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;
|
pub mod models;
|
||||||
mod postgres_database;
|
mod postgres_database;
|
||||||
|
pub use models::Project;
|
||||||
pub use models::Mod;
|
|
||||||
pub use models::Version;
|
pub use models::Version;
|
||||||
pub use postgres_database::check_for_migrations;
|
pub use postgres_database::check_for_migrations;
|
||||||
pub use postgres_database::connect;
|
pub use postgres_database::connect;
|
||||||
|
|||||||
@@ -2,19 +2,30 @@ use super::ids::*;
|
|||||||
use super::DatabaseError;
|
use super::DatabaseError;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
|
||||||
|
pub struct ProjectType {
|
||||||
|
pub id: ProjectTypeId,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Loader {
|
pub struct Loader {
|
||||||
pub id: LoaderId,
|
pub id: LoaderId,
|
||||||
pub loader: String,
|
pub loader: String,
|
||||||
|
pub icon: String,
|
||||||
|
pub supported_project_types: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GameVersion {
|
pub struct GameVersion {
|
||||||
pub id: GameVersionId,
|
pub id: GameVersionId,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
pub version_type: String,
|
||||||
|
pub date: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Category {
|
pub struct Category {
|
||||||
pub id: CategoryId,
|
pub id: CategoryId,
|
||||||
pub category: String,
|
pub category: String,
|
||||||
|
pub project_type: String,
|
||||||
|
pub icon: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ReportType {
|
pub struct ReportType {
|
||||||
@@ -36,11 +47,17 @@ pub struct DonationPlatform {
|
|||||||
|
|
||||||
pub struct CategoryBuilder<'a> {
|
pub struct CategoryBuilder<'a> {
|
||||||
pub name: Option<&'a str>,
|
pub name: Option<&'a str>,
|
||||||
|
pub project_type: Option<&'a ProjectTypeId>,
|
||||||
|
pub icon: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Category {
|
impl Category {
|
||||||
pub fn builder() -> CategoryBuilder<'static> {
|
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>
|
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
|
SELECT id FROM categories
|
||||||
WHERE category = $1
|
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)
|
.fetch_optional(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -84,18 +130,27 @@ impl Category {
|
|||||||
Ok(result.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
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query!(
|
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)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.category)) })
|
.try_filter_map(|e| async {
|
||||||
.try_collect::<Vec<String>>()
|
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?;
|
.await?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -133,24 +188,49 @@ impl<'a> CategoryBuilder<'a> {
|
|||||||
.chars()
|
.chars()
|
||||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||||
{
|
{
|
||||||
Ok(Self { name: Some(name) })
|
Ok(Self {
|
||||||
|
name: Some(name),
|
||||||
|
..self
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
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>
|
pub async fn insert<'b, E>(self, exec: E) -> Result<CategoryId, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
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!(
|
let result = sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO categories (category)
|
INSERT INTO categories (category, project_type, icon)
|
||||||
VALUES ($1)
|
VALUES ($1, $2, $3)
|
||||||
ON CONFLICT (category) DO NOTHING
|
ON CONFLICT (category, project_type, icon) DO NOTHING
|
||||||
RETURNING id
|
RETURNING id
|
||||||
",
|
",
|
||||||
self.name
|
self.name,
|
||||||
|
id as ProjectTypeId,
|
||||||
|
self.icon
|
||||||
)
|
)
|
||||||
.fetch_one(exec)
|
.fetch_one(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -161,11 +241,17 @@ impl<'a> CategoryBuilder<'a> {
|
|||||||
|
|
||||||
pub struct LoaderBuilder<'a> {
|
pub struct LoaderBuilder<'a> {
|
||||||
pub name: Option<&'a str>,
|
pub name: Option<&'a str>,
|
||||||
|
pub icon: Option<&'a str>,
|
||||||
|
pub supported_project_types: Option<&'a [ProjectTypeId]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Loader {
|
impl Loader {
|
||||||
pub fn builder() -> LoaderBuilder<'static> {
|
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>
|
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<LoaderId>, DatabaseError>
|
||||||
@@ -209,24 +295,41 @@ impl Loader {
|
|||||||
Ok(result.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
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query!(
|
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)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.loader)) })
|
.try_filter_map(|e| async {
|
||||||
.try_collect::<Vec<String>>()
|
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?;
|
.await?;
|
||||||
|
|
||||||
Ok(result)
|
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>
|
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
@@ -259,28 +362,74 @@ impl<'a> LoaderBuilder<'a> {
|
|||||||
.chars()
|
.chars()
|
||||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||||
{
|
{
|
||||||
Ok(Self { name: Some(name) })
|
Ok(Self {
|
||||||
|
name: Some(name),
|
||||||
|
..self
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert<'b, E>(self, exec: E) -> Result<LoaderId, DatabaseError>
|
pub fn icon(self, icon: &'a str) -> Result<LoaderBuilder<'a>, DatabaseError> {
|
||||||
where
|
Ok(Self {
|
||||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
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!(
|
let result = sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO loaders (loader)
|
INSERT INTO loaders (loader, icon)
|
||||||
VALUES ($1)
|
VALUES ($1, $2)
|
||||||
ON CONFLICT (loader) DO NOTHING
|
ON CONFLICT (loader, icon) DO NOTHING
|
||||||
RETURNING id
|
RETURNING id
|
||||||
",
|
",
|
||||||
self.name
|
self.name,
|
||||||
|
self.icon
|
||||||
)
|
)
|
||||||
.fetch_one(exec)
|
.fetch_one(&mut *transaction)
|
||||||
.await?;
|
.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))
|
Ok(LoaderId(result.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,19 +490,24 @@ impl GameVersion {
|
|||||||
Ok(result.version)
|
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
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query!(
|
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
|
ORDER BY created DESC
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||||
.try_collect::<Vec<String>>()
|
id: GameVersionId(c.id),
|
||||||
|
version: c.version_,
|
||||||
|
version_type: c.type_,
|
||||||
|
date: c.created
|
||||||
|
})) })
|
||||||
|
.try_collect::<Vec<GameVersion>>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -363,7 +517,7 @@ impl GameVersion {
|
|||||||
version_type_option: Option<&str>,
|
version_type_option: Option<&str>,
|
||||||
major_option: Option<bool>,
|
major_option: Option<bool>,
|
||||||
exec: E,
|
exec: E,
|
||||||
) -> Result<Vec<String>, DatabaseError>
|
) -> Result<Vec<GameVersion>, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
@@ -373,7 +527,7 @@ impl GameVersion {
|
|||||||
if let Some(major) = major_option {
|
if let Some(major) = major_option {
|
||||||
result = sqlx::query!(
|
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
|
WHERE major = $1 AND type = $2
|
||||||
ORDER BY created DESC
|
ORDER BY created DESC
|
||||||
",
|
",
|
||||||
@@ -381,35 +535,50 @@ impl GameVersion {
|
|||||||
version_type
|
version_type
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||||
.try_collect::<Vec<String>>()
|
id: GameVersionId(c.id),
|
||||||
|
version: c.version_,
|
||||||
|
version_type: c.type_,
|
||||||
|
date: c.created
|
||||||
|
})) })
|
||||||
|
.try_collect::<Vec<GameVersion>>()
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
result = sqlx::query!(
|
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
|
WHERE type = $1
|
||||||
ORDER BY created DESC
|
ORDER BY created DESC
|
||||||
",
|
",
|
||||||
version_type
|
version_type
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||||
.try_collect::<Vec<String>>()
|
id: GameVersionId(c.id),
|
||||||
|
version: c.version_,
|
||||||
|
version_type: c.type_,
|
||||||
|
date: c.created
|
||||||
|
})) })
|
||||||
|
.try_collect::<Vec<GameVersion>>()
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
} else if let Some(major) = major_option {
|
} else if let Some(major) = major_option {
|
||||||
result = sqlx::query!(
|
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
|
WHERE major = $1
|
||||||
ORDER BY created DESC
|
ORDER BY created DESC
|
||||||
",
|
",
|
||||||
major
|
major
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
.try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion {
|
||||||
.try_collect::<Vec<String>>()
|
id: GameVersionId(c.id),
|
||||||
|
version: c.version_,
|
||||||
|
version_type: c.type_,
|
||||||
|
date: c.created
|
||||||
|
})) })
|
||||||
|
.try_collect::<Vec<GameVersion>>()
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
result = Vec::new();
|
result = Vec::new();
|
||||||
@@ -867,7 +1036,6 @@ impl ReportType {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove loaders with mods using them
|
|
||||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
@@ -925,3 +1093,156 @@ impl<'a> ReportTypeBuilder<'a> {
|
|||||||
Ok(ReportTypeId(result.id))
|
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!(
|
generate_ids!(
|
||||||
pub generate_mod_id,
|
pub generate_project_id,
|
||||||
ModId,
|
ProjectId,
|
||||||
8,
|
8,
|
||||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
||||||
ModId
|
ProjectId
|
||||||
);
|
);
|
||||||
generate_ids!(
|
generate_ids!(
|
||||||
pub generate_version_id,
|
pub generate_version_id,
|
||||||
@@ -115,7 +115,11 @@ pub struct TeamMemberId(pub i64);
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Type)]
|
#[derive(Copy, Clone, Debug, Type)]
|
||||||
#[sqlx(transparent)]
|
#[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)]
|
#[derive(Copy, Clone, Debug, Type)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
pub struct StatusId(pub i32);
|
pub struct StatusId(pub i32);
|
||||||
@@ -169,14 +173,14 @@ pub struct NotificationActionId(pub i32);
|
|||||||
|
|
||||||
use crate::models::ids;
|
use crate::models::ids;
|
||||||
|
|
||||||
impl From<ids::ModId> for ModId {
|
impl From<ids::ProjectId> for ProjectId {
|
||||||
fn from(id: ids::ModId) -> Self {
|
fn from(id: ids::ProjectId) -> Self {
|
||||||
ModId(id.0 as i64)
|
ProjectId(id.0 as i64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<ModId> for ids::ModId {
|
impl From<ProjectId> for ids::ProjectId {
|
||||||
fn from(id: ModId) -> Self {
|
fn from(id: ProjectId) -> Self {
|
||||||
ids::ModId(id.0 as u64)
|
ids::ProjectId(id.0 as u64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<ids::UserId> for UserId {
|
impl From<ids::UserId> for UserId {
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ use thiserror::Error;
|
|||||||
|
|
||||||
pub mod categories;
|
pub mod categories;
|
||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub mod mod_item;
|
|
||||||
pub mod notification_item;
|
pub mod notification_item;
|
||||||
|
pub mod project_item;
|
||||||
pub mod report_item;
|
pub mod report_item;
|
||||||
pub mod team_item;
|
pub mod team_item;
|
||||||
pub mod user_item;
|
pub mod user_item;
|
||||||
pub mod version_item;
|
pub mod version_item;
|
||||||
|
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use mod_item::Mod;
|
pub use project_item::Project;
|
||||||
pub use team_item::Team;
|
pub use team_item::Team;
|
||||||
pub use team_item::TeamMember;
|
pub use team_item::TeamMember;
|
||||||
pub use user_item::User;
|
pub use user_item::User;
|
||||||
@@ -62,7 +62,7 @@ impl ids::ChannelId {
|
|||||||
|
|
||||||
impl ids::StatusId {
|
impl ids::StatusId {
|
||||||
pub async fn get_id<'a, E>(
|
pub async fn get_id<'a, E>(
|
||||||
status: &crate::models::mods::ModStatus,
|
status: &crate::models::projects::ProjectStatus,
|
||||||
exec: E,
|
exec: E,
|
||||||
) -> Result<Option<Self>, DatabaseError>
|
) -> Result<Option<Self>, DatabaseError>
|
||||||
where
|
where
|
||||||
@@ -84,7 +84,7 @@ impl ids::StatusId {
|
|||||||
|
|
||||||
impl ids::SideTypeId {
|
impl ids::SideTypeId {
|
||||||
pub async fn get_id<'a, E>(
|
pub async fn get_id<'a, E>(
|
||||||
side: &crate::models::mods::SideType,
|
side: &crate::models::projects::SideType,
|
||||||
exec: E,
|
exec: E,
|
||||||
) -> Result<Option<Self>, DatabaseError>
|
) -> Result<Option<Self>, DatabaseError>
|
||||||
where
|
where
|
||||||
@@ -122,3 +122,22 @@ impl ids::DonationPlatformId {
|
|||||||
Ok(result.map(|r| ids::DonationPlatformId(r.id)))
|
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
|
FROM notifications n
|
||||||
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
|
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
|
||||||
WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
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
|
¬ification_ids_parsed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
use super::ids::*;
|
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 struct DonationUrl {
|
||||||
pub mod_id: ModId,
|
pub project_id: ProjectId,
|
||||||
pub platform_id: DonationPlatformId,
|
pub platform_id: DonationPlatformId,
|
||||||
pub platform_short: String,
|
pub platform_short: String,
|
||||||
pub platform_name: String,
|
pub platform_name: String,
|
||||||
@@ -22,7 +26,7 @@ impl DonationUrl {
|
|||||||
$1, $2, $3
|
$1, $2, $3
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.mod_id as ModId,
|
self.project_id as ProjectId,
|
||||||
self.platform_id as DonationPlatformId,
|
self.platform_id as DonationPlatformId,
|
||||||
self.url,
|
self.url,
|
||||||
)
|
)
|
||||||
@@ -33,8 +37,9 @@ impl DonationUrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ModBuilder {
|
pub struct ProjectBuilder {
|
||||||
pub mod_id: ModId,
|
pub project_id: ProjectId,
|
||||||
|
pub project_type_id: ProjectTypeId,
|
||||||
pub team_id: TeamId,
|
pub team_id: TeamId,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
@@ -55,13 +60,14 @@ pub struct ModBuilder {
|
|||||||
pub donation_urls: Vec<DonationUrl>,
|
pub donation_urls: Vec<DonationUrl>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModBuilder {
|
impl ProjectBuilder {
|
||||||
pub async fn insert(
|
pub async fn insert(
|
||||||
self,
|
self,
|
||||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
) -> Result<ModId, super::DatabaseError> {
|
) -> Result<ProjectId, super::DatabaseError> {
|
||||||
let mod_struct = Mod {
|
let project_struct = Project {
|
||||||
id: self.mod_id,
|
id: self.project_id,
|
||||||
|
project_type: self.project_type_id,
|
||||||
team_id: self.team_id,
|
team_id: self.team_id,
|
||||||
title: self.title,
|
title: self.title,
|
||||||
description: self.description,
|
description: self.description,
|
||||||
@@ -83,15 +89,15 @@ impl ModBuilder {
|
|||||||
license: self.license,
|
license: self.license,
|
||||||
slug: self.slug,
|
slug: self.slug,
|
||||||
};
|
};
|
||||||
mod_struct.insert(&mut *transaction).await?;
|
project_struct.insert(&mut *transaction).await?;
|
||||||
|
|
||||||
for mut version in self.initial_versions {
|
for mut version in self.initial_versions {
|
||||||
version.mod_id = self.mod_id;
|
version.project_id = self.project_id;
|
||||||
version.insert(&mut *transaction).await?;
|
version.insert(&mut *transaction).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for mut donation in self.donation_urls {
|
for mut donation in self.donation_urls {
|
||||||
donation.mod_id = self.mod_id;
|
donation.project_id = self.project_id;
|
||||||
donation.insert(&mut *transaction).await?;
|
donation.insert(&mut *transaction).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,19 +107,20 @@ impl ModBuilder {
|
|||||||
INSERT INTO mods_categories (joining_mod_id, joining_category_id)
|
INSERT INTO mods_categories (joining_mod_id, joining_category_id)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2)
|
||||||
",
|
",
|
||||||
self.mod_id as ModId,
|
self.project_id as ProjectId,
|
||||||
category as CategoryId,
|
category as CategoryId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(self.mod_id)
|
Ok(self.project_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct Mod {
|
pub struct Project {
|
||||||
pub id: ModId,
|
pub id: ProjectId,
|
||||||
|
pub project_type: ProjectTypeId,
|
||||||
pub team_id: TeamId,
|
pub team_id: TeamId,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
@@ -136,7 +143,7 @@ pub struct Mod {
|
|||||||
pub slug: Option<String>,
|
pub slug: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mod {
|
impl Project {
|
||||||
pub async fn insert(
|
pub async fn insert(
|
||||||
&self,
|
&self,
|
||||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
@@ -148,17 +155,17 @@ impl Mod {
|
|||||||
published, downloads, icon_url, issues_url,
|
published, downloads, icon_url, issues_url,
|
||||||
source_url, wiki_url, status, discord_url,
|
source_url, wiki_url, status, discord_url,
|
||||||
client_side, server_side, license_url, license,
|
client_side, server_side, license_url, license,
|
||||||
slug
|
slug, project_type
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6, $7, $8, $9,
|
$6, $7, $8, $9,
|
||||||
$10, $11, $12, $13,
|
$10, $11, $12, $13,
|
||||||
$14, $15, $16, $17,
|
$14, $15, $16, $17,
|
||||||
LOWER($18)
|
LOWER($18), $19
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as ModId,
|
self.id as ProjectId,
|
||||||
self.team_id as TeamId,
|
self.team_id as TeamId,
|
||||||
&self.title,
|
&self.title,
|
||||||
&self.description,
|
&self.description,
|
||||||
@@ -175,7 +182,8 @@ impl Mod {
|
|||||||
self.server_side as SideTypeId,
|
self.server_side as SideTypeId,
|
||||||
self.license_url.as_ref(),
|
self.license_url.as_ref(),
|
||||||
self.license as LicenseId,
|
self.license as LicenseId,
|
||||||
self.slug.as_ref()
|
self.slug.as_ref(),
|
||||||
|
self.project_type as ProjectTypeId
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -183,13 +191,16 @@ impl Mod {
|
|||||||
Ok(())
|
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
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT title, description, downloads, follows,
|
SELECT project_type, title, description, downloads, follows,
|
||||||
icon_url, body, body_url, published,
|
icon_url, body, body_url, published,
|
||||||
updated, status,
|
updated, status,
|
||||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||||
@@ -197,14 +208,15 @@ impl Mod {
|
|||||||
FROM mods
|
FROM mods
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
)
|
)
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(row) = result {
|
if let Some(row) = result {
|
||||||
Ok(Some(Mod {
|
Ok(Some(Project {
|
||||||
id,
|
id,
|
||||||
|
project_type: ProjectTypeId(row.project_type),
|
||||||
team_id: TeamId(row.team_id),
|
team_id: TeamId(row.team_id),
|
||||||
title: row.title,
|
title: row.title,
|
||||||
description: row.description,
|
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
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::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();
|
||||||
let mods = sqlx::query!(
|
let projects = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT id, title, description, downloads, follows,
|
SELECT id, project_type, title, description, downloads, follows,
|
||||||
icon_url, body, body_url, published,
|
icon_url, body, body_url, published,
|
||||||
updated, status,
|
updated, status,
|
||||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||||
@@ -248,12 +263,13 @@ impl Mod {
|
|||||||
FROM mods
|
FROM mods
|
||||||
WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))
|
WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||||
",
|
",
|
||||||
&mod_ids_parsed
|
&project_ids_parsed
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async {
|
.try_filter_map(|e| async {
|
||||||
Ok(e.right().map(|m| Mod {
|
Ok(e.right().map(|m| Project {
|
||||||
id: ModId(m.id),
|
id: ProjectId(m.id),
|
||||||
|
project_type: ProjectTypeId(m.project_type),
|
||||||
team_id: TeamId(m.team_id),
|
team_id: TeamId(m.team_id),
|
||||||
title: m.title,
|
title: m.title,
|
||||||
description: m.description,
|
description: m.description,
|
||||||
@@ -276,14 +292,14 @@ impl Mod {
|
|||||||
follows: m.follows,
|
follows: m.follows,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<Mod>>()
|
.try_collect::<Vec<Project>>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(mods)
|
Ok(projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_full<'a, 'b, E>(
|
pub async fn remove_full<'a, 'b, E>(
|
||||||
id: ModId,
|
id: ProjectId,
|
||||||
exec: E,
|
exec: E,
|
||||||
) -> Result<Option<()>, sqlx::error::Error>
|
) -> Result<Option<()>, sqlx::error::Error>
|
||||||
where
|
where
|
||||||
@@ -293,7 +309,7 @@ impl Mod {
|
|||||||
"
|
"
|
||||||
SELECT team_id FROM mods WHERE id = $1
|
SELECT team_id FROM mods WHERE id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
)
|
)
|
||||||
.fetch_optional(exec)
|
.fetch_optional(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -309,7 +325,7 @@ impl Mod {
|
|||||||
DELETE FROM mod_follows
|
DELETE FROM mod_follows
|
||||||
WHERE mod_id = $1
|
WHERE mod_id = $1
|
||||||
",
|
",
|
||||||
id as ModId
|
id as ProjectId
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -319,7 +335,7 @@ impl Mod {
|
|||||||
DELETE FROM mod_follows
|
DELETE FROM mod_follows
|
||||||
WHERE mod_id = $1
|
WHERE mod_id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -329,7 +345,7 @@ impl Mod {
|
|||||||
DELETE FROM reports
|
DELETE FROM reports
|
||||||
WHERE mod_id = $1
|
WHERE mod_id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -339,7 +355,7 @@ impl Mod {
|
|||||||
DELETE FROM mods_categories
|
DELETE FROM mods_categories
|
||||||
WHERE joining_mod_id = $1
|
WHERE joining_mod_id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -349,7 +365,7 @@ impl Mod {
|
|||||||
DELETE FROM mods_donations
|
DELETE FROM mods_donations
|
||||||
WHERE joining_mod_id = $1
|
WHERE joining_mod_id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -360,7 +376,7 @@ impl Mod {
|
|||||||
SELECT id FROM versions
|
SELECT id FROM versions
|
||||||
WHERE mod_id = $1
|
WHERE mod_id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) })
|
.try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) })
|
||||||
@@ -376,7 +392,7 @@ impl Mod {
|
|||||||
DELETE FROM mods
|
DELETE FROM mods
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -407,7 +423,7 @@ impl Mod {
|
|||||||
pub async fn get_full_from_slug<'a, 'b, E>(
|
pub async fn get_full_from_slug<'a, 'b, E>(
|
||||||
slug: &str,
|
slug: &str,
|
||||||
executor: E,
|
executor: E,
|
||||||
) -> Result<Option<QueryMod>, sqlx::error::Error>
|
) -> Result<Option<QueryProject>, sqlx::error::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
@@ -421,49 +437,154 @@ impl Mod {
|
|||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(mod_id) = id {
|
if let Some(project_id) = id {
|
||||||
Mod::get_full(ModId(mod_id.id), executor).await
|
Project::get_full(ProjectId(project_id.id), executor).await
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_full<'a, 'b, E>(
|
pub async fn get_from_slug<'a, 'b, E>(
|
||||||
id: ModId,
|
slug: &str,
|
||||||
executor: E,
|
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
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
let result = sqlx::query!(
|
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.icon_url icon_url, m.body body, m.body_url body_url, m.published published,
|
||||||
m.updated updated, m.status status,
|
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.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
|
||||||
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,
|
m.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
|
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions
|
||||||
FROM mods m
|
FROM mods m
|
||||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
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 categories c ON mc.joining_category_id = c.id
|
||||||
LEFT OUTER JOIN versions v ON v.mod_id = m.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 statuses s ON s.id = m.status
|
||||||
INNER JOIN side_types cs ON m.client_side = cs.id
|
INNER JOIN side_types cs ON m.client_side = cs.id
|
||||||
INNER JOIN side_types ss ON m.server_side = ss.id
|
INNER JOIN side_types ss ON m.server_side = ss.id
|
||||||
INNER JOIN licenses l ON m.license = l.id
|
INNER JOIN licenses l ON m.license = l.id
|
||||||
WHERE m.id = $1
|
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)
|
.fetch_optional(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(m) = result {
|
if let Some(m) = result {
|
||||||
Ok(Some(QueryMod {
|
Ok(Some(QueryProject {
|
||||||
inner: Mod {
|
inner: Project {
|
||||||
id: ModId(m.id),
|
id: ProjectId(m.id),
|
||||||
|
project_type: ProjectTypeId(m.project_type),
|
||||||
team_id: TeamId(m.team_id),
|
team_id: TeamId(m.team_id),
|
||||||
title: m.title.clone(),
|
title: m.title.clone(),
|
||||||
description: m.description.clone(),
|
description: m.description.clone(),
|
||||||
@@ -485,6 +606,7 @@ impl Mod {
|
|||||||
body: m.body.clone(),
|
body: m.body.clone(),
|
||||||
follows: m.follows,
|
follows: m.follows,
|
||||||
},
|
},
|
||||||
|
project_type: m.project_type_name,
|
||||||
categories: m
|
categories: m
|
||||||
.categories
|
.categories
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -498,11 +620,11 @@ impl Mod {
|
|||||||
.map(|x| VersionId(x.parse().unwrap_or_default()))
|
.map(|x| VersionId(x.parse().unwrap_or_default()))
|
||||||
.collect(),
|
.collect(),
|
||||||
donation_urls: vec![],
|
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_id: m.short,
|
||||||
license_name: m.license_name,
|
license_name: m.license_name,
|
||||||
client_side: crate::models::mods::SideType::from_str(&m.client_side_type),
|
client_side: crate::models::projects::SideType::from_str(&m.client_side_type),
|
||||||
server_side: crate::models::mods::SideType::from_str(&m.server_side_type),
|
server_side: crate::models::projects::SideType::from_str(&m.server_side_type),
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -510,42 +632,44 @@ impl Mod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_many_full<'a, E>(
|
pub async fn get_many_full<'a, E>(
|
||||||
mod_ids: Vec<ModId>,
|
project_ids: Vec<ProjectId>,
|
||||||
exec: E,
|
exec: E,
|
||||||
) -> Result<Vec<QueryMod>, sqlx::Error>
|
) -> Result<Vec<QueryProject>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
use futures::TryStreamExt;
|
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!(
|
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.icon_url icon_url, m.body body, m.body_url body_url, m.published published,
|
||||||
m.updated updated, m.status status,
|
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.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
|
||||||
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,
|
m.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
|
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions
|
||||||
FROM mods m
|
FROM mods m
|
||||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
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 categories c ON mc.joining_category_id = c.id
|
||||||
LEFT OUTER JOIN versions v ON v.mod_id = m.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 statuses s ON s.id = m.status
|
||||||
INNER JOIN side_types cs ON m.client_side = cs.id
|
INNER JOIN side_types cs ON m.client_side = cs.id
|
||||||
INNER JOIN side_types ss ON m.server_side = ss.id
|
INNER JOIN side_types ss ON m.server_side = ss.id
|
||||||
INNER JOIN licenses l ON m.license = l.id
|
INNER JOIN licenses l ON m.license = l.id
|
||||||
WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
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)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async {
|
.try_filter_map(|e| async {
|
||||||
Ok(e.right().map(|m| QueryMod {
|
Ok(e.right().map(|m| QueryProject {
|
||||||
inner: Mod {
|
inner: Project {
|
||||||
id: ModId(m.id),
|
id: ProjectId(m.id),
|
||||||
|
project_type: ProjectTypeId(m.project_type),
|
||||||
team_id: TeamId(m.team_id),
|
team_id: TeamId(m.team_id),
|
||||||
title: m.title.clone(),
|
title: m.title.clone(),
|
||||||
description: m.description.clone(),
|
description: m.description.clone(),
|
||||||
@@ -567,30 +691,31 @@ impl Mod {
|
|||||||
body: m.body.clone(),
|
body: m.body.clone(),
|
||||||
follows: m.follows
|
follows: m.follows
|
||||||
},
|
},
|
||||||
|
project_type: m.project_type_name,
|
||||||
categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(),
|
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(),
|
versions: m.versions.unwrap_or_default().split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect(),
|
||||||
donation_urls: vec![],
|
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_id: m.short,
|
||||||
license_name: m.license_name,
|
license_name: m.license_name,
|
||||||
client_side: crate::models::mods::SideType::from_str(&m.client_side_type),
|
client_side: crate::models::projects::SideType::from_str(&m.client_side_type),
|
||||||
server_side: crate::models::mods::SideType::from_str(&m.server_side_type),
|
server_side: crate::models::projects::SideType::from_str(&m.server_side_type),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<QueryMod>>()
|
.try_collect::<Vec<QueryProject>>()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct QueryMod {
|
pub struct QueryProject {
|
||||||
pub inner: Mod,
|
pub inner: Project,
|
||||||
|
pub project_type: String,
|
||||||
pub categories: Vec<String>,
|
pub categories: Vec<String>,
|
||||||
pub versions: Vec<VersionId>,
|
pub versions: Vec<VersionId>,
|
||||||
pub donation_urls: Vec<DonationUrl>,
|
pub donation_urls: Vec<DonationUrl>,
|
||||||
pub status: crate::models::mods::ModStatus,
|
pub status: crate::models::projects::ProjectStatus,
|
||||||
pub license_id: String,
|
pub license_id: String,
|
||||||
pub license_name: String,
|
pub license_name: String,
|
||||||
pub client_side: crate::models::mods::SideType,
|
pub client_side: crate::models::projects::SideType,
|
||||||
pub server_side: crate::models::mods::SideType,
|
pub server_side: crate::models::projects::SideType,
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ use super::ids::*;
|
|||||||
pub struct Report {
|
pub struct Report {
|
||||||
pub id: ReportId,
|
pub id: ReportId,
|
||||||
pub report_type_id: ReportTypeId,
|
pub report_type_id: ReportTypeId,
|
||||||
pub mod_id: Option<ModId>,
|
pub project_id: Option<ProjectId>,
|
||||||
pub version_id: Option<VersionId>,
|
pub version_id: Option<VersionId>,
|
||||||
pub user_id: Option<UserId>,
|
pub user_id: Option<UserId>,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
@@ -14,7 +14,7 @@ pub struct Report {
|
|||||||
pub struct QueryReport {
|
pub struct QueryReport {
|
||||||
pub id: ReportId,
|
pub id: ReportId,
|
||||||
pub report_type: String,
|
pub report_type: String,
|
||||||
pub mod_id: Option<ModId>,
|
pub project_id: Option<ProjectId>,
|
||||||
pub version_id: Option<VersionId>,
|
pub version_id: Option<VersionId>,
|
||||||
pub user_id: Option<UserId>,
|
pub user_id: Option<UserId>,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
@@ -40,7 +40,7 @@ impl Report {
|
|||||||
",
|
",
|
||||||
self.id as ReportId,
|
self.id as ReportId,
|
||||||
self.report_type_id as ReportTypeId,
|
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.version_id.map(|x| x.0 as i64),
|
||||||
self.user_id.map(|x| x.0 as i64),
|
self.user_id.map(|x| x.0 as i64),
|
||||||
self.body,
|
self.body,
|
||||||
@@ -72,7 +72,7 @@ impl Report {
|
|||||||
Ok(Some(QueryReport {
|
Ok(Some(QueryReport {
|
||||||
id,
|
id,
|
||||||
report_type: row.name,
|
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),
|
version_id: row.version_id.map(VersionId),
|
||||||
user_id: row.user_id.map(UserId),
|
user_id: row.user_id.map(UserId),
|
||||||
body: row.body,
|
body: row.body,
|
||||||
@@ -108,7 +108,7 @@ impl Report {
|
|||||||
Ok(e.right().map(|row| QueryReport {
|
Ok(e.right().map(|row| QueryReport {
|
||||||
id: ReportId(row.id),
|
id: ReportId(row.id),
|
||||||
report_type: row.name,
|
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),
|
version_id: row.version_id.map(VersionId),
|
||||||
user_id: row.user_id.map(UserId),
|
user_id: row.user_id.map(UserId),
|
||||||
body: row.body,
|
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 {
|
pub struct Team {
|
||||||
/// The id of the team
|
/// The id of the team
|
||||||
pub id: TeamId,
|
pub id: TeamId,
|
||||||
@@ -412,8 +412,8 @@ impl TeamMember {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_from_user_id_mod<'a, 'b, E>(
|
pub async fn get_from_user_id_project<'a, 'b, E>(
|
||||||
id: ModId,
|
id: ProjectId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
executor: E,
|
executor: E,
|
||||||
) -> Result<Option<Self>, super::DatabaseError>
|
) -> 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
|
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE
|
||||||
WHERE m.id = $1
|
WHERE m.id = $1
|
||||||
",
|
",
|
||||||
id as ModId,
|
id as ProjectId,
|
||||||
user_id as UserId
|
user_id as UserId
|
||||||
)
|
)
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::ids::{ModId, UserId};
|
use super::ids::{ProjectId, UserId};
|
||||||
|
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
@@ -24,7 +24,7 @@ impl User {
|
|||||||
avatar_url, bio, created
|
avatar_url, bio, created
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, LOWER($3), $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6, $7, $8
|
$6, $7, $8
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
@@ -186,17 +186,17 @@ impl User {
|
|||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_mods<'a, E>(
|
pub async fn get_projects<'a, E>(
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
status: &str,
|
status: &str,
|
||||||
exec: E,
|
exec: E,
|
||||||
) -> Result<Vec<ModId>, sqlx::Error>
|
) -> Result<Vec<ProjectId>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
|
|
||||||
let mods = sqlx::query!(
|
let projects = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT m.id FROM mods m
|
SELECT m.id FROM mods m
|
||||||
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
||||||
@@ -206,23 +206,23 @@ impl User {
|
|||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) })
|
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) })
|
||||||
.try_collect::<Vec<ModId>>()
|
.try_collect::<Vec<ProjectId>>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(mods)
|
Ok(projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_mods_private<'a, E>(
|
pub async fn get_projects_private<'a, E>(
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
exec: E,
|
exec: E,
|
||||||
) -> Result<Vec<ModId>, sqlx::Error>
|
) -> Result<Vec<ProjectId>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
|
|
||||||
let mods = sqlx::query!(
|
let projects = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT m.id FROM mods m
|
SELECT m.id FROM mods m
|
||||||
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
||||||
@@ -231,11 +231,11 @@ impl User {
|
|||||||
user_id as UserId,
|
user_id as UserId,
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) })
|
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) })
|
||||||
.try_collect::<Vec<ModId>>()
|
.try_collect::<Vec<ProjectId>>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(mods)
|
Ok(projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove<'a, 'b, E>(id: UserId, exec: E) -> Result<Option<()>, sqlx::error::Error>
|
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,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
let mods: Vec<ModId> = sqlx::query!(
|
let projects: Vec<ProjectId> = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT m.id FROM mods m
|
SELECT m.id FROM mods m
|
||||||
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
INNER JOIN team_members tm ON tm.team_id = m.team_id
|
||||||
@@ -363,12 +363,12 @@ impl User {
|
|||||||
crate::models::teams::OWNER_ROLE
|
crate::models::teams::OWNER_ROLE
|
||||||
)
|
)
|
||||||
.fetch_many(exec)
|
.fetch_many(exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) })
|
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) })
|
||||||
.try_collect::<Vec<ModId>>()
|
.try_collect::<Vec<ProjectId>>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for mod_id in mods {
|
for project_id in projects {
|
||||||
let _result = super::mod_item::Mod::remove_full(mod_id, exec).await?;
|
let _result = super::project_item::Project::remove_full(project_id, exec).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let notifications: Vec<i64> = sqlx::query!(
|
let notifications: Vec<i64> = sqlx::query!(
|
||||||
@@ -439,4 +439,56 @@ impl User {
|
|||||||
|
|
||||||
Ok(Some(()))
|
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 struct VersionBuilder {
|
||||||
pub version_id: VersionId,
|
pub version_id: VersionId,
|
||||||
pub mod_id: ModId,
|
pub project_id: ProjectId,
|
||||||
pub author_id: UserId,
|
pub author_id: UserId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version_number: String,
|
pub version_number: String,
|
||||||
@@ -75,7 +75,7 @@ impl VersionBuilder {
|
|||||||
) -> Result<VersionId, DatabaseError> {
|
) -> Result<VersionId, DatabaseError> {
|
||||||
let version = Version {
|
let version = Version {
|
||||||
id: self.version_id,
|
id: self.version_id,
|
||||||
mod_id: self.mod_id,
|
project_id: self.project_id,
|
||||||
author_id: self.author_id,
|
author_id: self.author_id,
|
||||||
name: self.name,
|
name: self.name,
|
||||||
version_number: self.version_number,
|
version_number: self.version_number,
|
||||||
@@ -95,7 +95,7 @@ impl VersionBuilder {
|
|||||||
SET updated = NOW()
|
SET updated = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
self.mod_id as ModId,
|
self.project_id as ProjectId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -150,7 +150,7 @@ impl VersionBuilder {
|
|||||||
|
|
||||||
pub struct Version {
|
pub struct Version {
|
||||||
pub id: VersionId,
|
pub id: VersionId,
|
||||||
pub mod_id: ModId,
|
pub project_id: ProjectId,
|
||||||
pub author_id: UserId,
|
pub author_id: UserId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version_number: String,
|
pub version_number: String,
|
||||||
@@ -182,7 +182,7 @@ impl Version {
|
|||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as VersionId,
|
self.id as VersionId,
|
||||||
self.mod_id as ModId,
|
self.project_id as ProjectId,
|
||||||
self.author_id as UserId,
|
self.author_id as UserId,
|
||||||
&self.name,
|
&self.name,
|
||||||
&self.version_number,
|
&self.version_number,
|
||||||
@@ -359,8 +359,8 @@ impl Version {
|
|||||||
Ok(vec)
|
Ok(vec)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_mod_versions<'a, E>(
|
pub async fn get_project_versions<'a, E>(
|
||||||
mod_id: ModId,
|
project_id: ProjectId,
|
||||||
game_versions: Option<Vec<String>>,
|
game_versions: Option<Vec<String>>,
|
||||||
loaders: Option<Vec<String>>,
|
loaders: Option<Vec<String>>,
|
||||||
exec: E,
|
exec: E,
|
||||||
@@ -382,7 +382,7 @@ impl Version {
|
|||||||
) AS version
|
) AS version
|
||||||
ORDER BY version.date_published ASC
|
ORDER BY version.date_published ASC
|
||||||
",
|
",
|
||||||
mod_id as ModId,
|
project_id as ProjectId,
|
||||||
&game_versions.unwrap_or_default(),
|
&game_versions.unwrap_or_default(),
|
||||||
&loaders.unwrap_or_default(),
|
&loaders.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
@@ -417,7 +417,7 @@ impl Version {
|
|||||||
if let Some(row) = result {
|
if let Some(row) = result {
|
||||||
Ok(Some(Version {
|
Ok(Some(Version {
|
||||||
id,
|
id,
|
||||||
mod_id: ModId(row.mod_id),
|
project_id: ProjectId(row.mod_id),
|
||||||
author_id: UserId(row.author_id),
|
author_id: UserId(row.author_id),
|
||||||
name: row.name,
|
name: row.name,
|
||||||
version_number: row.version_number,
|
version_number: row.version_number,
|
||||||
@@ -450,6 +450,7 @@ impl Version {
|
|||||||
v.release_channel, v.featured
|
v.release_channel, v.featured
|
||||||
FROM versions v
|
FROM versions v
|
||||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||||
|
ORDER BY v.date_published ASC
|
||||||
",
|
",
|
||||||
&version_ids_parsed
|
&version_ids_parsed
|
||||||
)
|
)
|
||||||
@@ -457,7 +458,7 @@ impl Version {
|
|||||||
.try_filter_map(|e| async {
|
.try_filter_map(|e| async {
|
||||||
Ok(e.right().map(|v| Version {
|
Ok(e.right().map(|v| Version {
|
||||||
id: VersionId(v.id),
|
id: VersionId(v.id),
|
||||||
mod_id: ModId(v.mod_id),
|
project_id: ProjectId(v.mod_id),
|
||||||
author_id: UserId(v.author_id),
|
author_id: UserId(v.author_id),
|
||||||
name: v.name,
|
name: v.name,
|
||||||
version_number: v.version_number,
|
version_number: v.version_number,
|
||||||
@@ -480,7 +481,7 @@ impl Version {
|
|||||||
executor: E,
|
executor: E,
|
||||||
) -> Result<Option<QueryVersion>, sqlx::error::Error>
|
) -> Result<Option<QueryVersion>, sqlx::error::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -566,7 +567,7 @@ impl Version {
|
|||||||
|
|
||||||
Ok(Some(QueryVersion {
|
Ok(Some(QueryVersion {
|
||||||
id: VersionId(v.id),
|
id: VersionId(v.id),
|
||||||
mod_id: ModId(v.mod_id),
|
project_id: ProjectId(v.mod_id),
|
||||||
author_id: UserId(v.author_id),
|
author_id: UserId(v.author_id),
|
||||||
name: v.version_name,
|
name: v.version_name,
|
||||||
version_number: v.version_number,
|
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 hashes h on f.id = h.file_id
|
||||||
LEFT OUTER JOIN dependencies d on v.id = d.dependent_id
|
LEFT OUTER JOIN dependencies d on v.id = d.dependent_id
|
||||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
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
|
&version_ids_parsed
|
||||||
)
|
)
|
||||||
@@ -683,7 +685,7 @@ impl Version {
|
|||||||
|
|
||||||
QueryVersion {
|
QueryVersion {
|
||||||
id: VersionId(v.id),
|
id: VersionId(v.id),
|
||||||
mod_id: ModId(v.mod_id),
|
project_id: ProjectId(v.mod_id),
|
||||||
author_id: UserId(v.author_id),
|
author_id: UserId(v.author_id),
|
||||||
name: v.version_name,
|
name: v.version_name,
|
||||||
version_number: v.version_number,
|
version_number: v.version_number,
|
||||||
@@ -727,7 +729,7 @@ pub struct FileHash {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct QueryVersion {
|
pub struct QueryVersion {
|
||||||
pub id: VersionId,
|
pub id: VersionId,
|
||||||
pub mod_id: ModId,
|
pub project_id: ProjectId,
|
||||||
pub author_id: UserId,
|
pub author_id: UserId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version_number: 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 gumdrop::Options;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use search::indexing::index_mods;
|
use search::indexing::index_projects;
|
||||||
use search::indexing::IndexingSettings;
|
use search::indexing::IndexingSettings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ mod models;
|
|||||||
mod routes;
|
mod routes;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod search;
|
mod search;
|
||||||
|
mod validate;
|
||||||
|
|
||||||
#[derive(Debug, Options)]
|
#[derive(Debug, Options)]
|
||||||
struct Config {
|
struct Config {
|
||||||
@@ -158,13 +159,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info!("Indexing local database");
|
info!("Indexing local database");
|
||||||
let settings = IndexingSettings {
|
let settings = IndexingSettings { index_local: true };
|
||||||
index_local: true,
|
let result = index_projects(pool_ref, settings, &thread_search_config).await;
|
||||||
index_external: false,
|
|
||||||
};
|
|
||||||
let result = index_mods(pool_ref, settings, &thread_search_config).await;
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
warn!("Local mod indexing failed: {:?}", e);
|
warn!("Local project indexing failed: {:?}", e);
|
||||||
}
|
}
|
||||||
info!("Done indexing local database");
|
info!("Done indexing local database");
|
||||||
}
|
}
|
||||||
@@ -229,12 +227,12 @@ async fn main() -> std::io::Result<()> {
|
|||||||
if local_skip {
|
if local_skip {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info!("Indexing created mod queue");
|
info!("Indexing created project queue");
|
||||||
let result = search::indexing::queue::index_queue(&*queue, &thread_search_config).await;
|
let result = search::indexing::queue::index_queue(&*queue, &thread_search_config).await;
|
||||||
if let Err(e) = result {
|
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 || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(
|
.wrap(
|
||||||
Cors::new()
|
Cors::default()
|
||||||
.allowed_methods(vec!["GET", "POST", "DELETE", "PATCH", "PUT"])
|
.allowed_methods(vec!["GET", "POST", "DELETE", "PATCH", "PUT"])
|
||||||
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
||||||
.allowed_header(http::header::CONTENT_TYPE)
|
.allowed_header(http::header::CONTENT_TYPE)
|
||||||
.send_wildcard()
|
.allow_any_origin()
|
||||||
.max_age(3600)
|
.max_age(3600),
|
||||||
.finish(),
|
|
||||||
)
|
)
|
||||||
.wrap(
|
.wrap(
|
||||||
// This is a hacky workaround to allowing the frontend server-side renderer to have
|
// 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(indexing_queue.clone())
|
||||||
.data(search_config.clone())
|
.data(search_config.clone())
|
||||||
.data(ip_salt.clone())
|
.data(ip_salt.clone())
|
||||||
|
.configure(routes::v1_config)
|
||||||
|
.configure(routes::v2_config)
|
||||||
.service(routes::index_get)
|
.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))
|
.service(web::scope("/maven/").configure(routes::maven_config))
|
||||||
.default_service(web::get().to(routes::not_found))
|
.default_service(web::get().to(routes::not_found))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use super::mods::{ModId, VersionId};
|
|
||||||
pub use super::notifications::NotificationId;
|
pub use super::notifications::NotificationId;
|
||||||
|
pub use super::projects::{ProjectId, VersionId};
|
||||||
pub use super::reports::ReportId;
|
pub use super::reports::ReportId;
|
||||||
pub use super::teams::TeamId;
|
pub use super::teams::TeamId;
|
||||||
pub use super::users::UserId;
|
pub use super::users::UserId;
|
||||||
@@ -105,7 +105,7 @@ macro_rules! base62_id_impl {
|
|||||||
impl_base62_display!($struct);
|
impl_base62_display!($struct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
base62_id_impl!(ModId, ModId);
|
base62_id_impl!(ProjectId, ProjectId);
|
||||||
base62_id_impl!(UserId, UserId);
|
base62_id_impl!(UserId, UserId);
|
||||||
base62_id_impl!(VersionId, VersionId);
|
base62_id_impl!(VersionId, VersionId);
|
||||||
base62_id_impl!(TeamId, TeamId);
|
base62_id_impl!(TeamId, TeamId);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub mod mods;
|
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
|
pub mod projects;
|
||||||
pub mod reports;
|
pub mod reports;
|
||||||
pub mod teams;
|
pub mod teams;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|||||||
@@ -3,74 +3,77 @@ use super::teams::TeamId;
|
|||||||
use super::users::UserId;
|
use super::users::UserId;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(from = "Base62Id")]
|
#[serde(from = "Base62Id")]
|
||||||
#[serde(into = "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)]
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(from = "Base62Id")]
|
#[serde(from = "Base62Id")]
|
||||||
#[serde(into = "Base62Id")]
|
#[serde(into = "Base62Id")]
|
||||||
pub struct VersionId(pub u64);
|
pub struct VersionId(pub u64);
|
||||||
|
|
||||||
/// A mod returned from the API
|
/// A project returned from the API
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Mod {
|
pub struct Project {
|
||||||
/// The ID of the mod, encoded as a base62 string.
|
/// The ID of the project, encoded as a base62 string.
|
||||||
pub id: ModId,
|
pub id: ProjectId,
|
||||||
/// The slug of a mod, used for vanity URLs
|
/// The slug of a project, used for vanity URLs
|
||||||
pub slug: Option<String>,
|
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,
|
pub team: TeamId,
|
||||||
/// The title or name of the mod.
|
/// The title or name of the project.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// A short description of the mod.
|
/// A short description of the project.
|
||||||
pub description: String,
|
pub description: String,
|
||||||
/// A long form description of the mod.
|
/// A long form description of the project.
|
||||||
pub body: String,
|
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>,
|
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>,
|
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>,
|
pub updated: DateTime<Utc>,
|
||||||
/// The status of the mod
|
/// The status of the project
|
||||||
pub status: ModStatus,
|
pub status: ProjectStatus,
|
||||||
/// The license of this mod
|
/// The license of this project
|
||||||
pub license: License,
|
pub license: License,
|
||||||
|
|
||||||
/// The support range for the client mod
|
/// The support range for the client project*
|
||||||
pub client_side: SideType,
|
pub client_side: SideType,
|
||||||
/// The support range for the server mod
|
/// The support range for the server project
|
||||||
pub server_side: SideType,
|
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,
|
pub downloads: u32,
|
||||||
/// The total number of followers this mod has accumulated
|
/// The total number of followers this project has accumulated
|
||||||
pub followers: u32,
|
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>,
|
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>,
|
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>,
|
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>,
|
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>,
|
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>,
|
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>,
|
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>>,
|
pub donation_urls: Option<Vec<DonationLink>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum SideType {
|
pub enum SideType {
|
||||||
Required,
|
Required,
|
||||||
@@ -113,22 +116,23 @@ pub struct License {
|
|||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||||
pub struct DonationLink {
|
pub struct DonationLink {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub platform: String,
|
pub platform: String,
|
||||||
|
#[validate(url)]
|
||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A status decides the visbility of a mod in search, URLs, and the whole site itself.
|
/// A status decides the visbility of a project in search, URLs, and the whole site itself.
|
||||||
/// Approved - Mod is displayed on search, and accessible by URL
|
/// Approved - Project 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)
|
/// Rejected - Project is not displayed on search, and not accessible by URL (Temporary state, project can reapply)
|
||||||
/// Draft - Mod is not displayed on search, and not accessible by URL
|
/// Draft - Project is not displayed on search, and not accessible by URL
|
||||||
/// Unlisted - Mod is not displayed on search, but accessible by URL
|
/// Unlisted - Project 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)
|
/// Processing - Project is not displayed on search, and not accessible by URL (Temporary state, project under review)
|
||||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ModStatus {
|
pub enum ProjectStatus {
|
||||||
Approved,
|
Approved,
|
||||||
Rejected,
|
Rejected,
|
||||||
Draft,
|
Draft,
|
||||||
@@ -137,57 +141,57 @@ pub enum ModStatus {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ModStatus {
|
impl std::fmt::Display for ProjectStatus {
|
||||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
write!(fmt, "{}", self.as_str())
|
write!(fmt, "{}", self.as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModStatus {
|
impl ProjectStatus {
|
||||||
pub fn from_str(string: &str) -> ModStatus {
|
pub fn from_str(string: &str) -> ProjectStatus {
|
||||||
match string {
|
match string {
|
||||||
"processing" => ModStatus::Processing,
|
"processing" => ProjectStatus::Processing,
|
||||||
"rejected" => ModStatus::Rejected,
|
"rejected" => ProjectStatus::Rejected,
|
||||||
"approved" => ModStatus::Approved,
|
"approved" => ProjectStatus::Approved,
|
||||||
"draft" => ModStatus::Draft,
|
"draft" => ProjectStatus::Draft,
|
||||||
"unlisted" => ModStatus::Unlisted,
|
"unlisted" => ProjectStatus::Unlisted,
|
||||||
_ => ModStatus::Unknown,
|
_ => ProjectStatus::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ModStatus::Approved => "approved",
|
ProjectStatus::Approved => "approved",
|
||||||
ModStatus::Rejected => "rejected",
|
ProjectStatus::Rejected => "rejected",
|
||||||
ModStatus::Draft => "draft",
|
ProjectStatus::Draft => "draft",
|
||||||
ModStatus::Unlisted => "unlisted",
|
ProjectStatus::Unlisted => "unlisted",
|
||||||
ModStatus::Processing => "processing",
|
ProjectStatus::Processing => "processing",
|
||||||
ModStatus::Unknown => "unknown",
|
ProjectStatus::Unknown => "unknown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_hidden(&self) -> bool {
|
pub fn is_hidden(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ModStatus::Approved => false,
|
ProjectStatus::Approved => false,
|
||||||
ModStatus::Rejected => true,
|
ProjectStatus::Rejected => true,
|
||||||
ModStatus::Draft => true,
|
ProjectStatus::Draft => true,
|
||||||
ModStatus::Unlisted => false,
|
ProjectStatus::Unlisted => false,
|
||||||
ModStatus::Processing => true,
|
ProjectStatus::Processing => true,
|
||||||
ModStatus::Unknown => true,
|
ProjectStatus::Unknown => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_searchable(&self) -> bool {
|
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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Version {
|
pub struct Version {
|
||||||
/// The ID of the version, encoded as a base62 string.
|
/// The ID of the version, encoded as a base62 string.
|
||||||
pub id: VersionId,
|
pub id: VersionId,
|
||||||
/// The ID of the mod this version is for.
|
/// The ID of the project this version is for.
|
||||||
pub mod_id: ModId,
|
pub project_id: ProjectId,
|
||||||
/// The ID of the author who published this version
|
/// The ID of the author who published this version
|
||||||
pub author_id: UserId,
|
pub author_id: UserId,
|
||||||
/// Whether the version is featured or not
|
/// Whether the version is featured or not
|
||||||
@@ -197,9 +201,9 @@ pub struct Version {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
/// The version number. Ideally will follow semantic versioning
|
/// The version number. Ideally will follow semantic versioning
|
||||||
pub version_number: String,
|
pub version_number: String,
|
||||||
/// The changelog for this version of the mod.
|
/// The changelog for this version of the project.
|
||||||
pub changelog: String,
|
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>,
|
pub changelog_url: Option<String>,
|
||||||
/// The date that this version was published.
|
/// The date that this version was published.
|
||||||
pub date_published: DateTime<Utc>,
|
pub date_published: DateTime<Utc>,
|
||||||
@@ -210,15 +214,15 @@ pub struct Version {
|
|||||||
|
|
||||||
/// A list of files available for download for this version.
|
/// A list of files available for download for this version.
|
||||||
pub files: Vec<VersionFile>,
|
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>,
|
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>,
|
pub game_versions: Vec<GameVersion>,
|
||||||
/// The loaders that this version works on
|
/// 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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct VersionFile {
|
pub struct VersionFile {
|
||||||
/// A map of hashes of the file. The key is the hashing algorithm
|
/// A map of hashes of the file. The key is the hashing algorithm
|
||||||
@@ -310,14 +314,14 @@ impl DependencyType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A specific version of Minecraft
|
/// A specific version of Minecraft
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct GameVersion(pub String);
|
pub struct GameVersion(pub String);
|
||||||
|
|
||||||
/// A mod loader
|
/// A project loader
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct ModLoader(pub String);
|
pub struct Loader(pub String);
|
||||||
|
|
||||||
// These fields must always succeed parsing; deserialize errors aren't
|
// These fields must always succeed parsing; deserialize errors aren't
|
||||||
// processed correctly (don't return JSON errors)
|
// processed correctly (don't return JSON errors)
|
||||||
@@ -22,7 +22,7 @@ pub struct Report {
|
|||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum ItemType {
|
pub enum ItemType {
|
||||||
Mod,
|
Project,
|
||||||
Version,
|
Version,
|
||||||
User,
|
User,
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -31,7 +31,7 @@ pub enum ItemType {
|
|||||||
impl ItemType {
|
impl ItemType {
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ItemType::Mod => "mod",
|
ItemType::Project => "project",
|
||||||
ItemType::Version => "version",
|
ItemType::Version => "version",
|
||||||
ItemType::User => "user",
|
ItemType::User => "user",
|
||||||
ItemType::Unknown => "unknown",
|
ItemType::Unknown => "unknown",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct TeamId(pub u64);
|
|||||||
pub const OWNER_ROLE: &str = "Owner";
|
pub const OWNER_ROLE: &str = "Owner";
|
||||||
|
|
||||||
// TODO: permissions, role names, etc
|
// TODO: permissions, role names, etc
|
||||||
/// A team of users who control a mod
|
/// A team of users who control a project
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Team {
|
pub struct Team {
|
||||||
/// The id of the team
|
/// The id of the team
|
||||||
@@ -31,7 +31,7 @@ bitflags::bitflags! {
|
|||||||
const MANAGE_INVITES = 1 << 4;
|
const MANAGE_INVITES = 1 << 4;
|
||||||
const REMOVE_MEMBER = 1 << 5;
|
const REMOVE_MEMBER = 1 << 5;
|
||||||
const EDIT_MEMBER = 1 << 6;
|
const EDIT_MEMBER = 1 << 6;
|
||||||
const DELETE_MOD = 1 << 7;
|
const DELETE_PROJECT = 1 << 7;
|
||||||
const ALL = 0b11111111;
|
const ALL = 0b11111111;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::auth::get_user_from_headers;
|
use crate::auth::get_user_from_headers;
|
||||||
use crate::database;
|
use crate::database;
|
||||||
use crate::models::mods::ModId;
|
use crate::models::projects::ProjectId;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -55,22 +55,13 @@ pub async fn maven_metadata(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let string = info.into_inner().0;
|
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 {
|
let project_data =
|
||||||
match database::models::Mod::get_full(id.into(), &**pool).await {
|
database::models::Project::get_full_from_slug_or_project_id(string, &**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 user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
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
|
data
|
||||||
} else {
|
} else {
|
||||||
return Ok(HttpResponse::NotFound().body(""));
|
return Ok(HttpResponse::NotFound().body(""));
|
||||||
@@ -85,17 +76,16 @@ pub async fn maven_metadata(
|
|||||||
} else {
|
} else {
|
||||||
let user_id: database::models::ids::UserId = user.id.into();
|
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)",
|
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)",
|
||||||
data.inner.team_id as database::models::ids::TeamId,
|
data.inner.team_id as database::models::ids::TeamId,
|
||||||
user_id as database::models::ids::UserId,
|
user_id as database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.fetch_one(&**pool)
|
.fetch_one(&**pool)
|
||||||
.await
|
.await?
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
.exists;
|
.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
|
LEFT JOIN release_channels ON release_channels.id = versions.release_channel
|
||||||
WHERE mod_id = $1
|
WHERE mod_id = $1
|
||||||
",
|
",
|
||||||
data.inner.id as database::models::ids::ModId
|
data.inner.id as database::models::ids::ProjectId
|
||||||
)
|
)
|
||||||
.fetch_all(&**pool)
|
.fetch_all(&**pool)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
let project_id: ProjectId = data.inner.id.into();
|
||||||
|
|
||||||
let respdata = Metadata {
|
let respdata = Metadata {
|
||||||
group_id: "maven.modrinth".to_string(),
|
group_id: "maven.modrinth".to_string(),
|
||||||
artifact_id: string,
|
artifact_id: format!("{}", project_id),
|
||||||
versioning: Versioning {
|
versioning: Versioning {
|
||||||
latest: version_names
|
latest: version_names
|
||||||
.last()
|
.last()
|
||||||
@@ -141,7 +132,7 @@ pub async fn maven_metadata(
|
|||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type("text/xml")
|
.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}")]
|
#[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)>,
|
web::Path((string, vnum, file)): web::Path<(String, String, String)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> 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 {
|
let project_data = if let Some(id) = id_option {
|
||||||
match database::models::Mod::get_full(id.into(), &**pool).await {
|
match database::models::Project::get_full(id.into(), &**pool).await {
|
||||||
Ok(Some(data)) => Ok(Some(data)),
|
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),
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
database::models::Mod::get_full_from_slug(&string, &**pool).await
|
database::models::Project::get_full_from_slug(&string, &**pool).await
|
||||||
}
|
}?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
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
|
data
|
||||||
} else {
|
} else {
|
||||||
return Ok(HttpResponse::NotFound().body(""));
|
return Ok(HttpResponse::NotFound().body(""));
|
||||||
@@ -180,17 +170,16 @@ pub async fn version_file(
|
|||||||
} else {
|
} else {
|
||||||
let user_id: database::models::ids::UserId = user.id.into();
|
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)",
|
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)",
|
||||||
data.inner.team_id as database::models::ids::TeamId,
|
data.inner.team_id as database::models::ids::TeamId,
|
||||||
user_id as database::models::ids::UserId,
|
user_id as database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.fetch_one(&**pool)
|
.fetch_one(&**pool)
|
||||||
.await
|
.await?
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
.exists;
|
.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!(
|
let vid = if let Some(vid) = sqlx::query!(
|
||||||
"SELECT id FROM versions WHERE mod_id = $1 AND version_number = $2",
|
"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
|
vnum
|
||||||
)
|
)
|
||||||
.fetch_optional(&**pool)
|
.fetch_optional(&**pool)
|
||||||
.await
|
.await?
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
{
|
{
|
||||||
vid
|
vid
|
||||||
} else {
|
} else {
|
||||||
@@ -215,8 +203,7 @@ pub async fn version_file(
|
|||||||
|
|
||||||
let version = if let Some(version) =
|
let version = if let Some(version) =
|
||||||
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
|
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
|
||||||
.await
|
.await?
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
{
|
{
|
||||||
version
|
version
|
||||||
} else {
|
} else {
|
||||||
@@ -238,19 +225,17 @@ pub async fn version_file(
|
|||||||
};
|
};
|
||||||
return Ok(HttpResponse::Ok()
|
return Ok(HttpResponse::Ok()
|
||||||
.content_type("text/xml")
|
.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)?));
|
||||||
} else {
|
} else if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) {
|
||||||
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()
|
return Ok(HttpResponse::TemporaryRedirect()
|
||||||
.header("Location", &*selected_file.url)
|
.header("Location", &*selected_file.url)
|
||||||
.body(""));
|
.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(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
|
|
||||||
|
mod v1;
|
||||||
|
pub use v1::v1_config;
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
mod index;
|
mod index;
|
||||||
mod maven;
|
mod maven;
|
||||||
mod mod_creation;
|
|
||||||
mod moderation;
|
mod moderation;
|
||||||
mod mods;
|
|
||||||
mod not_found;
|
mod not_found;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
mod project_creation;
|
||||||
|
mod projects;
|
||||||
mod reports;
|
mod reports;
|
||||||
mod tags;
|
mod tags;
|
||||||
mod teams;
|
mod teams;
|
||||||
mod users;
|
mod users;
|
||||||
mod version_creation;
|
mod version_creation;
|
||||||
|
mod version_file;
|
||||||
mod versions;
|
mod versions;
|
||||||
|
|
||||||
pub use auth::config as auth_config;
|
pub use auth::config as auth_config;
|
||||||
@@ -22,21 +26,35 @@ pub use self::index::index_get;
|
|||||||
pub use self::not_found::not_found;
|
pub use self::not_found::not_found;
|
||||||
use crate::file_hosting::FileHostingError;
|
use crate::file_hosting::FileHostingError;
|
||||||
|
|
||||||
pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
pub fn v2_config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(mods::mod_search);
|
cfg.service(
|
||||||
cfg.service(mods::mods_get);
|
web::scope("/v2/")
|
||||||
cfg.service(mod_creation::mod_create);
|
.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(
|
cfg.service(
|
||||||
web::scope("mod")
|
web::scope("project")
|
||||||
.service(mods::mod_slug_get)
|
.service(projects::project_get)
|
||||||
.service(mods::mod_get)
|
.service(projects::project_delete)
|
||||||
.service(mods::mod_delete)
|
.service(projects::project_edit)
|
||||||
.service(mods::mod_edit)
|
.service(projects::project_icon_edit)
|
||||||
.service(mods::mod_icon_edit)
|
.service(projects::project_follow)
|
||||||
.service(mods::mod_follow)
|
.service(projects::project_unfollow)
|
||||||
.service(mods::mod_unfollow)
|
.service(web::scope("{project_id}").service(versions::version_list)),
|
||||||
.service(web::scope("{mod_id}").service(versions::version_list)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +75,17 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
|||||||
);
|
);
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("version_file")
|
web::scope("version_file")
|
||||||
.service(versions::delete_file)
|
.service(version_file::delete_file)
|
||||||
.service(versions::get_version_from_hash)
|
.service(version_file::get_version_from_hash)
|
||||||
.service(versions::download_version),
|
.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(users::users_get);
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("user")
|
web::scope("user")
|
||||||
.service(users::user_username_get)
|
|
||||||
.service(users::user_get)
|
.service(users::user_get)
|
||||||
.service(users::mods_list)
|
.service(users::projects_list)
|
||||||
.service(users::user_delete)
|
.service(users::user_delete)
|
||||||
.service(users::user_edit)
|
.service(users::user_edit)
|
||||||
.service(users::user_icon_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) {
|
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) {
|
pub fn reports_config(cfg: &mut web::ServiceConfig) {
|
||||||
@@ -117,8 +142,10 @@ pub enum ApiError {
|
|||||||
EnvError(#[from] dotenv::Error),
|
EnvError(#[from] dotenv::Error),
|
||||||
#[error("Error while uploading file")]
|
#[error("Error while uploading file")]
|
||||||
FileHostingError(#[from] FileHostingError),
|
FileHostingError(#[from] FileHostingError),
|
||||||
#[error("Internal server error: {0}")]
|
#[error("Database Error: {0}")]
|
||||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||||
|
#[error("Database Error: {0}")]
|
||||||
|
SqlxDatabaseError(#[from] sqlx::Error),
|
||||||
#[error("Internal server error: {0}")]
|
#[error("Internal server error: {0}")]
|
||||||
XmlError(String),
|
XmlError(String),
|
||||||
#[error("Deserialization error: {0}")]
|
#[error("Deserialization error: {0}")]
|
||||||
@@ -129,6 +156,8 @@ pub enum ApiError {
|
|||||||
CustomAuthenticationError(String),
|
CustomAuthenticationError(String),
|
||||||
#[error("Invalid Input: {0}")]
|
#[error("Invalid Input: {0}")]
|
||||||
InvalidInputError(String),
|
InvalidInputError(String),
|
||||||
|
#[error("Error while validating input: {0}")]
|
||||||
|
ValidationError(#[from] validator::ValidationErrors),
|
||||||
#[error("Search Error: {0}")]
|
#[error("Search Error: {0}")]
|
||||||
SearchError(#[from] meilisearch_sdk::errors::Error),
|
SearchError(#[from] meilisearch_sdk::errors::Error),
|
||||||
#[error("Indexing Error: {0}")]
|
#[error("Indexing Error: {0}")]
|
||||||
@@ -140,6 +169,7 @@ impl actix_web::ResponseError for ApiError {
|
|||||||
match self {
|
match self {
|
||||||
ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
ApiError::SqlxDatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||||
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||||
ApiError::XmlError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
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::IndexingError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ApiError::FileHostingError(..) => 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::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 {
|
crate::models::error::ApiError {
|
||||||
error: match self {
|
error: match self {
|
||||||
ApiError::EnvError(..) => "environment_error",
|
ApiError::EnvError(..) => "environment_error",
|
||||||
|
ApiError::SqlxDatabaseError(..) => "database_error",
|
||||||
ApiError::DatabaseError(..) => "database_error",
|
ApiError::DatabaseError(..) => "database_error",
|
||||||
ApiError::AuthenticationError(..) => "unauthorized",
|
ApiError::AuthenticationError(..) => "unauthorized",
|
||||||
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
||||||
@@ -165,6 +197,7 @@ impl actix_web::ResponseError for ApiError {
|
|||||||
ApiError::IndexingError(..) => "indexing_error",
|
ApiError::IndexingError(..) => "indexing_error",
|
||||||
ApiError::FileHostingError(..) => "file_hosting_error",
|
ApiError::FileHostingError(..) => "file_hosting_error",
|
||||||
ApiError::InvalidInputError(..) => "invalid_input",
|
ApiError::InvalidInputError(..) => "invalid_input",
|
||||||
|
ApiError::ValidationError(..) => "invalid_input",
|
||||||
},
|
},
|
||||||
description: &self.to_string(),
|
description: &self.to_string(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::ApiError;
|
use super::ApiError;
|
||||||
use crate::auth::check_is_moderator_from_headers;
|
use crate::auth::check_is_moderator_from_headers;
|
||||||
use crate::database;
|
use crate::database;
|
||||||
use crate::models::mods::{Mod, ModStatus};
|
use crate::models::projects::{Project, ProjectStatus};
|
||||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -9,15 +9,15 @@ use sqlx::PgPool;
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ResultCount {
|
pub struct ResultCount {
|
||||||
#[serde(default = "default_count")]
|
#[serde(default = "default_count")]
|
||||||
count: i16,
|
pub count: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_count() -> i16 {
|
fn default_count() -> i16 {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("mods")]
|
#[get("projects")]
|
||||||
pub async fn mods(
|
pub async fn get_projects(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
count: web::Query<ResultCount>,
|
count: web::Query<ResultCount>,
|
||||||
@@ -26,7 +26,7 @@ pub async fn mods(
|
|||||||
|
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
|
|
||||||
let mod_ids = sqlx::query!(
|
let project_ids = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT id FROM mods
|
SELECT id FROM mods
|
||||||
WHERE status = (
|
WHERE status = (
|
||||||
@@ -35,21 +35,19 @@ pub async fn mods(
|
|||||||
ORDER BY updated ASC
|
ORDER BY updated ASC
|
||||||
LIMIT $2;
|
LIMIT $2;
|
||||||
",
|
",
|
||||||
ModStatus::Processing.as_str(),
|
ProjectStatus::Processing.as_str(),
|
||||||
count.count as i64
|
count.count as i64
|
||||||
)
|
)
|
||||||
.fetch_many(&**pool)
|
.fetch_many(&**pool)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ids::ModId(m.id))) })
|
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ProjectId(m.id))) })
|
||||||
.try_collect::<Vec<database::models::ModId>>()
|
.try_collect::<Vec<database::models::ProjectId>>()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let mods: Vec<Mod> = database::models::mod_item::Mod::get_many_full(mod_ids, &**pool)
|
let projects: Vec<Project> = database::Project::get_many_full(project_ids, &**pool)
|
||||||
.await
|
.await?
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(super::mods::convert_mod)
|
.map(super::projects::convert_project)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(mods))
|
Ok(HttpResponse::Ok().json(projects))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ pub async fn notifications_get(
|
|||||||
|
|
||||||
let notifications_data =
|
let notifications_data =
|
||||||
database::models::notification_item::Notification::get_many(notification_ids, &**pool)
|
database::models::notification_item::Notification::get_many(notification_ids, &**pool)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let mut notifications: Vec<Notification> = Vec::new();
|
let mut notifications: Vec<Notification> = Vec::new();
|
||||||
|
|
||||||
@@ -52,9 +51,7 @@ pub async fn notification_get(
|
|||||||
let id = info.into_inner().0;
|
let id = info.into_inner().0;
|
||||||
|
|
||||||
let notification_data =
|
let notification_data =
|
||||||
database::models::notification_item::Notification::get(id.into(), &**pool)
|
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if let Some(data) = notification_data {
|
if let Some(data) = notification_data {
|
||||||
if user.id == data.user_id.into() || user.role.is_mod() {
|
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 id = info.into_inner().0;
|
||||||
|
|
||||||
let notification_data =
|
let notification_data =
|
||||||
database::models::notification_item::Notification::get(id.into(), &**pool)
|
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if let Some(data) = notification_data {
|
if let Some(data) = notification_data {
|
||||||
if data.user_id == user.id.into() || user.role.is_mod() {
|
if data.user_id == user.id.into() || user.role.is_mod() {
|
||||||
database::models::notification_item::Notification::remove(id.into(), &**pool)
|
database::models::notification_item::Notification::remove(id.into(), &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError::CustomAuthenticationError(
|
Err(ApiError::CustomAuthenticationError(
|
||||||
"You are not authorized to delete this notification!".to_string(),
|
"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::database::models;
|
||||||
use crate::file_hosting::{FileHost, FileHostingError};
|
use crate::file_hosting::{FileHost, FileHostingError};
|
||||||
use crate::models::error::ApiError;
|
use crate::models::error::ApiError;
|
||||||
use crate::models::mods::{DonationLink, License, ModId, ModStatus, SideType, VersionId};
|
use crate::models::projects::{
|
||||||
|
DonationLink, License, ProjectId, ProjectStatus, SideType, VersionId,
|
||||||
|
};
|
||||||
use crate::models::users::UserId;
|
use crate::models::users::UserId;
|
||||||
use crate::routes::version_creation::InitialVersionData;
|
use crate::routes::version_creation::InitialVersionData;
|
||||||
use crate::search::indexing::{queue::CreationQueue, IndexingError};
|
use crate::search::indexing::{queue::CreationQueue, IndexingError};
|
||||||
@@ -11,16 +13,19 @@ use actix_web::http::StatusCode;
|
|||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{post, HttpRequest, HttpResponse};
|
use actix_web::{post, HttpRequest, HttpResponse};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::postgres::PgPool;
|
use sqlx::postgres::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum CreateError {
|
pub enum CreateError {
|
||||||
#[error("Environment Error")]
|
#[error("Environment Error")]
|
||||||
EnvError(#[from] dotenv::Error),
|
EnvError(#[from] dotenv::Error),
|
||||||
#[error("An unknown database error occured")]
|
#[error("An unknown database error occurred")]
|
||||||
SqlxDatabaseError(#[from] sqlx::Error),
|
SqlxDatabaseError(#[from] sqlx::Error),
|
||||||
#[error("Database Error: {0}")]
|
#[error("Database Error: {0}")]
|
||||||
DatabaseError(#[from] models::DatabaseError),
|
DatabaseError(#[from] models::DatabaseError),
|
||||||
@@ -30,11 +35,15 @@ pub enum CreateError {
|
|||||||
MultipartError(actix_multipart::MultipartError),
|
MultipartError(actix_multipart::MultipartError),
|
||||||
#[error("Error while parsing JSON: {0}")]
|
#[error("Error while parsing JSON: {0}")]
|
||||||
SerDeError(#[from] serde_json::Error),
|
SerDeError(#[from] serde_json::Error),
|
||||||
|
#[error("Error while validating input: {0}")]
|
||||||
|
ValidationError(#[from] validator::ValidationErrors),
|
||||||
#[error("Error while uploading file")]
|
#[error("Error while uploading file")]
|
||||||
FileHostingError(#[from] FileHostingError),
|
FileHostingError(#[from] FileHostingError),
|
||||||
|
#[error("Error while validating uploaded file: {0}")]
|
||||||
|
FileValidationError(#[from] crate::validate::ValidationError),
|
||||||
#[error("{}", .0)]
|
#[error("{}", .0)]
|
||||||
MissingValueError(String),
|
MissingValueError(String),
|
||||||
#[error("Invalid format for mod icon: {0}")]
|
#[error("Invalid format for project icon: {0}")]
|
||||||
InvalidIconFormat(String),
|
InvalidIconFormat(String),
|
||||||
#[error("Error with multipart data: {0}")]
|
#[error("Error with multipart data: {0}")]
|
||||||
InvalidInput(String),
|
InvalidInput(String),
|
||||||
@@ -46,7 +55,7 @@ pub enum CreateError {
|
|||||||
InvalidCategory(String),
|
InvalidCategory(String),
|
||||||
#[error("Invalid file type for version file: {0}")]
|
#[error("Invalid file type for version file: {0}")]
|
||||||
InvalidFileType(String),
|
InvalidFileType(String),
|
||||||
#[error("Slug collides with other mod's id!")]
|
#[error("Slug collides with other project's id!")]
|
||||||
SlugCollision,
|
SlugCollision,
|
||||||
#[error("Authentication Error: {0}")]
|
#[error("Authentication Error: {0}")]
|
||||||
Unauthorized(#[from] AuthenticationError),
|
Unauthorized(#[from] AuthenticationError),
|
||||||
@@ -74,6 +83,8 @@ impl actix_web::ResponseError for CreateError {
|
|||||||
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
|
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
|
||||||
CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED,
|
CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED,
|
||||||
CreateError::SlugCollision => StatusCode::BAD_REQUEST,
|
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::Unauthorized(..) => "unauthorized",
|
||||||
CreateError::CustomAuthenticationError(..) => "unauthorized",
|
CreateError::CustomAuthenticationError(..) => "unauthorized",
|
||||||
CreateError::SlugCollision => "invalid_input",
|
CreateError::SlugCollision => "invalid_input",
|
||||||
|
CreateError::ValidationError(..) => "invalid_input",
|
||||||
|
CreateError::FileValidationError(..) => "invalid_input",
|
||||||
},
|
},
|
||||||
description: &self.to_string(),
|
description: &self.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
lazy_static! {
|
||||||
struct ModCreateData {
|
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap();
|
||||||
/// The title or name of the mod.
|
}
|
||||||
pub mod_name: String,
|
|
||||||
/// The slug of a mod, used for vanity URLs
|
fn default_project_type() -> String {
|
||||||
pub mod_slug: String,
|
"mod".to_string()
|
||||||
/// A short description of the mod.
|
}
|
||||||
pub mod_description: String,
|
|
||||||
/// A long description of the mod, in markdown.
|
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||||
pub mod_body: String,
|
struct ProjectCreateData {
|
||||||
/// A list of initial versions to upload with the created mod
|
#[validate(length(min = 3, max = 256))]
|
||||||
pub initial_versions: Vec<InitialVersionData>,
|
#[serde(alias = "mod_name")]
|
||||||
/// A list of the categories that the mod is in.
|
/// The title or name of the project.
|
||||||
pub categories: Vec<String>,
|
pub title: String,
|
||||||
/// An optional link to where to submit bugs or issues with the mod.
|
#[validate(length(min = 1, max = 64))]
|
||||||
pub issues_url: Option<String>,
|
#[serde(default = "default_project_type")]
|
||||||
/// An optional link to the source code for the mod.
|
/// The project type of this mod
|
||||||
pub source_url: Option<String>,
|
pub project_type: String,
|
||||||
/// An optional link to the mod's wiki page or other relevant information.
|
#[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")]
|
||||||
pub wiki_url: Option<String>,
|
#[serde(alias = "mod_slug")]
|
||||||
/// An optional link to the mod's license page
|
/// The slug of a project, used for vanity URLs
|
||||||
pub license_url: Option<String>,
|
pub slug: String,
|
||||||
/// An optional link to the mod's discord.
|
#[validate(length(min = 3, max = 2048))]
|
||||||
pub discord_url: Option<String>,
|
#[serde(alias = "mod_description")]
|
||||||
/// An optional boolean. If true, the mod will be created as a draft.
|
/// A short description of the project.
|
||||||
pub is_draft: Option<bool>,
|
pub description: String,
|
||||||
/// The support range for the client mod
|
#[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,
|
pub client_side: SideType,
|
||||||
/// The support range for the server mod
|
/// The support range for the server project
|
||||||
pub server_side: SideType,
|
pub server_side: SideType,
|
||||||
/// The license id that the mod follows
|
|
||||||
pub license_id: String,
|
#[validate(length(max = 64))]
|
||||||
/// An optional list of all donation links the mod has
|
/// 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>>,
|
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 {
|
pub struct UploadedFile {
|
||||||
@@ -156,8 +202,8 @@ pub async fn undo_uploads(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("mod")]
|
#[post("project")]
|
||||||
pub async fn mod_create(
|
pub async fn project_create(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
payload: Multipart,
|
payload: Multipart,
|
||||||
client: Data<PgPool>,
|
client: Data<PgPool>,
|
||||||
@@ -167,7 +213,7 @@ pub async fn mod_create(
|
|||||||
let mut transaction = client.begin().await?;
|
let mut transaction = client.begin().await?;
|
||||||
let mut uploaded_files = Vec::new();
|
let mut uploaded_files = Vec::new();
|
||||||
|
|
||||||
let result = mod_create_inner(
|
let result = project_create_inner(
|
||||||
req,
|
req,
|
||||||
payload,
|
payload,
|
||||||
&mut transaction,
|
&mut transaction,
|
||||||
@@ -196,7 +242,7 @@ pub async fn mod_create(
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Mod Creation Steps:
|
Project Creation Steps:
|
||||||
Get logged in user
|
Get logged in user
|
||||||
Must match the author in the version creation
|
Must match the author in the version creation
|
||||||
|
|
||||||
@@ -206,12 +252,12 @@ Get logged in user
|
|||||||
- Create versions
|
- Create versions
|
||||||
- Some shared logic with version creation
|
- Some shared logic with version creation
|
||||||
- Create list of VersionBuilders
|
- Create list of VersionBuilders
|
||||||
- Create ModBuilder
|
- Create ProjectBuilder
|
||||||
|
|
||||||
2. Upload
|
2. Upload
|
||||||
- Icon: check file format & size
|
- Icon: check file format & size
|
||||||
- Upload to backblaze & record URL
|
- Upload to backblaze & record URL
|
||||||
- Mod files
|
- Project files
|
||||||
- Check for matching version
|
- Check for matching version
|
||||||
- File size limits?
|
- File size limits?
|
||||||
- Check file type
|
- Check file type
|
||||||
@@ -221,10 +267,10 @@ Get logged in user
|
|||||||
|
|
||||||
3. Creation
|
3. Creation
|
||||||
- Database stuff
|
- 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,
|
req: HttpRequest,
|
||||||
mut payload: Multipart,
|
mut payload: Multipart,
|
||||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
@@ -238,15 +284,17 @@ async fn mod_create_inner(
|
|||||||
// The currently logged in user
|
// The currently logged in user
|
||||||
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
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;
|
||||||
let mut versions_map = std::collections::HashMap::new();
|
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
|
// The first multipart field must be named "data" and contain a
|
||||||
// JSON `ModCreateData` object.
|
// JSON `ProjectCreateData` object.
|
||||||
|
|
||||||
let mut field = payload
|
let mut field = payload
|
||||||
.next()
|
.next()
|
||||||
@@ -275,75 +323,20 @@ async fn mod_create_inner(
|
|||||||
while let Some(chunk) = field.next().await {
|
while let Some(chunk) = field.next().await {
|
||||||
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
|
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)?;
|
||||||
|
|
||||||
{
|
create_data.validate()?;
|
||||||
// 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,
|
|
||||||
|
|
||||||
initial_versions: Vec<InitialVersionData>,
|
let slug_project_id_option: Option<ProjectId> =
|
||||||
team_members: Vec<TeamMember>,
|
serde_json::from_str(&*format!("\"{}\"", create_data.slug)).ok();
|
||||||
|
|
||||||
# TeamMember:
|
if let Some(slug_project_id) = slug_project_id_option {
|
||||||
name: 3..=64
|
let slug_project_id: models::ids::ProjectId = slug_project_id.into();
|
||||||
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();
|
|
||||||
let results = sqlx::query!(
|
let results = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)
|
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)
|
.fetch_one(&mut *transaction)
|
||||||
.await
|
.await
|
||||||
@@ -366,13 +359,31 @@ async fn mod_create_inner(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
versions
|
versions.push(
|
||||||
.push(create_initial_version(data, mod_id, current_user.id, transaction).await?);
|
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;
|
let mut icon_url = None;
|
||||||
|
|
||||||
while let Some(item) = payload.next().await {
|
while let Some(item) = payload.next().await {
|
||||||
@@ -391,14 +402,14 @@ async fn mod_create_inner(
|
|||||||
if name == "icon" {
|
if name == "icon" {
|
||||||
if icon_url.is_some() {
|
if icon_url.is_some() {
|
||||||
return Err(CreateError::InvalidInput(String::from(
|
return Err(CreateError::InvalidInput(String::from(
|
||||||
"Mods can only have one icon",
|
"Projects can only have one icon",
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// Upload the icon to the cdn
|
// Upload the icon to the cdn
|
||||||
icon_url = Some(
|
icon_url = Some(
|
||||||
process_icon_upload(
|
process_icon_upload(
|
||||||
uploaded_files,
|
uploaded_files,
|
||||||
mod_id,
|
project_id,
|
||||||
file_extension,
|
file_extension,
|
||||||
file_host,
|
file_host,
|
||||||
field,
|
field,
|
||||||
@@ -420,27 +431,33 @@ async fn mod_create_inner(
|
|||||||
|
|
||||||
// `index` is always valid for these lists
|
// `index` is always valid for these lists
|
||||||
let created_version = versions.get_mut(index).unwrap();
|
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
|
// Upload the new jar file
|
||||||
let file_builder = super::version_creation::upload_file(
|
super::version_creation::upload_file(
|
||||||
&mut field,
|
&mut field,
|
||||||
file_host,
|
file_host,
|
||||||
uploaded_files,
|
uploaded_files,
|
||||||
|
&mut created_version.files,
|
||||||
&cdn_url,
|
&cdn_url,
|
||||||
&content_disposition,
|
&content_disposition,
|
||||||
mod_id,
|
project_id,
|
||||||
&version_data.version_number,
|
&version_data.version_number,
|
||||||
|
&*project_create_data.project_type,
|
||||||
|
version_data.loaders.clone(),
|
||||||
|
version_data.game_versions.clone(),
|
||||||
|
&all_game_versions,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.await?;
|
.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
|
// 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() {
|
if version_data.file_parts.len() != builder.files.len() {
|
||||||
return Err(CreateError::InvalidInput(String::from(
|
return Err(CreateError::InvalidInput(String::from(
|
||||||
@@ -450,11 +467,15 @@ async fn mod_create_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert the list of category names to actual categories
|
// Convert the list of category names to actual categories
|
||||||
let mut categories = Vec::with_capacity(mod_create_data.categories.len());
|
let mut categories = Vec::with_capacity(project_create_data.categories.len());
|
||||||
for category in &mod_create_data.categories {
|
for category in &project_create_data.categories {
|
||||||
let id = models::categories::Category::get_id(&category, &mut *transaction)
|
let id = models::categories::Category::get_id_project(
|
||||||
.await?
|
&category,
|
||||||
.ok_or_else(|| CreateError::InvalidCategory(category.clone()))?;
|
project_type_id,
|
||||||
|
&mut *transaction,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| CreateError::InvalidCategory(category.clone()))?;
|
||||||
categories.push(id);
|
categories.push(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,10 +491,10 @@ async fn mod_create_inner(
|
|||||||
let team_id = team.insert(&mut *transaction).await?;
|
let team_id = team.insert(&mut *transaction).await?;
|
||||||
|
|
||||||
let status;
|
let status;
|
||||||
if mod_create_data.is_draft.unwrap_or(false) {
|
if project_create_data.is_draft.unwrap_or(false) {
|
||||||
status = ModStatus::Draft;
|
status = ProjectStatus::Draft;
|
||||||
} else {
|
} else {
|
||||||
status = ModStatus::Processing;
|
status = ProjectStatus::Processing;
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_id = models::StatusId::get_id(&status, &mut *transaction)
|
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()))
|
CreateError::InvalidInput(format!("Status {} does not exist.", status.clone()))
|
||||||
})?;
|
})?;
|
||||||
let client_side_id =
|
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?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
CreateError::InvalidInput(
|
CreateError::InvalidInput(
|
||||||
@@ -491,7 +512,7 @@ async fn mod_create_inner(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let server_side_id =
|
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?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
CreateError::InvalidInput(
|
CreateError::InvalidInput(
|
||||||
@@ -500,14 +521,14 @@ async fn mod_create_inner(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let license_id =
|
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?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
CreateError::InvalidInput("License specified does not exist.".to_string())
|
CreateError::InvalidInput("License specified does not exist.".to_string())
|
||||||
})?;
|
})?;
|
||||||
let mut donation_urls = vec![];
|
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 {
|
for url in urls {
|
||||||
let platform_id = models::DonationPlatformId::get_id(&url.id, &mut *transaction)
|
let platform_id = models::DonationPlatformId::get_id(&url.id, &mut *transaction)
|
||||||
.await?
|
.await?
|
||||||
@@ -518,8 +539,8 @@ async fn mod_create_inner(
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
donation_urls.push(models::mod_item::DonationUrl {
|
donation_urls.push(models::project_item::DonationUrl {
|
||||||
mod_id: mod_id.into(),
|
project_id: project_id.into(),
|
||||||
platform_id,
|
platform_id,
|
||||||
platform_short: "".to_string(),
|
platform_short: "".to_string(),
|
||||||
platform_name: "".to_string(),
|
platform_name: "".to_string(),
|
||||||
@@ -528,72 +549,76 @@ async fn mod_create_inner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mod_builder = models::mod_item::ModBuilder {
|
let project_builder = models::project_item::ProjectBuilder {
|
||||||
mod_id: mod_id.into(),
|
project_id: project_id.into(),
|
||||||
|
project_type_id,
|
||||||
team_id,
|
team_id,
|
||||||
title: mod_create_data.mod_name,
|
title: project_create_data.title,
|
||||||
description: mod_create_data.mod_description,
|
description: project_create_data.description,
|
||||||
body: mod_create_data.mod_body,
|
body: project_create_data.body,
|
||||||
icon_url,
|
icon_url,
|
||||||
issues_url: mod_create_data.issues_url,
|
issues_url: project_create_data.issues_url,
|
||||||
source_url: mod_create_data.source_url,
|
source_url: project_create_data.source_url,
|
||||||
wiki_url: mod_create_data.wiki_url,
|
wiki_url: project_create_data.wiki_url,
|
||||||
|
|
||||||
license_url: mod_create_data.license_url,
|
license_url: project_create_data.license_url,
|
||||||
discord_url: mod_create_data.discord_url,
|
discord_url: project_create_data.discord_url,
|
||||||
categories,
|
categories,
|
||||||
initial_versions: versions,
|
initial_versions: versions,
|
||||||
status: status_id,
|
status: status_id,
|
||||||
client_side: client_side_id,
|
client_side: client_side_id,
|
||||||
server_side: server_side_id,
|
server_side: server_side_id,
|
||||||
license: license_id,
|
license: license_id,
|
||||||
slug: Some(mod_create_data.mod_slug),
|
slug: Some(project_create_data.slug),
|
||||||
donation_urls,
|
donation_urls,
|
||||||
};
|
};
|
||||||
|
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
|
|
||||||
let response = crate::models::mods::Mod {
|
let response = crate::models::projects::Project {
|
||||||
id: mod_id,
|
id: project_id,
|
||||||
slug: mod_builder.slug.clone(),
|
slug: project_builder.slug.clone(),
|
||||||
|
project_type: project_create_data.project_type.clone(),
|
||||||
team: team_id.into(),
|
team: team_id.into(),
|
||||||
title: mod_builder.title.clone(),
|
title: project_builder.title.clone(),
|
||||||
description: mod_builder.description.clone(),
|
description: project_builder.description.clone(),
|
||||||
body: mod_builder.body.clone(),
|
body: project_builder.body.clone(),
|
||||||
body_url: None,
|
body_url: None,
|
||||||
published: now,
|
published: now,
|
||||||
updated: now,
|
updated: now,
|
||||||
status: status.clone(),
|
status: status.clone(),
|
||||||
license: License {
|
license: License {
|
||||||
id: mod_create_data.license_id.clone(),
|
id: project_create_data.license_id.clone(),
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
url: mod_builder.license_url.clone(),
|
url: project_builder.license_url.clone(),
|
||||||
},
|
},
|
||||||
client_side: mod_create_data.client_side,
|
client_side: project_create_data.client_side,
|
||||||
server_side: mod_create_data.server_side,
|
server_side: project_create_data.server_side,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
followers: 0,
|
followers: 0,
|
||||||
categories: mod_create_data.categories,
|
categories: project_create_data.categories,
|
||||||
versions: mod_builder
|
versions: project_builder
|
||||||
.initial_versions
|
.initial_versions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| v.version_id.into())
|
.map(|v| v.version_id.into())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
icon_url: mod_builder.icon_url.clone(),
|
icon_url: project_builder.icon_url.clone(),
|
||||||
issues_url: mod_builder.issues_url.clone(),
|
issues_url: project_builder.issues_url.clone(),
|
||||||
source_url: mod_builder.source_url.clone(),
|
source_url: project_builder.source_url.clone(),
|
||||||
wiki_url: mod_builder.wiki_url.clone(),
|
wiki_url: project_builder.wiki_url.clone(),
|
||||||
discord_url: mod_builder.discord_url.clone(),
|
discord_url: project_builder.discord_url.clone(),
|
||||||
donation_urls: mod_create_data.donation_urls.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() {
|
if status.is_searchable() {
|
||||||
let index_mod =
|
let index_project = crate::search::indexing::local_import::query_one(
|
||||||
crate::search::indexing::local_import::query_one(mod_id.into(), &mut *transaction)
|
project_id.into(),
|
||||||
.await?;
|
&mut *transaction,
|
||||||
indexing_queue.add(index_mod);
|
)
|
||||||
|
.await?;
|
||||||
|
indexing_queue.add(index_project);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
Ok(HttpResponse::Ok().json(response))
|
||||||
@@ -602,18 +627,18 @@ async fn mod_create_inner(
|
|||||||
|
|
||||||
async fn create_initial_version(
|
async fn create_initial_version(
|
||||||
version_data: &InitialVersionData,
|
version_data: &InitialVersionData,
|
||||||
mod_id: ModId,
|
project_id: ProjectId,
|
||||||
author: UserId,
|
author: UserId,
|
||||||
|
all_game_versions: &[models::categories::GameVersion],
|
||||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
) -> Result<models::version_item::VersionBuilder, CreateError> {
|
) -> 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(
|
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)?;
|
version_data.validate()?;
|
||||||
check_length(1..=32, "version number", &version_data.version_number)?;
|
|
||||||
|
|
||||||
// Randomly generate a new id to be used for the version
|
// Randomly generate a new id to be used for the version
|
||||||
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
|
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
|
||||||
@@ -623,13 +648,17 @@ async fn create_initial_version(
|
|||||||
.await?
|
.await?
|
||||||
.expect("Release Channel not found in database");
|
.expect("Release Channel not found in database");
|
||||||
|
|
||||||
let mut game_versions = Vec::with_capacity(version_data.game_versions.len());
|
let game_versions = version_data
|
||||||
for v in &version_data.game_versions {
|
.game_versions
|
||||||
let id = models::categories::GameVersion::get_id(&v.0, &mut *transaction)
|
.iter()
|
||||||
.await?
|
.map(|x| {
|
||||||
.ok_or_else(|| CreateError::InvalidGameVersion(v.0.clone()))?;
|
all_game_versions
|
||||||
game_versions.push(id);
|
.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());
|
let mut loaders = Vec::with_capacity(version_data.loaders.len());
|
||||||
for l in &version_data.loaders {
|
for l in &version_data.loaders {
|
||||||
@@ -647,7 +676,7 @@ async fn create_initial_version(
|
|||||||
|
|
||||||
let version = models::version_item::VersionBuilder {
|
let version = models::version_item::VersionBuilder {
|
||||||
version_id: version_id.into(),
|
version_id: version_id.into(),
|
||||||
mod_id: mod_id.into(),
|
project_id: project_id.into(),
|
||||||
author_id: author.into(),
|
author_id: author.into(),
|
||||||
name: version_data.version_title.clone(),
|
name: version_data.version_title.clone(),
|
||||||
version_number: version_data.version_number.clone(),
|
version_number: version_data.version_number.clone(),
|
||||||
@@ -668,7 +697,7 @@ async fn create_initial_version(
|
|||||||
|
|
||||||
async fn process_icon_upload(
|
async fn process_icon_upload(
|
||||||
uploaded_files: &mut Vec<UploadedFile>,
|
uploaded_files: &mut Vec<UploadedFile>,
|
||||||
mod_id: ModId,
|
project_id: ProjectId,
|
||||||
file_extension: &str,
|
file_extension: &str,
|
||||||
file_host: &dyn FileHost,
|
file_host: &dyn FileHost,
|
||||||
mut field: actix_multipart::Field,
|
mut field: actix_multipart::Field,
|
||||||
@@ -689,7 +718,7 @@ async fn process_icon_upload(
|
|||||||
let upload_data = file_host
|
let upload_data = file_host
|
||||||
.upload_file(
|
.upload_file(
|
||||||
content_type,
|
content_type,
|
||||||
&format!("data/{}/icon.{}", mod_id, file_extension),
|
&format!("data/{}/icon.{}", project_id, file_extension),
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -723,31 +752,3 @@ pub fn get_image_content_type(extension: &str) -> Option<&'static str> {
|
|||||||
None
|
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::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::models::reports::{ItemType, Report};
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
|
||||||
@@ -21,10 +21,7 @@ pub async fn report_create(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
mut body: web::Payload,
|
mut body: web::Payload,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let mut transaction = pool
|
let mut transaction = pool.begin().await?;
|
||||||
.begin()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let current_user = get_user_from_headers(req.headers(), &mut *transaction).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 {
|
let mut report = crate::database::models::report_item::Report {
|
||||||
id,
|
id,
|
||||||
report_type_id: report_type,
|
report_type_id: report_type,
|
||||||
mod_id: None,
|
project_id: None,
|
||||||
version_id: None,
|
version_id: None,
|
||||||
user_id: None,
|
user_id: None,
|
||||||
body: new_report.body.clone(),
|
body: new_report.body.clone(),
|
||||||
@@ -57,9 +54,10 @@ pub async fn report_create(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match new_report.item_type {
|
match new_report.item_type {
|
||||||
ItemType::Mod => {
|
ItemType::Project => {
|
||||||
report.mod_id =
|
report.project_id = Some(
|
||||||
Some(serde_json::from_str::<ModId>(&*format!("\"{}\"", new_report.item_id))?.into())
|
serde_json::from_str::<ProjectId>(&*format!("\"{}\"", new_report.item_id))?.into(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ItemType::Version => {
|
ItemType::Version => {
|
||||||
report.version_id = Some(
|
report.version_id = Some(
|
||||||
@@ -79,14 +77,8 @@ pub async fn report_create(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report
|
report.insert(&mut transaction).await?;
|
||||||
.insert(&mut transaction)
|
transaction.commit().await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
transaction
|
|
||||||
.commit()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(Report {
|
Ok(HttpResponse::Ok().json(Report {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
@@ -133,12 +125,10 @@ pub async fn reports(
|
|||||||
.map(|m| crate::database::models::ids::ReportId(m.id)))
|
.map(|m| crate::database::models::ids::ReportId(m.id)))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<crate::database::models::ids::ReportId>>()
|
.try_collect::<Vec<crate::database::models::ids::ReportId>>()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let query_reports = crate::database::models::report_item::Report::get_many(report_ids, &**pool)
|
let query_reports =
|
||||||
.await
|
crate::database::models::report_item::Report::get_many(report_ids, &**pool).await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let mut reports = Vec::new();
|
let mut reports = Vec::new();
|
||||||
|
|
||||||
@@ -146,9 +136,9 @@ pub async fn reports(
|
|||||||
let mut item_id = "".to_string();
|
let mut item_id = "".to_string();
|
||||||
let mut item_type = ItemType::Unknown;
|
let mut item_type = ItemType::Unknown;
|
||||||
|
|
||||||
if let Some(mod_id) = x.mod_id {
|
if let Some(project_id) = x.project_id {
|
||||||
item_id = serde_json::to_string::<ModId>(&mod_id.into())?;
|
item_id = serde_json::to_string::<ProjectId>(&project_id.into())?;
|
||||||
item_type = ItemType::Mod;
|
item_type = ItemType::Project;
|
||||||
} else if let Some(version_id) = x.version_id {
|
} else if let Some(version_id) = x.version_id {
|
||||||
item_id = serde_json::to_string::<VersionId>(&version_id.into())?;
|
item_id = serde_json::to_string::<VersionId>(&version_id.into())?;
|
||||||
item_type = ItemType::Version;
|
item_type = ItemType::Version;
|
||||||
@@ -183,11 +173,10 @@ pub async fn delete_report(
|
|||||||
info.into_inner().0.into(),
|
info.into_inner().0.into(),
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::ApiError;
|
use super::ApiError;
|
||||||
use crate::auth::check_is_admin_from_headers;
|
use crate::auth::check_is_admin_from_headers;
|
||||||
use crate::database::models;
|
use crate::database::models;
|
||||||
use crate::database::models::categories::{DonationPlatform, License, ReportType};
|
use crate::database::models::categories::{DonationPlatform, License, ProjectType, ReportType};
|
||||||
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
|
||||||
use models::categories::{Category, GameVersion, Loader};
|
use models::categories::{Category, GameVersion, Loader};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -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
|
// TODO: searching / filtering? Could be used to implement a live
|
||||||
// searching category list
|
// searching category list
|
||||||
#[get("category")]
|
#[get("category")]
|
||||||
pub async fn category_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
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))
|
Ok(HttpResponse::Ok().json(results))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("category/{name}")]
|
#[put("category")]
|
||||||
pub async fn category_create(
|
pub async fn category_create(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
category: web::Path<(String,)>,
|
new_category: web::Json<CategoryData>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
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}")]
|
#[delete("category/{name}")]
|
||||||
@@ -72,31 +100,56 @@ pub async fn category_delete(
|
|||||||
.map_err(models::DatabaseError::from)?;
|
.map_err(models::DatabaseError::from)?;
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct LoaderData {
|
||||||
|
icon: String,
|
||||||
|
name: String,
|
||||||
|
supported_project_types: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("loader")]
|
#[get("loader")]
|
||||||
pub async fn loader_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
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))
|
Ok(HttpResponse::Ok().json(results))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("loader/{name}")]
|
#[put("loader")]
|
||||||
pub async fn loader_create(
|
pub async fn loader_create(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
loader: web::Path<(String,)>,
|
new_loader: web::Json<LoaderData>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
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}")]
|
#[delete("loader/{name}")]
|
||||||
@@ -118,14 +171,21 @@ pub async fn loader_delete(
|
|||||||
.map_err(models::DatabaseError::from)?;
|
.map_err(models::DatabaseError::from)?;
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct GameVersionQueryData {
|
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")]
|
#[serde(rename = "type")]
|
||||||
type_: Option<String>,
|
type_: Option<String>,
|
||||||
major: Option<bool>,
|
major: Option<bool>,
|
||||||
@@ -134,16 +194,22 @@ pub struct GameVersionQueryData {
|
|||||||
#[get("game_version")]
|
#[get("game_version")]
|
||||||
pub async fn game_version_list(
|
pub async fn game_version_list(
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
query: web::Query<GameVersionQueryData>,
|
query: web::Query<GameVersionQuery>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
if query.type_.is_some() || query.major.is_some() {
|
let results: Vec<GameVersionQueryData> = if query.type_.is_some() || query.major.is_some() {
|
||||||
let results =
|
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?
|
||||||
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?;
|
|
||||||
Ok(HttpResponse::Ok().json(results))
|
|
||||||
} else {
|
} else {
|
||||||
let results = GameVersion::list(&**pool).await?;
|
GameVersion::list(&**pool).await?
|
||||||
Ok(HttpResponse::Ok().json(results))
|
|
||||||
}
|
}
|
||||||
|
.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)]
|
#[derive(serde::Deserialize)]
|
||||||
@@ -177,7 +243,7 @@ pub async fn game_version_create(
|
|||||||
|
|
||||||
let _id = builder.insert(&**pool).await?;
|
let _id = builder.insert(&**pool).await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("game_version/{name}")]
|
#[delete("game_version/{name}")]
|
||||||
@@ -199,7 +265,7 @@ pub async fn game_version_delete(
|
|||||||
.map_err(models::DatabaseError::from)?;
|
.map_err(models::DatabaseError::from)?;
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
@@ -246,7 +312,7 @@ pub async fn license_create(
|
|||||||
.insert(&**pool)
|
.insert(&**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("license/{name}")]
|
#[delete("license/{name}")]
|
||||||
@@ -268,7 +334,7 @@ pub async fn license_delete(
|
|||||||
.map_err(models::DatabaseError::from)?;
|
.map_err(models::DatabaseError::from)?;
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
@@ -315,7 +381,7 @@ pub async fn donation_platform_create(
|
|||||||
.insert(&**pool)
|
.insert(&**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("donation_platform/{name}")]
|
#[delete("donation_platform/{name}")]
|
||||||
@@ -337,7 +403,7 @@ pub async fn donation_platform_delete(
|
|||||||
.map_err(models::DatabaseError::from)?;
|
.map_err(models::DatabaseError::from)?;
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
@@ -361,7 +427,7 @@ pub async fn report_type_create(
|
|||||||
|
|
||||||
let _id = ReportType::builder().name(&name)?.insert(&**pool).await?;
|
let _id = ReportType::builder().name(&name)?.insert(&**pool).await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("report_type/{name}")]
|
#[delete("report_type/{name}")]
|
||||||
@@ -383,7 +449,7 @@ pub async fn report_type_delete(
|
|||||||
.map_err(models::DatabaseError::from)?;
|
.map_err(models::DatabaseError::from)?;
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::auth::get_user_from_headers;
|
use crate::auth::get_user_from_headers;
|
||||||
use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder};
|
use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder};
|
||||||
use crate::database::models::TeamMember;
|
use crate::database::models::TeamMember;
|
||||||
use crate::models::ids::ModId;
|
use crate::models::ids::ProjectId;
|
||||||
use crate::models::teams::{Permissions, TeamId};
|
use crate::models::teams::{Permissions, TeamId};
|
||||||
use crate::models::users::UserId;
|
use crate::models::users::UserId;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
@@ -76,10 +76,7 @@ pub async fn join_team(
|
|||||||
"You are already a member of this team".to_string(),
|
"You are already a member of this team".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let mut transaction = pool
|
let mut transaction = pool.begin().await?;
|
||||||
.begin()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
// Edit Team Member to set Accepted to True
|
// Edit Team Member to set Accepted to True
|
||||||
TeamMember::edit_team_member(
|
TeamMember::edit_team_member(
|
||||||
@@ -92,17 +89,14 @@ pub async fn join_team(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
transaction
|
transaction.commit().await?;
|
||||||
.commit()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
} else {
|
} else {
|
||||||
return Err(ApiError::InvalidInputError(
|
return Err(ApiError::InvalidInputError(
|
||||||
"There is no pending request from this team".to_string(),
|
"There is no pending request from this team".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_role() -> String {
|
fn default_role() -> String {
|
||||||
@@ -127,10 +121,7 @@ pub async fn add_team_member(
|
|||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let team_id = info.into_inner().0.into();
|
let team_id = info.into_inner().0.into();
|
||||||
|
|
||||||
let mut transaction = pool
|
let mut transaction = pool.begin().await?;
|
||||||
.begin()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
let team_member =
|
let team_member =
|
||||||
@@ -181,8 +172,7 @@ pub async fn add_team_member(
|
|||||||
}
|
}
|
||||||
|
|
||||||
crate::database::models::User::get(member.user_id, &**pool)
|
crate::database::models::User::get(member.user_id, &**pool)
|
||||||
.await
|
.await?
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
.ok_or_else(|| ApiError::InvalidInputError("An invalid User ID specified".to_string()))?;
|
.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?;
|
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,
|
accepted: false,
|
||||||
}
|
}
|
||||||
.insert(&mut transaction)
|
.insert(&mut transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -206,17 +195,16 @@ pub async fn add_team_member(
|
|||||||
team_id as crate::database::models::ids::TeamId
|
team_id as crate::database::models::ids::TeamId
|
||||||
)
|
)
|
||||||
.fetch_one(&**pool)
|
.fetch_one(&**pool)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let team: TeamId = team_id.into();
|
let team: TeamId = team_id.into();
|
||||||
NotificationBuilder {
|
NotificationBuilder {
|
||||||
title: "You have been invited to join a team!".to_string(),
|
title: "You have been invited to join a team!".to_string(),
|
||||||
text: format!(
|
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
|
current_user.username, result.title
|
||||||
),
|
),
|
||||||
link: format!("mod/{}", ModId(result.id as u64)),
|
link: format!("project/{}", ProjectId(result.id as u64)),
|
||||||
actions: vec![
|
actions: vec![
|
||||||
NotificationActionBuilder {
|
NotificationActionBuilder {
|
||||||
title: "Accept".to_string(),
|
title: "Accept".to_string(),
|
||||||
@@ -234,12 +222,9 @@ pub async fn add_team_member(
|
|||||||
.insert(new_member.user_id.into(), &mut transaction)
|
.insert(new_member.user_id.into(), &mut transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
transaction
|
transaction.commit().await?;
|
||||||
.commit()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[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 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 team_member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?;
|
||||||
|
|
||||||
let mut transaction = pool
|
let mut transaction = pool.begin().await?;
|
||||||
.begin()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let member = match team_member {
|
let member = match team_member {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
@@ -306,12 +288,9 @@ pub async fn edit_team_member(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
transaction
|
transaction.commit().await?;
|
||||||
.commit()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("{id}/members/{user_id}")]
|
#[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(),
|
"You do not have permission to cancel a team invite".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,28 @@
|
|||||||
use crate::auth::get_user_from_headers;
|
use crate::auth::get_user_from_headers;
|
||||||
use crate::database::models::User;
|
use crate::database::models::User;
|
||||||
use crate::file_hosting::FileHost;
|
use crate::file_hosting::FileHost;
|
||||||
use crate::models::ids::ModId;
|
use crate::models::ids::ProjectId;
|
||||||
use crate::models::mods::ModStatus;
|
|
||||||
use crate::models::notifications::Notification;
|
use crate::models::notifications::Notification;
|
||||||
|
use crate::models::projects::ProjectStatus;
|
||||||
use crate::models::users::{Role, UserId};
|
use crate::models::users::{Role, UserId};
|
||||||
use crate::routes::notifications::convert_notification;
|
use crate::routes::notifications::convert_notification;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
#[get("user")]
|
#[get("user")]
|
||||||
pub async fn user_auth_get(
|
pub async fn user_auth_get(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
Ok(HttpResponse::Ok().json(
|
Ok(HttpResponse::Ok()
|
||||||
get_user_from_headers(
|
.json(get_user_from_headers(req.headers(), &mut *pool.acquire().await?).await?))
|
||||||
req.headers(),
|
|
||||||
&mut *pool
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -45,33 +40,13 @@ pub async fn users_get(
|
|||||||
.map(|x| x.into())
|
.map(|x| x.into())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let users_data = User::get_many(user_ids, &**pool)
|
let users_data = User::get_many(user_ids, &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let users: Vec<crate::models::users::User> = users_data.into_iter().map(convert_user).collect();
|
let users: Vec<crate::models::users::User> = users_data.into_iter().map(convert_user).collect();
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(users))
|
Ok(HttpResponse::Ok().json(users))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("@{id}")]
|
|
||||||
pub async fn user_username_get(
|
|
||||||
info: web::Path<(String,)>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
) -> Result<HttpResponse, ApiError> {
|
|
||||||
let id = info.into_inner().0;
|
|
||||||
let user_data = User::get_from_username(id, &**pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if let Some(data) = user_data {
|
|
||||||
let response = convert_user(data);
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
|
||||||
} else {
|
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("{id}")]
|
#[get("{id}")]
|
||||||
pub async fn user_get(
|
pub async fn user_get(
|
||||||
info: web::Path<(String,)>,
|
info: web::Path<(String,)>,
|
||||||
@@ -83,19 +58,13 @@ pub async fn user_get(
|
|||||||
let mut user_data;
|
let mut user_data;
|
||||||
|
|
||||||
if let Some(id) = id_option {
|
if let Some(id) = id_option {
|
||||||
user_data = User::get(id.into(), &**pool)
|
user_data = User::get(id.into(), &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if user_data.is_none() {
|
if user_data.is_none() {
|
||||||
user_data = User::get_from_username(string, &**pool)
|
user_data = User::get_from_username(string, &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_data = User::get_from_username(string, &**pool)
|
user_data = User::get_from_username(string, &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(data) = user_data {
|
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")]
|
#[get("{user_id}/projects")]
|
||||||
pub async fn mods_list(
|
pub async fn projects_list(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(UserId,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(req.headers(), &**pool).await.ok();
|
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!(
|
if let Some(id) = id_option {
|
||||||
"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) {
|
|
||||||
let user_id: UserId = id.into();
|
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 {
|
if current_user.role.is_mod() || current_user.id == user_id {
|
||||||
User::get_mods_private(id, &**pool)
|
User::get_projects_private(id, &**pool).await?
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
} else {
|
} else {
|
||||||
User::get_mods(id, ModStatus::Approved.as_str(), &**pool)
|
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await?
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
User::get_mods(id, ModStatus::Approved.as_str(), &**pool)
|
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await?
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = mod_data
|
let response = project_data
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| v.into())
|
.map(|v| v.into())
|
||||||
.collect::<Vec<crate::models::ids::ModId>>();
|
.collect::<Vec<crate::models::ids::ProjectId>>();
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
Ok(HttpResponse::Ok().json(response))
|
||||||
} else {
|
} 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 {
|
pub struct EditUser {
|
||||||
|
#[validate(length(min = 1, max = 255), regex = "RE_URL_SAFE")]
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Option::is_none",
|
skip_serializing_if = "Option::is_none",
|
||||||
with = "::serde_with::rust::double_option"
|
with = "::serde_with::rust::double_option"
|
||||||
)]
|
)]
|
||||||
|
#[validate(length(min = 1, max = 255), regex = "RE_URL_SAFE")]
|
||||||
pub name: Option<Option<String>>,
|
pub name: Option<Option<String>>,
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Option::is_none",
|
skip_serializing_if = "Option::is_none",
|
||||||
with = "::serde_with::rust::double_option"
|
with = "::serde_with::rust::double_option"
|
||||||
)]
|
)]
|
||||||
|
#[validate(email)]
|
||||||
pub email: Option<Option<String>>,
|
pub email: Option<Option<String>>,
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Option::is_none",
|
skip_serializing_if = "Option::is_none",
|
||||||
with = "::serde_with::rust::double_option"
|
with = "::serde_with::rust::double_option"
|
||||||
)]
|
)]
|
||||||
|
#[validate(length(max = 160))]
|
||||||
pub bio: Option<Option<String>>,
|
pub bio: Option<Option<String>>,
|
||||||
pub role: Option<String>,
|
pub role: Option<Role>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("{id}")]
|
#[patch("{id}")]
|
||||||
pub async fn user_edit(
|
pub async fn user_edit(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(UserId,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
new_user: web::Json<EditUser>,
|
new_user: web::Json<EditUser>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
|
|
||||||
let user_id = info.into_inner().0;
|
new_user.validate()?;
|
||||||
let id: crate::database::models::ids::UserId = user_id.into();
|
|
||||||
|
|
||||||
if user.id == user_id || user.role.is_mod() {
|
let id_option =
|
||||||
let mut transaction = pool
|
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
||||||
.begin()
|
.await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if let Some(username) = &new_user.username {
|
if let Some(id) = id_option {
|
||||||
sqlx::query!(
|
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
|
UPDATE users
|
||||||
SET username = $1
|
SET username = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
username,
|
username,
|
||||||
id as crate::database::models::ids::UserId,
|
id as crate::database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(name) = &new_user.name {
|
if let Some(name) = &new_user.name {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET name = $1
|
SET name = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
name.as_deref(),
|
name.as_deref(),
|
||||||
id as crate::database::models::ids::UserId,
|
id as crate::database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(bio) = &new_user.bio {
|
if let Some(bio) = &new_user.bio {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET bio = $1
|
SET bio = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
bio.as_deref(),
|
bio.as_deref(),
|
||||||
id as crate::database::models::ids::UserId,
|
id as crate::database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(email) = &new_user.email {
|
if let Some(email) = &new_user.email {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET email = $1
|
SET email = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
email.as_deref(),
|
email.as_deref(),
|
||||||
id as crate::database::models::ids::UserId,
|
id as crate::database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(role) = &new_user.role {
|
|
||||||
if !user.role.is_mod() {
|
|
||||||
return Err(ApiError::CustomAuthenticationError(
|
|
||||||
"You do not have the permissions to edit the role of this user!".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let role = Role::from_string(role).to_string();
|
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
|
UPDATE users
|
||||||
SET role = $1
|
SET role = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
role,
|
role,
|
||||||
id as crate::database::models::ids::UserId,
|
id as crate::database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
transaction
|
transaction.commit().await?;
|
||||||
.commit()
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
.await
|
} else {
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
Err(ApiError::CustomAuthenticationError(
|
||||||
Ok(HttpResponse::Ok().body(""))
|
"You do not have permission to edit this user!".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError::CustomAuthenticationError(
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
"You do not have permission to edit this user!".to_string(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,81 +278,87 @@ pub struct Extension {
|
|||||||
pub async fn user_icon_edit(
|
pub async fn user_icon_edit(
|
||||||
web::Query(ext): web::Query<Extension>,
|
web::Query(ext): web::Query<Extension>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(UserId,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||||
mut payload: web::Payload,
|
mut payload: web::Payload,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> 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 cdn_url = dotenv::var("CDN_URL")?;
|
||||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
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() {
|
if let Some(id) = id_option {
|
||||||
return Err(ApiError::CustomAuthenticationError(
|
if user.id != id.into() && !user.role.is_mod() {
|
||||||
"You don't have permission to edit this user's icon.".to_string(),
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
));
|
"You don't have permission to edit this user's icon.".to_string(),
|
||||||
}
|
));
|
||||||
|
|
||||||
let mut icon_url = user.avatar_url;
|
|
||||||
|
|
||||||
if user.id != id {
|
|
||||||
let new_user = User::get(id.into(), &**pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if let Some(new) = new_user {
|
|
||||||
icon_url = new.avatar_url;
|
|
||||||
} else {
|
|
||||||
return Ok(HttpResponse::NotFound().body(""));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(icon) = icon_url {
|
let mut icon_url = user.avatar_url;
|
||||||
if icon.starts_with(&cdn_url) {
|
|
||||||
let name = icon.split('/').next();
|
|
||||||
|
|
||||||
if let Some(icon_path) = name {
|
let user_id: UserId = id.into();
|
||||||
file_host.delete_file_version("", icon_path).await?;
|
|
||||||
|
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();
|
if let Some(icon) = icon_url {
|
||||||
while let Some(item) = payload.next().await {
|
if icon.starts_with(&cdn_url) {
|
||||||
bytes.extend_from_slice(&item.map_err(|_| {
|
let name = icon.split('/').next();
|
||||||
ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string())
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.len() >= 262144 {
|
if let Some(icon_path) = name {
|
||||||
return Err(ApiError::InvalidInputError(String::from(
|
file_host.delete_file_version("", icon_path).await?;
|
||||||
"Icons must be smaller than 256KiB",
|
}
|
||||||
)));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let upload_data = file_host
|
let mut bytes = web::BytesMut::new();
|
||||||
.upload_file(
|
while let Some(item) = payload.next().await {
|
||||||
content_type,
|
bytes.extend_from_slice(&item.map_err(|_| {
|
||||||
&format!("user/{}/icon.{}", id, ext.ext),
|
ApiError::InvalidInputError(
|
||||||
bytes.to_vec(),
|
"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?;
|
.await?;
|
||||||
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
let mod_id: crate::database::models::ids::UserId = id.into();
|
} else {
|
||||||
sqlx::query!(
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
"
|
}
|
||||||
UPDATE users
|
|
||||||
SET avatar_url = $1
|
|
||||||
WHERE (id = $2)
|
|
||||||
",
|
|
||||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
|
||||||
mod_id as crate::database::models::ids::UserId,
|
|
||||||
)
|
|
||||||
.execute(&**pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError::InvalidInputError(format!(
|
Err(ApiError::InvalidInputError(format!(
|
||||||
"Invalid format for user icon: {}",
|
"Invalid format for user icon: {}",
|
||||||
@@ -411,32 +380,34 @@ fn default_removal() -> String {
|
|||||||
#[delete("{id}")]
|
#[delete("{id}")]
|
||||||
pub async fn user_delete(
|
pub async fn user_delete(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(UserId,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
removal_type: web::Query<RemovalType>,
|
removal_type: web::Query<RemovalType>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
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 {
|
if let Some(id) = id_option {
|
||||||
return Err(ApiError::CustomAuthenticationError(
|
if !user.role.is_mod() && user.id != id.into() {
|
||||||
"You do not have permission to delete this user!".to_string(),
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
));
|
"You do not have permission to delete this user!".to_string(),
|
||||||
}
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
if &*removal_type.removal_type == "full" {
|
if &*removal_type.removal_type == "full" {
|
||||||
result = crate::database::models::User::remove_full(id.into(), &**pool)
|
result = crate::database::models::User::remove_full(id, &**pool).await?;
|
||||||
.await
|
} else {
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
result = crate::database::models::User::remove(id, &**pool).await?;
|
||||||
} else {
|
};
|
||||||
result = crate::database::models::User::remove(id.into(), &**pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
};
|
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
@@ -445,59 +416,68 @@ pub async fn user_delete(
|
|||||||
#[get("{id}/follows")]
|
#[get("{id}/follows")]
|
||||||
pub async fn user_follows(
|
pub async fn user_follows(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(UserId,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
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 {
|
if let Some(id) = id_option {
|
||||||
return Err(ApiError::CustomAuthenticationError(
|
if !user.role.is_mod() && user.id != id.into() {
|
||||||
"You do not have permission to see the mods this user follows!".to_string(),
|
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")]
|
#[get("{id}/notifications")]
|
||||||
pub async fn user_notifications(
|
pub async fn user_notifications(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(UserId,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
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 {
|
if let Some(id) = id_option {
|
||||||
return Err(ApiError::CustomAuthenticationError(
|
if !user.role.is_mod() && user.id != id.into() {
|
||||||
"You do not have permission to see the mods this user follows!".to_string(),
|
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::notification_item::NotificationBuilder;
|
||||||
use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder};
|
use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder};
|
||||||
use crate::file_hosting::FileHost;
|
use crate::file_hosting::FileHost;
|
||||||
use crate::models::mods::{
|
use crate::models::projects::{
|
||||||
Dependency, GameVersion, ModId, ModLoader, Version, VersionFile, VersionId, VersionType,
|
Dependency, GameVersion, Loader, ProjectId, Version, VersionFile, VersionId, VersionType,
|
||||||
};
|
};
|
||||||
use crate::models::teams::Permissions;
|
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_multipart::{Field, Multipart};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{post, HttpRequest, HttpResponse};
|
use actix_web::{post, HttpRequest, HttpResponse};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::postgres::PgPool;
|
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 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>,
|
pub file_parts: Vec<String>,
|
||||||
|
#[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")]
|
||||||
pub version_number: String,
|
pub version_number: String,
|
||||||
|
#[validate(length(min = 3, max = 256))]
|
||||||
pub version_title: String,
|
pub version_title: String,
|
||||||
|
#[validate(length(max = 65536))]
|
||||||
pub version_body: Option<String>,
|
pub version_body: Option<String>,
|
||||||
|
#[validate(length(min = 0, max = 256))]
|
||||||
pub dependencies: Vec<Dependency>,
|
pub dependencies: Vec<Dependency>,
|
||||||
pub game_versions: Vec<GameVersion>,
|
pub game_versions: Vec<GameVersion>,
|
||||||
pub release_channel: VersionType,
|
pub release_channel: VersionType,
|
||||||
pub loaders: Vec<ModLoader>,
|
pub loaders: Vec<Loader>,
|
||||||
pub featured: bool,
|
pub featured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,42 +48,6 @@ struct InitialFileData {
|
|||||||
// TODO: hashes?
|
// 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`
|
// under `/api/v1/version`
|
||||||
#[post("version")]
|
#[post("version")]
|
||||||
pub async fn version_create(
|
pub async fn version_create(
|
||||||
@@ -91,7 +69,8 @@ pub async fn version_create(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if result.is_err() {
|
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;
|
let rollback_result = transaction.rollback().await;
|
||||||
|
|
||||||
if let Err(e) = undo_result {
|
if let Err(e) = undo_result {
|
||||||
@@ -119,6 +98,8 @@ async fn version_create_inner(
|
|||||||
let mut initial_version_data = None;
|
let mut initial_version_data = None;
|
||||||
let mut version_builder = 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?;
|
let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||||
|
|
||||||
while let Some(item) = payload.next().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)?;
|
let version_create_data: InitialVersionData = serde_json::from_slice(&data)?;
|
||||||
initial_version_data = Some(version_create_data);
|
initial_version_data = Some(version_create_data);
|
||||||
let version_create_data = initial_version_data.as_ref().unwrap();
|
let version_create_data = initial_version_data.as_ref().unwrap();
|
||||||
if version_create_data.mod_id.is_none() {
|
if version_create_data.project_id.is_none() {
|
||||||
return Err(CreateError::MissingValueError("Missing mod id".to_string()));
|
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!(
|
let results = sqlx::query!(
|
||||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
||||||
mod_id as models::ModId
|
project_id as models::ProjectId
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *transaction)
|
.fetch_one(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !results.exists.unwrap_or(false) {
|
if !results.exists.unwrap_or(false) {
|
||||||
return Err(CreateError::InvalidInput(
|
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
|
// same version number
|
||||||
let results = sqlx::query!(
|
let results = sqlx::query!(
|
||||||
"SELECT EXISTS(SELECT 1 FROM versions WHERE (version_number=$1) AND (mod_id=$2))",
|
"SELECT EXISTS(SELECT 1 FROM versions WHERE (version_number=$1) AND (mod_id=$2))",
|
||||||
version_create_data.version_number,
|
version_create_data.version_number,
|
||||||
mod_id as models::ModId,
|
project_id as models::ProjectId,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *transaction)
|
.fetch_one(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -178,15 +161,18 @@ async fn version_create_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check that the user creating this version is a team member
|
// Check that the user creating this version is a team member
|
||||||
// of the mod the version is being added to.
|
// of the project the version is being added to.
|
||||||
let team_member =
|
let team_member = models::TeamMember::get_from_user_id_project(
|
||||||
models::TeamMember::get_from_user_id_mod(mod_id, user.id.into(), &mut *transaction)
|
project_id,
|
||||||
.await?
|
user.id.into(),
|
||||||
.ok_or_else(|| {
|
&mut *transaction,
|
||||||
CreateError::CustomAuthenticationError(
|
)
|
||||||
"You don't have permission to upload this version!".to_string(),
|
.await?
|
||||||
)
|
.ok_or_else(|| {
|
||||||
})?;
|
CreateError::CustomAuthenticationError(
|
||||||
|
"You don't have permission to upload this version!".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
if !team_member
|
if !team_member
|
||||||
.permissions
|
.permissions
|
||||||
@@ -206,13 +192,17 @@ async fn version_create_inner(
|
|||||||
.await?
|
.await?
|
||||||
.expect("Release channel not found in database");
|
.expect("Release channel not found in database");
|
||||||
|
|
||||||
let mut game_versions = Vec::with_capacity(version_create_data.game_versions.len());
|
let game_versions = version_create_data
|
||||||
for v in &version_create_data.game_versions {
|
.game_versions
|
||||||
let id = models::categories::GameVersion::get_id(&v.0, &mut *transaction)
|
.iter()
|
||||||
.await?
|
.map(|x| {
|
||||||
.ok_or_else(|| CreateError::InvalidGameVersion(v.0.clone()))?;
|
all_game_versions
|
||||||
game_versions.push(id);
|
.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());
|
let mut loaders = Vec::with_capacity(version_create_data.loaders.len());
|
||||||
for l in &version_create_data.loaders {
|
for l in &version_create_data.loaders {
|
||||||
@@ -230,7 +220,7 @@ async fn version_create_inner(
|
|||||||
|
|
||||||
version_builder = Some(VersionBuilder {
|
version_builder = Some(VersionBuilder {
|
||||||
version_id: version_id.into(),
|
version_id: version_id.into(),
|
||||||
mod_id: version_create_data.mod_id.unwrap().into(),
|
project_id,
|
||||||
author_id: user.id.into(),
|
author_id: user.id.into(),
|
||||||
name: version_create_data.version_title.clone(),
|
name: version_create_data.version_title.clone(),
|
||||||
version_number: version_create_data.version_number.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"))
|
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,
|
&mut field,
|
||||||
file_host,
|
file_host,
|
||||||
uploaded_files,
|
uploaded_files,
|
||||||
|
&mut version.files,
|
||||||
&cdn_url,
|
&cdn_url,
|
||||||
&content_disposition,
|
&content_disposition,
|
||||||
version.mod_id.into(),
|
version.project_id.into(),
|
||||||
&version.version_number,
|
&version.version_number,
|
||||||
|
&*project_type,
|
||||||
|
version_data.loaders,
|
||||||
|
version_data.game_versions,
|
||||||
|
&all_game_versions,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Add the newly uploaded file to the existing or new version
|
|
||||||
version.files.push(file_builder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let version_data = initial_version_data
|
let version_data = initial_version_data
|
||||||
@@ -278,7 +287,7 @@ async fn version_create_inner(
|
|||||||
SELECT m.title FROM mods m
|
SELECT m.title FROM mods m
|
||||||
WHERE id = $1
|
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)
|
.fetch_one(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -290,7 +299,7 @@ async fn version_create_inner(
|
|||||||
SELECT follower_id FROM mod_follows
|
SELECT follower_id FROM mod_follows
|
||||||
WHERE mod_id = $1
|
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)
|
.fetch_many(&mut *transaction)
|
||||||
.try_filter_map(|e| async {
|
.try_filter_map(|e| async {
|
||||||
@@ -300,17 +309,17 @@ async fn version_create_inner(
|
|||||||
.try_collect::<Vec<crate::database::models::ids::UserId>>()
|
.try_collect::<Vec<crate::database::models::ids::UserId>>()
|
||||||
.await?;
|
.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();
|
let version_id: VersionId = builder.version_id.into();
|
||||||
|
|
||||||
NotificationBuilder {
|
NotificationBuilder {
|
||||||
title: "A mod you followed has been updated!".to_string(),
|
title: "A project you followed has been updated!".to_string(),
|
||||||
text: format!(
|
text: format!(
|
||||||
"Mod {} has been updated to version {}",
|
"Project {} has been updated to version {}",
|
||||||
result.title,
|
result.title,
|
||||||
version_data.version_number.clone()
|
version_data.version_number.clone()
|
||||||
),
|
),
|
||||||
link: format!("mod/{}/version/{}", mod_id, version_id),
|
link: format!("project/{}/version/{}", project_id, version_id),
|
||||||
actions: vec![],
|
actions: vec![],
|
||||||
}
|
}
|
||||||
.insert_many(users, &mut *transaction)
|
.insert_many(users, &mut *transaction)
|
||||||
@@ -318,7 +327,7 @@ async fn version_create_inner(
|
|||||||
|
|
||||||
let response = Version {
|
let response = Version {
|
||||||
id: builder.version_id.into(),
|
id: builder.version_id.into(),
|
||||||
mod_id: builder.mod_id.into(),
|
project_id: builder.project_id.into(),
|
||||||
author_id: user.id,
|
author_id: user.id,
|
||||||
featured: builder.featured,
|
featured: builder.featured,
|
||||||
name: builder.name.clone(),
|
name: builder.name.clone(),
|
||||||
@@ -388,7 +397,8 @@ pub async fn upload_file_to_version(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if result.is_err() {
|
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;
|
let rollback_result = transaction.rollback().await;
|
||||||
|
|
||||||
if let Err(e) = undo_result {
|
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 user = get_user_from_headers(req.headers(), &mut *transaction).await?;
|
||||||
|
|
||||||
let result = sqlx::query!(
|
let result = models::Version::get_full(version_id, &mut *transaction).await?;
|
||||||
"
|
|
||||||
SELECT mod_id, version_number, author_id
|
|
||||||
FROM versions
|
|
||||||
WHERE id = $1
|
|
||||||
",
|
|
||||||
version_id as models::VersionId,
|
|
||||||
)
|
|
||||||
.fetch_optional(&mut *transaction)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let version = match result {
|
let version = match result {
|
||||||
Some(v) => v,
|
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 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 {
|
while let Some(item) = payload.next().await {
|
||||||
let mut field: Field = item.map_err(CreateError::MultipartError)?;
|
let mut field: Field = item.map_err(CreateError::MultipartError)?;
|
||||||
let content_disposition = field.content_disposition().ok_or_else(|| {
|
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"))
|
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let file_builder = upload_file(
|
upload_file(
|
||||||
&mut field,
|
&mut field,
|
||||||
file_host,
|
file_host,
|
||||||
uploaded_files,
|
uploaded_files,
|
||||||
|
&mut file_builders,
|
||||||
&cdn_url,
|
&cdn_url,
|
||||||
&content_disposition,
|
&content_disposition,
|
||||||
mod_id,
|
project_id,
|
||||||
&version_number,
|
&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?;
|
.await?;
|
||||||
|
|
||||||
// TODO: Malware scan + file validation
|
|
||||||
file_builders.push(file_builder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if file_builders.is_empty() {
|
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
|
// 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(
|
pub async fn upload_file(
|
||||||
field: &mut Field,
|
field: &mut Field,
|
||||||
file_host: &dyn FileHost,
|
file_host: &dyn FileHost,
|
||||||
uploaded_files: &mut Vec<UploadedFile>,
|
uploaded_files: &mut Vec<UploadedFile>,
|
||||||
|
version_files: &mut Vec<models::version_item::VersionFileBuilder>,
|
||||||
cdn_url: &str,
|
cdn_url: &str,
|
||||||
content_disposition: &actix_web::http::header::ContentDisposition,
|
content_disposition: &actix_web::http::header::ContentDisposition,
|
||||||
mod_id: crate::models::ids::ModId,
|
project_id: crate::models::ids::ProjectId,
|
||||||
version_number: &str,
|
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 (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()))?;
|
.ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?;
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
@@ -534,20 +564,32 @@ pub async fn upload_file(
|
|||||||
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
|
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);
|
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 {
|
if data.len() >= FILE_SIZE_CAP {
|
||||||
return Err(CreateError::InvalidInput(
|
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
|
let upload_data = file_host
|
||||||
.upload_file(
|
.upload_file(
|
||||||
content_type,
|
content_type,
|
||||||
&format!("data/{}/versions/{}/{}", mod_id, version_number, file_name),
|
&format!(
|
||||||
|
"data/{}/versions/{}/{}",
|
||||||
|
project_id, version_number, file_name
|
||||||
|
),
|
||||||
data.to_vec(),
|
data.to_vec(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -558,7 +600,7 @@ pub async fn upload_file(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Malware scan + file validation
|
// TODO: Malware scan + file validation
|
||||||
Ok(models::version_item::VersionFileBuilder {
|
version_files.push(models::version_item::VersionFileBuilder {
|
||||||
filename: file_name.to_string(),
|
filename: file_name.to_string(),
|
||||||
url: format!("{}/{}", cdn_url, upload_data.file_name),
|
url: format!("{}/{}", cdn_url, upload_data.file_name),
|
||||||
hashes: vec![
|
hashes: vec![
|
||||||
@@ -575,12 +617,16 @@ pub async fn upload_file(
|
|||||||
hash: upload_data.content_sha512.into_bytes(),
|
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?)
|
// Currently we only support jar projects; this may change in the future (datapacks?)
|
||||||
fn mod_file_type(ext: &str) -> Option<&str> {
|
fn project_file_type(ext: &str) -> Option<&str> {
|
||||||
match ext {
|
match ext {
|
||||||
"jar" => Some("application/java-archive"),
|
"jar" => Some("application/java-archive"),
|
||||||
_ => None,
|
_ => 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 super::ApiError;
|
||||||
use crate::auth::get_user_from_headers;
|
use crate::auth::get_user_from_headers;
|
||||||
use crate::file_hosting::FileHost;
|
use crate::database;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::mods::{Dependency, DependencyType};
|
use crate::models::projects::{Dependency, DependencyType};
|
||||||
use crate::models::teams::Permissions;
|
use crate::models::teams::Permissions;
|
||||||
use crate::{database, Pepper};
|
|
||||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::borrow::Borrow;
|
use validator::Validate;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct VersionListFilters {
|
pub struct VersionListFilters {
|
||||||
@@ -20,23 +20,18 @@ pub struct VersionListFilters {
|
|||||||
|
|
||||||
#[get("version")]
|
#[get("version")]
|
||||||
pub async fn version_list(
|
pub async fn version_list(
|
||||||
info: web::Path<(models::ids::ModId,)>,
|
info: web::Path<(String,)>,
|
||||||
web::Query(filters): web::Query<VersionListFilters>,
|
web::Query(filters): web::Query<VersionListFilters>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let id = info.into_inner().0.into();
|
let string = info.into_inner().0;
|
||||||
|
|
||||||
let mod_exists = sqlx::query!(
|
let result = database::models::Project::get_from_slug_or_project_id(string, &**pool).await?;
|
||||||
"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;
|
|
||||||
|
|
||||||
if mod_exists.unwrap_or(false) {
|
if let Some(project) = result {
|
||||||
let version_ids = database::models::Version::get_mod_versions(
|
let id = project.id;
|
||||||
|
|
||||||
|
let version_ids = database::models::Version::get_project_versions(
|
||||||
id,
|
id,
|
||||||
filters
|
filters
|
||||||
.game_versions
|
.game_versions
|
||||||
@@ -48,12 +43,9 @@ pub async fn version_list(
|
|||||||
.map(|x| serde_json::from_str(x).unwrap_or_default()),
|
.map(|x| serde_json::from_str(x).unwrap_or_default()),
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let mut versions = database::models::Version::get_many_full(version_ids, &**pool)
|
let mut versions = database::models::Version::get_many_full(version_ids, &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let mut response = versions
|
let mut response = versions
|
||||||
.iter()
|
.iter()
|
||||||
@@ -87,8 +79,8 @@ pub async fn version_list(
|
|||||||
versions
|
versions
|
||||||
.iter()
|
.iter()
|
||||||
.find(|version| {
|
.find(|version| {
|
||||||
version.game_versions.contains(&filter.0)
|
version.game_versions.contains(&filter.0.version)
|
||||||
&& version.loaders.contains(&filter.1)
|
&& version.loaders.contains(&filter.1.loader)
|
||||||
})
|
})
|
||||||
.map(|version| response.push(convert_version(version.clone())))
|
.map(|version| response.push(convert_version(version.clone())))
|
||||||
.unwrap_or(());
|
.unwrap_or(());
|
||||||
@@ -124,9 +116,7 @@ pub async fn versions_get(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| x.into())
|
.map(|x| x.into())
|
||||||
.collect();
|
.collect();
|
||||||
let versions_data = database::models::Version::get_many_full(version_ids, &**pool)
|
let versions_data = database::models::Version::get_many_full(version_ids, &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
let mut versions = Vec::new();
|
let mut versions = Vec::new();
|
||||||
|
|
||||||
@@ -143,9 +133,7 @@ pub async fn version_get(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let id = info.into_inner().0;
|
let id = info.into_inner().0;
|
||||||
let version_data = database::models::Version::get_full(id.into(), &**pool)
|
let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if let Some(data) = version_data {
|
if let Some(data) = version_data {
|
||||||
Ok(HttpResponse::Ok().json(convert_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 {
|
pub fn convert_version(
|
||||||
use models::mods::VersionType;
|
data: database::models::version_item::QueryVersion,
|
||||||
|
) -> models::projects::Version {
|
||||||
|
use models::projects::VersionType;
|
||||||
|
|
||||||
models::mods::Version {
|
models::projects::Version {
|
||||||
id: data.id.into(),
|
id: data.id.into(),
|
||||||
mod_id: data.mod_id.into(),
|
project_id: data.project_id.into(),
|
||||||
author_id: data.author_id.into(),
|
author_id: data.author_id.into(),
|
||||||
|
|
||||||
featured: data.featured,
|
featured: data.featured,
|
||||||
@@ -180,7 +170,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
|||||||
.files
|
.files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|f| {
|
.map(|f| {
|
||||||
models::mods::VersionFile {
|
models::projects::VersionFile {
|
||||||
url: f.url,
|
url: f.url,
|
||||||
filename: f.filename,
|
filename: f.filename,
|
||||||
// FIXME: Hashes are currently stored as an ascii byte slice instead
|
// 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: data
|
||||||
.game_versions
|
.game_versions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(models::mods::GameVersion)
|
.map(models::projects::GameVersion)
|
||||||
.collect(),
|
.collect(),
|
||||||
loaders: data
|
loaders: data
|
||||||
.loaders
|
.loaders
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(models::mods::ModLoader)
|
.map(models::projects::Loader)
|
||||||
.collect(),
|
.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 {
|
pub struct EditVersion {
|
||||||
|
#[validate(length(min = 3, max = 256))]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
#[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")]
|
||||||
pub version_number: Option<String>,
|
pub version_number: Option<String>,
|
||||||
|
#[validate(length(max = 65536))]
|
||||||
pub changelog: Option<String>,
|
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 dependencies: Option<Vec<Dependency>>,
|
||||||
pub game_versions: Option<Vec<models::mods::GameVersion>>,
|
pub game_versions: Option<Vec<models::projects::GameVersion>>,
|
||||||
pub loaders: Option<Vec<models::mods::ModLoader>>,
|
pub loaders: Option<Vec<models::projects::Loader>>,
|
||||||
pub featured: Option<bool>,
|
pub featured: Option<bool>,
|
||||||
pub primary_file: Option<(String, String)>,
|
pub primary_file: Option<(String, String)>,
|
||||||
}
|
}
|
||||||
@@ -238,12 +236,12 @@ pub async fn version_edit(
|
|||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
|
|
||||||
|
new_version.validate()?;
|
||||||
|
|
||||||
let version_id = info.into_inner().0;
|
let version_id = info.into_inner().0;
|
||||||
let id = version_id.into();
|
let id = version_id.into();
|
||||||
|
|
||||||
let result = database::models::Version::get_full(id, &**pool)
|
let result = database::models::Version::get_full(id, &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if let Some(version_item) = result {
|
if let Some(version_item) = result {
|
||||||
let team_member = database::models::TeamMember::get_from_user_id_version(
|
let team_member = database::models::TeamMember::get_from_user_id_version(
|
||||||
@@ -269,18 +267,9 @@ pub async fn version_edit(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut transaction = pool
|
let mut transaction = pool.begin().await?;
|
||||||
.begin()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if let Some(name) = &new_version.name {
|
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!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE versions
|
UPDATE versions
|
||||||
@@ -291,17 +280,10 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(number) = &new_version.version_number {
|
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!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE versions
|
UPDATE versions
|
||||||
@@ -312,8 +294,7 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(version_type) = &new_version.version_type {
|
if let Some(version_type) = &new_version.version_type {
|
||||||
@@ -338,8 +319,7 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dependencies) = &new_version.dependencies {
|
if let Some(dependencies) = &new_version.dependencies {
|
||||||
@@ -350,8 +330,7 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
for dependency in dependencies {
|
for dependency in dependencies {
|
||||||
let dependency_id: database::models::ids::VersionId =
|
let dependency_id: database::models::ids::VersionId =
|
||||||
@@ -367,8 +346,7 @@ pub async fn version_edit(
|
|||||||
dependency.dependency_type.as_str()
|
dependency.dependency_type.as_str()
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,8 +358,7 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
for game_version in game_versions {
|
for game_version in game_versions {
|
||||||
let game_version_id = database::models::categories::GameVersion::get_id(
|
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,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,8 +393,7 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
for loader in loaders {
|
for loader in loaders {
|
||||||
let loader_id =
|
let loader_id =
|
||||||
@@ -439,8 +414,7 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,8 +429,7 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(primary_file) = &new_version.primary_file {
|
if let Some(primary_file) = &new_version.primary_file {
|
||||||
@@ -470,8 +443,7 @@ pub async fn version_edit(
|
|||||||
primary_file.0
|
primary_file.0
|
||||||
)
|
)
|
||||||
.fetch_optional(&**pool)
|
.fetch_optional(&**pool)
|
||||||
.await
|
.await?
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::InvalidInputError(format!(
|
ApiError::InvalidInputError(format!(
|
||||||
"Specified file with hash {} does not exist.",
|
"Specified file with hash {} does not exist.",
|
||||||
@@ -488,8 +460,7 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -500,18 +471,10 @@ pub async fn version_edit(
|
|||||||
result.id,
|
result.id,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(body) = &new_version.changelog {
|
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!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE versions
|
UPDATE versions
|
||||||
@@ -522,15 +485,11 @@ pub async fn version_edit(
|
|||||||
id as database::models::ids::VersionId,
|
id as database::models::ids::VersionId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction
|
transaction.commit().await?;
|
||||||
.commit()
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
Ok(HttpResponse::Ok().body(""))
|
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError::CustomAuthenticationError(
|
Err(ApiError::CustomAuthenticationError(
|
||||||
"You do not have permission to edit this version!".to_string(),
|
"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)
|
let result = database::models::Version::remove_full(id.into(), &**pool).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::NoContent().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(""))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,32 @@ use futures::{StreamExt, TryStreamExt};
|
|||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use super::IndexingError;
|
use super::IndexingError;
|
||||||
use crate::models::mods::SideType;
|
use crate::models::projects::SideType;
|
||||||
use crate::search::UploadSearchMod;
|
use crate::search::UploadSearchProject;
|
||||||
use sqlx::postgres::PgPool;
|
use sqlx::postgres::PgPool;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
// TODO: only loaders for recent versions? For mods that have moved from forge to fabric
|
// TODO: only loaders for recent versions? For projects that have moved from forge to fabric
|
||||||
pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingError> {
|
pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, IndexingError> {
|
||||||
info!("Indexing local mods!");
|
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
|
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);
|
).fetch(&pool);
|
||||||
|
|
||||||
while let Some(result) = mods.next().await {
|
while let Some(result) = projects.next().await {
|
||||||
if let Ok(mod_data) = result {
|
if let Ok(project_data) = result {
|
||||||
let status = crate::models::mods::ModStatus::from_str(
|
let status = crate::models::projects::ProjectStatus::from_str(
|
||||||
&sqlx::query!(
|
&sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT status FROM statuses
|
SELECT status FROM statuses
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
mod_data.status,
|
project_data.status,
|
||||||
)
|
)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await?
|
.await?
|
||||||
@@ -46,7 +46,7 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
|||||||
WHERE versions.mod_id = $1
|
WHERE versions.mod_id = $1
|
||||||
ORDER BY gv.created ASC
|
ORDER BY gv.created ASC
|
||||||
",
|
",
|
||||||
mod_data.id
|
project_data.id
|
||||||
)
|
)
|
||||||
.fetch_many(&pool)
|
.fetch_many(&pool)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
.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
|
INNER JOIN loaders ON loaders.id = lv.loader_id
|
||||||
WHERE versions.mod_id = $1
|
WHERE versions.mod_id = $1
|
||||||
",
|
",
|
||||||
mod_data.id
|
project_data.id
|
||||||
)
|
)
|
||||||
.fetch_many(&pool)
|
.fetch_many(&pool)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.loader))) })
|
.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
|
INNER JOIN categories c ON mc.joining_category_id=c.id
|
||||||
WHERE mc.joining_mod_id = $1
|
WHERE mc.joining_mod_id = $1
|
||||||
",
|
",
|
||||||
mod_data.id
|
project_data.id
|
||||||
)
|
)
|
||||||
.fetch_many(&pool)
|
.fetch_many(&pool)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.category))) })
|
.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
|
WHERE tm.team_id = $2 AND tm.role = $1
|
||||||
",
|
",
|
||||||
crate::models::teams::OWNER_ROLE,
|
crate::models::teams::OWNER_ROLE,
|
||||||
mod_data.team_id,
|
project_data.team_id,
|
||||||
)
|
)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut icon_url = "".to_string();
|
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;
|
icon_url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mod_id = crate::models::ids::ModId(mod_data.id as u64);
|
let project_id = crate::models::ids::ProjectId(project_data.id as u64);
|
||||||
let author_id = crate::models::ids::UserId(user.id as u64);
|
|
||||||
|
|
||||||
// TODO: is this correct? This just gets the latest version of
|
// 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.
|
// take betas or other info into account.
|
||||||
let latest_version = versions
|
let latest_version = versions
|
||||||
.last()
|
.last()
|
||||||
@@ -116,10 +115,10 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
|||||||
let client_side = SideType::from_str(
|
let client_side = SideType::from_str(
|
||||||
&sqlx::query!(
|
&sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT name FROM side_types
|
SELECT name FROM side_types
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
mod_data.client_side,
|
project_data.client_side,
|
||||||
)
|
)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await?
|
.await?
|
||||||
@@ -129,10 +128,10 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
|||||||
let server_side = SideType::from_str(
|
let server_side = SideType::from_str(
|
||||||
&sqlx::query!(
|
&sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT name FROM side_types
|
SELECT name FROM side_types
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
mod_data.server_side,
|
project_data.server_side,
|
||||||
)
|
)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await?
|
.await?
|
||||||
@@ -140,33 +139,31 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
|||||||
);
|
);
|
||||||
|
|
||||||
let license = crate::database::models::categories::License::get(
|
let license = crate::database::models::categories::License::get(
|
||||||
crate::database::models::LicenseId(mod_data.license),
|
crate::database::models::LicenseId(project_data.license),
|
||||||
&pool,
|
&pool,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
docs_to_add.push(UploadSearchMod {
|
docs_to_add.push(UploadSearchProject {
|
||||||
mod_id: format!("local-{}", mod_id),
|
project_id: format!("local-{}", project_id),
|
||||||
title: mod_data.title,
|
title: project_data.title,
|
||||||
description: mod_data.description,
|
description: project_data.description,
|
||||||
categories,
|
categories,
|
||||||
versions,
|
versions,
|
||||||
follows: mod_data.follows,
|
follows: project_data.follows,
|
||||||
downloads: mod_data.downloads,
|
downloads: project_data.downloads,
|
||||||
page_url: format!("https://modrinth.com/mod/{}", mod_id),
|
|
||||||
icon_url,
|
icon_url,
|
||||||
author: user.username,
|
author: user.username,
|
||||||
author_url: format!("https://modrinth.com/user/{}", author_id),
|
date_created: project_data.published,
|
||||||
date_created: mod_data.published,
|
created_timestamp: project_data.published.timestamp(),
|
||||||
created_timestamp: mod_data.published.timestamp(),
|
date_modified: project_data.updated,
|
||||||
date_modified: mod_data.updated,
|
modified_timestamp: project_data.updated.timestamp(),
|
||||||
modified_timestamp: mod_data.updated.timestamp(),
|
|
||||||
latest_version,
|
latest_version,
|
||||||
license: license.short,
|
license: license.short,
|
||||||
client_side: client_side.to_string(),
|
client_side: client_side.to_string(),
|
||||||
server_side: server_side.to_string(),
|
server_side: server_side.to_string(),
|
||||||
host: Cow::Borrowed("modrinth"),
|
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(
|
pub async fn query_one(
|
||||||
id: crate::database::models::ModId,
|
id: crate::database::models::ProjectId,
|
||||||
exec: &mut sqlx::PgConnection,
|
exec: &mut sqlx::PgConnection,
|
||||||
) -> Result<UploadSearchMod, IndexingError> {
|
) -> Result<UploadSearchProject, IndexingError> {
|
||||||
let mod_data = sqlx::query!(
|
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
|
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
|
FROM mods m
|
||||||
@@ -195,7 +192,7 @@ pub async fn query_one(
|
|||||||
WHERE versions.mod_id = $1
|
WHERE versions.mod_id = $1
|
||||||
ORDER BY gv.created ASC
|
ORDER BY gv.created ASC
|
||||||
",
|
",
|
||||||
mod_data.id
|
project_data.id
|
||||||
)
|
)
|
||||||
.fetch_many(&mut *exec)
|
.fetch_many(&mut *exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
.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
|
INNER JOIN loaders ON loaders.id = lv.loader_id
|
||||||
WHERE versions.mod_id = $1
|
WHERE versions.mod_id = $1
|
||||||
",
|
",
|
||||||
mod_data.id
|
project_data.id
|
||||||
)
|
)
|
||||||
.fetch_many(&mut *exec)
|
.fetch_many(&mut *exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.loader))) })
|
.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
|
INNER JOIN categories c ON mc.joining_category_id=c.id
|
||||||
WHERE mc.joining_mod_id = $1
|
WHERE mc.joining_mod_id = $1
|
||||||
",
|
",
|
||||||
mod_data.id
|
project_data.id
|
||||||
)
|
)
|
||||||
.fetch_many(&mut *exec)
|
.fetch_many(&mut *exec)
|
||||||
.try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.category))) })
|
.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
|
WHERE tm.team_id = $2 AND tm.role = $1
|
||||||
",
|
",
|
||||||
crate::models::teams::OWNER_ROLE,
|
crate::models::teams::OWNER_ROLE,
|
||||||
mod_data.team_id,
|
project_data.team_id,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *exec)
|
.fetch_one(&mut *exec)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut icon_url = "".to_string();
|
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;
|
icon_url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mod_id = crate::models::ids::ModId(mod_data.id as u64);
|
let project_id = crate::models::ids::ProjectId(project_data.id as u64);
|
||||||
let author_id = crate::models::ids::UserId(user.id as u64);
|
|
||||||
|
|
||||||
// TODO: is this correct? This just gets the latest version of
|
// 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.
|
// take betas or other info into account.
|
||||||
let latest_version = versions
|
let latest_version = versions
|
||||||
.last()
|
.last()
|
||||||
@@ -265,10 +261,10 @@ pub async fn query_one(
|
|||||||
let client_side = SideType::from_str(
|
let client_side = SideType::from_str(
|
||||||
&sqlx::query!(
|
&sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT name FROM side_types
|
SELECT name FROM side_types
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
mod_data.client_side,
|
project_data.client_side,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *exec)
|
.fetch_one(&mut *exec)
|
||||||
.await?
|
.await?
|
||||||
@@ -278,10 +274,10 @@ pub async fn query_one(
|
|||||||
let server_side = SideType::from_str(
|
let server_side = SideType::from_str(
|
||||||
&sqlx::query!(
|
&sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT name FROM side_types
|
SELECT name FROM side_types
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
mod_data.server_side,
|
project_data.server_side,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *exec)
|
.fetch_one(&mut *exec)
|
||||||
.await?
|
.await?
|
||||||
@@ -289,32 +285,30 @@ pub async fn query_one(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let license = crate::database::models::categories::License::get(
|
let license = crate::database::models::categories::License::get(
|
||||||
crate::database::models::LicenseId(mod_data.license),
|
crate::database::models::LicenseId(project_data.license),
|
||||||
&mut *exec,
|
&mut *exec,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(UploadSearchMod {
|
Ok(UploadSearchProject {
|
||||||
mod_id: format!("local-{}", mod_id),
|
project_id: format!("local-{}", project_id),
|
||||||
title: mod_data.title,
|
title: project_data.title,
|
||||||
description: mod_data.description,
|
description: project_data.description,
|
||||||
categories,
|
categories,
|
||||||
versions,
|
versions,
|
||||||
follows: mod_data.follows,
|
follows: project_data.follows,
|
||||||
downloads: mod_data.downloads,
|
downloads: project_data.downloads,
|
||||||
page_url: format!("https://modrinth.com/mod/{}", mod_id),
|
|
||||||
icon_url,
|
icon_url,
|
||||||
author: user.username,
|
author: user.username,
|
||||||
author_url: format!("https://modrinth.com/user/{}", author_id),
|
date_created: project_data.published,
|
||||||
date_created: mod_data.published,
|
created_timestamp: project_data.published.timestamp(),
|
||||||
created_timestamp: mod_data.published.timestamp(),
|
date_modified: project_data.updated,
|
||||||
date_modified: mod_data.updated,
|
modified_timestamp: project_data.updated.timestamp(),
|
||||||
modified_timestamp: mod_data.updated.timestamp(),
|
|
||||||
latest_version,
|
latest_version,
|
||||||
license: license.short,
|
license: license.short,
|
||||||
client_side: client_side.to_string(),
|
client_side: client_side.to_string(),
|
||||||
server_side: server_side.to_string(),
|
server_side: server_side.to_string(),
|
||||||
host: Cow::Borrowed("modrinth"),
|
host: Cow::Borrowed("modrinth"),
|
||||||
slug: mod_data.slug,
|
slug: project_data.slug,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
pub mod local_import;
|
pub mod local_import;
|
||||||
pub mod queue;
|
pub mod queue;
|
||||||
|
|
||||||
use crate::search::{SearchConfig, UploadSearchMod};
|
use crate::search::{SearchConfig, UploadSearchProject};
|
||||||
use local_import::index_local;
|
use local_import::index_local;
|
||||||
use meilisearch_sdk::client::Client;
|
use meilisearch_sdk::client::Client;
|
||||||
use meilisearch_sdk::indexes::Index;
|
use meilisearch_sdk::indexes::Index;
|
||||||
@@ -27,14 +27,13 @@ pub enum IndexingError {
|
|||||||
EnvError(#[from] dotenv::Error),
|
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
|
// 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;
|
const MEILISEARCH_CHUNK_SIZE: usize = 10000;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct IndexingSettings {
|
pub struct IndexingSettings {
|
||||||
pub index_external: bool,
|
|
||||||
pub index_local: bool,
|
pub index_local: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,31 +41,24 @@ impl IndexingSettings {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
let index_local = true;
|
let index_local = true;
|
||||||
let index_external = dotenv::var("INDEX_CURSEFORGE")
|
|
||||||
.ok()
|
|
||||||
.and_then(|b| b.parse::<bool>().ok())
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
Self {
|
Self { index_local }
|
||||||
index_external,
|
|
||||||
index_local,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn index_mods(
|
pub async fn index_projects(
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
settings: IndexingSettings,
|
settings: IndexingSettings,
|
||||||
config: &SearchConfig,
|
config: &SearchConfig,
|
||||||
) -> Result<(), IndexingError> {
|
) -> Result<(), IndexingError> {
|
||||||
let mut docs_to_add: Vec<UploadSearchMod> = vec![];
|
let mut docs_to_add: Vec<UploadSearchProject> = vec![];
|
||||||
|
|
||||||
if settings.index_local {
|
if settings.index_local {
|
||||||
docs_to_add.append(&mut index_local(pool.clone()).await?);
|
docs_to_add.append(&mut index_local(pool.clone()).await?);
|
||||||
}
|
}
|
||||||
// Write Indices
|
// Write Indices
|
||||||
|
|
||||||
add_mods(docs_to_add, config).await?;
|
add_projects(docs_to_add, config).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -74,12 +66,12 @@ pub async fn index_mods(
|
|||||||
pub async fn reset_indices(config: &SearchConfig) -> Result<(), IndexingError> {
|
pub async fn reset_indices(config: &SearchConfig) -> Result<(), IndexingError> {
|
||||||
let client = Client::new(&*config.address, &*config.key);
|
let client = Client::new(&*config.address, &*config.key);
|
||||||
|
|
||||||
client.delete_index("relevance_mods").await?;
|
client.delete_index("relevance_projects").await?;
|
||||||
client.delete_index("downloads_mods").await?;
|
client.delete_index("downloads_projects").await?;
|
||||||
client.delete_index("follows_mods").await?;
|
client.delete_index("follows_projects").await?;
|
||||||
client.delete_index("alphabetically_mods").await?;
|
client.delete_index("updated_projects").await?;
|
||||||
client.delete_index("updated_mods").await?;
|
client.delete_index("newest_projects").await?;
|
||||||
client.delete_index("newest_mods").await?;
|
client.delete_index("alphabetically_projects").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +79,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
|||||||
let client = Client::new(&*config.address, &*config.key);
|
let client = Client::new(&*config.address, &*config.key);
|
||||||
|
|
||||||
// Relevance Index
|
// Relevance Index
|
||||||
update_index(&client, "relevance_mods", {
|
update_index(&client, "relevance_projects", {
|
||||||
let mut relevance_rules = default_rules();
|
let mut relevance_rules = default_rules();
|
||||||
relevance_rules.push_back("desc(downloads)".to_string());
|
relevance_rules.push_back("desc(downloads)".to_string());
|
||||||
relevance_rules.into()
|
relevance_rules.into()
|
||||||
@@ -95,7 +87,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Downloads Index
|
// Downloads Index
|
||||||
update_index(&client, "downloads_mods", {
|
update_index(&client, "downloads_projects", {
|
||||||
let mut downloads_rules = default_rules();
|
let mut downloads_rules = default_rules();
|
||||||
downloads_rules.push_front("desc(downloads)".to_string());
|
downloads_rules.push_front("desc(downloads)".to_string());
|
||||||
downloads_rules.into()
|
downloads_rules.into()
|
||||||
@@ -103,7 +95,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Follows Index
|
// Follows Index
|
||||||
update_index(&client, "follows_mods", {
|
update_index(&client, "follows_projects", {
|
||||||
let mut follows_rules = default_rules();
|
let mut follows_rules = default_rules();
|
||||||
follows_rules.push_front("desc(follows)".to_string());
|
follows_rules.push_front("desc(follows)".to_string());
|
||||||
follows_rules.into()
|
follows_rules.into()
|
||||||
@@ -111,15 +103,15 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Alphabetically Index
|
// Alphabetically Index
|
||||||
update_index(&client, "alphabetically_mods", {
|
update_index(&client, "alphabetically_projects", {
|
||||||
let mut alphabetically_rules = default_rules();
|
let mut alphabetically_rules = default_rules();
|
||||||
alphabetically_rules.push_front("desc(title)".to_string());
|
alphabetically_rules.push_front("desc(title)".to_string());
|
||||||
alphabetically_rules.into()
|
alphabetically_rules.into()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Updated Index
|
// Updated Index
|
||||||
update_index(&client, "updated_mods", {
|
update_index(&client, "updated_projects", {
|
||||||
let mut updated_rules = default_rules();
|
let mut updated_rules = default_rules();
|
||||||
updated_rules.push_front("desc(modified_timestamp)".to_string());
|
updated_rules.push_front("desc(modified_timestamp)".to_string());
|
||||||
updated_rules.into()
|
updated_rules.into()
|
||||||
@@ -127,7 +119,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Created Index
|
// Created Index
|
||||||
update_index(&client, "newest_mods", {
|
update_index(&client, "newest_projects", {
|
||||||
let mut newest_rules = default_rules();
|
let mut newest_rules = default_rules();
|
||||||
newest_rules.push_front("desc(created_timestamp)".to_string());
|
newest_rules.push_front("desc(created_timestamp)".to_string());
|
||||||
newest_rules.into()
|
newest_rules.into()
|
||||||
@@ -147,7 +139,7 @@ async fn update_index<'a>(
|
|||||||
Err(meilisearch_sdk::errors::Error::MeiliSearchError {
|
Err(meilisearch_sdk::errors::Error::MeiliSearchError {
|
||||||
error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound,
|
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) => {
|
Err(e) => {
|
||||||
return Err(IndexingError::IndexDBError(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
|
// 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
|
index
|
||||||
.set_settings(&default_settings().with_ranking_rules(rules()))
|
.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) {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_mods(
|
pub async fn add_projects(
|
||||||
mods: Vec<UploadSearchMod>,
|
projects: Vec<UploadSearchProject>,
|
||||||
config: &SearchConfig,
|
config: &SearchConfig,
|
||||||
) -> Result<(), IndexingError> {
|
) -> Result<(), IndexingError> {
|
||||||
let client = Client::new(&*config.address, &*config.key);
|
let client = Client::new(&*config.address, &*config.key);
|
||||||
|
|
||||||
// Relevance Index
|
// Relevance Index
|
||||||
let relevance_index = create_index(&client, "relevance_mods", || {
|
let relevance_index = create_index(&client, "relevance_projects", || {
|
||||||
let mut relevance_rules = default_rules();
|
let mut relevance_rules = default_rules();
|
||||||
relevance_rules.push_back("desc(downloads)".to_string());
|
relevance_rules.push_back("desc(downloads)".to_string());
|
||||||
relevance_rules.into()
|
relevance_rules.into()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
add_to_index(relevance_index, &mods).await?;
|
add_to_index(relevance_index, &projects).await?;
|
||||||
|
|
||||||
// Downloads Index
|
// Downloads Index
|
||||||
let downloads_index = create_index(&client, "downloads_mods", || {
|
let downloads_index = create_index(&client, "downloads_projects", || {
|
||||||
let mut downloads_rules = default_rules();
|
let mut downloads_rules = default_rules();
|
||||||
downloads_rules.push_front("desc(downloads)".to_string());
|
downloads_rules.push_front("desc(downloads)".to_string());
|
||||||
downloads_rules.into()
|
downloads_rules.into()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
add_to_index(downloads_index, &mods).await?;
|
add_to_index(downloads_index, &projects).await?;
|
||||||
|
|
||||||
// Follows Index
|
// Follows Index
|
||||||
let follows_index = create_index(&client, "follows_mods", || {
|
let follows_index = create_index(&client, "follows_projects", || {
|
||||||
let mut follows_rules = default_rules();
|
let mut follows_rules = default_rules();
|
||||||
follows_rules.push_front("desc(follows)".to_string());
|
follows_rules.push_front("desc(follows)".to_string());
|
||||||
follows_rules.into()
|
follows_rules.into()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
add_to_index(follows_index, &mods).await?;
|
add_to_index(follows_index, &projects).await?;
|
||||||
|
|
||||||
// Alphabetically Index
|
// Alphabetically Index
|
||||||
let alphabetically_index = create_index(&client, "alphabetically_mods", || {
|
let alphabetically_index = create_index(&client, "alphabetically_projects", || {
|
||||||
let mut alphabetically_rules = default_rules();
|
let mut alphabetically_rules = default_rules();
|
||||||
alphabetically_rules.push_front("desc(title)".to_string());
|
alphabetically_rules.push_front("desc(title)".to_string());
|
||||||
alphabetically_rules.into()
|
alphabetically_rules.into()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
add_to_index(alphabetically_index, &mods).await?;
|
add_to_index(alphabetically_index, &projects).await?;
|
||||||
|
|
||||||
// Updated Index
|
// Updated Index
|
||||||
let updated_index = create_index(&client, "updated_mods", || {
|
let updated_index = create_index(&client, "updated_projects", || {
|
||||||
let mut updated_rules = default_rules();
|
let mut updated_rules = default_rules();
|
||||||
updated_rules.push_front("desc(modified_timestamp)".to_string());
|
updated_rules.push_front("desc(modified_timestamp)".to_string());
|
||||||
updated_rules.into()
|
updated_rules.into()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
add_to_index(updated_index, &mods).await?;
|
add_to_index(updated_index, &projects).await?;
|
||||||
|
|
||||||
// Created Index
|
// Created Index
|
||||||
let newest_index = create_index(&client, "newest_mods", || {
|
let newest_index = create_index(&client, "newest_projects", || {
|
||||||
let mut newest_rules = default_rules();
|
let mut newest_rules = default_rules();
|
||||||
newest_rules.push_front("desc(created_timestamp)".to_string());
|
newest_rules.push_front("desc(created_timestamp)".to_string());
|
||||||
newest_rules.into()
|
newest_rules.into()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
add_to_index(newest_index, &mods).await?;
|
add_to_index(newest_index, &projects).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -271,7 +263,7 @@ fn default_rules() -> VecDeque<String> {
|
|||||||
|
|
||||||
fn default_settings() -> Settings {
|
fn default_settings() -> Settings {
|
||||||
let displayed_attributes = vec![
|
let displayed_attributes = vec![
|
||||||
"mod_id".to_string(),
|
"project_id".to_string(),
|
||||||
"slug".to_string(),
|
"slug".to_string(),
|
||||||
"author".to_string(),
|
"author".to_string(),
|
||||||
"title".to_string(),
|
"title".to_string(),
|
||||||
@@ -280,16 +272,13 @@ fn default_settings() -> Settings {
|
|||||||
"versions".to_string(),
|
"versions".to_string(),
|
||||||
"downloads".to_string(),
|
"downloads".to_string(),
|
||||||
"follows".to_string(),
|
"follows".to_string(),
|
||||||
"page_url".to_string(),
|
|
||||||
"icon_url".to_string(),
|
"icon_url".to_string(),
|
||||||
"author_url".to_string(),
|
|
||||||
"date_created".to_string(),
|
"date_created".to_string(),
|
||||||
"date_modified".to_string(),
|
"date_modified".to_string(),
|
||||||
"latest_version".to_string(),
|
"latest_version".to_string(),
|
||||||
"license".to_string(),
|
"license".to_string(),
|
||||||
"client_side".to_string(),
|
"client_side".to_string(),
|
||||||
"server_side".to_string(),
|
"server_side".to_string(),
|
||||||
"host".to_string(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let searchable_attributes = vec![
|
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
|
// 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.
|
// keeping this mess in case someone needs it in the future.
|
||||||
#[allow(dead_code)]
|
#[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;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
let cmp = a.contains('.').cmp(&b.contains('.'));
|
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 crate::search::SearchConfig;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ pub struct CreationQueue {
|
|||||||
// and I don't think this can deadlock. This queue requires fast
|
// and I don't think this can deadlock. This queue requires fast
|
||||||
// writes and then a single potentially slower read/write that
|
// writes and then a single potentially slower read/write that
|
||||||
// empties the queue.
|
// empties the queue.
|
||||||
queue: Mutex<Vec<UploadSearchMod>>,
|
queue: Mutex<Vec<UploadSearchProject>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreationQueue {
|
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
|
// 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))
|
std::mem::replace(&mut *self.queue.lock().unwrap(), Vec::with_capacity(10))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,5 +31,5 @@ pub async fn index_queue(
|
|||||||
config: &SearchConfig,
|
config: &SearchConfig,
|
||||||
) -> Result<(), IndexingError> {
|
) -> Result<(), IndexingError> {
|
||||||
let queue = queue.take();
|
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::error::ApiError;
|
||||||
use crate::models::mods::SearchRequest;
|
use crate::models::projects::SearchRequest;
|
||||||
use actix_web::http::StatusCode;
|
use actix_web::http::StatusCode;
|
||||||
use actix_web::web::HttpResponse;
|
use actix_web::web::HttpResponse;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
@@ -57,11 +57,11 @@ pub struct SearchConfig {
|
|||||||
pub key: String,
|
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.
|
/// This contains some extra data that is not returned by search results.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct UploadSearchMod {
|
pub struct UploadSearchProject {
|
||||||
pub mod_id: String,
|
pub project_id: String,
|
||||||
pub slug: Option<String>,
|
pub slug: Option<String>,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -70,17 +70,15 @@ pub struct UploadSearchMod {
|
|||||||
pub versions: Vec<String>,
|
pub versions: Vec<String>,
|
||||||
pub follows: i32,
|
pub follows: i32,
|
||||||
pub downloads: i32,
|
pub downloads: i32,
|
||||||
pub page_url: String,
|
|
||||||
pub icon_url: String,
|
pub icon_url: String,
|
||||||
pub author_url: String,
|
|
||||||
pub latest_version: Cow<'static, str>,
|
pub latest_version: Cow<'static, str>,
|
||||||
pub license: String,
|
pub license: String,
|
||||||
pub client_side: String,
|
pub client_side: String,
|
||||||
pub server_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>,
|
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,
|
pub created_timestamp: i64,
|
||||||
/// RFC 3339 formatted date/time of last major modification (update)
|
/// RFC 3339 formatted date/time of last major modification (update)
|
||||||
pub date_modified: DateTime<Utc>,
|
pub date_modified: DateTime<Utc>,
|
||||||
@@ -92,15 +90,15 @@ pub struct UploadSearchMod {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct SearchResults {
|
pub struct SearchResults {
|
||||||
pub hits: Vec<ResultSearchMod>,
|
pub hits: Vec<ResultSearchProject>,
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
pub total_hits: usize,
|
pub total_hits: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct ResultSearchMod {
|
pub struct ResultSearchProject {
|
||||||
pub mod_id: String,
|
pub project_id: String,
|
||||||
pub slug: Option<String>,
|
pub slug: Option<String>,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -110,39 +108,34 @@ pub struct ResultSearchMod {
|
|||||||
pub versions: Vec<String>,
|
pub versions: Vec<String>,
|
||||||
pub downloads: i32,
|
pub downloads: i32,
|
||||||
pub follows: i32,
|
pub follows: i32,
|
||||||
pub page_url: String,
|
|
||||||
pub icon_url: String,
|
pub icon_url: String,
|
||||||
pub author_url: String,
|
/// RFC 3339 formatted creation date of the project
|
||||||
/// RFC 3339 formatted creation date of the mod
|
|
||||||
pub date_created: String,
|
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 date_modified: String,
|
||||||
pub latest_version: String,
|
pub latest_version: String,
|
||||||
pub license: String,
|
pub license: String,
|
||||||
pub client_side: String,
|
pub client_side: String,
|
||||||
pub server_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;
|
type UIDType = String;
|
||||||
|
|
||||||
fn get_uid(&self) -> &Self::UIDType {
|
fn get_uid(&self) -> &Self::UIDType {
|
||||||
&self.mod_id
|
&self.project_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Document for ResultSearchMod {
|
impl Document for ResultSearchProject {
|
||||||
type UIDType = String;
|
type UIDType = String;
|
||||||
|
|
||||||
fn get_uid(&self) -> &Self::UIDType {
|
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,
|
info: &SearchRequest,
|
||||||
config: &SearchConfig,
|
config: &SearchConfig,
|
||||||
) -> Result<SearchResults, SearchError> {
|
) -> Result<SearchResults, SearchError> {
|
||||||
@@ -160,12 +153,12 @@ pub async fn search_for_mod(
|
|||||||
let limit = info.limit.as_deref().unwrap_or("10").parse()?;
|
let limit = info.limit.as_deref().unwrap_or("10").parse()?;
|
||||||
|
|
||||||
let index = match index {
|
let index = match index {
|
||||||
"relevance" => "relevance_mods",
|
"relevance" => "relevance_projects",
|
||||||
"downloads" => "downloads_mods",
|
"downloads" => "downloads_projects",
|
||||||
"follows" => "follows_mods",
|
"follows" => "follows_projects",
|
||||||
"alphabetically" => "alphabetically_mods",
|
"updated" => "updated_projects",
|
||||||
"updated" => "updated_mods",
|
"newest" => "newest_projects",
|
||||||
"newest" => "newest_mods",
|
"alphabetically" => "alphabetically_projects",
|
||||||
i => return Err(SearchError::InvalidIndex(i.to_string())),
|
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);
|
query.with_facet_filters(&why_must_you_do_this);
|
||||||
}
|
}
|
||||||
|
|
||||||
let results = query.execute::<ResultSearchMod>().await?;
|
let results = query.execute::<ResultSearchProject>().await?;
|
||||||
|
|
||||||
Ok(SearchResults {
|
Ok(SearchResults {
|
||||||
hits: results.hits.into_iter().map(|r| r.result).collect(),
|
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