mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cac5648c5 | ||
|
|
f0294e9485 | ||
|
|
b50738d5b0 | ||
|
|
7e7d0192c8 | ||
|
|
b26d048a63 | ||
|
|
25089c5252 | ||
|
|
c36a004ef0 | ||
|
|
4c114e1df7 | ||
|
|
01f4019db0 | ||
|
|
5543f9db7e | ||
|
|
9ecd757ddd | ||
|
|
a4615bfd7d | ||
|
|
d41b71293b | ||
|
|
78a5edd069 | ||
|
|
d28f791f17 | ||
|
|
4b4a3fa4c1 | ||
|
|
d5be2bd739 | ||
|
|
0dac5684af | ||
|
|
ca0b8d5ca4 | ||
|
|
ad65cd6505 | ||
|
|
3bea5ed19a | ||
|
|
f7ac372e76 | ||
|
|
f9bf83d299 | ||
|
|
b6651efe52 | ||
|
|
d4c6466a1d | ||
|
|
bd17856bde | ||
|
|
eb28121c0a | ||
|
|
a5ebc85356 | ||
|
|
c530671be1 | ||
|
|
bb6d8a6fc4 | ||
|
|
256e8f946a | ||
|
|
7f8c7f8b39 | ||
|
|
b0e9c560ba | ||
|
|
dd3f0ece6d | ||
|
|
d0e8edbc18 | ||
|
|
4f67775261 | ||
|
|
7e5590b8b9 | ||
|
|
d20f05fb04 | ||
|
|
a00a7971d1 | ||
|
|
26e98b7155 | ||
|
|
77b06f679e | ||
|
|
50e9472dd9 | ||
|
|
b569354793 | ||
|
|
b667b3ae9e | ||
|
|
16de6182e4 | ||
|
|
364fed52dd | ||
|
|
0fe695721a | ||
|
|
692f22b749 | ||
|
|
ed7792097d | ||
|
|
c56ce6c9ee | ||
|
|
91bc58b8ee | ||
|
|
38d757b66a | ||
|
|
862b54f00d | ||
|
|
c2f4835fe7 | ||
|
|
b97c191d75 |
@@ -76,13 +76,6 @@ jobs:
|
||||
env:
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
|
||||
@@ -78,14 +78,8 @@ jobs:
|
||||
GIT_HASH: ${{ github.sha }}
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
RUST_MIN_STACK: '16777216'
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
@@ -81,13 +81,6 @@ jobs:
|
||||
# blacksmith runner)
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
|
||||
steps:
|
||||
|
||||
Generated
+147
-64
@@ -59,9 +59,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.11.2"
|
||||
version = "3.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
|
||||
checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-rt",
|
||||
@@ -75,7 +75,7 @@ dependencies = [
|
||||
"derive_more 2.1.1",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"foldhash 0.1.5",
|
||||
"foldhash 0.2.0",
|
||||
"futures-core",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
@@ -87,8 +87,8 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"rand 0.10.1",
|
||||
"sha1 0.11.0",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -146,9 +146,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "actix-router"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
|
||||
checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7"
|
||||
dependencies = [
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
@@ -209,9 +209,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.11.0"
|
||||
version = "4.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea"
|
||||
checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
@@ -228,7 +228,7 @@ dependencies = [
|
||||
"cookie 0.16.2",
|
||||
"derive_more 2.1.1",
|
||||
"encoding_rs",
|
||||
"foldhash 0.1.5",
|
||||
"foldhash 0.2.0",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"impl-more",
|
||||
@@ -244,7 +244,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.1",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -1127,7 +1127,7 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"md-5",
|
||||
"pin-project-lite",
|
||||
"sha1",
|
||||
"sha1 0.10.6",
|
||||
"sha2",
|
||||
"tracing",
|
||||
]
|
||||
@@ -1499,7 +1499,7 @@ version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1511,6 +1511,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
@@ -2224,6 +2233,12 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
@@ -2424,7 +2439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d"
|
||||
dependencies = [
|
||||
"crc",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"rustversion",
|
||||
"spin 0.10.0",
|
||||
]
|
||||
@@ -2515,6 +2530,15 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser"
|
||||
version = "0.29.6"
|
||||
@@ -2791,7 +2815,7 @@ version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"const-oid 0.9.6",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -2904,12 +2928,23 @@ version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"block-buffer 0.10.4",
|
||||
"const-oid 0.9.6",
|
||||
"crypto-common 0.1.6",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.1",
|
||||
"const-oid 0.10.2",
|
||||
"crypto-common 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
@@ -3127,7 +3162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"elliptic-curve",
|
||||
"rfc6979",
|
||||
"signature",
|
||||
@@ -3151,7 +3186,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
@@ -4402,7 +4437,7 @@ version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4532,6 +4567,15 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
@@ -4893,9 +4937,9 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
|
||||
|
||||
[[package]]
|
||||
name = "impl-more"
|
||||
version = "0.1.9"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
|
||||
checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870"
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
@@ -5390,11 +5434,12 @@ dependencies = [
|
||||
"rust_iso3166",
|
||||
"rustls 0.23.32",
|
||||
"rusty-money",
|
||||
"scalar_api_reference",
|
||||
"sentry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"sha1",
|
||||
"sha1 0.10.6",
|
||||
"sha2",
|
||||
"spdx",
|
||||
"sqlx",
|
||||
@@ -5411,8 +5456,6 @@ dependencies = [
|
||||
"url",
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
"utoipa-actix-web",
|
||||
"utoipa-scalar",
|
||||
"uuid 1.23.3",
|
||||
"validator",
|
||||
"webauthn-rs",
|
||||
@@ -5822,7 +5865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8494,8 +8537,8 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
"const-oid 0.9.6",
|
||||
"digest 0.10.7",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
@@ -8508,6 +8551,40 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.106",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
@@ -8827,6 +8904,18 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scalar_api_reference"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d67c85edd56c41f364603eff688a330f01dee99bdfc84c005d3bc3d0721d6b2"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.4.0"
|
||||
@@ -9252,15 +9341,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9455,7 +9544,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9472,7 +9572,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9505,7 +9605,7 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
@@ -9782,7 +9882,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"dotenvy",
|
||||
"either",
|
||||
"futures-channel",
|
||||
@@ -9803,7 +9903,7 @@ dependencies = [
|
||||
"rsa",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"sha1",
|
||||
"sha1 0.10.6",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
@@ -11290,7 +11390,7 @@ dependencies = [
|
||||
"constant_time_eq",
|
||||
"hmac",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"sha1 0.10.6",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
@@ -11505,7 +11605,7 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
"rustls 0.23.32",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"sha1 0.10.6",
|
||||
"thiserror 2.0.17",
|
||||
"utf-8",
|
||||
]
|
||||
@@ -11524,9 +11624,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
@@ -11757,17 +11857,6 @@ dependencies = [
|
||||
"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"
|
||||
@@ -11782,18 +11871,6 @@ dependencies = [
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-scalar"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
@@ -13450,6 +13527,12 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.2"
|
||||
|
||||
+1
-2
@@ -158,6 +158,7 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [
|
||||
] }
|
||||
rustls = "0.23.32"
|
||||
rusty-money = "0.4.1"
|
||||
scalar_api_reference = { version = "0.2.2", default-features = false }
|
||||
secrecy = "0.10.3"
|
||||
sentry = { version = "0.45.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
@@ -217,8 +218,6 @@ 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-scalar = { version = "0.3.0", default-features = false }
|
||||
uuid = "1.18.1"
|
||||
validator = "0.20.0"
|
||||
webauthn-rs = "0.5.5"
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<script setup>
|
||||
import { WrenchIcon, XIcon } from '@modrinth/assets'
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
FileTreeSelect,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { readDir, stat } from '@tauri-apps/plugin-fs'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { export_instance_mrpack, get_pack_export_candidates } from '@/helpers/instance'
|
||||
import { PackageIcon } from '@/assets/icons'
|
||||
import {
|
||||
export_instance_mrpack,
|
||||
get_full_path,
|
||||
get_pack_export_candidates,
|
||||
} from '@/helpers/instance'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
header: { id: 'app.export-modal.header', defaultMessage: 'Export modpack' },
|
||||
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack Name' },
|
||||
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack name' },
|
||||
modpackNamePlaceholder: {
|
||||
id: 'app.export-modal.modpack-name-placeholder',
|
||||
defaultMessage: 'Modpack name',
|
||||
@@ -39,15 +43,7 @@ const messages = defineMessages({
|
||||
id: 'app.export-modal.description-placeholder',
|
||||
defaultMessage: 'Enter modpack description...',
|
||||
},
|
||||
selectFilesLabel: {
|
||||
id: 'app.export-modal.select-files-label',
|
||||
defaultMessage: 'Configure which files are included in this export',
|
||||
},
|
||||
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
|
||||
includeFile: {
|
||||
id: 'app.export-modal.include-file-accessibility-label',
|
||||
defaultMessage: 'Include "{file}"?',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -59,8 +55,9 @@ const props = defineProps({
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
resetExportState()
|
||||
exportModal.value.show()
|
||||
initFiles()
|
||||
void initFiles().catch(handleError)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -69,62 +66,33 @@ const nameInput = ref(props.instance.name)
|
||||
const exportDescription = ref('')
|
||||
const versionInput = ref('1.0.0')
|
||||
const files = ref([])
|
||||
const folders = ref([])
|
||||
const selectedFilePaths = ref([])
|
||||
const fileTreeKey = ref(0)
|
||||
const filesLoadId = ref(0)
|
||||
const instanceRoot = ref('')
|
||||
const loadedDirectories = ref(new Set())
|
||||
|
||||
const initFiles = async () => {
|
||||
const newFolders = new Map()
|
||||
const sep = '/'
|
||||
files.value = []
|
||||
await get_pack_export_candidates(props.instance.id).then((filePaths) =>
|
||||
filePaths
|
||||
.map((folder) => ({
|
||||
path: folder,
|
||||
name: folder.split(sep).pop(),
|
||||
selected:
|
||||
folder.startsWith('mods') ||
|
||||
folder.startsWith('datapacks') ||
|
||||
folder.startsWith('resourcepacks') ||
|
||||
folder.startsWith('shaderpacks') ||
|
||||
folder.startsWith('config'),
|
||||
disabled:
|
||||
folder === 'profile.json' ||
|
||||
folder.startsWith('modrinth_logs') ||
|
||||
folder.startsWith('.fabric') ||
|
||||
folder.startsWith('__MACOSX'),
|
||||
}))
|
||||
.forEach((pathData) => {
|
||||
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
||||
if (parent !== '') {
|
||||
if (newFolders.has(parent)) {
|
||||
newFolders.get(parent).push(pathData)
|
||||
} else {
|
||||
newFolders.set(parent, [pathData])
|
||||
}
|
||||
} else {
|
||||
files.value.push(pathData)
|
||||
}
|
||||
}),
|
||||
)
|
||||
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
||||
{
|
||||
name,
|
||||
showingMore: false,
|
||||
},
|
||||
value,
|
||||
async function initFiles() {
|
||||
const loadId = ++filesLoadId.value
|
||||
const [filePaths, root] = await Promise.all([
|
||||
get_pack_export_candidates(props.instance.id),
|
||||
get_full_path(props.instance.id),
|
||||
])
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
instanceRoot.value = root
|
||||
const exportCandidates = await Promise.all(
|
||||
filePaths.map((path) => buildExportCandidateItem(root, path)),
|
||||
)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
files.value = exportCandidates
|
||||
selectedFilePaths.value = files.value
|
||||
.filter((file) => !file.disabled && isDefaultSelectedExportCandidate(file.path))
|
||||
.map((file) => file.path)
|
||||
}
|
||||
|
||||
await initFiles()
|
||||
|
||||
const exportPack = async () => {
|
||||
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
||||
folders.value.forEach((args) => {
|
||||
args[1].forEach((child) => {
|
||||
if (child.selected) {
|
||||
filesToExport.push(child.path)
|
||||
}
|
||||
})
|
||||
})
|
||||
const outputPath = await save({
|
||||
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
filters: [
|
||||
@@ -139,7 +107,7 @@ const exportPack = async () => {
|
||||
export_instance_mrpack(
|
||||
props.instance.id,
|
||||
outputPath,
|
||||
filesToExport,
|
||||
selectedFilePaths.value,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value,
|
||||
@@ -147,94 +115,176 @@ const exportPack = async () => {
|
||||
exportModal.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
function resetExportState() {
|
||||
nameInput.value = props.instance.name
|
||||
exportDescription.value = ''
|
||||
versionInput.value = '1.0.0'
|
||||
files.value = []
|
||||
selectedFilePaths.value = []
|
||||
fileTreeKey.value += 1
|
||||
instanceRoot.value = ''
|
||||
loadedDirectories.value = new Set()
|
||||
}
|
||||
|
||||
async function loadExportDirectory(path) {
|
||||
if (!path || !instanceRoot.value || loadedDirectories.value.has(path)) return
|
||||
|
||||
const loadId = filesLoadId.value
|
||||
loadedDirectories.value.add(path)
|
||||
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot.value}/${path}`)
|
||||
const childItems = await Promise.all(
|
||||
entries.map((entry) => buildExportDirectoryChildItem(instanceRoot.value, path, entry)),
|
||||
)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
appendExportItems(childItems)
|
||||
} catch {
|
||||
loadedDirectories.value.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportCandidateItem(instanceRoot, path) {
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot}/${path}`)
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
count: entries.length,
|
||||
}
|
||||
} catch {
|
||||
return buildExportFileItem(instanceRoot, path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportDirectoryChildItem(instanceRoot, parentPath, entry) {
|
||||
const path = `${parentPath}/${entry.name}`
|
||||
if (entry.isDirectory) {
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
}
|
||||
}
|
||||
|
||||
return buildExportFileItem(instanceRoot, path)
|
||||
}
|
||||
|
||||
async function buildExportFileItem(instanceRoot, path) {
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'file',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
size: metadata.size,
|
||||
modified: metadata.modified,
|
||||
}
|
||||
}
|
||||
|
||||
function appendExportItems(items) {
|
||||
const nextFiles = new Map(files.value.map((file) => [normalizeExportPath(file.path), file]))
|
||||
for (const item of items) {
|
||||
nextFiles.set(normalizeExportPath(item.path), item)
|
||||
}
|
||||
files.value = [...nextFiles.values()]
|
||||
}
|
||||
|
||||
async function getExportCandidateMetadata(instanceRoot, path) {
|
||||
try {
|
||||
const metadata = await stat(`${instanceRoot}/${path}`)
|
||||
return {
|
||||
size: metadata.size,
|
||||
modified: metadata.mtime ? Math.floor(metadata.mtime.getTime() / 1000) : undefined,
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExportPath(path) {
|
||||
return path.replaceAll('\\', '/').split('/').filter(Boolean).join('/')
|
||||
}
|
||||
|
||||
function isDefaultSelectedExportCandidate(path) {
|
||||
return (
|
||||
path.startsWith('mods') ||
|
||||
path.startsWith('datapacks') ||
|
||||
path.startsWith('resourcepacks') ||
|
||||
path.startsWith('shaderpacks') ||
|
||||
path.startsWith('config')
|
||||
)
|
||||
}
|
||||
|
||||
function isExportCandidateDisabled(path) {
|
||||
return (
|
||||
path === 'profile.json' ||
|
||||
path.startsWith('modrinth_logs') ||
|
||||
path.startsWith('.fabric') ||
|
||||
path.startsWith('__MACOSX')
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
|
||||
<div class="flex flex-col gap-4 w-[40rem]">
|
||||
<NewModal
|
||||
ref="exportModal"
|
||||
:header="formatMessage(messages.header)"
|
||||
scrollable
|
||||
width="46rem"
|
||||
max-width="calc(100vw - 2rem)"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<div class="labeled_input w-full">
|
||||
<p class="text-contrast font-semibold">{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="nameInput"
|
||||
:icon="PackageIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
|
||||
<div class="labeled_input w-full">
|
||||
<p class="text-contrast font-semibold">
|
||||
{{ formatMessage(messages.versionNumberLabel) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
v-model="versionInput"
|
||||
:icon="VersionIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
|
||||
<div class="flex flex-col gap-2 min-w-0">
|
||||
<p class="m-0 text-contrast font-semibold">
|
||||
{{ formatMessage(commonMessages.descriptionLabel) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
v-model="exportDescription"
|
||||
multiline
|
||||
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Accordion
|
||||
class="w-full bg-surface-4 border border-solid border-surface-5 rounded-2xl overflow-clip"
|
||||
button-class="p-4 w-full border-b border-solid border-b-surface-5 bg-surface-2 -mb-px hover:brightness-[--hover-brightness] group"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-3 text-contrast group-active:scale-[0.98]">
|
||||
<WrenchIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
Configure which files are included in this export
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col [&>*:nth-child(even)]:bg-surface-3">
|
||||
<div v-for="[path, children] in folders" :key="path.name" class="flex flex-col">
|
||||
<Accordion
|
||||
class="flex flex-col"
|
||||
button-class="flex gap-3 pr-4 hover:bg-surface-5 group"
|
||||
>
|
||||
<template #title>
|
||||
<Checkbox
|
||||
:model-value="children.every((child) => child.selected)"
|
||||
:indeterminate="
|
||||
!children.every((child) => child.selected) &&
|
||||
children.some((child) => child.selected)
|
||||
"
|
||||
:description="formatMessage(messages.includeFile, { file: path.name })"
|
||||
class="pl-4 py-2"
|
||||
:disabled="children.every((x) => x.disabled)"
|
||||
@update:model-value="
|
||||
(newValue) => children.forEach((child) => (child.selected = newValue))
|
||||
"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
|
||||
</template>
|
||||
<div v-for="child in children" :key="child.path">
|
||||
<Checkbox
|
||||
v-model="child.selected"
|
||||
:label="child.name"
|
||||
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
|
||||
:disabled="child.disabled"
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
<Checkbox
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
v-model="file.selected"
|
||||
:label="file.name"
|
||||
:disabled="file.disabled"
|
||||
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
<FileTreeSelect
|
||||
:key="fileTreeKey"
|
||||
v-model="selectedFilePaths"
|
||||
class="min-w-0"
|
||||
:items="files"
|
||||
@navigate="loadExportDirectory"
|
||||
/>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="exportModal.hide">
|
||||
@@ -249,6 +299,6 @@ const exportPack = async () => {
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { MailIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { MailIcon, SearchIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
@@ -301,11 +301,12 @@ const messages = defineMessages({
|
||||
</ButtonStyled>
|
||||
<StyledInput
|
||||
v-model="search"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchFriends)"
|
||||
clearable
|
||||
variant="outlined"
|
||||
wrapper-class="flex-1"
|
||||
input-class="!bg-transparent !border !border-solid !border-button-bg !text-primary !placeholder:text-primary"
|
||||
wrapper-class="flex-1 [&>svg]:!text-primary [&>svg]:!opacity-100"
|
||||
@keyup.esc="search = ''"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "إسم حزمة التعديل"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "أختيار الملفات التي سيتم تصديرها"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "رقم الإصدار"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportovat modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Zahrnout \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Název modpacku"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Název modpacku"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Číslo verze"
|
||||
},
|
||||
|
||||
@@ -164,18 +164,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Eksporter modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inkludere \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Navn"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack navn"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurer hvilke filer inkludere i denne eksport"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Modpack exportieren"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\" einschliessen?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpaketname"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpaketname"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Modpack exportieren"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\" einschließen?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpackname"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpackname"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Export modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Include \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Name"
|
||||
"message": "Modpack name"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack name"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configure which files are included in this export"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Version number"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportar modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "¿Incluir \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configura que archivos incluir en esta exportación"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número de la versión"
|
||||
},
|
||||
|
||||
@@ -182,18 +182,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportar modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "¿Incluir \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configura que archivos se incluirán en esta instancia"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número de versión"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Vie modipaketti"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Sisällytä \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Muuta mitkä tiedostot sisältyvät tähän vientiin"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versio numero"
|
||||
},
|
||||
|
||||
@@ -164,18 +164,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Iluwas ang modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Salihin ang \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Pangalan ng Modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Pangalan ng modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numero ng bersiyon"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exporter le modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inclure « {file} » ?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nom du modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nom du modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configurez quels fichiers sont inclus dans cette exportation"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numéro de version"
|
||||
},
|
||||
|
||||
@@ -176,18 +176,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Modcsomag exportálása"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Tartalmaz „{file}” fájlokat?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "A modcsomag neve"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "A modcsomag neve"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Állítsd be, hogy mely fájlok kerüljenek bele az exportba"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Verziószám"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Ekspor paket mod"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Sertakan \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nama Paket Mod"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nama paket mod"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurasikan berkas mana yang disertakan dalam pengeksporan ini"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Nomor versi"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Esporta pacchetto"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Includere \"{file}\" nell'esportazione?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nome del pacchetto"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nome del pacchetto"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Seleziona i file da includere in questa esportazione"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numero di versione"
|
||||
},
|
||||
|
||||
@@ -164,18 +164,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "모드팩 내보내기"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\"(을)를 포함할까요?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "모드팩 이름"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "모드팩 이름"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "내보내기에 어느 파일이 포함될지 구성"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "버전 구분"
|
||||
},
|
||||
|
||||
@@ -155,18 +155,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Eksport pek mod"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Sertakan \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nama Pek Mod"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nama pek mod"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurasikan fail yang disertakan dalam eksport ini"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Nombor versi"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exporteer modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Betrek \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Naam"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack naam"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configureer welke bestanden bij deze export zijn betrokken"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versie nummer"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Eksportuj paczkę modów"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Dołączyć \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nazwa paczki modów"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nazwa paczki modów"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Skonfiguruje jakie pliki zostaną załączone w tym eksporcie"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numer wersji"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportar pacote de mods"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Incluir \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nome do pacote de mods"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nome do pacote de mods"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configure quais arquivos serão incluídos na exportação"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número da versão"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Экспорт сборки"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Добавить «{file}» в экспорт?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Название сборки"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Название сборки"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Выберите файлы для экспорта"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Номер версии"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportera modpaket"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inkludera \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpaketets namn"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpaketets namn"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurera vilka filer som inkluderas i denna export"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "ส่งออกแพ็กม็อด"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "รวมถึง \"{file}\" ใช่หรือไม่"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "ชื่อแพ็กม็อด"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "ชื่อแพ็กม็อด"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "กำหนดไฟล์ที่ต้องการจะส่งออก"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "หมายเลขเวอร์ชัน"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Mod paketini dışa aktar"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\" dahil mi?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Mod Paketi Adı"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Mod paketi adı"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Bu dışa aktarmaya hangi dosyaların dahil edileceğini yapılandırma"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Sürüm numarası"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Експортувати збірку"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Уключити «{file}»?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Назва збірки"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Назва збірки"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Змініть файли, які додані до експорту"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Номер версії"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Xuất modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Bao gồm \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Tên modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Tên modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Cấu hình các tệp nào được bao gồm trong quá trình xuất phiên bản này"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Phiên bản"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "导出整合包"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "包含 \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "整合包名称"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "整合包名称"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "配置此导出中包含哪些文件"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "版本号"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "匯出模組包"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "要包含「{file}」嗎?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "模組包名稱"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "模組包名稱"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "設定要包含在此匯出檔案中的檔案"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "版本號碼"
|
||||
},
|
||||
|
||||
@@ -676,7 +676,7 @@ const handleOptionsClick = async (args: { option: string; item: unknown }) => {
|
||||
break
|
||||
case 'copy_path': {
|
||||
if (instance.value) {
|
||||
const fullPath = await get_full_path(instance.value?.path)
|
||||
const fullPath = await get_full_path(instance.value.id)
|
||||
await navigator.clipboard.writeText(fullPath)
|
||||
}
|
||||
break
|
||||
|
||||
@@ -1443,7 +1443,7 @@ onMounted(() => {
|
||||
props.instance &&
|
||||
event.instance_id === props.instance.id &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing' &&
|
||||
props.instance.install_stage === 'installed' &&
|
||||
!isBulkOperating.value
|
||||
) {
|
||||
await initProjects()
|
||||
|
||||
@@ -414,7 +414,7 @@ components:
|
||||
- unknown
|
||||
- signature
|
||||
example: required-resource-pack
|
||||
# https://github.com/modrinth/labrinth/blob/master/src/routes/version_creation.rs#L27-L57
|
||||
# https://github.com/modrinth/code/blob/main/apps/labrinth/src/routes/v2/version_creation.rs#L32-L76
|
||||
CreatableVersion:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseVersion'
|
||||
@@ -446,6 +446,11 @@ components:
|
||||
- client_or_server_prefers_both
|
||||
- unknown
|
||||
description: The environment that this version is for.
|
||||
file_types:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/FileTypeEnum'
|
||||
description: A map of file parts to their associated file type, a file type is used for additional files such as sources jars.
|
||||
required:
|
||||
- file_parts
|
||||
- project_id
|
||||
|
||||
+30
-30
@@ -41,38 +41,38 @@ export const VISIBLE_PROJECT_STATUS_CHANGE_EVENT_STATUS_SET =
|
||||
|
||||
export const LIGHT_LEGEND_PALETTE = [
|
||||
'hsl(152, 100%, 34%)',
|
||||
'hsl(26, 100%, 42%)',
|
||||
'hsl(202, 100%, 35%)',
|
||||
'hsl(327, 45%, 64%)',
|
||||
'hsl(41, 100%, 45%)',
|
||||
'hsl(250, 60%, 33%)',
|
||||
'hsl(170, 43%, 47%)',
|
||||
'hsl(330, 60%, 33%)',
|
||||
'hsl(46, 100%, 36%)',
|
||||
'hsl(167, 100%, 30%)',
|
||||
'hsl(343, 38%, 45%)',
|
||||
'hsl(222, 100%, 28%)',
|
||||
'hsl(270, 62%, 60%)',
|
||||
'hsl(32, 100%, 37%)',
|
||||
'hsl(349, 57%, 51%)',
|
||||
'hsl(191, 43%, 37%)',
|
||||
'hsl(41, 79%, 46%)',
|
||||
'hsl(203, 76%, 64%)',
|
||||
'hsl(0, 93%, 62%)',
|
||||
'hsl(143, 66%, 29%)',
|
||||
'hsl(58, 89%, 25%)',
|
||||
'hsl(311, 64%, 49%)',
|
||||
'hsl(198, 91%, 32%)',
|
||||
'hsl(12, 88%, 27%)',
|
||||
'hsl(236, 61%, 60%)',
|
||||
'hsl(102, 59%, 74%)',
|
||||
'hsl(293, 76%, 79%)',
|
||||
'hsl(67, 99%, 41%)',
|
||||
'hsl(179, 100%, 50%)',
|
||||
'hsl(102, 100%, 61%)',
|
||||
'hsl(0, 100%, 32%)',
|
||||
]
|
||||
|
||||
export const DARK_LEGEND_PALETTE = [
|
||||
'hsl(145, 78%, 48%)',
|
||||
'hsl(41, 100%, 50%)',
|
||||
'hsl(202, 77%, 63%)',
|
||||
'hsl(323, 66%, 72%)',
|
||||
'hsl(56, 85%, 60%)',
|
||||
'hsl(255, 92%, 80%)',
|
||||
'hsl(12, 100%, 67%)',
|
||||
'hsl(176, 58%, 56%)',
|
||||
'hsl(60, 100%, 41%)',
|
||||
'hsl(165, 80%, 38%)',
|
||||
'hsl(341, 36%, 56%)',
|
||||
'hsl(226, 60%, 49%)',
|
||||
'hsl(252, 53%, 62%)',
|
||||
'hsl(75, 59%, 50%)',
|
||||
'hsl(195, 56%, 42%)',
|
||||
'hsl(30, 59%, 56%)',
|
||||
'hsl(41, 79%, 46%)',
|
||||
'hsl(203, 76%, 64%)',
|
||||
'hsl(0, 93%, 62%)',
|
||||
'hsl(143, 66%, 29%)',
|
||||
'hsl(58, 94%, 45%)',
|
||||
'hsl(311, 64%, 49%)',
|
||||
'hsl(198, 91%, 32%)',
|
||||
'hsl(12, 88%, 27%)',
|
||||
'hsl(236, 61%, 60%)',
|
||||
'hsl(102, 59%, 74%)',
|
||||
'hsl(293, 76%, 79%)',
|
||||
'hsl(61, 92%, 33%)',
|
||||
'hsl(179, 100%, 50%)',
|
||||
'hsl(102, 100%, 61%)',
|
||||
'hsl(0, 100%, 32%)',
|
||||
]
|
||||
|
||||
+45
-14
@@ -288,6 +288,7 @@ type PaletteRankEntry = {
|
||||
key: string
|
||||
label: string
|
||||
total: number
|
||||
excludedFromRank?: boolean
|
||||
}
|
||||
|
||||
function formatDatasetTooltip(projectName: string | undefined): string | undefined {
|
||||
@@ -330,9 +331,12 @@ function buildPaletteColorsByDownloadRank(
|
||||
const colorsByKey = new Map<string, string>()
|
||||
if (palette.length === 0) return colorsByKey
|
||||
|
||||
const sortedEntries = [...entries].sort(
|
||||
(a, b) => b.total - a.total || a.label.localeCompare(b.label) || a.key.localeCompare(b.key),
|
||||
)
|
||||
const compareEntries = (a: PaletteRankEntry, b: PaletteRankEntry) =>
|
||||
b.total - a.total || a.label.localeCompare(b.label) || a.key.localeCompare(b.key)
|
||||
const rankedEntries = entries.filter((entry) => !entry.excludedFromRank).sort(compareEntries)
|
||||
const excludedEntries = entries.filter((entry) => entry.excludedFromRank).sort(compareEntries)
|
||||
const sortedEntries = [...rankedEntries, ...excludedEntries]
|
||||
|
||||
sortedEntries.forEach((entry, index) => {
|
||||
colorsByKey.set(entry.key, getPaletteColorForIndex(index, palette))
|
||||
})
|
||||
@@ -340,6 +344,27 @@ function buildPaletteColorsByDownloadRank(
|
||||
return colorsByKey
|
||||
}
|
||||
|
||||
function isExcludedFromPaletteRank(breakdownValues: readonly string[]): boolean {
|
||||
return breakdownValues.some(
|
||||
(value) =>
|
||||
isUnknownAnalyticsBreakdownValue(value) || isNoDependentAnalyticsBreakdownValue(value),
|
||||
)
|
||||
}
|
||||
|
||||
function buildPaletteRankEntry(
|
||||
key: string,
|
||||
breakdownValues: readonly string[],
|
||||
total: number,
|
||||
formatLabel: (breakdownValues: readonly string[]) => string,
|
||||
): PaletteRankEntry {
|
||||
return {
|
||||
key,
|
||||
label: formatLabel(breakdownValues),
|
||||
total,
|
||||
excludedFromRank: isExcludedFromPaletteRank(breakdownValues),
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricValue(
|
||||
point: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
activeStat: AnalyticsDashboardStat,
|
||||
@@ -499,11 +524,14 @@ export function buildChartDatasets(
|
||||
})
|
||||
|
||||
const colorsByBreakdown = buildPaletteColorsByDownloadRank(
|
||||
Array.from(dataByBreakdown.keys()).map((breakdownKey) => ({
|
||||
key: breakdownKey,
|
||||
label: formatChartBreakdownLabels(breakdownValuesByKey.get(breakdownKey) ?? []),
|
||||
total: downloadTotalsByBreakdown.get(breakdownKey) ?? 0,
|
||||
})),
|
||||
Array.from(dataByBreakdown.keys()).map((breakdownKey) =>
|
||||
buildPaletteRankEntry(
|
||||
breakdownKey,
|
||||
breakdownValuesByKey.get(breakdownKey) ?? [],
|
||||
downloadTotalsByBreakdown.get(breakdownKey) ?? 0,
|
||||
formatChartBreakdownLabels,
|
||||
),
|
||||
),
|
||||
palette,
|
||||
)
|
||||
|
||||
@@ -538,7 +566,7 @@ export function buildChartDatasets(
|
||||
? isNoDependentAnalyticsBreakdownValue(dependentProjectId)
|
||||
? formatMessage(analyticsMessages.noDependentTooltip)
|
||||
: isUnknownAnalyticsBreakdownValue(dependentProjectId)
|
||||
? formatMessage(analyticsMessages.unknown)
|
||||
? formatMessage(analyticsMessages.unknownDependentTooltip)
|
||||
: formatDependentProjectDatasetTooltip(
|
||||
versionName,
|
||||
dependentProjectName,
|
||||
@@ -675,11 +703,14 @@ export function buildChartDatasets(
|
||||
})
|
||||
|
||||
const colorsByBreakdown = buildPaletteColorsByDownloadRank(
|
||||
Array.from(dataByProjectBreakdown.keys()).map((breakdownKey) => ({
|
||||
key: breakdownKey,
|
||||
label: formatChartBreakdownLabels(breakdownValuesByKey.get(breakdownKey) ?? []),
|
||||
total: downloadTotalsByProjectBreakdown.get(breakdownKey) ?? 0,
|
||||
})),
|
||||
Array.from(dataByProjectBreakdown.keys()).map((breakdownKey) =>
|
||||
buildPaletteRankEntry(
|
||||
breakdownKey,
|
||||
breakdownValuesByKey.get(breakdownKey) ?? [],
|
||||
downloadTotalsByProjectBreakdown.get(breakdownKey) ?? 0,
|
||||
formatChartBreakdownLabels,
|
||||
),
|
||||
),
|
||||
palette,
|
||||
)
|
||||
|
||||
|
||||
@@ -92,6 +92,11 @@ export const analyticsMessages = defineMessages({
|
||||
id: 'analytics.value.no-dependent-tooltip',
|
||||
defaultMessage: 'Downloaded for reasons other than being a dependency',
|
||||
},
|
||||
unknownDependentTooltip: {
|
||||
id: 'analytics.value.unknown-dependent-tooltip',
|
||||
defaultMessage:
|
||||
"There's no metadata to determine which dependent project this download attributes to.",
|
||||
},
|
||||
other: {
|
||||
id: 'analytics.value.other',
|
||||
defaultMessage: 'Other',
|
||||
|
||||
@@ -515,7 +515,7 @@ function getDependentProjectTooltip(row: AnalyticsTableRow) {
|
||||
return formatMessage(analyticsMessages.noDependentTooltip)
|
||||
}
|
||||
if (isUnknownAnalyticsBreakdownValue(row.breakdownValues.dependent_project_download)) {
|
||||
return formatMessage(analyticsMessages.unknown)
|
||||
return formatMessage(analyticsMessages.unknownDependentTooltip)
|
||||
}
|
||||
|
||||
const dependencyProjectIds = new Set(row.dependentOnProjectIds)
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
:options="[userOption, ...ownerOptions]"
|
||||
searchable
|
||||
:disabled="hasHitLimit"
|
||||
select-search-text-on-focus
|
||||
show-icon-in-selected
|
||||
/>
|
||||
<span>{{ formatMessage(messages.ownerDescription) }}</span>
|
||||
@@ -129,7 +130,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { OrganizationIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Chips,
|
||||
@@ -362,7 +363,13 @@ async function fetchOrganizations() {
|
||||
}),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
: markRaw(
|
||||
defineAsyncComponent(() =>
|
||||
Promise.resolve({
|
||||
setup: () => () => h(OrganizationIcon, { class: 'size-5' }),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}))
|
||||
if (props.organizationId) owner.value = props.organizationId
|
||||
} catch (err) {
|
||||
@@ -395,6 +402,7 @@ async function createProject() {
|
||||
server_side: 'required',
|
||||
license_id: 'LicenseRef-Unknown',
|
||||
is_draft: true,
|
||||
organization_id: owner.value !== 'self' ? owner.value : undefined,
|
||||
}
|
||||
|
||||
formData.append('data', JSON.stringify(projectData))
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
<div class="pointer-events-none absolute inset-0 z-[-1]">
|
||||
<div id="absolute-background-teleport" class="relative"></div>
|
||||
</div>
|
||||
<div
|
||||
class="pride-backdrop pointer-events-none absolute inset-0 z-[-1]"
|
||||
:class="{ shown: showPrideBackdrop }"
|
||||
></div>
|
||||
<div class="pointer-events-none absolute inset-0 z-50">
|
||||
<div
|
||||
class="over-the-top-random-animation"
|
||||
@@ -801,6 +805,7 @@ import ProjectCreateModal from '~/components/ui/create/ProjectCreateModal.vue'
|
||||
import ModrinthFooter from '~/components/ui/ModrinthFooter.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { errors as generatedStateErrors } from '~/generated/state.json'
|
||||
import { provideCurrentProjectId } from '~/providers/current-project.ts'
|
||||
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
|
||||
import { hasActiveMidas } from '~/utils/user-membership.ts'
|
||||
|
||||
@@ -867,6 +872,32 @@ const showTinMismatchBanner = computed(() => {
|
||||
return !!auth.value.user && status === 'tin-mismatch'
|
||||
})
|
||||
|
||||
const PRIDE_COLLECTION_ID = 'M4c3ITvd'
|
||||
const PRIDE_ARTICLE_SLUGS = ['pride-campaign-2025', 'pride-campaign-2026', 'proud-of-you-2026']
|
||||
const PRIDE_CACHE_TIME = 1000 * 60 * 60 * 24
|
||||
|
||||
const { data: prideCollection } = useQuery({
|
||||
queryKey: computed(() => ['collection', PRIDE_COLLECTION_ID]),
|
||||
queryFn: () => client.labrinth.collections.get(PRIDE_COLLECTION_ID),
|
||||
staleTime: PRIDE_CACHE_TIME,
|
||||
gcTime: PRIDE_CACHE_TIME,
|
||||
})
|
||||
|
||||
const prideProjectIds = computed(() => new Set(prideCollection.value?.projects ?? []))
|
||||
|
||||
const currentProjectId = ref()
|
||||
provideCurrentProjectId(currentProjectId)
|
||||
|
||||
const showPrideBackdrop = computed(() => {
|
||||
if (PRIDE_ARTICLE_SLUGS.includes(route.params.slug)) {
|
||||
return true
|
||||
}
|
||||
if (route.params.collection === PRIDE_COLLECTION_ID) {
|
||||
return true
|
||||
}
|
||||
return !!currentProjectId.value && prideProjectIds.value.has(currentProjectId.value)
|
||||
})
|
||||
|
||||
const basePopoutId = useId()
|
||||
|
||||
async function fetchIntercomToken() {
|
||||
@@ -1695,4 +1726,21 @@ const { cycle: changeTheme } = useTheme()
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.pride-backdrop {
|
||||
background-image: linear-gradient(to right, #c20732, #f57203, #ffd632, #21ca8b, #2f9ff2, #e420fc);
|
||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0) 80%);
|
||||
height: 30rem;
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease;
|
||||
}
|
||||
|
||||
.pride-backdrop.shown {
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.light-mode .pride-backdrop.shown,
|
||||
.light .pride-backdrop.shown {
|
||||
opacity: 0.15;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -518,6 +518,9 @@
|
||||
"analytics.value.unknown": {
|
||||
"message": "Unknown"
|
||||
},
|
||||
"analytics.value.unknown-dependent-tooltip": {
|
||||
"message": "There's no metadata to determine which dependent project this download attributes to."
|
||||
},
|
||||
"analytics.value.unmonetized": {
|
||||
"message": "Unmonetized"
|
||||
},
|
||||
|
||||
@@ -1114,7 +1114,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import { nextTick, readonly, ref, useTemplateRef, watch } from 'vue'
|
||||
import { nextTick, onScopeDispose, readonly, ref, useTemplateRef, watch, watchEffect } from 'vue'
|
||||
|
||||
import { navigateTo } from '#app'
|
||||
import Accordion from '~/components/ui/Accordion.vue'
|
||||
@@ -1130,6 +1130,7 @@ import { saveFeatureFlags } from '~/composables/featureFlags.ts'
|
||||
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'
|
||||
import { versionQueryOptions } from '~/composables/queries/version'
|
||||
import { userCollectProject, userFollowProject } from '~/composables/user.js'
|
||||
import { injectCurrentProjectId } from '~/providers/current-project.ts'
|
||||
import {
|
||||
loadChecklistOpenState,
|
||||
saveChecklistOpenState,
|
||||
@@ -1705,6 +1706,16 @@ const project = computed(() => {
|
||||
// Use actual project ID for dependent queries (ensures cache consistency)
|
||||
const projectId = computed(() => projectRaw.value?.id)
|
||||
|
||||
const sharedProjectId = injectCurrentProjectId(null)
|
||||
if (sharedProjectId) {
|
||||
watchEffect(() => {
|
||||
sharedProjectId.value = projectId.value ?? undefined
|
||||
})
|
||||
onScopeDispose(() => {
|
||||
sharedProjectId.value = undefined
|
||||
})
|
||||
}
|
||||
|
||||
// V3 Project
|
||||
const {
|
||||
data: projectV3,
|
||||
|
||||
@@ -954,5 +954,9 @@ function openEditModal(event) {
|
||||
color: var(--color-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createContext } from '@modrinth/ui'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Informs the default layout of the current project ID, if any, because it can't be gleaned from the route which may be a slug
|
||||
*/
|
||||
export const [injectCurrentProjectId, provideCurrentProjectId] = createContext<
|
||||
Ref<string | undefined>
|
||||
>('root', 'currentProjectId')
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 777 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"articles": [
|
||||
{
|
||||
"title": "Proud of you all",
|
||||
"summary": "Over 2,400 of you came together to raise more than $40,000 for charity this Pride month!",
|
||||
"thumbnail": "https://modrinth.com/news/article/proud-of-you-2026/thumbnail.webp",
|
||||
"date": "2026-07-02T01:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/proud-of-you-2026"
|
||||
},
|
||||
{
|
||||
"title": "Improving Modpack review delays",
|
||||
"summary": "Reducing the back-and-forth needed to get your modpack approved.",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c"
|
||||
}
|
||||
Generated
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\tselect\n\t\t\tf.url,\n\t\t\tf.version_id as \"version_id: DBVersionId\",\n\t\t\tv.mod_id as \"project_id: DBProjectId\"\n\t\tfrom files f\n\t\tinner join versions v on v.id = f.version_id\n\t\twhere f.id = $1\n\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "version_id: DBVersionId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "project_id: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "17f415f1140df5b3dd42a161c0f77b2475edb8041bc2b9701d51cf9cbfd69ba1"
|
||||
}
|
||||
Generated
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "status: PayoutStatus",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\tdelete from attributions_exemptions\n\t\twhere version_id = $1\n\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5ed1e727901573ee92683f206f3321fa4e192c75caa5a62a2790b5136e9a7dde"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO payouts_values_notifications (date_available, user_id, notified)\n VALUES ($1, $2, FALSE)\n ON CONFLICT (date_available, user_id) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850"
|
||||
}
|
||||
Generated
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n select\n (\n select count(*)\n from project_attribution_groups\n where project_id = $1\n ) as \"groups!\",\n (\n select count(*)\n from project_attribution_files paf\n inner join project_attribution_groups pag on pag.id = paf.group_id\n where pag.project_id = $1\n ) as \"files!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "groups!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "files!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "686994a5bfc061b4c6e1ca594fae9d5d3fb18974de6605a650f666d440dfe684"
|
||||
}
|
||||
Generated
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)\n VALUES ($1, NULL, $2, NOW(), $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Numeric",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT status FROM mods WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "91d2ce7ee6a29a47a20655fef577c42f1cbb2f8de4d9ea8ab361fc210f08aa20"
|
||||
}
|
||||
Generated
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET status = 'rejected'\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "95e17b2512494ffcbfe6278b87aa273edc5729633aeaa87f6239667d2f861e68"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "status: PayoutStatus",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3"
|
||||
}
|
||||
Generated
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02"
|
||||
}
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41"
|
||||
}
|
||||
@@ -100,6 +100,7 @@ rust_decimal = { workspace = true, features = [
|
||||
rust_iso3166 = { workspace = true }
|
||||
rustls.workspace = true
|
||||
rusty-money = { workspace = true }
|
||||
scalar_api_reference = { workspace = true, features = ["actix-web"] }
|
||||
sentry = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
@@ -128,8 +129,6 @@ tracing-actix-web = { workspace = true }
|
||||
url = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
utoipa = { workspace = true, features = ["url"] }
|
||||
utoipa-actix-web = { workspace = true }
|
||||
utoipa-scalar = { workspace = true, features = ["actix-web"] }
|
||||
uuid = { workspace = true, features = ["fast-rng", "serde", "v4", "v7"] }
|
||||
validator = { workspace = true, features = ["derive"] }
|
||||
webauthn-rs = { workspace = true, features = [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
INSERT INTO notifications_types
|
||||
(name, delivery_priority, expose_in_user_preferences, expose_in_site_notifications)
|
||||
VALUES ('shared_instance_invite', 1, FALSE, TRUE);
|
||||
|
||||
INSERT INTO users_notifications_preferences (user_id, channel, notification_type, enabled)
|
||||
VALUES (NULL, 'email', 'shared_instance_invite', FALSE);
|
||||
@@ -0,0 +1,7 @@
|
||||
Modrinth Internal API.
|
||||
|
||||
> [!caution]
|
||||
> This API is internal and is subject to breaking changes without notice. Please avoid using it in production.
|
||||
|
||||
> [!tip]
|
||||
> You're probably looking for [Modrinth's public API v2](/docs?api=v2)
|
||||
@@ -0,0 +1,101 @@
|
||||
> [!success]
|
||||
> API v2 is Modrinth's current, official API. It is the recommended API for integrations and applications and will receive long-term support.
|
||||
|
||||
## Authentication
|
||||
This API has two options for authentication: personal access tokens and [OAuth2](https://en.wikipedia.org/wiki/OAuth).
|
||||
All tokens are tied to a Modrinth user and use the `Authorization` header of the request.
|
||||
|
||||
Example:
|
||||
```
|
||||
Authorization: mrp_RNtLRSPmGj2pd1v1ubi52nX7TJJM9sznrmwhAuj511oe4t1jAqAQ3D6Wc8Ic
|
||||
```
|
||||
|
||||
> [!tip] You do not need a token for most requests.
|
||||
>
|
||||
> Generally speaking, only the following types of requests require a token:
|
||||
> - those which create data (such as version creation)
|
||||
> - those which modify data (such as editing a project)
|
||||
> - those which access private data (such as draft projects, notifications, emails, and payout data)
|
||||
|
||||
Each request requiring authentication has a certain scope. For example, to view the email of the user being requested, the token must have the `USER_READ_EMAIL` scope.
|
||||
You can find the list of available scopes [on GitHub](https://github.com/modrinth/code/blob/main/apps/labrinth/src/models/v3/pats.rs#L9). Making a request with an invalid scope will return a 401 error.
|
||||
|
||||
Please note that certain scopes and requests cannot be completed with a personal access token or using OAuth.
|
||||
For example, deleting a user account can only be done through Modrinth's frontend.
|
||||
|
||||
### Personal access tokens
|
||||
Personal access tokens (PATs) can be generated from [the user settings](https://modrinth.com/settings/account).
|
||||
|
||||
### GitHub tokens
|
||||
For backwards compatibility purposes, some types of GitHub tokens also work for authenticating a user with Modrinth's API, granting all scopes.
|
||||
**We urge any application still using GitHub tokens to start using personal access tokens for security and reliability purposes.**
|
||||
|
||||
## Cross-Origin Resource Sharing
|
||||
This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with the [W3C spec](https://www.w3.org/TR/cors/).
|
||||
This allows for cross-domain communication from the browser.
|
||||
All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.
|
||||
|
||||
## Identifiers
|
||||
The majority of items you can interact with in the API have a unique eight-digit base62 ID.
|
||||
Projects, versions, users, threads, teams, and reports all use this same way of identifying themselves.
|
||||
Version files use the sha1 or sha512 file hashes as identifiers.
|
||||
|
||||
Each project and user has a friendlier way of identifying them; slugs and usernames, respectively.
|
||||
While unique IDs are constant, slugs and usernames can change at any moment.
|
||||
If you want to store something in the long term, it is recommended to use the unique ID.
|
||||
|
||||
## Ratelimits
|
||||
The API has a ratelimit defined per IP. Limits and remaining amounts are given in the response headers.
|
||||
- `X-Ratelimit-Limit`: the maximum number of requests that can be made in a minute
|
||||
- `X-Ratelimit-Remaining`: the number of requests remaining in the current ratelimit window
|
||||
- `X-Ratelimit-Reset`: the time in seconds until the ratelimit window resets
|
||||
|
||||
**Ratelimits are the same no matter whether you use a token or not.**
|
||||
|
||||
The ratelimit is currently 300 requests per minute per IP. If your application is hitting rate limits, try caching responses, using batch requests, or spreading out your requests over time.
|
||||
|
||||
Higher rate limits are granted only in very rare cases. If you believe your use case requires an exception, please [contact us](mailto:support@modrinth.com).
|
||||
|
||||
## User Agents
|
||||
To access the Modrinth API, you **must** use provide a uniquely-identifying `User-Agent` header.
|
||||
Providing a user agent that only identifies your HTTP client library (such as "okhttp/4.9.3") increases the likelihood that we will block your traffic.
|
||||
It is recommended, but not required, to include contact information in your user agent.
|
||||
This allows us to contact you if we would like a change in your application's behavior without having to block your traffic.
|
||||
- Bad: `User-Agent: okhttp/4.9.3`
|
||||
- Good: `User-Agent: project_name`
|
||||
- Better: `User-Agent: github_username/project_name/1.56.0`
|
||||
- Best: `User-Agent: github_username/project_name/1.56.0 (launcher.com)` or `User-Agent: github_username/project_name/1.56.0 (contact@launcher.com)`
|
||||
|
||||
## Versioning
|
||||
Modrinth follows a simple pattern for its API versioning.
|
||||
In the event of a breaking API change, the API version in the URL path is bumped, and migration steps will be published below.
|
||||
|
||||
### Migrations
|
||||
Inside the following spoiler, you will be able to find all changes between versions of the Modrinth API, accompanied by tips and a guide to migrate applications to newer versions.
|
||||
|
||||
<details><summary>API v1 to API v2</summary>
|
||||
|
||||
These bullet points cover most changes in the v2 API, but please note that fields containing `mod` in most contexts have been shifted to `project`. For example, in the search route, the field `mod_id` was renamed to `project_id`.
|
||||
|
||||
- The search route has been moved from `/api/v1/mod` to `/v2/search`
|
||||
- New project fields: `project_type` (may be `mod` or `modpack`), `moderation_message` (which has a `message` and `body`), `gallery`
|
||||
- New search facet: `project_type`
|
||||
- Alphabetical sort removed (it didn't work and is not possible due to limits in MeiliSearch)
|
||||
- New search fields: `project_type`, `gallery`
|
||||
- The gallery field is an array of URLs to images that are part of the project's gallery
|
||||
- The gallery is a new feature which allows the user to upload images showcasing their mod to the CDN which will be displayed on their mod page
|
||||
- Internal change: Any project file uploaded to Modrinth is now validated to make sure it's a valid Minecraft mod, Modpack, etc.
|
||||
- For example, a Forge 1.17 mod with a JAR not containing a mods.toml will not be allowed to be uploaded to Modrinth
|
||||
- In project creation, projects may not upload a mod with no versions to review, however they can be saved as a draft
|
||||
- Similarly, for version creation, a version may not be uploaded without any files
|
||||
- Donation URLs have been enabled
|
||||
- New project status: `archived`. Projects with this status do not appear in search
|
||||
- Tags (such as categories, loaders) now have icons (SVGs) and specific project types attached
|
||||
- Dependencies have been wiped and replaced with a new system
|
||||
- Notifications now have a `type` field, such as `project_update`
|
||||
|
||||
Along with this, project subroutes (such as `/v2/project/{id}/version`) now allow the slug to be used as the ID. This is also the case with user routes.
|
||||
|
||||
<br />
|
||||
|
||||
The above snippet about User Agents was adapted from https://crates.io/policies, copyright (c) 2014 The Rust Project Developers under MIT license.
|
||||
@@ -0,0 +1,8 @@
|
||||
> [!caution]
|
||||
> API v3 is Modrinth's experimental API.
|
||||
>
|
||||
> **Please do not use this API in production. It will be deprecated in the future.**
|
||||
|
||||
|
||||
> [!tip]
|
||||
> You're probably looking for [Modrinth's public API v2](/docs?api=v2)
|
||||
@@ -18,7 +18,7 @@ use crate::models::ids::OAuthClientId;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use actix_web::http::header::{CACHE_CONTROL, LOCATION, PRAGMA};
|
||||
use actix_web::web::{Data, Query, ServiceConfig};
|
||||
use actix_web::web::{Data, Query};
|
||||
use actix_web::{HttpRequest, HttpResponse, get, post, web};
|
||||
use chrono::{DateTime, Duration};
|
||||
use rand::distributions::Alphanumeric;
|
||||
@@ -33,7 +33,7 @@ use super::AuthenticationError;
|
||||
pub mod errors;
|
||||
pub mod uris;
|
||||
|
||||
pub fn config(cfg: &mut ServiceConfig) {
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(init_oauth)
|
||||
.service(accept_client_scopes)
|
||||
.service(reject_client_scopes)
|
||||
@@ -57,6 +57,18 @@ pub struct OAuthClientAccessRequest {
|
||||
pub requested_scopes: Scopes,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
context_path = "/oauth",
|
||||
path = "/authorize",
|
||||
tag = "oauth",
|
||||
params(
|
||||
("client_id" = OAuthClientId, Query),
|
||||
("redirect_uri" = Option<String>, Query),
|
||||
("scope" = Option<String>, Query),
|
||||
("state" = Option<String>, Query)
|
||||
),
|
||||
responses((status = OK))
|
||||
)]
|
||||
#[get("authorize")]
|
||||
pub async fn init_oauth(
|
||||
req: HttpRequest,
|
||||
@@ -165,11 +177,15 @@ pub async fn init_oauth(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct RespondToOAuthClientScopes {
|
||||
pub flow: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
context_path = "/oauth",
|
||||
path = "/accept", tag = "oauth", responses((status = OK))
|
||||
)]
|
||||
#[post("accept")]
|
||||
pub async fn accept_client_scopes(
|
||||
req: HttpRequest,
|
||||
@@ -189,6 +205,10 @@ pub async fn accept_client_scopes(
|
||||
.await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
context_path = "/oauth",
|
||||
path = "/reject", tag = "oauth", responses((status = OK))
|
||||
)]
|
||||
#[post("reject")]
|
||||
pub async fn reject_client_scopes(
|
||||
req: HttpRequest,
|
||||
@@ -201,7 +221,7 @@ pub async fn reject_client_scopes(
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct TokenRequest {
|
||||
pub grant_type: String,
|
||||
pub code: String,
|
||||
@@ -216,6 +236,10 @@ pub struct TokenResponse {
|
||||
pub expires_in: i64,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
context_path = "/oauth",
|
||||
path = "/token", tag = "oauth", responses((status = OK))
|
||||
)]
|
||||
#[post("token")]
|
||||
/// Params should be in the urlencoded request body
|
||||
/// And client secret should be in the HTTP basic authorization header
|
||||
|
||||
+42
-30
@@ -20,7 +20,6 @@ use crate::background_task::update_versions;
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::env::ENV;
|
||||
use crate::queue::billing::{index_billing, index_subscriptions};
|
||||
use crate::queue::moderation::AutomatedModerationQueue;
|
||||
use crate::routes::internal::delphi::rescan::rescan_projects_in_queue;
|
||||
use crate::util::anrok;
|
||||
use crate::util::archon::ArchonClient;
|
||||
@@ -68,7 +67,6 @@ pub struct LabrinthConfig {
|
||||
pub payouts_queue: web::Data<PayoutsQueue>,
|
||||
pub analytics_queue: Arc<AnalyticsQueue>,
|
||||
pub active_sockets: web::Data<ActiveSockets>,
|
||||
pub automated_moderation_queue: web::Data<AutomatedModerationQueue>,
|
||||
pub rate_limiter: web::Data<AsyncRateLimiter>,
|
||||
pub stripe_client: stripe::Client,
|
||||
pub anrok_client: anrok::Client,
|
||||
@@ -98,21 +96,6 @@ pub fn app_setup(
|
||||
) -> LabrinthConfig {
|
||||
info!("Starting labrinth on {}", &ENV.BIND_ADDR);
|
||||
|
||||
let automated_moderation_queue =
|
||||
web::Data::new(AutomatedModerationQueue::default());
|
||||
|
||||
{
|
||||
let automated_moderation_queue_ref = automated_moderation_queue.clone();
|
||||
let pool_ref = pool.clone();
|
||||
let ro_pool_ref = ro_pool.clone();
|
||||
let redis_pool_ref = redis_pool.clone();
|
||||
actix_rt::spawn(async move {
|
||||
automated_moderation_queue_ref
|
||||
.task(pool_ref, ro_pool_ref, redis_pool_ref)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
let scheduler = scheduler::Scheduler::new();
|
||||
|
||||
let http_client = web::Data::new(HttpClient::new());
|
||||
@@ -341,7 +324,6 @@ pub fn app_setup(
|
||||
payouts_queue: web::Data::new(PayoutsQueue::new()),
|
||||
analytics_queue,
|
||||
active_sockets,
|
||||
automated_moderation_queue,
|
||||
rate_limiter: limiter,
|
||||
stripe_client,
|
||||
anrok_client,
|
||||
@@ -361,6 +343,14 @@ pub fn app_setup(
|
||||
pub fn app_config(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
app_data_config(cfg, labrinth_config);
|
||||
app_fallback_config(cfg);
|
||||
}
|
||||
|
||||
pub fn app_data_config(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
cfg.app_data(web::FormConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
@@ -391,23 +381,23 @@ pub fn app_config(
|
||||
.app_data(web::Data::new(labrinth_config.analytics_queue.clone()))
|
||||
.app_data(web::Data::new(labrinth_config.clickhouse.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())
|
||||
.app_data(labrinth_config.kafka_client.clone())
|
||||
.app_data(labrinth_config.search_state.clone())
|
||||
.app_data(labrinth_config.webauthn.clone())
|
||||
.configure(routes::v3::config)
|
||||
.configure(routes::internal::config)
|
||||
.configure(routes::root_config)
|
||||
.default_service(web::get().wrap(default_cors()).to(routes::not_found));
|
||||
.app_data(labrinth_config.webauthn.clone());
|
||||
}
|
||||
|
||||
pub fn utoipa_app_config(
|
||||
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
|
||||
_labrinth_config: LabrinthConfig,
|
||||
pub fn app_fallback_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.configure(routes::root_config)
|
||||
.default_service(web::get().to(routes::not_found).wrap(default_cors()));
|
||||
}
|
||||
|
||||
pub fn app_routes_config(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
cfg.configure({
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -419,7 +409,29 @@ pub fn utoipa_app_config(
|
||||
|_cfg| ()
|
||||
}
|
||||
})
|
||||
.configure(routes::v2::utoipa_config)
|
||||
.configure(routes::v3::utoipa_config)
|
||||
.configure(routes::internal::utoipa_config);
|
||||
.configure(|cfg| app_routes_config_v2(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| app_routes_config_v3(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| app_routes_config_internal(cfg, labrinth_config));
|
||||
}
|
||||
|
||||
pub fn app_routes_config_v2(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
_labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
cfg.configure(routes::v2::config);
|
||||
}
|
||||
|
||||
pub fn app_routes_config_v3(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
_labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
cfg.configure(routes::public_config)
|
||||
.configure(routes::v3::config);
|
||||
}
|
||||
|
||||
pub fn app_routes_config_internal(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
_labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
cfg.configure(routes::internal::config);
|
||||
}
|
||||
|
||||
+169
-24
@@ -2,7 +2,7 @@
|
||||
|
||||
use actix_web::dev::Service;
|
||||
use actix_web::middleware::from_fn;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use actix_web::{App, HttpResponse, HttpServer, web};
|
||||
use actix_web_prom::PrometheusMetricsBuilder;
|
||||
use clap::Parser;
|
||||
|
||||
@@ -15,17 +15,22 @@ use labrinth::search;
|
||||
use labrinth::util::anrok;
|
||||
use labrinth::util::gotenberg::GotenbergClient;
|
||||
use labrinth::util::ratelimit::rate_limit_middleware;
|
||||
use labrinth::utoipa_app_config;
|
||||
use labrinth::{app_config, env};
|
||||
use labrinth::{app_data_config, app_fallback_config, env};
|
||||
use labrinth::{
|
||||
app_routes_config_internal, app_routes_config_v2, app_routes_config_v3,
|
||||
};
|
||||
use labrinth::{clickhouse, database, file_hosting};
|
||||
use scalar_api_reference::actix_web::config as scalar_config;
|
||||
use serde_json::json;
|
||||
use std::ffi::CStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::{Instrument, info, info_span};
|
||||
use tracing_actix_web::TracingLogger;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
|
||||
use utoipa_actix_web::AppExt;
|
||||
use utoipa_scalar::Servable;
|
||||
use utoipa::PartialSchema;
|
||||
use utoipa::openapi::Content;
|
||||
use utoipa::openapi::response::Response;
|
||||
use utoipa::openapi::schema::Components;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[global_allocator]
|
||||
@@ -223,7 +228,16 @@ async fn app() -> std::io::Result<()> {
|
||||
info!("Starting Actix HTTP server!");
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
let mut docs_v2 = labrinth::routes::v2::ApiDoc::openapi();
|
||||
let mut docs_v3 = labrinth::routes::v3::ApiDoc::openapi();
|
||||
let mut docs_internal = labrinth::routes::internal::ApiDoc::openapi();
|
||||
#[cfg(target_os = "linux")]
|
||||
docs_v3.merge(labrinth::routes::debug::ApiDoc::openapi());
|
||||
document_error_responses(&mut docs_v2);
|
||||
document_error_responses(&mut docs_v3);
|
||||
document_error_responses(&mut docs_internal);
|
||||
|
||||
let app = App::new()
|
||||
.wrap(TracingLogger::default())
|
||||
.wrap_fn(|req, srv| {
|
||||
// We capture the same fields as `tracing-actix-web`'s `RootSpanBuilder`.
|
||||
@@ -260,31 +274,162 @@ async fn app() -> std::io::Result<()> {
|
||||
// transactions out of HTTP requests. However, we have to use our
|
||||
// own - See `sentry::SentryErrorReporting` for why.
|
||||
.wrap(labrinth::util::sentry::SentryErrorReporting)
|
||||
// Use `utoipa` for OpenAPI generation
|
||||
.into_utoipa_app()
|
||||
.configure(|cfg| utoipa_app_config(cfg, labrinth_config.clone()))
|
||||
.openapi_service(|api| utoipa_scalar::Scalar::with_url("/docs", ApiDoc::openapi().merge_from(api)))
|
||||
.into_app()
|
||||
.configure(|cfg| app_config(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| app_data_config(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| app_routes_config_v2(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| app_routes_config_v3(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| {
|
||||
app_routes_config_internal(cfg, labrinth_config.clone())
|
||||
});
|
||||
|
||||
let scalar_configuration = json!({
|
||||
"sources": [
|
||||
{
|
||||
"title": "API v2",
|
||||
"slug": "v2",
|
||||
"url": "/openapi/v2.json",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"title": "API v3 (UNSTABLE)",
|
||||
"slug": "v3",
|
||||
"url": "/openapi/v3.json"
|
||||
},
|
||||
{
|
||||
"title": "Internal API (HIGHLY UNSTABLE)",
|
||||
"slug": "internal",
|
||||
"url": "/openapi/internal.json"
|
||||
}
|
||||
],
|
||||
"agent": {
|
||||
"disabled": true
|
||||
},
|
||||
"mcp": {
|
||||
"disabled": true
|
||||
},
|
||||
"telemetry": false,
|
||||
|
||||
"metaData": {
|
||||
"title": "Modrinth API Documentation",
|
||||
"description": "Reference documentation for the Modrinth API.",
|
||||
"ogTitle": "Modrinth API Documentation",
|
||||
"ogDescription": "Reference documentation for the Modrinth API."
|
||||
},
|
||||
|
||||
"modelsSectionLabel": "Schemas",
|
||||
"defaultOpenFirstTag": true,
|
||||
"defaultOpenAllTags": false,
|
||||
"expandAllResponses": false,
|
||||
"expandAllSchemaProperties": false,
|
||||
"expandAllModelSections": false,
|
||||
"orderSchemaPropertiesBy": "preserve",
|
||||
"orderRequiredPropertiesFirst": true,
|
||||
"hideSearch": false,
|
||||
"searchHotKey": "k",
|
||||
"showOperationId": false,
|
||||
|
||||
"defaultHttpClient": {
|
||||
"targetKey": "shell",
|
||||
"clientKey": "curl"
|
||||
},
|
||||
|
||||
"persistAuth": false,
|
||||
"showDeveloperTools": "never",
|
||||
});
|
||||
|
||||
app.service(openapi_json_service("/openapi/v2.json", docs_v2))
|
||||
.service(openapi_json_service("/openapi/v3.json", docs_v3))
|
||||
.service(openapi_json_service(
|
||||
"/openapi/internal.json",
|
||||
docs_internal,
|
||||
))
|
||||
.configure(scalar_config("/docs", &scalar_configuration))
|
||||
.configure(app_fallback_config)
|
||||
})
|
||||
.bind(&ENV.BIND_ADDR)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(info(title = "Labrinth"), modifiers(&SecurityAddon))]
|
||||
struct ApiDoc;
|
||||
fn openapi_json_service(
|
||||
path: &'static str,
|
||||
openapi: utoipa::openapi::OpenApi,
|
||||
) -> actix_web::Resource {
|
||||
web::resource(path).route(web::get().to(move || {
|
||||
let openapi = openapi.clone();
|
||||
async move { openapi_json(openapi) }
|
||||
}))
|
||||
}
|
||||
|
||||
struct SecurityAddon;
|
||||
fn openapi_json(openapi: utoipa::openapi::OpenApi) -> HttpResponse {
|
||||
match serde_json::to_string_pretty(&openapi) {
|
||||
Ok(body) => HttpResponse::Ok()
|
||||
.content_type("application/json; charset=utf-8")
|
||||
.body(body),
|
||||
Err(error) => {
|
||||
tracing::error!(%error, "Failed to serialize OpenAPI schema");
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl utoipa::Modify for SecurityAddon {
|
||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||
let components = openapi.components.as_mut().unwrap();
|
||||
components.add_security_scheme(
|
||||
"bearer_auth",
|
||||
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new(
|
||||
"authorization",
|
||||
fn document_error_responses(openapi: &mut utoipa::openapi::OpenApi) {
|
||||
let components = openapi.components.get_or_insert_with(Components::new);
|
||||
components.schemas.insert(
|
||||
"ApiError".to_string(),
|
||||
labrinth::models::error::ApiError::schema(),
|
||||
);
|
||||
|
||||
for path_item in openapi.paths.paths.values_mut() {
|
||||
add_default_error_response(&mut path_item.get);
|
||||
add_default_error_response(&mut path_item.put);
|
||||
add_default_error_response(&mut path_item.post);
|
||||
add_default_error_response(&mut path_item.delete);
|
||||
add_default_error_response(&mut path_item.options);
|
||||
add_default_error_response(&mut path_item.head);
|
||||
add_default_error_response(&mut path_item.patch);
|
||||
add_default_error_response(&mut path_item.trace);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_default_error_response(
|
||||
operation: &mut Option<utoipa::openapi::path::Operation>,
|
||||
) {
|
||||
if let Some(operation) = operation {
|
||||
for (status, response) in &mut operation.responses.responses {
|
||||
if !is_error_response_status(status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let utoipa::openapi::RefOr::T(response) = response {
|
||||
add_error_content(response);
|
||||
}
|
||||
}
|
||||
|
||||
operation
|
||||
.responses
|
||||
.responses
|
||||
.entry("500".to_string())
|
||||
.or_insert_with(|| error_response().into());
|
||||
}
|
||||
}
|
||||
|
||||
fn is_error_response_status(status: &str) -> bool {
|
||||
matches!(status.as_bytes().first(), Some(b'4' | b'5'))
|
||||
|| status == "default"
|
||||
}
|
||||
|
||||
fn error_response() -> Response {
|
||||
let mut response = Response::new("Error response");
|
||||
add_error_content(&mut response);
|
||||
response
|
||||
}
|
||||
|
||||
fn add_error_content(response: &mut Response) {
|
||||
if response.content.is_empty() {
|
||||
response.content.insert(
|
||||
"application/json".to_string(),
|
||||
Content::new(Some(utoipa::openapi::Ref::from_schema_name(
|
||||
"ApiError",
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An error returned by the API
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ApiError<'a> {
|
||||
#[schema(value_type = String)]
|
||||
pub error: &'a str,
|
||||
pub description: String,
|
||||
#[schema(value_type = Option<serde_json::Value>)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub details: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::large_stack_arrays)]
|
||||
|
||||
use crate::models::ids::{ThreadMessageId, VersionId};
|
||||
use crate::models::v3::billing::PriceDuration;
|
||||
use crate::models::{
|
||||
@@ -13,7 +15,7 @@ use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct LegacyNotification {
|
||||
pub id: NotificationId,
|
||||
pub user_id: UserId,
|
||||
@@ -30,14 +32,14 @@ pub struct LegacyNotification {
|
||||
pub actions: Vec<LegacyNotificationAction>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
pub struct LegacyNotificationAction {
|
||||
pub title: String,
|
||||
/// The route to call when this notification action is called. Formatted HTTP Method, route
|
||||
pub action_route: (String, String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum LegacyNotificationBody {
|
||||
TaxNotification {
|
||||
@@ -73,6 +75,10 @@ pub enum LegacyNotificationBody {
|
||||
invited_by: UserId,
|
||||
role: String,
|
||||
},
|
||||
SharedInstanceInvite {
|
||||
shared_instance_id: String,
|
||||
shared_instance_name: String,
|
||||
},
|
||||
StatusChange {
|
||||
project_id: ProjectId,
|
||||
old_status: ProjectStatus,
|
||||
@@ -177,6 +183,9 @@ impl LegacyNotification {
|
||||
NotificationBody::ServerInvite { .. } => {
|
||||
Some("server_invite".to_string())
|
||||
}
|
||||
NotificationBody::SharedInstanceInvite { .. } => {
|
||||
Some("shared_instance_invite".to_string())
|
||||
}
|
||||
NotificationBody::StatusChange { .. } => {
|
||||
Some("status_change".to_string())
|
||||
}
|
||||
@@ -294,6 +303,13 @@ impl LegacyNotification {
|
||||
invited_by,
|
||||
role,
|
||||
},
|
||||
NotificationBody::SharedInstanceInvite {
|
||||
shared_instance_id,
|
||||
shared_instance_name,
|
||||
} => LegacyNotificationBody::SharedInstanceInvite {
|
||||
shared_instance_id,
|
||||
shared_instance_name,
|
||||
},
|
||||
NotificationBody::StatusChange {
|
||||
project_id,
|
||||
old_status,
|
||||
|
||||
@@ -4,7 +4,7 @@ use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct LegacyReport {
|
||||
pub id: ReportId,
|
||||
pub report_type: String,
|
||||
@@ -17,7 +17,7 @@ pub struct LegacyReport {
|
||||
pub thread_id: ThreadId,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LegacyItemType {
|
||||
Project,
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{routes::v2_reroute, search::ResultSearchProject};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)]
|
||||
pub struct LegacySearchResults {
|
||||
pub hits: Vec<LegacyResultSearchProject>,
|
||||
pub offset: usize,
|
||||
@@ -11,7 +11,7 @@ pub struct LegacySearchResults {
|
||||
pub total_hits: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, utoipa::ToSchema)]
|
||||
pub struct LegacyResultSearchProject {
|
||||
pub project_id: String,
|
||||
pub project_type: String,
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::models::{
|
||||
};
|
||||
|
||||
/// A member of a team
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
pub struct LegacyTeamMember {
|
||||
pub role: String,
|
||||
// is_owner removed, and role hardcoded to Owner if true,
|
||||
|
||||
@@ -7,7 +7,7 @@ use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct LegacyThread {
|
||||
pub id: ThreadId,
|
||||
#[serde(rename = "type")]
|
||||
@@ -18,7 +18,7 @@ pub struct LegacyThread {
|
||||
pub members: Vec<User>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct LegacyThreadMessage {
|
||||
pub id: ThreadMessageId,
|
||||
pub author_id: Option<UserId>,
|
||||
@@ -26,7 +26,7 @@ pub struct LegacyThreadMessage {
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum LegacyMessageBody {
|
||||
Text {
|
||||
@@ -49,7 +49,9 @@ pub enum LegacyMessageBody {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Eq, PartialEq, Copy, Clone, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LegacyThreadType {
|
||||
Report,
|
||||
|
||||
@@ -6,7 +6,7 @@ use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct LegacyUser {
|
||||
pub id: UserId,
|
||||
pub username: String,
|
||||
|
||||
@@ -75,7 +75,17 @@ impl Price {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PriceDuration {
|
||||
FiveDays,
|
||||
|
||||
@@ -28,7 +28,7 @@ impl From<crate::database::models::DBModerationNote> for ModerationNote {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct PatchModerationNote {
|
||||
pub notes: Option<String>,
|
||||
pub user_rating: Option<i32>,
|
||||
|
||||
@@ -36,6 +36,7 @@ pub enum NotificationType {
|
||||
TeamInvite,
|
||||
OrganizationInvite,
|
||||
ServerInvite,
|
||||
SharedInstanceInvite,
|
||||
StatusChange,
|
||||
ModeratorMessage,
|
||||
LegacyMarkdown,
|
||||
@@ -71,6 +72,7 @@ impl NotificationType {
|
||||
NotificationType::TeamInvite => "team_invite",
|
||||
NotificationType::OrganizationInvite => "organization_invite",
|
||||
NotificationType::ServerInvite => "server_invite",
|
||||
NotificationType::SharedInstanceInvite => "shared_instance_invite",
|
||||
NotificationType::StatusChange => "status_change",
|
||||
NotificationType::ModeratorMessage => "moderator_message",
|
||||
NotificationType::LegacyMarkdown => "legacy_markdown",
|
||||
@@ -112,6 +114,7 @@ impl NotificationType {
|
||||
"team_invite" => NotificationType::TeamInvite,
|
||||
"organization_invite" => NotificationType::OrganizationInvite,
|
||||
"server_invite" => NotificationType::ServerInvite,
|
||||
"shared_instance_invite" => NotificationType::SharedInstanceInvite,
|
||||
"status_change" => NotificationType::StatusChange,
|
||||
"moderator_message" => NotificationType::ModeratorMessage,
|
||||
"legacy_markdown" => NotificationType::LegacyMarkdown,
|
||||
@@ -173,6 +176,10 @@ pub enum NotificationBody {
|
||||
invited_by: UserId,
|
||||
role: String,
|
||||
},
|
||||
SharedInstanceInvite {
|
||||
shared_instance_id: String,
|
||||
shared_instance_name: String,
|
||||
},
|
||||
StatusChange {
|
||||
project_id: ProjectId,
|
||||
old_status: ProjectStatus,
|
||||
@@ -288,6 +295,9 @@ impl NotificationBody {
|
||||
NotificationBody::ServerInvite { .. } => {
|
||||
NotificationType::ServerInvite
|
||||
}
|
||||
NotificationBody::SharedInstanceInvite { .. } => {
|
||||
NotificationType::SharedInstanceInvite
|
||||
}
|
||||
NotificationBody::StatusChange { .. } => {
|
||||
NotificationType::StatusChange
|
||||
}
|
||||
@@ -470,6 +480,32 @@ impl From<DBNotification> for Notification {
|
||||
},
|
||||
],
|
||||
),
|
||||
NotificationBody::SharedInstanceInvite {
|
||||
shared_instance_name,
|
||||
..
|
||||
} => (
|
||||
"You have been invited to a shared instance!".to_string(),
|
||||
format!(
|
||||
"An invite has been sent for you to join {shared_instance_name}"
|
||||
),
|
||||
"#".to_string(),
|
||||
vec![
|
||||
NotificationAction {
|
||||
name: "Accept".to_string(),
|
||||
action_route: (
|
||||
"POST".to_string(),
|
||||
String::new(),
|
||||
),
|
||||
},
|
||||
NotificationAction {
|
||||
name: "Deny".to_string(),
|
||||
action_route: (
|
||||
"POST".to_string(),
|
||||
String::new(),
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
NotificationBody::StatusChange {
|
||||
old_status,
|
||||
new_status,
|
||||
@@ -670,7 +706,7 @@ impl From<DBNotification> for Notification {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
pub struct NotificationAction {
|
||||
pub name: String,
|
||||
/// The route to call when this notification action is called. Formatted HTTP Method, route
|
||||
|
||||
@@ -10,14 +10,14 @@ use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct OAuthRedirectUri {
|
||||
pub id: OAuthRedirectUriId,
|
||||
pub client_id: OAuthClientId,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct OAuthClientCreationResult {
|
||||
#[serde(flatten)]
|
||||
pub client: OAuthClient,
|
||||
@@ -25,7 +25,7 @@ pub struct OAuthClientCreationResult {
|
||||
pub client_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct OAuthClient {
|
||||
pub id: OAuthClientId,
|
||||
pub name: String,
|
||||
@@ -48,7 +48,7 @@ pub struct OAuthClient {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct OAuthClientAuthorization {
|
||||
pub id: OAuthClientAuthorizationId,
|
||||
pub app_id: OAuthClientId,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::models::ids::{OrganizationId, TeamId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An organization of users who control a project
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct Organization {
|
||||
/// The id of the organization
|
||||
pub id: OrganizationId,
|
||||
|
||||
@@ -234,7 +234,7 @@ impl PayoutStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
pub struct PayoutMethod {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
@@ -252,7 +252,7 @@ pub struct PayoutMethod {
|
||||
pub exchange_rate: Option<Decimal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct PayoutMethodFee {
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub percentage: Decimal,
|
||||
@@ -274,6 +274,17 @@ impl PayoutMethodFee {
|
||||
#[derive(Clone)]
|
||||
pub struct PayoutDecimal(pub Decimal);
|
||||
|
||||
impl utoipa::PartialSchema for PayoutDecimal {
|
||||
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
|
||||
utoipa::openapi::ObjectBuilder::new()
|
||||
.schema_type(utoipa::openapi::schema::Type::Number)
|
||||
.build()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl utoipa::ToSchema for PayoutDecimal {}
|
||||
|
||||
impl Serialize for PayoutDecimal {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@@ -293,7 +304,7 @@ impl<'de> Deserialize<'de> for PayoutDecimal {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PayoutInterval {
|
||||
Standard {
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::models::exp;
|
||||
use crate::models::ids::{
|
||||
FileId, OrganizationId, ProjectId, TeamId, ThreadId, VersionId,
|
||||
};
|
||||
use crate::routes::{FileHash, HashAlgorithm};
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use itertools::Itertools;
|
||||
@@ -970,6 +971,7 @@ pub struct VersionFile {
|
||||
pub id: Option<FileId>,
|
||||
/// A map of hashes of the file. The key is the hashing algorithm
|
||||
/// and the value is the string version of the hash.
|
||||
#[schema(value_type = std::collections::HashMap<HashAlgorithm, FileHash>)]
|
||||
pub hashes: std::collections::HashMap<String, String>,
|
||||
/// A direct link to the file for downloading it.
|
||||
pub url: String,
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct Report {
|
||||
pub thread_id: ThreadId,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ItemType {
|
||||
Project,
|
||||
|
||||
@@ -38,7 +38,7 @@ impl SharedInstance {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct SharedInstanceVersion {
|
||||
pub id: SharedInstanceVersionId,
|
||||
pub shared_instance: SharedInstanceId,
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub const DEFAULT_ROLE: &str = "Member";
|
||||
|
||||
/// A team of users who control a project
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct Team {
|
||||
/// The id of the team
|
||||
pub id: TeamId,
|
||||
@@ -157,7 +157,7 @@ impl OrganizationPermissions {
|
||||
}
|
||||
|
||||
/// A member of a team
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
pub struct TeamMember {
|
||||
/// The ID of the team this team member is a member of
|
||||
pub team_id: TeamId,
|
||||
|
||||
@@ -242,7 +242,7 @@ impl Role {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UserFriend {
|
||||
// The user who accepted the friend request
|
||||
pub id: UserId,
|
||||
|
||||
@@ -772,6 +772,7 @@ async fn collect_template_variables(
|
||||
}
|
||||
|
||||
NotificationBody::ProjectUpdate { .. }
|
||||
| NotificationBody::SharedInstanceInvite { .. }
|
||||
| NotificationBody::ModeratorMessage { .. }
|
||||
| NotificationBody::LegacyMarkdown { .. }
|
||||
| NotificationBody::Unknown => Ok(EmailTemplate::Static(map)),
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
use chrono::Utc;
|
||||
use eyre::{Result, eyre};
|
||||
use hex::ToHex;
|
||||
use serde::Serialize;
|
||||
use sha1::Digest;
|
||||
use tokio::task::{spawn, spawn_blocking};
|
||||
use tracing::{Instrument, info, info_span, warn};
|
||||
@@ -42,6 +43,16 @@ struct PendingFileScan {
|
||||
project_id: DBProjectId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, utoipa::ToSchema)]
|
||||
pub struct FileScanSummary {
|
||||
/// Number of attribution groups newly created by the scan.
|
||||
pub new_attribution_groups: u64,
|
||||
/// Number of attribution files newly created by the scan.
|
||||
pub new_attribution_files: u64,
|
||||
/// Override file paths found and scanned in the file archive.
|
||||
pub scanned_file_names: Vec<String>,
|
||||
}
|
||||
|
||||
/// Attribution enforcement is version/project-scoped, not file-hash-scoped.
|
||||
///
|
||||
/// Versions or projects listed in `attributions_exemptions` predate this
|
||||
@@ -277,7 +288,7 @@ pub async fn scan_file(
|
||||
project_id: DBProjectId,
|
||||
file_id: DBFileId,
|
||||
file_url: &str,
|
||||
) -> Result<()> {
|
||||
) -> Result<FileScanSummary> {
|
||||
let result =
|
||||
scan_file_inner(txn, redis, file_host, project_id, file_id, file_url)
|
||||
.await;
|
||||
@@ -296,7 +307,7 @@ async fn scan_file_inner(
|
||||
project_id: DBProjectId,
|
||||
file_id: DBFileId,
|
||||
file_url: &str,
|
||||
) -> Result<()> {
|
||||
) -> Result<FileScanSummary> {
|
||||
let overrides =
|
||||
extract_override_files_from_storage(file_host, file_id, file_url)
|
||||
.await
|
||||
@@ -304,7 +315,16 @@ async fn scan_file_inner(
|
||||
eyre!("extracting overrides for file {file_id:?}")
|
||||
})?;
|
||||
|
||||
let scanned_file_names =
|
||||
overrides.iter().map(|file| file.path.clone()).collect();
|
||||
let mut summary = FileScanSummary {
|
||||
scanned_file_names,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if !overrides.is_empty() {
|
||||
let before = count_project_attributions(project_id, txn).await?;
|
||||
|
||||
let resolved = resolve_overrides(&overrides, redis, txn)
|
||||
.await
|
||||
.wrap_err_with(|| {
|
||||
@@ -318,15 +338,58 @@ async fn scan_file_inner(
|
||||
.wrap_err_with(|| {
|
||||
eyre!("persisting attribution results for file {file_id:?}")
|
||||
})?;
|
||||
|
||||
let after = count_project_attributions(project_id, txn).await?;
|
||||
summary.new_attribution_groups =
|
||||
after.groups.saturating_sub(before.groups);
|
||||
summary.new_attribution_files =
|
||||
after.files.saturating_sub(before.files);
|
||||
|
||||
log_marked_override_projects(&resolved);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn file_scan_result(result: &Result<()>) -> FileScanResult<'static> {
|
||||
struct ProjectAttributionCounts {
|
||||
groups: u64,
|
||||
files: u64,
|
||||
}
|
||||
|
||||
async fn count_project_attributions(
|
||||
project_id: DBProjectId,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<ProjectAttributionCounts> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
select
|
||||
(
|
||||
select count(*)
|
||||
from project_attribution_groups
|
||||
where project_id = $1
|
||||
) as "groups!",
|
||||
(
|
||||
select count(*)
|
||||
from project_attribution_files paf
|
||||
inner join project_attribution_groups pag on pag.id = paf.group_id
|
||||
where pag.project_id = $1
|
||||
) as "files!"
|
||||
"#,
|
||||
project_id as DBProjectId,
|
||||
)
|
||||
.fetch_one(&mut *txn)
|
||||
.await
|
||||
.wrap_err("counting project attributions")?;
|
||||
|
||||
Ok(ProjectAttributionCounts {
|
||||
groups: row.groups as u64,
|
||||
files: row.files as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn file_scan_result<T>(result: &Result<T>) -> FileScanResult<'static> {
|
||||
match result {
|
||||
Ok(()) => Ok(()),
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(ApiError {
|
||||
error: "internal_error",
|
||||
description: format!("{err:#}"),
|
||||
@@ -480,6 +543,7 @@ const OVERRIDE_PREFIXES: &[&str] = &[
|
||||
];
|
||||
|
||||
fn should_scan(name: &str) -> bool {
|
||||
let name = name.to_lowercase();
|
||||
let should_skip = name.starts_with("mods/.connector/")
|
||||
|| name.starts_with(".sable/natives/")
|
||||
|| name.starts_with("local/crash_assistant/")
|
||||
@@ -491,8 +555,10 @@ fn should_scan(name: &str) -> bool {
|
||||
|| name.starts_with("essential/")
|
||||
|| name.ends_with(".rpo")
|
||||
|| name.ends_with(".txt");
|
||||
let is_archive = name.contains(".jar") || name.contains(".zip");
|
||||
|
||||
let is_archive = name.ends_with(".jar")
|
||||
|| name.ends_with(".zip")
|
||||
|| name.ends_with(".jar.disabled")
|
||||
|| name.ends_with(".zip.disabled");
|
||||
is_archive && !should_skip
|
||||
}
|
||||
|
||||
|
||||
@@ -1,456 +1,5 @@
|
||||
use crate::database;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::env::ENV;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::projects::ProjectStatus;
|
||||
use crate::models::threads::MessageBody;
|
||||
use crate::routes::ApiError;
|
||||
use dashmap::DashSet;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
pub const AUTOMOD_ID: i64 = 0;
|
||||
|
||||
pub struct ModerationMessages {
|
||||
pub messages: Vec<ModerationMessage>,
|
||||
pub version_specific: HashMap<String, Vec<ModerationMessage>>,
|
||||
}
|
||||
|
||||
impl ModerationMessages {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.messages.is_empty() && self.version_specific.is_empty()
|
||||
}
|
||||
|
||||
pub fn markdown(&self, auto_mod: bool) -> String {
|
||||
let mut str = String::new();
|
||||
|
||||
for message in &self.messages {
|
||||
write!(&mut str, "## {}\n{}\n\n", message.header(), message.body())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for (version_num, messages) in &self.version_specific {
|
||||
for message in messages {
|
||||
write!(
|
||||
&mut str,
|
||||
"### Version {}: {}\n{}\n\n",
|
||||
version_num,
|
||||
message.header(),
|
||||
message.body()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if auto_mod {
|
||||
str.push_str(
|
||||
"<hr />\n\n\
|
||||
🤖 This is an automated message generated by AutoMod (BETA). If you are facing issues, please [contact support](https://support.modrinth.com)."
|
||||
);
|
||||
}
|
||||
|
||||
str
|
||||
}
|
||||
|
||||
pub fn should_reject(&self, first_time: bool) -> bool {
|
||||
self.messages.iter().any(|x| x.rejectable(first_time))
|
||||
|| self
|
||||
.version_specific
|
||||
.values()
|
||||
.any(|x| x.iter().any(|x| x.rejectable(first_time)))
|
||||
}
|
||||
|
||||
pub fn approvable(&self) -> bool {
|
||||
self.messages.iter().all(|x| x.approvable())
|
||||
&& self
|
||||
.version_specific
|
||||
.values()
|
||||
.all(|x| x.iter().all(|x| x.approvable()))
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ModerationMessage {
|
||||
MissingGalleryImage,
|
||||
NoPrimaryFile,
|
||||
NoSideTypes,
|
||||
PackFilesNotAllowed {
|
||||
files: HashMap<String, IdentifiedFile>,
|
||||
incomplete: bool,
|
||||
},
|
||||
MissingLicense,
|
||||
MissingCustomLicenseUrl {
|
||||
license: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl ModerationMessage {
|
||||
pub fn rejectable(&self, first_time: bool) -> bool {
|
||||
match self {
|
||||
ModerationMessage::NoPrimaryFile => true,
|
||||
ModerationMessage::PackFilesNotAllowed { files, incomplete } => {
|
||||
(!incomplete || first_time)
|
||||
&& files.values().any(|x| match x.status {
|
||||
ApprovalType::Yes => false,
|
||||
ApprovalType::WithAttributionAndSource => false,
|
||||
ApprovalType::WithAttribution => false,
|
||||
ApprovalType::No => first_time,
|
||||
ApprovalType::PermanentNo => true,
|
||||
ApprovalType::Unidentified => first_time,
|
||||
})
|
||||
}
|
||||
ModerationMessage::MissingGalleryImage => true,
|
||||
ModerationMessage::MissingLicense => true,
|
||||
ModerationMessage::MissingCustomLicenseUrl { .. } => true,
|
||||
ModerationMessage::NoSideTypes => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn approvable(&self) -> bool {
|
||||
match self {
|
||||
ModerationMessage::NoPrimaryFile => false,
|
||||
ModerationMessage::PackFilesNotAllowed { files, .. } => {
|
||||
files.values().all(|x| x.status.approved())
|
||||
}
|
||||
ModerationMessage::MissingGalleryImage => false,
|
||||
ModerationMessage::MissingLicense => false,
|
||||
ModerationMessage::MissingCustomLicenseUrl { .. } => false,
|
||||
ModerationMessage::NoSideTypes => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &'static str {
|
||||
match self {
|
||||
ModerationMessage::NoPrimaryFile => "No primary files",
|
||||
ModerationMessage::PackFilesNotAllowed { .. } => {
|
||||
"Copyrighted Content"
|
||||
}
|
||||
ModerationMessage::MissingGalleryImage => "Missing Gallery Images",
|
||||
ModerationMessage::MissingLicense => "Missing License",
|
||||
ModerationMessage::MissingCustomLicenseUrl { .. } => {
|
||||
"Missing License URL"
|
||||
}
|
||||
ModerationMessage::NoSideTypes => "Missing Environment Information",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn body(&self) -> String {
|
||||
match self {
|
||||
ModerationMessage::NoPrimaryFile => "Please attach a file to this version. All files on Modrinth must have files associated with their versions.\n".to_string(),
|
||||
ModerationMessage::PackFilesNotAllowed { files, .. } => {
|
||||
let mut str = String::from("This pack redistributes copyrighted material. Please refer to [Modrinth's guide on obtaining modpack permissions](https://support.modrinth.com/en/articles/8797527-obtaining-modpack-permissions) for more information.\n\n");
|
||||
|
||||
let mut attribute_mods = Vec::new();
|
||||
let mut no_mods = Vec::new();
|
||||
let mut permanent_no_mods = Vec::new();
|
||||
let mut unidentified_mods = Vec::new();
|
||||
for approval in files.values() {
|
||||
match approval.status {
|
||||
ApprovalType::Yes | ApprovalType::WithAttributionAndSource => {}
|
||||
ApprovalType::WithAttribution => attribute_mods.push(&approval.file_name),
|
||||
ApprovalType::No => no_mods.push(&approval.file_name),
|
||||
ApprovalType::PermanentNo => permanent_no_mods.push(&approval.file_name),
|
||||
ApprovalType::Unidentified => unidentified_mods.push(&approval.file_name),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_mods(projects: Vec<&String>, headline: &str, val: &mut String) {
|
||||
if projects.is_empty() { return }
|
||||
|
||||
write!(val, "{headline}\n\n").unwrap();
|
||||
|
||||
for project in &projects {
|
||||
let additional_text = if project.contains("ftb-quests") {
|
||||
Some(("Odyssey Quests", "lo90fZoB"))
|
||||
} else if project.contains("ftb-ranks") || project.contains("ftb-essentials") {
|
||||
Some(("Odyssey Roles", "iYcNKH7W"))
|
||||
} else if project.contains("ftb-teams") {
|
||||
Some(("Odyssey Guilds", "bb2EpKpx"))
|
||||
} else if project.contains("ftb-chunks") {
|
||||
Some(("Odyssey Claims", "fEWKxVzh"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(additional_text) = additional_text {
|
||||
writeln!(val, "- {project} (consider using [{}](https://modrinth.com/project/{}) instead)", additional_text.0, additional_text.1).unwrap();
|
||||
} else {
|
||||
writeln!(val, "- {project}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if !projects.is_empty() {
|
||||
val.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
print_mods(attribute_mods, "The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your modpack description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):", &mut str);
|
||||
print_mods(no_mods, "The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:", &mut str);
|
||||
print_mods(permanent_no_mods, "The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:", &mut str);
|
||||
print_mods(unidentified_mods, "The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:", &mut str);
|
||||
|
||||
str
|
||||
},
|
||||
ModerationMessage::MissingGalleryImage => "We ask that resource packs like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of the content in your pack per section 2.1 of [Modrinth's content rules](https://modrinth.com/legal/rules#general-expectations).\n
|
||||
Keep in mind that you should:\n
|
||||
- Set a featured image that best represents your pack.
|
||||
- Ensure all your images have titles that accurately label the image, and optionally, details on the contents of the image in the images Description.
|
||||
- Upload any relevant images in your Description to your Gallery tab for best results.".to_string(),
|
||||
ModerationMessage::MissingLicense => "You must select a License before your project can be published publicly, having a License associated with your project is important to protecting your rights and allowing others to use your content as you intend. For more information, you can see our [Guide to Licensing Mods](<https://modrinth.com/news/article/licensing-guide/>).".to_string(),
|
||||
ModerationMessage::MissingCustomLicenseUrl { license } => format!("It looks like you've selected the License \"{license}\" without providing a valid License link. When using a custom License you must provide a link directly to the License in the License Link field."),
|
||||
ModerationMessage::NoSideTypes => "Your project's side types are currently set to Unknown on both sides. Please set accurate side types!".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AutomatedModerationQueue {
|
||||
pub projects: DashSet<ProjectId>,
|
||||
}
|
||||
|
||||
impl Default for AutomatedModerationQueue {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
projects: DashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AutomatedModerationQueue {
|
||||
pub async fn task(
|
||||
&self,
|
||||
pool: PgPool,
|
||||
ro_pool: ReadOnlyPgPool,
|
||||
redis: RedisPool,
|
||||
) {
|
||||
loop {
|
||||
let projects = self.projects.clone();
|
||||
self.projects.clear();
|
||||
|
||||
for project in projects {
|
||||
async {
|
||||
let project =
|
||||
database::DBProject::get_id((project).into(), &*ro_pool, &redis)
|
||||
.await?;
|
||||
|
||||
if let Some(project) = project {
|
||||
let res = async {
|
||||
let mut mod_messages = ModerationMessages {
|
||||
messages: vec![],
|
||||
version_specific: HashMap::new(),
|
||||
};
|
||||
|
||||
if project.project_types.iter().any(|x| ["mod", "modpack"].contains(&&**x)) && !project.aggregate_version_fields.iter().any(|x| x.field_name == "environment") {
|
||||
mod_messages.messages.push(ModerationMessage::NoSideTypes);
|
||||
}
|
||||
|
||||
if project.inner.components.minecraft_server.is_none() {
|
||||
let license = &project.inner.license;
|
||||
if license == "LicenseRef-Unknown" || license == "LicenseRef-" {
|
||||
mod_messages
|
||||
.messages
|
||||
.push(ModerationMessage::MissingLicense);
|
||||
} else if license.starts_with("LicenseRef-")
|
||||
&& license != "LicenseRef-All-Rights-Reserved"
|
||||
&& project.inner.license_url.is_none()
|
||||
{
|
||||
mod_messages.messages.push(
|
||||
ModerationMessage::MissingCustomLicenseUrl {
|
||||
license: project.inner.license.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (project.project_types.contains(&"resourcepack".to_string()) || project.project_types.contains(&"shader".to_string())) &&
|
||||
project.gallery_items.is_empty() &&
|
||||
!project.categories.contains(&"audio".to_string()) &&
|
||||
!project.categories.contains(&"locale".to_string())
|
||||
{
|
||||
mod_messages.messages.push(ModerationMessage::MissingGalleryImage);
|
||||
}
|
||||
|
||||
let versions =
|
||||
database::DBVersion::get_many(&project.versions, &*ro_pool, &redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
// we only support modpacks at this time
|
||||
.filter(|x| x.project_types.contains(&"modpack".to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for version in versions {
|
||||
let primary_file = version.files.iter().find_or_first(|x| x.primary);
|
||||
|
||||
if primary_file.is_none() {
|
||||
let val = mod_messages.version_specific.entry(version.inner.version_number).or_default();
|
||||
val.push(ModerationMessage::NoPrimaryFile);
|
||||
}
|
||||
}
|
||||
|
||||
if !mod_messages.is_empty() {
|
||||
let is_server_project =
|
||||
project.inner.components.minecraft_server.is_some();
|
||||
let first_time = database::models::DBThread::get(project.thread_id, &pool).await?
|
||||
.is_none_or(|x| x.messages.iter().all(|x| x.author_id == Some(database::models::DBUserId(AUTOMOD_ID)) || x.hide_identity));
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let id = ThreadMessageBuilder {
|
||||
author_id: Some(database::models::DBUserId(AUTOMOD_ID)),
|
||||
body: MessageBody::Text {
|
||||
body: mod_messages.markdown(true),
|
||||
private: is_server_project,
|
||||
replying_to: None,
|
||||
associated_images: vec![],
|
||||
},
|
||||
thread_id: project.thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await?;
|
||||
|
||||
let members = database::models::DBTeamMember::get_from_team_full(
|
||||
project.inner.team_id,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if mod_messages.should_reject(first_time) && !is_server_project {
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(database::models::DBUserId(AUTOMOD_ID)),
|
||||
body: MessageBody::StatusChange {
|
||||
new_status: ProjectStatus::Rejected,
|
||||
old_status: project.inner.status,
|
||||
},
|
||||
thread_id: project.thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::StatusChange {
|
||||
project_id: project.inner.id.into(),
|
||||
old_status: project.inner.status,
|
||||
new_status: ProjectStatus::Rejected,
|
||||
},
|
||||
}
|
||||
.insert_many(members.into_iter().map(|x| x.user_id).collect(), &mut transaction, &redis)
|
||||
.await?;
|
||||
|
||||
if !ENV.MODERATION_SLACK_WEBHOOK.is_empty() {
|
||||
crate::util::webhook::send_slack_project_webhook(
|
||||
project.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
&ENV.MODERATION_SLACK_WEBHOOK,
|
||||
Some(
|
||||
format!(
|
||||
"*<{}/user/AutoMod|AutoMod>* changed project status from *{}* to *Rejected*",
|
||||
ENV.SITE_URL,
|
||||
&project.inner.status.as_friendly_str(),
|
||||
)
|
||||
.to_string(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET status = 'rejected'
|
||||
WHERE id = $1
|
||||
",
|
||||
project.inner.id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
database::models::DBProject::clear_cache(
|
||||
project.inner.id,
|
||||
project.inner.slug.clone(),
|
||||
None,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::ModeratorMessage {
|
||||
thread_id: project.thread_id.into(),
|
||||
message_id: id.into(),
|
||||
project_id: Some(project.inner.id.into()),
|
||||
report_id: None,
|
||||
},
|
||||
}
|
||||
.insert_many(
|
||||
members.iter().map(|x| x.user_id).collect(),
|
||||
&mut transaction,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::ModerationMessageReceived {
|
||||
project_id: project.inner.id.into(),
|
||||
},
|
||||
}
|
||||
.insert_many(
|
||||
members.iter().map(|x| x.user_id).collect(),
|
||||
&mut transaction,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
}
|
||||
|
||||
Ok::<(), ApiError>(())
|
||||
}.await;
|
||||
|
||||
if let Err(err) = res {
|
||||
let err = err.as_api_error();
|
||||
|
||||
let str = format!(
|
||||
"## Internal AutoMod Error\n\n\
|
||||
Error code: {}\n\n\
|
||||
Error description: {}\n\n",
|
||||
err.error, err.description
|
||||
);
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(database::models::DBUserId(AUTOMOD_ID)),
|
||||
body: MessageBody::Text {
|
||||
body: str,
|
||||
private: true,
|
||||
replying_to: None,
|
||||
associated_images: vec![],
|
||||
},
|
||||
thread_id: project.thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), ApiError>(())
|
||||
}.await.ok();
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(5)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct MissingMetadata {
|
||||
|
||||
@@ -46,20 +46,26 @@ pub const FILTERED_HEADERS: &[&str] = &[
|
||||
"x-vercel-ip-country",
|
||||
];
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(page_view_ingest)
|
||||
.service(playtime_ingest)
|
||||
.service(minecraft_server_play_ingest);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct UrlInput {
|
||||
url: String,
|
||||
}
|
||||
|
||||
//this route should be behind the cloudflare WAF to prevent non-browsers from calling it
|
||||
#[post("view")]
|
||||
async fn page_view_ingest(
|
||||
#[utoipa::path(
|
||||
context_path = "/analytics",
|
||||
tag = "analytics",
|
||||
request_body = UrlInput,
|
||||
responses((status = NO_CONTENT))
|
||||
)]
|
||||
#[post("/view")]
|
||||
pub async fn page_view_ingest(
|
||||
req: HttpRequest,
|
||||
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
@@ -171,7 +177,7 @@ async fn page_view_ingest(
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, utoipa::ToSchema)]
|
||||
pub struct PlaytimeInput {
|
||||
seconds: u16,
|
||||
loader: String,
|
||||
@@ -179,8 +185,14 @@ pub struct PlaytimeInput {
|
||||
parent: Option<crate::models::ids::VersionId>,
|
||||
}
|
||||
|
||||
#[post("playtime")]
|
||||
async fn playtime_ingest(
|
||||
#[utoipa::path(
|
||||
context_path = "/analytics",
|
||||
tag = "analytics",
|
||||
request_body = serde_json::Value,
|
||||
responses((status = NO_CONTENT))
|
||||
)]
|
||||
#[post("/playtime")]
|
||||
pub async fn playtime_ingest(
|
||||
req: HttpRequest,
|
||||
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
@@ -249,7 +261,7 @@ struct MinecraftProfile {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct MinecraftJavaServerPlayInput {
|
||||
project_id: ProjectId,
|
||||
username: String,
|
||||
@@ -258,8 +270,14 @@ pub struct MinecraftJavaServerPlayInput {
|
||||
|
||||
pub const MINECRAFT_SERVER_PLAYS: &str = "minecraft_server_plays";
|
||||
|
||||
#[post("minecraft-server-play")]
|
||||
async fn minecraft_server_play_ingest(
|
||||
#[utoipa::path(
|
||||
context_path = "/analytics",
|
||||
tag = "analytics",
|
||||
request_body = MinecraftJavaServerPlayInput,
|
||||
responses((status = NO_CONTENT))
|
||||
)]
|
||||
#[post("/minecraft-server-play")]
|
||||
pub async fn minecraft_server_play_ingest(
|
||||
req: HttpRequest,
|
||||
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_web::web;
|
||||
use eyre::Context;
|
||||
use eyre::eyre;
|
||||
use prometheus::IntGauge;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use super::SecurityAddon;
|
||||
use crate::util::cors::default_cors;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod pprof;
|
||||
pub(crate) mod pprof;
|
||||
|
||||
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
|
||||
cfg.service(
|
||||
utoipa_actix_web::scope("/debug")
|
||||
.wrap(default_cors())
|
||||
.configure({
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
pprof::config
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
|_cfg| ()
|
||||
}
|
||||
}),
|
||||
);
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(
|
||||
paths(pprof::heap, pprof::flame_graph),
|
||||
modifiers(&DebugPathModifier, &SecurityAddon)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
struct DebugPathModifier;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl utoipa::Modify for DebugPathModifier {
|
||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||
super::prefix_openapi_paths(openapi, "/debug", |_| false);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(web::scope("/debug").wrap(default_cors()).configure({
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
pprof::config
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
|_cfg| ()
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn register_and_set_metrics(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user