commit
fc2786f5e8
3
migrations/20210118161307_remove-version-access.sql
Normal file
3
migrations/20210118161307_remove-version-access.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE versions
|
||||
DROP COLUMN accepted;
|
||||
1081
sqlx-data.json
1081
sqlx-data.json
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,8 @@ use super::ids::*;
|
||||
pub struct DonationUrl {
|
||||
pub mod_id: ModId,
|
||||
pub platform_id: DonationPlatformId,
|
||||
pub platform_short: String,
|
||||
pub platform_name: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
@ -429,7 +431,8 @@ impl Mod {
|
||||
|
||||
let donations: Vec<DonationUrl> = sqlx::query!(
|
||||
"
|
||||
SELECT joining_platform_id, url FROM mods_donations
|
||||
SELECT d.joining_platform_id, d.url, dp.short, dp.name FROM mods_donations d
|
||||
INNER JOIN donation_platforms dp ON d.joining_platform_id=dp.id
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
@ -439,6 +442,8 @@ impl Mod {
|
||||
Ok(e.right().map(|c| DonationUrl {
|
||||
mod_id: id,
|
||||
platform_id: DonationPlatformId(c.joining_platform_id),
|
||||
platform_short: c.short,
|
||||
platform_name: c.name,
|
||||
url: c.url,
|
||||
}))
|
||||
})
|
||||
|
||||
@ -83,7 +83,6 @@ impl VersionBuilder {
|
||||
date_published: chrono::Utc::now(),
|
||||
downloads: 0,
|
||||
release_channel: self.release_channel,
|
||||
accepted: false,
|
||||
featured: self.featured,
|
||||
};
|
||||
|
||||
@ -158,7 +157,6 @@ pub struct Version {
|
||||
pub date_published: chrono::DateTime<chrono::Utc>,
|
||||
pub downloads: i32,
|
||||
pub release_channel: ChannelId,
|
||||
pub accepted: bool,
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
@ -172,13 +170,13 @@ impl Version {
|
||||
INSERT INTO versions (
|
||||
id, mod_id, author_id, name, version_number,
|
||||
changelog_url, date_published,
|
||||
downloads, release_channel, accepted, featured
|
||||
downloads, release_channel, featured
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7,
|
||||
$8, $9,
|
||||
$10, $11
|
||||
$10
|
||||
)
|
||||
",
|
||||
self.id as VersionId,
|
||||
@ -190,7 +188,6 @@ impl Version {
|
||||
self.date_published,
|
||||
self.downloads,
|
||||
self.release_channel as ChannelId,
|
||||
self.accepted,
|
||||
self.featured
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
@ -385,7 +382,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog, v.changelog_url, v.date_published, v.downloads,
|
||||
v.release_channel, v.accepted, v.featured
|
||||
v.release_channel, v.featured
|
||||
FROM versions v
|
||||
WHERE v.id = $1
|
||||
",
|
||||
@ -406,7 +403,6 @@ impl Version {
|
||||
date_published: row.date_published,
|
||||
downloads: row.downloads,
|
||||
release_channel: ChannelId(row.release_channel),
|
||||
accepted: row.accepted,
|
||||
featured: row.featured,
|
||||
}))
|
||||
} else {
|
||||
@ -428,7 +424,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog, v.changelog_url, v.date_published, v.downloads,
|
||||
v.release_channel, v.accepted, v.featured
|
||||
v.release_channel, v.featured
|
||||
FROM versions v
|
||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
",
|
||||
@ -447,7 +443,6 @@ impl Version {
|
||||
date_published: v.date_published,
|
||||
downloads: v.downloads,
|
||||
release_channel: ChannelId(v.release_channel),
|
||||
accepted: v.accepted,
|
||||
featured: v.featured,
|
||||
}))
|
||||
})
|
||||
@ -468,7 +463,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog, v.changelog_url, v.date_published, v.downloads,
|
||||
release_channels.channel, v.accepted, v.featured
|
||||
release_channels.channel, v.featured
|
||||
FROM versions v
|
||||
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
||||
WHERE v.id = $1
|
||||
@ -486,6 +481,7 @@ impl Version {
|
||||
SELECT gv.version FROM game_versions_versions gvv
|
||||
INNER JOIN game_versions gv ON gvv.game_version_id=gv.id
|
||||
WHERE gvv.joining_version_id = $1
|
||||
ORDER BY gv.created
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
@ -558,7 +554,6 @@ impl Version {
|
||||
files,
|
||||
loaders,
|
||||
game_versions,
|
||||
accepted: row.accepted,
|
||||
featured: row.featured,
|
||||
}))
|
||||
} else {
|
||||
@ -613,7 +608,6 @@ pub struct QueryVersion {
|
||||
pub files: Vec<QueryFile>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
pub accepted: bool,
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
|
||||
@ -113,7 +113,7 @@ pub async fn init(
|
||||
"https://github.com/login/oauth/authorize?client_id={}&state={}&scope={}",
|
||||
client_id,
|
||||
to_base62(state.0 as u64),
|
||||
"read%3Auser%20user%3Aemail"
|
||||
"read%3Auser"
|
||||
);
|
||||
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
|
||||
@ -81,11 +81,7 @@ pub fn teams_config(cfg: &mut web::ServiceConfig) {
|
||||
}
|
||||
|
||||
pub fn moderation_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("moderation")
|
||||
.service(moderation::mods)
|
||||
.service(moderation::versions),
|
||||
);
|
||||
cfg.service(web::scope("moderation").service(moderation::mods));
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
||||
@ -493,6 +493,8 @@ async fn mod_create_inner(
|
||||
donation_urls.push(models::mod_item::DonationUrl {
|
||||
mod_id: mod_id.into(),
|
||||
platform_id,
|
||||
platform_short: "".to_string(),
|
||||
platform_name: "".to_string(),
|
||||
url: url.url.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::check_is_moderator_from_headers;
|
||||
use crate::database;
|
||||
use crate::models;
|
||||
use crate::models::mods::{ModId, ModStatus, VersionType};
|
||||
use crate::models::mods::{ModId, ModStatus};
|
||||
use crate::models::teams::TeamId;
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -103,50 +102,3 @@ pub async fn mods(
|
||||
|
||||
Ok(HttpResponse::Ok().json(mods))
|
||||
}
|
||||
|
||||
/// Returns a list of versions that need to be approved
|
||||
#[get("versions")]
|
||||
pub async fn versions(
|
||||
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 versions = sqlx::query!(
|
||||
"
|
||||
SELECT * FROM versions
|
||||
WHERE accepted = FALSE
|
||||
ORDER BY date_published ASC
|
||||
LIMIT $1;
|
||||
",
|
||||
count.count as i64
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| models::mods::Version {
|
||||
id: database::models::ids::VersionId(m.id).into(),
|
||||
mod_id: database::models::ids::ModId(m.mod_id).into(),
|
||||
author_id: database::models::ids::UserId(m.author_id).into(),
|
||||
featured: m.featured,
|
||||
name: m.name,
|
||||
version_number: m.version_number,
|
||||
changelog: m.changelog,
|
||||
changelog_url: m.changelog_url,
|
||||
date_published: m.date_published,
|
||||
downloads: m.downloads as u32,
|
||||
version_type: VersionType::Release,
|
||||
files: vec![],
|
||||
dependencies: vec![],
|
||||
game_versions: vec![],
|
||||
loaders: vec![],
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<models::mods::Version>>()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(versions))
|
||||
}
|
||||
|
||||
@ -223,7 +223,16 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
||||
source_url: m.source_url,
|
||||
wiki_url: m.wiki_url,
|
||||
discord_url: m.discord_url,
|
||||
donation_urls: None,
|
||||
donation_urls: Some(
|
||||
data.donation_urls
|
||||
.into_iter()
|
||||
.map(|d| DonationLink {
|
||||
id: d.platform_short,
|
||||
platform: d.platform_name,
|
||||
url: d.url,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -49,8 +49,7 @@ pub fn check_version(version: &InitialVersionData) -> Result<(), CreateError> {
|
||||
version
|
||||
.file_parts
|
||||
.iter()
|
||||
.map(|f| check_length(1..=256, "file part name", f))
|
||||
.collect::<Result<_, _>>()?;
|
||||
.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)?;
|
||||
@ -61,13 +60,11 @@ pub fn check_version(version: &InitialVersionData) -> Result<(), CreateError> {
|
||||
version
|
||||
.game_versions
|
||||
.iter()
|
||||
.map(|v| check_length(1..=256, "game version", &v.0))
|
||||
.collect::<Result<_, _>>()?;
|
||||
.try_for_each(|v| check_length(1..=256, "game version", &v.0))?;
|
||||
version
|
||||
.loaders
|
||||
.iter()
|
||||
.map(|l| check_length(1..=256, "loader name", &l.0))
|
||||
.collect::<Result<_, _>>()?;
|
||||
.try_for_each(|l| check_length(1..=256, "loader name", &l.0))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -54,7 +54,6 @@ pub struct VersionIds {
|
||||
|
||||
#[get("versions")]
|
||||
pub async fn versions_get(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<VersionIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@ -66,39 +65,11 @@ pub async fn versions_get(
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let mut versions = Vec::new();
|
||||
|
||||
for version_data in versions_data {
|
||||
if let Some(version) = version_data {
|
||||
let mut authorized = version.accepted;
|
||||
|
||||
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 member_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM team_members tm INNER JOIN mods m ON m.team_id = tm.team_id AND m.id = $1 WHERE tm.user_id = $2)",
|
||||
version.mod_id as database::models::ModId,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists;
|
||||
|
||||
authorized = member_exists.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if authorized {
|
||||
versions.push(convert_version(version));
|
||||
}
|
||||
versions.push(convert_version(version));
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +78,6 @@ pub async fn versions_get(
|
||||
|
||||
#[get("{version_id}")]
|
||||
pub async fn version_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(models::ids::VersionId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@ -115,33 +85,8 @@ pub async fn version_get(
|
||||
let version_data = database::models::Version::get_full(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
if let Some(data) = version_data {
|
||||
if !data.accepted {
|
||||
if let Some(user) = user_option {
|
||||
if !user.role.is_mod() {
|
||||
let user_id: database::models::ids::UserId = user.id.into();
|
||||
|
||||
let member_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM team_members tm INNER JOIN mods m ON m.team_id = tm.team_id AND m.id = $1 WHERE tm.user_id = $2)",
|
||||
data.mod_id as database::models::ModId,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists;
|
||||
|
||||
if !member_exists.unwrap_or(false) {
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(convert_version(data)))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
@ -212,7 +157,6 @@ pub struct EditVersion {
|
||||
pub dependencies: Option<Vec<models::ids::VersionId>>,
|
||||
pub game_versions: Option<Vec<models::mods::GameVersion>>,
|
||||
pub loaders: Option<Vec<models::mods::ModLoader>>,
|
||||
pub accepted: Option<bool>,
|
||||
pub featured: Option<bool>,
|
||||
pub primary_file: Option<(String, String)>,
|
||||
}
|
||||
@ -262,28 +206,6 @@ pub async fn version_edit(
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(accepted) = &new_version.accepted {
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the approval of this version!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
SET accepted = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
accepted,
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(name) = &new_version.name {
|
||||
sqlx::query!(
|
||||
"
|
||||
|
||||
@ -2,6 +2,7 @@ use futures::{StreamExt, TryStreamExt};
|
||||
use log::info;
|
||||
|
||||
use super::IndexingError;
|
||||
use crate::models::mods::SideType;
|
||||
use crate::search::UploadSearchMod;
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::borrow::Cow;
|
||||
@ -14,7 +15,7 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
|
||||
let mut mods = sqlx::query!(
|
||||
"
|
||||
SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug 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, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m
|
||||
"
|
||||
).fetch(&pool);
|
||||
|
||||
@ -112,6 +113,38 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|| Cow::Borrowed(""));
|
||||
|
||||
let client_side = SideType::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.client_side,
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
.name,
|
||||
);
|
||||
|
||||
let server_side = SideType::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.server_side,
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
.name,
|
||||
);
|
||||
|
||||
let license = crate::database::models::categories::License::get(
|
||||
crate::database::models::LicenseId(mod_data.license),
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
docs_to_add.push(UploadSearchMod {
|
||||
mod_id: format!("local-{}", mod_id),
|
||||
title: mod_data.title,
|
||||
@ -128,6 +161,9 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
date_modified: mod_data.updated,
|
||||
modified_timestamp: mod_data.updated.timestamp(),
|
||||
latest_version,
|
||||
license: license.short,
|
||||
client_side: client_side.to_string(),
|
||||
server_side: server_side.to_string(),
|
||||
host: Cow::Borrowed("modrinth"),
|
||||
slug: mod_data.slug,
|
||||
});
|
||||
@ -143,7 +179,7 @@ pub async fn query_one(
|
||||
) -> Result<UploadSearchMod, IndexingError> {
|
||||
let mod_data = sqlx::query!(
|
||||
"
|
||||
SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug
|
||||
SELECT m.id, m.title, m.description, m.downloads, 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
|
||||
WHERE id = $1
|
||||
",
|
||||
@ -225,6 +261,38 @@ pub async fn query_one(
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|| Cow::Borrowed(""));
|
||||
|
||||
let client_side = SideType::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.client_side,
|
||||
)
|
||||
.fetch_one(&mut *exec)
|
||||
.await?
|
||||
.name,
|
||||
);
|
||||
|
||||
let server_side = SideType::from_str(
|
||||
&sqlx::query!(
|
||||
"
|
||||
SELECT name FROM side_types
|
||||
WHERE id = $1
|
||||
",
|
||||
mod_data.server_side,
|
||||
)
|
||||
.fetch_one(&mut *exec)
|
||||
.await?
|
||||
.name,
|
||||
);
|
||||
|
||||
let license = crate::database::models::categories::License::get(
|
||||
crate::database::models::LicenseId(mod_data.license),
|
||||
&mut *exec,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(UploadSearchMod {
|
||||
mod_id: format!("local-{}", mod_id),
|
||||
title: mod_data.title,
|
||||
@ -241,6 +309,9 @@ pub async fn query_one(
|
||||
date_modified: mod_data.updated,
|
||||
modified_timestamp: mod_data.updated.timestamp(),
|
||||
latest_version,
|
||||
license: license.short,
|
||||
client_side: client_side.to_string(),
|
||||
server_side: server_side.to_string(),
|
||||
host: Cow::Borrowed("modrinth"),
|
||||
slug: mod_data.slug,
|
||||
})
|
||||
|
||||
@ -15,14 +15,14 @@ use thiserror::Error;
|
||||
pub enum IndexingError {
|
||||
#[error("Error while connecting to the MeiliSearch database")]
|
||||
IndexDBError(#[from] meilisearch_sdk::errors::Error),
|
||||
#[error("Error while importing mods from CurseForge")]
|
||||
CurseforgeImportError(#[from] reqwest::Error),
|
||||
#[error("Error while serializing or deserializing JSON: {0}")]
|
||||
SerDeError(#[from] serde_json::Error),
|
||||
#[error("Error while parsing a timestamp: {0}")]
|
||||
ParseDateError(#[from] chrono::format::ParseError),
|
||||
#[error("Database Error: {0}")]
|
||||
DatabaseError(#[from] sqlx::error::Error),
|
||||
SqlxError(#[from] sqlx::error::Error),
|
||||
#[error("Database Error: {0}")]
|
||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||
#[error("Environment Error")]
|
||||
EnvError(#[from] dotenv::Error),
|
||||
}
|
||||
@ -268,6 +268,9 @@ fn default_settings() -> Settings {
|
||||
String::from("categories"),
|
||||
String::from("host"),
|
||||
String::from("versions"),
|
||||
String::from("license"),
|
||||
String::from("client_side"),
|
||||
String::from("server_side"),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@ -73,6 +73,9 @@ pub struct UploadSearchMod {
|
||||
pub icon_url: String,
|
||||
pub author_url: String,
|
||||
pub latest_version: Cow<'static, str>,
|
||||
pub license: String,
|
||||
pub client_side: String,
|
||||
pub server_side: String,
|
||||
|
||||
/// RFC 3339 formatted creation date of the mod
|
||||
pub date_created: DateTime<Utc>,
|
||||
@ -113,6 +116,9 @@ pub struct ResultSearchMod {
|
||||
/// RFC 3339 formatted modification date of the mod
|
||||
pub date_modified: String,
|
||||
pub latest_version: String,
|
||||
pub license: String,
|
||||
pub client_side: String,
|
||||
pub server_side: String,
|
||||
|
||||
/// The host of the mod: Either `modrinth` or `curseforge`
|
||||
pub host: String,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user