diff --git a/migrations/20201014165954_create-statuses.sql b/migrations/20201014165954_create-statuses.sql new file mode 100644 index 000000000..2fcdcc7ef --- /dev/null +++ b/migrations/20201014165954_create-statuses.sql @@ -0,0 +1,15 @@ +CREATE TABLE statuses ( + id serial PRIMARY KEY UNIQUE NOT NULL, + status varchar(64) UNIQUE NOT NULL +); + +ALTER TABLE mods +ADD COLUMN status integer REFERENCES statuses NOT NULL; +ALTER TABLE mods +ADD COLUMN updated timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP; + +INSERT INTO statuses (status) VALUES ('approved'); +INSERT INTO statuses (status) VALUES ('rejected'); +INSERT INTO statuses (status) VALUES ('draft'); +INSERT INTO statuses (status) VALUES ('unlisted'); +INSERT INTO statuses (status) VALUES ('processing'); \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index 6ef14c6ea..15a8b4bf9 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1,5 +1,45 @@ { "db": "PostgreSQL", + "01858339ec93e444b83c5d8793285d7b03982344770221c8a5a9bab9958d78ff": { + "query": "\n SELECT id\n FROM release_channels\n WHERE channel = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "02d8895627dfe108735a6e10ad63239348b71b3322b4734526a2646f17aedf05": { + "query": "\n SELECT status FROM statuses\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, "03209c5bda2d704e688439919a7b3903db6ad7caebf7ddafb3ea52d312d47bfb": { "query": "\n INSERT INTO users (\n id, github_id, username, name, email,\n avatar_url, bio, created\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8\n )\n ", "describe": { @@ -251,48 +291,8 @@ ] } }, - "1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": { - "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - } - }, - "25131559cb73a088000ab6379a769233440ade6c7511542da410065190d203fc": { - "query": "\n SELECT id FROM loaders\n WHERE loader = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false - ] - } - }, - "268efd8c8145bbb2bc70bd5ae0a5e6f90c5917ffd18575afcdeaf78b4f895d1e": { - "query": "\n SELECT id, title, description, downloads,\n icon_url, body_url, published,\n issues_url, source_url, wiki_url,\n team_id\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "1d144ed2bfb98d93f0515bb663708f2f9eff26548a50383f4013ba80305f7ac5": { + "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id FROM mods m\n ", "describe": { "columns": [ { @@ -332,29 +332,17 @@ }, { "ordinal": 7, - "name": "issues_url", - "type_info": "Varchar" + "name": "updated", + "type_info": "Timestamptz" }, { "ordinal": 8, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 10, "name": "team_id", "type_info": "Int8" } ], "parameters": { - "Left": [ - "Int8Array" - ] + "Left": [] }, "nullable": [ false, @@ -364,9 +352,47 @@ true, false, false, - true, - true, - true, + false, + false + ] + } + }, + "1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": { + "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + } + }, + "25131559cb73a088000ab6379a769233440ade6c7511542da410065190d203fc": { + "query": "\n SELECT id FROM loaders\n WHERE loader = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ false ] } @@ -422,6 +448,26 @@ ] } }, + "3135db1c5309dac7580a731b2829397ae7bdd6c9a67b21e813f26a4f5aa251a9": { + "query": "\n SELECT status FROM statuses\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, "33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": { "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n ", "describe": { @@ -446,6 +492,26 @@ "nullable": [] } }, + "40597b84607e77809c13ffa9c6b0b1674bd6378a4737a8f6118e91ae2ede7e4a": { + "query": "\n SELECT id\n FROM release_channels\n WHERE channel = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "4411f2aefd43881450da34db81e826110ac86c3a6cef9fd6a3e9e341508d1f09": { "query": "\n SELECT id FROM versions\n WHERE mod_id = $1\n ", "describe": { @@ -787,73 +853,91 @@ "nullable": [] } }, - "96d7b2c8b7b69fc370bb1a2d4a449f972eb3893dad5d6c59e498663cfc93a5c3": { - "query": "\n SELECT title, description, downloads,\n icon_url, body_url, published,\n issues_url, source_url, wiki_url,\n team_id\n FROM mods\n WHERE id = $1\n ", + "9a41d6c1d5c250df6114157edf5621a88bc336c5c628ba89182ba999e0af3ba8": { + "query": "\n SELECT id, title, description, downloads,\n icon_url, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url,\n team_id\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", "describe": { "columns": [ { "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, "name": "title", "type_info": "Varchar" }, { - "ordinal": 1, + "ordinal": 2, "name": "description", "type_info": "Varchar" }, { - "ordinal": 2, + "ordinal": 3, "name": "downloads", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 4, "name": "icon_url", "type_info": "Varchar" }, { - "ordinal": 4, + "ordinal": 5, "name": "body_url", "type_info": "Varchar" }, { - "ordinal": 5, + "ordinal": 6, "name": "published", "type_info": "Timestamptz" }, { - "ordinal": 6, + "ordinal": 7, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 9, "name": "issues_url", "type_info": "Varchar" }, { - "ordinal": 7, + "ordinal": 10, "name": "source_url", "type_info": "Varchar" }, { - "ordinal": 8, + "ordinal": 11, "name": "wiki_url", "type_info": "Varchar" }, { - "ordinal": 9, + "ordinal": 12, "name": "team_id", "type_info": "Int8" } ], "parameters": { "Left": [ - "Int8" + "Int8Array" ] }, "nullable": [ false, false, false, + false, true, false, false, + false, + false, true, true, true, @@ -979,6 +1063,26 @@ ] } }, + "adecffb1c41f9ddddf60c7c3c5a6be129aa8c49c53a698c6f4c2ee9ba15d6347": { + "query": "\n SELECT id\n FROM statuses\n WHERE status = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": { "query": "\n DELETE FROM teams\n WHERE id = $1\n ", "describe": { @@ -1182,6 +1286,33 @@ ] } }, + "c7ee59f31b20790b6e865b2ec14ecd9985cacb0a84e40f087d0f908033566166": { + "query": "\n SELECT u.id, u.username FROM users u\n INNER JOIN team_members tm ON tm.role = $1\n WHERE tm.team_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [ + false, + false + ] + } + }, "c82eb1b059b62444ab1d17e5a0bd7ef8acea4b05c6f3576c07d20c4ca7635a11": { "query": "\n INSERT INTO dependencies (dependent_id, dependency_id)\n VALUES ($1, $2)\n ", "describe": { @@ -1593,60 +1724,6 @@ ] } }, - "efe1bc80203f608226fa33e44654b681cc4430cec63bf7cf09b5281ff8c1c437": { - "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published FROM mods m\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "published", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false - ] - } - }, "f0db9d8606ccc2196a9cfafe0e7090dab42bf790f25e0469b8947fac1cf043d5": { "query": "\n SELECT version FROM game_versions\n WHERE id = $1\n ", "describe": { @@ -1687,6 +1764,92 @@ ] } }, + "f78dac3d15be1ea0d0ed43a4beadc04ec00d8ba68be2bb68cbc3f2ebe5c93dbd": { + "query": "\n SELECT title, description, downloads,\n icon_url, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url,\n team_id\n FROM mods\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "team_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + true, + true, + true, + false + ] + } + }, "f7bea04e8e279e27a24de1bdf3c413daa8677994df5131494b28691ed6611efc": { "query": "\n SELECT url,expires FROM states\n WHERE id = $1\n ", "describe": { diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 39e7ee025..e5b7784d4 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -102,6 +102,9 @@ pub struct TeamMemberId(pub i64); #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] pub struct ModId(pub i64); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct StatusId(pub i32); #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] diff --git a/src/database/models/mod_item.rs b/src/database/models/mod_item.rs index 0a91487e0..2cf5bb226 100644 --- a/src/database/models/mod_item.rs +++ b/src/database/models/mod_item.rs @@ -12,6 +12,7 @@ pub struct ModBuilder { pub wiki_url: Option, pub categories: Vec, pub initial_versions: Vec, + pub status: StatusId, } impl ModBuilder { @@ -26,6 +27,8 @@ impl ModBuilder { description: self.description, body_url: self.body_url, published: chrono::Utc::now(), + updated: chrono::Utc::now(), + status: self.status, downloads: 0, icon_url: self.icon_url, issues_url: self.issues_url, @@ -63,6 +66,8 @@ pub struct Mod { pub description: String, pub body_url: String, pub published: chrono::DateTime, + pub updated: chrono::DateTime, + pub status: StatusId, pub downloads: i32, pub icon_url: Option, pub issues_url: Option, @@ -114,6 +119,7 @@ impl Mod { " SELECT title, description, downloads, icon_url, body_url, published, + updated, status, issues_url, source_url, wiki_url, team_id FROM mods @@ -134,9 +140,11 @@ impl Mod { body_url: row.body_url, icon_url: row.icon_url, published: row.published, + updated: row.updated, issues_url: row.issues_url, source_url: row.source_url, wiki_url: row.wiki_url, + status: StatusId(row.status), })) } else { Ok(None) @@ -154,6 +162,7 @@ impl Mod { " SELECT id, title, description, downloads, icon_url, body_url, published, + updated, status, issues_url, source_url, wiki_url, team_id FROM mods @@ -172,9 +181,11 @@ impl Mod { body_url: m.body_url, icon_url: m.icon_url, published: m.published, + updated: m.updated, issues_url: m.issues_url, source_url: m.source_url, wiki_url: m.wiki_url, + status: StatusId(m.status), })) }) .try_collect::>() diff --git a/src/models/mods.rs b/src/models/mods.rs index 9dbb569b8..68f83501b 100644 --- a/src/models/mods.rs +++ b/src/models/mods.rs @@ -31,6 +31,10 @@ pub struct Mod { pub body_url: String, /// The date at which the mod was first published. pub published: DateTime, + /// The date at which the mod was first published. + pub updated: DateTime, + /// The status of the mod + pub status: ModStatus, /// The total number of downloads the mod has had. pub downloads: u32, @@ -48,6 +52,49 @@ pub struct Mod { pub wiki_url: Option, } +/// A status decides the visbility of a mod in search, URLs, and the whole site itself. +/// Approved - Mod is displayed on search, and accessible by URL +/// Rejected - Mod is not displayed on search, and not accessible by URL (Temporary state, mod can reapply) +/// Draft - Mod is not displayed on search, and not accessible by URL +/// Unlisted - Mod is not displayed on search, but accessible by URL +/// Processing - Mod is not displayed on search, and not accessible by URL (Temporary state, mod under review) +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ModStatus { + Approved, + Rejected, + Draft, + Unlisted, + Processing, + Unknown, +} + +impl std::fmt::Display for ModStatus { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ModStatus::Approved => write!(fmt, "release"), + ModStatus::Rejected => write!(fmt, "beta"), + ModStatus::Draft => write!(fmt, "alpha"), + ModStatus::Unlisted => write!(fmt, "unlisted"), + ModStatus::Processing => write!(fmt, "Processing"), + ModStatus::Unknown => write!(fmt, "Unknown"), + } + } +} + +impl ModStatus { + pub fn from_str(string: &str) -> ModStatus { + match string { + "processing" => ModStatus::Processing, + "rejected" => ModStatus::Processing, + "approved" => ModStatus::Processing, + "draft" => ModStatus::Processing, + "unlisted" => ModStatus::Processing, + _ => ModStatus::Unknown, + } + } +} + /// A specific version of a mod #[derive(Serialize, Deserialize)] pub struct Version { @@ -128,7 +175,7 @@ pub struct SearchRequest { pub query: Option, /// Must match a json 2 deep array of strings `[["categories:misc"]]` // TODO: We may want to have a better representation of this, so that - // we are less likely to break backwards compatability + // we are less likely to break backwards compatibility pub facets: Option, pub filters: Option, pub version: Option, diff --git a/src/models/teams.rs b/src/models/teams.rs index 31692953d..e1cffaad3 100644 --- a/src/models/teams.rs +++ b/src/models/teams.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; #[serde(into = "Base62Id")] pub struct TeamId(pub u64); +pub const OWNER_ROLE: &str = "Owner"; + // TODO: permissions, role names, etc /// A team of users who control a mod #[derive(Serialize, Deserialize)] diff --git a/src/routes/index.rs b/src/routes/index.rs index 1a8d3f052..e51423ac2 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -6,8 +6,7 @@ pub async fn index_get() -> HttpResponse { let data = json!({ "name": "modrinth-labrinth", "version": env!("CARGO_PKG_VERSION"), - //TODO: Add the documentation link - "documentation": "Nowhere yet", + "documentation": "https://modrinth.com/documentation", "about": "Welcome traveler !" }); diff --git a/src/routes/mod_creation.rs b/src/routes/mod_creation.rs index c316ecb88..5d77b0bce 100644 --- a/src/routes/mod_creation.rs +++ b/src/routes/mod_creation.rs @@ -1,8 +1,9 @@ use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models; +use crate::database::models::StatusId; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; -use crate::models::mods::{ModId, VersionId, VersionType}; +use crate::models::mods::{ModId, ModStatus, VersionId}; use crate::models::teams::TeamMember; use crate::models::users::UserId; use crate::routes::version_creation::InitialVersionData; @@ -97,8 +98,6 @@ impl actix_web::ResponseError for CreateError { struct ModCreateData { /// The title or name of the mod. pub mod_name: String, - /// The namespace of the mod - pub mod_namespace: String, /// A short description of the mod. pub mod_description: String, /// A long description of the mod, in markdown. @@ -212,6 +211,9 @@ async fn mod_create_inner( CreateError::InvalidInput(String::from("`data` field must come before file fields")) })?; + check_length("mod_name", 3, 255, &*create_data.mod_name)?; + check_length("mod_description", 3, 2048, &*create_data.mod_description)?; + let (file_name, file_extension) = super::version_creation::get_name_ext(&content_disposition)?; @@ -267,12 +269,19 @@ async fn mod_create_inner( file_name: uploaded_text.file_name.clone(), }); - // TODO: do a real lookup for the channels - let release_channel = match version_data.release_channel { - VersionType::Release => models::ChannelId(1), - VersionType::Beta => models::ChannelId(3), - VersionType::Alpha => models::ChannelId(5), - }; + let release_channel = models::ChannelId( + sqlx::query!( + " + SELECT id + FROM release_channels + WHERE channel = $1 + ", + version_data.release_channel.to_string() + ) + .fetch_one(&mut *transaction) + .await? + .id, + ); let mut game_versions = Vec::with_capacity(version_data.game_versions.len()); for v in &version_data.game_versions { @@ -379,7 +388,18 @@ async fn mod_create_inner( let team_id = team.insert(&mut *transaction).await?; - // Insert the new mod into the database + let status = ModStatus::Processing; + let status_id = sqlx::query!( + " + SELECT id + FROM statuses + WHERE status = $1 + ", + status.to_string() + ) + .fetch_one(&mut *transaction) + .await? + .id; let mod_builder = models::mod_item::ModBuilder { mod_id: mod_id.into(), @@ -394,6 +414,7 @@ async fn mod_create_inner( categories, initial_versions: created_versions, + status: StatusId(status_id), }; let versions_list = mod_builder @@ -410,7 +431,6 @@ async fn mod_create_inner( let now = chrono::Utc::now(); let timestamp = now.timestamp(); - let formatted = now.to_string(); let index_mod = crate::search::UploadSearchMod { mod_id: format!("local-{}", mod_id), @@ -418,17 +438,16 @@ async fn mod_create_inner( description: mod_builder.description.clone(), categories: create_data.categories.clone(), versions: versions_list, - page_url: mod_builder.body_url.clone(), + page_url: format!("https://modrinth.com/mod/{}", mod_id), icon_url: mod_builder.icon_url.clone().unwrap(), author: user.username, author_url: format!("https://modrinth.com/user/{}", user.id), // TODO: latest version info latest_version: String::new(), downloads: 0, - date_created: formatted.clone(), + date_created: now, created_timestamp: timestamp, - // TODO: store and return modified time - date_modified: formatted, + date_modified: now, modified_timestamp: timestamp, host: Cow::Borrowed("modrinth"), empty: Cow::Borrowed("{}{}{}"), @@ -443,6 +462,8 @@ async fn mod_create_inner( description: mod_builder.description.clone(), body_url: mod_builder.body_url.clone(), published: now, + updated: now, + status, downloads: 0, categories: create_data.categories.clone(), versions: mod_builder @@ -514,3 +535,19 @@ fn get_image_content_type(extension: &str) -> Option<&'static str> { None } } + +fn check_length( + var_name: &str, + min_length: usize, + max_length: usize, + string: &str, +) -> Result<(), CreateError> { + if string.len() > max_length || string.len() < min_length { + Err(CreateError::InvalidInput(format!( + "The {} must be between {} and {} characters; got {}.", + var_name, string, min_length, max_length + ))) + } else { + Ok(()) + } +} diff --git a/src/routes/mods.rs b/src/routes/mods.rs index 026b8e11f..269bc500f 100644 --- a/src/routes/mods.rs +++ b/src/routes/mods.rs @@ -36,15 +36,29 @@ pub async fn mods_get( .await .map_err(|e| ApiError::DatabaseError(e.into()))?; - let mods: Vec = mods_data - .into_iter() - .map(|m| models::mods::Mod { + let mut mods: Vec = Vec::new(); + for m in mods_data { + let status = sqlx::query!( + " + SELECT status FROM statuses + WHERE id = $1 + ", + m.status.0, + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .status; + + mods.push(models::mods::Mod { id: m.id.into(), team: m.team_id.into(), title: m.title, description: m.description, body_url: m.body_url, published: m.published, + updated: m.updated, + status: models::mods::ModStatus::from_str(&*status), downloads: m.downloads as u32, categories: vec![], @@ -54,7 +68,7 @@ pub async fn mods_get( source_url: m.source_url, wiki_url: m.wiki_url, }) - .collect(); + } Ok(HttpResponse::Ok().json(mods)) } @@ -71,6 +85,19 @@ pub async fn mod_get( if let Some(data) = mod_data { let m = data.inner; + + let status = sqlx::query!( + " + SELECT status FROM statuses + WHERE id = $1 + ", + m.status.0, + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .status; + let response = models::mods::Mod { id: m.id.into(), team: m.team_id.into(), @@ -78,6 +105,8 @@ pub async fn mod_get( description: m.description, body_url: m.body_url, published: m.published, + updated: m.updated, + status: models::mods::ModStatus::from_str(&*status), downloads: m.downloads as u32, categories: data.categories, diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index 3c398e7a2..e0ea55f46 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -68,6 +68,7 @@ pub async fn version_create( result } +/// TODO: Update mod timestamp when new version is created async fn version_create_inner( req: HttpRequest, mut payload: Multipart, @@ -167,12 +168,35 @@ async fn version_create_inner( file_name: uploaded_text.file_name.clone(), }); - // TODO: do a real lookup for the channels - let release_channel = match version_create_data.release_channel { - VersionType::Release => models::ChannelId(1), - VersionType::Beta => models::ChannelId(3), - VersionType::Alpha => models::ChannelId(5), - }; + let release_channel = models::ChannelId( + sqlx::query!( + " + SELECT id + FROM release_channels + WHERE channel = $1 + ", + version_create_data.release_channel.to_string() + ) + .fetch_one(&mut *transaction) + .await? + .id, + ); + + let mut game_versions = Vec::with_capacity(version_create_data.game_versions.len()); + for v in &version_create_data.game_versions { + let id = models::categories::GameVersion::get_id(&v.0, &mut *transaction) + .await? + .ok_or_else(|| CreateError::InvalidGameVersion(v.0.clone()))?; + game_versions.push(id); + } + + let mut loaders = Vec::with_capacity(version_create_data.loaders.len()); + for l in &version_create_data.loaders { + let id = models::categories::Loader::get_id(&l.0, &mut *transaction) + .await? + .ok_or_else(|| CreateError::InvalidLoader(l.0.clone()))?; + loaders.push(id); + } version_builder = Some(VersionBuilder { version_id: version_id.into(), @@ -187,9 +211,8 @@ async fn version_create_inner( .iter() .map(|x| (*x).into()) .collect::>(), - // TODO: add game_versions and loaders info - game_versions: vec![], - loaders: vec![], + game_versions, + loaders, release_channel, }); diff --git a/src/search/indexing/curseforge_import.rs b/src/search/indexing/curseforge_import.rs index 32c21cb71..bdd21f13a 100644 --- a/src/search/indexing/curseforge_import.rs +++ b/src/search/indexing/curseforge_import.rs @@ -197,9 +197,9 @@ pub async fn index_curseforge( page_url: curseforge_mod.website_url, icon_url, author_url: (&curseforge_mod.authors[0].url).to_string(), - date_created: created.to_rfc3339(), + date_created: created, created_timestamp: created.timestamp(), - date_modified: modified.to_rfc3339(), + date_modified: modified, modified_timestamp: modified.timestamp(), latest_version, host: Cow::Borrowed("curseforge"), diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 7a2092598..d5794fab2 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -6,6 +6,7 @@ use crate::search::UploadSearchMod; use sqlx::postgres::PgPool; use std::borrow::Cow; +// TODO: only loaders for recent versions? For mods that have moved from forge to fabric pub async fn index_local(pool: PgPool) -> Result, IndexingError> { info!("Indexing local mods!"); @@ -13,10 +14,9 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE let mut results = sqlx::query!( " - SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published FROM mods m + SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id FROM mods m " - ) - .fetch(&pool); + ).fetch(&pool); while let Some(result) = results.next().await { if let Ok(result) = result { @@ -34,7 +34,6 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE .try_collect::>() .await?; - // TODO: only loaders for recent versions? For mods that have moved from forge to fabric let loaders: Vec = sqlx::query!( " SELECT loaders.loader FROM versions @@ -65,14 +64,24 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE categories.extend(loaders); + let user = sqlx::query!( + " + SELECT u.id, u.username FROM users u + INNER JOIN team_members tm ON tm.role = $1 + WHERE tm.team_id = $2 + ", + crate::models::teams::OWNER_ROLE, + result.team_id, + ) + .fetch_one(&pool) + .await?; + let mut icon_url = "".to_string(); if let Some(url) = result.icon_url { icon_url = url; } - let formatted = result.published.to_rfc3339(); - let timestamp = result.published.timestamp(); docs_to_add.push(UploadSearchMod { mod_id: format!("local-{}", crate::models::ids::ModId(result.id as u64)), title: result.title, @@ -80,14 +89,14 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE categories, versions, downloads: result.downloads, - page_url: result.body_url, + page_url: format!("https://modrinth.com/mod/{}", result.id), icon_url, - author: "".to_string(), // TODO: author/team info - author_url: "".to_string(), - date_created: formatted.clone(), - created_timestamp: timestamp, - date_modified: formatted, - modified_timestamp: timestamp, + author: user.username, + author_url: format!("https://modrinth.com/user/{}", user.id), + date_created: result.published, + created_timestamp: result.published.timestamp(), + date_modified: result.updated, + modified_timestamp: result.updated.timestamp(), latest_version: "".to_string(), // TODO: Info about latest version host: Cow::Borrowed("modrinth"), empty: Cow::Borrowed("{}{}{}"), diff --git a/src/search/mod.rs b/src/search/mod.rs index fe8e165cc..f40eaa0e8 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -2,6 +2,7 @@ use crate::models::error::ApiError; use crate::models::mods::SearchRequest; use actix_web::http::StatusCode; use actix_web::web::HttpResponse; +use chrono::{DateTime, Utc}; use meilisearch_sdk::client::Client; use meilisearch_sdk::document::Document; use meilisearch_sdk::search::Query; @@ -64,11 +65,11 @@ pub struct UploadSearchMod { pub latest_version: String, /// RFC 3339 formatted creation date of the mod - pub date_created: String, + pub date_created: DateTime, /// Unix timestamp of the creation date of the mod pub created_timestamp: i64, /// RFC 3339 formatted date/time of last major modification (update) - pub date_modified: String, + pub date_modified: DateTime, /// Unix timestamp of the last major modification pub modified_timestamp: i64,