From 4ee470bc43e5681e29259e53538b7ec603a7a78d Mon Sep 17 00:00:00 2001 From: aecsocket Date: Tue, 13 Jan 2026 00:37:04 +0000 Subject: [PATCH 1/3] Move version compatibility info to backend version list route --- apps/labrinth/src/routes/v2/versions.rs | 27 +-- apps/labrinth/src/routes/v3/versions.rs | 290 +++++++++++++++++------- docker-compose.yml | 1 + packages/muralpay/src/account.rs | 27 ++- packages/muralpay/src/client/error.rs | 3 +- packages/muralpay/src/client/mock.rs | 9 +- packages/muralpay/src/client/mod.rs | 24 +- packages/muralpay/src/counterparty.rs | 34 ++- packages/muralpay/src/organization.rs | 21 +- packages/muralpay/src/payout_method.rs | 37 ++- packages/muralpay/src/serde_iso3166.rs | 9 +- packages/muralpay/src/transaction.rs | 36 ++- packages/muralpay/src/util.rs | 3 +- 13 files changed, 386 insertions(+), 135 deletions(-) diff --git a/apps/labrinth/src/routes/v2/versions.rs b/apps/labrinth/src/routes/v2/versions.rs index 6e91f8a38a..5f6f10f7b6 100644 --- a/apps/labrinth/src/routes/v2/versions.rs +++ b/apps/labrinth/src/routes/v2/versions.rs @@ -94,10 +94,10 @@ pub async fn version_list( featured: filters.featured, version_type: filters.version_type, limit: filters.limit, - offset: filters.offset, + ..Default::default() }; - let response = v3::versions::version_list( + let response = match v3::versions::version_list( req, info, web::Query(filters), @@ -106,19 +106,20 @@ pub async fn version_list( session_queue, ) .await - .or_else(v2_reroute::flatten_404_error)?; + { + Ok(r) => r, + Err(ApiError::NotFound) => return Ok(HttpResponse::NotFound().body("")), + Err(e) => return Err(e), + }; // Convert response to V2 format - match v2_reroute::extract_ok_json::>(response).await { - Ok(versions) => { - let v2_versions = versions - .into_iter() - .map(LegacyVersion::from) - .collect::>(); - Ok(HttpResponse::Ok().json(v2_versions)) - } - Err(response) => Ok(response), - } + let v2_versions = response + .0 + .versions + .into_iter() + .map(LegacyVersion::from) + .collect::>(); + Ok(HttpResponse::Ok().json(v2_versions)) } // Given a project ID/slug and a version slug diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index cdca240744..bf45e25628 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -19,7 +19,7 @@ use crate::models::ids::VersionId; use crate::models::images::ImageContext; use crate::models::pats::Scopes; use crate::models::projects::{ - Dependency, FileType, VersionStatus, VersionType, + Dependency, FileType, Version, VersionStatus, VersionType, }; use crate::models::projects::{Loader, skip_nulls}; use crate::models::teams::ProjectPermissions; @@ -701,13 +701,13 @@ pub async fn version_edit_helper( } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Validate, Default, Debug)] pub struct VersionListFilters { pub loaders: Option, pub featured: Option, pub version_type: Option, pub limit: Option, - pub offset: Option, + pub next_id: Option, /* Loader fields to filter with: "game_versions": ["1.16.5", "1.17"] @@ -717,6 +717,36 @@ pub struct VersionListFilters { pub loader_fields: Option, } +#[derive(Serialize)] +pub struct VersionListResponse { + pub versions: Vec, + pub available_game_versions: Vec, + pub available_loaders: Vec, + pub latest_versions: LatestVersions, +} + +#[derive(Serialize)] +pub struct LatestVersions { + pub release: Option, + pub beta: Option, + pub alpha: Option, +} + +fn find_latest_by_type( + versions: &[database::models::version_item::VersionQueryResult], + version_type: VersionType, + after_date: Option>, +) -> Option { + versions + .iter() + .filter(|v| { + v.inner.version_type.as_str() == version_type.as_str() + && after_date.map_or(true, |d| v.inner.date_published > d) + }) + .max_by_key(|v| v.inner.date_published) + .cloned() +} + pub async fn version_list( req: HttpRequest, info: web::Path<(String,)>, @@ -724,7 +754,7 @@ pub async fn version_list( pool: web::Data, redis: web::Data, session_queue: web::Data, -) -> Result { +) -> Result, ApiError> { let string = info.into_inner().0; let result = @@ -748,6 +778,37 @@ pub async fn version_list( return Err(ApiError::NotFound); } + // Fetch all versions first (for computing metadata) + let all_versions = database::models::DBVersion::get_many( + &project.versions, + &**pool, + &redis, + ) + .await?; + + // Compute available_game_versions and available_loaders from ALL versions + let available_game_versions: Vec = all_versions + .iter() + .flat_map(|v| { + v.version_fields + .iter() + .find(|vf| vf.field_name == "game_versions") + .map(|vf| vf.value.as_strings()) + .unwrap_or_default() + }) + .sorted() + .dedup() + .collect(); + + let available_loaders: Vec = all_versions + .iter() + .flat_map(|v| v.loaders.iter().cloned()) + .sorted() + .dedup() + .map(Loader) + .collect(); + + // Parse filter parameters let loader_field_filters = filters.loader_fields.as_ref().map(|x| { serde_json::from_str::>>(x) .unwrap_or_default() @@ -755,70 +816,100 @@ pub async fn version_list( let loader_filters = filters.loaders.as_ref().map(|x| { serde_json::from_str::>(x).unwrap_or_default() }); - let mut versions = database::models::DBVersion::get_many( - &project.versions, - &**pool, - &redis, - ) - .await? - .into_iter() - .skip(filters.offset.unwrap_or(0)) - .take(filters.limit.unwrap_or(usize::MAX)) - .filter(|x| { - let mut bool = true; - - if let Some(version_type) = filters.version_type { - bool &= &*x.inner.version_type == version_type.as_str(); - } - if let Some(loaders) = &loader_filters { - bool &= x.loaders.iter().any(|y| loaders.contains(y)); - } - if let Some(loader_fields) = &loader_field_filters { - for (key, values) in loader_fields { - bool &= if let Some(x_vf) = - x.version_fields.iter().find(|y| y.field_name == *key) - { - values.iter().any(|v| x_vf.value.contains_json_value(v)) - } else { - true - }; + + // Apply filters to all versions + let mut filtered_versions = all_versions + .into_iter() + .filter(|x| { + let mut valid = true; + + if let Some(version_type) = filters.version_type { + valid &= &*x.inner.version_type == version_type.as_str(); + } + if let Some(loaders) = &loader_filters { + valid &= x.loaders.iter().any(|y| loaders.contains(y)); + } + if let Some(loader_fields) = &loader_field_filters { + for (key, values) in loader_fields { + valid &= if let Some(x_vf) = x + .version_fields + .iter() + .find(|y| y.field_name == *key) + { + values + .iter() + .any(|v| x_vf.value.contains_json_value(v)) + } else { + true + }; + } + } + if let Some(featured) = filters.featured { + valid &= featured == x.inner.featured; } - } - bool - }) - .collect::>(); - let mut response = versions - .iter() - .filter(|version| { - filters - .featured - .is_none_or(|featured| featured == version.inner.featured) + valid }) - .cloned() .collect::>(); - versions.sort_by(|a, b| { - b.inner.date_published.cmp(&a.inner.date_published) + // Sort by (date_published DESC, id DESC) + filtered_versions.sort_by(|a, b| { + let date_cmp = b.inner.date_published.cmp(&a.inner.date_published); + if date_cmp == std::cmp::Ordering::Equal { + b.inner.id.0.cmp(&a.inner.id.0) + } else { + date_cmp + } }); + // Clone filtered_versions for later use (auto-featured logic and latest_versions computation) + let filtered_versions_for_pagination = filtered_versions.clone(); + + // Apply cursor pagination using next_id + let paginated_versions: Vec<_> = match filters.next_id { + Some(next_id) => { + let cursor_index = filtered_versions_for_pagination + .iter() + .position(|v| v.inner.id == next_id.into()); + + match cursor_index { + Some(idx) => filtered_versions_for_pagination + .into_iter() + .skip(idx + 1) + .take(filters.limit.unwrap_or(20)) + .collect(), + None => { + // Invalid cursor, return first page + filtered_versions_for_pagination + .into_iter() + .take(filters.limit.unwrap_or(20)) + .collect() + } + } + } + None => filtered_versions_for_pagination + .into_iter() + .take(filters.limit.unwrap_or(20)) + .collect(), + }; + // Attempt to populate versions with "auto featured" versions - if response.is_empty() - && !versions.is_empty() + let mut response = if paginated_versions.is_empty() + && !filtered_versions.is_empty() && filters.featured.unwrap_or(false) { // TODO: This is a bandaid fix for detecting auto-featured versions. // In the future, not all versions will have 'game_versions' fields, so this will need to be changed. let (loaders, game_versions) = futures::future::try_join( - database::models::loader_fields::Loader::list(&**pool, &redis), - database::models::legacy_loader_fields::MinecraftGameVersion::list( - None, - Some(true), - &**pool, - &redis, - ), - ) - .await?; + database::models::loader_fields::Loader::list(&**pool, &redis), + database::models::legacy_loader_fields::MinecraftGameVersion::list( + None, + Some(true), + &**pool, + &redis, + ), + ) + .await?; let mut joined_filters = Vec::new(); for game_version in &game_versions { @@ -827,40 +918,87 @@ pub async fn version_list( } } + let mut auto_featured = Vec::new(); joined_filters.into_iter().for_each(|filter| { - if let Some(version) = versions.iter().find(|version| { - // TODO: This is the bandaid fix for detecting auto-featured versions. - let game_versions = version - .version_fields - .iter() - .find(|vf| vf.field_name == "game_versions") - .map(|vf| vf.value.clone()) - .map(|v| v.as_strings()) - .unwrap_or_default(); - game_versions.contains(&filter.0.version) - && version.loaders.contains(&filter.1.loader) - }) { - response.push(version.clone()); + if let Some(version) = + filtered_versions.iter().find(|version| { + // TODO: This is the bandaid fix for detecting auto-featured versions. + let game_versions = version + .version_fields + .iter() + .find(|vf| vf.field_name == "game_versions") + .map(|vf| vf.value.clone()) + .map(|v| v.as_strings()) + .unwrap_or_default(); + game_versions.contains(&filter.0.version) + && version.loaders.contains(&filter.1.loader) + }) + { + auto_featured.push(version.clone()); } }); - if response.is_empty() { - versions - .into_iter() - .for_each(|version| response.push(version)); + if auto_featured.is_empty() { + filtered_versions.clone() + } else { + auto_featured } - } + } else { + paginated_versions + }; response.sort_by(|a, b| { - b.inner.date_published.cmp(&a.inner.date_published) + let date_cmp = b.inner.date_published.cmp(&a.inner.date_published); + if date_cmp == std::cmp::Ordering::Equal { + b.inner.id.0.cmp(&a.inner.id.0) + } else { + date_cmp + } }); response.dedup_by(|a, b| a.inner.id == b.inner.id); - let response = + // Compute latest_versions from filtered versions (before pagination) + let filtered_for_latest = filtered_versions.clone(); + let latest_release = find_latest_by_type( + &filtered_for_latest, + VersionType::Release, + None, + ); + let latest_beta = find_latest_by_type( + &filtered_for_latest, + VersionType::Beta, + latest_release.as_ref().map(|r| r.inner.date_published), + ); + let latest_alpha = find_latest_by_type( + &filtered_for_latest, + VersionType::Alpha, + latest_beta + .as_ref() + .or(latest_release.as_ref()) + .map(|v| v.inner.date_published), + ); + + // Filter visible versions + let visible_versions = filter_visible_versions(response, &user_option, &pool, &redis) .await?; - Ok(HttpResponse::Ok().json(response)) + // Convert to API models + let versions: Vec = + visible_versions.into_iter().map(Version::from).collect(); + + let latest_versions = LatestVersions { + release: latest_release.map(Version::from), + beta: latest_beta.map(Version::from), + alpha: latest_alpha.map(Version::from), + }; + + Ok(web::Json(VersionListResponse { + versions, + available_game_versions, + available_loaders, + latest_versions, + })) } else { Err(ApiError::NotFound) } diff --git a/docker-compose.yml b/docker-compose.yml index 8ec3603919..bcbc18dd2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: POSTGRES_USER: labrinth POSTGRES_PASSWORD: labrinth POSTGRES_HOST_AUTH_METHOD: trust + PGUSER: labrinth healthcheck: test: ['CMD', 'pg_isready', '-U', 'labrinth'] interval: 3s diff --git a/packages/muralpay/src/account.rs b/packages/muralpay/src/account.rs index 7a124b9042..959c1c054a 100644 --- a/packages/muralpay/src/account.rs +++ b/packages/muralpay/src/account.rs @@ -13,7 +13,9 @@ const _: () = { use crate::{MuralError, RequestExt}; impl crate::Client { - pub async fn get_all_accounts(&self) -> Result, MuralError> { + pub async fn get_all_accounts( + &self, + ) -> Result, MuralError> { maybe_mock!(self, get_all_accounts()); self.http_get(|base| format!("{base}/api/accounts")) @@ -21,7 +23,10 @@ const _: () = { .await } - pub async fn get_account(&self, id: AccountId) -> Result { + pub async fn get_account( + &self, + id: AccountId, + ) -> Result { maybe_mock!(self, get_account(id)); self.http_get(|base| format!("{base}/api/accounts/{id}")) @@ -43,7 +48,10 @@ const _: () = { maybe_mock!( self, - create_account(name.as_ref(), description.as_ref().map(AsRef::as_ref)) + create_account( + name.as_ref(), + description.as_ref().map(AsRef::as_ref) + ) ); let body = Body { @@ -59,7 +67,18 @@ const _: () = { } }; -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] +#[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Deref, + Serialize, + Deserialize, +)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[display("{}", _0.hyphenated())] pub struct AccountId(pub Uuid); diff --git a/packages/muralpay/src/client/error.rs b/packages/muralpay/src/client/error.rs index 507f73689a..b71129b889 100644 --- a/packages/muralpay/src/client/error.rs +++ b/packages/muralpay/src/client/error.rs @@ -69,7 +69,8 @@ impl fmt::Display for ApiError { if !self.params.is_empty() { lines.push("params:".into()); - lines.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}"))); + lines + .extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}"))); } lines.push(format!("error name: {}", self.name)); diff --git a/packages/muralpay/src/client/mock.rs b/packages/muralpay/src/client/mock.rs index c06e4231ae..91ebb185c3 100644 --- a/packages/muralpay/src/client/mock.rs +++ b/packages/muralpay/src/client/mock.rs @@ -2,10 +2,11 @@ use { crate::{ - Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId, CreateCounterparty, - CreatePayout, FiatAndRailCode, FiatFeeRequest, FiatPayoutFee, MuralError, Organization, - OrganizationId, PayoutMethod, PayoutMethodDetails, PayoutMethodId, PayoutRequest, - PayoutRequestId, PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse, + Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId, + CreateCounterparty, CreatePayout, FiatAndRailCode, FiatFeeRequest, + FiatPayoutFee, MuralError, Organization, OrganizationId, PayoutMethod, + PayoutMethodDetails, PayoutMethodId, PayoutRequest, PayoutRequestId, + PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse, TokenFeeRequest, TokenPayoutFee, UpdateCounterparty, transaction::{Transaction, TransactionId}, }, diff --git a/packages/muralpay/src/client/mod.rs b/packages/muralpay/src/client/mod.rs index 2af1885535..ec8bebdd96 100644 --- a/packages/muralpay/src/client/mod.rs +++ b/packages/muralpay/src/client/mod.rs @@ -46,26 +46,40 @@ impl Client { api_url: String::new(), api_key: SecretString::from(String::new()), transfer_api_key: SecretString::from(String::new()), - mock: std::sync::Arc::new(arc_swap::ArcSwapOption::from_pointee(mock)), + mock: std::sync::Arc::new(arc_swap::ArcSwapOption::from_pointee( + mock, + )), } } - fn http_req(&self, make_req: impl FnOnce() -> RequestBuilder) -> RequestBuilder { + fn http_req( + &self, + make_req: impl FnOnce() -> RequestBuilder, + ) -> RequestBuilder { make_req() .bearer_auth(self.api_key.expose_secret()) .header("accept", "application/json") .header("content-type", "application/json") } - pub(crate) fn http_get(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { + pub(crate) fn http_get( + &self, + make_url: impl FnOnce(&str) -> U, + ) -> RequestBuilder { self.http_req(|| self.http.get(make_url(&self.api_url))) } - pub(crate) fn http_post(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { + pub(crate) fn http_post( + &self, + make_url: impl FnOnce(&str) -> U, + ) -> RequestBuilder { self.http_req(|| self.http.post(make_url(&self.api_url))) } - pub(crate) fn http_put(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { + pub(crate) fn http_put( + &self, + make_url: impl FnOnce(&str) -> U, + ) -> RequestBuilder { self.http_req(|| self.http.put(make_url(&self.api_url))) } diff --git a/packages/muralpay/src/counterparty.rs b/packages/muralpay/src/counterparty.rs index a7fae8ac59..bb61f70259 100644 --- a/packages/muralpay/src/counterparty.rs +++ b/packages/muralpay/src/counterparty.rs @@ -15,7 +15,8 @@ const _: () = { pub async fn search_counterparties( &self, params: Option>, - ) -> Result, MuralError> { + ) -> Result, MuralError> + { maybe_mock!(self, search_counterparties(params)); self.http_post(|base| format!("{base}/api/counterparties/search")) @@ -30,9 +31,11 @@ const _: () = { ) -> Result { maybe_mock!(self, get_counterparty(id)); - self.http_get(|base| format!("{base}/api/counterparties/counterparty/{id}")) - .send_mural() - .await + self.http_get(|base| { + format!("{base}/api/counterparties/counterparty/{id}") + }) + .send_mural() + .await } pub async fn create_counterparty( @@ -70,15 +73,28 @@ const _: () = { let body = Body { counterparty }; - self.http_put(|base| format!("{base}/api/counterparties/counterparty/{id}")) - .json(&body) - .send_mural() - .await + self.http_put(|base| { + format!("{base}/api/counterparties/counterparty/{id}") + }) + .json(&body) + .send_mural() + .await } } }; -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] +#[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Deref, + Serialize, + Deserialize, +)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[display("{}", _0.hyphenated())] pub struct CounterpartyId(pub Uuid); diff --git a/packages/muralpay/src/organization.rs b/packages/muralpay/src/organization.rs index 71053e8fb5..10edc14fbb 100644 --- a/packages/muralpay/src/organization.rs +++ b/packages/muralpay/src/organization.rs @@ -15,7 +15,8 @@ const _: () = { pub async fn search_organizations( &self, req: SearchRequest, - ) -> Result, MuralError> { + ) -> Result, MuralError> + { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct Body { @@ -41,8 +42,9 @@ const _: () = { let query = [ req.limit.map(|limit| ("limit", limit.to_string())), - req.next_id - .map(|next_id| ("nextId", next_id.hyphenated().to_string())), + req.next_id.map(|next_id| { + ("nextId", next_id.hyphenated().to_string()) + }), ] .into_iter() .flatten() @@ -75,7 +77,18 @@ const _: () = { } }; -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] +#[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Deref, + Serialize, + Deserialize, +)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[display("{}", _0.hyphenated())] pub struct OrganizationId(pub Uuid); diff --git a/packages/muralpay/src/payout_method.rs b/packages/muralpay/src/payout_method.rs index cde1907133..bf98fb107f 100644 --- a/packages/muralpay/src/payout_method.rs +++ b/packages/muralpay/src/payout_method.rs @@ -1,8 +1,8 @@ use { crate::{ - ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId, CrcSymbol, - DocumentType, EurSymbol, FiatAccountType, MxnSymbol, PenSymbol, UsdSymbol, WalletDetails, - ZarSymbol, + ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId, + CrcSymbol, DocumentType, EurSymbol, FiatAccountType, MxnSymbol, + PenSymbol, UsdSymbol, WalletDetails, ZarSymbol, }, chrono::{DateTime, Utc}, derive_more::{Deref, Display, Error}, @@ -21,7 +21,8 @@ const _: () = { &self, counterparty_id: CounterpartyId, params: Option>, - ) -> Result, MuralError> { + ) -> Result, MuralError> + { maybe_mock!(self, search_payout_methods(counterparty_id, params)); self.http_post(|base| { @@ -37,7 +38,10 @@ const _: () = { counterparty_id: CounterpartyId, payout_method_id: PayoutMethodId, ) -> Result { - maybe_mock!(self, get_payout_method(counterparty_id, payout_method_id)); + maybe_mock!( + self, + get_payout_method(counterparty_id, payout_method_id) + ); self.http_get(|base| { format!( @@ -63,7 +67,11 @@ const _: () = { maybe_mock!( self, - create_payout_method(counterparty_id, alias.as_ref(), payout_method) + create_payout_method( + counterparty_id, + alias.as_ref(), + payout_method + ) ); let body = Body { @@ -72,7 +80,9 @@ const _: () = { }; self.http_post(|base| { - format!("{base}/api/counterparties/{counterparty_id}/payout-methods") + format!( + "{base}/api/counterparties/{counterparty_id}/payout-methods" + ) }) .json(&body) .send_mural() @@ -121,7 +131,18 @@ pub enum PayoutMethodPixAccountType { BankAccount, } -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] +#[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Deref, + Serialize, + Deserialize, +)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[display("{}", _0.hyphenated())] pub struct PayoutMethodId(pub Uuid); diff --git a/packages/muralpay/src/serde_iso3166.rs b/packages/muralpay/src/serde_iso3166.rs index 8769107cb6..209792835a 100644 --- a/packages/muralpay/src/serde_iso3166.rs +++ b/packages/muralpay/src/serde_iso3166.rs @@ -4,7 +4,10 @@ use { std::borrow::Cow, }; -pub fn serialize(v: &CountryCode, serializer: S) -> Result { +pub fn serialize( + v: &CountryCode, + serializer: S, +) -> Result { serializer.serialize_str(v.alpha2) } @@ -15,6 +18,8 @@ pub fn deserialize<'de, D: serde::Deserializer<'de>>( rust_iso3166::ALPHA2_MAP .get(&country_code) .copied() - .ok_or_else(|| D::Error::custom("invalid ISO 3166 alpha-2 country code")) + .ok_or_else(|| { + D::Error::custom("invalid ISO 3166 alpha-2 country code") + }) }) } diff --git a/packages/muralpay/src/transaction.rs b/packages/muralpay/src/transaction.rs index 088f8dd217..c299750ce4 100644 --- a/packages/muralpay/src/transaction.rs +++ b/packages/muralpay/src/transaction.rs @@ -5,14 +5,21 @@ use derive_more::{Deref, Display}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount}; +use crate::{ + AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount, +}; #[cfg(feature = "client")] const _: () = { - use crate::{Account, MuralError, RequestExt, SearchParams, SearchResponse}; + use crate::{ + Account, MuralError, RequestExt, SearchParams, SearchResponse, + }; impl crate::Client { - pub async fn get_transaction(&self, id: TransactionId) -> Result { + pub async fn get_transaction( + &self, + id: TransactionId, + ) -> Result { maybe_mock!(self, get_transaction(id)); self.http_get(|base| format!("{base}/api/transactions/{id}")) @@ -27,15 +34,28 @@ const _: () = { ) -> Result, MuralError> { maybe_mock!(self, search_transactions(account_id, params)); - self.http_post(|base| format!("{base}/api/transactions/search/account/{account_id}")) - .query(¶ms.map(|p| p.to_query()).unwrap_or_default()) - .send_mural() - .await + self.http_post(|base| { + format!("{base}/api/transactions/search/account/{account_id}") + }) + .query(¶ms.map(|p| p.to_query()).unwrap_or_default()) + .send_mural() + .await } } }; -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)] +#[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Deref, + Serialize, + Deserialize, +)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[display("{}", _0.hyphenated())] pub struct TransactionId(pub Uuid); diff --git a/packages/muralpay/src/util.rs b/packages/muralpay/src/util.rs index 70aae6c6f2..a3374eae2b 100644 --- a/packages/muralpay/src/util.rs +++ b/packages/muralpay/src/util.rs @@ -5,7 +5,8 @@ macro_rules! display_as_serialize { impl fmt::Display for $T { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let value = serde_json::to_value(self).map_err(|_| fmt::Error)?; + let value = + serde_json::to_value(self).map_err(|_| fmt::Error)?; let value = value.as_str().ok_or(fmt::Error)?; write!(f, "{value}") } From ddbf3d2b74c4ccaf741570ad6c3a8da262413485 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Tue, 13 Jan 2026 11:25:57 +0000 Subject: [PATCH 2/3] reroute /v2/versions properly --- apps/labrinth/src/routes/internal/delphi.rs | 6 +++--- apps/labrinth/src/routes/mod.rs | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 9af6797fa7..86989568ec 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -327,7 +327,7 @@ pub async fn run( .send() .await .and_then(|res| res.error_for_status()) - .map_err(ApiError::Delphi)?; + .map_err(ApiError::delphi)?; Ok(HttpResponse::NoContent().finish()) } @@ -411,10 +411,10 @@ async fn issue_type_schema( .send() .await .and_then(|res| res.error_for_status()) - .map_err(ApiError::Delphi)? + .map_err(ApiError::delphi)? .json::>() .await - .map_err(ApiError::Delphi)?, + .map_err(ApiError::delphi)?, Instant::now(), )) .0, diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index 49ef754ff0..a8f9d08ce6 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -163,8 +163,8 @@ pub enum ApiError { RateLimitError(u128, u32), #[error("Error while interacting with payment processor: {0}")] Stripe(#[from] stripe::StripeError), - #[error("Error while interacting with Delphi: {0}")] - Delphi(reqwest::Error), + #[error("Error while interacting with Delphi: {0:?}")] + Delphi(eyre::Error), #[error(transparent)] Mural(#[from] Box), #[error("report still has {} issue details with no verdict", details.len())] @@ -174,6 +174,10 @@ pub enum ApiError { } impl ApiError { + pub fn delphi(err: impl Into) -> Self { + Self::Delphi(err.into()) + } + pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> { crate::models::error::ApiError { error: match self { From a3316ba5c157f25df96ac010c35a1a332eab71b6 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Tue, 13 Jan 2026 12:44:14 +0000 Subject: [PATCH 3/3] update frontend to work with new versions list route --- apps/frontend/src/pages/[type]/[id].vue | 53 ++++--------------------- 1 file changed, 8 insertions(+), 45 deletions(-) diff --git a/apps/frontend/src/pages/[type]/[id].vue b/apps/frontend/src/pages/[type]/[id].vue index 11e72a7a65..e162ea41d6 100644 --- a/apps/frontend/src/pages/[type]/[id].vue +++ b/apps/frontend/src/pages/[type]/[id].vue @@ -1059,15 +1059,11 @@ const currentGameVersion = computed(() => { }) const possibleGameVersions = computed(() => { - return versions.value - .filter((x) => !currentPlatform.value || x.loaders.includes(currentPlatform.value)) - .flatMap((x) => x.game_versions) + return versionsV3.value?.available_game_versions || [] }) const possiblePlatforms = computed(() => { - return versions.value - .filter((x) => !currentGameVersion.value || x.game_versions.includes(currentGameVersion.value)) - .flatMap((x) => x.loaders) + return versionsV3.value?.available_loaders || [] }) const currentPlatform = computed(() => { @@ -1414,29 +1410,11 @@ const filteredVersions = computed(() => { ) }) -const filteredRelease = computed(() => { - return filteredVersions.value.find((x) => x.version_type === 'release') -}) +const filteredRelease = computed(() => versionsV3.value?.latest_versions?.release || null) -const filteredBeta = computed(() => { - return filteredVersions.value.find( - (x) => - x.version_type === 'beta' && - (!filteredRelease.value || - dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))), - ) -}) +const filteredBeta = computed(() => versionsV3.value?.latest_versions?.beta || null) -const filteredAlpha = computed(() => { - return filteredVersions.value.find( - (x) => - x.version_type === 'alpha' && - (!filteredRelease.value || - dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))) && - (!filteredBeta.value || - dayjs(x.date_published).isAfter(dayjs(filteredBeta.value.date_published))), - ) -}) +const filteredAlpha = computed(() => versionsV3.value?.latest_versions?.alpha || null) const displayCollectionsSearch = ref('') const collections = computed(() => @@ -1472,14 +1450,12 @@ let project, dependencies, versions, versionsV3, - resetVersionsV2, organization, resetOrganization, projectV2Error, projectV3Error, membersError, dependenciesError, - versionsError, versionsV3Error, resetVersionsV3 try { @@ -1488,7 +1464,6 @@ try { { data: projectV3, error: projectV3Error, refresh: resetProjectV3 }, { data: allMembers, error: membersError, refresh: resetMembers }, { data: dependencies, error: dependenciesError }, - { data: versions, error: versionsError, refresh: resetVersionsV2 }, { data: versionsV3, error: versionsV3Error, refresh: resetVersionsV3 }, { data: organization, refresh: resetOrganization }, ] = await Promise.all([ @@ -1528,9 +1503,6 @@ try { useAsyncData(`project/${projectId.value}/dependencies`, () => useBaseFetch(`project/${projectId.value}/dependencies`, {}), ), - useAsyncData(`project/${projectId.value}/version`, () => - useBaseFetch(`project/${projectId.value}/version`), - ), useAsyncData(`project/${projectId.value}/version/v3`, () => useBaseFetch(`project/${projectId.value}/version`, { apiVersion: 3 }), ), @@ -1541,12 +1513,8 @@ try { await updateProjectRoute() - versions = shallowRef(toRaw(versions)) - versionsV3 = shallowRef(toRaw(versionsV3)) - versions.value = (versions.value ?? []).map((v) => ({ - ...v, - environment: versionsV3.value?.find((v3) => v3.id === v.id)?.environment, - })) + versions = shallowRef(toRaw(versionsV3.value?.versions)) + versionsV3 = shallowRef(toRaw(versionsV3.value)) } catch (err) { throw createError({ fatal: true, @@ -1584,13 +1552,9 @@ async function resetProject() { } async function resetVersions() { - await resetVersionsV2() await resetVersionsV3() - versions.value = (versions.value ?? []).map((v) => ({ - ...v, - environment: versionsV3.value?.find((v3) => v3.id === v.id)?.environment, - })) + versions.value = versionsV3.value?.versions || [] } function handleError(err, project = false) { @@ -1610,7 +1574,6 @@ handleError(projectV2Error, true) handleError(projectV3Error) handleError(membersError) handleError(dependenciesError) -handleError(versionsError) handleError(versionsV3Error) if (!project.value) {