mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
Merge branch 'main' into boris/dev-1205-trace-rules
This commit is contained in:
Generated
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH random_id_point AS (\n SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point\n )\n SELECT id FROM mods\n WHERE status = ANY($1)\n ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0d01a3991e7551a8b7936bf8f4cc1760d2e89af99dd71849eda35d6c6820aa43"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH random_id_point AS (\n SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point\n )\n SELECT id FROM mods\n WHERE status = ANY($1)\n ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "20da3e21ce6115bd80746be3f6e7273771aed45eea03e46c23ef74a0a59ecfe3"
|
||||
}
|
||||
Generated
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH random_id_point AS (\n SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point\n )\n SELECT id FROM mods\n WHERE status = ANY($1)\n AND EXISTS (\n SELECT 1 FROM versions v\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = lv.loader_id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.mod_id = mods.id AND pt.name = $3\n -- prevents decorrelation, so this stops at the first match instead\n -- of scanning all versions before the outer sort/limit applies\n OFFSET 0\n )\n ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c97315540d36355668a1fdd33175b946cd026c718bf4c7d16b23953f6ec5b840"
|
||||
}
|
||||
@@ -42,6 +42,7 @@ pub struct LegacyResultSearchProject {
|
||||
pub client_side: String,
|
||||
pub server_side: String,
|
||||
pub environment: Vec<String>,
|
||||
pub disclosure_types: Vec<String>,
|
||||
pub gallery: Vec<String>,
|
||||
pub featured_gallery: Option<String>,
|
||||
pub color: Option<u32>,
|
||||
@@ -151,6 +152,7 @@ impl LegacyResultSearchProject {
|
||||
client_side,
|
||||
server_side,
|
||||
environment: environments,
|
||||
disclosure_types: result_search_project.disclosure_types,
|
||||
versions,
|
||||
latest_version: result_search_project
|
||||
.version_id
|
||||
|
||||
@@ -111,8 +111,10 @@ impl DisclosureLockStatus {
|
||||
pub struct ProjectDisclosureData {
|
||||
#[serde(flatten)]
|
||||
pub disclosure: ProjectDisclosure,
|
||||
pub set_by_moderator: bool,
|
||||
pub lock_status: DisclosureLockStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub set_by_moderator: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lock_status: Option<DisclosureLockStatus>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_by: Option<UserId>,
|
||||
@@ -124,16 +126,18 @@ impl ProjectDisclosureData {
|
||||
pub fn from_db(
|
||||
value: DBProjectDisclosure,
|
||||
viewer_is_moderator: bool,
|
||||
viewer_is_member: bool,
|
||||
) -> Self {
|
||||
let updated_by = (!value.set_by_moderator || viewer_is_moderator)
|
||||
.then_some(value.updated_by.into());
|
||||
|
||||
Self {
|
||||
disclosure: value.disclosure,
|
||||
set_by_moderator: value.set_by_moderator,
|
||||
lock_status: value.lock_status,
|
||||
set_by_moderator: (viewer_is_member || viewer_is_moderator)
|
||||
.then_some(value.set_by_moderator),
|
||||
lock_status: (viewer_is_member || viewer_is_moderator)
|
||||
.then_some(value.lock_status),
|
||||
updated_at: value.updated_at,
|
||||
updated_by,
|
||||
updated_by: ((!value.set_by_moderator && viewer_is_member)
|
||||
|| viewer_is_moderator)
|
||||
.then_some(value.updated_by.into()),
|
||||
deleted_at: value.deleted_at,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ pub struct UserPreferences {
|
||||
#[component(nested)]
|
||||
pub appearance: AppearancePreferences,
|
||||
#[component(nested)]
|
||||
pub behavior: BehaviorPreferences,
|
||||
#[component(nested)]
|
||||
pub localization: LocalizationPreferences,
|
||||
#[component(nested)]
|
||||
pub layouts: LayoutPreferences,
|
||||
@@ -34,6 +36,33 @@ pub struct AppearancePreferences {
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Component)]
|
||||
pub struct BehaviorPreferences {
|
||||
pub minimize_app: bool,
|
||||
pub hide_right_sidebar: bool,
|
||||
pub show_jump_in: bool,
|
||||
pub compact_instance_cards: bool,
|
||||
pub show_play_time: bool,
|
||||
pub hide_nametag: bool,
|
||||
pub warn_on_unknown_modpacks: bool,
|
||||
pub skip_non_essential_warnings: bool,
|
||||
}
|
||||
|
||||
impl Default for BehaviorPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
minimize_app: false,
|
||||
hide_right_sidebar: false,
|
||||
show_jump_in: true,
|
||||
compact_instance_cards: false,
|
||||
show_play_time: true,
|
||||
hide_nametag: false,
|
||||
warn_on_unknown_modpacks: true,
|
||||
skip_non_essential_warnings: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, ToSchema, Default, Clone, PartialEq,
|
||||
)]
|
||||
|
||||
@@ -277,8 +277,11 @@ fn find_file<'a>(
|
||||
|
||||
if file_name.eq_ignore_ascii_case(&formatted_name) {
|
||||
return filtered_files
|
||||
.find(|x| x.primary)
|
||||
.or_else(|| filtered_files.next_back());
|
||||
.try_fold(
|
||||
None,
|
||||
|_, x| if x.primary { Err(x) } else { Ok(Some(x)) },
|
||||
)
|
||||
.unwrap_or_else(Some);
|
||||
} else if file_name.len() > formatted_name.len()
|
||||
&& file_name.as_bytes()[..formatted_name.len()]
|
||||
.eq_ignore_ascii_case(formatted_name.as_bytes())
|
||||
|
||||
@@ -193,7 +193,10 @@ pub async fn random_projects_get(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let count = v3::projects::RandomProjects { count: count.count };
|
||||
let count = v3::projects::RandomProjects {
|
||||
count: count.count,
|
||||
project_type: None,
|
||||
};
|
||||
|
||||
let response = v3::projects::random_projects_get(
|
||||
web::Query(count),
|
||||
@@ -376,7 +379,7 @@ struct DependencyInfo {
|
||||
get,
|
||||
operation_id = "getDependencies",
|
||||
params(
|
||||
("id" = String, Path, description = "The ID or slug of the project")
|
||||
("project_id" = String, Path, description = "The ID or slug of the project")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Expected response to a valid request", body = DependencyInfo),
|
||||
|
||||
@@ -661,6 +661,8 @@ static DOWNLOAD_SOURCE_PATTERNS: LazyLock<Vec<(Regex, DownloadSourcePattern)>> =
|
||||
(r"^DawnLauncher/", P::Named("Dawn")),
|
||||
(r"^Complementary-Installer", P::Named("Complementary Installer")),
|
||||
(r"^noriskclient-launcher-v3/", P::Named("NoRisk Client")),
|
||||
(r"^Resourcify/", P::Named("Resourcify")),
|
||||
(r"^OneClient", P::Named("OneClient")),
|
||||
(
|
||||
r"^(Mozilla/|Chrome/|Chromium/|Firefox/|Safari/|AppleWebKit/|Edg/|OPR/)",
|
||||
P::Website,
|
||||
|
||||
@@ -73,14 +73,15 @@ pub async fn get_project_disclosures(
|
||||
let viewer_is_moderator =
|
||||
user_option.as_ref().is_some_and(|user| user.role.is_mod());
|
||||
|
||||
let include_deleted = viewer_is_moderator
|
||||
// Moderators can see regardless of membership, short circuit to avoid extra db call
|
||||
let viewer_is_member = viewer_is_moderator
|
||||
|| is_team_member_project(&project.inner, &user_option, &pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to check project team membership")?;
|
||||
|
||||
let disclosures = db_models::DBProjectDisclosure::get_many_for_project(
|
||||
project.inner.id,
|
||||
include_deleted,
|
||||
viewer_is_moderator || viewer_is_member,
|
||||
&***ro_pool,
|
||||
)
|
||||
.await
|
||||
@@ -90,7 +91,11 @@ pub async fn get_project_disclosures(
|
||||
disclosures: disclosures
|
||||
.into_iter()
|
||||
.map(|disclosure| {
|
||||
ProjectDisclosureData::from_db(disclosure, viewer_is_moderator)
|
||||
ProjectDisclosureData::from_db(
|
||||
disclosure,
|
||||
viewer_is_moderator,
|
||||
viewer_is_member,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
|
||||
@@ -86,7 +86,7 @@ pub async fn notifications_get(
|
||||
Ok(HttpResponse::Ok().json(notifications))
|
||||
}
|
||||
|
||||
#[utoipa::path(tag = "notifications", responses((status = OK)))]
|
||||
#[utoipa::path(tag = "notifications", params(("id" = NotificationId, Path, description = "Notification id",)), responses((status = OK)))]
|
||||
#[get("/notification/{id}")]
|
||||
pub async fn notification_get_route(
|
||||
req: HttpRequest,
|
||||
@@ -137,7 +137,7 @@ pub async fn notification_get(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(tag = "notifications", responses((status = NO_CONTENT)))]
|
||||
#[utoipa::path(tag = "notifications", params(("id" = NotificationId, Path, description = "Notification id",)), responses((status = NO_CONTENT)))]
|
||||
#[patch("/notification/{id}")]
|
||||
pub async fn notification_read_route(
|
||||
req: HttpRequest,
|
||||
@@ -208,7 +208,7 @@ pub async fn notification_read(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(tag = "notifications", responses((status = NO_CONTENT)))]
|
||||
#[utoipa::path(tag = "notifications", params(("id" = NotificationId, Path, description = "Notification id",)), responses((status = NO_CONTENT)))]
|
||||
#[delete("/notification/{id}")]
|
||||
pub async fn notification_delete_route(
|
||||
req: HttpRequest,
|
||||
|
||||
@@ -44,6 +44,7 @@ use chrono::Utc;
|
||||
use eyre::eyre;
|
||||
use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use rand::seq::SliceRandom;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
@@ -104,11 +105,15 @@ pub async fn clear_project_cache_and_queue_search(
|
||||
pub struct RandomProjects {
|
||||
#[validate(range(min = 1, max = 100))]
|
||||
pub count: u32,
|
||||
pub project_type: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "projects",
|
||||
params(("count" = u32, Query)),
|
||||
params(
|
||||
("count" = u32, Query),
|
||||
("project_type" = Option<String>, Query),
|
||||
),
|
||||
responses((status = OK))
|
||||
)]
|
||||
#[get("/projects_random")]
|
||||
@@ -120,37 +125,84 @@ pub async fn random_projects_get_route(
|
||||
random_projects_get(count, pool, redis).await
|
||||
}
|
||||
|
||||
// Filtered candidates are sparser and unevenly spaced, so the nearest-point pick
|
||||
// tends to repeat; oversample a neighborhood and shuffle it down to counter that.
|
||||
const RANDOM_PROJECT_TYPE_OVERSAMPLE_FACTOR: u32 = 20;
|
||||
|
||||
pub async fn random_projects_get(
|
||||
web::Query(count): web::Query<RandomProjects>,
|
||||
web::Query(params): web::Query<RandomProjects>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
count
|
||||
params
|
||||
.validate()
|
||||
.map_err(|err| eyre::eyre!(err))
|
||||
.wrap_request_err("validating request")?;
|
||||
|
||||
let project_ids = sqlx::query!(
|
||||
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
|
||||
// number of mods nearest to a random point in the ID space is equivalent to
|
||||
// random sampling
|
||||
"WITH random_id_point AS (
|
||||
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
|
||||
let statuses = crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut project_ids = if let Some(project_type) = ¶ms.project_type {
|
||||
let fetch_limit = params.count * RANDOM_PROJECT_TYPE_OVERSAMPLE_FACTOR;
|
||||
|
||||
sqlx::query!(
|
||||
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
|
||||
// number of mods nearest to a random point in the ID space is equivalent to
|
||||
// random sampling
|
||||
"WITH random_id_point AS (
|
||||
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
|
||||
)
|
||||
SELECT id FROM mods
|
||||
WHERE status = ANY($1)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM versions v
|
||||
INNER JOIN loaders_versions lv ON v.id = lv.version_id
|
||||
INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = lv.loader_id
|
||||
INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id
|
||||
WHERE v.mod_id = mods.id AND pt.name = $3
|
||||
-- prevents decorrelation, so this stops at the first match instead
|
||||
-- of scanning all versions before the outer sort/limit applies
|
||||
OFFSET 0
|
||||
)
|
||||
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
|
||||
LIMIT $2",
|
||||
&statuses,
|
||||
fetch_limit as i32,
|
||||
project_type,
|
||||
)
|
||||
SELECT id FROM mods
|
||||
WHERE status = ANY($1)
|
||||
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
|
||||
LIMIT $2",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
count.count as i32,
|
||||
)
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| db_ids::DBProjectId(m.id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await.wrap_internal_err("querying random project IDs")?;
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| db_ids::DBProjectId(m.id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.wrap_internal_err("querying random project IDs")?
|
||||
} else {
|
||||
sqlx::query!(
|
||||
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
|
||||
// number of mods nearest to a random point in the ID space is equivalent to
|
||||
// random sampling
|
||||
"WITH random_id_point AS (
|
||||
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
|
||||
)
|
||||
SELECT id FROM mods
|
||||
WHERE status = ANY($1)
|
||||
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
|
||||
LIMIT $2",
|
||||
&statuses,
|
||||
params.count as i32,
|
||||
)
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| db_ids::DBProjectId(m.id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.wrap_internal_err("querying random project IDs")?
|
||||
};
|
||||
|
||||
if params.project_type.is_some() {
|
||||
project_ids.shuffle(&mut rand::thread_rng());
|
||||
project_ids.truncate(params.count as usize);
|
||||
}
|
||||
|
||||
let projects_data =
|
||||
db_models::DBProject::get_many_ids(&project_ids, &**pool, &redis)
|
||||
|
||||
Reference in New Issue
Block a user