Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/labrinth/src/auth/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ pub async fn filter_visible_versions(
)
.await
.wrap_api_err("filtering visible version ids")?;
let filtered_version_ids = filtered_version_ids
.into_iter()
.collect::<std::collections::HashSet<_>>();
versions.retain(|x| filtered_version_ids.contains(&x.inner.id));

let missing = get_files_missing_attribution(
Expand Down
325 changes: 267 additions & 58 deletions apps/labrinth/src/routes/v2/versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,63 @@ pub struct VersionListFilters {
pub include_changelog: bool,
}

impl VersionListFilters {
fn into_v3(self) -> v3::versions::VersionListFilters {
let filters = self;
let loaders = if let Some(loaders) = filters.loaders {
if let Ok(mut loaders) =
serde_json::from_str::<Vec<String>>(&loaders)
{
loaders.push("mrpack".to_string());
Some(loaders)
} else {
None
}
} else {
None
};

let loader_fields = if let Some(game_versions) = filters.game_versions {
// TODO: extract this logic which is similar to the other v2->v3 version_file functions
let mut loader_fields = HashMap::new();
serde_json::from_str::<Vec<String>>(&game_versions)
.ok()
.and_then(|versions| {
let mut game_versions: Vec<serde_json::Value> = vec![];
for gv in versions {
game_versions.push(serde_json::json!(gv.clone()));
}
loader_fields
.insert("game_versions".to_string(), game_versions);

if let Some(ref loaders) = loaders {
loader_fields.insert(
"loaders".to_string(),
loaders
.iter()
.map(|x| serde_json::json!(x.clone()))
.collect(),
);
}

serde_json::to_string(&loader_fields).ok()
})
} else {
None
};

v3::versions::VersionListFilters {
loader_fields,
loaders: loaders.and_then(|x| serde_json::to_string(&x).ok()),
featured: filters.featured,
version_type: filters.version_type,
limit: filters.limit,
offset: filters.offset,
include_changelog: filters.include_changelog,
}
}
}

fn default_true() -> bool {
true
}
Expand Down Expand Up @@ -94,55 +151,7 @@ pub async fn version_list(
return Ok(response);
}

let loaders = if let Some(loaders) = filters.loaders {
if let Ok(mut loaders) = serde_json::from_str::<Vec<String>>(&loaders) {
loaders.push("mrpack".to_string());
Some(loaders)
} else {
None
}
} else {
None
};

let loader_fields = if let Some(game_versions) = filters.game_versions {
// TODO: extract this logic which is similar to the other v2->v3 version_file functions
let mut loader_fields = HashMap::new();
serde_json::from_str::<Vec<String>>(&game_versions)
.ok()
.and_then(|versions| {
let mut game_versions: Vec<serde_json::Value> = vec![];
for gv in versions {
game_versions.push(serde_json::json!(gv.clone()));
}
loader_fields
.insert("game_versions".to_string(), game_versions);

if let Some(ref loaders) = loaders {
loader_fields.insert(
"loaders".to_string(),
loaders
.iter()
.map(|x| serde_json::json!(x.clone()))
.collect(),
);
}

serde_json::to_string(&loader_fields).ok()
})
} else {
None
};

let filters = v3::versions::VersionListFilters {
loader_fields,
loaders: loaders.and_then(|x| serde_json::to_string(&x).ok()),
featured: filters.featured,
version_type: filters.version_type,
limit: filters.limit,
offset: filters.offset,
include_changelog: filters.include_changelog,
};
let filters = filters.into_v3();

let response = v3::versions::version_list_internal(
req,
Expand Down Expand Up @@ -234,35 +243,235 @@ pub async fn version_project_get(
}

#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct VersionIds {
pub ids: String,
pub struct VersionsFilters {
pub ids: Option<String>,
pub project_ids: Option<String>,
pub game_versions: Option<String>,
pub loaders: Option<String>,
pub featured: Option<String>,
pub version_type: Option<String>,
pub limit: Option<String>,
pub offset: Option<String>,
#[serde(default = "default_true")]
pub include_changelog: bool,
}

/// Get multiple versions by ID.
/// Get multiple versions by ID, or list versions across projects.
#[utoipa::path(
tag = "versions",
get,
operation_id = "getVersions",
params(
("ids" = String, Query, description = "The JSON array of version IDs"),
("ids" = Option<String>, Query, description = "JSON array of version IDs; exactly one of ids and project_ids is required. Listing filters are ignored for ids"),
("project_ids" = Option<String>, Query, description = "JSON array of at most 100 project IDs or slugs; missing or inaccessible projects are omitted"),
("game_versions" = Option<String>, Query, description = "JSON array of game versions to filter project listings by"),
("loaders" = Option<String>, Query, description = "JSON array of loaders to filter project listings by"),
("featured" = Option<bool>, Query, description = "Featured filter with per-project automatic featured fallback"),
("version_type" = Option<VersionType>, Query, description = "Filter project listings by version type"),
("limit" = Option<usize>, Query, description = "Global maximum after filtering and visibility; omitted means no limit. Only for project_ids"),
("offset" = Option<usize>, Query, description = "Global offset in newest-first order after filtering and visibility. Only for project_ids"),
("include_changelog" = Option<bool>, Query, description = "Whether to include changelog fields")
),
responses((status = 200, description = "Expected response to a valid request", body = Vec<LegacyVersion>))
responses((status = 200, description = "Expected response to a valid request", body = Vec<LegacyVersion>), (status = 400, description = "Missing or mutually exclusive selectors, more than 100 projects, or invalid parameters"))
)]
#[get("/versions")]
pub async fn versions_get(
req: HttpRequest,
web::Query(ids): web::Query<VersionIds>,
web::Query(query): web::Query<VersionsFilters>,
pool: web::Data<PgPool>,
ro_pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let ids = v3::versions::VersionIds {
ids: ids.ids,
include_changelog: ids.include_changelog,
let ids = match (query.ids, query.project_ids) {
(Some(ids), None) => v3::versions::VersionIds {
ids,
include_changelog: query.include_changelog,
},
(None, Some(project_ids)) => {
let filters = VersionListFilters {
game_versions: query.game_versions,
loaders: query.loaders,
featured: query
.featured
.map(|value| value.parse::<bool>())
.transpose()
.wrap_request_err("deserializing `featured`")?,
version_type: query
.version_type
.map(|value| {
serde_json::from_value(serde_json::Value::String(value))
})
.transpose()
.wrap_request_err("deserializing `version_type`")?,
limit: query
.limit
.map(|value| value.parse::<usize>())
.transpose()
.wrap_request_err("deserializing `limit`")?,
offset: query
.offset
.map(|value| value.parse::<usize>())
.transpose()
.wrap_request_err("deserializing `offset`")?,
include_changelog: query.include_changelog,
};
let project_ids = serde_json::from_str::<Vec<String>>(&project_ids)
.wrap_request_err("deserializing `project_ids`")?;
if project_ids.len() > 100 {
return Err(ApiError::Request(eyre::eyre!(
"`project_ids` must contain at most 100 projects"
)));
}
for (name, value) in [
("game_versions", &filters.game_versions),
("loaders", &filters.loaders),
] {
if let Some(value) = value {
serde_json::from_str::<Vec<String>>(value)
.wrap_request_err_with(|| {
format!("deserializing `{name}`")
})?;
}
}
let requested_loaders = filters
.loaders
.as_deref()
.map(serde_json::from_str::<Vec<String>>)
.transpose()
.wrap_request_err("deserializing `loaders`")?;
let user_option = crate::auth::get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ | Scopes::VERSION_READ,
)
.await
.map(|x| x.1)
.ok();
let mut projects = crate::database::models::DBProject::get_many(
&project_ids,
&***ro_pool,
&redis,
)
.await
.wrap_internal_err("fetching projects from database")?;
let visible = crate::auth::checks::filter_visible_project_ids(
projects.iter().map(|p| &p.inner).collect(),
&user_option,
&pool,
false,
)
.await
.wrap_api_err("filtering visible projects")?;
projects.retain(|p| visible.contains(&p.inner.id));
let project_ids = projects
.iter()
.map(|project| project.inner.id.0)
.collect::<Vec<_>>();
let version_ids = sqlx::query!(
r#"SELECT id FROM versions WHERE mod_id = ANY($1::bigint[]) ORDER BY id"#,
&project_ids
)
.fetch_all(&***ro_pool)
.await
.wrap_internal_err("fetching project version ids from database")?
.into_iter()
.map(|version| crate::database::models::ids::DBVersionId(version.id))
.collect::<Vec<_>>();
let versions = crate::database::models::DBVersion::get_many(
&version_ids,
&***ro_pool,
&redis,
)
.await
.wrap_internal_err("fetching versions from database")?;
let mut grouped = HashMap::new();
for version in versions {
if let Some(loaders) = &requested_loaders {
let legacy_loaders = if version
.loaders
.iter()
.any(|loader| loader == "mrpack")
{
version
.version_fields
.iter()
.find(|field| field.field_name == "mrpack_loaders")
.map(|field| field.value.as_strings())
.unwrap_or_else(|| version.loaders.clone())
} else {
version.loaders.clone()
};
if !legacy_loaders
.iter()
.any(|loader| loaders.contains(loader))
{
continue;
}
}
grouped
.entry(version.inner.project_id)
.or_insert_with(Vec::new)
.push(version);
}
let mut filters = filters.into_v3();
let limit = filters.limit.take();
let offset = filters.offset.take();
let auto_featured = if filters.featured == Some(true) {
Some(
v3::versions::auto_featured_filters(&ro_pool, &redis)
.await?,
)
} else {
None
};
let mut response = Vec::new();
for mut versions in grouped.into_values() {
versions.sort_by_key(|version| version.inner.id.0);
response.extend(
v3::versions::filter_project_versions(
versions,
&filters,
&ro_pool,
&redis,
auto_featured.as_deref(),
)
.await?,
);
}
let mut response = crate::auth::checks::filter_visible_versions(
response,
&user_option,
&ro_pool,
&redis,
)
.await
.wrap_api_err("filtering visible versions")?;
response.sort_by(|a, b| {
b.date_published
.cmp(&a.date_published)
.then_with(|| a.id.0.cmp(&b.id.0))
});
let response = response
.into_iter()
.map(|mut version| {
if !filters.include_changelog {
version.changelog = None;
}
LegacyVersion::from(version)
})
.skip(offset.unwrap_or(0))
.take(limit.unwrap_or(usize::MAX))
.collect::<Vec<_>>();
return Ok(HttpResponse::Ok().json(response));
}
_ => {
return Err(ApiError::Request(eyre::eyre!(
"exactly one of `ids` and `project_ids` is required"
)));
}
};
let response = v3::versions::versions_get(
req,
Expand Down
Loading
Loading