From b23d3e674f1f17c80ed3026026a6962fa900522f Mon Sep 17 00:00:00 2001
From: Josiah Glosson
Date: Wed, 15 Oct 2025 14:45:47 -0600
Subject: [PATCH 01/53] Update Rust & Java dependencies (#4540)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Update Java dependencies
* Baselint lint fixes
* Update Rust version
* Update actix-files 0.6.6 -> 0.6.8
* Update actix-http 3.11.0 -> 3.11.2
* Update actix-rt 2.10.0 -> 2.11.0
* Update async_zip 0.0.17 -> 0.0.18
* Update async-compression 0.4.27 -> 0.4.32
* Update async-trait 0.1.88 -> 0.1.89
* Update async-tungstenite 0.30.0 -> 0.31.0
* Update const_format 0.2.34 -> 0.2.35
* Update bitflags 2.9.1 -> 2.9.4
* Update bytemuck 1.23.1 -> 1.24.0
* Update typed-path 0.11.0 -> 0.12.0
* Update chrono 0.4.41 -> 0.4.42
* Update cidre 0.11.2 -> 0.11.3
* Update clap 4.5.43 -> 4.5.48
* Update data-url 0.3.1 -> 0.3.2
* Update discord-rich-presence 0.2.5 -> 1.0.0
* Update enumset 1.1.7 -> 1.1.10
* Update flate2 1.1.2 -> 1.1.4
* Update hyper 1.6.0 -> 1.7.0
* Update hyper-util 0.1.16 -> 0.1.17
* Update iana-time-zone 0.1.63 -> 0.1.64
* Update image 0.25.6 -> 0.25.8
* Update indexmap 2.10.0 -> 2.11.4
* Update json-patch 4.0.0 -> 4.1.0
* Update meilisearch-sdk 0.29.1 -> 0.30.0
* Update clickhouse 0.13.3 -> 0.14.0
* Fix some prettier things
* Update lettre 0.11.18 -> 0.11.19
* Update phf 0.12.1 -> 0.13.1
* Update png 0.17.16 -> 0.18.0
* Update quick-xml 0.38.1 -> 0.38.3
* Update redis 0.32.4 -> 0.32.7
* Update regex 1.11.1 -> 1.11.3
* Update reqwest 0.12.22 -> 0.12.23
* Update rust_decimal 1.37.2 -> 1.38.0
* Update rust-s3 0.35.1 -> 0.37.0
* Update serde 1.0.219 -> 1.0.228
* Update serde_bytes 0.11.17 -> 0.11.19
* Update serde_json 1.0.142 -> 1.0.145
* Update serde_with 3.14.0 -> 3.15.0
* Update sentry 0.42.0 -> 0.45.0 and sentry-actix 0.42.0 -> 0.45.0
* Update spdx 0.10.9 -> 0.12.0
* Update sysinfo 0.36.1 -> 0.37.2
* Update tauri 2.7.0 -> 2.8.5
* Update tauri-build 2.3.1 -> 2.4.1
* Update tauri-plugin-deep-link 2.4.1 -> 2.4.3
* Update tauri-plugin-dialog 2.3.2 -> 2.4.0
* Update tauri-plugin-http 2.5.1 -> 2.5.2
* Update tauri-plugin-opener 2.4.0 -> 2.5.0
* Update tauri-plugin-os 2.3.0 -> 2.3.1
* Update tauri-plugin-single-instance 2.3.2 -> 2.3.4
* Update tempfile 3.20.0 -> 3.23.0
* Update thiserror 2.0.12 -> 2.0.17
* Update tracing-subscriber 0.3.19 -> 0.3.20
* Update url 2.5.4 -> 2.5.7
* Update uuid 1.17.0 -> 1.18.1
* Update webp 0.3.0 -> 0.3.1
* Update whoami 1.6.0 -> 1.6.1
* Note that windows and windows-core can't be updated yet
* Update zbus 5.9.0 -> 5.11.0
* Update zip 4.3.0 -> 6.0.0
* Fix build
* Enforce rustls crypto provider
* Refresh Cargo.lock
* Update transitive dependencies
* Bump Gradle usage to Java 17
* Use ubuntu-latest consistently across workflows
* Fix lint
* Fix lint in Rust
* Update native-dialog 0.9.0 -> 0.9.2
* Update regex 1.11.3 -> 1.12.2
* Update reqwest 0.12.23 -> 0.12.24
* Update rust_decimal 1.38.0 -> 1.39.0
* Remaining lock-only updates
* chore: move TLS impl of some other dependencies to aws-lc-rs
The AWS bloatware "virus" expands by sheer force of widespread adoption
by the ecosystem... 🫣
* chore(fmt): run Tombi
---------
Co-authored-by: Alejandro González
---
.github/workflows/i18n-pull.yml | 2 +-
.github/workflows/i18n-push.yml | 2 +-
.github/workflows/theseus-build.yml | 6 +-
.github/workflows/turbo-ci.yml | 2 +-
Cargo.lock | 691 ++++++++++--------
Cargo.toml | 132 ++--
apps/daedalus_client/Dockerfile | 2 +-
.../src/content/docs/contributing/labrinth.md | 2 +-
apps/docs/src/styles/modrinth.css | 5 +-
apps/labrinth/.prettierignore | 4 +
apps/labrinth/Cargo.toml | 3 +-
apps/labrinth/Dockerfile | 2 +-
apps/labrinth/src/main.rs | 4 +
apps/labrinth/src/queue/analytics.rs | 6 +-
apps/labrinth/src/queue/email/templates.rs | 2 +-
apps/labrinth/src/routes/v3/analytics_get.rs | 7 +-
apps/labrinth/src/search/indexing/mod.rs | 1 +
apps/labrinth/src/validate/datapack.rs | 2 +-
apps/labrinth/src/validate/forge.rs | 6 +-
apps/labrinth/src/validate/quilt.rs | 2 +-
apps/labrinth/src/validate/resourcepack.rs | 6 +-
apps/labrinth/tests/common/mod.rs | 2 +
clippy.toml | 2 +-
packages/app-lib/.prettierignore | 2 +
packages/app-lib/java/build.gradle.kts | 70 +-
.../app-lib/java/gradle/libs.versions.toml | 2 +-
.../gradle/wrapper/gradle-wrapper.properties | 2 +-
packages/app-lib/java/settings.gradle.kts | 2 +-
.../src/api/minecraft_skins/png_util.rs | 131 ++--
packages/app-lib/src/error.rs | 3 +
packages/app-lib/src/state/discord.rs | 42 +-
packages/app-lib/src/state/friends.rs | 2 +-
packages/app-lib/src/state/process.rs | 2 +-
packages/blog/compiled/index.ts | 60 +-
rust-toolchain.toml | 2 +-
35 files changed, 630 insertions(+), 583 deletions(-)
diff --git a/.github/workflows/i18n-pull.yml b/.github/workflows/i18n-pull.yml
index 14a1c814..530dec45 100644
--- a/.github/workflows/i18n-pull.yml
+++ b/.github/workflows/i18n-pull.yml
@@ -11,7 +11,7 @@ concurrency:
jobs:
pull_translations:
name: 'Pull translations from Crowdin'
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
concurrency:
group: i18n-pull:${{ github.ref }}
diff --git a/.github/workflows/i18n-push.yml b/.github/workflows/i18n-push.yml
index 812b6e0d..0e34d9cf 100644
--- a/.github/workflows/i18n-push.yml
+++ b/.github/workflows/i18n-push.yml
@@ -18,7 +18,7 @@ concurrency:
jobs:
push_translations:
name: Push sources to Crowdin
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
concurrency:
group: i18n-push:${{ github.ref }}
diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml
index 64ae2b33..287f5b6d 100644
--- a/.github/workflows/theseus-build.yml
+++ b/.github/workflows/theseus-build.yml
@@ -28,13 +28,13 @@ jobs:
strategy:
fail-fast: false
matrix:
- platform: [macos-latest, windows-latest, ubuntu-22.04]
+ platform: [macos-latest, windows-latest, ubuntu-latest]
include:
- platform: macos-latest
artifact-target-name: universal-apple-darwin
- platform: windows-latest
artifact-target-name: x86_64-pc-windows-msvc
- - platform: ubuntu-22.04
+ - platform: ubuntu-latest
artifact-target-name: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.platform }}
@@ -127,7 +127,7 @@ jobs:
run: |
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
- $env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
+ $env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
if: startsWith(matrix.platform, 'windows')
diff --git a/.github/workflows/turbo-ci.yml b/.github/workflows/turbo-ci.yml
index e5140c2f..ba62ac5a 100644
--- a/.github/workflows/turbo-ci.yml
+++ b/.github/workflows/turbo-ci.yml
@@ -11,7 +11,7 @@ on:
jobs:
build:
name: Lint and Test
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-latest
env:
# Ensure pnpm output is colored in GitHub Actions logs
diff --git a/Cargo.lock b/Cargo.lock
index d6068768..10b4e70c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -59,9 +59,9 @@ dependencies = [
[[package]]
name = "actix-http"
-version = "3.11.1"
+version = "3.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0"
+checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
dependencies = [
"actix-codec",
"actix-rt",
@@ -603,7 +603,7 @@ dependencies = [
"polling",
"rustix 1.1.2",
"slab",
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -661,7 +661,7 @@ dependencies = [
"rustix 1.1.2",
"signal-hook-registry",
"slab",
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -729,9 +729,9 @@ dependencies = [
[[package]]
name = "async-tungstenite"
-version = "0.30.0"
+version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e308e9866b891743e3fdf9dfd6b57f85c5062ca01ce4fed6f393e76eb5accea4"
+checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f"
dependencies = [
"atomic-waker",
"futures-core",
@@ -744,7 +744,7 @@ dependencies = [
"tokio",
"tokio-rustls 0.26.4",
"tungstenite",
- "webpki-roots 1.0.2",
+ "webpki-roots 1.0.3",
]
[[package]]
@@ -760,16 +760,16 @@ dependencies = [
[[package]]
name = "async_zip"
-version = "0.0.17"
+version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
+checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6"
dependencies = [
"async-compression",
"chrono",
"crc32fast",
"futures-lite 2.6.1",
"pin-project",
- "thiserror 1.0.69",
+ "thiserror 2.0.17",
"tokio",
"tokio-util",
]
@@ -814,17 +814,18 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "attohttpc"
-version = "0.28.5"
+version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07a9b245ba0739fc90935094c29adbaee3f977218b5fb95e822e261cda7f56a3"
+checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9"
dependencies = [
+ "base64 0.22.1",
"http 1.3.1",
"log",
"rustls 0.23.32",
"serde",
"serde_json",
"url",
- "webpki-roots 0.26.11",
+ "webpki-roots 1.0.3",
]
[[package]]
@@ -858,28 +859,52 @@ dependencies = [
[[package]]
name = "aws-creds"
-version = "0.37.0"
+version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f84143206b9c72b3c5cb65415de60c7539c79cd1559290fddec657939131be0"
+checksum = "b13804829a843b3f26e151c97acbb315ee1177a2724690edfcd28f1894146200"
dependencies = [
"attohttpc",
"home",
"log",
- "quick-xml 0.32.0",
+ "quick-xml 0.38.3",
"rust-ini",
"serde",
- "thiserror 1.0.69",
+ "thiserror 2.0.17",
"time",
"url",
]
[[package]]
-name = "aws-region"
-version = "0.25.5"
+name = "aws-lc-rs"
+version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9aed3f9c7eac9be28662fdb3b0f4d1951e812f7c64fed4f0327ba702f459b3b"
+checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
dependencies = [
- "thiserror 1.0.69",
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2b715a6010afb9e457ca2b7c9d2b9c344baa8baed7b38dc476034c171b32575"
+dependencies = [
+ "bindgen",
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "libloading 0.8.8",
+]
+
+[[package]]
+name = "aws-region"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5532f65342f789f9c1b7078ea9c9cd9293cd62dcc284fa99adc4a1c9ba43469c"
+dependencies = [
+ "thiserror 2.0.17",
]
[[package]]
@@ -941,7 +966,7 @@ dependencies = [
"miniz_oxide",
"object",
"rustc-demangle",
- "windows-link 0.2.0",
+ "windows-link 0.2.1",
]
[[package]]
@@ -980,6 +1005,26 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "bitflags 2.9.4",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 2.0.106",
+]
+
[[package]]
name = "bit-set"
version = "0.5.3"
@@ -1279,14 +1324,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
dependencies = [
"serde",
- "toml 0.9.7",
+ "toml 0.9.8",
+]
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
]
[[package]]
name = "cc"
-version = "1.2.40"
+version = "1.2.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb"
+checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1309,6 +1363,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom 7.1.3",
+]
+
[[package]]
name = "cfb"
version = "0.7.3"
@@ -1364,7 +1427,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
- "windows-link 0.2.0",
+ "windows-link 0.2.1",
]
[[package]]
@@ -1398,6 +1461,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93a719913643003b84bd13022b4b7e703c09342cd03b679c4641c7d2e50dc34d"
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading 0.8.8",
+]
+
[[package]]
name = "clap"
version = "4.5.48"
@@ -1440,16 +1514,17 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "clickhouse"
-version = "0.13.3"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a9a81a1dffadd762ee662635ce409232258ce9beebd7cc0fa227df0b5e7efc0"
+checksum = "52d6ac02411e84914fdf4e0565bfe02fc4bebdf375bd1fc58168bad74b3707a2"
dependencies = [
"bstr",
"bytes",
"cityhash-rs",
- "clickhouse-derive",
- "futures",
+ "clickhouse-macros",
+ "clickhouse-types",
"futures-channel",
+ "futures-util",
"http-body-util",
"hyper 1.7.0",
"hyper-util",
@@ -1458,7 +1533,7 @@ dependencies = [
"sealed",
"serde",
"static_assertions",
- "thiserror 1.0.69",
+ "thiserror 2.0.17",
"time",
"tokio",
"url",
@@ -1466,10 +1541,10 @@ dependencies = [
]
[[package]]
-name = "clickhouse-derive"
-version = "0.2.0"
+name = "clickhouse-macros"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d70f3e2893f7d3e017eeacdc9a708fbc29a10488e3ebca21f9df6a5d2b616dbb"
+checksum = "ff6669899e23cb87b43daf7996f0ea3b9c07d0fb933d745bb7b815b052515ae3"
dependencies = [
"proc-macro2",
"quote",
@@ -1477,6 +1552,25 @@ dependencies = [
"syn 2.0.106",
]
+[[package]]
+name = "clickhouse-types"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "235f72141cfbe1d2d930d8156a34814c8a3d60491febb9af64cc52a203444764"
+dependencies = [
+ "bytes",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "color-eyre"
version = "0.6.5"
@@ -1539,6 +1633,19 @@ dependencies = [
"tokio-util",
]
+[[package]]
+name = "compact_str"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "ryu",
+ "static_assertions",
+]
+
[[package]]
name = "compression-codecs"
version = "0.4.31"
@@ -1579,8 +1686,8 @@ dependencies = [
"encode_unicode",
"libc",
"once_cell",
- "unicode-width 0.2.1",
- "windows-sys 0.61.1",
+ "unicode-width 0.2.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2264,7 +2371,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2280,14 +2387,16 @@ dependencies = [
[[package]]
name = "discord-rich-presence"
-version = "0.2.5"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75db747ecd252c01bfecaf709b07fcb4c634adf0edb5fed47bc9c3052e7076b"
+checksum = "ead3c5edc7e048c317c6fc4a7e24aff0c7e4c136918e2ba38106a385b2cc53a5"
dependencies = [
+ "log",
"serde",
"serde_derive",
"serde_json",
"serde_repr",
+ "thiserror 2.0.17",
"uuid 0.8.2",
]
@@ -2326,7 +2435,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
- "libloading 0.8.9",
+ "libloading 0.8.8",
]
[[package]]
@@ -2492,7 +2601,7 @@ dependencies = [
"cc",
"memchr",
"rustc_version",
- "toml 0.9.7",
+ "toml 0.9.8",
"vswhom",
"winreg 0.55.0",
]
@@ -2581,9 +2690,9 @@ dependencies = [
[[package]]
name = "env_filter"
-version = "0.1.3"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
dependencies = [
"log",
]
@@ -2648,7 +2757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2696,7 +2805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
dependencies = [
"bit_field",
- "half 2.6.0",
+ "half 2.7.0",
"lebe",
"miniz_oxide",
"rayon-core",
@@ -2803,9 +2912,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
-version = "0.1.3"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3"
+checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
[[package]]
name = "findshlibs"
@@ -2821,9 +2930,9 @@ dependencies = [
[[package]]
name = "flate2"
-version = "1.1.2"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9"
dependencies = [
"crc32fast",
"libz-rs-sys",
@@ -2906,6 +3015,12 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@@ -3169,9 +3284,9 @@ dependencies = [
[[package]]
name = "generic-array"
-version = "0.14.7"
+version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
@@ -3447,12 +3562,13 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "half"
-version = "2.6.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
+checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5"
dependencies = [
"cfg-if",
"crunchy",
+ "zerocopy",
]
[[package]]
@@ -3812,7 +3928,7 @@ dependencies = [
"tokio",
"tokio-rustls 0.26.4",
"tower-service",
- "webpki-roots 1.0.2",
+ "webpki-roots 1.0.3",
]
[[package]]
@@ -3846,7 +3962,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.6.0",
+ "socket2 0.6.1",
"system-configuration",
"tokio",
"tower-service",
@@ -3866,7 +3982,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.62.1",
+ "windows-core 0.62.2",
]
[[package]]
@@ -4084,7 +4200,7 @@ checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
"console",
"portable-atomic",
- "unicode-width 0.2.1",
+ "unicode-width 0.2.2",
"unit-prefix",
"web-time",
]
@@ -4531,6 +4647,7 @@ dependencies = [
"rust-s3",
"rust_decimal",
"rust_iso3166",
+ "rustls 0.23.32",
"rusty-money",
"sentry",
"sentry-actix",
@@ -4559,7 +4676,7 @@ dependencies = [
"webp",
"woothee",
"yaserde",
- "zip",
+ "zip 6.0.0",
"zxcvbn",
]
@@ -4586,9 +4703,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "lettre"
-version = "0.11.18"
+version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56"
+checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -4607,7 +4724,7 @@ dependencies = [
"quoted_printable",
"rustls 0.23.32",
"rustls-native-certs 0.8.1",
- "socket2 0.6.0",
+ "socket2 0.6.1",
"tokio",
"tokio-rustls 0.26.4",
"url",
@@ -4645,9 +4762,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]]
name = "libc"
-version = "0.2.176"
+version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libfuzzer-sys"
@@ -4671,12 +4788,12 @@ dependencies = [
[[package]]
name = "libloading"
-version = "0.8.9"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
- "windows-link 0.2.0",
+ "windows-targets 0.53.5",
]
[[package]]
@@ -4914,15 +5031,15 @@ dependencies = [
[[package]]
name = "md5"
-version = "0.7.0"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
[[package]]
name = "meilisearch-index-setting-macro"
-version = "0.29.1"
+version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14a9c03a7c7c9c2b6396bf824b7e9181feb242aafaddf43dede6ccff0ab8b229"
+checksum = "e36e5cad3754ed329feb201c24f26c0694442bef50a90dbcff0ba6d12b5ca133"
dependencies = [
"convert_case 0.8.0",
"proc-macro2",
@@ -4933,15 +5050,17 @@ dependencies = [
[[package]]
name = "meilisearch-sdk"
-version = "0.29.1"
+version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4bde2e2d304697ec15ec6475d2f299e73bb24347ab1d356de8709a83e3bf6f3"
+checksum = "f620c9ed9b790cf292b66c8dab16f0e4aafb377583ec0e589da7c3ee81ff2038"
dependencies = [
"async-trait",
"bytes",
"either",
- "futures",
+ "futures-channel",
+ "futures-core",
"futures-io",
+ "futures-util",
"iso8601",
"jsonwebtoken",
"log",
@@ -4952,6 +5071,7 @@ dependencies = [
"serde_json",
"thiserror 2.0.17",
"time",
+ "tokio",
"uuid 1.18.1",
"wasm-bindgen-futures",
"web-sys",
@@ -4991,9 +5111,9 @@ dependencies = [
[[package]]
name = "minidom"
-version = "0.15.2"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f45614075738ce1b77a1768912a60c0227525971b03e09122a05b8a34a2a6278"
+checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d"
dependencies = [
"rxml",
]
@@ -5052,9 +5172,9 @@ dependencies = [
[[package]]
name = "moxcms"
-version = "0.7.5"
+version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
+checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40"
dependencies = [
"num-traits",
"pxfm",
@@ -5095,9 +5215,9 @@ checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577"
[[package]]
name = "native-dialog"
-version = "0.9.0"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f006431cea71a83e6668378cb5abc2d52af299cbac6dca1780c6eeca90822df"
+checksum = "1657b63bf0e60ee0eca886b5df70269240b6197b6ee46ec37da9a7d28d8e8e24"
dependencies = [
"ascii",
"block2 0.6.2",
@@ -5244,11 +5364,11 @@ dependencies = [
[[package]]
name = "nu-ansi-term"
-version = "0.50.1"
+version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5477,9 +5597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.9.4",
- "block2 0.6.2",
"dispatch2",
- "libc",
"objc2 0.6.3",
]
@@ -5490,13 +5608,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.9.4",
- "block2 0.6.2",
"dispatch2",
- "libc",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-io-surface",
- "objc2-metal 0.3.2",
]
[[package]]
@@ -5617,17 +5732,6 @@ dependencies = [
"objc2-foundation 0.2.2",
]
-[[package]]
-name = "objc2-metal"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
-dependencies = [
- "bitflags 2.9.4",
- "objc2 0.6.3",
- "objc2-foundation 0.3.2",
-]
-
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
@@ -5650,7 +5754,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
- "objc2-metal 0.2.2",
+ "objc2-metal",
]
[[package]]
@@ -5867,7 +5971,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
- "windows-link 0.2.0",
+ "windows-link 0.2.1",
]
[[package]]
@@ -5956,12 +6060,12 @@ dependencies = [
[[package]]
name = "phf"
-version = "0.12.1"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
- "phf_macros 0.12.1",
- "phf_shared 0.12.1",
+ "phf_macros 0.13.1",
+ "phf_shared 0.13.1",
"serde",
]
@@ -6017,12 +6121,12 @@ dependencies = [
[[package]]
name = "phf_generator"
-version = "0.12.1"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand 2.3.0",
- "phf_shared 0.12.1",
+ "phf_shared 0.13.1",
]
[[package]]
@@ -6054,12 +6158,12 @@ dependencies = [
[[package]]
name = "phf_macros"
-version = "0.12.1"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
- "phf_generator 0.12.1",
- "phf_shared 0.12.1",
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
"proc-macro2",
"quote",
"syn 2.0.106",
@@ -6094,9 +6198,9 @@ dependencies = [
[[package]]
name = "phf_shared"
-version = "0.12.1"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher 1.0.1",
]
@@ -6221,7 +6325,7 @@ dependencies = [
"hermit-abi",
"pin-project-lite",
"rustix 1.1.2",
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -6275,6 +6379,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.106",
+]
+
[[package]]
name = "prettytable-rs"
version = "0.10.0"
@@ -6324,7 +6438,7 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
- "toml_edit 0.23.6",
+ "toml_edit 0.23.7",
]
[[package]]
@@ -6545,9 +6659,9 @@ dependencies = [
[[package]]
name = "pxfm"
-version = "0.1.24"
+version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
+checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
dependencies = [
"num-traits",
]
@@ -6592,16 +6706,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
-[[package]]
-name = "quick-xml"
-version = "0.32.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
-dependencies = [
- "memchr",
- "serde",
-]
-
[[package]]
name = "quick-xml"
version = "0.37.5"
@@ -6618,6 +6722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
dependencies = [
"memchr",
+ "serde",
"tokio",
]
@@ -6634,7 +6739,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls 0.23.32",
- "socket2 0.6.0",
+ "socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -6671,7 +6776,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
- "socket2 0.6.0",
+ "socket2 0.6.1",
"tracing",
"windows-sys 0.60.2",
]
@@ -6918,7 +7023,7 @@ dependencies = [
"r2d2",
"ryu",
"sha1_smol",
- "socket2 0.6.0",
+ "socket2 0.6.1",
"tokio",
"tokio-util",
"url",
@@ -6977,9 +7082,9 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.11.3"
+version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
+checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
@@ -6989,9 +7094,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.11"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
@@ -7000,15 +7105,15 @@ dependencies = [
[[package]]
name = "regex-lite"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
+checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da"
[[package]]
name = "regex-syntax"
-version = "0.8.6"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rend"
@@ -7027,9 +7132,9 @@ checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884"
[[package]]
name = "reqwest"
-version = "0.12.23"
+version = "0.12.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
+checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
dependencies = [
"async-compression",
"base64 0.22.1",
@@ -7072,7 +7177,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
- "webpki-roots 1.0.2",
+ "webpki-roots 1.0.3",
]
[[package]]
@@ -7206,9 +7311,9 @@ dependencies = [
[[package]]
name = "rust-s3"
-version = "0.35.1"
+version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3df3f353b1f4209dcf437d777cda90279c397ab15a0cd6fd06bd32c88591533"
+checksum = "94f9b973bd4097f5bb47e5827dcb9fb5dc17e93879e46badc27d2a4e9a4e5588"
dependencies = [
"async-trait",
"aws-creds",
@@ -7216,37 +7321,34 @@ dependencies = [
"base64 0.22.1",
"bytes",
"cfg-if",
- "futures",
+ "futures-util",
"hex",
"hmac",
- "http 0.2.12",
- "hyper 0.14.32",
- "hyper-rustls 0.24.2",
+ "http 1.3.1",
"log",
"maybe-async",
"md5",
"minidom",
"percent-encoding",
- "quick-xml 0.32.0",
- "rustls 0.21.12",
- "rustls-native-certs 0.6.3",
+ "quick-xml 0.38.3",
+ "reqwest",
"serde",
"serde_derive",
"serde_json",
"sha2",
- "thiserror 1.0.69",
+ "sysinfo",
+ "thiserror 2.0.17",
"time",
"tokio",
- "tokio-rustls 0.24.1",
"tokio-stream",
"url",
]
[[package]]
name = "rust_decimal"
-version = "1.38.0"
+version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0"
+checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282"
dependencies = [
"arrayvec",
"borsh",
@@ -7260,9 +7362,9 @@ dependencies = [
[[package]]
name = "rust_decimal_macros"
-version = "1.38.0"
+version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dae310b657d2d686616e215c84c3119c675450d64c4b9f9e3467209191c3bcf"
+checksum = "ae8c0cb48f413ebe24dc2d148788e0efbe09ba3e011d9277162f2eaf8e1069a3"
dependencies = [
"quote",
"syn 2.0.106",
@@ -7324,7 +7426,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -7345,6 +7447,7 @@ version = "0.23.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
dependencies = [
+ "aws-lc-rs",
"log",
"once_cell",
"ring",
@@ -7422,6 +7525,7 @@ version = "0.103.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
dependencies = [
+ "aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -7445,20 +7549,22 @@ dependencies = [
[[package]]
name = "rxml"
-version = "0.9.1"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a98f186c7a2f3abbffb802984b7f1dfd65dac8be1aafdaabbca4137f53f0dff7"
+checksum = "65bc94b580d0f5a6b7a2d604e597513d3c673154b52ddeccd1d5c32360d945ee"
dependencies = [
"bytes",
"rxml_validation",
- "smartstring",
]
[[package]]
name = "rxml_validation"
-version = "0.9.1"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22a197350ece202f19a166d1ad6d9d6de145e1d2a8ef47db299abe164dbd7530"
+checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4"
+dependencies = [
+ "compact_str",
+]
[[package]]
name = "ryu"
@@ -7481,7 +7587,7 @@ version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -7663,9 +7769,9 @@ dependencies = [
[[package]]
name = "sentry"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "989425268ab5c011e06400187eed6c298272f8ef913e49fcadc3fda788b45030"
+checksum = "48b85e25e8a1fc13928885e8bf13abe8a09e15c46993aed05d6405f7755d6e20"
dependencies = [
"httpdate",
"reqwest",
@@ -7682,9 +7788,9 @@ dependencies = [
[[package]]
name = "sentry-actix"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5c675bdf6118764a8e265c3395c311b4d905d12866c92df52870c0223d2ffc1"
+checksum = "cc694e6ffc8d5d7fdb2a33923b0358f6ad41c0b428ced034b349b9e2b08260bc"
dependencies = [
"actix-http",
"actix-web",
@@ -7695,9 +7801,9 @@ dependencies = [
[[package]]
name = "sentry-backtrace"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68e299dd3f7bcf676875eee852c9941e1d08278a743c32ca528e2debf846a653"
+checksum = "f3253a495ab536f6de1746a58d5d7824b77d75e08e1a4b8ca6fb356839077ae0"
dependencies = [
"backtrace",
"regex",
@@ -7706,9 +7812,9 @@ dependencies = [
[[package]]
name = "sentry-contexts"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fac0c5d6892cd4c414492fc957477b620026fb3411fca9fa12774831da561c88"
+checksum = "027f81a728836e66b88c07666a10f5ed5a35e2695b04eb7aa0fcbed93f814900"
dependencies = [
"hostname",
"libc",
@@ -7720,9 +7826,9 @@ dependencies = [
[[package]]
name = "sentry-core"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "deaa38b94e70820ff3f1f9db3c8b0aef053b667be130f618e615e0ff2492cbcc"
+checksum = "d3b6729c8e71ac968edbe9bf2dd4109c162e552b52bacd2b07e24ede1aba84a5"
dependencies = [
"rand 0.9.2",
"sentry-types",
@@ -7733,9 +7839,9 @@ dependencies = [
[[package]]
name = "sentry-debug-images"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00950648aa0d371c7f57057434ad5671bd4c106390df7e7284739330786a01b6"
+checksum = "dc85b59c1dfb19912bfba1af73a592e2e5548cae241a79ecb805afab3333d04c"
dependencies = [
"findshlibs",
"sentry-core",
@@ -7743,9 +7849,9 @@ dependencies = [
[[package]]
name = "sentry-panic"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b7a23b13c004873de3ce7db86eb0f59fe4adfc655a31f7bbc17fd10bacc9bfe"
+checksum = "1ac0471f04f8f97af0c17eeca2c516e23faa1c0271a55bc64371d9ce488c2d40"
dependencies = [
"sentry-backtrace",
"sentry-core",
@@ -7753,9 +7859,9 @@ dependencies = [
[[package]]
name = "sentry-tracing"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080"
+checksum = "428f780866a613142dcc81b7f8551ae4d1c056f4df22b6d7ddd9154a9974eb03"
dependencies = [
"bitflags 2.9.4",
"sentry-backtrace",
@@ -7766,9 +7872,9 @@ dependencies = [
[[package]]
name = "sentry-types"
-version = "0.42.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e477f4d4db08ddb4ab553717a8d3a511bc9e81dde0c808c680feacbb8105c412"
+checksum = "2c19d1d1967b55659c358886d0f1aa3076488d445f84c7d727d384c675adaec1"
dependencies = [
"debugid",
"hex",
@@ -7954,9 +8060,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
+checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
dependencies = [
"serde_core",
]
@@ -8169,17 +8275,6 @@ dependencies = [
"syn 1.0.109",
]
-[[package]]
-name = "smartstring"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
-dependencies = [
- "autocfg",
- "static_assertions",
- "version_check",
-]
-
[[package]]
name = "smol_str"
version = "0.1.24"
@@ -8201,12 +8296,12 @@ dependencies = [
[[package]]
name = "socket2"
-version = "0.6.0"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -8259,9 +8354,9 @@ dependencies = [
[[package]]
name = "spdx"
-version = "0.10.9"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
+checksum = "41cf87c0efffc158b9dde4d6e0567a43e4383adc4c949e687a2039732db2f23a"
dependencies = [
"smallvec",
]
@@ -8488,9 +8583,9 @@ dependencies = [
[[package]]
name = "stable_deref_trait"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stacker"
@@ -8658,9 +8753,9 @@ dependencies = [
[[package]]
name = "sysinfo"
-version = "0.36.1"
+version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d"
+checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f"
dependencies = [
"libc",
"memchr",
@@ -8857,7 +8952,7 @@ dependencies = [
"tauri-codegen",
"tauri-utils",
"tauri-winres",
- "toml 0.9.7",
+ "toml 0.9.8",
"walkdir",
]
@@ -8915,7 +9010,7 @@ dependencies = [
"serde",
"serde_json",
"tauri-utils",
- "toml 0.9.7",
+ "toml 0.9.8",
"walkdir",
]
@@ -8976,7 +9071,7 @@ dependencies = [
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.17",
- "toml 0.9.7",
+ "toml 0.9.8",
"url",
]
@@ -9088,7 +9183,7 @@ dependencies = [
"tokio",
"url",
"windows-sys 0.60.2",
- "zip",
+ "zip 4.6.1",
]
[[package]]
@@ -9189,7 +9284,7 @@ dependencies = [
"serde_with",
"swift-rs",
"thiserror 2.0.17",
- "toml 0.9.7",
+ "toml 0.9.8",
"url",
"urlpattern",
"uuid 1.18.1",
@@ -9203,7 +9298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074"
dependencies = [
"embed-resource",
- "toml 0.9.7",
+ "toml 0.9.8",
]
[[package]]
@@ -9216,7 +9311,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.2",
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -9280,8 +9375,8 @@ dependencies = [
"p256",
"paste",
"path-util",
- "phf 0.12.1",
- "png 0.17.16",
+ "phf 0.13.1",
+ "png 0.18.0",
"quartz_nbt",
"quick-xml 0.38.3",
"rand 0.8.5",
@@ -9311,7 +9406,7 @@ dependencies = [
"windows-core 0.61.2",
"winreg 0.55.0",
"zbus",
- "zip",
+ "zip 6.0.0",
]
[[package]]
@@ -9419,7 +9514,7 @@ checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
dependencies = [
"fax",
"flate2",
- "half 2.6.0",
+ "half 2.7.0",
"quick-error",
"weezl",
"zune-jpeg",
@@ -9536,7 +9631,7 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"slab",
- "socket2 0.6.0",
+ "socket2 0.6.1",
"tokio-macros",
"tracing",
"windows-sys 0.59.0",
@@ -9613,14 +9708,14 @@ dependencies = [
[[package]]
name = "toml"
-version = "0.9.7"
+version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
+checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [
"indexmap 2.11.4",
"serde_core",
- "serde_spanned 1.0.2",
- "toml_datetime 0.7.2",
+ "serde_spanned 1.0.3",
+ "toml_datetime 0.7.3",
"toml_parser",
"toml_writer",
"winnow 0.7.13",
@@ -9637,9 +9732,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
-version = "0.7.2"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
+checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
dependencies = [
"serde_core",
]
@@ -9670,30 +9765,30 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.23.6"
+version = "0.23.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b"
+checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
dependencies = [
"indexmap 2.11.4",
- "toml_datetime 0.7.2",
+ "toml_datetime 0.7.3",
"toml_parser",
"winnow 0.7.13",
]
[[package]]
name = "toml_parser"
-version = "1.0.3"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
+checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
dependencies = [
"winnow 0.7.13",
]
[[package]]
name = "toml_writer"
-version = "1.0.3"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
+checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "tonic"
@@ -9955,9 +10050,9 @@ dependencies = [
[[package]]
name = "typed-path"
-version = "0.11.0"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566"
+checksum = "7922f2cdc51280d47b491af9eafc41eb0cdab85eabcb390c854412fcbf26dbe8"
[[package]]
name = "typeid"
@@ -10079,9 +10174,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
-version = "0.2.1"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
@@ -10115,7 +10210,7 @@ dependencies = [
"rustls-pki-types",
"ureq-proto",
"utf-8",
- "webpki-roots 1.0.2",
+ "webpki-roots 1.0.3",
]
[[package]]
@@ -10592,14 +10687,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
- "webpki-roots 1.0.2",
+ "webpki-roots 1.0.3",
]
[[package]]
name = "webpki-roots"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
+checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8"
dependencies = [
"rustls-pki-types",
]
@@ -10681,9 +10776,9 @@ dependencies = [
[[package]]
name = "widestring"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
+checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi"
@@ -10707,7 +10802,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.1",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -10768,15 +10863,15 @@ dependencies = [
[[package]]
name = "windows-core"
-version = "0.62.1"
+version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
- "windows-link 0.2.0",
- "windows-result 0.4.0",
- "windows-strings 0.5.0",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
]
[[package]]
@@ -10792,9 +10887,9 @@ dependencies = [
[[package]]
name = "windows-implement"
-version = "0.60.1"
+version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
@@ -10803,9 +10898,9 @@ dependencies = [
[[package]]
name = "windows-interface"
-version = "0.59.2"
+version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
@@ -10820,9 +10915,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
@@ -10856,11 +10951,11 @@ dependencies = [
[[package]]
name = "windows-result"
-version = "0.4.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
- "windows-link 0.2.0",
+ "windows-link 0.2.1",
]
[[package]]
@@ -10874,11 +10969,11 @@ dependencies = [
[[package]]
name = "windows-strings"
-version = "0.5.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
- "windows-link 0.2.0",
+ "windows-link 0.2.1",
]
[[package]]
@@ -10923,16 +11018,16 @@ version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
- "windows-targets 0.53.4",
+ "windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
-version = "0.61.1"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows-link 0.2.0",
+ "windows-link 0.2.1",
]
[[package]]
@@ -10983,19 +11078,19 @@ dependencies = [
[[package]]
name = "windows-targets"
-version = "0.53.4"
+version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
- "windows-link 0.2.0",
- "windows_aarch64_gnullvm 0.53.0",
- "windows_aarch64_msvc 0.53.0",
- "windows_i686_gnu 0.53.0",
- "windows_i686_gnullvm 0.53.0",
- "windows_i686_msvc 0.53.0",
- "windows_x86_64_gnu 0.53.0",
- "windows_x86_64_gnullvm 0.53.0",
- "windows_x86_64_msvc 0.53.0",
+ "windows-link 0.2.1",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
]
[[package]]
@@ -11009,11 +11104,11 @@ dependencies = [
[[package]]
name = "windows-version"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "700dad7c058606087f6fdc1f88da5841e06da40334413c6cd4367b25ef26d24e"
+checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
dependencies = [
- "windows-link 0.2.0",
+ "windows-link 0.2.1",
]
[[package]]
@@ -11036,9 +11131,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.53.0"
+version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
@@ -11060,9 +11155,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.53.0"
+version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
@@ -11084,9 +11179,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
-version = "0.53.0"
+version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
@@ -11096,9 +11191,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
-version = "0.53.0"
+version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
@@ -11120,9 +11215,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
-version = "0.53.0"
+version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
@@ -11144,9 +11239,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.53.0"
+version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
@@ -11168,9 +11263,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.53.0"
+version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
@@ -11192,9 +11287,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.53.0"
+version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
@@ -11264,9 +11359,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "wry"
-version = "0.53.3"
+version = "0.53.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90"
+checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6"
dependencies = [
"base64 0.22.1",
"block2 0.6.2",
@@ -11561,6 +11656,18 @@ name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "indexmap 2.11.4",
+ "memchr",
+]
+
+[[package]]
+name = "zip"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b"
dependencies = [
"arbitrary",
"bzip2",
diff --git a/Cargo.toml b/Cargo.toml
index 8d42c330..11de2144 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,84 +16,84 @@ edition = "2024"
[workspace.dependencies]
actix-cors = "0.7.1"
-actix-files = "0.6.6"
-actix-http = "3.11.0"
+actix-files = "0.6.8"
+actix-http = "3.11.2"
actix-multipart = "0.7.2"
-actix-rt = "2.10.0"
+actix-rt = "2.11.0"
actix-web = "4.11.0"
actix-web-prom = "0.10.0"
actix-ws = "0.3.0"
argon2 = { version = "0.5.3", features = ["std"] }
ariadne = { path = "packages/ariadne" }
-async-compression = { version = "0.4.27", default-features = false }
+async-compression = { version = "0.4.32", default-features = false }
async-recursion = "1.1.1"
async-stripe = { version = "0.41.0", default-features = false, features = [
"runtime-tokio-hyper-rustls",
] }
-async-trait = "0.1.88"
-async-tungstenite = { version = "0.30.0", default-features = false, features = [
+async-trait = "0.1.89"
+async-tungstenite = { version = "0.31.0", default-features = false, features = [
"futures-03-sink",
] }
async-walkdir = "2.1.0"
-async_zip = "0.0.17"
+async_zip = "0.0.18"
base64 = "0.22.1"
-bitflags = "2.9.1"
-bytemuck = "1.23.1"
+bitflags = "2.9.4"
+bytemuck = "1.24.0"
bytes = "1.10.1"
censor = "0.3.0"
chardetng = "0.1.17"
-chrono = "0.4.41"
-cidre = { version = "0.11.2", default-features = false, features = [
+chrono = "0.4.42"
+cidre = { version = "0.11.3", default-features = false, features = [
"macos_15_0",
] }
-clap = "4.5.43"
-clickhouse = "0.13.3"
+clap = "4.5.48"
+clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
console-subscriber = "0.4.1"
const_format = "0.2.34"
daedalus = { path = "packages/daedalus" }
dashmap = "6.1.0"
-data-url = "0.3.1"
+data-url = "0.3.2"
deadpool-redis = "0.22.0"
derive_more = "2.0.1"
dirs = "6.0.0"
-discord-rich-presence = "0.2.5"
+discord-rich-presence = "1.0.0"
dotenv-build = "0.1.1"
dotenvy = "0.15.7"
dunce = "1.0.5"
either = "1.15.0"
encoding_rs = "0.8.35"
-enumset = "1.1.7"
+enumset = "1.1.10"
eyre = "0.6.12"
-flate2 = "1.1.2"
+flate2 = "1.1.4"
fs4 = { version = "0.13.1", default-features = false }
-futures = { version = "0.3.31", default-features = false }
+futures = "0.3.31"
futures-util = "0.3.31"
heck = "0.5.0"
hex = "0.4.3"
hickory-resolver = "0.25.2"
hmac = "0.12.1"
-hyper = "1.6.0"
+hyper = "1.7.0"
hyper-rustls = { version = "0.27.7", default-features = false, features = [
+ "aws-lc-rs",
"http1",
"native-tokio",
- "ring",
"tls12",
] }
-hyper-util = "0.1.16"
-iana-time-zone = "0.1.63"
-image = { version = "0.25.6", default-features = false, features = ["rayon"] }
-indexmap = "2.10.0"
+hyper-util = "0.1.17"
+iana-time-zone = "0.1.64"
+image = { version = "0.25.8", default-features = false, features = ["rayon"] }
+indexmap = "2.11.4"
indicatif = "0.18.0"
itertools = "0.14.0"
jemalloc_pprof = "0.8.1"
-json-patch = { version = "4.0.0", default-features = false }
-lettre = { version = "0.11.18", default-features = false, features = [
+json-patch = { version = "4.1.0", default-features = false }
+lettre = { version = "0.11.19", default-features = false, features = [
+ "aws-lc-rs",
"builder",
"hostname",
"pool",
- "ring",
"rustls",
"rustls-native-certs",
"smtp-transport",
@@ -101,37 +101,38 @@ lettre = { version = "0.11.18", default-features = false, features = [
"tokio1-rustls",
] }
maxminddb = "0.26.0"
-meilisearch-sdk = { version = "0.29.1", default-features = false }
+meilisearch-sdk = { version = "0.30.0", default-features = false }
murmur2 = "0.1.0"
-native-dialog = "0.9.0"
+native-dialog = "0.9.2"
notify = { version = "8.2.0", default-features = false }
notify-debouncer-mini = { version = "0.7.0", default-features = false }
p256 = "0.13.2"
paste = "1.0.15"
path-util = { path = "packages/path-util" }
-phf = { version = "0.12.1", features = ["macros"] }
-png = "0.17.16"
+phf = { version = "0.13.1", features = ["macros"] }
+png = "0.18.0"
prometheus = "0.14.0"
quartz_nbt = "0.2.9"
-quick-xml = "0.38.1"
+quick-xml = "0.38.3"
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
-redis = "0.32.4"
-regex = "1.11.1"
-reqwest = { version = "0.12.22", default-features = false }
+redis = "0.32.7"
+regex = "1.12.2"
+reqwest = { version = "0.12.24", default-features = false }
rgb = "0.8.52"
-rust_decimal = { version = "1.37.2", features = [
+rust_decimal = { version = "1.39.0", features = [
"serde-with-float",
"serde-with-str",
] }
rust_iso3166 = "0.1.14"
-rust-s3 = { version = "0.35.1", default-features = false, features = [
+rust-s3 = { version = "0.37.0", default-features = false, features = [
"fail-on-err",
"tags",
"tokio-rustls-tls",
] }
+rustls = "0.23.32"
rusty-money = "0.4.1"
-sentry = { version = "0.42.0", default-features = false, features = [
+sentry = { version = "0.45.0", default-features = false, features = [
"backtrace",
"contexts",
"debug-images",
@@ -139,37 +140,37 @@ sentry = { version = "0.42.0", default-features = false, features = [
"reqwest",
"rustls",
] }
-sentry-actix = "0.42.0"
-serde = "1.0.219"
-serde_bytes = "0.11.17"
+sentry-actix = "0.45.0"
+serde = "1.0.228"
+serde_bytes = "0.11.19"
serde_cbor = "0.11.2"
serde_ini = "0.2.0"
-serde_json = "1.0.142"
-serde_with = "3.14.0"
+serde_json = "1.0.145"
+serde_with = "3.15.0"
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
sha1 = "0.10.6"
sha1_smol = { version = "1.0.1", features = ["std"] }
sha2 = "0.10.9"
-spdx = "0.10.9"
+spdx = "0.12.0"
sqlx = { version = "0.8.6", default-features = false }
-sysinfo = { version = "0.36.1", default-features = false }
+sysinfo = { version = "0.37.2", default-features = false }
tar = "0.4.44"
-tauri = "2.7.0"
-tauri-build = "2.3.1"
-tauri-plugin-deep-link = "2.4.1"
-tauri-plugin-dialog = "2.3.2"
-tauri-plugin-http = "2.5.1"
-tauri-plugin-opener = "2.4.0"
-tauri-plugin-os = "2.3.0"
-tauri-plugin-single-instance = "2.3.2"
+tauri = "2.8.5"
+tauri-build = "2.4.1"
+tauri-plugin-deep-link = "2.4.3"
+tauri-plugin-dialog = "2.4.0"
+tauri-plugin-http = "2.5.2"
+tauri-plugin-opener = "2.5.0"
+tauri-plugin-os = "2.3.1"
+tauri-plugin-single-instance = "2.3.4"
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
"rustls-tls",
"zip",
] }
tauri-plugin-window-state = "2.4.0"
-tempfile = "3.20.0"
+tempfile = "3.23.0"
theseus = { path = "packages/app-lib" }
-thiserror = "2.0.12"
+thiserror = "2.0.17"
tikv-jemalloc-ctl = "0.6.0"
tikv-jemallocator = "0.6.0"
tokio = "1.47.1"
@@ -180,22 +181,22 @@ tracing = "0.1.41"
tracing-actix-web = { version = "0.7.19", default-features = false }
tracing-ecs = "0.5.0"
tracing-error = "0.2.1"
-tracing-subscriber = "0.3.19"
-typed-path = "0.11.0"
-url = "2.5.4"
+tracing-subscriber = "0.3.20"
+typed-path = "0.12.0"
+url = "2.5.7"
urlencoding = "2.1.3"
-uuid = "1.17.0"
+uuid = "1.18.1"
validator = "0.20.0"
-webp = { version = "0.3.0", default-features = false }
+webp = { version = "0.3.1", default-features = false }
webview2-com = "0.38.0" # Should be updated in lockstep with wry
-whoami = "1.6.0"
-windows = "0.61.3"
-windows-core = "0.61.2"
+whoami = "1.6.1"
+windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62
+windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62
winreg = "0.55.0"
woothee = "0.13.0"
yaserde = "0.12.0"
-zbus = "5.9.0"
-zip = { version = "4.3.0", default-features = false, features = [
+zbus = "5.11.0"
+zip = { version = "6.0.0", default-features = false, features = [
"bzip2",
"deflate",
"deflate64",
@@ -234,6 +235,7 @@ redundant_clone = "warn"
redundant_feature_names = "warn"
redundant_type_annotations = "warn"
todo = "warn"
+uninlined_format_args = "warn"
unnested_or_patterns = "warn"
wildcard_dependencies = "warn"
diff --git a/apps/daedalus_client/Dockerfile b/apps/daedalus_client/Dockerfile
index 8a7d1881..0d040dca 100644
--- a/apps/daedalus_client/Dockerfile
+++ b/apps/daedalus_client/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
-FROM rust:1.89.0 AS build
+FROM rust:1.90.0 AS build
WORKDIR /usr/src/daedalus
COPY . .
diff --git a/apps/docs/src/content/docs/contributing/labrinth.md b/apps/docs/src/content/docs/contributing/labrinth.md
index 02c50b0f..9fd1626c 100644
--- a/apps/docs/src/content/docs/contributing/labrinth.md
+++ b/apps/docs/src/content/docs/contributing/labrinth.md
@@ -5,7 +5,7 @@ description: Guide for contributing to Modrinth's backend
This project is part of our [monorepo](https://github.com/modrinth/code). You can find it in the `apps/labrinth` directory. The instructions below assume that you have switched your working directory to the `apps/labrinth` subdirectory.
-[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432, a MeiliSearch instance on port 7700, and a [Mailpit](https://mailpit.axllent.org/) SMTP server on port 1025, with a web UI to inspect sent emails on port 8025. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
+[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), cmake, and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432, a MeiliSearch instance on port 7700, and a [Mailpit](https://mailpit.axllent.org/) SMTP server on port 1025, with a web UI to inspect sent emails on port 8025. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
To get a basic configuration, copy the `.env.local` file to `.env`. Now, you'll have to install the sqlx CLI, which can be done with cargo:
diff --git a/apps/docs/src/styles/modrinth.css b/apps/docs/src/styles/modrinth.css
index b61c7fbe..4f149c26 100644
--- a/apps/docs/src/styles/modrinth.css
+++ b/apps/docs/src/styles/modrinth.css
@@ -2,9 +2,8 @@
::backdrop,
:root[data-theme='light'],
[data-theme='light'] ::backdrop {
- --sl-font-system:
- Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell,
- Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ --sl-font-system: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
+ Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
--sl-color-white: var(--color-contrast); /* “white” */
--sl-color-gray-1: var(--color-base);
diff --git a/apps/labrinth/.prettierignore b/apps/labrinth/.prettierignore
index ffe984b7..ae989a74 100644
--- a/apps/labrinth/.prettierignore
+++ b/apps/labrinth/.prettierignore
@@ -113,6 +113,10 @@ migrations/20250725230041_reports-closed-status-index.sql
migrations/20250727184120_user-newsletter-subscription-column.sql
migrations/20250804221014_users-redeemals.sql
migrations/20250805001654_product-prices-public.sql
+migrations/20250823233518_user-compliance.sql
+migrations/20250902133943_notification-extension.sql
+migrations/20250914190749_affiliate_codes.sql
+migrations/20250927120742_user_limits.sql
# Prettier reformats some of the PostgreSQL-specific COPY syntax here,
# which is very likely to break things
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index 3e57bec3..5bcc0ea7 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -91,6 +91,7 @@ rust_decimal = { workspace = true, features = [
] }
rust_iso3166 = { workspace = true }
rust-s3 = { workspace = true }
+rustls.workspace = true
rusty-money = { workspace = true }
sentry = { workspace = true }
sentry-actix = { workspace = true }
@@ -108,7 +109,7 @@ sqlx = { workspace = true, features = [
"postgres",
"runtime-tokio",
"rust_decimal",
- "tls-rustls-ring",
+ "tls-rustls-aws-lc-rs",
] }
tar = { workspace = true }
thiserror = { workspace = true }
diff --git a/apps/labrinth/Dockerfile b/apps/labrinth/Dockerfile
index d35198c2..e6ad9ac9 100644
--- a/apps/labrinth/Dockerfile
+++ b/apps/labrinth/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
-FROM rust:1.89.0 AS build
+FROM rust:1.90.0 AS build
WORKDIR /usr/src/labrinth
COPY . .
diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs
index 7284a36d..d4b8e906 100644
--- a/apps/labrinth/src/main.rs
+++ b/apps/labrinth/src/main.rs
@@ -113,6 +113,10 @@ async fn main() -> std::io::Result<()> {
std::process::exit(1);
}
+ rustls::crypto::aws_lc_rs::default_provider()
+ .install_default()
+ .unwrap();
+
// DSN is from SENTRY_DSN env variable.
// Has no effect if not set.
let sentry = sentry::init(sentry::ClientOptions {
diff --git a/apps/labrinth/src/queue/analytics.rs b/apps/labrinth/src/queue/analytics.rs
index 4269edae..d44f8a2b 100644
--- a/apps/labrinth/src/queue/analytics.rs
+++ b/apps/labrinth/src/queue/analytics.rs
@@ -66,7 +66,7 @@ impl AnalyticsQueue {
self.playtime_queue.clear();
if !playtime_queue.is_empty() {
- let mut playtimes = client.insert("playtime")?;
+ let mut playtimes = client.insert::("playtime").await?;
for playtime in playtime_queue {
playtimes.write(&playtime).await?;
@@ -132,7 +132,7 @@ impl AnalyticsQueue {
.await
.map_err(DatabaseError::CacheError)?;
- let mut views = client.insert("views")?;
+ let mut views = client.insert::("views").await?;
for (all_views, monetized) in raw_views {
for (idx, mut view) in all_views.into_iter().enumerate() {
@@ -200,7 +200,7 @@ impl AnalyticsQueue {
.map_err(DatabaseError::CacheError)?;
let mut transaction = pool.begin().await?;
- let mut downloads = client.insert("downloads")?;
+ let mut downloads = client.insert::("downloads").await?;
let mut version_downloads: HashMap = HashMap::new();
let mut project_downloads: HashMap = HashMap::new();
diff --git a/apps/labrinth/src/queue/email/templates.rs b/apps/labrinth/src/queue/email/templates.rs
index 2a13baac..25792f5e 100644
--- a/apps/labrinth/src/queue/email/templates.rs
+++ b/apps/labrinth/src/queue/email/templates.rs
@@ -704,7 +704,7 @@ async fn dynamic_email_body(
.wrap_internal_err("SITE_URL is not set")?;
let site_url = site_url.trim_end_matches('/');
- let url = format!("{}/_internal/templates/email/dynamic", site_url);
+ let url = format!("{site_url}/_internal/templates/email/dynamic");
std::str::from_utf8(
reqwest::Client::new()
diff --git a/apps/labrinth/src/routes/v3/analytics_get.rs b/apps/labrinth/src/routes/v3/analytics_get.rs
index ad21c1e5..7713b1da 100644
--- a/apps/labrinth/src/routes/v3/analytics_get.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get.rs
@@ -686,11 +686,12 @@ async fn query_clickhouse(
cx: &mut QueryClickhouseContext<'_>,
query: &str,
use_columns: &[(&str, bool)],
- row_get_bucket: impl Fn(&Row) -> u64,
- row_to_analytics: impl Fn(Row) -> AnalyticsData,
+ // I hate using the hidden type Row::Value here, but it's what next() returns, so I see no other option
+ row_get_bucket: impl Fn(&Row::Value<'_>) -> u64,
+ row_to_analytics: impl Fn(Row::Value<'_>) -> AnalyticsData,
) -> Result<(), ApiError>
where
- Row: clickhouse::Row + serde::de::DeserializeOwned + std::fmt::Debug,
+ Row: clickhouse::RowRead + serde::de::DeserializeOwned + std::fmt::Debug,
{
let mut query = cx
.clickhouse
diff --git a/apps/labrinth/src/search/indexing/mod.rs b/apps/labrinth/src/search/indexing/mod.rs
index 1ecb70ad..89f98d2f 100644
--- a/apps/labrinth/src/search/indexing/mod.rs
+++ b/apps/labrinth/src/search/indexing/mod.rs
@@ -127,6 +127,7 @@ pub async fn swap_index(
let index_name = config.get_index_name(index_name, false);
let swap_indices = SwapIndexes {
indexes: (index_name_next, index_name),
+ rename: None,
};
client
.swap_indexes([&swap_indices])
diff --git a/apps/labrinth/src/validate/datapack.rs b/apps/labrinth/src/validate/datapack.rs
index f152486d..b1639874 100644
--- a/apps/labrinth/src/validate/datapack.rs
+++ b/apps/labrinth/src/validate/datapack.rs
@@ -18,7 +18,7 @@ impl super::Validator for DataPackValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions {
// Time since release of 17w43a, 2017-10-25, which introduced datapacks
SupportedGameVersions::PastDate(
- DateTime::from_timestamp(1508889600, 0).unwrap(),
+ DateTime::from_timestamp_secs(1508889600).unwrap(),
)
}
diff --git a/apps/labrinth/src/validate/forge.rs b/apps/labrinth/src/validate/forge.rs
index da8b49a8..375b31e6 100644
--- a/apps/labrinth/src/validate/forge.rs
+++ b/apps/labrinth/src/validate/forge.rs
@@ -19,7 +19,7 @@ impl super::Validator for ForgeValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions {
// Time since release of 1.13, the first forge version which uses the new TOML system
SupportedGameVersions::PastDate(
- DateTime::from_timestamp(1540122067, 0).unwrap(),
+ DateTime::from_timestamp_secs(1540122067).unwrap(),
)
}
@@ -56,8 +56,8 @@ impl super::Validator for LegacyForgeValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions {
// Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods
SupportedGameVersions::Range(
- DateTime::from_timestamp(0, 0).unwrap(),
- DateTime::from_timestamp(1540122066, 0).unwrap(),
+ DateTime::from_timestamp_secs(0).unwrap(),
+ DateTime::from_timestamp_secs(1540122066).unwrap(),
)
}
diff --git a/apps/labrinth/src/validate/quilt.rs b/apps/labrinth/src/validate/quilt.rs
index 08312f56..29ed0d54 100644
--- a/apps/labrinth/src/validate/quilt.rs
+++ b/apps/labrinth/src/validate/quilt.rs
@@ -18,7 +18,7 @@ impl super::Validator for QuiltValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions {
SupportedGameVersions::PastDate(
- DateTime::from_timestamp(1646070100, 0).unwrap(),
+ DateTime::from_timestamp_secs(1646070100).unwrap(),
)
}
diff --git a/apps/labrinth/src/validate/resourcepack.rs b/apps/labrinth/src/validate/resourcepack.rs
index 1d9d52c3..7f204f6d 100644
--- a/apps/labrinth/src/validate/resourcepack.rs
+++ b/apps/labrinth/src/validate/resourcepack.rs
@@ -20,7 +20,7 @@ impl super::Validator for PackValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions {
// Time since release of 13w24a which replaced texture packs with resource packs
SupportedGameVersions::PastDate(
- DateTime::from_timestamp(1371137542, 0).unwrap(),
+ DateTime::from_timestamp_secs(1371137542).unwrap(),
)
}
@@ -59,8 +59,8 @@ impl super::Validator for TexturePackValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions {
// a1.2.2a to 13w23b
SupportedGameVersions::Range(
- DateTime::from_timestamp(1289339999, 0).unwrap(),
- DateTime::from_timestamp(1370651522, 0).unwrap(),
+ DateTime::from_timestamp_secs(1289339999).unwrap(),
+ DateTime::from_timestamp_secs(1370651522).unwrap(),
)
}
diff --git a/apps/labrinth/tests/common/mod.rs b/apps/labrinth/tests/common/mod.rs
index 1056ab0d..84fa4778 100644
--- a/apps/labrinth/tests/common/mod.rs
+++ b/apps/labrinth/tests/common/mod.rs
@@ -27,6 +27,8 @@ pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig {
println!("Some environment variables are missing!");
}
+ let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
+
let pool = db.pool.clone();
let ro_pool = db.ro_pool.clone();
let redis_pool = db.redis_pool.clone();
diff --git a/clippy.toml b/clippy.toml
index b77c9241..ddc612e9 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -1,2 +1,2 @@
allow-dbg-in-tests = true
-msrv = "1.89.0"
+msrv = "1.90.0"
diff --git a/packages/app-lib/.prettierignore b/packages/app-lib/.prettierignore
index 9237fdf3..42a37415 100644
--- a/packages/app-lib/.prettierignore
+++ b/packages/app-lib/.prettierignore
@@ -1,6 +1,8 @@
**/*.rs
.sqlx
+java/build
+
# Migrations existing before Prettier formatted them shall always be ignored,
# as any changes to them will break existing deployments
migrations/20240711194701_init.sql
diff --git a/packages/app-lib/java/build.gradle.kts b/packages/app-lib/java/build.gradle.kts
index f0d88f86..10435b98 100644
--- a/packages/app-lib/java/build.gradle.kts
+++ b/packages/app-lib/java/build.gradle.kts
@@ -1,18 +1,7 @@
-import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction
-import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer
-import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer
-import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
-import org.apache.tools.zip.ZipEntry
-import org.apache.tools.zip.ZipOutputStream
-import java.io.IOException
-import java.util.jar.JarFile
-import java.util.jar.Attributes as JarAttributes
-import java.util.jar.Manifest as JarManifest
-
plugins {
java
- id("com.diffplug.spotless") version "7.0.4"
- id("com.gradleup.shadow") version "9.0.0-rc2"
+ id("com.diffplug.spotless") version "8.0.0"
+ id("com.gradleup.shadow") version "9.2.2"
}
repositories {
@@ -20,9 +9,9 @@ repositories {
}
dependencies {
- implementation("org.ow2.asm:asm:9.8")
- implementation("org.ow2.asm:asm-tree:9.8")
- implementation("com.google.code.gson:gson:2.13.1")
+ implementation("org.ow2.asm:asm:9.9")
+ implementation("org.ow2.asm:asm-tree:9.9")
+ implementation("com.google.code.gson:gson:2.13.2")
testImplementation(libs.junit.jupiter)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
@@ -30,7 +19,7 @@ dependencies {
java {
toolchain {
- languageVersion = JavaLanguageVersion.of(11)
+ languageVersion = JavaLanguageVersion.of(17)
}
}
@@ -56,52 +45,9 @@ tasks.shadowJar {
attributes["Premain-Class"] = "com.modrinth.theseus.agent.TheseusAgent"
}
- enableRelocation = true
+ addMultiReleaseAttribute = false
+ enableAutoRelocation = true
relocationPrefix = "com.modrinth.theseus.shadow"
-
- // Adapted from ManifestResourceTransformer to do one thing: remove Multi-Release.
- // Multi-Release gets added by shadow because gson has Multi-Release set to true, however
- // shadow strips the actual versions directory, as gson only has a module-info.class in there.
- // However, older versions of SecureJarHandler crash if Multi-Release is set to true but the
- // versions directory is missing.
- transform(@CacheableTransformer object : ResourceTransformer {
- private var manifestDiscovered = false
- private var manifest: JarManifest? = null
-
- override fun canTransformResource(element: FileTreeElement): Boolean {
- return JarFile.MANIFEST_NAME.equals(element.path, ignoreCase = true)
- }
-
- override fun transform(context: TransformerContext) {
- if (!manifestDiscovered) {
- try {
- manifest = JarManifest(context.inputStream)
- manifestDiscovered = true
- } catch (e: IOException) {
- logger.warn("Failed to read MANIFEST.MF", e)
- }
- }
- }
-
- override fun hasTransformedResource(): Boolean = true
-
- override fun modifyOutputStream(
- os: ZipOutputStream,
- preserveFileTimestamps: Boolean
- ) {
- // If we didn't find a manifest, then let's create one.
- if (manifest == null) {
- manifest = JarManifest()
- }
-
- manifest!!.mainAttributes.remove(JarAttributes.Name.MULTI_RELEASE)
-
- os.putNextEntry(ZipEntry(JarFile.MANIFEST_NAME).apply {
- time = ShadowCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES
- })
- manifest!!.write(os)
- }
- })
}
tasks.named("test") {
diff --git a/packages/app-lib/java/gradle/libs.versions.toml b/packages/app-lib/java/gradle/libs.versions.toml
index cd649555..dacbebcd 100644
--- a/packages/app-lib/java/gradle/libs.versions.toml
+++ b/packages/app-lib/java/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-junit-jupiter = "5.12.1"
+junit-jupiter = "5.14.0"
[libraries]
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
diff --git a/packages/app-lib/java/gradle/wrapper/gradle-wrapper.properties b/packages/app-lib/java/gradle/wrapper/gradle-wrapper.properties
index ff23a68d..2e111328 100644
--- a/packages/app-lib/java/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/app-lib/java/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/packages/app-lib/java/settings.gradle.kts b/packages/app-lib/java/settings.gradle.kts
index 01d2944d..2a0aaa65 100644
--- a/packages/app-lib/java/settings.gradle.kts
+++ b/packages/app-lib/java/settings.gradle.kts
@@ -1,6 +1,6 @@
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
- id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
rootProject.name = "theseus"
diff --git a/packages/app-lib/src/api/minecraft_skins/png_util.rs b/packages/app-lib/src/api/minecraft_skins/png_util.rs
index 36c74cee..e4ae3dcb 100644
--- a/packages/app-lib/src/api/minecraft_skins/png_util.rs
+++ b/packages/app-lib/src/api/minecraft_skins/png_util.rs
@@ -1,6 +1,6 @@
//! Miscellaneous PNG utilities for Minecraft skins.
-use std::io::Read;
+use std::io::{BufRead, Cursor, Seek};
use std::sync::Arc;
use base64::Engine;
@@ -9,7 +9,8 @@ use data_url::DataUrl;
use futures::{Stream, TryStreamExt, future::Either, stream};
use itertools::Itertools;
use rgb::Rgba;
-use tokio_util::{compat::FuturesAsyncReadCompatExt, io::SyncIoBridge};
+use tokio::io::AsyncReadExt;
+use tokio_util::compat::FuturesAsyncReadCompatExt;
use url::Url;
use crate::{
@@ -95,7 +96,8 @@ pub fn dimensions(png_data: &[u8]) -> crate::Result<(u32, u32)> {
pub async fn normalize_skin_texture(
texture: &UrlOrBlob,
) -> crate::Result {
- let texture_stream = SyncIoBridge::new(Box::pin(
+ let mut texture_data = Vec::with_capacity(8192);
+ Box::pin(
match texture {
UrlOrBlob::Url(url) => Either::Left(
url_to_data_stream(url)
@@ -112,84 +114,84 @@ pub async fn normalize_skin_texture(
),
}
.compat(),
- ));
+ )
+ .read_to_end(&mut texture_data)
+ .await?;
- tokio::task::spawn_blocking(|| {
- let mut png_reader = {
- let mut decoder = png::Decoder::new(texture_stream);
- decoder.set_transformations(
- png::Transformations::normalize_to_color8(),
- );
- decoder.read_info()
- }?;
+ let mut png_reader = {
+ let mut decoder = png::Decoder::new(Cursor::new(texture_data));
+ decoder
+ .set_transformations(png::Transformations::normalize_to_color8());
+ decoder.read_info()
+ }?;
- // The code below assumes that the skin texture has valid dimensions.
- // This also serves as a way to bail out early for obviously invalid or
- // adversarial textures
- if png_reader.info().width != 64
- || ![64, 32].contains(&png_reader.info().height)
- {
- Err(ErrorKind::InvalidSkinTexture)?;
- }
+ // The code below assumes that the skin texture has valid dimensions.
+ // This also serves as a way to bail out early for obviously invalid or
+ // adversarial textures
+ if png_reader.info().width != 64
+ || ![64, 32].contains(&png_reader.info().height)
+ {
+ Err(ErrorKind::InvalidSkinTexture)?;
+ }
- let is_legacy_skin = png_reader.info().height == 32;
- let mut texture_buf =
- get_skin_texture_buffer(&mut png_reader, is_legacy_skin)?;
- if is_legacy_skin {
- convert_legacy_skin_texture(&mut texture_buf, png_reader.info());
- do_notch_transparency_hack(&mut texture_buf, png_reader.info());
- }
- make_inner_parts_opaque(&mut texture_buf, png_reader.info());
+ let is_legacy_skin = png_reader.info().height == 32;
+ let mut texture_buf =
+ get_skin_texture_buffer(&mut png_reader, is_legacy_skin)?;
+ if is_legacy_skin {
+ convert_legacy_skin_texture(&mut texture_buf, png_reader.info());
+ do_notch_transparency_hack(&mut texture_buf, png_reader.info());
+ }
+ make_inner_parts_opaque(&mut texture_buf, png_reader.info());
- let mut encoded_png = vec![];
+ let mut encoded_png = vec![];
- let mut png_encoder = png::Encoder::new(&mut encoded_png, 64, 64);
- png_encoder.set_color(png::ColorType::Rgba);
- png_encoder.set_depth(png::BitDepth::Eight);
- png_encoder.set_filter(png::FilterType::NoFilter);
- png_encoder.set_compression(png::Compression::Fast);
+ let mut png_encoder = png::Encoder::new(&mut encoded_png, 64, 64);
+ png_encoder.set_color(png::ColorType::Rgba);
+ png_encoder.set_depth(png::BitDepth::Eight);
+ png_encoder.set_filter(png::Filter::NoFilter);
+ png_encoder.set_compression(png::Compression::Fast);
- // Keeping color space information properly set, to handle the occasional
- // strange PNG with non-sRGB chromaticities and/or different grayscale spaces
- // that keeps most people wondering, is what sets a carefully crafted image
- // manipulation routine apart :)
- if let Some(source_chromaticities) =
- png_reader.info().source_chromaticities.as_ref().copied()
- {
- png_encoder.set_source_chromaticities(source_chromaticities);
- }
- if let Some(source_gamma) =
- png_reader.info().source_gamma.as_ref().copied()
- {
- png_encoder.set_source_gamma(source_gamma);
- }
- if let Some(source_srgb) = png_reader.info().srgb.as_ref().copied() {
- png_encoder.set_source_srgb(source_srgb);
- }
+ // Keeping color space information properly set, to handle the occasional
+ // strange PNG with non-sRGB chromaticities and/or different grayscale spaces
+ // that keeps most people wondering, is what sets a carefully crafted image
+ // manipulation routine apart :)
+ if let Some(source_chromaticities) =
+ png_reader.info().source_chromaticities.as_ref().copied()
+ {
+ png_encoder.set_source_chromaticities(source_chromaticities);
+ }
+ if let Some(source_gamma) = png_reader.info().source_gamma.as_ref().copied()
+ {
+ png_encoder.set_source_gamma(source_gamma);
+ }
+ if let Some(source_srgb) = png_reader.info().srgb.as_ref().copied() {
+ png_encoder.set_source_srgb(source_srgb);
+ }
- let png_buf = bytemuck::try_cast_slice(&texture_buf)
- .map_err(|_| ErrorKind::InvalidPng)?;
- let mut png_writer = png_encoder.write_header()?;
- png_writer.write_image_data(png_buf)?;
- png_writer.finish()?;
+ let png_buf = bytemuck::try_cast_slice(&texture_buf)
+ .map_err(|_| ErrorKind::InvalidPng)?;
+ let mut png_writer = png_encoder.write_header()?;
+ png_writer.write_image_data(png_buf)?;
+ png_writer.finish()?;
- Ok(encoded_png.into())
- })
- .await?
+ Ok(encoded_png.into())
}
/// Reads a skin texture and returns a 64x64 buffer in RGBA format.
-fn get_skin_texture_buffer(
+fn get_skin_texture_buffer(
png_reader: &mut png::Reader,
is_legacy_skin: bool,
) -> crate::Result>> {
+ let output_buffer_size = png_reader
+ .output_buffer_size()
+ .expect("Reasonable skin texture size verified already");
let mut png_buf = if is_legacy_skin {
// Legacy skins have half the height, so duplicate the rows to
// turn them into a 64x64 texture
- vec![0; png_reader.output_buffer_size() * 2]
+ vec![0; output_buffer_size * 2]
} else {
// Modern skins are left as-is
- vec![0; png_reader.output_buffer_size()]
+ vec![0; output_buffer_size]
};
png_reader.next_frame(&mut png_buf)?;
@@ -373,9 +375,10 @@ fn set_alpha(
#[tokio::test]
async fn normalize_skin_texture_works() {
let decode_to_pixels = |png_data: &[u8]| {
- let decoder = png::Decoder::new(png_data);
+ let decoder = png::Decoder::new(Cursor::new(png_data));
let mut reader = decoder.read_info().expect("Failed to read PNG info");
- let mut buffer = vec![0; reader.output_buffer_size()];
+ let mut buffer =
+ vec![0; reader.output_buffer_size().expect("Skin size too large")];
reader
.next_frame(&mut buffer)
.expect("Failed to decode PNG");
diff --git a/packages/app-lib/src/error.rs b/packages/app-lib/src/error.rs
index 096b948c..f1993524 100644
--- a/packages/app-lib/src/error.rs
+++ b/packages/app-lib/src/error.rs
@@ -176,6 +176,9 @@ pub enum ErrorKind {
#[error("Deserialization error: {0}")]
DeserializationError(#[from] serde::de::value::Error),
+
+ #[error("Discord IPC error: {0}")]
+ DiscordRichPresenceError(#[from] discord_rich_presence::error::Error),
}
#[derive(Debug)]
diff --git a/packages/app-lib/src/state/discord.rs b/packages/app-lib/src/state/discord.rs
index d54ad0cb..2704cc6d 100644
--- a/packages/app-lib/src/state/discord.rs
+++ b/packages/app-lib/src/state/discord.rs
@@ -18,12 +18,7 @@ impl DiscordGuard {
/// Initialize discord IPC client, and attempt to connect to it
/// If it fails, it will still return a DiscordGuard, but the client will be unconnected
pub fn init() -> crate::Result {
- let dipc =
- DiscordIpcClient::new("1123683254248148992").map_err(|e| {
- crate::ErrorKind::OtherError(format!(
- "Could not create Discord client {e}",
- ))
- })?;
+ let dipc = DiscordIpcClient::new("1123683254248148992");
Ok(DiscordGuard {
client: Arc::new(RwLock::new(dipc)),
@@ -87,25 +82,14 @@ impl DiscordGuard {
let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> =
self.client.write().await;
let res = client.set_activity(activity.clone());
- let could_not_set_err = |e: Box| {
- crate::ErrorKind::OtherError(format!(
- "Could not update Discord activity {e}",
- ))
- };
if reconnect_if_fail {
if let Err(_e) = res {
- client.reconnect().map_err(|e| {
- crate::ErrorKind::OtherError(format!(
- "Could not reconnect to Discord IPC {e}",
- ))
- })?;
- return Ok(client
- .set_activity(activity)
- .map_err(could_not_set_err)?); // try again, but don't reconnect if it fails again
+ client.reconnect()?;
+ return Ok(client.set_activity(activity)?); // try again, but don't reconnect if it fails again
}
} else {
- res.map_err(could_not_set_err)?;
+ res?;
}
Ok(())
@@ -126,25 +110,13 @@ impl DiscordGuard {
let mut client = self.client.write().await;
let res = client.clear_activity();
- let could_not_clear_err = |e: Box| {
- crate::ErrorKind::OtherError(format!(
- "Could not clear Discord activity {e}",
- ))
- };
-
if reconnect_if_fail {
if res.is_err() {
- client.reconnect().map_err(|e| {
- crate::ErrorKind::OtherError(format!(
- "Could not reconnect to Discord IPC {e}",
- ))
- })?;
- return Ok(client
- .clear_activity()
- .map_err(could_not_clear_err)?); // try again, but don't reconnect if it fails again
+ client.reconnect()?;
+ return Ok(client.clear_activity()?); // try again, but don't reconnect if it fails again
}
} else {
- res.map_err(could_not_clear_err)?;
+ res?;
}
Ok(())
}
diff --git a/packages/app-lib/src/state/friends.rs b/packages/app-lib/src/state/friends.rs
index b697cd04..ad28ffeb 100644
--- a/packages/app-lib/src/state/friends.rs
+++ b/packages/app-lib/src/state/friends.rs
@@ -272,7 +272,7 @@ impl FriendsSocket {
pub async fn disconnect(&self) -> crate::Result<()> {
let mut write_lock = self.write.write().await;
if let Some(ref mut write_half) = *write_lock {
- write_half.close().await?;
+ SinkExt::close(write_half).await?;
*write_lock = None;
}
Ok(())
diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs
index 4cff0a33..ee527fb2 100644
--- a/packages/app-lib/src/state/process.rs
+++ b/packages/app-lib/src/state/process.rs
@@ -516,7 +516,7 @@ impl Process {
chrono::DateTime::::from_timestamp(secs, nsecs)
.unwrap_or_default()
} else {
- chrono::DateTime::::from_timestamp(timestamp_val, 0)
+ chrono::DateTime::::from_timestamp_secs(timestamp_val)
.unwrap_or_default()
};
diff --git a/packages/blog/compiled/index.ts b/packages/blog/compiled/index.ts
index 33ce60b1..bc4bb95d 100644
--- a/packages/blog/compiled/index.ts
+++ b/packages/blog/compiled/index.ts
@@ -1,36 +1,36 @@
// AUTO-GENERATED FILE - DO NOT EDIT
-import { article as a_new_chapter_for_modrinth_servers } from "./a_new_chapter_for_modrinth_servers";
-import { article as accelerating_development } from "./accelerating_development";
-import { article as becoming_sustainable } from "./becoming_sustainable";
-import { article as capital_return } from "./capital_return";
-import { article as carbon_ads } from "./carbon_ads";
-import { article as creator_monetization } from "./creator_monetization";
-import { article as creator_update } from "./creator_update";
-import { article as creator_updates_july_2025 } from "./creator_updates_july_2025";
-import { article as design_refresh } from "./design_refresh";
-import { article as download_adjustment } from "./download_adjustment";
-import { article as free_server_medal } from "./free_server_medal";
-import { article as knossos_v2_1_0 } from "./knossos_v2_1_0";
-import { article as licensing_guide } from "./licensing_guide";
-import { article as modpack_changes } from "./modpack_changes";
-import { article as modpacks_alpha } from "./modpacks_alpha";
-import { article as modrinth_app_beta } from "./modrinth_app_beta";
-import { article as modrinth_beta } from "./modrinth_beta";
-import { article as modrinth_servers_asia } from "./modrinth_servers_asia";
-import { article as modrinth_servers_beta } from "./modrinth_servers_beta";
-import { article as new_environments } from "./new_environments";
-import { article as new_site_beta } from "./new_site_beta";
-import { article as plugins_resource_packs } from "./plugins_resource_packs";
-import { article as pride_campaign_2025 } from "./pride_campaign_2025";
-import { article as redesign } from "./redesign";
-import { article as russian_censorship } from "./russian_censorship";
-import { article as skins_now_in_modrinth_app } from "./skins_now_in_modrinth_app";
-import { article as standing_by_our_values } from "./standing_by_our_values";
-import { article as standing_by_our_values_russian } from "./standing_by_our_values_russian";
+import { article as windows_borderless_malware_disclosure } from "./windows_borderless_malware_disclosure";
+import { article as whats_modrinth } from "./whats_modrinth";
import { article as two_years_of_modrinth } from "./two_years_of_modrinth";
import { article as two_years_of_modrinth_history } from "./two_years_of_modrinth_history";
-import { article as whats_modrinth } from "./whats_modrinth";
-import { article as windows_borderless_malware_disclosure } from "./windows_borderless_malware_disclosure";
+import { article as standing_by_our_values } from "./standing_by_our_values";
+import { article as standing_by_our_values_russian } from "./standing_by_our_values_russian";
+import { article as skins_now_in_modrinth_app } from "./skins_now_in_modrinth_app";
+import { article as russian_censorship } from "./russian_censorship";
+import { article as redesign } from "./redesign";
+import { article as pride_campaign_2025 } from "./pride_campaign_2025";
+import { article as plugins_resource_packs } from "./plugins_resource_packs";
+import { article as new_site_beta } from "./new_site_beta";
+import { article as new_environments } from "./new_environments";
+import { article as modrinth_servers_beta } from "./modrinth_servers_beta";
+import { article as modrinth_servers_asia } from "./modrinth_servers_asia";
+import { article as modrinth_beta } from "./modrinth_beta";
+import { article as modrinth_app_beta } from "./modrinth_app_beta";
+import { article as modpacks_alpha } from "./modpacks_alpha";
+import { article as modpack_changes } from "./modpack_changes";
+import { article as licensing_guide } from "./licensing_guide";
+import { article as knossos_v2_1_0 } from "./knossos_v2_1_0";
+import { article as free_server_medal } from "./free_server_medal";
+import { article as download_adjustment } from "./download_adjustment";
+import { article as design_refresh } from "./design_refresh";
+import { article as creator_updates_july_2025 } from "./creator_updates_july_2025";
+import { article as creator_update } from "./creator_update";
+import { article as creator_monetization } from "./creator_monetization";
+import { article as carbon_ads } from "./carbon_ads";
+import { article as capital_return } from "./capital_return";
+import { article as becoming_sustainable } from "./becoming_sustainable";
+import { article as accelerating_development } from "./accelerating_development";
+import { article as a_new_chapter_for_modrinth_servers } from "./a_new_chapter_for_modrinth_servers";
export const articles = [
windows_borderless_malware_disclosure,
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index 65c0d610..2ce412d5 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
[toolchain]
-channel = "1.89.0"
+channel = "1.90.0"
profile = "default"
From 5db5bf4c4c6e9a6619e63e191cf3433606d2fe3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?=
<108630700+fetchfern@users.noreply.github.com>
Date: Fri, 17 Oct 2025 16:57:36 +0100
Subject: [PATCH 02/53] Changes to handling of refunds in Anrok (#4556)
* Use negations, track transaction version/accounting time, use original charge accounting time in refunds
* query cache
* chore: query cache, clippy, fmt
* Fix tax drift calculation
* Fix migration
* Increase update_tax_transactions rate
---
...558cd0ea1d9dba5948e8fa2496ed99de8fea.json} | 16 +-
...ec76952f1505a75eb1a006b3ad9b8aa91a51.json} | 16 +-
...c76e645cde49f95b0e5bfecd3d3d2330ed5c.json} | 16 +-
...c34b86c33703f984c869346b0c0be1c4a4883.json | 34 +++++
...d1a27929e05b497aaea93e9e8f318770c64c.json} | 16 +-
...242239eb5a4c6bef3748dac57fa339260c9c1.json | 143 ++++++++++++++++++
...987e082a000ec1b397957650e1332191613ca.json | 130 ----------------
...f178a7828a45ed3134d3336cb59572f40beab.json | 32 ----
...34310be545293b2693f1c747425295b367a8.json} | 16 +-
...4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json | 130 ----------------
...2d87b8805e87ac0cefb43828fc6d3aca52399.json | 142 +++++++++++++++++
...f7c45548b4cf3d8cb6545aad801f9fcc5a56.json} | 16 +-
...cbf6f13bddc80a9719cbd401395db718b2f2.json} | 17 ++-
...85756_tax-platform-transaction-version.sql | 2 +
.../src/database/models/charge_item.rs | 20 ++-
apps/labrinth/src/queue/billing.rs | 17 ++-
apps/labrinth/src/routes/internal/billing.rs | 31 +++-
apps/labrinth/src/util/anrok.rs | 54 +++++++
18 files changed, 530 insertions(+), 318 deletions(-)
rename apps/labrinth/.sqlx/{query-e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54.json => query-372c03a6daf0045f615faa9a6205558cd0ea1d9dba5948e8fa2496ed99de8fea.json} (73%)
rename apps/labrinth/.sqlx/{query-51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda.json => query-4a0e5c7ebd4565b95fb99983484cec76952f1505a75eb1a006b3ad9b8aa91a51.json} (83%)
rename apps/labrinth/.sqlx/{query-9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d.json => query-4ed57832b7c02e1f4c683e256455c76e645cde49f95b0e5bfecd3d3d2330ed5c.json} (78%)
create mode 100644 apps/labrinth/.sqlx/query-5a972c49ccacf8735ec36d691f1c34b86c33703f984c869346b0c0be1c4a4883.json
rename apps/labrinth/.sqlx/{query-4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04.json => query-64233913683d187ee6c449eb106bd1a27929e05b497aaea93e9e8f318770c64c.json} (83%)
create mode 100644 apps/labrinth/.sqlx/query-70236e8be98967070160f703ed0242239eb5a4c6bef3748dac57fa339260c9c1.json
delete mode 100644 apps/labrinth/.sqlx/query-9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca.json
delete mode 100644 apps/labrinth/.sqlx/query-c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab.json
rename apps/labrinth/.sqlx/{query-e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f.json => query-caf16ed13389398c1ee3456c0e2534310be545293b2693f1c747425295b367a8.json} (81%)
delete mode 100644 apps/labrinth/.sqlx/query-cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json
create mode 100644 apps/labrinth/.sqlx/query-ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399.json
rename apps/labrinth/.sqlx/{query-7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6.json => query-ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56.json} (81%)
rename apps/labrinth/.sqlx/{query-050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2.json => query-f0618e69765ba605b1db7f25a233cbf6f13bddc80a9719cbd401395db718b2f2.json} (83%)
create mode 100644 apps/labrinth/migrations/20251015085756_tax-platform-transaction-version.sql
diff --git a/apps/labrinth/.sqlx/query-e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54.json b/apps/labrinth/.sqlx/query-372c03a6daf0045f615faa9a6205558cd0ea1d9dba5948e8fa2496ed99de8fea.json
similarity index 73%
rename from apps/labrinth/.sqlx/query-e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54.json
rename to apps/labrinth/.sqlx/query-372c03a6daf0045f615faa9a6205558cd0ea1d9dba5948e8fa2496ed99de8fea.json
index 84342011..8cbb2c28 100644
--- a/apps/labrinth/.sqlx/query-e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54.json
+++ b/apps/labrinth/.sqlx/query-372c03a6daf0045f615faa9a6205558cd0ea1d9dba5948e8fa2496ed99de8fea.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n status = 'failed' AND due < NOW() - INTERVAL '30 days'\n ",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n INNER JOIN users_subscriptions us ON us.id = charges.subscription_id\n WHERE\n charges.charge_type = $1 AND\n (\n (charges.status = 'cancelled' AND charges.due < NOW()) OR\n (charges.status = 'expiring' AND charges.due < NOW()) OR\n (charges.status = 'failed' AND charges.last_attempt < NOW() - INTERVAL '2 days')\n )\n AND us.status = 'provisioned'\n ",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
+ true,
+ true,
true
]
},
- "hash": "e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54"
+ "hash": "372c03a6daf0045f615faa9a6205558cd0ea1d9dba5948e8fa2496ed99de8fea"
}
diff --git a/apps/labrinth/.sqlx/query-51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda.json b/apps/labrinth/.sqlx/query-4a0e5c7ebd4565b95fb99983484cec76952f1505a75eb1a006b3ad9b8aa91a51.json
similarity index 83%
rename from apps/labrinth/.sqlx/query-51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda.json
rename to apps/labrinth/.sqlx/query-4a0e5c7ebd4565b95fb99983484cec76952f1505a75eb1a006b3ad9b8aa91a51.json
index b3efa342..00c9c93d 100644
--- a/apps/labrinth/.sqlx/query-51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda.json
+++ b/apps/labrinth/.sqlx/query-4a0e5c7ebd4565b95fb99983484cec76952f1505a75eb1a006b3ad9b8aa91a51.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE parent_charge_id = $1",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
+ true,
+ true,
true
]
},
- "hash": "51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda"
+ "hash": "4a0e5c7ebd4565b95fb99983484cec76952f1505a75eb1a006b3ad9b8aa91a51"
}
diff --git a/apps/labrinth/.sqlx/query-9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d.json b/apps/labrinth/.sqlx/query-4ed57832b7c02e1f4c683e256455c76e645cde49f95b0e5bfecd3d3d2330ed5c.json
similarity index 78%
rename from apps/labrinth/.sqlx/query-9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d.json
rename to apps/labrinth/.sqlx/query-4ed57832b7c02e1f4c683e256455c76e645cde49f95b0e5bfecd3d3d2330ed5c.json
index f5dc4759..41437210 100644
--- a/apps/labrinth/.sqlx/query-9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d.json
+++ b/apps/labrinth/.sqlx/query-4ed57832b7c02e1f4c683e256455c76e645cde49f95b0e5bfecd3d3d2330ed5c.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n INNER JOIN users_subscriptions us ON us.id = charges.subscription_id\n WHERE\n charges.charge_type = $1 AND\n (\n (charges.status = 'cancelled' AND charges.due < NOW()) OR\n (charges.status = 'expiring' AND charges.due < NOW()) OR\n (charges.status = 'failed' AND charges.last_attempt < NOW() - INTERVAL '2 days')\n )\n AND us.status = 'provisioned'\n ",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'open' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
+ true,
+ true,
true
]
},
- "hash": "9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d"
+ "hash": "4ed57832b7c02e1f4c683e256455c76e645cde49f95b0e5bfecd3d3d2330ed5c"
}
diff --git a/apps/labrinth/.sqlx/query-5a972c49ccacf8735ec36d691f1c34b86c33703f984c869346b0c0be1c4a4883.json b/apps/labrinth/.sqlx/query-5a972c49ccacf8735ec36d691f1c34b86c33703f984c869346b0c0be1c4a4883.json
new file mode 100644
index 00000000..1616cd7c
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-5a972c49ccacf8735ec36d691f1c34b86c33703f984c869346b0c0be1c4a4883.json
@@ -0,0 +1,34 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss, tax_transaction_version, tax_platform_accounting_time)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval,\n payment_platform = EXCLUDED.payment_platform,\n payment_platform_id = EXCLUDED.payment_platform_id,\n parent_charge_id = EXCLUDED.parent_charge_id,\n net = EXCLUDED.net,\n tax_amount = EXCLUDED.tax_amount,\n tax_platform_id = EXCLUDED.tax_platform_id,\n tax_last_updated = EXCLUDED.tax_last_updated,\n price_id = EXCLUDED.price_id,\n amount = EXCLUDED.amount,\n currency_code = EXCLUDED.currency_code,\n charge_type = EXCLUDED.charge_type,\n\t\t\t\t\ttax_drift_loss = EXCLUDED.tax_drift_loss,\n\t\t\t\t\ttax_transaction_version = EXCLUDED.tax_transaction_version,\n\t\t\t\t\ttax_platform_accounting_time = EXCLUDED.tax_platform_accounting_time\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8",
+ "Int8",
+ "Int8",
+ "Text",
+ "Text",
+ "Varchar",
+ "Timestamptz",
+ "Timestamptz",
+ "Int8",
+ "Text",
+ "Text",
+ "Text",
+ "Int8",
+ "Int8",
+ "Int8",
+ "Text",
+ "Timestamptz",
+ "Int8",
+ "Int4",
+ "Timestamptz"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "5a972c49ccacf8735ec36d691f1c34b86c33703f984c869346b0c0be1c4a4883"
+}
diff --git a/apps/labrinth/.sqlx/query-4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04.json b/apps/labrinth/.sqlx/query-64233913683d187ee6c449eb106bd1a27929e05b497aaea93e9e8f318770c64c.json
similarity index 83%
rename from apps/labrinth/.sqlx/query-4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04.json
rename to apps/labrinth/.sqlx/query-64233913683d187ee6c449eb106bd1a27929e05b497aaea93e9e8f318770c64c.json
index 77bc16da..68e8fac7 100644
--- a/apps/labrinth/.sqlx/query-4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04.json
+++ b/apps/labrinth/.sqlx/query-64233913683d187ee6c449eb106bd1a27929e05b497aaea93e9e8f318770c64c.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE id = $1",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE parent_charge_id = $1",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
+ true,
+ true,
true
]
},
- "hash": "4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04"
+ "hash": "64233913683d187ee6c449eb106bd1a27929e05b497aaea93e9e8f318770c64c"
}
diff --git a/apps/labrinth/.sqlx/query-70236e8be98967070160f703ed0242239eb5a4c6bef3748dac57fa339260c9c1.json b/apps/labrinth/.sqlx/query-70236e8be98967070160f703ed0242239eb5a4c6bef3748dac57fa339260c9c1.json
new file mode 100644
index 00000000..23c73e91
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-70236e8be98967070160f703ed0242239eb5a4c6bef3748dac57fa339260c9c1.json
@@ -0,0 +1,143 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n AND payment_platform_id IS NOT NULL\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n OFFSET $1\n\t\t\tLIMIT $2\n\t\t\t",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "user_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "price_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 3,
+ "name": "amount",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 4,
+ "name": "currency_code",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 5,
+ "name": "status",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "due",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 7,
+ "name": "last_attempt",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 8,
+ "name": "charge_type",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 9,
+ "name": "subscription_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 10,
+ "name": "tax_amount",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 11,
+ "name": "tax_platform_id",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 12,
+ "name": "subscription_interval?",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 13,
+ "name": "payment_platform",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 14,
+ "name": "payment_platform_id?",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 15,
+ "name": "parent_charge_id?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 16,
+ "name": "net?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 17,
+ "name": "tax_last_updated?",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 18,
+ "name": "tax_drift_loss?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ true,
+ false,
+ true,
+ true,
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "70236e8be98967070160f703ed0242239eb5a4c6bef3748dac57fa339260c9c1"
+}
diff --git a/apps/labrinth/.sqlx/query-9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca.json b/apps/labrinth/.sqlx/query-9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca.json
deleted file mode 100644
index 4659b4c7..00000000
--- a/apps/labrinth/.sqlx/query-9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca.json
+++ /dev/null
@@ -1,130 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "id",
- "type_info": "Int8"
- },
- {
- "ordinal": 1,
- "name": "user_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 2,
- "name": "price_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 3,
- "name": "amount",
- "type_info": "Int8"
- },
- {
- "ordinal": 4,
- "name": "currency_code",
- "type_info": "Text"
- },
- {
- "ordinal": 5,
- "name": "status",
- "type_info": "Varchar"
- },
- {
- "ordinal": 6,
- "name": "due",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 7,
- "name": "last_attempt",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 8,
- "name": "charge_type",
- "type_info": "Text"
- },
- {
- "ordinal": 9,
- "name": "subscription_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 10,
- "name": "tax_amount",
- "type_info": "Int8"
- },
- {
- "ordinal": 11,
- "name": "tax_platform_id",
- "type_info": "Text"
- },
- {
- "ordinal": 12,
- "name": "subscription_interval?",
- "type_info": "Text"
- },
- {
- "ordinal": 13,
- "name": "payment_platform",
- "type_info": "Text"
- },
- {
- "ordinal": 14,
- "name": "payment_platform_id?",
- "type_info": "Text"
- },
- {
- "ordinal": 15,
- "name": "parent_charge_id?",
- "type_info": "Int8"
- },
- {
- "ordinal": 16,
- "name": "net?",
- "type_info": "Int8"
- },
- {
- "ordinal": 17,
- "name": "tax_last_updated?",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 18,
- "name": "tax_drift_loss?",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": [
- "Int8"
- ]
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false,
- false,
- false,
- true,
- false,
- true,
- false,
- true,
- true,
- false,
- true,
- true,
- true,
- true,
- true
- ]
- },
- "hash": "9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca"
-}
diff --git a/apps/labrinth/.sqlx/query-c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab.json b/apps/labrinth/.sqlx/query-c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab.json
deleted file mode 100644
index 75a3edf5..00000000
--- a/apps/labrinth/.sqlx/query-c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval,\n payment_platform = EXCLUDED.payment_platform,\n payment_platform_id = EXCLUDED.payment_platform_id,\n parent_charge_id = EXCLUDED.parent_charge_id,\n net = EXCLUDED.net,\n tax_amount = EXCLUDED.tax_amount,\n tax_platform_id = EXCLUDED.tax_platform_id,\n tax_last_updated = EXCLUDED.tax_last_updated,\n price_id = EXCLUDED.price_id,\n amount = EXCLUDED.amount,\n currency_code = EXCLUDED.currency_code,\n charge_type = EXCLUDED.charge_type,\n\t\t\t\t\ttax_drift_loss = EXCLUDED.tax_drift_loss\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Int8",
- "Int8",
- "Int8",
- "Text",
- "Text",
- "Varchar",
- "Timestamptz",
- "Timestamptz",
- "Int8",
- "Text",
- "Text",
- "Text",
- "Int8",
- "Int8",
- "Int8",
- "Text",
- "Timestamptz",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab"
-}
diff --git a/apps/labrinth/.sqlx/query-e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f.json b/apps/labrinth/.sqlx/query-caf16ed13389398c1ee3456c0e2534310be545293b2693f1c747425295b367a8.json
similarity index 81%
rename from apps/labrinth/.sqlx/query-e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f.json
rename to apps/labrinth/.sqlx/query-caf16ed13389398c1ee3456c0e2534310be545293b2693f1c747425295b367a8.json
index d17a3276..f44b89a8 100644
--- a/apps/labrinth/.sqlx/query-e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f.json
+++ b/apps/labrinth/.sqlx/query-caf16ed13389398c1ee3456c0e2534310be545293b2693f1c747425295b367a8.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'open' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n status = 'failed' AND due < NOW() - INTERVAL '30 days'\n ",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
+ true,
+ true,
true
]
},
- "hash": "e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f"
+ "hash": "caf16ed13389398c1ee3456c0e2534310be545293b2693f1c747425295b367a8"
}
diff --git a/apps/labrinth/.sqlx/query-cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json b/apps/labrinth/.sqlx/query-cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json
deleted file mode 100644
index 458f19f9..00000000
--- a/apps/labrinth/.sqlx/query-cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json
+++ /dev/null
@@ -1,130 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '14 days' < NOW() -- Due between 7 and 14 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "id",
- "type_info": "Int8"
- },
- {
- "ordinal": 1,
- "name": "user_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 2,
- "name": "price_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 3,
- "name": "amount",
- "type_info": "Int8"
- },
- {
- "ordinal": 4,
- "name": "currency_code",
- "type_info": "Text"
- },
- {
- "ordinal": 5,
- "name": "status",
- "type_info": "Varchar"
- },
- {
- "ordinal": 6,
- "name": "due",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 7,
- "name": "last_attempt",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 8,
- "name": "charge_type",
- "type_info": "Text"
- },
- {
- "ordinal": 9,
- "name": "subscription_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 10,
- "name": "tax_amount",
- "type_info": "Int8"
- },
- {
- "ordinal": 11,
- "name": "tax_platform_id",
- "type_info": "Text"
- },
- {
- "ordinal": 12,
- "name": "subscription_interval?",
- "type_info": "Text"
- },
- {
- "ordinal": 13,
- "name": "payment_platform",
- "type_info": "Text"
- },
- {
- "ordinal": 14,
- "name": "payment_platform_id?",
- "type_info": "Text"
- },
- {
- "ordinal": 15,
- "name": "parent_charge_id?",
- "type_info": "Int8"
- },
- {
- "ordinal": 16,
- "name": "net?",
- "type_info": "Int8"
- },
- {
- "ordinal": 17,
- "name": "tax_last_updated?",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 18,
- "name": "tax_drift_loss?",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": [
- "Int8"
- ]
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false,
- false,
- false,
- true,
- false,
- true,
- false,
- true,
- true,
- false,
- true,
- true,
- true,
- true,
- true
- ]
- },
- "hash": "cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8"
-}
diff --git a/apps/labrinth/.sqlx/query-ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399.json b/apps/labrinth/.sqlx/query-ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399.json
new file mode 100644
index 00000000..0e9a5c30
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399.json
@@ -0,0 +1,142 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '14 days' < NOW() -- Due between 7 and 14 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "user_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "price_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 3,
+ "name": "amount",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 4,
+ "name": "currency_code",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 5,
+ "name": "status",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "due",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 7,
+ "name": "last_attempt",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 8,
+ "name": "charge_type",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 9,
+ "name": "subscription_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 10,
+ "name": "tax_amount",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 11,
+ "name": "tax_platform_id",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 12,
+ "name": "subscription_interval?",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 13,
+ "name": "payment_platform",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 14,
+ "name": "payment_platform_id?",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 15,
+ "name": "parent_charge_id?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 16,
+ "name": "net?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 17,
+ "name": "tax_last_updated?",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 18,
+ "name": "tax_drift_loss?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ true,
+ false,
+ true,
+ true,
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399"
+}
diff --git a/apps/labrinth/.sqlx/query-7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6.json b/apps/labrinth/.sqlx/query-ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56.json
similarity index 81%
rename from apps/labrinth/.sqlx/query-7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6.json
rename to apps/labrinth/.sqlx/query-ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56.json
index d8265d33..7ef53f0c 100644
--- a/apps/labrinth/.sqlx/query-7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6.json
+++ b/apps/labrinth/.sqlx/query-ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
+ true,
+ true,
true
]
},
- "hash": "7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6"
+ "hash": "ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56"
}
diff --git a/apps/labrinth/.sqlx/query-050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2.json b/apps/labrinth/.sqlx/query-f0618e69765ba605b1db7f25a233cbf6f13bddc80a9719cbd401395db718b2f2.json
similarity index 83%
rename from apps/labrinth/.sqlx/query-050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2.json
rename to apps/labrinth/.sqlx/query-f0618e69765ba605b1db7f25a233cbf6f13bddc80a9719cbd401395db718b2f2.json
index 5779cc0f..823d2cdb 100644
--- a/apps/labrinth/.sqlx/query-050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2.json
+++ b/apps/labrinth/.sqlx/query-f0618e69765ba605b1db7f25a233cbf6f13bddc80a9719cbd401395db718b2f2.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n AND payment_platform_id IS NOT NULL\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n OFFSET $1\n\t\t\tLIMIT $2\n\t\t\t",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE id = $1",
"describe": {
"columns": [
{
@@ -97,11 +97,20 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
+ },
+ {
+ "ordinal": 19,
+ "name": "tax_transaction_version?",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 20,
+ "name": "tax_platform_accounting_time?",
+ "type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
- "Int8",
"Int8"
]
},
@@ -124,8 +133,10 @@
true,
true,
true,
+ true,
+ true,
true
]
},
- "hash": "050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2"
+ "hash": "f0618e69765ba605b1db7f25a233cbf6f13bddc80a9719cbd401395db718b2f2"
}
diff --git a/apps/labrinth/migrations/20251015085756_tax-platform-transaction-version.sql b/apps/labrinth/migrations/20251015085756_tax-platform-transaction-version.sql
new file mode 100644
index 00000000..1efca15c
--- /dev/null
+++ b/apps/labrinth/migrations/20251015085756_tax-platform-transaction-version.sql
@@ -0,0 +1,2 @@
+ALTER TABLE charges ADD COLUMN tax_transaction_version INTEGER;
+ALTER TABLE charges ADD COLUMN tax_platform_accounting_time TIMESTAMP WITH TIME ZONE;
diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs
index 1e7bdc40..a1cded23 100644
--- a/apps/labrinth/src/database/models/charge_item.rs
+++ b/apps/labrinth/src/database/models/charge_item.rs
@@ -30,6 +30,8 @@ pub struct DBCharge {
pub tax_amount: i64,
pub tax_platform_id: Option,
pub tax_last_updated: Option>,
+ pub tax_transaction_version: Option,
+ pub tax_platform_accounting_time: Option>,
// Net is always in USD
pub net: Option,
@@ -56,6 +58,8 @@ struct ChargeQueryResult {
tax_last_updated: Option>,
net: Option,
tax_drift_loss: Option,
+ tax_transaction_version: Option,
+ tax_platform_accounting_time: Option>,
}
impl TryFrom for DBCharge {
@@ -84,6 +88,8 @@ impl TryFrom for DBCharge {
net: r.net,
tax_last_updated: r.tax_last_updated,
tax_drift_loss: r.tax_drift_loss,
+ tax_transaction_version: r.tax_transaction_version,
+ tax_platform_accounting_time: r.tax_platform_accounting_time,
})
}
}
@@ -103,7 +109,9 @@ macro_rules! select_charges_with_predicate {
charges.parent_charge_id AS "parent_charge_id?",
charges.net AS "net?",
charges.tax_last_updated AS "tax_last_updated?",
- charges.tax_drift_loss AS "tax_drift_loss?"
+ charges.tax_drift_loss AS "tax_drift_loss?",
+ charges.tax_transaction_version AS "tax_transaction_version?",
+ charges.tax_platform_accounting_time AS "tax_platform_accounting_time?"
FROM charges
"#
+ $predicate,
@@ -119,8 +127,8 @@ impl DBCharge {
) -> Result {
sqlx::query!(
r#"
- INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
+ INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss, tax_transaction_version, tax_platform_accounting_time)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
ON CONFLICT (id)
DO UPDATE
SET status = EXCLUDED.status,
@@ -139,7 +147,9 @@ impl DBCharge {
amount = EXCLUDED.amount,
currency_code = EXCLUDED.currency_code,
charge_type = EXCLUDED.charge_type,
- tax_drift_loss = EXCLUDED.tax_drift_loss
+ tax_drift_loss = EXCLUDED.tax_drift_loss,
+ tax_transaction_version = EXCLUDED.tax_transaction_version,
+ tax_platform_accounting_time = EXCLUDED.tax_platform_accounting_time
"#,
self.id.0,
self.user_id.0,
@@ -160,6 +170,8 @@ impl DBCharge {
self.tax_platform_id.as_deref(),
self.tax_last_updated,
self.tax_drift_loss,
+ self.tax_transaction_version,
+ self.tax_platform_accounting_time,
)
.execute(&mut **transaction)
.await?;
diff --git a/apps/labrinth/src/queue/billing.rs b/apps/labrinth/src/queue/billing.rs
index e259662c..c160ca0a 100644
--- a/apps/labrinth/src/queue/billing.rs
+++ b/apps/labrinth/src/queue/billing.rs
@@ -432,12 +432,17 @@ async fn update_anrok_transactions(
match result {
Ok(response) => {
- let should_have_collected = response.tax_amount_to_collect;
+ let version = response.version.ok_or_else(|| {
+ ApiError::InvalidInput(
+ "Anrok response is missing tax transaction version"
+ .to_owned(),
+ )
+ })?;
- let drift = should_have_collected - c.tax_amount;
-
- c.tax_drift_loss = Some(drift);
+ c.tax_drift_loss = Some(response.tax_amount_to_collect);
c.tax_platform_id = Some(tax_platform_id);
+ c.tax_transaction_version = Some(version);
+ c.tax_platform_accounting_time = Some(c.due);
c.upsert(txn).await?;
Ok(())
@@ -647,6 +652,8 @@ pub async fn try_process_user_redeemal(
net: None,
tax_last_updated: Some(Utc::now()),
tax_drift_loss: Some(0),
+ tax_transaction_version: None,
+ tax_platform_accounting_time: None,
}
.upsert(&mut txn)
.await?;
@@ -1016,7 +1023,7 @@ pub async fn index_subscriptions(
&redis,
&anrok_client,
&stripe_client,
- 750,
+ 1000,
),
)
.await;
diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs
index a2e230ca..606c96b7 100644
--- a/apps/labrinth/src/routes/internal/billing.rs
+++ b/apps/labrinth/src/routes/internal/billing.rs
@@ -267,6 +267,24 @@ pub async fn refund_charge(
.tax_identifier
.tax_processor_id;
+ let Some((
+ (
+ original_tax_platform_id,
+ original_tax_transaction_version,
+ ),
+ original_tax_platform_accounting_time,
+ )) = charge
+ .tax_platform_id
+ .clone()
+ .zip(charge.tax_transaction_version)
+ .zip(charge.tax_platform_accounting_time)
+ else {
+ return Err(ApiError::InvalidInput(
+ "Charge is missing full tax information. Please wait for the original charge to be synchronized with the tax processor."
+ .to_owned(),
+ ));
+ };
+
let refund = stripe::Refund::create(
&stripe_client,
CreateRefund {
@@ -281,13 +299,16 @@ pub async fn refund_charge(
)
.await?;
- let anrok_txn_result = anrok_client.create_or_update_txn(
+ let anrok_txn_result = anrok_client.negate_or_create_partial_negation(
+ original_tax_platform_id,
+ original_tax_transaction_version,
+ charge.amount + charge.tax_amount,
&anrok::Transaction {
id: anrok::transaction_id_stripe_pyr(&refund.id),
fields: anrok::TransactionFields {
customer_address: anrok::Address::from_stripe_address(&billing_address),
currency_code: charge.currency_code.clone(),
- accounting_time: Utc::now(),
+ accounting_time: original_tax_platform_accounting_time,
accounting_time_zone: anrok::AccountingTimeZone::Utc,
line_items: vec![anrok::LineItem::new_including_tax_amount(tax_id, -refund_amount)],
customer_id: Some(format!("stripe:cust:{}", user.stripe_customer_id.unwrap_or_else(|| "unknown".to_owned()))),
@@ -347,6 +368,8 @@ pub async fn refund_charge(
currency_code: charge.currency_code,
tax_last_updated: Some(Utc::now()),
tax_drift_loss: Some(0),
+ tax_transaction_version: None,
+ tax_platform_accounting_time: None,
}
.upsert(&mut transaction)
.await?;
@@ -1641,6 +1664,8 @@ pub async fn stripe_webhook(
net: None,
tax_last_updated: Some(Utc::now()),
tax_drift_loss: Some(0),
+ tax_transaction_version: None,
+ tax_platform_accounting_time: None,
};
if charge_status != ChargeStatus::Failed {
@@ -2004,6 +2029,8 @@ pub async fn stripe_webhook(
tax_platform_id: None,
tax_last_updated: Some(Utc::now()),
tax_drift_loss: Some(0),
+ tax_transaction_version: None,
+ tax_platform_accounting_time: None,
}
.upsert(&mut transaction)
.await?;
diff --git a/apps/labrinth/src/util/anrok.rs b/apps/labrinth/src/util/anrok.rs
index e7d896dd..b9ed7998 100644
--- a/apps/labrinth/src/util/anrok.rs
+++ b/apps/labrinth/src/util/anrok.rs
@@ -182,6 +182,60 @@ impl Client {
.await
}
+ pub async fn negate_or_create_partial_negation(
+ &self,
+ original_txn_anrok_id: String,
+ original_txn_version: i32,
+ original_txn_tax_amount_with_tax: i64,
+ body: &Transaction,
+ ) -> Result<(), AnrokError> {
+ let refund_amount = body
+ .fields
+ .line_items
+ .iter()
+ .map(|l| l.amount_in_smallest_denominations)
+ .sum::();
+
+ if -refund_amount == original_txn_tax_amount_with_tax {
+ self.create_full_negation(
+ original_txn_anrok_id,
+ original_txn_version,
+ body.id.clone(),
+ )
+ .await?;
+ } else {
+ self.create_or_update_txn(body).await?;
+ }
+
+ Ok(())
+ }
+
+ pub async fn create_full_negation(
+ &self,
+ original_txn_anrok_id: String,
+ original_txn_version: i32,
+ new_txn_id: String,
+ ) -> Result {
+ #[derive(Serialize)]
+ #[serde(rename_all = "camelCase")]
+ struct NegationBody {
+ original_transaction_id: String,
+ new_transaction_id: String,
+ original_transaction_expected_version: i32,
+ }
+
+ self.make_request(
+ Method::POST,
+ "/v1/seller/transactions/createNegation",
+ Some(&NegationBody {
+ original_transaction_id: original_txn_anrok_id,
+ new_transaction_id: new_txn_id,
+ original_transaction_expected_version: original_txn_version,
+ }),
+ )
+ .await
+ }
+
pub async fn create_or_update_txn(
&self,
body: &Transaction,
From e719ae2f423fbed4c92b86c8977b6971633c1bf0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?=
<7822554+AlexTMjugador@users.noreply.github.com>
Date: Fri, 17 Oct 2025 18:43:04 +0200
Subject: [PATCH 03/53] fix(daedalus-client): backport new Mojang MC version
library patches from PrismLauncher (#4493)
While researching and fixing other issue, it caught my attention that we
are embedding a library patches JSON file from the PrismLauncher meta
repository. However, since we copied that file, a new revision of it was
published with patches that improve compatibility with Apple Silicon
macOS platforms.
These changes update such a file and, perhaps most importantly, add a
comment explaining the provenance and licensing of such a file.
---
apps/daedalus_client/library-patches.json | 115 +++++++++++++++++++++-
apps/daedalus_client/src/minecraft.rs | 2 +
2 files changed, 113 insertions(+), 4 deletions(-)
diff --git a/apps/daedalus_client/library-patches.json b/apps/daedalus_client/library-patches.json
index fa9bfe93..df750e02 100644
--- a/apps/daedalus_client/library-patches.json
+++ b/apps/daedalus_client/library-patches.json
@@ -2762,10 +2762,7 @@
},
{
"_comment": "Replace glfw from 3.3.1 with version from 3.3.2 to prevent stack smashing",
- "match": [
- "org.lwjgl:lwjgl-glfw-natives-linux:3.3.1",
- "org.lwjgl:lwjgl-glfw:3.3.1:natives-linux"
- ],
+ "match": ["org.lwjgl:lwjgl-glfw-natives-linux:3.3.1"],
"override": {
"downloads": {
"artifact": {
@@ -2776,5 +2773,115 @@
},
"name": "org.lwjgl:lwjgl-glfw-natives-linux:3.3.2-lwjgl.1"
}
+ },
+ {
+ "_comment": "Use newer JNA on macOS to prevent crashes due to faulty assertion",
+ "match": [
+ "net.java.dev.jna:jna:5.6.0",
+ "net.java.dev.jna:jna:5.8.0",
+ "net.java.dev.jna:jna:5.9.0",
+ "net.java.dev.jna:jna:5.10.0",
+ "net.java.dev.jna:jna:5.12.1"
+ ],
+ "override": {
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx-arm64"
+ }
+ }
+ ]
+ },
+ "additionalLibraries": [
+ {
+ "downloads": {
+ "artifact": {
+ "sha1": "1200e7ebeedbe0d10062093f32925a912020e747",
+ "size": 1879325,
+ "url": "https://libraries.minecraft.net/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar"
+ }
+ },
+ "name": "net.java.dev.jna:jna:5.13.0",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ },
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx-arm64"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "_comment": "Use newer JNA on macOS to prevent crashes due to faulty assertion",
+ "match": [
+ "net.java.dev.jna:jna-platform:5.6.0",
+ "net.java.dev.jna:jna-platform:5.8.0",
+ "net.java.dev.jna:jna-platform:5.9.0",
+ "net.java.dev.jna:jna-platform:5.10.0",
+ "net.java.dev.jna:jna-platform:5.12.1"
+ ],
+ "override": {
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx-arm64"
+ }
+ }
+ ]
+ },
+ "additionalLibraries": [
+ {
+ "downloads": {
+ "artifact": {
+ "sha1": "88e9a306715e9379f3122415ef4ae759a352640d",
+ "size": 1363209,
+ "url": "https://libraries.minecraft.net/net/java/dev/jna/jna-platform/5.13.0/jna-platform-5.13.0.jar"
+ }
+ },
+ "name": "net.java.dev.jna:jna-platform:5.13.0",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ },
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx-arm64"
+ }
+ }
+ ]
+ }
+ ]
}
]
diff --git a/apps/daedalus_client/src/minecraft.rs b/apps/daedalus_client/src/minecraft.rs
index f90cf6dd..bdeed603 100644
--- a/apps/daedalus_client/src/minecraft.rs
+++ b/apps/daedalus_client/src/minecraft.rs
@@ -186,6 +186,8 @@ pub struct LibraryPatch {
}
fn fetch_library_patches() -> Result, Error> {
+ // The file below is a copy of https://github.com/PrismLauncher/meta/blob/main/meta/common/mojang-library-patches.json.
+ // That file belongs to a repository licensed under the Microsoft Public License (Ms-PL)
let patches = include_bytes!("../library-patches.json");
Ok(serde_json::from_slice(patches)?)
}
From d1ffed564d205725b59d92e837cd1655923bb8df Mon Sep 17 00:00:00 2001
From: "Calum H."
Date: Fri, 17 Oct 2025 18:56:25 +0100
Subject: [PATCH 04/53] fix: #4567 (#4571)
---
apps/frontend/src/pages/auth/verify-email.vue | 41 +++++++++++--------
1 file changed, 24 insertions(+), 17 deletions(-)
diff --git a/apps/frontend/src/pages/auth/verify-email.vue b/apps/frontend/src/pages/auth/verify-email.vue
index 95a80b00..f7b3bcda 100644
--- a/apps/frontend/src/pages/auth/verify-email.vue
+++ b/apps/frontend/src/pages/auth/verify-email.vue
@@ -18,12 +18,17 @@
@@ -40,24 +45,26 @@
-
- {{ formatMessage(failedVerificationMessages.action) }}
-
+
+
+ {{ formatMessage(failedVerificationMessages.action) }}
+
+
+
-
- {{ formatMessage(messages.signIn) }}
-
+
+
+ {{ formatMessage(messages.signIn) }}
+
+
+
-
- Add friends to share what you're playing!
-
-
-
-
-
{{ friend.username }}
-
-
-
-
- Remove
-
-
-
-
-
-
- You have no pending friend requests :C
-
-
+
You have no pending friend requests :C
+
+
-
+
- {{ friend.username }} sent you a friend request
+ {{ friend.username }} sent you a friend request
You sent {{ friend.username }} a friend request
@@ -246,77 +250,81 @@ onUnmounted(() => {
-
-
-
-
You can add friends with their Modrinth username.
-
+
+
+
+ {{ formatMessage(messages.usernameTitle) }}
+
+
+ {{ formatMessage(messages.usernameDescription) }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.sendFriendRequest) }}
+
+
+
-
-
+
+
+
+
- Add friend
-
-
-
Friends
-
-
+
+
-
-
-
- Add friend
-
-
-
- Manage friends
-
- {{ acceptedFriends.length }}
-
-
-
-
- View friend requests
-
- {{ pendingFriends.length }}
-
-
-
+
+
+
+
+
+
+
+ {{ incomingRequests.length }}
+
+
-
+
+ {{ formatMessage(messages.friends) }}
@@ -325,10 +333,13 @@ onUnmounted(() => {
-
-
+
+
- Sign in to add friends!
+ Sign in to a Modrinth account
+ to add friends and see what they're playing!
Add friends
@@ -337,38 +348,54 @@ onUnmounted(() => {
-
- Remove friend
-
-
- friendOptions.showMenu(event, friend, [
- {
- name: 'remove-friend',
- color: 'danger',
- },
- ])
- "
- >
-
-
-
- {{ friend.username }}
-
- {{ friend.status }}
-
-
+
+
+
+
+
+ {{ formatMessage(messages.noFriendsMatch, { query: search }) }}
+
+
diff --git a/apps/app-frontend/src/components/ui/friends/FriendsSection.vue b/apps/app-frontend/src/components/ui/friends/FriendsSection.vue
new file mode 100644
index 00000000..8b5822bb
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/friends/FriendsSection.vue
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+ {{ formatMessage(messages.viewProfile) }}
+
+ {{ formatMessage(messages.removeFriend) }}
+ {{ formatMessage(messages.cancelRequest) }}
+
+
+
+
+ {{ formatMessage(messages.heading, { title: heading, count: friends.length }) }}
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/helpers/friends.js b/apps/app-frontend/src/helpers/friends.js
deleted file mode 100644
index 16d64f11..00000000
--- a/apps/app-frontend/src/helpers/friends.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { invoke } from '@tauri-apps/api/core'
-
-export async function friends() {
- return await invoke('plugin:friends|friends')
-}
-
-export async function friend_statuses() {
- return await invoke('plugin:friends|friend_statuses')
-}
-
-export async function add_friend(userId) {
- return await invoke('plugin:friends|add_friend', { userId })
-}
-
-export async function remove_friend(userId) {
- return await invoke('plugin:friends|remove_friend', { userId })
-}
diff --git a/apps/app-frontend/src/helpers/friends.ts b/apps/app-frontend/src/helpers/friends.ts
new file mode 100644
index 00000000..61ad8688
--- /dev/null
+++ b/apps/app-frontend/src/helpers/friends.ts
@@ -0,0 +1,79 @@
+import type { User } from '@modrinth/utils'
+import { invoke } from '@tauri-apps/api/core'
+import type { Dayjs } from 'dayjs'
+import dayjs from 'dayjs'
+
+import { get_user_many } from '@/helpers/cache'
+import type { ModrinthCredentials } from '@/helpers/mr_auth'
+
+export type UserStatus = {
+ user_id: string
+ profile_name: string | null
+ last_update: string
+}
+
+export type UserFriend = {
+ id: string
+ friend_id: string
+ accepted: boolean
+ created: string
+}
+
+export async function friends(): Promise
{
+ return await invoke('plugin:friends|friends')
+}
+
+export async function friend_statuses(): Promise {
+ return await invoke('plugin:friends|friend_statuses')
+}
+
+export async function add_friend(userId: string): Promise {
+ return await invoke('plugin:friends|add_friend', { userId })
+}
+
+export async function remove_friend(userId: string): Promise {
+ return await invoke('plugin:friends|remove_friend', { userId })
+}
+
+export type FriendWithUserData = {
+ id: string
+ friend_id: string | null
+ status: string | null
+ last_updated: Dayjs | null
+ created: Dayjs
+ username: string
+ accepted: boolean
+ online: boolean
+ avatar: string
+}
+export async function transformFriends(
+ friends: UserFriend[],
+ credentials: ModrinthCredentials | null,
+): Promise {
+ if (friends.length === 0) {
+ return []
+ }
+
+ const friendStatuses = await friend_statuses()
+ const users = await get_user_many(
+ friends.map((x) => (x.id === credentials?.user_id ? x.friend_id : x.id)),
+ )
+
+ return friends.map((friend) => {
+ const user = users.find((x: User) => x.id === friend.id || x.id === friend.friend_id)
+ const status = friendStatuses.find(
+ (x) => x.user_id === friend.id || x.user_id === friend.friend_id,
+ )
+ return {
+ id: friend.id,
+ friend_id: friend.friend_id,
+ status: status?.profile_name ?? null,
+ last_updated: status && status.last_update ? dayjs(status.last_update) : null,
+ created: dayjs(friend.created),
+ avatar: user?.avatar_url ?? '',
+ username: user?.username ?? '',
+ online: !!status,
+ accepted: friend.accepted,
+ }
+ })
+}
diff --git a/apps/app-frontend/src/helpers/mr_auth.js b/apps/app-frontend/src/helpers/mr_auth.ts
similarity index 60%
rename from apps/app-frontend/src/helpers/mr_auth.js
rename to apps/app-frontend/src/helpers/mr_auth.ts
index 2690957e..dfb476d2 100644
--- a/apps/app-frontend/src/helpers/mr_auth.js
+++ b/apps/app-frontend/src/helpers/mr_auth.ts
@@ -5,18 +5,25 @@
*/
import { invoke } from '@tauri-apps/api/core'
-export async function login() {
+export type ModrinthCredentials = {
+ session: string
+ expires: string
+ user_id: string
+ active: boolean
+}
+
+export async function login(): Promise {
return await invoke('plugin:mr-auth|modrinth_login')
}
-export async function logout() {
+export async function logout(): Promise {
return await invoke('plugin:mr-auth|logout')
}
-export async function get() {
+export async function get(): Promise {
return await invoke('plugin:mr-auth|get')
}
-export async function cancelLogin() {
+export async function cancelLogin(): Promise {
return await invoke('plugin:mr-auth|cancel_modrinth_login')
}
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index 9ec019ff..ef869d32 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -65,6 +65,63 @@
"app.update.reload-to-update": {
"message": "Reload to install update"
},
+ "friends.action.add-friend": {
+ "message": "Add a friend"
+ },
+ "friends.action.view-friend-requests": {
+ "message": "{count} friend requests"
+ },
+ "friends.add-friend.submit": {
+ "message": "Send friend request"
+ },
+ "friends.add-friend.title": {
+ "message": "Adding a friend"
+ },
+ "friends.add-friend.username.description": {
+ "message": "It may be different from their Minecraft username!"
+ },
+ "friends.add-friend.username.placeholder": {
+ "message": "Enter Modrinth username..."
+ },
+ "friends.add-friend.username.title": {
+ "message": "What's your friend's Modrinth username?"
+ },
+ "friends.friend.cancel-request": {
+ "message": "Cancel request"
+ },
+ "friends.friend.remove-friend": {
+ "message": "Remove friend"
+ },
+ "friends.friend.request-sent": {
+ "message": "Friend request sent"
+ },
+ "friends.friend.view-profile": {
+ "message": "View profile"
+ },
+ "friends.heading": {
+ "message": "Friends"
+ },
+ "friends.heading.active": {
+ "message": "Active"
+ },
+ "friends.heading.offline": {
+ "message": "Offline"
+ },
+ "friends.heading.online": {
+ "message": "Online"
+ },
+ "friends.heading.pending": {
+ "message": "Pending"
+ },
+ "friends.no-friends-match": {
+ "message": "No friends matching ''{query}''"
+ },
+ "friends.search-friends-placeholder": {
+ "message": "Search friends..."
+ },
+ "friends.section.heading": {
+ "message": "{title} - {count}"
+ },
"instance.add-server.add-and-play": {
"message": "Add and play"
},
diff --git a/apps/frontend/src/components/ui/AdPlaceholder.vue b/apps/frontend/src/components/ui/AdPlaceholder.vue
index 937aeefd..eeaf861d 100644
--- a/apps/frontend/src/components/ui/AdPlaceholder.vue
+++ b/apps/frontend/src/components/ui/AdPlaceholder.vue
@@ -1,20 +1,23 @@
-
-
+
-
+
@@ -23,6 +26,8 @@
+
+
diff --git a/apps/frontend/src/templates/emails/account/SubscriptionCredited.vue b/apps/frontend/src/templates/emails/account/SubscriptionCredited.vue
new file mode 100644
index 00000000..aeb5be9d
--- /dev/null
+++ b/apps/frontend/src/templates/emails/account/SubscriptionCredited.vue
@@ -0,0 +1,25 @@
+
+
+
+
+ We’ve added time to your server
+
+ Hi {user.name},
+ {credit.header_message}
+
+
+ To make up for it, we've added {credit.days_formatted} to your {credit.subscription.type}
+ subscription.
+
+
+ Your next charge was scheduled for {credit.previous_due} and will now be on {credit.next_due}.
+
+
+ Thank you for supporting us,
+ The Modrinth Team
+
+
diff --git a/apps/frontend/src/templates/emails/index.ts b/apps/frontend/src/templates/emails/index.ts
index 8ec330e9..719611b6 100644
--- a/apps/frontend/src/templates/emails/index.ts
+++ b/apps/frontend/src/templates/emails/index.ts
@@ -18,6 +18,7 @@ export default {
// Subscriptions
'subscription-tax-change': () => import('./account/SubscriptionTaxChange.vue'),
+ 'subscription-credited': () => import('./account/SubscriptionCredited.vue'),
// Moderation
'report-submitted': () => import('./moderation/ReportSubmitted.vue'),
diff --git a/apps/labrinth/.sqlx/query-3d05de766a0987028d465a3305938164e4d79734c17c07111f8923f2faa517bf.json b/apps/labrinth/.sqlx/query-3d05de766a0987028d465a3305938164e4d79734c17c07111f8923f2faa517bf.json
new file mode 100644
index 00000000..f1960f40
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-3d05de766a0987028d465a3305938164e4d79734c17c07111f8923f2faa517bf.json
@@ -0,0 +1,19 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n INSERT INTO users_subscriptions_credits\n (subscription_id, user_id, creditor_id, days, previous_due, next_due)\n SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::bigint[], $4::int[], $5::timestamptz[], $6::timestamptz[])\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8Array",
+ "Int8Array",
+ "Int8Array",
+ "Int4Array",
+ "TimestamptzArray",
+ "TimestamptzArray"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "3d05de766a0987028d465a3305938164e4d79734c17c07111f8923f2faa517bf"
+}
diff --git a/apps/labrinth/.sqlx/query-68968755bd5eacce9009d1d873d08ef679e6638e57e4711c2c215f32e691d856.json b/apps/labrinth/.sqlx/query-68968755bd5eacce9009d1d873d08ef679e6638e57e4711c2c215f32e691d856.json
new file mode 100644
index 00000000..a7d64639
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-68968755bd5eacce9009d1d873d08ef679e6638e57e4711c2c215f32e691d856.json
@@ -0,0 +1,27 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n INSERT INTO users_subscriptions_credits\n (subscription_id, user_id, creditor_id, days, previous_due, next_due)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int4"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8",
+ "Int8",
+ "Int4",
+ "Timestamptz",
+ "Timestamptz"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "68968755bd5eacce9009d1d873d08ef679e6638e57e4711c2c215f32e691d856"
+}
diff --git a/apps/labrinth/.sqlx/query-ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56.json b/apps/labrinth/.sqlx/query-91866517bf34fb8bf31a7a49832b18fca60c293ad349eaec07b573d22a28301c.json
similarity index 92%
rename from apps/labrinth/.sqlx/query-ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56.json
rename to apps/labrinth/.sqlx/query-91866517bf34fb8bf31a7a49832b18fca60c293ad349eaec07b573d22a28301c.json
index 7ef53f0c..cc851a50 100644
--- a/apps/labrinth/.sqlx/query-ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56.json
+++ b/apps/labrinth/.sqlx/query-91866517bf34fb8bf31a7a49832b18fca60c293ad349eaec07b573d22a28301c.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE\n\t\t\t subscription_id = $1\n\t\t\t AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')\n\t\t\tORDER BY due ASC LIMIT 1",
"describe": {
"columns": [
{
@@ -138,5 +138,5 @@
true
]
},
- "hash": "ead967d7a8e268a583eb44900a5ef7c45548b4cf3d8cb6545aad801f9fcc5a56"
+ "hash": "91866517bf34fb8bf31a7a49832b18fca60c293ad349eaec07b573d22a28301c"
}
diff --git a/apps/labrinth/.sqlx/query-aafa63e08f9d556dcd55c4687f0323aa502ce9feb3f439a614bb7759dcb03b23.json b/apps/labrinth/.sqlx/query-aafa63e08f9d556dcd55c4687f0323aa502ce9feb3f439a614bb7759dcb03b23.json
new file mode 100644
index 00000000..fca33354
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-aafa63e08f9d556dcd55c4687f0323aa502ce9feb3f439a614bb7759dcb03b23.json
@@ -0,0 +1,58 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata\n FROM users_subscriptions us\n WHERE us.metadata->>'type' = 'pyro' AND us.metadata->>'id' = ANY($1::text[])\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "user_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "price_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 3,
+ "name": "interval",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 4,
+ "name": "created",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 5,
+ "name": "status",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "metadata",
+ "type_info": "Jsonb"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "TextArray"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true
+ ]
+ },
+ "hash": "aafa63e08f9d556dcd55c4687f0323aa502ce9feb3f439a614bb7759dcb03b23"
+}
diff --git a/apps/labrinth/migrations/20251017120000_subscription-credited-notification.sql b/apps/labrinth/migrations/20251017120000_subscription-credited-notification.sql
new file mode 100644
index 00000000..42396e1a
--- /dev/null
+++ b/apps/labrinth/migrations/20251017120000_subscription-credited-notification.sql
@@ -0,0 +1,44 @@
+CREATE TABLE users_subscriptions_credits (
+ id SERIAL PRIMARY KEY,
+ subscription_id BIGINT NOT NULL REFERENCES users_subscriptions (id),
+ user_id BIGINT NOT NULL REFERENCES users (id),
+ creditor_id BIGINT NOT NULL REFERENCES users (id),
+ days INTEGER NOT NULL,
+ previous_due TIMESTAMPTZ NOT NULL,
+ next_due TIMESTAMPTZ NOT NULL,
+ created TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+INSERT INTO notifications_types
+ (name, delivery_priority, expose_in_user_preferences, expose_in_site_notifications)
+VALUES ('subscription_credited', 1, FALSE, FALSE);
+
+INSERT INTO users_notifications_preferences (user_id, channel, notification_type, enabled)
+VALUES (NULL, 'email', 'subscription_credited', TRUE);
+
+INSERT INTO notifications_templates
+ (channel, notification_type, subject_line, body_fetch_url, plaintext_fallback)
+VALUES
+ (
+ 'email',
+ 'subscription_credited',
+ 'We’ve added time to your server',
+ 'https://modrinth.com/_internal/templates/email/subscription-credited',
+ CONCAT(
+ 'Hi {user.name},',
+ CHR(10),
+ CHR(10),
+ '{credit.header_message}',
+ CHR(10),
+ CHR(10),
+ 'To make up for it, we''ve added {credit.days_formatted} to your {credit.subscription.type} subscription.',
+ CHR(10),
+ CHR(10),
+ 'Your next charge was scheduled for {credit.previous_due} and will now be on {credit.next_due}.',
+ CHR(10),
+ CHR(10),
+ 'Thank you for supporting us,',
+ CHR(10),
+ 'The Modrinth Team'
+ )
+ );
diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs
index a1cded23..652f5040 100644
--- a/apps/labrinth/src/database/models/charge_item.rs
+++ b/apps/labrinth/src/database/models/charge_item.rs
@@ -233,7 +233,10 @@ impl DBCharge {
) -> Result
, DatabaseError> {
let user_subscription_id = user_subscription_id.0;
let res = select_charges_with_predicate!(
- "WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
+ "WHERE
+ subscription_id = $1
+ AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')
+ ORDER BY due ASC LIMIT 1",
user_subscription_id
)
.fetch_optional(exec)
diff --git a/apps/labrinth/src/database/models/mod.rs b/apps/labrinth/src/database/models/mod.rs
index fa43cff7..0e5f31cd 100644
--- a/apps/labrinth/src/database/models/mod.rs
+++ b/apps/labrinth/src/database/models/mod.rs
@@ -35,6 +35,7 @@ pub mod user_subscription_item;
pub mod users_compliance;
pub mod users_notifications_preferences_item;
pub mod users_redeemals;
+pub mod users_subscriptions_credits;
pub mod version_item;
pub use affiliate_code_item::DBAffiliateCode;
diff --git a/apps/labrinth/src/database/models/user_subscription_item.rs b/apps/labrinth/src/database/models/user_subscription_item.rs
index 87189077..63eb6665 100644
--- a/apps/labrinth/src/database/models/user_subscription_item.rs
+++ b/apps/labrinth/src/database/models/user_subscription_item.rs
@@ -160,6 +160,32 @@ impl DBUserSubscription {
Ok(())
}
+
+ pub async fn get_many_by_server_ids(
+ server_ids: &[String],
+ exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
+ ) -> Result, DatabaseError> {
+ if server_ids.is_empty() {
+ return Ok(vec![]);
+ }
+
+ let results = sqlx::query_as!(
+ UserSubscriptionQueryResult,
+ r#"
+ SELECT us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata
+ FROM users_subscriptions us
+ WHERE us.metadata->>'type' = 'pyro' AND us.metadata->>'id' = ANY($1::text[])
+ "#,
+ server_ids
+ )
+ .fetch_all(exec)
+ .await?;
+
+ Ok(results
+ .into_iter()
+ .map(|r| r.try_into())
+ .collect::, serde_json::Error>>()?)
+ }
}
pub struct SubscriptionWithCharge {
diff --git a/apps/labrinth/src/database/models/users_subscriptions_credits.rs b/apps/labrinth/src/database/models/users_subscriptions_credits.rs
new file mode 100644
index 00000000..1c96410c
--- /dev/null
+++ b/apps/labrinth/src/database/models/users_subscriptions_credits.rs
@@ -0,0 +1,82 @@
+use crate::database::models::{DBUserId, DBUserSubscriptionId};
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use sqlx::query_scalar;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DBUserSubscriptionCredit {
+ pub id: i32,
+ pub subscription_id: DBUserSubscriptionId,
+ pub user_id: DBUserId,
+ pub creditor_id: DBUserId,
+ pub days: i32,
+ pub previous_due: DateTime,
+ pub next_due: DateTime,
+ pub created: DateTime,
+}
+
+impl DBUserSubscriptionCredit {
+ /// Inserts this audit entry and sets its id.
+ pub async fn insert<'a, E>(&mut self, exec: E) -> sqlx::Result<()>
+ where
+ E: sqlx::PgExecutor<'a>,
+ {
+ let id = query_scalar!(
+ r#"
+ INSERT INTO users_subscriptions_credits
+ (subscription_id, user_id, creditor_id, days, previous_due, next_due)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ RETURNING id
+ "#,
+ self.subscription_id.0,
+ self.user_id.0,
+ self.creditor_id.0,
+ self.days,
+ self.previous_due,
+ self.next_due,
+ )
+ .fetch_one(exec)
+ .await?;
+
+ self.id = id;
+ Ok(())
+ }
+
+ pub async fn insert_many(
+ exec: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ subscription_ids: &[DBUserSubscriptionId],
+ user_ids: &[DBUserId],
+ creditor_ids: &[DBUserId],
+ days: &[i32],
+ previous_dues: &[DateTime],
+ next_dues: &[DateTime],
+ ) -> sqlx::Result<()> {
+ debug_assert_eq!(subscription_ids.len(), user_ids.len());
+ debug_assert_eq!(user_ids.len(), creditor_ids.len());
+ debug_assert_eq!(creditor_ids.len(), days.len());
+ debug_assert_eq!(days.len(), previous_dues.len());
+ debug_assert_eq!(previous_dues.len(), next_dues.len());
+
+ let subs: Vec = subscription_ids.iter().map(|x| x.0).collect();
+ let users: Vec = user_ids.iter().map(|x| x.0).collect();
+ let creditors: Vec = creditor_ids.iter().map(|x| x.0).collect();
+
+ sqlx::query!(
+ r#"
+ INSERT INTO users_subscriptions_credits
+ (subscription_id, user_id, creditor_id, days, previous_due, next_due)
+ SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::bigint[], $4::int[], $5::timestamptz[], $6::timestamptz[])
+ "#,
+ &subs[..],
+ &users[..],
+ &creditors[..],
+ &days[..],
+ &previous_dues[..],
+ &next_dues[..],
+ )
+ .execute(&mut **exec)
+ .await?;
+
+ Ok(())
+ }
+}
diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs
index 02857f71..d86a19f2 100644
--- a/apps/labrinth/src/lib.rs
+++ b/apps/labrinth/src/lib.rs
@@ -21,6 +21,7 @@ use crate::database::ReadOnlyPgPool;
use crate::queue::billing::{index_billing, index_subscriptions};
use crate::queue::moderation::AutomatedModerationQueue;
use crate::util::anrok;
+use crate::util::archon::ArchonClient;
use crate::util::env::{parse_strings_from_var, parse_var};
use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters};
use sync::friends::handle_pubsub;
@@ -64,6 +65,7 @@ pub struct LabrinthConfig {
pub stripe_client: stripe::Client,
pub anrok_client: anrok::Client,
pub email_queue: web::Data,
+ pub archon_client: web::Data,
pub gotenberg_client: GotenbergClient,
}
@@ -283,6 +285,10 @@ pub fn app_setup(
stripe_client,
anrok_client,
gotenberg_client,
+ archon_client: web::Data::new(
+ ArchonClient::from_env()
+ .expect("ARCHON_URL and PYRO_API_KEY must be set"),
+ ),
email_queue: web::Data::new(email_queue),
}
}
@@ -315,9 +321,9 @@ pub fn app_config(
.app_data(web::Data::new(labrinth_config.ip_salt.clone()))
.app_data(web::Data::new(labrinth_config.analytics_queue.clone()))
.app_data(web::Data::new(labrinth_config.clickhouse.clone()))
- .app_data(labrinth_config.maxmind.clone())
.app_data(labrinth_config.active_sockets.clone())
.app_data(labrinth_config.automated_moderation_queue.clone())
+ .app_data(labrinth_config.archon_client.clone())
.app_data(web::Data::new(labrinth_config.stripe_client.clone()))
.app_data(web::Data::new(labrinth_config.anrok_client.clone()))
.app_data(labrinth_config.rate_limiter.clone())
@@ -478,7 +484,6 @@ pub fn check_env_vars() -> bool {
failed |= check_var::("CLICKHOUSE_PASSWORD");
failed |= check_var::("CLICKHOUSE_DATABASE");
- failed |= check_var::("MAXMIND_ACCOUNT_ID");
failed |= check_var::("MAXMIND_LICENSE_KEY");
failed |= check_var::("FLAME_ANVIL_URL");
diff --git a/apps/labrinth/src/models/v2/notifications.rs b/apps/labrinth/src/models/v2/notifications.rs
index 8619af06..66252e5d 100644
--- a/apps/labrinth/src/models/v2/notifications.rs
+++ b/apps/labrinth/src/models/v2/notifications.rs
@@ -109,6 +109,13 @@ pub enum LegacyNotificationBody {
amount: String,
service: String,
},
+ SubscriptionCredited {
+ subscription_id: UserSubscriptionId,
+ days: i32,
+ previous_due: DateTime,
+ next_due: DateTime,
+ header_message: Option,
+ },
PatCreated {
token_name: String,
},
@@ -219,6 +226,9 @@ impl LegacyNotification {
NotificationBody::TaxNotification { .. } => {
Some("tax_notification".to_string())
}
+ NotificationBody::SubscriptionCredited { .. } => {
+ Some("subscription_credited".to_string())
+ }
NotificationBody::PayoutAvailable { .. } => {
Some("payout_available".to_string())
}
@@ -396,6 +406,19 @@ impl LegacyNotification {
NotificationBody::PaymentFailed { amount, service } => {
LegacyNotificationBody::PaymentFailed { amount, service }
}
+ NotificationBody::SubscriptionCredited {
+ subscription_id,
+ days,
+ previous_due,
+ next_due,
+ header_message,
+ } => LegacyNotificationBody::SubscriptionCredited {
+ subscription_id,
+ days,
+ previous_due,
+ next_due,
+ header_message,
+ },
NotificationBody::Unknown => LegacyNotificationBody::Unknown,
};
diff --git a/apps/labrinth/src/models/v3/notifications.rs b/apps/labrinth/src/models/v3/notifications.rs
index edd0ecd3..dc9ea448 100644
--- a/apps/labrinth/src/models/v3/notifications.rs
+++ b/apps/labrinth/src/models/v3/notifications.rs
@@ -46,6 +46,7 @@ pub enum NotificationType {
PasswordChanged,
PasswordRemoved,
EmailChanged,
+ SubscriptionCredited,
PaymentFailed,
TaxNotification,
PatCreated,
@@ -78,6 +79,7 @@ impl NotificationType {
NotificationType::PasswordChanged => "password_changed",
NotificationType::PasswordRemoved => "password_removed",
NotificationType::EmailChanged => "email_changed",
+ NotificationType::SubscriptionCredited => "subscription_credited",
NotificationType::PaymentFailed => "payment_failed",
NotificationType::TaxNotification => "tax_notification",
NotificationType::PatCreated => "pat_created",
@@ -114,6 +116,7 @@ impl NotificationType {
"password_changed" => NotificationType::PasswordChanged,
"password_removed" => NotificationType::PasswordRemoved,
"email_changed" => NotificationType::EmailChanged,
+ "subscription_credited" => NotificationType::SubscriptionCredited,
"payment_failed" => NotificationType::PaymentFailed,
"tax_notification" => NotificationType::TaxNotification,
"payout_available" => NotificationType::PayoutAvailable,
@@ -220,6 +223,13 @@ pub enum NotificationBody {
new_email: String,
to_email: String,
},
+ SubscriptionCredited {
+ subscription_id: UserSubscriptionId,
+ days: i32,
+ previous_due: DateTime,
+ next_due: DateTime,
+ header_message: Option,
+ },
PaymentFailed {
amount: String,
service: String,
@@ -312,6 +322,9 @@ impl NotificationBody {
NotificationBody::EmailChanged { .. } => {
NotificationType::EmailChanged
}
+ NotificationBody::SubscriptionCredited { .. } => {
+ NotificationType::SubscriptionCredited
+ }
NotificationBody::PaymentFailed { .. } => {
NotificationType::PaymentFailed
}
@@ -554,6 +567,12 @@ impl From for Notification {
"#".to_string(),
vec![],
),
+ NotificationBody::SubscriptionCredited { .. } => (
+ "Subscription credited".to_string(),
+ "Your subscription has been credited with additional service time.".to_string(),
+ "#".to_string(),
+ vec![],
+ ),
NotificationBody::PayoutAvailable { .. } => (
"Payout available".to_string(),
"A payout is available!".to_string(),
diff --git a/apps/labrinth/src/queue/email/templates.rs b/apps/labrinth/src/queue/email/templates.rs
index 25792f5e..c2234fef 100644
--- a/apps/labrinth/src/queue/email/templates.rs
+++ b/apps/labrinth/src/queue/email/templates.rs
@@ -42,6 +42,12 @@ const TAXNOTIFICATION_BILLING_INTERVAL: &str =
const TAXNOTIFICATION_DUE: &str = "taxnotification.due";
const TAXNOTIFICATION_SERVICE: &str = "taxnotification.service";
+const CREDIT_DAYS: &str = "credit.days_formatted";
+const CREDIT_PREVIOUS_DUE: &str = "credit.previous_due";
+const CREDIT_NEXT_DUE: &str = "credit.next_due";
+const CREDIT_HEADER_MESSAGE: &str = "credit.header_message";
+const CREDIT_SUBSCRIPTION_TYPE: &str = "credit.subscription.type";
+
const PAYMENTFAILED_AMOUNT: &str = "paymentfailed.amount";
const PAYMENTFAILED_SERVICE: &str = "paymentfailed.service";
@@ -676,6 +682,47 @@ async fn collect_template_variables(
Ok(EmailTemplate::Static(map))
}
+ NotificationBody::SubscriptionCredited {
+ subscription_id,
+ days,
+ previous_due,
+ next_due,
+ header_message,
+ } => {
+ map.insert(
+ CREDIT_DAYS,
+ format!("{days} day{}", if *days == 1 { "" } else { "s" }),
+ );
+ map.insert(CREDIT_PREVIOUS_DUE, date_human_readable(*previous_due));
+ map.insert(CREDIT_NEXT_DUE, date_human_readable(*next_due));
+ map.insert(SUBSCRIPTION_ID, to_base62(subscription_id.0));
+
+ // Only insert header message if provided; frontend sets default fallback
+ if let Some(h) = header_message.clone() {
+ map.insert(CREDIT_HEADER_MESSAGE, h);
+ }
+
+ // Derive subscription type label for templates
+ // Resolve product metadata via price_id join
+ if let Some(info) = crate::database::models::user_subscription_item::DBUserSubscription::get(
+ (*subscription_id).into(),
+ &mut **exec,
+ )
+ .await
+ .ok()
+ .flatten()
+ && let Ok(Some(pinfo)) = crate::database::models::products_tax_identifier_item::product_info_by_product_price_id(info.price_id, &mut **exec).await {
+ let label = match pinfo.product_metadata {
+ crate::models::billing::ProductMetadata::Pyro { .. } => "server".to_string(),
+ crate::models::billing::ProductMetadata::Medal { .. } => "server".to_string(),
+ crate::models::billing::ProductMetadata::Midas => "Modrinth+".to_string(),
+ };
+ map.insert(CREDIT_SUBSCRIPTION_TYPE, label);
+ }
+
+ Ok(EmailTemplate::Static(map))
+ }
+
NotificationBody::Custom {
title,
body_md,
diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs
index 43e6d36b..3fb75a13 100644
--- a/apps/labrinth/src/routes/internal/billing.rs
+++ b/apps/labrinth/src/routes/internal/billing.rs
@@ -3,6 +3,7 @@ use crate::auth::get_user_from_headers;
use crate::database::models::charge_item::DBCharge;
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::products_tax_identifier_item::product_info_by_product_price_id;
+use crate::database::models::users_subscriptions_credits::DBUserSubscriptionCredit;
use crate::database::models::{
charge_item, generate_charge_id, product_item, user_subscription_item,
};
@@ -48,6 +49,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.service(edit_payment_method)
.service(remove_payment_method)
.service(charges)
+ .service(credit)
.service(active_servers)
.service(initiate_payment)
.service(stripe_webhook)
@@ -2188,3 +2190,238 @@ pub async fn stripe_webhook(
}
pub mod payments;
+
+#[allow(clippy::too_many_arguments)]
+async fn apply_credit_many_in_txn(
+ transaction: &mut Transaction<'_, Postgres>,
+ redis: &RedisPool,
+ current_user_id: crate::database::models::ids::DBUserId,
+ subscription_ids: Vec,
+ days: i32,
+ send_email: bool,
+ message: String,
+) -> Result<(), ApiError> {
+ use crate::database::models::ids::DBUserSubscriptionId;
+
+ let mut credit_sub_ids: Vec =
+ Vec::with_capacity(subscription_ids.len());
+ let mut credit_user_ids: Vec =
+ Vec::with_capacity(subscription_ids.len());
+ let mut credit_creditor_ids: Vec =
+ Vec::with_capacity(subscription_ids.len());
+ let mut credit_days: Vec = Vec::with_capacity(subscription_ids.len());
+ let mut credit_prev_dues: Vec> =
+ Vec::with_capacity(subscription_ids.len());
+ let mut credit_next_dues: Vec> =
+ Vec::with_capacity(subscription_ids.len());
+
+ let subs_ids: Vec = subscription_ids
+ .iter()
+ .map(|id| DBUserSubscriptionId(id.0 as i64))
+ .collect();
+ let subs = user_subscription_item::DBUserSubscription::get_many(
+ &subs_ids,
+ &mut **transaction,
+ )
+ .await?;
+
+ for subscription in subs {
+ let mut open_charge = charge_item::DBCharge::get_open_subscription(
+ subscription.id,
+ &mut **transaction,
+ )
+ .await?
+ .ok_or_else(|| {
+ ApiError::InvalidInput(format!(
+ "Could not find open charge for subscription {}",
+ to_base62(subscription.id.0 as u64)
+ ))
+ })?;
+
+ let previous_due = open_charge.due;
+ open_charge.due = previous_due + Duration::days(days as i64);
+ let next_due = open_charge.due;
+ open_charge.upsert(&mut *transaction).await?;
+
+ credit_sub_ids.push(subscription.id);
+ credit_user_ids.push(subscription.user_id);
+ credit_creditor_ids.push(current_user_id);
+ credit_days.push(days);
+ credit_prev_dues.push(previous_due);
+ credit_next_dues.push(next_due);
+
+ if send_email {
+ NotificationBuilder {
+ body: NotificationBody::SubscriptionCredited {
+ subscription_id: subscription.id.into(),
+ days,
+ previous_due,
+ next_due,
+ header_message: Some(message.clone()),
+ },
+ }
+ .insert(subscription.user_id, &mut *transaction, redis)
+ .await?;
+ }
+ }
+
+ DBUserSubscriptionCredit::insert_many(
+ &mut *transaction,
+ &credit_sub_ids,
+ &credit_user_ids,
+ &credit_creditor_ids,
+ &credit_days,
+ &credit_prev_dues,
+ &credit_next_dues,
+ )
+ .await
+ .map_err(|e| ApiError::Internal(eyre::eyre!(e)))?;
+
+ Ok(())
+}
+
+#[derive(Deserialize)]
+pub struct CreditRequest {
+ #[serde(flatten)]
+ pub target: CreditTarget,
+ pub days: i32,
+ pub send_email: bool,
+ pub message: String,
+}
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+pub enum CreditTarget {
+ Subscriptions {
+ subscription_ids: Vec,
+ },
+ Nodes {
+ nodes: Vec,
+ },
+ Region {
+ region: String,
+ },
+}
+
+#[post("credit")]
+pub async fn credit(
+ req: HttpRequest,
+ pool: web::Data,
+ redis: web::Data,
+ session_queue: web::Data,
+ archon_client: web::Data,
+ body: web::Json,
+) -> Result {
+ let user = get_user_from_headers(
+ &req,
+ &**pool,
+ &redis,
+ &session_queue,
+ Scopes::SESSION_ACCESS,
+ )
+ .await?
+ .1;
+
+ if !user.role.is_admin() {
+ return Err(ApiError::CustomAuthentication(
+ "You do not have permission to credit subscriptions!".to_string(),
+ ));
+ }
+
+ let CreditRequest {
+ target,
+ days,
+ send_email,
+ message,
+ } = body.into_inner();
+
+ if days <= 0 {
+ return Err(ApiError::InvalidInput(
+ "Days must be greater than zero".to_string(),
+ ));
+ }
+ let mut transaction = pool.begin().await?;
+
+ match target {
+ CreditTarget::Subscriptions { subscription_ids } => {
+ if subscription_ids.is_empty() {
+ return Err(ApiError::InvalidInput(
+ "You must specify at least one subscription id".to_string(),
+ ));
+ }
+ apply_credit_many_in_txn(
+ &mut transaction,
+ &redis,
+ crate::database::models::ids::DBUserId(user.id.0 as i64),
+ subscription_ids,
+ days,
+ send_email,
+ message,
+ )
+ .await?;
+ }
+ CreditTarget::Nodes { nodes } => {
+ if nodes.is_empty() {
+ return Err(ApiError::InvalidInput(
+ "You must specify at least one node hostname".to_string(),
+ ));
+ }
+ let mut server_ids: Vec = Vec::new();
+ for hostname in nodes {
+ let ids =
+ archon_client.get_servers_by_hostname(&hostname).await?;
+ server_ids.extend(ids);
+ }
+ server_ids.sort();
+ server_ids.dedup();
+ let subs = user_subscription_item::DBUserSubscription::get_many_by_server_ids(
+ &server_ids,
+ &mut *transaction,
+ )
+ .await?;
+ if subs.is_empty() {
+ return Err(ApiError::InvalidInput(
+ "No subscriptions found for provided nodes".to_string(),
+ ));
+ }
+ apply_credit_many_in_txn(
+ &mut transaction,
+ &redis,
+ crate::database::models::ids::DBUserId(user.id.0 as i64),
+ subs.into_iter().map(|s| s.id.into()).collect(),
+ days,
+ send_email,
+ message,
+ )
+ .await?;
+ }
+ CreditTarget::Region { region } => {
+ let parsed_active =
+ archon_client.get_active_servers_by_region(®ion).await?;
+ let subs = user_subscription_item::DBUserSubscription::get_many_by_server_ids(
+ &parsed_active,
+ &mut *transaction,
+ )
+ .await?;
+ if subs.is_empty() {
+ return Err(ApiError::InvalidInput(
+ "No subscriptions found for provided region".to_string(),
+ ));
+ }
+ apply_credit_many_in_txn(
+ &mut transaction,
+ &redis,
+ crate::database::models::ids::DBUserId(user.id.0 as i64),
+ subs.into_iter().map(|s| s.id.into()).collect(),
+ days,
+ send_email,
+ message,
+ )
+ .await?;
+ }
+ }
+
+ transaction.commit().await?;
+
+ Ok(HttpResponse::NoContent().finish())
+}
diff --git a/apps/labrinth/src/util/archon.rs b/apps/labrinth/src/util/archon.rs
index 471faa7b..384648be 100644
--- a/apps/labrinth/src/util/archon.rs
+++ b/apps/labrinth/src/util/archon.rs
@@ -72,4 +72,58 @@ impl ArchonClient {
Ok(response.json::().await?.uuid)
}
+
+ pub async fn get_servers_by_hostname(
+ &self,
+ hostname: &str,
+ ) -> Result, reqwest::Error> {
+ #[derive(Deserialize)]
+ struct NodeByHostnameResponse {
+ servers: Vec,
+ }
+ #[derive(Deserialize)]
+ struct NodeServerEntry {
+ id: String,
+ #[allow(dead_code)]
+ available: Option,
+ }
+
+ let res = self
+ .client
+ .get(format!(
+ "{}/_internal/nodes/by-hostname/{}",
+ self.base_url, hostname
+ ))
+ .header(X_MASTER_KEY, &self.pyro_api_key)
+ .send()
+ .await?
+ .error_for_status()?;
+
+ let parsed: NodeByHostnameResponse = res.json().await?;
+ Ok(parsed.servers.into_iter().map(|s| s.id).collect())
+ }
+
+ pub async fn get_active_servers_by_region(
+ &self,
+ region: &str,
+ ) -> Result, reqwest::Error> {
+ #[derive(Deserialize)]
+ struct RegionResponse {
+ active_servers: Vec,
+ }
+
+ let res = self
+ .client
+ .get(format!(
+ "{}/_internal/nodes/regions/{}",
+ self.base_url, region
+ ))
+ .header(X_MASTER_KEY, &self.pyro_api_key)
+ .send()
+ .await?
+ .error_for_status()?;
+
+ let parsed: RegionResponse = res.json().await?;
+ Ok(parsed.active_servers)
+ }
}
diff --git a/packages/utils/utils.ts b/packages/utils/utils.ts
index 2b2ca8bf..26481c67 100644
--- a/packages/utils/utils.ts
+++ b/packages/utils/utils.ts
@@ -373,3 +373,5 @@ export function arrayBufferToBase64(buffer: Uint8Array | ArrayBuffer): string {
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
return btoa(String.fromCharCode(...bytes))
}
+export const DEFAULT_CREDIT_EMAIL_MESSAGE =
+ "We're really sorry about the recent issues with your server."
From c379e4b17376597fc3fa85a0c5abf48138f7b5a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?=
<108630700+fetchfern@users.noreply.github.com>
Date: Mon, 20 Oct 2025 21:31:20 +0100
Subject: [PATCH 24/53] admin/credit: don't credit unprovisioned subscriptions
(#4594)
* Remove pointless sorting
* Filter subscriptions by labrinth's provisioned state
---
apps/labrinth/src/routes/internal/billing.rs | 55 +++++++++++---------
apps/labrinth/src/util/archon.rs | 8 +--
2 files changed, 35 insertions(+), 28 deletions(-)
diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs
index 3fb75a13..6e6c180e 100644
--- a/apps/labrinth/src/routes/internal/billing.rs
+++ b/apps/labrinth/src/routes/internal/billing.rs
@@ -1,6 +1,7 @@
use self::payments::*;
use crate::auth::get_user_from_headers;
use crate::database::models::charge_item::DBCharge;
+use crate::database::models::ids::DBUserSubscriptionId;
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::products_tax_identifier_item::product_info_by_product_price_id;
use crate::database::models::users_subscriptions_credits::DBUserSubscriptionCredit;
@@ -2189,10 +2190,8 @@ pub async fn stripe_webhook(
Ok(HttpResponse::Ok().finish())
}
-pub mod payments;
-
#[allow(clippy::too_many_arguments)]
-async fn apply_credit_many_in_txn(
+async fn apply_credit_many(
transaction: &mut Transaction<'_, Postgres>,
redis: &RedisPool,
current_user_id: crate::database::models::ids::DBUserId,
@@ -2201,20 +2200,6 @@ async fn apply_credit_many_in_txn(
send_email: bool,
message: String,
) -> Result<(), ApiError> {
- use crate::database::models::ids::DBUserSubscriptionId;
-
- let mut credit_sub_ids: Vec =
- Vec::with_capacity(subscription_ids.len());
- let mut credit_user_ids: Vec =
- Vec::with_capacity(subscription_ids.len());
- let mut credit_creditor_ids: Vec =
- Vec::with_capacity(subscription_ids.len());
- let mut credit_days: Vec = Vec::with_capacity(subscription_ids.len());
- let mut credit_prev_dues: Vec> =
- Vec::with_capacity(subscription_ids.len());
- let mut credit_next_dues: Vec> =
- Vec::with_capacity(subscription_ids.len());
-
let subs_ids: Vec = subscription_ids
.iter()
.map(|id| DBUserSubscriptionId(id.0 as i64))
@@ -2225,7 +2210,28 @@ async fn apply_credit_many_in_txn(
)
.await?;
+ let provisioned_count = subs
+ .iter()
+ .filter(|s| s.status == SubscriptionStatus::Provisioned)
+ .count();
+
+ let mut credit_sub_ids: Vec =
+ Vec::with_capacity(provisioned_count);
+ let mut credit_user_ids: Vec =
+ Vec::with_capacity(provisioned_count);
+ let mut credit_creditor_ids: Vec =
+ Vec::with_capacity(provisioned_count);
+ let mut credit_days: Vec = Vec::with_capacity(provisioned_count);
+ let mut credit_prev_dues: Vec> =
+ Vec::with_capacity(provisioned_count);
+ let mut credit_next_dues: Vec> =
+ Vec::with_capacity(provisioned_count);
+
for subscription in subs {
+ if subscription.status != SubscriptionStatus::Provisioned {
+ continue;
+ }
+
let mut open_charge = charge_item::DBCharge::get_open_subscription(
subscription.id,
&mut **transaction,
@@ -2349,7 +2355,7 @@ pub async fn credit(
"You must specify at least one subscription id".to_string(),
));
}
- apply_credit_many_in_txn(
+ apply_credit_many(
&mut transaction,
&redis,
crate::database::models::ids::DBUserId(user.id.0 as i64),
@@ -2370,9 +2376,8 @@ pub async fn credit(
for hostname in nodes {
let ids =
archon_client.get_servers_by_hostname(&hostname).await?;
- server_ids.extend(ids);
+ server_ids.extend(ids.into_iter().map(|id| id.to_string()));
}
- server_ids.sort();
server_ids.dedup();
let subs = user_subscription_item::DBUserSubscription::get_many_by_server_ids(
&server_ids,
@@ -2384,7 +2389,7 @@ pub async fn credit(
"No subscriptions found for provided nodes".to_string(),
));
}
- apply_credit_many_in_txn(
+ apply_credit_many(
&mut transaction,
&redis,
crate::database::models::ids::DBUserId(user.id.0 as i64),
@@ -2396,10 +2401,10 @@ pub async fn credit(
.await?;
}
CreditTarget::Region { region } => {
- let parsed_active =
+ let servers =
archon_client.get_active_servers_by_region(®ion).await?;
let subs = user_subscription_item::DBUserSubscription::get_many_by_server_ids(
- &parsed_active,
+ &servers.into_iter().map(|id| id.to_string()).collect::>(),
&mut *transaction,
)
.await?;
@@ -2408,7 +2413,7 @@ pub async fn credit(
"No subscriptions found for provided region".to_string(),
));
}
- apply_credit_many_in_txn(
+ apply_credit_many(
&mut transaction,
&redis,
crate::database::models::ids::DBUserId(user.id.0 as i64),
@@ -2425,3 +2430,5 @@ pub async fn credit(
Ok(HttpResponse::NoContent().finish())
}
+
+pub mod payments;
diff --git a/apps/labrinth/src/util/archon.rs b/apps/labrinth/src/util/archon.rs
index 384648be..621e923f 100644
--- a/apps/labrinth/src/util/archon.rs
+++ b/apps/labrinth/src/util/archon.rs
@@ -76,14 +76,14 @@ impl ArchonClient {
pub async fn get_servers_by_hostname(
&self,
hostname: &str,
- ) -> Result, reqwest::Error> {
+ ) -> Result, reqwest::Error> {
#[derive(Deserialize)]
struct NodeByHostnameResponse {
servers: Vec,
}
#[derive(Deserialize)]
struct NodeServerEntry {
- id: String,
+ id: Uuid,
#[allow(dead_code)]
available: Option,
}
@@ -106,10 +106,10 @@ impl ArchonClient {
pub async fn get_active_servers_by_region(
&self,
region: &str,
- ) -> Result, reqwest::Error> {
+ ) -> Result, reqwest::Error> {
#[derive(Deserialize)]
struct RegionResponse {
- active_servers: Vec,
+ active_servers: Vec,
}
let res = self
From 977de0e18a70c6d8e7e0a9fab5bff68ad08207e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?=
<108630700+fetchfern@users.noreply.github.com>
Date: Mon, 20 Oct 2025 23:24:47 +0100
Subject: [PATCH 25/53] Fix MaxMind (#4595)
* add maxmind to app data
* add back maxmind account id check
---
apps/labrinth/src/lib.rs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs
index d86a19f2..2524c122 100644
--- a/apps/labrinth/src/lib.rs
+++ b/apps/labrinth/src/lib.rs
@@ -318,6 +318,7 @@ pub fn app_config(
.app_data(labrinth_config.session_queue.clone())
.app_data(labrinth_config.payouts_queue.clone())
.app_data(labrinth_config.email_queue.clone())
+ .app_data(labrinth_config.maxmind.clone())
.app_data(web::Data::new(labrinth_config.ip_salt.clone()))
.app_data(web::Data::new(labrinth_config.analytics_queue.clone()))
.app_data(web::Data::new(labrinth_config.clickhouse.clone()))
@@ -484,6 +485,7 @@ pub fn check_env_vars() -> bool {
failed |= check_var::("CLICKHOUSE_PASSWORD");
failed |= check_var::("CLICKHOUSE_DATABASE");
+ failed |= check_var::("MAXMIND_ACCOUNT_ID");
failed |= check_var::("MAXMIND_LICENSE_KEY");
failed |= check_var::("FLAME_ANVIL_URL");
From a4015d9df3ded7c658fe632d36bcfec495659f3e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?=
<108630700+fetchfern@users.noreply.github.com>
Date: Tue, 21 Oct 2025 07:40:10 +0100
Subject: [PATCH 26/53] Fix v1 servers handling (#4596)
---
apps/frontend/src/composables/servers/servers-fetch.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts
index b46db7da..9b90822c 100644
--- a/apps/frontend/src/composables/servers/servers-fetch.ts
+++ b/apps/frontend/src/composables/servers/servers-fetch.ts
@@ -84,7 +84,7 @@ export async function useServersFetch(
? `${base}/modrinth/v${version}/${path.replace(/^\//, '')}`
: version === 'internal'
? `${base}/_internal/${path.replace(/^\//, '')}`
- : `${base}/modrinth/v${version}/${path.replace(/^\//, '')}`
+ : `${base}/v${version}/${path.replace(/^\//, '')}`
const headers: Record = {
'User-Agent': 'Modrinth/1.0 (https://modrinth.com)',
From f375913c62edf515297503c73a6c253db58ecec7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?=
<108630700+fetchfern@users.noreply.github.com>
Date: Tue, 21 Oct 2025 16:55:54 +0100
Subject: [PATCH 27/53] Adjust some values in tax-related tasks (#4598)
* Adjust some values for tax processing
* chore: query cache, clippy, fmt
---
...1061295ca82f44546527e1a31c03e5bb7a07c1e63.json} | 4 ++--
apps/labrinth/src/database/models/charge_item.rs | 2 +-
apps/labrinth/src/queue/billing.rs | 14 +++-----------
3 files changed, 6 insertions(+), 14 deletions(-)
rename apps/labrinth/.sqlx/{query-ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399.json => query-7d8de27065490edc560b0c81061295ca82f44546527e1a31c03e5bb7a07c1e63.json} (95%)
diff --git a/apps/labrinth/.sqlx/query-ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399.json b/apps/labrinth/.sqlx/query-7d8de27065490edc560b0c81061295ca82f44546527e1a31c03e5bb7a07c1e63.json
similarity index 95%
rename from apps/labrinth/.sqlx/query-ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399.json
rename to apps/labrinth/.sqlx/query-7d8de27065490edc560b0c81061295ca82f44546527e1a31c03e5bb7a07c1e63.json
index 0e9a5c30..ba3e1bd7 100644
--- a/apps/labrinth/.sqlx/query-ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399.json
+++ b/apps/labrinth/.sqlx/query-7d8de27065490edc560b0c81061295ca82f44546527e1a31c03e5bb7a07c1e63.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '14 days' < NOW() -- Due between 7 and 14 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t",
+ "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '30 days' < NOW() -- Due between 7 and 30 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t",
"describe": {
"columns": [
{
@@ -138,5 +138,5 @@
true
]
},
- "hash": "ce23f89106ef7b34f5a935f6e792d87b8805e87ac0cefb43828fc6d3aca52399"
+ "hash": "7d8de27065490edc560b0c81061295ca82f44546527e1a31c03e5bb7a07c1e63"
}
diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs
index 652f5040..4b9e704f 100644
--- a/apps/labrinth/src/database/models/charge_item.rs
+++ b/apps/labrinth/src/database/models/charge_item.rs
@@ -338,7 +338,7 @@ impl DBCharge {
AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'
AND u.email IS NOT NULL
AND due - INTERVAL '7 days' > NOW()
- AND due - INTERVAL '14 days' < NOW() -- Due between 7 and 14 days from now
+ AND due - INTERVAL '30 days' < NOW() -- Due between 7 and 30 days from now
ORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC
FOR NO KEY UPDATE SKIP LOCKED
LIMIT $1
diff --git a/apps/labrinth/src/queue/billing.rs b/apps/labrinth/src/queue/billing.rs
index c160ca0a..bfb3d749 100644
--- a/apps/labrinth/src/queue/billing.rs
+++ b/apps/labrinth/src/queue/billing.rs
@@ -35,9 +35,6 @@ use tracing::{debug, error, info, warn};
/// Updates charges which need to have their tax amount updated. This is done within a timer to avoid reaching
/// Anrok API limits.
-///
-/// The global rate limit for Anrok API operations is 10 RPS, so we run ~8 requests every second up
-/// to the specified limit of processed charges.
async fn update_tax_amounts(
pg: &PgPool,
redis: &RedisPool,
@@ -45,17 +42,12 @@ async fn update_tax_amounts(
stripe_client: &stripe::Client,
limit: i64,
) -> Result<(), ApiError> {
- let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
- interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
-
let mut processed_charges = 0;
loop {
- interval.tick().await;
-
let mut txn = pg.begin().await?;
- let charges = DBCharge::get_updateable_lock(&mut *txn, 8).await?;
+ let charges = DBCharge::get_updateable_lock(&mut *txn, 5).await?;
if charges.is_empty() {
info!("No more charges to process");
@@ -1023,14 +1015,14 @@ pub async fn index_subscriptions(
&redis,
&anrok_client,
&stripe_client,
- 1000,
+ 500,
),
)
.await;
run_and_time(
"update_tax_amounts",
- update_tax_amounts(&pool, &redis, &anrok_client, &stripe_client, 50),
+ update_tax_amounts(&pool, &redis, &anrok_client, &stripe_client, 500),
)
.await;
From f78fbe3215aeb75a052f935791cddf390de49560 Mon Sep 17 00:00:00 2001
From: "Calum H."
Date: Wed, 22 Oct 2025 17:25:55 +0100
Subject: [PATCH 28/53] fix: disable start button on backup restore/create
(#4582)
* fix: CLAUDE.md
* fix: allowing start server on backup create/restore
---------
Signed-off-by: Calum H.
---
CLAUDE.md | 31 +++++++++
.../ui/servers/PanelServerActionButton.vue | 16 ++++-
.../composables/servers/modules/backups.ts | 63 ++++++++++++++-----
.../src/pages/servers/manage/[id].vue | 15 +++--
4 files changed, 105 insertions(+), 20 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 172f4d8d..d84f325e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,5 +1,35 @@
# Architecture
+Use TAB instead of spaces.
+
+## Frontend
+
+There are two similar frontends in the Modrinth monorepo, the website (apps/frontend) and the app frontend (apps/app-frontend).
+
+Both use Tailwind v3, and their respective configs can be seen at `tailwind.config.ts` and `tailwind.config.js` respectively.
+
+Both utilize shared and common components from `@modrinth/ui` which can be found at `packages/ui`, and stylings from `@modrinth/assets` which can be found at `packages/assets`.
+
+Both can utilize icons from `@modrinth/assets`, which are automatically generated based on what's available within the `icons` folder of the `packages/assets` directory. You can see the generated icons list in `generated-icons.ts`.
+
+Both have access to our dependency injection framework, examples as seen in `packages/ui/src/providers/`. Ideally any state which is shared between a page and it's subpages should be shared using this dependency injection framework.
+
+### Website (apps/frontend)
+
+Before a pull request can be opened for the website, `pnpm web:fix` and `pnpm web:intl:extract` must be run, otherwise CI will fail.
+
+To run a development version of the frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within the `apps/frontend` folder into `apps/frontend/.env`. Then you can run the frontend by running `pnpm web:dev` in the root folder.
+
+### App Frontend (apps/app-frontend)
+
+Before a pull request can be opened for the website, you must CD into the `app-frontend` folder; `pnpm fix` and `pnpm intl:extract` must be run, otherwise CI will fail.
+
+To run a development version of the app frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within `packages/app-lib` into `packages/app-lib/.env`. Then you must run the app itself by running `pnpm app:dev` in the root folder.
+
+### Localization
+
+Refer to `.github/instructions/i18n-convert.instructions.md` if the user asks you to perform any i18n conversion work on a component, set of components, pages or sets of pages.
+
## Labrinth
Labrinth is the backend API service for Modrinth.
@@ -15,6 +45,7 @@ To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`.
Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services.
When the user refers to "performing pre-PR checks", do the following:
+
- Run clippy as described above
- DO NOT run tests unless explicitly requested (they take a long time)
- Prepare the sqlx cache
diff --git a/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue b/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue
index c7ff4c38..4899838a 100644
--- a/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue
+++ b/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue
@@ -68,7 +68,11 @@
-
+
@@ -122,12 +126,15 @@ import { useStorage } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
+import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
+
import LoadingIcon from './icons/LoadingIcon.vue'
import PanelSpinner from './PanelSpinner.vue'
import ServerInfoLabels from './ServerInfoLabels.vue'
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
const flags = useFeatureFlags()
+const { formatMessage } = useVIntl()
interface PowerAction {
action: ServerPowerAction
@@ -142,6 +149,7 @@ const props = defineProps<{
serverName?: string
serverData: object
uptimeSeconds: number
+ backupInProgress?: BackupInProgressReason
}>()
const emit = defineEmits<{
@@ -163,7 +171,11 @@ const dontAskAgain = ref(false)
const startingDelay = ref(false)
const canTakeAction = computed(
- () => !props.isActioning && !startingDelay.value && !isTransitionState.value,
+ () =>
+ !props.isActioning &&
+ !startingDelay.value &&
+ !isTransitionState.value &&
+ !props.backupInProgress,
)
const isRunning = computed(() => serverState.value === 'running')
const isTransitionState = computed(() =>
diff --git a/apps/frontend/src/composables/servers/modules/backups.ts b/apps/frontend/src/composables/servers/modules/backups.ts
index fd2f2646..921b9a35 100644
--- a/apps/frontend/src/composables/servers/modules/backups.ts
+++ b/apps/frontend/src/composables/servers/modules/backups.ts
@@ -11,12 +11,35 @@ export class BackupsModule extends ServerModule {
}
async create(backupName: string): Promise {
- const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
- method: 'POST',
- body: { name: backupName },
- })
- await this.fetch() // Refresh this module
- return response.id
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).substring(7)}`
+ const tempBackup: Backup = {
+ id: tempId,
+ name: backupName,
+ created_at: new Date().toISOString(),
+ locked: false,
+ automated: false,
+ interrupted: false,
+ ongoing: true,
+ task: { create: { progress: 0, state: 'ongoing' } },
+ }
+ this.data.push(tempBackup)
+
+ try {
+ const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
+ method: 'POST',
+ body: { name: backupName },
+ })
+
+ const backup = this.data.find((b) => b.id === tempId)
+ if (backup) {
+ backup.id = response.id
+ }
+
+ return response.id
+ } catch (error) {
+ this.data = this.data.filter((b) => b.id !== tempId)
+ throw error
+ }
}
async rename(backupId: string, newName: string): Promise {
@@ -24,35 +47,47 @@ export class BackupsModule extends ServerModule {
method: 'POST',
body: { name: newName },
})
- await this.fetch() // Refresh this module
+ await this.fetch()
}
async delete(backupId: string): Promise {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
method: 'DELETE',
})
- await this.fetch() // Refresh this module
+ await this.fetch()
}
async restore(backupId: string): Promise {
- await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
- method: 'POST',
- })
- await this.fetch() // Refresh this module
+ const backup = this.data.find((b) => b.id === backupId)
+ if (backup) {
+ if (!backup.task) backup.task = {}
+ backup.task.restore = { progress: 0, state: 'ongoing' }
+ }
+
+ try {
+ await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
+ method: 'POST',
+ })
+ } catch (error) {
+ if (backup?.task?.restore) {
+ delete backup.task.restore
+ }
+ throw error
+ }
}
async lock(backupId: string): Promise {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
method: 'POST',
})
- await this.fetch() // Refresh this module
+ await this.fetch()
}
async unlock(backupId: string): Promise {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
method: 'POST',
})
- await this.fetch() // Refresh this module
+ await this.fetch()
}
async retry(backupId: string): Promise {
diff --git a/apps/frontend/src/pages/servers/manage/[id].vue b/apps/frontend/src/pages/servers/manage/[id].vue
index 4e2c046f..653c9f5c 100644
--- a/apps/frontend/src/pages/servers/manage/[id].vue
+++ b/apps/frontend/src/pages/servers/manage/[id].vue
@@ -151,6 +151,7 @@
:server-name="serverData.name"
:server-data="serverData"
:uptime-seconds="uptimeSeconds"
+ :backup-in-progress="backupInProgress"
@action="sendPowerAction"
/>
@@ -354,7 +355,7 @@
>
{{
- JSON.stringify(server, null, ' ')
+ JSON.stringify(server, null, ' ')
}}
@@ -759,9 +760,14 @@ const handleWebSocketMessage = (data: WSEvent) => {
curBackup.task = {}
}
- curBackup.task[data.task] = {
- progress: data.progress,
- state: data.state,
+ const currentState = curBackup.task[data.task]?.state
+ const shouldUpdate = !(currentState === 'ongoing' && data.state === 'unchanged')
+
+ if (shouldUpdate) {
+ curBackup.task[data.task] = {
+ progress: data.progress,
+ state: data.state,
+ }
}
curBackup.ongoing = data.task === 'create' && data.state === 'ongoing'
@@ -1277,6 +1283,7 @@ useHead({
opacity: 0;
transform: translateX(1rem);
}
+
100% {
opacity: 1;
transform: none;
From a547f7a9b06ca2d5fa9c4b614d4c35f30f220430 Mon Sep 17 00:00:00 2001
From: Prospector <6166773+Prospector@users.noreply.github.com>
Date: Wed, 22 Oct 2025 18:13:49 -0700
Subject: [PATCH 29/53] Update issue templates (#4606)
* Update 1-app-bug.yml
Signed-off-by: Prospector <6166773+Prospector@users.noreply.github.com>
* update the rest of the templates
* Update issue template formatting further
* Disable blank issue + get rid of some contact links
* fix issue location id
* more updates
---------
Signed-off-by: Prospector <6166773+Prospector@users.noreply.github.com>
---
.github/ISSUE_TEMPLATE/1-app-bug.yml | 7 ++-
.github/ISSUE_TEMPLATE/2-web-bug.yml | 7 ++-
.github/ISSUE_TEMPLATE/3-servers-bug.yml | 63 +++++++++++++++++++
.../{3-api-bug.yml => 4-api-bug.yml} | 7 ++-
...ture-request.yml => 5-feature-request.yml} | 11 ++--
.github/ISSUE_TEMPLATE/config.yml | 14 ++---
6 files changed, 85 insertions(+), 24 deletions(-)
create mode 100644 .github/ISSUE_TEMPLATE/3-servers-bug.yml
rename .github/ISSUE_TEMPLATE/{3-api-bug.yml => 4-api-bug.yml} (90%)
rename .github/ISSUE_TEMPLATE/{4-feature-request.yml => 5-feature-request.yml} (90%)
diff --git a/.github/ISSUE_TEMPLATE/1-app-bug.yml b/.github/ISSUE_TEMPLATE/1-app-bug.yml
index 3b0a6791..fad0c15c 100644
--- a/.github/ISSUE_TEMPLATE/1-app-bug.yml
+++ b/.github/ISSUE_TEMPLATE/1-app-bug.yml
@@ -1,6 +1,7 @@
-name: 🎮 Modrinth App bug
-description: Report an issue in the Modrinth Launcher.
-labels: [bug, app]
+name: 🎮 Bug with Modrinth App
+description: For issues with Modrinth App.
+labels: [app]
+type: 'bug'
body:
- type: checkboxes
attributes:
diff --git a/.github/ISSUE_TEMPLATE/2-web-bug.yml b/.github/ISSUE_TEMPLATE/2-web-bug.yml
index 118f2133..983279c1 100644
--- a/.github/ISSUE_TEMPLATE/2-web-bug.yml
+++ b/.github/ISSUE_TEMPLATE/2-web-bug.yml
@@ -1,6 +1,7 @@
-name: 🌐 Website bug (modrinth.com)
-description: Report an issue on the Modrinth website.
-labels: [bug, web]
+name: 🌐 Bug with Modrinth.com
+description: For issues with the Modrinth website.
+labels: [website]
+type: 'bug'
body:
- type: checkboxes
attributes:
diff --git a/.github/ISSUE_TEMPLATE/3-servers-bug.yml b/.github/ISSUE_TEMPLATE/3-servers-bug.yml
new file mode 100644
index 00000000..a0677ce7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/3-servers-bug.yml
@@ -0,0 +1,63 @@
+name: 🌐 Bug with Modrinth Servers
+description: For issues with a Modrinth Servers product.
+labels: [servers]
+type: 'bug'
+body:
+ - type: checkboxes
+ attributes:
+ label: Please confirm the following.
+ options:
+ - label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate problems
+ required: true
+ - label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
+ required: true
+ - type: dropdown
+ id: issue-location
+ attributes:
+ label: Is this an issue in the control panel or with the Minecraft server itself?
+ options:
+ - Control panel (on Modrinth.com)
+ - Minecraft server
+ validations:
+ required: true
+ - type: dropdown
+ id: browsers
+ attributes:
+ label: What browsers are you seeing the problem on? (if a panel issue)
+ multiple: true
+ options:
+ - N/A
+ - Chrome (including Arc, Brave, Opera, Vivaldi)
+ - Microsoft Edge
+ - Firefox
+ - Safari
+ - Other (please specify)
+ - type: textarea
+ attributes:
+ label: Describe the bug
+ description: A clear and concise description of what the bug is. Include screenshots if applicable.
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Steps to reproduce
+ description: Steps to reproduce the behavior.
+ placeholder: |
+ 1. Go to '...'
+ 2. Click on '...'
+ 3. Scroll down to '...'
+ 4. See error
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Expected behavior
+ description: A clear and concise description of what you expected to happen.
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Additional context
+ description: Add any other context about the problem here.
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/3-api-bug.yml b/.github/ISSUE_TEMPLATE/4-api-bug.yml
similarity index 90%
rename from .github/ISSUE_TEMPLATE/3-api-bug.yml
rename to .github/ISSUE_TEMPLATE/4-api-bug.yml
index 19f7f764..00fac9ba 100644
--- a/.github/ISSUE_TEMPLATE/3-api-bug.yml
+++ b/.github/ISSUE_TEMPLATE/4-api-bug.yml
@@ -1,6 +1,7 @@
-name: 🛠️ API issue (api.modrinth.com)
-description: Report an issue regarding the Modrinth API.
-labels: [bug, backend]
+name: 🛠 Bug with Modrinth API
+description: For issues with the Modrinth API for developers.
+labels: [api, backend]
+type: 'bug'
body:
- type: checkboxes
attributes:
diff --git a/.github/ISSUE_TEMPLATE/4-feature-request.yml b/.github/ISSUE_TEMPLATE/5-feature-request.yml
similarity index 90%
rename from .github/ISSUE_TEMPLATE/4-feature-request.yml
rename to .github/ISSUE_TEMPLATE/5-feature-request.yml
index 945d6841..0b358e9b 100644
--- a/.github/ISSUE_TEMPLATE/4-feature-request.yml
+++ b/.github/ISSUE_TEMPLATE/5-feature-request.yml
@@ -1,6 +1,6 @@
-name: 💡 Feature Request
+name: 💡 Feature request
description: Suggest an idea
-labels: [enhancement]
+type: 'feature'
body:
- type: checkboxes
@@ -17,9 +17,10 @@ body:
label: What parts of Modrinth is your feature request related too?
multiple: true
options:
- - App
- - Website
- - API
+ - Modrinth App
+ - Modrinth.com website
+ - Modrinth Servers
+ - Modrinth API for developers
- type: textarea
attributes:
label: Is your suggested feature related to a problem? Please describe.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 26d7b239..0540a64e 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,14 +1,8 @@
-blank_issues_enabled: true
+blank_issues_enabled: false
contact_links:
- - name: 🫶 Support Portal
- about: Get support using through our portal.
+ - name: 🫶 Support portal
+ about: Get support using through our support website.
url: https://support.modrinth.com
- - name: 💬 Chat
+ - name: 💬 Chat on Discord
about: Join our Discord server to chat about Modrinth.
url: https://discord.modrinth.com
- - name: 🛣️ Roadmap
- about: View our Roadmap. Please do not open issues for items on our roadmap.
- url: https://roadmap.modrinth.com
- - name: 📚 Documentation
- about: Useful documentation about Modrinth's API
- url: https://docs.modrinth.com
From 8d80433c2ce778c3f6c0228732a1b84c7a3a3c4d Mon Sep 17 00:00:00 2001
From: Prospector <6166773+Prospector@users.noreply.github.com>
Date: Wed, 22 Oct 2025 18:15:41 -0700
Subject: [PATCH 30/53] Update 3-servers-bug.yml (#4607)
Signed-off-by: Prospector <6166773+Prospector@users.noreply.github.com>
---
.github/ISSUE_TEMPLATE/3-servers-bug.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/3-servers-bug.yml b/.github/ISSUE_TEMPLATE/3-servers-bug.yml
index a0677ce7..39214a8f 100644
--- a/.github/ISSUE_TEMPLATE/3-servers-bug.yml
+++ b/.github/ISSUE_TEMPLATE/3-servers-bug.yml
@@ -1,4 +1,4 @@
-name: 🌐 Bug with Modrinth Servers
+name: 👥 Bug with Modrinth Servers
description: For issues with a Modrinth Servers product.
labels: [servers]
type: 'bug'
From 707ff2146bb6e98af29c47f63268e13d01b07031 Mon Sep 17 00:00:00 2001
From: aecsocket
Date: Fri, 24 Oct 2025 07:19:53 -0700
Subject: [PATCH 31/53] Update appropriate rows when removing a user (#4597)
* Update appropriate rows when removing a user
* Update sqlx cache
* Delete rows from payouts_values_notifications instead of make ghost user
---
...7ae62351eeeb2ae3b8148cf8a8fd0deb2795a.json | 15 +++++++
...044f6760683cb89ff39255a177bb025e7638e.json | 14 +++++++
...3a8ba3a2c2be461ff9a6309d7e36c3148aeea.json | 15 +++++++
...8add5d93855f599a48eeb5f1811f14e7fe610.json | 14 +++++++
.../labrinth-seed-data-202508052143.sql | 2 +
.../labrinth/src/database/models/user_item.rs | 40 +++++++++++++++++++
6 files changed, 100 insertions(+)
create mode 100644 apps/labrinth/.sqlx/query-6443da83032ef5d6cb907f97fb37ae62351eeeb2ae3b8148cf8a8fd0deb2795a.json
create mode 100644 apps/labrinth/.sqlx/query-713034d4968b290a0096e41b9da044f6760683cb89ff39255a177bb025e7638e.json
create mode 100644 apps/labrinth/.sqlx/query-b97afaa6cab8e042ab0117e64b43a8ba3a2c2be461ff9a6309d7e36c3148aeea.json
create mode 100644 apps/labrinth/.sqlx/query-ca9b41de4618bcf8ff4f6086f658add5d93855f599a48eeb5f1811f14e7fe610.json
diff --git a/apps/labrinth/.sqlx/query-6443da83032ef5d6cb907f97fb37ae62351eeeb2ae3b8148cf8a8fd0deb2795a.json b/apps/labrinth/.sqlx/query-6443da83032ef5d6cb907f97fb37ae62351eeeb2ae3b8148cf8a8fd0deb2795a.json
new file mode 100644
index 00000000..1f1eb4f3
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-6443da83032ef5d6cb907f97fb37ae62351eeeb2ae3b8148cf8a8fd0deb2795a.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE affiliate_codes\n SET created_by = $1\n WHERE created_by = $2",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "6443da83032ef5d6cb907f97fb37ae62351eeeb2ae3b8148cf8a8fd0deb2795a"
+}
diff --git a/apps/labrinth/.sqlx/query-713034d4968b290a0096e41b9da044f6760683cb89ff39255a177bb025e7638e.json b/apps/labrinth/.sqlx/query-713034d4968b290a0096e41b9da044f6760683cb89ff39255a177bb025e7638e.json
new file mode 100644
index 00000000..2465ba1b
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-713034d4968b290a0096e41b9da044f6760683cb89ff39255a177bb025e7638e.json
@@ -0,0 +1,14 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n DELETE FROM payouts_values_notifications\n WHERE user_id = $1",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "713034d4968b290a0096e41b9da044f6760683cb89ff39255a177bb025e7638e"
+}
diff --git a/apps/labrinth/.sqlx/query-b97afaa6cab8e042ab0117e64b43a8ba3a2c2be461ff9a6309d7e36c3148aeea.json b/apps/labrinth/.sqlx/query-b97afaa6cab8e042ab0117e64b43a8ba3a2c2be461ff9a6309d7e36c3148aeea.json
new file mode 100644
index 00000000..30cda8db
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-b97afaa6cab8e042ab0117e64b43a8ba3a2c2be461ff9a6309d7e36c3148aeea.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE payouts_values\n SET user_id = $1\n WHERE user_id = $2",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "b97afaa6cab8e042ab0117e64b43a8ba3a2c2be461ff9a6309d7e36c3148aeea"
+}
diff --git a/apps/labrinth/.sqlx/query-ca9b41de4618bcf8ff4f6086f658add5d93855f599a48eeb5f1811f14e7fe610.json b/apps/labrinth/.sqlx/query-ca9b41de4618bcf8ff4f6086f658add5d93855f599a48eeb5f1811f14e7fe610.json
new file mode 100644
index 00000000..7d1b773b
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-ca9b41de4618bcf8ff4f6086f658add5d93855f599a48eeb5f1811f14e7fe610.json
@@ -0,0 +1,14 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n DELETE FROM affiliate_codes\n WHERE affiliate = $1",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "ca9b41de4618bcf8ff4f6086f658add5d93855f599a48eeb5f1811f14e7fe610"
+}
diff --git a/apps/labrinth/fixtures/labrinth-seed-data-202508052143.sql b/apps/labrinth/fixtures/labrinth-seed-data-202508052143.sql
index 1ad6d4ec..5f4b0d20 100644
--- a/apps/labrinth/fixtures/labrinth-seed-data-202508052143.sql
+++ b/apps/labrinth/fixtures/labrinth-seed-data-202508052143.sql
@@ -1104,5 +1104,7 @@ COPY public.users (id, github_id, username, email, avatar_url, bio, created, rol
103587649610509 \N Default admin user admin@modrinth.invalid https://avatars.githubusercontent.com/u/106493074 $ chmod 777 labrinth 2020-07-18 16:03:00.000000+00 admin 0 0.00000000000000000000 \N \N \N \N \N $argon2i$v=19$m=4096,t=3,p=1$c2FsdEl0V2l0aFNhbHQ$xTGvQNICqetaNA0Wu1GwFmYhQjAreRcjBz6ornhaFXA t \N \N \N \N \N \N https://avatars.githubusercontent.com/u/106493074 t
\.
+INSERT INTO sessions (id, session, user_id, created, last_login, expires, refresh_expires, city, country, ip, os, platform, user_agent)
+VALUES (93083445641246, 'mra_admin', 103587649610509, '2025-10-20 14:58:53.128901+00', '2025-10-20 14:58:53.128901+00', '2025-11-03 14:58:53.128901+00', '2025-12-19 14:58:53.128901+00', '', '', '127.0.0.1', 'Linux', 'Chrome', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36');
COMMIT;
diff --git a/apps/labrinth/src/database/models/user_item.rs b/apps/labrinth/src/database/models/user_item.rs
index 090d99e4..18e4e7f7 100644
--- a/apps/labrinth/src/database/models/user_item.rs
+++ b/apps/labrinth/src/database/models/user_item.rs
@@ -753,6 +753,46 @@ impl DBUser {
.execute(&mut **transaction)
.await?;
+ sqlx::query!(
+ "
+ UPDATE affiliate_codes
+ SET created_by = $1
+ WHERE created_by = $2",
+ deleted_user as DBUserId,
+ id as DBUserId,
+ )
+ .execute(&mut **transaction)
+ .await?;
+
+ sqlx::query!(
+ "
+ DELETE FROM affiliate_codes
+ WHERE affiliate = $1",
+ id as DBUserId,
+ )
+ .execute(&mut **transaction)
+ .await?;
+
+ sqlx::query!(
+ "
+ UPDATE payouts_values
+ SET user_id = $1
+ WHERE user_id = $2",
+ deleted_user as DBUserId,
+ id as DBUserId,
+ )
+ .execute(&mut **transaction)
+ .await?;
+
+ sqlx::query!(
+ "
+ DELETE FROM payouts_values_notifications
+ WHERE user_id = $1",
+ id as DBUserId,
+ )
+ .execute(&mut **transaction)
+ .await?;
+
let open_subscriptions =
DBUserSubscription::get_all_user(id, &mut **transaction)
.await?;
From 03b0eba695e764f6df09dae12c7f32f767b991be Mon Sep 17 00:00:00 2001
From: aecsocket
Date: Fri, 24 Oct 2025 07:44:50 -0700
Subject: [PATCH 32/53] Add `utoipa` Swagger UI support (#4602)
* Add utoipa Swagger UI support
* remove unused code
* remove unused code
* consistency with trailing slash
---
Cargo.lock | 112 ++++++++++++++++++
Cargo.toml | 3 +
apps/labrinth/Cargo.toml | 3 +
apps/labrinth/src/lib.rs | 7 ++
apps/labrinth/src/main.rs | 14 +++
apps/labrinth/src/routes/mod.rs | 8 +-
apps/labrinth/src/routes/v3/analytics_get.rs | 74 +++++++-----
.../old.rs} | 79 ++++++------
apps/labrinth/src/routes/v3/mod.rs | 12 +-
apps/labrinth/tests/common/api_v2/mod.rs | 13 +-
apps/labrinth/tests/common/api_v3/mod.rs | 13 +-
packages/ariadne/Cargo.toml | 1 +
packages/ariadne/src/ids.rs | 1 +
13 files changed, 253 insertions(+), 87 deletions(-)
rename apps/labrinth/src/routes/v3/{analytics_get_old.rs => analytics_get/old.rs} (91%)
diff --git a/Cargo.lock b/Cargo.lock
index 85a87329..41bea058 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -478,6 +478,7 @@ dependencies = [
"serde_cbor",
"serde_json",
"thiserror 2.0.17",
+ "utoipa",
"uuid 1.18.1",
]
@@ -4678,6 +4679,9 @@ dependencies = [
"tracing-subscriber",
"url",
"urlencoding",
+ "utoipa",
+ "utoipa-actix-web",
+ "utoipa-swagger-ui",
"uuid 1.18.1",
"validator",
"webp",
@@ -7335,6 +7339,40 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rust-embed"
+version = "8.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.106",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
[[package]]
name = "rust-ini"
version = "0.21.3"
@@ -10309,6 +10347,66 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+[[package]]
+name = "utoipa"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
+dependencies = [
+ "indexmap 2.11.4",
+ "serde",
+ "serde_json",
+ "utoipa-gen",
+]
+
+[[package]]
+name = "utoipa-actix-web"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f"
+dependencies = [
+ "actix-service",
+ "actix-web",
+ "utoipa",
+]
+
+[[package]]
+name = "utoipa-gen"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "utoipa-swagger-ui"
+version = "9.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
+dependencies = [
+ "actix-web",
+ "base64 0.22.1",
+ "mime_guess",
+ "regex",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "url",
+ "utoipa",
+ "utoipa-swagger-ui-vendored",
+ "zip 3.0.0",
+]
+
+[[package]]
+name = "utoipa-swagger-ui-vendored"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d"
+
[[package]]
name = "uuid"
version = "0.8.2"
@@ -11687,6 +11785,20 @@ dependencies = [
"syn 2.0.106",
]
+[[package]]
+name = "zip"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "flate2",
+ "indexmap 2.11.4",
+ "memchr",
+ "zopfli",
+]
+
[[package]]
name = "zip"
version = "4.6.1"
diff --git a/Cargo.toml b/Cargo.toml
index a661c69d..eb231050 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -192,6 +192,9 @@ tracing-subscriber = "0.3.20"
typed-path = "0.12.0"
url = "2.5.7"
urlencoding = "2.1.3"
+utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
+utoipa-actix-web = { version = "0.1.2" }
+utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web", "vendored"] }
uuid = "1.18.1"
validator = "0.20.0"
webp = { version = "0.3.1", default-features = false }
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index 9f3075b9..055e2e58 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -120,6 +120,9 @@ tracing-ecs = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
+utoipa = { workspace = true }
+utoipa-actix-web = { workspace = true }
+utoipa-swagger-ui = { workspace = true }
uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
validator = { workspace = true, features = ["derive"] }
webp = { workspace = true }
diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs
index 2524c122..2dff7031 100644
--- a/apps/labrinth/src/lib.rs
+++ b/apps/labrinth/src/lib.rs
@@ -345,6 +345,13 @@ pub fn app_config(
.default_service(web::get().wrap(default_cors()).to(routes::not_found));
}
+pub fn utoipa_app_config(
+ cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
+ _labrinth_config: LabrinthConfig,
+) {
+ cfg.configure(routes::v3::utoipa_config);
+}
+
// This is so that env vars not used immediately don't panic at runtime
pub fn check_env_vars() -> bool {
let mut failed = false;
diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs
index f2dbf027..643d6062 100644
--- a/apps/labrinth/src/main.rs
+++ b/apps/labrinth/src/main.rs
@@ -14,6 +14,7 @@ use labrinth::util::anrok;
use labrinth::util::env::parse_var;
use labrinth::util::gotenberg::GotenbergClient;
use labrinth::util::ratelimit::rate_limit_middleware;
+use labrinth::utoipa_app_config;
use labrinth::{check_env_vars, clickhouse, database, file_hosting};
use std::ffi::CStr;
use std::str::FromStr;
@@ -25,6 +26,9 @@ use tracing_ecs::ECSLayerBuilder;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
+use utoipa::OpenApi;
+use utoipa_actix_web::AppExt;
+use utoipa_swagger_ui::SwaggerUi;
#[cfg(target_os = "linux")]
#[global_allocator]
@@ -293,6 +297,12 @@ async fn main() -> std::io::Result<()> {
.wrap(from_fn(rate_limit_middleware))
.wrap(actix_web::middleware::Compress::default())
.wrap(sentry_actix::Sentry::new())
+ .into_utoipa_app()
+ .configure(|cfg| utoipa_app_config(cfg, labrinth_config.clone()))
+ .openapi_service(|api| SwaggerUi::new("/docs/swagger-ui/{_:.*}")
+ .config(utoipa_swagger_ui::Config::default().try_it_out_enabled(true))
+ .url("/docs/openapi.json", ApiDoc::openapi().merge_from(api)))
+ .into_app()
.configure(|cfg| app_config(cfg, labrinth_config.clone()))
})
.bind(dotenvy::var("BIND_ADDR").unwrap())?
@@ -300,6 +310,10 @@ async fn main() -> std::io::Result<()> {
.await
}
+#[derive(utoipa::OpenApi)]
+#[openapi(info(title = "Labrinth"))]
+struct ApiDoc;
+
fn log_error(err: &actix_web::Error) {
if err.as_response_error().status_code().is_client_error() {
tracing::debug!(
diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs
index 66a20a91..fad90bea 100644
--- a/apps/labrinth/src/routes/mod.rs
+++ b/apps/labrinth/src/routes/mod.rs
@@ -77,12 +77,8 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
}.boxed_local()
})
);
- cfg.service(
- web::scope("")
- .wrap(default_cors())
- .service(index::index_get)
- .service(Files::new("/", "assets/")),
- );
+ cfg.service(index::index_get);
+ cfg.service(Files::new("/", "assets/"));
}
#[derive(thiserror::Error, Debug)]
diff --git a/apps/labrinth/src/routes/v3/analytics_get.rs b/apps/labrinth/src/routes/v3/analytics_get.rs
index 7713b1da..924bcae5 100644
--- a/apps/labrinth/src/routes/v3/analytics_get.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get.rs
@@ -7,9 +7,11 @@
//! requests, you have to zip together M arrays of N elements
//! - this makes it inconvenient to have separate endpoints
+mod old;
+
use std::num::NonZeroU64;
-use actix_web::{HttpRequest, web};
+use actix_web::{HttpRequest, post, web};
use chrono::{DateTime, TimeDelta, Utc};
use futures::StreamExt;
use rust_decimal::Decimal;
@@ -32,10 +34,9 @@ use crate::{
routes::ApiError,
};
-// TODO: this service `analytics` is shadowed by `analytics_get_old`'s
-// see the TODO in `analytics_get_old.rs`
-pub fn config(cfg: &mut web::ServiceConfig) {
- cfg.service(web::scope("analytics").route("", web::post().to(get)));
+pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
+ cfg.service(fetch_analytics);
+ cfg.configure(old::config);
}
// request
@@ -43,7 +44,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
/// Requests analytics data, aggregating over all possible analytics sources
/// like projects and affiliate codes, returning the data in a list of time
/// slices.
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct GetRequest {
/// What time range to return statistics for.
pub time_range: TimeRange,
@@ -52,7 +53,7 @@ pub struct GetRequest {
}
/// Time range for fetching analytics.
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TimeRange {
/// When to start including data.
pub start: DateTime,
@@ -68,20 +69,22 @@ pub struct TimeRange {
/// Determines how many time slices between the start and end will be
/// included, and how fine-grained those time slices will be.
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum TimeRangeResolution {
/// Use a set number of time slices, with the resolution being determined
/// automatically.
+ #[schema(value_type = u64)]
Slices(NonZeroU64),
/// Each time slice will be a set number of minutes long, and the number of
/// slices is determined automatically.
+ #[schema(value_type = u64)]
Minutes(NonZeroU64),
}
/// What metrics the caller would like to receive from this analytics get
/// request.
-#[derive(Debug, Default, Serialize, Deserialize)]
+#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ReturnMetrics {
/// How many times a project page has been viewed.
pub project_views: Option>,
@@ -90,11 +93,15 @@ pub struct ReturnMetrics {
/// How long users have been playing a project.
pub project_playtime: Option>,
/// How much payout revenue a project has generated.
- pub project_revenue: Option>,
+ pub project_revenue: Option>,
}
+/// Replacement for `()` because of a `utoipa` limitation.
+#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
+pub struct Unit {}
+
/// See [`ReturnMetrics`].
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct Metrics {
/// When collecting metrics, what fields do we want to group the results by?
///
@@ -114,7 +121,9 @@ pub struct Metrics {
}
/// Fields for [`ReturnMetrics::project_views`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(
+ Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
+)]
#[serde(rename_all = "snake_case")]
pub enum ProjectViewsField {
/// Project ID.
@@ -132,7 +141,9 @@ pub enum ProjectViewsField {
}
/// Fields for [`ReturnMetrics::project_downloads`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(
+ Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
+)]
#[serde(rename_all = "snake_case")]
pub enum ProjectDownloadsField {
/// Project ID.
@@ -150,7 +161,9 @@ pub enum ProjectDownloadsField {
}
/// Fields for [`ReturnMetrics::project_playtime`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(
+ Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
+)]
#[serde(rename_all = "snake_case")]
pub enum ProjectPlaytimeField {
/// Project ID.
@@ -177,15 +190,15 @@ pub const MAX_TIME_SLICES: usize = 1024;
/// This is a list of N [`TimeSlice`]s, where each slice represents an equal
/// time interval of metrics collection. The number of slices is determined
/// by [`GetRequest::time_range`].
-#[derive(Debug, Default, Serialize, Deserialize)]
-pub struct GetResponse(pub Vec);
+#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
+pub struct FetchResponse(pub Vec);
/// Single time interval of metrics collection.
-#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TimeSlice(pub Vec);
/// Metrics collected in a single [`TimeSlice`].
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(untagged)] // the presence of `source_project`, `source_affiliate_code` determines the kind
pub enum AnalyticsData {
/// Project metrics.
@@ -194,7 +207,7 @@ pub enum AnalyticsData {
}
/// Project metrics.
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectAnalytics {
/// What project these metrics are for.
source_project: ProjectId,
@@ -213,7 +226,7 @@ impl ProjectAnalytics {
/// Project metrics of a specific kind.
///
/// If a field is not included in [`Metrics::bucket_by`], it will be [`None`].
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case", tag = "metric_kind")]
pub enum ProjectMetrics {
/// [`ReturnMetrics::project_views`].
@@ -227,7 +240,7 @@ pub enum ProjectMetrics {
}
/// [`ReturnMetrics::project_views`].
-#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectViews {
/// [`ProjectViewsField::Domain`].
#[serde(skip_serializing_if = "Option::is_none")]
@@ -246,7 +259,7 @@ pub struct ProjectViews {
}
/// [`ReturnMetrics::project_downloads`].
-#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectDownloads {
/// [`ProjectDownloadsField::Domain`].
#[serde(skip_serializing_if = "Option::is_none")]
@@ -265,7 +278,7 @@ pub struct ProjectDownloads {
}
/// [`ReturnMetrics::project_playtime`].
-#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectPlaytime {
/// [`ProjectPlaytimeField::VersionId`].
#[serde(skip_serializing_if = "Option::is_none")]
@@ -281,7 +294,7 @@ pub struct ProjectPlaytime {
}
/// [`ReturnMetrics::project_revenue`].
-#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectRevenue {
/// Total revenue for this bucket.
revenue: Decimal,
@@ -414,14 +427,19 @@ mod query {
};
}
-pub async fn get(
+/// Fetches analytics data for the authorized user's projects.
+#[utoipa::path(
+ responses((status = OK, body = inline(FetchResponse))),
+)]
+#[post("")]
+pub async fn fetch_analytics(
http_req: HttpRequest,
req: web::Json,
pool: web::Data,
redis: web::Data,
session_queue: web::Data,
clickhouse: web::Data,
-) -> Result, ApiError> {
+) -> Result, ApiError> {
let (scopes, user) = get_user_from_headers(
&http_req,
&**pool,
@@ -655,7 +673,7 @@ pub async fn get(
}
}
- Ok(web::Json(GetResponse(time_slices)))
+ Ok(web::Json(FetchResponse(time_slices)))
}
fn none_if_empty(s: String) -> Option {
@@ -824,7 +842,7 @@ mod tests {
let test_project_2 = ProjectId(456);
let test_project_3 = ProjectId(789);
- let src = GetResponse(vec![
+ let src = FetchResponse(vec![
TimeSlice(vec![
AnalyticsData::Project(ProjectAnalytics {
source_project: test_project_1,
diff --git a/apps/labrinth/src/routes/v3/analytics_get_old.rs b/apps/labrinth/src/routes/v3/analytics_get/old.rs
similarity index 91%
rename from apps/labrinth/src/routes/v3/analytics_get_old.rs
rename to apps/labrinth/src/routes/v3/analytics_get/old.rs
index 0d50014b..79c5330c 100644
--- a/apps/labrinth/src/routes/v3/analytics_get_old.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/old.rs
@@ -7,13 +7,10 @@ use crate::models::teams::ProjectPermissions;
use crate::{
auth::get_user_from_headers,
database::models::user_item,
- models::{
- ids::{ProjectId, VersionId},
- pats::Scopes,
- },
+ models::{ids::ProjectId, pats::Scopes},
queue::session::AuthQueue,
};
-use actix_web::{HttpRequest, HttpResponse, web};
+use actix_web::{HttpRequest, HttpResponse, get, web};
use ariadne::ids::base62_impl::to_base62;
use chrono::{DateTime, Duration, Utc};
use eyre::eyre;
@@ -24,28 +21,21 @@ use std::collections::HashMap;
use std::convert::TryInto;
use std::num::NonZeroU32;
-pub fn config(cfg: &mut web::ServiceConfig) {
- cfg.service(
- web::scope("analytics")
- // TODO: since our service shadows analytics v2, we have to redirect here
- .route("", web::post().to(super::analytics_get::get))
- .route("playtime", web::get().to(playtimes_get))
- .route("views", web::get().to(views_get))
- .route("downloads", web::get().to(downloads_get))
- .route("revenue", web::get().to(revenue_get))
- .route(
- "countries/downloads",
- web::get().to(countries_downloads_get),
- )
- .route("countries/views", web::get().to(countries_views_get)),
- );
+pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
+ cfg.service(playtimes_get)
+ .service(views_get)
+ .service(downloads_get)
+ .service(revenue_get)
+ .service(countries_downloads_get)
+ .service(countries_views_get);
}
-/// The json data to be passed to fetch analytic data
+/// The json data to be passed to fetch analytic data.
+///
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// start_date and end_date are optional, and default to two weeks ago, and the maximum date respectively.
/// resolution_minutes is optional. This refers to the window by which we are looking (every day, every minute, etc) and defaults to 1440 (1 day)
-#[derive(Serialize, Deserialize, Clone, Debug)]
+#[derive(Serialize, Deserialize, Clone, Debug, utoipa::ToSchema)]
pub struct GetData {
// only one of project_ids or version_ids should be used
// if neither are provided, all projects the user has access to will be used
@@ -54,26 +44,12 @@ pub struct GetData {
pub start_date: Option>, // defaults to 2 weeks ago
pub end_date: Option>, // defaults to now
+ #[schema(value_type = Option, minimum = 1)]
pub resolution_minutes: Option, // defaults to 1 day. Ignored in routes that do not aggregate over a resolution (eg: /countries)
}
-/// Get playtime data for a set of projects or versions
-/// Data is returned as a hashmap of project/version ids to a hashmap of days to playtime data
-/// eg:
-/// {
-/// "4N1tEhnO": {
-/// "20230824": 23
-/// }
-///}
-/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
-#[derive(Serialize, Deserialize, Clone)]
-pub struct FetchedPlaytime {
- pub time: u64,
- pub total_seconds: u64,
- pub loader_seconds: HashMap,
- pub game_version_seconds: HashMap,
- pub parent_seconds: HashMap,
-}
+#[utoipa::path]
+#[get("/playtime")]
pub async fn playtimes_get(
req: HttpRequest,
clickhouse: web::Data,
@@ -134,7 +110,8 @@ pub async fn playtimes_get(
Ok(HttpResponse::Ok().json(hm))
}
-/// Get view data for a set of projects or versions
+/// Get view data for a set of projects or versions.
+///
/// Data is returned as a hashmap of project/version ids to a hashmap of days to views
/// eg:
/// {
@@ -143,6 +120,8 @@ pub async fn playtimes_get(
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
+#[utoipa::path]
+#[get("/views")]
pub async fn views_get(
req: HttpRequest,
clickhouse: web::Data,
@@ -203,7 +182,8 @@ pub async fn views_get(
Ok(HttpResponse::Ok().json(hm))
}
-/// Get download data for a set of projects or versions
+/// Get download data for a set of projects or versions.
+///
/// Data is returned as a hashmap of project/version ids to a hashmap of days to downloads
/// eg:
/// {
@@ -212,6 +192,8 @@ pub async fn views_get(
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
+#[utoipa::path]
+#[get("/downloads")]
pub async fn downloads_get(
req: HttpRequest,
clickhouse: web::Data,
@@ -273,7 +255,8 @@ pub async fn downloads_get(
Ok(HttpResponse::Ok().json(hm))
}
-/// Get payout data for a set of projects
+/// Get payout data for a set of projects.
+///
/// Data is returned as a hashmap of project ids to a hashmap of days to amount earned per day
/// eg:
/// {
@@ -282,6 +265,8 @@ pub async fn downloads_get(
/// }
///}
/// ONLY project IDs can be used. Unauthorized projects will be filtered out.
+#[utoipa::path]
+#[get("/revenue")]
pub async fn revenue_get(
req: HttpRequest,
data: web::Query,
@@ -409,7 +394,8 @@ pub async fn revenue_get(
Ok(HttpResponse::Ok().json(hm))
}
-/// Get country data for a set of projects or versions
+/// Get country data for a set of projects or versions.
+///
/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to downloads.
/// Unknown countries are labeled "".
/// This is usable to see significant performing countries per project
@@ -421,6 +407,8 @@ pub async fn revenue_get(
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch
+#[utoipa::path]
+#[get("/countries/downloads")]
pub async fn countries_downloads_get(
req: HttpRequest,
clickhouse: web::Data,
@@ -482,7 +470,8 @@ pub async fn countries_downloads_get(
Ok(HttpResponse::Ok().json(hm))
}
-/// Get country data for a set of projects or versions
+/// Get country data for a set of projects or versions.
+///
/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to views.
/// Unknown countries are labeled "".
/// This is usable to see significant performing countries per project
@@ -494,6 +483,8 @@ pub async fn countries_downloads_get(
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch
+#[utoipa::path]
+#[get("/countries/views")]
pub async fn countries_views_get(
req: HttpRequest,
clickhouse: web::Data,
diff --git a/apps/labrinth/src/routes/v3/mod.rs b/apps/labrinth/src/routes/v3/mod.rs
index 7a989d81..4e4c1aac 100644
--- a/apps/labrinth/src/routes/v3/mod.rs
+++ b/apps/labrinth/src/routes/v3/mod.rs
@@ -4,7 +4,6 @@ use actix_web::{HttpResponse, web};
use serde_json::json;
pub mod analytics_get;
-pub mod analytics_get_old;
pub mod collections;
pub mod friends;
pub mod images;
@@ -33,8 +32,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
web::scope("v3")
.wrap(default_cors())
.configure(limits::config)
- // .configure(analytics_get::config) // TODO: see `analytics_get`
- .configure(analytics_get_old::config)
.configure(collections::config)
.configure(images::config)
.configure(notifications::config)
@@ -56,6 +53,15 @@ pub fn config(cfg: &mut web::ServiceConfig) {
);
}
+pub fn utoipa_config(
+ cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
+) {
+ cfg.service(
+ utoipa_actix_web::scope("/v3/analytics")
+ .configure(analytics_get::config),
+ );
+}
+
pub async fn hello_world() -> Result {
Ok(HttpResponse::Ok().json(json!({
"hello": "world",
diff --git a/apps/labrinth/tests/common/api_v2/mod.rs b/apps/labrinth/tests/common/api_v2/mod.rs
index 20d0e6e3..e197c4b7 100644
--- a/apps/labrinth/tests/common/api_v2/mod.rs
+++ b/apps/labrinth/tests/common/api_v2/mod.rs
@@ -6,6 +6,7 @@ use actix_web::{App, dev::ServiceResponse, test};
use async_trait::async_trait;
use labrinth::LabrinthConfig;
use std::rc::Rc;
+use utoipa_actix_web::AppExt;
pub mod project;
pub mod request_data;
@@ -22,9 +23,15 @@ pub struct ApiV2 {
#[async_trait(?Send)]
impl ApiBuildable for ApiV2 {
async fn build(labrinth_config: LabrinthConfig) -> Self {
- let app = App::new().configure(|cfg| {
- labrinth::app_config(cfg, labrinth_config.clone())
- });
+ let app = App::new()
+ .into_utoipa_app()
+ .configure(|cfg| {
+ labrinth::utoipa_app_config(cfg, labrinth_config.clone())
+ })
+ .into_app()
+ .configure(|cfg| {
+ labrinth::app_config(cfg, labrinth_config.clone())
+ });
let test_app: Rc =
Rc::new(test::init_service(app).await);
diff --git a/apps/labrinth/tests/common/api_v3/mod.rs b/apps/labrinth/tests/common/api_v3/mod.rs
index e556d040..1f28896d 100644
--- a/apps/labrinth/tests/common/api_v3/mod.rs
+++ b/apps/labrinth/tests/common/api_v3/mod.rs
@@ -6,6 +6,7 @@ use actix_web::{App, dev::ServiceResponse, test};
use async_trait::async_trait;
use labrinth::LabrinthConfig;
use std::rc::Rc;
+use utoipa_actix_web::AppExt;
pub mod collections;
pub mod limits;
@@ -27,9 +28,15 @@ pub struct ApiV3 {
#[async_trait(?Send)]
impl ApiBuildable for ApiV3 {
async fn build(labrinth_config: LabrinthConfig) -> Self {
- let app = App::new().configure(|cfg| {
- labrinth::app_config(cfg, labrinth_config.clone())
- });
+ let app = App::new()
+ .into_utoipa_app()
+ .configure(|cfg| {
+ labrinth::utoipa_app_config(cfg, labrinth_config.clone())
+ })
+ .into_app()
+ .configure(|cfg| {
+ labrinth::app_config(cfg, labrinth_config.clone())
+ });
let test_app: Rc =
Rc::new(test::init_service(app).await);
diff --git a/packages/ariadne/Cargo.toml b/packages/ariadne/Cargo.toml
index 5b5dcd59..1cab28f9 100644
--- a/packages/ariadne/Cargo.toml
+++ b/packages/ariadne/Cargo.toml
@@ -12,6 +12,7 @@ serde_bytes = { workspace = true }
serde_cbor = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
+utoipa = { workspace = true }
uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
[lints]
diff --git a/packages/ariadne/src/ids.rs b/packages/ariadne/src/ids.rs
index 5b389c8f..6f91cecd 100644
--- a/packages/ariadne/src/ids.rs
+++ b/packages/ariadne/src/ids.rs
@@ -94,6 +94,7 @@ macro_rules! base62_id {
serde::Deserialize,
Debug,
Hash,
+ utoipa::ToSchema,
)]
#[serde(from = "ariadne::ids::Base62Id")]
#[serde(into = "ariadne::ids::Base62Id")]
From ab886a5ea89cdbd105a0b2e96f2ed78a119ab369 Mon Sep 17 00:00:00 2001
From: aecsocket
Date: Fri, 24 Oct 2025 11:27:44 -0700
Subject: [PATCH 33/53] Fix CORS (#4610)
---
apps/labrinth/src/routes/mod.rs | 8 ++++++--
apps/labrinth/src/routes/v3/mod.rs | 1 +
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs
index fad90bea..66a20a91 100644
--- a/apps/labrinth/src/routes/mod.rs
+++ b/apps/labrinth/src/routes/mod.rs
@@ -77,8 +77,12 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
}.boxed_local()
})
);
- cfg.service(index::index_get);
- cfg.service(Files::new("/", "assets/"));
+ cfg.service(
+ web::scope("")
+ .wrap(default_cors())
+ .service(index::index_get)
+ .service(Files::new("/", "assets/")),
+ );
}
#[derive(thiserror::Error, Debug)]
diff --git a/apps/labrinth/src/routes/v3/mod.rs b/apps/labrinth/src/routes/v3/mod.rs
index 4e4c1aac..96c54ce4 100644
--- a/apps/labrinth/src/routes/v3/mod.rs
+++ b/apps/labrinth/src/routes/v3/mod.rs
@@ -58,6 +58,7 @@ pub fn utoipa_config(
) {
cfg.service(
utoipa_actix_web::scope("/v3/analytics")
+ .wrap(default_cors())
.configure(analytics_get::config),
);
}
From 5dd6c804d09fb4f40217dbf3076fb00ff187d046 Mon Sep 17 00:00:00 2001
From: Prospector <6166773+Prospector@users.noreply.github.com>
Date: Fri, 24 Oct 2025 11:58:20 -0700
Subject: [PATCH 34/53] fix padding issues (#4604)
---
apps/app-frontend/src/App.vue | 12 +-
.../src/components/ui/friends/FriendsList.vue | 105 +++++++++++-------
.../components/ui/friends/FriendsSection.vue | 4 +-
apps/app-frontend/src/helpers/friends.ts | 4 +-
.../app-frontend/src/locales/en-US/index.json | 6 +
5 files changed, 86 insertions(+), 45 deletions(-)
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index 0fa6893f..e281beb6 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -990,7 +990,9 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>